Haptic Controller
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1#include <Arduino.h>
2#include "ODriveCAN.h"
3#include <FlexCAN_T4.h>
4#include "ODriveFlexCAN.hpp"
5#include "teensy_can.h"
6#include "virtualTimer.h"
7#include "defines.h"
8
10
11#include "utils/angles.hpp"
13#include "utils/helpers.hpp"
14#include "utils/filters.hpp"
15#include "utils/led.hpp"
16
20#include <Adafruit_NeoPixel.h>
21
22#include "tests.hpp"
23
24#define ODRV_CAN0 CAN2
25#define ODRV_CAN1 CAN3
26
27VirtualTimerGroup timer_group{};
28VirtualTimerGroup micro_timer_group{};
29TeensyCAN<1> teensy_can;
30
31struct ODriveStatus; // hack to prevent teensy compile error
32
34
35// Declaring static members of the CommsController class
36FlexCAN_T4<CAN2, RX_SIZE_256, TX_SIZE_256> odrive_can0;
37FlexCAN_T4<CAN3, RX_SIZE_256, TX_SIZE_256> odrive_can1;
38
41
44
47
48// Called for every message that arrives on the CAN bus
49void onOdriveCanMessage(const CanMsg &msg)
50{
51 onReceive(msg, odrv0.odrv_);
52 onReceive(msg, odrv1.odrv_);
53}
54
62
64
69
70std::vector<float> joint_thetas = {0.0f, 0.0f};
71std::vector<float> joint_vels = {0.0f, 0.0f};
72std::vector<float> pos = {0.0f, 0.0f};
73std::vector<float> vel = {0.0f, 0.0f};
74
77
78void enterState(ControllerState state);
79void exitState(ControllerState state);
80
105
106// Need to update members wherever they change
108
111{
112 // db_println("Possible Errors:");
113 // db_print("Teensy heartbeat timeout: ");
114 // db_println(sys_inf.teensy_heartbeat_timeout);
115 // db_print("ODrive 0 heartbeat timeout: ");
116 // db_println(sys_inf.odrive0_heartbeat_timeout);
117 // db_print("ODrive 1 heartbeat timeout: ");
118 // db_println(sys_inf.odrive1_heartbeat_timeout);
119 // db_print("Estop enabled: ");
120 // db_println(sys_inf.estop_enabled);
121 // db_print("Relays open: ");
122 // db_println(sys_inf.relays_open);
123 // db_print("Deadman switch pressed: ");
124 // db_print(sys_inf.deadman_switch_pressed);
125 // db_println("\n");
126 // db_print("Mirror Teensy State: ");
127 // db_println(sys_inf.mirror_teensy_state);
128 const auto mirror_pos_0 = static_cast<float>(comms_controller.end_eff_pos_0_rx);
129 const auto mirror_pos_2 = static_cast<float>(comms_controller.end_eff_pos_1_rx);
130 #ifdef FINGER
131 db_printVal(pos.at(0));
132 db_printVal(pos.at(2));
133 db_printVal(vel.at(0));
134 db_printVal(vel.at(1));
135 db_printVal(mirror_pos_0);
136 db_printVal(mirror_pos_2);
137 #else
138 db_printVal(joint_thetas.at(0));
139 db_printVal(joint_thetas.at(1));
140 db_printVal(joint_vels.at(0));
141 db_printVal(joint_vels.at(1));
142 db_printVal(mirror_pos_0);
143 db_printVal(mirror_pos_2);
144 #endif
145}
146
154{
161 comms_controller.get_odrv0(),
162 comms_controller.get_odrv0_data().last_error)) ||
164 comms_controller.get_odrv1(),
165 comms_controller.get_odrv1_data().last_error)) ||
166 (odrv0_user_data.last_heartbeat.Axis_State != ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL) ||
167 (odrv1_user_data.last_heartbeat.Axis_State != ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL);
168 // (sys_inf.mirror_teensy_state == ControllerState::ERROR);
169}
170
172
178{
179 SCB_AIRCR = 0x05FA0004;
180}
181
187{
188 digitalWrite(RELAY_PIN_OUTPUT, LOW);
189}
190
196{
197 digitalWrite(RELAY_PIN_OUTPUT, HIGH);
198}
199
205{
206 Estop_msg_t estop_msg;
207 comms_controller.get_odrv0().send(estop_msg);
208 comms_controller.get_odrv1().send(estop_msg);
209}
210
213{
214 clear_safe_errors(comms_controller.get_odrv0(), comms_controller.get_odrv0_data().last_error);
215 comms_controller.get_odrv0().setState(ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL);
216 clear_safe_errors(comms_controller.get_odrv1(), comms_controller.get_odrv1_data().last_error);
217 comms_controller.get_odrv1().setState(ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL);
218}
219
227{
228 return true;
229 // return float_close_compare(joint_0.get_position(), init_0.get_position()) &&
230 // float_close_compare(joint_1.get_position(), init_1.get_position());
231}
232
240{
241 const auto other_end_eff_x = static_cast<float>(comms_controller.end_eff_pos_0_rx);
242 const auto other_end_eff_z = static_cast<float>(comms_controller.end_eff_pos_1_rx);
243
244 // mirror_x_state.update_position(other_end_eff_x, 0.01);
245 // mirror_z_state.update_position(other_end_eff_z, 0.01);
246
247 #ifdef FINGER
248 return float_close_compare(other_end_eff_x, pos.at(0), 0.03) &&
249 float_close_compare(other_end_eff_z, pos.at(2), 0.03);
250 #else
251 return float_close_compare(other_end_eff_x, joint_thetas.at(0), 0.34) &&
252 float_close_compare(other_end_eff_z, joint_thetas.at(1), 0.34);
253 #endif
254}
255
261{
262 if (millis() - last_received_time > 1000)
263 {
264 digitalWrite(LED_BUILTIN, LOW);
265 }
266 else
267 {
268 digitalWrite(LED_BUILTIN, HIGH);
269 }
270
271 switch (controller_state)
272 {
274 setColor(1, blue);
275 break;
277 setColor(1, yellow);
278 break;
280 setColor(1, green);
281 break;
283 setColor(1, red);
284 break;
285 }
286
288 {
289 setColor(2, red);
290 }
291 else
292 {
293 setColor(2, green);
294 }
295
297 {
298 setColor(3, red);
299 }
301 {
302 setColor(3, cyan);
303 }
305 {
306 setColor(3, magneta);
307 }
308 else
309 {
310 setColor(3, green);
311 }
312}
313
319{
320 // db_print("Current state: ");
321 // db_println(controller_state);
322 switch (controller_state)
323 {
325 // do nothing
326 break;
328 // check position syncing
329 break;
331 // check position syncing
332 // mirror positiong
333 #ifdef FINGER
335 #else
337 #endif
338 break;
341 {
342 openRelays();
343 }
344 break;
345 }
346
347 // things that should constantly be happening
348 // check if teensy heartbeat timeout
349 // check if odrives heartbeat timeout
350 // check if estop enabled -> idk how to do this
351 // check if relays are open
352 // check mirror teensy state
353 // light leds -> 1 for teensy heartbeat, 1 for odrive heartbeat, 1 for cur state
354 // check deadman switch
355}
356
362{
363 // check errors, if errors, change state to ERROR and return
364 // check if estop enabled, change state to ERROR and return
365 // check if teensy heartbeat timeout, change state to ERROR and return
366 // check if odrives heartbeats timeout, change state to ERROR and return
367 // check if relays are open, change state to ERROR and return
368 if (shouldError())
369 {
371 {
372 return;
373 }
377 db_println("Switching to ERROR");
379 return;
380 }
381
382 switch (controller_state)
383 {
385 // check if in init position
386 // check if comms controller is ready
387 // if both, change state to READY
388 // printEverything();
389 if (isInitialized() && !comms_controller.comms_timeout())
390 {
394 db_println("Switching to READY");
395 }
396 break;
398 // if mirroring teensy in ready
399 // AND if position synced
400 // AND deadman switch pressed
401 // change state to RUNNING
402 // db_println("In ready state: ");
403 // db_println(sys_inf.mirror_teensy_state);
404 // db_println(sys_inf.deadman_switch_pressed);
408 isSynced())
409 {
413 db_println("Switching to RUNNING");
414 }
415 break;
417 // if deadman switch not pressed
418 // OR if teensy not in running
419 // OR out of sync
420 // change state to READY
424 !isSynced())
425 {
429 db_println("Switching to READY");
430 }
431 break;
433 // check if manual error clear was pressed, if so, change state to initializing
434 // if estop disabled, comms controller ready, then change state to initializing
435 if (!comms_controller.comms_timeout())
436 {
440 db_println("Switching to INITIALIZING");
441 }
442 break;
443 }
444}
445
449{
450 comms_controller.system_state_tx = state;
451 switch (state)
452 {
455 break;
457 break;
459 break;
462 // if both teensy and odrive heartbeat timeout, reboot
465 {
466 doReboot();
467 }
468 break;
469 }
470}
471
475{
476 switch (state)
477 {
479 break;
481 break;
483 comms_controller.get_odrv0().setTorque(0.0);
484 comms_controller.get_odrv1().setTorque(0.0);
485 break;
487 closeRelays();
488 break;
489 }
490}
491
494{
495 sys_inf.teensy_heartbeat_timeout = comms_controller.teensy_heartbeat_timeout;
497}
498
501{
502 // Motor angles in radians
503 const auto phi0 = filter_phi_0.update(
505 odrv0_user_data.last_feedback.Pos_Estimate,
508 const auto phi1 = filter_phi_1.update(
510 odrv1_user_data.last_feedback.Pos_Estimate,
513
514 const auto phi_dot_0 = filter_phi_dot_0.update(
516 odrv0_user_data.last_feedback.Vel_Estimate,
519 const auto phi_dot_1 = filter_phi_dot_1.update(
521 odrv1_user_data.last_feedback.Vel_Estimate,
524
525 joint_thetas = phi_to_theta({phi0, phi1});
526 joint_vels = motors_to_joints({phi_dot_0, phi_dot_1});
527
528 #ifdef FINGER
530
531 const auto J_s = spatial_jacobian(joint_thetas);
532
533 vel = J_s * joint_vels;
534
535 comms_controller.end_eff_pos_0_tx = pos.at(0);
536 comms_controller.end_eff_pos_1_tx = pos.at(2);
537
538 comms_controller.end_eff_vel_0_tx = vel.at(0);
539 // .at(1) is correct here, vel only contains x and z velocity due to y be === 0
540 comms_controller.end_eff_vel_1_tx = vel.at(1);
541 #else
542 comms_controller.end_eff_pos_0_tx = joint_thetas.at(0);
543 comms_controller.end_eff_pos_1_tx = joint_thetas.at(1);
544
545 comms_controller.end_eff_vel_0_tx = joint_vels.at(0);
546 // .at(1) is correct here, vel only contains x and z velocity due to y be === 0
547 comms_controller.end_eff_vel_1_tx = joint_vels.at(1);
548
549 #endif
550
551 // const auto mirror_pos_0 = static_cast<float>(comms_controller.end_eff_pos_0_rx);
552 // const auto mirror_pos_2 = static_cast<float>(comms_controller.end_eff_pos_1_rx);
553 // const auto mirror_vel_0 = static_cast<float>(comms_controller.end_eff_vel_0_rx);
554 // const auto mirror_vel_2 = static_cast<float>(comms_controller.end_eff_vel_1_rx);
555
556 // db_printVal(mirror_vel_0);
557 // db_printVal(mirror_vel_2);
558}
559
563{
564 const auto mirror_joint_0 = static_cast<float>(comms_controller.end_eff_pos_0_rx);
565 const auto mirror_joint_1 = static_cast<float>(comms_controller.end_eff_pos_1_rx);
566 const auto mirror_joint_0_vel = static_cast<float>(comms_controller.end_eff_vel_0_rx);
567 const auto mirror_joint_1_vel = static_cast<float>(comms_controller.end_eff_vel_1_rx);
568
569 const auto delta_0 = mirror_joint_0 - joint_thetas.at(0);
570 const auto delta_1 = mirror_joint_1 - joint_thetas.at(1);
571
572 const auto delta_vel_0 = mirror_joint_0_vel - joint_vels.at(0);
573 const auto delta_vel_1 = mirror_joint_1_vel - joint_vels.at(1);
574
575 const float k_0 = 4.0f;
576 const float k_1 = 2.0f;
577
578 const float c_0 = 0.00f;
579 const float c_1 = 0.00f;
580
581 const float joint_0_torque = k_0 * delta_0 + c_0 * delta_vel_0;
582 const float joint_1_torque = k_1 * delta_1 + c_1 * delta_vel_1;
583
584 const auto soft_limit = soft_limit_torques(joint_thetas);
585
586 const auto torques = tau_to_torque({joint_0_torque + soft_limit.at(0), joint_1_torque + soft_limit.at(1), 0.0});
587
588 const auto clamped_torque_0 = limit<float>(torques.at(0), motor_torque_limit);
589 const auto clamped_torque_1 = limit<float>(torques.at(1), motor_torque_limit);
590
591 // May not be needed anymore since it is being lmited in OdriveController as long as motor limits is set
592 comms_controller.get_odrv0().setTorque(clamped_torque_0);
593 comms_controller.get_odrv1().setTorque(clamped_torque_1);
594}
595
596
600{
601 // db_printVal(joint_thetas.at(0));
602 // db_printVal(joint_thetas.at(1));
603
604 // db_printVal(pos.at(0));
605 // db_printVal(pos.at(2));
606
607 auto J_s = spatial_jacobian(joint_thetas);
608
609 const auto mirror_x = static_cast<float>(comms_controller.end_eff_pos_0_rx);
610 const auto mirror_z = static_cast<float>(comms_controller.end_eff_pos_1_rx);
611 const auto mirror_x_vel = static_cast<float>(comms_controller.end_eff_vel_0_rx);
612 const auto mirror_z_vel = static_cast<float>(comms_controller.end_eff_vel_1_rx);
613
614 // db_printVal(mirror_x);
615 // db_printVal(mirror_x_vel);
616
617 // db_printVal(mirror_z);
618 // db_printVal(mirror_z_vel);
619 // Uses the last estimated position and the new position
620 // If the first reading is not near 0 this may be very large
621 const auto delta_x = mirror_x - pos.at(0);
622 const auto delta_z = mirror_z - pos.at(2);
623
624 const auto delta_x_vel = mirror_x_vel - vel.at(0);
625 const auto delta_z_vel = mirror_z_vel - vel.at(1);
626
627 // db_printVal(delta_x);
628 // db_printVal(delta_z);
629
630 const auto k_x = 500.0f; // 170
631 const auto k_z = 500.0f; // 170
632
633 const auto c_x = 1.65f;
634 const auto c_z = 1.65f;
635
636 const auto F_stiffness_x = k_x * delta_x;
637 const auto F_stiffness_z = k_z * delta_z;
638
639 const auto F_damping_x = c_x * delta_x_vel;
640 const auto F_damping_z = c_z * delta_z_vel;
641
642 const auto F_kx = F_stiffness_x + F_damping_x;
643 const auto F_kz = F_stiffness_z + F_damping_z;
644
645 auto tau = J_s.T() * std::vector<float>{F_kx, F_kz};
646 const auto soft_limit = soft_limit_torques(joint_thetas);
647 // db_printVal(tau);
648 tau.at(0) += soft_limit.at(0);
649 tau.at(1) += soft_limit.at(1);
650 const auto torques = tau_to_torque(tau);
651
652
653 const auto clamped_torque_0 = limit<float>(torques.at(0), motor_torque_limit);
654 const auto clamped_torque_1 = limit<float>(torques.at(1), motor_torque_limit);
655
656 // db_printVal(soft_limit.at(0));
657 // db_printVal(soft_limit.at(1));
658
659 // db_printVal(clamped_torque_0);
660 // db_printVal(clamped_torque_1);
661
662 // May not be needed anymore since it is being lmited in OdriveController as long as motor limits is set
663 comms_controller.get_odrv0().setTorque(clamped_torque_0);
664 comms_controller.get_odrv1().setTorque(clamped_torque_1);
665}
666
669{
670 comms_controller.Tick();
671 // Needed?
672 delay(100);
673
674 if (digitalRead(DEADMAN_SWITCH) == LOW)
675 {
677 return;
678 }
680 return;
681}
682
684void setup()
685{
686 // Serial setup
687 Serial.begin(115200);
688 for (int i = 0; i < 30 && !Serial; ++i)
689 {
690 delay(100);
691 }
692 delay(200);
693
694#ifdef TESTING
695 allUnitTests();
696#endif
697
698 // CAN setup
699 db_println("Starting CAN setup");
700 teensy_can.Initialize(ICAN::BaudRate::kBaud1M);
701 if (!comms_controller.Initialize())
702 {
703 db_println("CAN failed to initialize: reset required");
704 }
705
706 // Led setup
707 ledSetup();
708 pinMode(DEADMAN_SWITCH, INPUT);
709 pinMode(RELAY_PIN_OUTPUT, OUTPUT);
710 ledSetup();
711
712 // Interrupt setup
713 attachInterrupt(digitalPinToInterrupt(DEADMAN_SWITCH), checkDeadman, CHANGE);
714 attachInterrupt(
715 digitalPinToInterrupt(CLEAR_ERROR_BUTTON), []()
716 {
717 comms_controller.get_odrv0().clearErrors();
718 comms_controller.get_odrv1().clearErrors();
719 comms_controller.get_odrv0().setAbsolutePosition(0.0f);
720 comms_controller.get_odrv1().setAbsolutePosition(0.0f);
724 db_println("Switching to INITIALIZING"); },
725 RISING);
726
727 // Start initial state
728 checkDeadman();
729 db_println("Switching to INITIALIZING");
732
733 // Millisecond timer setup
734 db_println("Starting timer setup");
735 timer_group.AddTimer(10, updateSystemInfo);
736 timer_group.AddTimer(10, ledUpdate);
737 timer_group.AddTimer(10, changeState);
738 timer_group.AddTimer(10, printEverything);
739
740 // Microsecond timer setup
741 micro_timer_group.AddTimer(500, doState);
743 db_println("Timer complete");
744
745 db_println("Setup complete\n\n");
746}
747
749void loop()
750{
751 db_endTiming("loop");
752 db_startTiming("loop");
753 timer_group.Tick(millis());
754 micro_timer_group.Tick(micros());
755 comms_controller.Tick();
756 teensy_can.Tick();
757}
constexpr float convert_angular_units(float ang, const AngleUnits curr_unit, const AngleUnits desr_unit)
Converts an angle from one unit to another.
Definition angles.hpp:18
@ RADIANS
Definition angles.hpp:9
@ REVS
Definition angles.hpp:11
Comms Controller class for managing Canlines.
Infinite Impulse Response Filter.
Definition filters.hpp:8
Moving Average Filter.
Definition filters.hpp:33
float update(float new_sample)
Definition filters.hpp:42
Class that abstracts operations with the ODrive/.
ODriveCAN odrv_
Odrive object to control.
ControllerState
The state of the system.
@ RUNNING
@ ERROR
@ INITIALIZING
@ READY
std::vector< float > motors_to_joints(std::vector< float > motor_vars)
std::vector< float > phi_to_theta(std::vector< float > motor_phis)
std::vector< float > tau_to_torque(std::vector< float > joint_taus)
std::vector< float > forward_kinematics(std::vector< float > joint_angles)
Performs forward kinematics for the haptic finger.
Matrix spatial_jacobian(std::vector< float > joint_angles)
std::vector< float > soft_limit_torques(std::vector< float > joint_thetas)
bool float_close_compare(float a, float b, float tol=1e-6)
Compare two floats for equality under a tolerance.
Definition helpers.hpp:13
T limit(T input, const T max_value, const std::optional< T >(min_value)=std::nullopt)
Clamps an input value to a.
Definition helpers.hpp:25
Color cyan
Definition led.hpp:23
Color yellow
Definition led.hpp:22
Color magneta
Definition led.hpp:24
void ledSetup()
Sets the pins for the LEDs as output.
Definition led.hpp:79
Color green
Definition led.hpp:20
Color blue
Definition led.hpp:21
void setColor(int idx, Color color)
Sets the color for the data board LEDs using color struct.
Definition led.hpp:31
Color red
Definition led.hpp:19
bool isInitialized()
Check if the motors are in the initial position.
Definition main.cpp:226
void printEverything()
Print statements, set at a separate frequency than others.
Definition main.cpp:110
void endEffMirrorUpdate()
: Links end effector positions together
Definition main.cpp:599
std::vector< float > vel
Definition main.cpp:73
ODriveUserData odrv0_user_data
Definition main.cpp:39
void checkDeadman()
Check the deadman switch state.
Definition main.cpp:668
void updateSystemInfo()
Update system information struct with new CAN information.
Definition main.cpp:493
TeensyCAN< 1 > teensy_can
Definition main.cpp:29
void ledUpdate()
Update the LED strip and on board LED based on the current state, timeouts, and mirror states.
Definition main.cpp:260
uint32_t last_received_time
Definition main.cpp:33
void request_enable()
Process enable request, clear errors.
Definition main.cpp:212
void setup()
setup function before main loop
Definition main.cpp:684
CommsController< ODRV_CAN0, ODRV_CAN1 > comms_controller
Definition main.cpp:55
VirtualTimerGroup micro_timer_group
Definition main.cpp:28
std::vector< float > joint_thetas
Definition main.cpp:70
OdriveController< CAN3 > odrv1
: Odrive for second joint
Definition main.cpp:46
std::vector< float > pos
Definition main.cpp:72
void closeRelays()
Close relays to enable motors.
Definition main.cpp:195
MovingAverageFilter filter_phi_dot_1
Definition main.cpp:68
bool shouldError()
Check if there is a critical error that should prompt a transition to the error state.
Definition main.cpp:153
LowPassFilter filter
Definition main.cpp:63
FlexCAN_T4< CAN2, RX_SIZE_256, TX_SIZE_256 > odrive_can0
Definition main.cpp:36
ControllerState controller_state
Definition main.cpp:171
FlexCAN_T4< CAN3, RX_SIZE_256, TX_SIZE_256 > odrive_can1
Definition main.cpp:37
OdriveController< CAN2 > odrv0
: Odrive for first joint
Definition main.cpp:43
SystemInfo sys_inf
Definition main.cpp:107
void exitState(ControllerState state)
Process exit state requests.
Definition main.cpp:474
void enterState(ControllerState state)
Process enter state requests.
Definition main.cpp:448
void changeState()
Evaluates state transitions based on current state and system information.
Definition main.cpp:361
void updateAndSendSystemInfo()
Send the end effector position over CAN to the other teensy.
Definition main.cpp:500
MovingAverageFilter filter_phi_0
Definition main.cpp:65
VirtualTimerGroup timer_group
Definition main.cpp:27
std::vector< float > joint_vels
Definition main.cpp:71
void doReboot()
Reboot the Teensy.
Definition main.cpp:177
MovingAverageFilter filter_phi_dot_0
Definition main.cpp:67
MovingAverageFilter filter_phi_1
Definition main.cpp:66
void request_disable()
Request estop from odrives.
Definition main.cpp:204
ODriveUserData odrv1_user_data
Definition main.cpp:40
void jointMirrorUpdate()
: Links joints states
Definition main.cpp:562
void doState()
Handles the current state of the controller.
Definition main.cpp:318
void onOdriveCanMessage(const CanMsg &msg)
Definition main.cpp:49
bool isSynced()
Check if the motors are in sync.
Definition main.cpp:239
void openRelays()
Open relays to disable motors.
Definition main.cpp:186
void loop()
Main loop.
Definition main.cpp:749
bool clear_safe_errors(ODriveCAN odrv, uint32_t error)
: Clear errors deemed as "Safe to Clean Automatically"
: The Struct to contain feedback from the ODrive
Get_Encoder_Estimates_msg_t last_feedback
Heartbeat_msg_t last_heartbeat
Struct to hold important system information.
Definition main.cpp:86
SystemInfo()=default
SystemInfo(CommsController< ODRV_CAN0, ODRV_CAN1 > &cc)
Definition main.cpp:90
bool & odrive1_heartbeat_timeout
Definition main.cpp:99
ControllerState mirror_teensy_state
Definition main.cpp:103
bool teensy_heartbeat_timeout
Definition main.cpp:97
bool deadman_switch_pressed
Definition main.cpp:102
bool & odrive0_heartbeat_timeout
Definition main.cpp:98
bool estop_enabled
Definition main.cpp:100
bool relays_open
Definition main.cpp:101
void allUnitTests()
Call in setup() in main.cpp to run unit tests instead of normal operation.
Definition tests.hpp:36