From 3a995e31c71e1bc0cbcdb86cf2ae6cb56c21f176 Mon Sep 17 00:00:00 2001 From: Frank Sinapi Date: Thu, 9 Dec 2021 14:34:32 -0500 Subject: [PATCH] feat(thermocycler-refresh): Add Peltier closed-loop control (#230) * Added commands to set the Plate Temperature and deactivate the plate temperature control * Peltiers are controlled via PID if a temperature is currently set * Added tests for the commands * Added handling in the Plate Task for the PID settings message * Added tests for lid heater & plate to make sure outputs are being set in control loop * Fixed a bug where peltier outputs driving full power heating would do nothing, because the PWM ends up being 0 after inverting it * Added a script for basic temperature cycling for testing * Added error checking when updating peltier PID outputs --- .../thermocycler-refresh/gcodes.hpp | 90 +++++ .../thermocycler-refresh/host_comms_task.hpp | 66 +++- .../thermocycler-refresh/messages.hpp | 14 +- .../thermal_plate_task.hpp | 188 +++++++++- .../thermal/thermal_peltier_hardware.c | 2 +- .../scripts/test_plate_pid.py | 32 ++ .../scripts/test_utils.py | 24 ++ .../thermocycler-refresh/tests/CMakeLists.txt | 2 + .../tests/test_host_comms_task.cpp | 327 +++++++++++++++++- .../tests/test_lid_heater_task.cpp | 10 + .../thermocycler-refresh/tests/test_m104.cpp | 91 +++++ .../thermocycler-refresh/tests/test_m14.cpp | 58 ++++ .../tests/test_thermal_plate_task.cpp | 238 +++++++++++++ 13 files changed, 1129 insertions(+), 13 deletions(-) create mode 100644 stm32-modules/thermocycler-refresh/scripts/test_plate_pid.py create mode 100644 stm32-modules/thermocycler-refresh/tests/test_m104.cpp create mode 100644 stm32-modules/thermocycler-refresh/tests/test_m14.cpp diff --git a/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/gcodes.hpp b/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/gcodes.hpp index 7ce2b78cb..ecd2f1351 100644 --- a/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/gcodes.hpp +++ b/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/gcodes.hpp @@ -679,6 +679,96 @@ struct DeactivateLidHeating { } }; +struct SetPlateTemperature { + /** + * SetPlateTemperature uses M104. Parameters: + * - S - setpoint temperature + * - H - hold time (optional) + * + * M104 S44\n + */ + using ParseResult = std::optional; + static constexpr auto prefix = std::array{'M', '1', '0', '4', ' ', 'S'}; + static constexpr auto hold_prefix = std::array{' ', 'H'}; + static constexpr const char* response = "M104 OK\n"; + + // 0 seconds means infinite hold time + constexpr static double infinite_hold = 0.0F; + + double setpoint; + double hold_time; + + template + requires std::forward_iterator && + std::sized_sentinel_for + static auto write_response_into(InputIt buf, InLimit limit) -> InputIt { + return write_string_to_iterpair(buf, limit, response); + } + + template + requires std::forward_iterator && + std::sized_sentinel_for + static auto parse(const InputIt& input, Limit limit) + -> std::pair { + auto working = prefix_matches(input, limit, prefix); + if (working == input) { + return std::make_pair(ParseResult(), input); + } + // We are expecting a temperature setting + auto temperature = parse_value(working, limit); + if (!temperature.first.has_value()) { + return std::make_pair(ParseResult(), input); + } + auto temperature_val = temperature.first.value(); + + auto hold_val = infinite_hold; + working = prefix_matches(temperature.second, limit, hold_prefix); + if (working != temperature.second) { + // This command specified a hold temperature + auto hold = parse_value(working, limit); + if (!hold.first.has_value()) { + return std::make_pair(ParseResult(), input); + } + hold_val = hold.first.value(); + working = hold.second; + } + + return std::make_pair( + ParseResult(SetPlateTemperature{.setpoint = temperature_val, + .hold_time = hold_val}), + working); + } +}; + +struct DeactivatePlate { + /** + * DeactivatePlate uses M14. It has no parameters and just + * deactivates the plate peltiers + fan. + */ + using ParseResult = std::optional; + static constexpr auto prefix = std::array{'M', '1', '4'}; + static constexpr const char* response = "M14 OK\n"; + + template + requires std::forward_iterator && + std::sized_sentinel_for + static auto write_response_into(InputIt buf, InLimit limit) -> InputIt { + return write_string_to_iterpair(buf, limit, response); + } + + template + requires std::forward_iterator && + std::sized_sentinel_for + static auto parse(const InputIt& input, Limit limit) + -> std::pair { + auto working = prefix_matches(input, limit, prefix); + if (working == input) { + return std::make_pair(ParseResult(), input); + } + return std::make_pair(ParseResult(DeactivatePlate()), working); + } +}; + struct SetPIDConstants { /** * SetPIDConstants uses M301. It has three parameters, along with diff --git a/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/host_comms_task.hpp b/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/host_comms_task.hpp index a9ebcf452..f2f4afdbe 100644 --- a/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/host_comms_task.hpp +++ b/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/host_comms_task.hpp @@ -42,12 +42,14 @@ class HostCommsTask { gcode::GetLidTemperatureDebug, gcode::GetPlateTemperatureDebug, gcode::SetPeltierDebug, gcode::SetFanManual, gcode::SetHeaterDebug, gcode::GetPlateTemp, gcode::GetLidTemp, gcode::SetLidTemperature, - gcode::DeactivateLidHeating, gcode::SetPIDConstants>; + gcode::DeactivateLidHeating, gcode::SetPIDConstants, + gcode::SetPlateTemperature, gcode::DeactivatePlate>; using AckOnlyCache = AckCache<8, gcode::EnterBootloader, gcode::SetSerialNumber, gcode::SetPeltierDebug, gcode::SetFanManual, gcode::SetHeaterDebug, gcode::SetLidTemperature, - gcode::DeactivateLidHeating, gcode::SetPIDConstants>; + gcode::DeactivateLidHeating, gcode::SetPIDConstants, + gcode::SetPlateTemperature, gcode::DeactivatePlate>; using GetSystemInfoCache = AckCache<8, gcode::GetSystemInfo>; using GetLidTempDebugCache = AckCache<8, gcode::GetLidTemperatureDebug>; using GetPlateTempDebugCache = AckCache<8, gcode::GetPlateTemperatureDebug>; @@ -708,9 +710,63 @@ class HostCommsTask { .p = gcode.const_p, .i = gcode.const_i, .d = gcode.const_d}; - // TODO when PID is added to the peltiers and fans, will have to - // switch the target queue based on the selection in the message. - if (!task_registry->lid_heater->get_message_queue().try_send( + bool ret = false; + if (message.selection == PidSelection::HEATER) { + ret = task_registry->lid_heater->get_message_queue().try_send( + message, TICKS_TO_WAIT_ON_SEND); + } else { + ret = task_registry->thermal_plate->get_message_queue().try_send( + message, TICKS_TO_WAIT_ON_SEND); + } + if (!ret) { + auto wrote_to = errors::write_into( + tx_into, tx_limit, errors::ErrorCode::INTERNAL_QUEUE_FULL); + ack_only_cache.remove_if_present(id); + return std::make_pair(false, wrote_to); + } + + return std::make_pair(true, tx_into); + } + + template + requires std::forward_iterator && + std::sized_sentinel_for + auto visit_gcode(const gcode::SetPlateTemperature& gcode, InputIt tx_into, + InputLimit tx_limit) -> std::pair { + auto id = ack_only_cache.add(gcode); + if (id == 0) { + return std::make_pair( + false, errors::write_into(tx_into, tx_limit, + errors::ErrorCode::GCODE_CACHE_FULL)); + } + + auto message = messages::SetPlateTemperatureMessage{ + .id = id, .setpoint = gcode.setpoint, .hold_time = gcode.hold_time}; + if (!task_registry->thermal_plate->get_message_queue().try_send( + message, TICKS_TO_WAIT_ON_SEND)) { + auto wrote_to = errors::write_into( + tx_into, tx_limit, errors::ErrorCode::INTERNAL_QUEUE_FULL); + ack_only_cache.remove_if_present(id); + return std::make_pair(false, wrote_to); + } + + return std::make_pair(true, tx_into); + } + + template + requires std::forward_iterator && + std::sized_sentinel_for + auto visit_gcode(const gcode::DeactivatePlate& gcode, InputIt tx_into, + InputLimit tx_limit) -> std::pair { + auto id = ack_only_cache.add(gcode); + if (id == 0) { + return std::make_pair( + false, errors::write_into(tx_into, tx_limit, + errors::ErrorCode::GCODE_CACHE_FULL)); + } + + auto message = messages::DeactivatePlateMessage{.id = id}; + if (!task_registry->thermal_plate->get_message_queue().try_send( message, TICKS_TO_WAIT_ON_SEND)) { auto wrote_to = errors::write_into( tx_into, tx_limit, errors::ErrorCode::INTERNAL_QUEUE_FULL); diff --git a/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/messages.hpp b/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/messages.hpp index b9654a295..3861e9eff 100644 --- a/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/messages.hpp +++ b/stm32-modules/include/thermocycler-refresh/thermocycler-refresh/messages.hpp @@ -174,6 +174,16 @@ struct DeactivateLidHeatingMessage { uint32_t id; }; +struct SetPlateTemperatureMessage { + uint32_t id; + double setpoint; + double hold_time; +}; + +struct DeactivatePlateMessage { + uint32_t id; +}; + struct SetPIDConstantsMessage { uint32_t id; PidSelection selection; @@ -194,7 +204,9 @@ using HostCommsMessage = using ThermalPlateMessage = ::std::variant; + SetFanManualMessage, GetPlateTempMessage, + SetPlateTemperatureMessage, DeactivatePlateMessage, + SetPIDConstantsMessage>; using LidHeaterMessage = ::std::variant ThermalPlateTask& = delete; ThermalPlateTask(ThermalPlateTask&& other) noexcept = delete; @@ -220,6 +224,7 @@ class ThermalPlateTask { requires ThermalPlateExecutionPolicy auto visit_message(const messages::ThermalPlateTempReadComplete& msg, Policy& policy) -> void { + constexpr double thermistors_per_peltier = 2; auto old_error_bitmap = _state.error_bitmap; handle_temperature_conversion(msg.front_right, _thermistors[THERM_FRONT_RIGHT]); @@ -247,7 +252,35 @@ class ThermalPlateTask { } } - // TODO update outputs from PID if enabled + _peltier_left.temp_current = (_thermistors[THERM_FRONT_LEFT].temp_c + + _thermistors[THERM_BACK_LEFT].temp_c) / + thermistors_per_peltier; + _peltier_right.temp_current = (_thermistors[THERM_FRONT_RIGHT].temp_c + + _thermistors[THERM_BACK_RIGHT].temp_c) / + thermistors_per_peltier; + _peltier_center.temp_current = + (_thermistors[THERM_FRONT_CENTER].temp_c + + _thermistors[THERM_BACK_CENTER].temp_c) / + thermistors_per_peltier; + if (_state.system_status == State::CONTROLLING) { + policy.set_enabled(true); + // Each of the peltiers has its own PID loop, as does the fan + auto ret = update_peltier_pid(_peltier_left, policy); + if (ret) { + ret = update_peltier_pid(_peltier_right, policy); + } + if (ret) { + ret = update_peltier_pid(_peltier_center, policy); + } + if (!ret) { + _state.system_status = State::ERROR; + _state.error_bitmap |= State::PELTIER_ERROR; + } + } + // Not an `else` so we can immediately resolve any issue setting outputs + if (_state.system_status == State::ERROR) { + policy.set_enabled(false); + } } template @@ -345,6 +378,8 @@ class ThermalPlateTask { if (!ok) { response.with_error = errors::ErrorCode::THERMAL_PELTIER_ERROR; + _state.system_status = State::ERROR; + _state.error_bitmap |= State::PELTIER_ERROR; } static_cast( @@ -370,6 +405,123 @@ class ThermalPlateTask { _task_registry->comms->get_message_queue().try_send(response)); } + template + auto visit_message(const messages::SetPlateTemperatureMessage& msg, + Policy& policy) -> void { + auto response = + messages::AcknowledgePrevious{.responding_to_id = msg.id}; + if (_state.system_status == State::ERROR) { + response.with_error = most_relevant_error(); + static_cast( + _task_registry->comms->get_message_queue().try_send(response)); + return; + } + if (_state.system_status == State::PWM_TEST) { + // Reset all peltiers + auto ret = policy.set_peltier(_peltier_left.id, 0.0F, + PeltierDirection::PELTIER_HEATING); + if (ret) { + ret = policy.set_peltier(_peltier_right.id, 0.0F, + PeltierDirection::PELTIER_HEATING); + } + if (ret) { + ret = policy.set_peltier(_peltier_center.id, 0.0F, + PeltierDirection::PELTIER_HEATING); + } + if (!ret) { + policy.set_enabled(false); + response.with_error = errors::ErrorCode::THERMAL_PELTIER_ERROR; + _state.system_status = State::ERROR; + _state.error_bitmap |= State::PELTIER_ERROR; + static_cast( + _task_registry->comms->get_message_queue().try_send( + response)); + return; + } + } + + if (msg.setpoint <= 0.0F) { + _setpoint_c = 0.0F; + _state.system_status = State::IDLE; + policy.set_enabled(false); + } else { + _setpoint_c = msg.setpoint; + _state.system_status = State::CONTROLLING; + _peltier_left.pid.arm_integrator_reset(_setpoint_c - + _peltier_left.temp_current); + _peltier_right.pid.arm_integrator_reset( + _setpoint_c - _peltier_right.temp_current); + _peltier_center.pid.arm_integrator_reset( + _setpoint_c - _peltier_center.temp_current); + _peltier_left.temp_target = _setpoint_c; + _peltier_right.temp_target = _setpoint_c; + _peltier_center.temp_target = _setpoint_c; + _hold_time = msg.hold_time; + } + + static_cast( + _task_registry->comms->get_message_queue().try_send(response)); + } + + template + auto visit_message(const messages::DeactivatePlateMessage& msg, + Policy& policy) -> void { + auto response = + messages::AcknowledgePrevious{.responding_to_id = msg.id}; + + if (_state.system_status == State::ERROR) { + response.with_error = most_relevant_error(); + static_cast( + _task_registry->comms->get_message_queue().try_send(response)); + return; + } + + policy.set_enabled(false); + _state.system_status = State::IDLE; + + static_cast( + _task_registry->comms->get_message_queue().try_send(response)); + } + + template + auto visit_message(const messages::SetPIDConstantsMessage& msg, + Policy& policy) -> void { + static_cast(policy); + auto response = + messages::AcknowledgePrevious{.responding_to_id = msg.id}; + + if (_state.system_status == State::CONTROLLING) { + response.with_error = errors::ErrorCode::THERMAL_PLATE_BUSY; + static_cast( + _task_registry->comms->get_message_queue().try_send(response)); + return; + } + if ((msg.p < KP_MIN) || (msg.p > KP_MAX) || (msg.i < KI_MIN) || + (msg.i > KI_MAX) || (msg.d < KD_MIN) || (msg.d > KD_MAX)) { + response.with_error = + errors::ErrorCode::THERMAL_CONSTANT_OUT_OF_RANGE; + static_cast( + _task_registry->comms->get_message_queue().try_send(response)); + return; + } + + if (msg.selection == PidSelection::FANS) { + _fans_pid = + PID(msg.p, msg.i, msg.d, CONTROL_PERIOD_SECONDS, 1.0, -1.0); + } else { + // For now, all peltiers share the same PID values... + _peltier_right.pid = + PID(msg.p, msg.i, msg.d, CONTROL_PERIOD_SECONDS, 1.0, -1.0); + _peltier_left.pid = + PID(msg.p, msg.i, msg.d, CONTROL_PERIOD_SECONDS, 1.0, -1.0); + _peltier_center.pid = + PID(msg.p, msg.i, msg.d, CONTROL_PERIOD_SECONDS, 1.0, -1.0); + } + + static_cast( + _task_registry->comms->get_message_queue().try_send(response)); + } + auto handle_temperature_conversion(uint16_t conversion_result, Thermistor& thermistor) -> void { auto visitor = [this, &thermistor](const auto value) -> void { @@ -424,6 +576,10 @@ class ThermalPlateTask { // separately, but we also sometimes want to respond with just one error // condition that sums everything up. This method is used by code that // wants the single most relevant code for the current error condition. + if ((_state.error_bitmap & State::PELTIER_ERROR) == + State::PELTIER_ERROR) { + return errors::ErrorCode::THERMAL_PELTIER_ERROR; + } for (auto therm : _thermistors) { if ((_state.error_bitmap & therm.error_bit) == therm.error_bit) { return therm.error; @@ -442,6 +598,32 @@ class ThermalPlateTask { ((double)(PLATE_THERM_COUNT - 1)); } + /** + * @brief Updates the power of a peltier. Calculates a new PID value and + * updates the power output. The \c temp_current field in the peltier + * must be updated before invoking this function. + * + * @tparam Policy Provides platform-specific control mechanisms + * @param[in] peltier The peltier to update + * @param[in] policy Instance of the platform policy + * @return True on success, false if an error occurs + */ + template + auto update_peltier_pid(Peltier& peltier, Policy& policy) -> bool { + auto power = + peltier.pid.compute(peltier.temp_target - peltier.temp_current); + auto direction = PeltierDirection::PELTIER_HEATING; + if (power < 0.0F) { + // The set_peltier function takes a *positive* percentage and a + // direction + power = std::abs(power); + direction = PeltierDirection::PELTIER_COOLING; + } + return policy.set_peltier(peltier.id, + std::clamp(power, (double)0.0F, (double)1.0F), + direction); + } + Queue& _message_queue; tasks::Tasks* _task_registry; Peltier _peltier_left; @@ -451,6 +633,8 @@ class ThermalPlateTask { thermistor_conversion::Conversion _converter; State _state; double _setpoint_c; + PID _fans_pid; + double _hold_time; }; } // namespace thermal_plate_task diff --git a/stm32-modules/thermocycler-refresh/firmware/thermal/thermal_peltier_hardware.c b/stm32-modules/thermocycler-refresh/firmware/thermal/thermal_peltier_hardware.c index b943aaf0c..433e9d509 100644 --- a/stm32-modules/thermocycler-refresh/firmware/thermal/thermal_peltier_hardware.c +++ b/stm32-modules/thermocycler-refresh/firmware/thermal/thermal_peltier_hardware.c @@ -112,7 +112,7 @@ static void _update_outputs(Peltier_t *peltier) { dir_val = GPIO_PIN_SET; pwm = MAX_PWM - pwm; } - if(pwm > 0) { + if(peltier->power > 0) { HAL_GPIO_WritePin(peltier->direction_port, peltier->direction_pin, dir_val); diff --git a/stm32-modules/thermocycler-refresh/scripts/test_plate_pid.py b/stm32-modules/thermocycler-refresh/scripts/test_plate_pid.py new file mode 100644 index 000000000..c060459ed --- /dev/null +++ b/stm32-modules/thermocycler-refresh/scripts/test_plate_pid.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +import test_utils +import plot_temp + +HEAT_TARGET = 50 +COOL_TARGET = 10 + +if __name__ == '__main__': + ser = test_utils.build_serial() + direction = int(1) + + def update_cb(lid, heatsink, right, left, center): + global direction + global ser + if(direction > 0): + if(center > HEAT_TARGET): + test_utils.set_plate_temperature(temperature=COOL_TARGET, ser=ser) + direction = -1 + else: + if(center < COOL_TARGET): + test_utils.set_plate_temperature(temperature=HEAT_TARGET, ser=ser) + direction = 1 + + + test_utils.set_peltier_pid(0.97, 0.102, 1.901, ser) + test_utils.set_fans_manual(0.35, ser) + test_utils.set_plate_temperature(temperature = HEAT_TARGET, ser=ser) + plot_temp.graphTemperatures(ser, update_cb) + + test_utils.deactivate_plate(ser) + test_utils.set_fans_manual(0, ser) diff --git a/stm32-modules/thermocycler-refresh/scripts/test_utils.py b/stm32-modules/thermocycler-refresh/scripts/test_utils.py index 230c3dac5..657a438c5 100644 --- a/stm32-modules/thermocycler-refresh/scripts/test_utils.py +++ b/stm32-modules/thermocycler-refresh/scripts/test_utils.py @@ -134,3 +134,27 @@ def set_heater_pid(p: float, i: float, d: float, ser: serial.Serial): res = ser.readline() guard_error(res, b'M301 OK') print(res) + +# Sets the plate target as a temperature in celsius +def set_plate_temperature(temperature: float, ser: serial.Serial): + print(f'Setting plate temperature target to {temperature}C') + ser.write(f'M104 S{temperature}\n'.encode()) + res = ser.readline() + guard_error(res, b'M104 OK') + print(res) + +# Turn off the plate! +def deactivate_plate(ser: serial.Serial): + print('Deactivating plate') + ser.write('M14\n'.encode()) + res = ser.readline() + guard_error(res, b'M14 OK') + print(res) + +# Set the peltier PID constants +def set_peltier_pid(p: float, i: float, d: float, ser: serial.Serial): + print(f'Setting peltier PID to P={p} I={i} D={d}') + ser.write(f'M301 SP P{p} I{i} D{d}\n'.encode()) + res = ser.readline() + guard_error(res, b'M301 OK') + print(res) diff --git a/stm32-modules/thermocycler-refresh/tests/CMakeLists.txt b/stm32-modules/thermocycler-refresh/tests/CMakeLists.txt index a96af74ff..968dc2681 100644 --- a/stm32-modules/thermocycler-refresh/tests/CMakeLists.txt +++ b/stm32-modules/thermocycler-refresh/tests/CMakeLists.txt @@ -13,6 +13,8 @@ add_executable(${TARGET_MODULE_NAME} test_system_task.cpp test_thermal_plate_task.cpp # GCode parse tests + test_m14.cpp + test_m104.cpp test_m105.cpp test_m105d.cpp test_m141d.cpp diff --git a/stm32-modules/thermocycler-refresh/tests/test_host_comms_task.cpp b/stm32-modules/thermocycler-refresh/tests/test_host_comms_task.cpp index c55d8b4b0..8e5e7f71c 100644 --- a/stm32-modules/thermocycler-refresh/tests/test_host_comms_task.cpp +++ b/stm32-modules/thermocycler-refresh/tests/test_host_comms_task.cpp @@ -257,7 +257,7 @@ SCENARIO("message passing for ack-only gcodes from usb input") { tasks->get_lid_heater_queue().backing_deque.front(); auto set_lid_temp_message = std::get(lid_message); - tasks->get_system_queue().backing_deque.pop_front(); + tasks->get_lid_heater_queue().backing_deque.pop_front(); constexpr double test_temp = 101.0F; REQUIRE(set_lid_temp_message.setpoint == test_temp); REQUIRE(written_firstpass == tx_buf.begin()); @@ -336,7 +336,7 @@ SCENARIO("message passing for ack-only gcodes from usb input") { auto deactivate_lid_message = std::get( lid_message); - tasks->get_system_queue().backing_deque.pop_front(); + tasks->get_lid_heater_queue().backing_deque.pop_front(); REQUIRE(written_firstpass == tx_buf.begin()); REQUIRE(tasks->get_host_comms_queue().backing_deque.empty()); AND_WHEN("sending a good response back to the comms task") { @@ -395,7 +395,164 @@ SCENARIO("message passing for ack-only gcodes from usb input") { } } } - WHEN("sending a SetLidTemperature message") { + WHEN("sending a SetPlateTemperature message") { + std::string message_text = std::string("M104 S95.0 H40\n"); + auto message_obj = + messages::HostCommsMessage(messages::IncomingMessageFromHost( + &*message_text.begin(), &*message_text.end())); + tasks->get_host_comms_queue().backing_deque.push_back(message_obj); + auto written_firstpass = tasks->get_host_comms_task().run_once( + tx_buf.begin(), tx_buf.end()); + THEN( + "the task should pass the message on to the lid heater task " + "and not immediately ack") { + REQUIRE(tasks->get_thermal_plate_queue().backing_deque.size() != + 0); + auto lid_message = + tasks->get_thermal_plate_queue().backing_deque.front(); + auto set_plate_temp_message = + std::get(lid_message); + tasks->get_thermal_plate_queue().backing_deque.pop_front(); + constexpr double test_temp = 95.0F; + constexpr double test_hold = 40.0F; + REQUIRE(set_plate_temp_message.setpoint == test_temp); + REQUIRE(set_plate_temp_message.hold_time == test_hold); + REQUIRE(written_firstpass == tx_buf.begin()); + REQUIRE(tasks->get_host_comms_queue().backing_deque.empty()); + AND_WHEN("sending a good response back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = set_plate_temp_message.id}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN("the task should ack the previous message") { + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("M104 OK\n")); + REQUIRE(written_secondpass != tx_buf.begin()); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + } + } + AND_WHEN("sending a bad response back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = set_plate_temp_message.id + 1}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN( + "the task should pull the message and print an error") { + REQUIRE(written_secondpass > tx_buf.begin()); + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("ERR005")); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + } + } + AND_WHEN("sending an ack with error back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = set_plate_temp_message.id, + .with_error = + errors::ErrorCode::THERMAL_HEATER_ERROR}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN("the task should print the error rather than ack") { + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("ERR405:")); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + REQUIRE(written_secondpass != tx_buf.begin()); + } + } + } + } + WHEN("sending a DeactivatePlate message") { + std::string message_text = std::string("M14\n"); + auto message_obj = + messages::HostCommsMessage(messages::IncomingMessageFromHost( + &*message_text.begin(), &*message_text.end())); + tasks->get_host_comms_queue().backing_deque.push_back(message_obj); + auto written_firstpass = tasks->get_host_comms_task().run_once( + tx_buf.begin(), tx_buf.end()); + THEN( + "the task should pass the message on to the lid heater task " + "and not immediately ack") { + REQUIRE(tasks->get_thermal_plate_queue().backing_deque.size() != + 0); + auto plate_message = + tasks->get_thermal_plate_queue().backing_deque.front(); + auto deactivate_plate_message = + std::get(plate_message); + tasks->get_thermal_plate_queue().backing_deque.pop_front(); + REQUIRE(written_firstpass == tx_buf.begin()); + REQUIRE(tasks->get_host_comms_queue().backing_deque.empty()); + AND_WHEN("sending a good response back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = deactivate_plate_message.id}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN("the task should ack the previous message") { + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("M14 OK\n")); + REQUIRE(written_secondpass != tx_buf.begin()); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + } + } + AND_WHEN("sending a bad response back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = + deactivate_plate_message.id + 1}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN( + "the task should pull the message and print an error") { + REQUIRE(written_secondpass > tx_buf.begin()); + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("ERR005")); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + } + } + AND_WHEN("sending an ack with error back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = deactivate_plate_message.id, + .with_error = + errors::ErrorCode::THERMAL_PELTIER_ERROR}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN("the task should print the error rather than ack") { + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("ERR402:")); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + REQUIRE(written_secondpass != tx_buf.begin()); + } + } + } + } + WHEN("sending a SetPIDConstants message for the heaters") { std::string message_text = std::string("M301 SH P1 I1 D1\n"); auto message_obj = messages::HostCommsMessage(messages::IncomingMessageFromHost( @@ -412,7 +569,8 @@ SCENARIO("message passing for ack-only gcodes from usb input") { tasks->get_lid_heater_queue().backing_deque.front(); auto set_lid_temp_message = std::get(lid_message); - tasks->get_system_queue().backing_deque.pop_front(); + tasks->get_lid_heater_queue().backing_deque.pop_front(); + REQUIRE(set_lid_temp_message.selection == PidSelection::HEATER); REQUIRE(set_lid_temp_message.p == 1.0); REQUIRE(set_lid_temp_message.i == 1.0); REQUIRE(set_lid_temp_message.d == 1.0); @@ -473,6 +631,167 @@ SCENARIO("message passing for ack-only gcodes from usb input") { } } } + WHEN("sending a SetPIDConstants message for the peltiers") { + std::string message_text = std::string("M301 SP P1 I1 D1\n"); + auto message_obj = + messages::HostCommsMessage(messages::IncomingMessageFromHost( + &*message_text.begin(), &*message_text.end())); + tasks->get_host_comms_queue().backing_deque.push_back(message_obj); + auto written_firstpass = tasks->get_host_comms_task().run_once( + tx_buf.begin(), tx_buf.end()); + THEN( + "the task should pass the message on to the thermal plate task " + "and not immediately ack") { + REQUIRE(tasks->get_thermal_plate_queue().backing_deque.size() != + 0); + auto plate_message = + tasks->get_thermal_plate_queue().backing_deque.front(); + auto set_plate_pid_message = + std::get(plate_message); + tasks->get_thermal_plate_queue().backing_deque.pop_front(); + REQUIRE(set_plate_pid_message.selection == + PidSelection::PELTIERS); + REQUIRE(set_plate_pid_message.p == 1.0); + REQUIRE(set_plate_pid_message.i == 1.0); + REQUIRE(set_plate_pid_message.d == 1.0); + REQUIRE(written_firstpass == tx_buf.begin()); + REQUIRE(tasks->get_host_comms_queue().backing_deque.empty()); + AND_WHEN("sending a good response back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = set_plate_pid_message.id}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN("the task should ack the previous message") { + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("M301 OK\n")); + REQUIRE(written_secondpass != tx_buf.begin()); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + } + } + AND_WHEN("sending a bad response back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = set_plate_pid_message.id + 1}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN( + "the task should pull the message and print an error") { + REQUIRE(written_secondpass > tx_buf.begin()); + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("ERR005")); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + } + } + AND_WHEN("sending an ack with error back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = set_plate_pid_message.id, + .with_error = + errors::ErrorCode::THERMAL_PLATE_BUSY}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN("the task should print the error rather than ack") { + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("ERR401:")); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + REQUIRE(written_secondpass != tx_buf.begin()); + } + } + } + } + WHEN("sending a SetPIDConstants message for the fans") { + std::string message_text = std::string("M301 SF P1 I1 D1\n"); + auto message_obj = + messages::HostCommsMessage(messages::IncomingMessageFromHost( + &*message_text.begin(), &*message_text.end())); + tasks->get_host_comms_queue().backing_deque.push_back(message_obj); + auto written_firstpass = tasks->get_host_comms_task().run_once( + tx_buf.begin(), tx_buf.end()); + THEN( + "the task should pass the message on to the thermal plate task " + "and not immediately ack") { + REQUIRE(tasks->get_thermal_plate_queue().backing_deque.size() != + 0); + auto plate_message = + tasks->get_thermal_plate_queue().backing_deque.front(); + auto set_plate_pid_message = + std::get(plate_message); + tasks->get_thermal_plate_queue().backing_deque.pop_front(); + REQUIRE(set_plate_pid_message.selection == PidSelection::FANS); + REQUIRE(set_plate_pid_message.p == 1.0); + REQUIRE(set_plate_pid_message.i == 1.0); + REQUIRE(set_plate_pid_message.d == 1.0); + REQUIRE(written_firstpass == tx_buf.begin()); + REQUIRE(tasks->get_host_comms_queue().backing_deque.empty()); + AND_WHEN("sending a good response back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = set_plate_pid_message.id}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN("the task should ack the previous message") { + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("M301 OK\n")); + REQUIRE(written_secondpass != tx_buf.begin()); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + } + } + AND_WHEN("sending a bad response back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = set_plate_pid_message.id + 1}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN( + "the task should pull the message and print an error") { + REQUIRE(written_secondpass > tx_buf.begin()); + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("ERR005")); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + } + } + AND_WHEN("sending an ack with error back to the comms task") { + auto response = messages::HostCommsMessage( + messages::AcknowledgePrevious{ + .responding_to_id = set_plate_pid_message.id, + .with_error = + errors::ErrorCode::THERMAL_PLATE_BUSY}); + tasks->get_host_comms_queue().backing_deque.push_back( + response); + auto written_secondpass = + tasks->get_host_comms_task().run_once(tx_buf.begin(), + tx_buf.end()); + THEN("the task should print the error rather than ack") { + REQUIRE_THAT(tx_buf, + Catch::Matchers::StartsWith("ERR401:")); + REQUIRE(tasks->get_host_comms_queue() + .backing_deque.empty()); + REQUIRE(written_secondpass != tx_buf.begin()); + } + } + } + } } } diff --git a/stm32-modules/thermocycler-refresh/tests/test_lid_heater_task.cpp b/stm32-modules/thermocycler-refresh/tests/test_lid_heater_task.cpp index 868f59ff3..d6d7352c7 100644 --- a/stm32-modules/thermocycler-refresh/tests/test_lid_heater_task.cpp +++ b/stm32-modules/thermocycler-refresh/tests/test_lid_heater_task.cpp @@ -203,6 +203,16 @@ SCENARIO("lid heater task message passing") { } } } + AND_WHEN("sending updated temperatures below target") { + tasks->get_lid_heater_queue().backing_deque.push_back( + messages::LidHeaterMessage(read_message)); + tasks->run_lid_heater_task(); + THEN("the peltiers should be enabled") { + auto power = + tasks->get_lid_heater_policy().get_heater_power(); + REQUIRE(power > 0.0F); + } + } } AND_WHEN("sending a DeactivateLidHeating command") { tasks->get_host_comms_queue().backing_deque.pop_front(); diff --git a/stm32-modules/thermocycler-refresh/tests/test_m104.cpp b/stm32-modules/thermocycler-refresh/tests/test_m104.cpp new file mode 100644 index 000000000..2b55a87af --- /dev/null +++ b/stm32-modules/thermocycler-refresh/tests/test_m104.cpp @@ -0,0 +1,91 @@ +#include "catch2/catch.hpp" +#include "thermocycler-refresh/gcodes.hpp" + +SCENARIO("SetPlateTemperature (M104) parser works", "[gcode][parse][m104]") { + GIVEN("a response buffer large enough for the formatted response") { + std::string buffer(64, 'c'); + WHEN("filling response") { + auto written = gcode::SetPlateTemperature::write_response_into( + buffer.begin(), buffer.end()); + THEN("the response should be written in full") { + REQUIRE_THAT(buffer, Catch::Matchers::StartsWith("M104 OK\n")); + REQUIRE(written != buffer.begin()); + } + } + } + + GIVEN("a response buffer not large enough for the formatted response") { + std::string buffer(16, 'c'); + WHEN("filling response") { + auto written = gcode::SetPlateTemperature::write_response_into( + buffer.begin(), buffer.begin() + 6); + THEN("the response should write only up to the available space") { + std::string response = "M104 Occcccccccc"; + REQUIRE_THAT(buffer, Catch::Matchers::Equals(response)); + REQUIRE(written != buffer.begin()); + } + } + } + GIVEN("valid parameters") { + WHEN("Setting target to 95C") { + std::string buffer = "M104 S95\n"; + auto parsed = + gcode::SetPlateTemperature::parse(buffer.begin(), buffer.end()); + THEN("the target should be 100") { + auto &val = parsed.first; + REQUIRE(parsed.second != buffer.begin()); + REQUIRE(val.has_value()); + REQUIRE(val.value().setpoint == 95.0F); + REQUIRE(val.value().hold_time == + gcode::SetPlateTemperature::infinite_hold); + } + } + WHEN("Setting target to 0.0") { + std::string buffer = "M104 S0.0\n"; + auto parsed = + gcode::SetPlateTemperature::parse(buffer.begin(), buffer.end()); + THEN("the target should be 0.0") { + auto &val = parsed.first; + REQUIRE(parsed.second != buffer.begin()); + REQUIRE(val.has_value()); + REQUIRE(val.value().setpoint == 0.0F); + REQUIRE(val.value().hold_time == + gcode::SetPlateTemperature::infinite_hold); + } + } + WHEN("Setting target to 50C with a hold time of 40 seconds") { + std::string buffer = "M104 S50.0 H40\n"; + auto parsed = + gcode::SetPlateTemperature::parse(buffer.begin(), buffer.end()); + THEN("the target should be 50.0") { + auto &val = parsed.first; + REQUIRE(parsed.second != buffer.begin()); + REQUIRE(val.has_value()); + REQUIRE(val.value().setpoint == 50.0F); + REQUIRE(val.value().hold_time == 40.0F); + } + } + } + GIVEN("invalid input") { + std::string buffer = "M104\n"; + WHEN("parsing") { + auto parsed = + gcode::SetPlateTemperature::parse(buffer.begin(), buffer.end()); + THEN("parsing fails") { + REQUIRE(parsed.second == buffer.begin()); + REQUIRE(!parsed.first.has_value()); + } + } + } + GIVEN("wrong gcode") { + std::string buffer = "M1044 S\n"; + WHEN("parsing") { + auto parsed = + gcode::SetPlateTemperature::parse(buffer.begin(), buffer.end()); + THEN("parsing fails") { + REQUIRE(parsed.second == buffer.begin()); + REQUIRE(!parsed.first.has_value()); + } + } + } +} diff --git a/stm32-modules/thermocycler-refresh/tests/test_m14.cpp b/stm32-modules/thermocycler-refresh/tests/test_m14.cpp new file mode 100644 index 000000000..4c33a672f --- /dev/null +++ b/stm32-modules/thermocycler-refresh/tests/test_m14.cpp @@ -0,0 +1,58 @@ +#include "catch2/catch.hpp" + +// Push this diagnostic to avoid a compiler error about printing to too +// small of a buffer... which we're doing on purpose! +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-truncation" +#include "thermocycler-refresh/gcodes.hpp" +#pragma GCC diagnostic pop + +SCENARIO("DeactivatePlate (M14) parser works", "[gcode][parse][m14]") { + GIVEN("a response buffer large enough for the formatted response") { + std::string buffer(256, 'c'); + WHEN("filling response") { + auto written = gcode::DeactivatePlate::write_response_into( + buffer.begin(), buffer.end()); + THEN("the response should be written in full") { + REQUIRE_THAT(buffer, Catch::Matchers::StartsWith("M14 OK\n")); + REQUIRE(written != buffer.begin()); + } + } + } + + GIVEN("a response buffer not large enough for the formatted response") { + std::string buffer(16, 'c'); + WHEN("filling response") { + auto written = gcode::DeactivatePlate::write_response_into( + buffer.begin(), buffer.begin() + 5); + THEN("the response should write only up to the available space") { + std::string response = "M14 Occccccccccc"; + REQUIRE_THAT(buffer, Catch::Matchers::Equals(response)); + REQUIRE(written != buffer.begin()); + } + } + } + + GIVEN("a valid input") { + std::string buffer = "M14\n"; + WHEN("parsing") { + auto res = + gcode::DeactivatePlate::parse(buffer.begin(), buffer.end()); + THEN("a valid gcode should be produced") { + REQUIRE(res.first.has_value()); + REQUIRE(res.second != buffer.begin()); + } + } + } + GIVEN("an invalid input") { + std::string buffer = "M 108\n"; + WHEN("parsing") { + auto res = + gcode::DeactivatePlate::parse(buffer.begin(), buffer.end()); + THEN("an error should be produced") { + REQUIRE(!res.first.has_value()); + REQUIRE(res.second == buffer.begin()); + } + } + } +} diff --git a/stm32-modules/thermocycler-refresh/tests/test_thermal_plate_task.cpp b/stm32-modules/thermocycler-refresh/tests/test_thermal_plate_task.cpp index e99d71862..1aa7da9e7 100644 --- a/stm32-modules/thermocycler-refresh/tests/test_thermal_plate_task.cpp +++ b/stm32-modules/thermocycler-refresh/tests/test_thermal_plate_task.cpp @@ -224,6 +224,203 @@ SCENARIO("thermal plate task message passing") { } } } + WHEN("Sending a SetPIDConstants to configure the plate constants") { + auto message = messages::SetPIDConstantsMessage{ + .id = 123, + .selection = PidSelection::PELTIERS, + .p = 1, + .i = 1, + .d = 1}; + tasks->get_thermal_plate_queue().backing_deque.push_back( + messages::ThermalPlateMessage(message)); + tasks->run_thermal_plate_task(); + THEN("the task should get the message") { + REQUIRE(tasks->get_thermal_plate_queue().backing_deque.empty()); + AND_THEN("the task should act on the message") { + REQUIRE( + tasks->get_thermal_plate_queue().backing_deque.empty()); + + REQUIRE( + !tasks->get_host_comms_queue().backing_deque.empty()); + auto response = + tasks->get_host_comms_queue().backing_deque.front(); + tasks->get_host_comms_queue().backing_deque.pop_front(); + REQUIRE( + std::holds_alternative( + response)); + auto response_msg = + std::get(response); + REQUIRE(response_msg.responding_to_id == 123); + REQUIRE(response_msg.with_error == + errors::ErrorCode::NO_ERROR); + } + } + } + WHEN("Sending a SetPIDConstants with invalid constants") { + auto message = messages::SetPIDConstantsMessage{ + .id = 555, + .selection = PidSelection::PELTIERS, + .p = 1000, + .i = 1, + .d = 1}; + tasks->get_thermal_plate_queue().backing_deque.push_back( + messages::ThermalPlateMessage(message)); + tasks->run_thermal_plate_task(); + THEN("the task should get the message") { + REQUIRE(tasks->get_thermal_plate_queue().backing_deque.empty()); + AND_THEN("the task should act on the message") { + REQUIRE( + tasks->get_thermal_plate_queue().backing_deque.empty()); + + REQUIRE( + !tasks->get_host_comms_queue().backing_deque.empty()); + auto response = + tasks->get_host_comms_queue().backing_deque.front(); + tasks->get_host_comms_queue().backing_deque.pop_front(); + REQUIRE( + std::holds_alternative( + response)); + auto response_msg = + std::get(response); + REQUIRE(response_msg.responding_to_id == 555); + REQUIRE(response_msg.with_error == + errors::ErrorCode::THERMAL_CONSTANT_OUT_OF_RANGE); + } + } + } + WHEN("Sending a SetPlateTemperature message to enable the plate") { + auto message = messages::SetPlateTemperatureMessage{ + .id = 123, .setpoint = 90.0F, .hold_time = 10.0F}; + tasks->get_thermal_plate_queue().backing_deque.push_back( + messages::ThermalPlateMessage(message)); + tasks->run_thermal_plate_task(); + THEN("the task should get the message") { + REQUIRE(tasks->get_thermal_plate_queue().backing_deque.empty()); + AND_THEN("the task should respond to the message") { + REQUIRE( + !tasks->get_host_comms_queue().backing_deque.empty()); + auto response = + tasks->get_host_comms_queue().backing_deque.front(); + tasks->get_host_comms_queue().backing_deque.pop_front(); + REQUIRE( + std::holds_alternative( + response)); + auto response_msg = + std::get(response); + REQUIRE(response_msg.responding_to_id == 123); + REQUIRE(response_msg.with_error == + errors::ErrorCode::NO_ERROR); + AND_WHEN("sending a GetPlateTemp query") { + auto tempMessage = + messages::GetPlateTempMessage{.id = 555}; + tasks->get_thermal_plate_queue() + .backing_deque.push_back( + messages::ThermalPlateMessage(tempMessage)); + tasks->run_thermal_plate_task(); + THEN("the response should have the new setpoint") { + REQUIRE(!tasks->get_host_comms_queue() + .backing_deque.empty()); + REQUIRE(std::get( + tasks->get_host_comms_queue() + .backing_deque.front()) + .set_temp == message.setpoint); + } + } + } + AND_WHEN("sending updated temperatures below target") { + tasks->get_thermal_plate_queue().backing_deque.push_back( + messages::ThermalPlateMessage(read_message)); + tasks->run_thermal_plate_task(); + THEN("the peltiers should be enabled") { + auto p_right = + tasks->get_thermal_plate_policy().get_peltier( + PeltierID::PELTIER_RIGHT); + REQUIRE(p_right.first == + PeltierDirection::PELTIER_HEATING); + REQUIRE(p_right.second > 0.0F); + auto p_left = + tasks->get_thermal_plate_policy().get_peltier( + PeltierID::PELTIER_LEFT); + REQUIRE(p_left.first == + PeltierDirection::PELTIER_HEATING); + REQUIRE(p_left.second > 0.0F); + auto p_center = + tasks->get_thermal_plate_policy().get_peltier( + PeltierID::PELTIER_CENTER); + REQUIRE(p_center.first == + PeltierDirection::PELTIER_HEATING); + REQUIRE(p_center.second > 0.0F); + } + } + } + AND_WHEN("sending a DeactivatePlate command") { + tasks->get_host_comms_queue().backing_deque.pop_front(); + auto tempMessage = messages::DeactivatePlateMessage{.id = 321}; + tasks->get_thermal_plate_queue().backing_deque.push_back( + messages::ThermalPlateMessage(tempMessage)); + tasks->run_thermal_plate_task(); + THEN("the task should respond to the message") { + REQUIRE( + !tasks->get_host_comms_queue().backing_deque.empty()); + REQUIRE( + std::get( + tasks->get_host_comms_queue().backing_deque.front()) + .responding_to_id == 321); + tasks->get_host_comms_queue().backing_deque.pop_front(); + AND_WHEN("sending a GetPlateTemp query") { + auto tempMessage = + messages::GetPlateTempMessage{.id = 555}; + tasks->get_thermal_plate_queue() + .backing_deque.push_back( + messages::ThermalPlateMessage(tempMessage)); + tasks->run_thermal_plate_task(); + THEN("the response should have no setpoint") { + REQUIRE(!tasks->get_host_comms_queue() + .backing_deque.empty()); + REQUIRE(std::get( + tasks->get_host_comms_queue() + .backing_deque.front()) + .set_temp == 0.0F); + } + } + } + } + AND_WHEN( + "Sending a SetPIDConstants to configure the peltier " + "constants") { + tasks->get_host_comms_queue().backing_deque.pop_front(); + auto message = messages::SetPIDConstantsMessage{ + .id = 808, + .selection = PidSelection::PELTIERS, + .p = 1, + .i = 1, + .d = 1}; + tasks->get_thermal_plate_queue().backing_deque.push_back( + messages::ThermalPlateMessage(message)); + tasks->run_thermal_plate_task(); + THEN("the task should get the message") { + REQUIRE( + tasks->get_thermal_plate_queue().backing_deque.empty()); + AND_THEN("the task should respond with a busy error") { + REQUIRE(tasks->get_thermal_plate_queue() + .backing_deque.empty()); + + REQUIRE(!tasks->get_host_comms_queue() + .backing_deque.empty()); + auto response = + tasks->get_host_comms_queue().backing_deque.front(); + tasks->get_host_comms_queue().backing_deque.pop_front(); + REQUIRE(std::holds_alternative< + messages::AcknowledgePrevious>(response)); + auto response_msg = + std::get(response); + REQUIRE(response_msg.responding_to_id == 808); + REQUIRE(response_msg.with_error == + errors::ErrorCode::THERMAL_PLATE_BUSY); + } + } + } + } } GIVEN("a thermal plate task with shorted thermistors") { auto tasks = TaskBuilder::build(); @@ -340,6 +537,47 @@ SCENARIO("thermal plate task message passing") { } } } + WHEN("Sending a SetPlateTemperature message to enable the lid") { + auto message = messages::SetPlateTemperatureMessage{ + .id = 123, .setpoint = 68.0F, .hold_time = 111}; + tasks->get_thermal_plate_queue().backing_deque.push_back( + messages::ThermalPlateMessage(message)); + tasks->run_thermal_plate_task(); + THEN("the task should get the message") { + REQUIRE(tasks->get_thermal_plate_queue().backing_deque.empty()); + AND_THEN("the task should respond with an error") { + REQUIRE( + !tasks->get_host_comms_queue().backing_deque.empty()); + auto response = + tasks->get_host_comms_queue().backing_deque.front(); + tasks->get_host_comms_queue().backing_deque.pop_front(); + REQUIRE( + std::holds_alternative( + response)); + auto response_msg = + std::get(response); + REQUIRE(response_msg.responding_to_id == 123); + REQUIRE(response_msg.with_error != + errors::ErrorCode::NO_ERROR); + AND_WHEN("sending a GetPlateTemp query") { + auto tempMessage = + messages::GetPlateTempMessage{.id = 555}; + tasks->get_thermal_plate_queue() + .backing_deque.push_back( + messages::ThermalPlateMessage(tempMessage)); + tasks->run_thermal_plate_task(); + THEN("the response should have a setpoint of 0") { + REQUIRE(!tasks->get_host_comms_queue() + .backing_deque.empty()); + REQUIRE(std::get( + tasks->get_host_comms_queue() + .backing_deque.front()) + .set_temp == 0.0F); + } + } + } + } + } } GIVEN("a thermal plate task with disconnected thermistors") { auto tasks = TaskBuilder::build();