2 This file is part of Smoothie (http://smoothieware.org/). The motion control part is heavily based on Grbl (https://github.com/simen/grbl) with additions from Sungeun K. Jeon (https://github.com/chamnit/grbl)
3 Smoothie 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.
4 Smoothie 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.
5 You should have received a copy of the GNU General Public License along with Smoothie. If not, see <http://www.gnu.org/licenses/>.
8 #include "libs/Module.h"
9 #include "libs/Kernel.h"
16 #include "libs/nuts_bolts.h"
18 #include "libs/StepperMotor.h"
19 #include "../communication/utils/Gcode.h"
20 #include "PublicDataRequest.h"
21 #include "arm_solutions/BaseSolution.h"
22 #include "arm_solutions/CartesianSolution.h"
23 #include "arm_solutions/RotatableCartesianSolution.h"
24 #include "arm_solutions/RostockSolution.h"
25 #include "arm_solutions/HBotSolution.h"
27 // The Robot converts GCodes into actual movements, and then adds them to the Planner, which passes them to the Conveyor so they can be added to the queue
28 // It takes care of cutting arcs into segments, same thing for line that are too long
31 this->inch_mode
= false;
32 this->absolute_mode
= true;
33 this->motion_mode
= MOTION_MODE_SEEK
;
34 this->select_plane(X_AXIS
, Y_AXIS
, Z_AXIS
);
35 clear_vector(this->current_position
);
36 clear_vector(this->last_milestone
);
37 this->arm_solution
= NULL
;
38 seconds_per_minute
= 60.0;
41 //Called when the module has just been loaded
42 void Robot::on_module_loaded() {
43 register_for_event(ON_CONFIG_RELOAD
);
44 this->register_for_event(ON_GCODE_RECEIVED
);
45 this->register_for_event(ON_GET_PUBLIC_DATA
);
46 this->register_for_event(ON_SET_PUBLIC_DATA
);
49 this->on_config_reload(this);
51 // Make our 3 StepperMotors
52 this->alpha_stepper_motor
= this->kernel
->step_ticker
->add_stepper_motor( new StepperMotor(&alpha_step_pin
,&alpha_dir_pin
,&alpha_en_pin
) );
53 this->beta_stepper_motor
= this->kernel
->step_ticker
->add_stepper_motor( new StepperMotor(&beta_step_pin
, &beta_dir_pin
, &beta_en_pin
) );
54 this->gamma_stepper_motor
= this->kernel
->step_ticker
->add_stepper_motor( new StepperMotor(&gamma_step_pin
,&gamma_dir_pin
,&gamma_en_pin
) );
58 void Robot::on_config_reload(void* argument
){
60 // Arm solutions are used to convert positions in millimeters into position in steps for each stepper motor.
61 // While for a cartesian arm solution, this is a simple multiplication, in other, less simple cases, there is some serious math to be done.
62 // To make adding those solution easier, they have their own, separate object.
63 // Here we read the config to find out which arm solution to use
64 if (this->arm_solution
) delete this->arm_solution
;
65 int solution_checksum
= get_checksum(this->kernel
->config
->value(arm_solution_checksum
)->by_default("cartesian")->as_string());
66 // Note checksums are not const expressions when in debug mode, so don't use switch
67 if(solution_checksum
== hbot_checksum
) {
68 this->arm_solution
= new HBotSolution(this->kernel
->config
);
70 }else if(solution_checksum
== rostock_checksum
) {
71 this->arm_solution
= new RostockSolution(this->kernel
->config
);
73 }else if(solution_checksum
== delta_checksum
) {
74 // place holder for now
75 this->arm_solution
= new RostockSolution(this->kernel
->config
);
77 }else if(solution_checksum
== rotatable_cartesian_checksum
) {
78 this->arm_solution
= new RotatableCartesianSolution(this->kernel
->config
);
80 }else if(solution_checksum
== cartesian_checksum
) {
81 this->arm_solution
= new CartesianSolution(this->kernel
->config
);
84 this->arm_solution
= new CartesianSolution(this->kernel
->config
);
88 this->feed_rate
= this->kernel
->config
->value(default_feed_rate_checksum
)->by_default(100 )->as_number() / 60;
89 this->seek_rate
= this->kernel
->config
->value(default_seek_rate_checksum
)->by_default(100 )->as_number() / 60;
90 this->mm_per_line_segment
= this->kernel
->config
->value(mm_per_line_segment_checksum
)->by_default(0.0 )->as_number();
91 this->delta_segments_per_second
= this->kernel
->config
->value(delta_segments_per_second_checksum
)->by_default(0.0 )->as_number();
92 this->mm_per_arc_segment
= this->kernel
->config
->value(mm_per_arc_segment_checksum
)->by_default(0.5 )->as_number();
93 this->arc_correction
= this->kernel
->config
->value(arc_correction_checksum
)->by_default(5 )->as_number();
94 this->max_speeds
[X_AXIS
] = this->kernel
->config
->value(x_axis_max_speed_checksum
)->by_default(60000 )->as_number();
95 this->max_speeds
[Y_AXIS
] = this->kernel
->config
->value(y_axis_max_speed_checksum
)->by_default(60000 )->as_number();
96 this->max_speeds
[Z_AXIS
] = this->kernel
->config
->value(z_axis_max_speed_checksum
)->by_default(300 )->as_number();
97 this->alpha_step_pin
.from_string( this->kernel
->config
->value(alpha_step_pin_checksum
)->by_default("2.0" )->as_string())->as_output();
98 this->alpha_dir_pin
.from_string( this->kernel
->config
->value(alpha_dir_pin_checksum
)->by_default("0.5" )->as_string())->as_output();
99 this->alpha_en_pin
.from_string( this->kernel
->config
->value(alpha_en_pin_checksum
)->by_default("0.4" )->as_string())->as_output();
100 this->beta_step_pin
.from_string( this->kernel
->config
->value(beta_step_pin_checksum
)->by_default("2.1" )->as_string())->as_output();
101 this->gamma_step_pin
.from_string( this->kernel
->config
->value(gamma_step_pin_checksum
)->by_default("2.2" )->as_string())->as_output();
102 this->gamma_dir_pin
.from_string( this->kernel
->config
->value(gamma_dir_pin_checksum
)->by_default("0.20" )->as_string())->as_output();
103 this->gamma_en_pin
.from_string( this->kernel
->config
->value(gamma_en_pin_checksum
)->by_default("0.19" )->as_string())->as_output();
104 this->beta_dir_pin
.from_string( this->kernel
->config
->value(beta_dir_pin_checksum
)->by_default("0.11" )->as_string())->as_output();
105 this->beta_en_pin
.from_string( this->kernel
->config
->value(beta_en_pin_checksum
)->by_default("0.10" )->as_string())->as_output();
109 void Robot::on_get_public_data(void* argument
){
110 PublicDataRequest
* pdr
= static_cast<PublicDataRequest
*>(argument
);
112 if(!pdr
->starts_with(robot_checksum
)) return;
114 if(pdr
->second_element_is(speed_override_percent_checksum
)) {
115 static double return_data
= 100*60/seconds_per_minute
;
116 pdr
->set_data_ptr(&return_data
);
119 }else if(pdr
->second_element_is(current_position_checksum
)) {
120 static double return_data
[3];
121 return_data
[0]= from_millimeters(this->current_position
[0]);
122 return_data
[1]= from_millimeters(this->current_position
[1]);
123 return_data
[2]= from_millimeters(this->current_position
[2]);
125 pdr
->set_data_ptr(&return_data
);
130 void Robot::on_set_public_data(void* argument
){
131 PublicDataRequest
* pdr
= static_cast<PublicDataRequest
*>(argument
);
133 if(!pdr
->starts_with(robot_checksum
)) return;
135 if(pdr
->second_element_is(speed_override_percent_checksum
)) {
136 double t
= *static_cast<double*>(pdr
->get_data_ptr());
137 seconds_per_minute
= t
* 0.6;
142 //A GCode has been received
143 //See if the current Gcode line has some orders for us
144 void Robot::on_gcode_received(void * argument
){
145 Gcode
* gcode
= static_cast<Gcode
*>(argument
);
147 //Temp variables, constant properties are stored in the object
148 uint8_t next_action
= NEXT_ACTION_DEFAULT
;
149 this->motion_mode
= -1;
151 //G-letter Gcodes are mostly what the Robot module is interrested in, other modules also catch the gcode event and do stuff accordingly
154 case 0: this->motion_mode
= MOTION_MODE_SEEK
; gcode
->mark_as_taken(); break;
155 case 1: this->motion_mode
= MOTION_MODE_LINEAR
; gcode
->mark_as_taken(); break;
156 case 2: this->motion_mode
= MOTION_MODE_CW_ARC
; gcode
->mark_as_taken(); break;
157 case 3: this->motion_mode
= MOTION_MODE_CCW_ARC
; gcode
->mark_as_taken(); break;
158 case 17: this->select_plane(X_AXIS
, Y_AXIS
, Z_AXIS
); gcode
->mark_as_taken(); break;
159 case 18: this->select_plane(X_AXIS
, Z_AXIS
, Y_AXIS
); gcode
->mark_as_taken(); break;
160 case 19: this->select_plane(Y_AXIS
, Z_AXIS
, X_AXIS
); gcode
->mark_as_taken(); break;
161 case 20: this->inch_mode
= true; gcode
->mark_as_taken(); break;
162 case 21: this->inch_mode
= false; gcode
->mark_as_taken(); break;
163 case 90: this->absolute_mode
= true; gcode
->mark_as_taken(); break;
164 case 91: this->absolute_mode
= false; gcode
->mark_as_taken(); break;
166 if(gcode
->get_num_args() == 0){
167 clear_vector(this->last_milestone
);
169 for (char letter
= 'X'; letter
<= 'Z'; letter
++){
170 if ( gcode
->has_letter(letter
) )
171 this->last_milestone
[letter
-'X'] = this->to_millimeters(gcode
->get_value(letter
));
174 memcpy(this->current_position
, this->last_milestone
, sizeof(double)*3); // current_position[] = last_milestone[];
175 this->arm_solution
->millimeters_to_steps(this->current_position
, this->kernel
->planner
->position
);
176 gcode
->mark_as_taken();
177 return; // TODO: Wait until queue empty
180 }else if( gcode
->has_m
){
182 case 92: // M92 - set steps per mm
184 this->arm_solution
->get_steps_per_millimeter(steps
);
185 if (gcode
->has_letter('X'))
186 steps
[0] = this->to_millimeters(gcode
->get_value('X'));
187 if (gcode
->has_letter('Y'))
188 steps
[1] = this->to_millimeters(gcode
->get_value('Y'));
189 if (gcode
->has_letter('Z'))
190 steps
[2] = this->to_millimeters(gcode
->get_value('Z'));
191 if (gcode
->has_letter('F'))
192 seconds_per_minute
= gcode
->get_value('F');
193 this->arm_solution
->set_steps_per_millimeter(steps
);
194 // update current position in steps
195 this->arm_solution
->millimeters_to_steps(this->current_position
, this->kernel
->planner
->position
);
196 gcode
->stream
->printf("X:%g Y:%g Z:%g F:%g ", steps
[0], steps
[1], steps
[2], seconds_per_minute
);
197 gcode
->add_nl
= true;
198 gcode
->mark_as_taken();
200 case 114: gcode
->stream
->printf("C: X:%1.3f Y:%1.3f Z:%1.3f ",
201 from_millimeters(this->current_position
[0]),
202 from_millimeters(this->current_position
[1]),
203 from_millimeters(this->current_position
[2]));
204 gcode
->add_nl
= true;
205 gcode
->mark_as_taken();
207 case 220: // M220 - speed override percentage
208 gcode
->mark_as_taken();
209 if (gcode
->has_letter('S'))
211 double factor
= gcode
->get_value('S');
212 // enforce minimum 1% speed
215 seconds_per_minute
= factor
* 0.6;
219 if( this->motion_mode
< 0)
223 double target
[3], offset
[3];
224 clear_vector(target
); clear_vector(offset
);
226 memcpy(target
, this->current_position
, sizeof(target
)); //default to last target
228 for(char letter
= 'I'; letter
<= 'K'; letter
++){ if( gcode
->has_letter(letter
) ){ offset
[letter
-'I'] = this->to_millimeters(gcode
->get_value(letter
)); } }
229 for(char letter
= 'X'; letter
<= 'Z'; letter
++){ if( gcode
->has_letter(letter
) ){ target
[letter
-'X'] = this->to_millimeters(gcode
->get_value(letter
)) + ( this->absolute_mode
? 0 : target
[letter
-'X']); } }
231 if( gcode
->has_letter('F') )
233 if( this->motion_mode
== MOTION_MODE_SEEK
)
234 this->seek_rate
= this->to_millimeters( gcode
->get_value('F') ) / 60.0;
236 this->feed_rate
= this->to_millimeters( gcode
->get_value('F') ) / 60.0;
239 //Perform any physical actions
240 switch( next_action
){
241 case NEXT_ACTION_DEFAULT
:
242 switch(this->motion_mode
){
243 case MOTION_MODE_CANCEL
: break;
244 case MOTION_MODE_SEEK
: this->append_line(gcode
, target
, this->seek_rate
); break;
245 case MOTION_MODE_LINEAR
: this->append_line(gcode
, target
, this->feed_rate
); break;
246 case MOTION_MODE_CW_ARC
: case MOTION_MODE_CCW_ARC
: this->compute_arc(gcode
, offset
, target
); break;
251 // As far as the parser is concerned, the position is now == target. In reality the
252 // motion control system might still be processing the action and the real tool position
253 // in any intermediate location.
254 memcpy(this->current_position
, target
, sizeof(double)*3); // this->position[] = target[];
258 // We received a new gcode, and one of the functions
259 // determined the distance for that given gcode. So now we can attach this gcode to the right block
261 void Robot::distance_in_gcode_is_known(Gcode
* gcode
){
263 //If the queue is empty, execute immediatly, otherwise attach to the last added block
264 if( this->kernel
->conveyor
->queue
.size() == 0 ){
265 this->kernel
->call_event(ON_GCODE_EXECUTE
, gcode
);
267 Block
* block
= this->kernel
->conveyor
->queue
.get_ref( this->kernel
->conveyor
->queue
.size() - 1 );
268 block
->append_gcode(gcode
);
273 // Reset the position for all axes ( used in homing and G92 stuff )
274 void Robot::reset_axis_position(double position
, int axis
) {
275 this->last_milestone
[axis
] = this->current_position
[axis
] = position
;
276 this->arm_solution
->millimeters_to_steps(this->current_position
, this->kernel
->planner
->position
);
280 // Convert target from millimeters to steps, and append this to the planner
281 void Robot::append_milestone( double target
[], double rate
){
282 int steps
[3]; //Holds the result of the conversion
284 // We use an arm solution object so exotic arm solutions can be used and neatly abstracted
285 this->arm_solution
->millimeters_to_steps( target
, steps
);
288 for(int axis
=X_AXIS
;axis
<=Z_AXIS
;axis
++){deltas
[axis
]=target
[axis
]-this->last_milestone
[axis
];}
290 // Compute how long this move moves, so we can attach it to the block for later use
291 double millimeters_of_travel
= sqrt( pow( deltas
[X_AXIS
], 2 ) + pow( deltas
[Y_AXIS
], 2 ) + pow( deltas
[Z_AXIS
], 2 ) );
293 // Do not move faster than the configured limits
294 for(int axis
=X_AXIS
;axis
<=Z_AXIS
;axis
++){
295 if( this->max_speeds
[axis
] > 0 ){
296 double axis_speed
= ( fabs(deltas
[axis
]) / ( millimeters_of_travel
/ rate
)) * seconds_per_minute
;
297 if( axis_speed
> this->max_speeds
[axis
] ){
298 rate
= rate
* ( this->max_speeds
[axis
] / axis_speed
);
303 // Append the block to the planner
304 this->kernel
->planner
->append_block( steps
, rate
* seconds_per_minute
, millimeters_of_travel
, deltas
);
306 // Update the last_milestone to the current target for the next time we use last_milestone
307 memcpy(this->last_milestone
, target
, sizeof(double)*3); // this->last_milestone[] = target[];
311 // Append a move to the queue ( cutting it into segments if needed )
312 void Robot::append_line(Gcode
* gcode
, double target
[], double rate
){
314 // Find out the distance for this gcode
315 gcode
->millimeters_of_travel
= sqrt( pow( target
[X_AXIS
]-this->current_position
[X_AXIS
], 2 ) + pow( target
[Y_AXIS
]-this->current_position
[Y_AXIS
], 2 ) + pow( target
[Z_AXIS
]-this->current_position
[Z_AXIS
], 2 ) );
317 // We ignore non-moves ( for example, extruder moves are not XYZ moves )
318 if( gcode
->millimeters_of_travel
< 0.0001 ){ return; }
320 // Mark the gcode as having a known distance
321 this->distance_in_gcode_is_known( gcode
);
323 // We cut the line into smaller segments. This is not usefull in a cartesian robot, but necessary for robots with rotational axes.
324 // In cartesian robot, a high "mm_per_line_segment" setting will prevent waste.
325 // In delta robots either mm_per_line_segment can be used OR delta_segments_per_second The latter is more efficient and avoids splitting fast long lines into very small segments, like initial z move to 0, it is what Johanns Marlin delta port does
328 if(this->delta_segments_per_second
> 1.0) {
329 // enabled if set to something > 1, it is set to 0.0 by default
330 // segment based on current speed and requested segments per second
331 // the faster the travel speed the fewer segments needed
332 // NOTE rate is mm/sec and we take into account any speed override
333 float seconds
= 60.0/seconds_per_minute
* gcode
->millimeters_of_travel
/ rate
;
334 segments
= max(1, ceil(this->delta_segments_per_second
* seconds
));
335 // TODO if we are only moving in Z on a delta we don't really need to segment at all
338 if(this->mm_per_line_segment
== 0.0){
339 segments
= 1; // don't split it up
341 segments
= ceil( gcode
->millimeters_of_travel
/ this->mm_per_line_segment
);
345 // A vector to keep track of the endpoint of each segment
346 double temp_target
[3];
348 memcpy( temp_target
, this->current_position
, sizeof(double)*3); // temp_target[] = this->current_position[];
351 for( int i
=0; i
<segments
-1; i
++ ){
352 for(int axis
=X_AXIS
; axis
<= Z_AXIS
; axis
++ ){ temp_target
[axis
] += ( target
[axis
]-this->current_position
[axis
] )/segments
; }
353 // Append the end of this segment to the queue
354 this->append_milestone(temp_target
, rate
);
357 // Append the end of this full move to the queue
358 this->append_milestone(target
, rate
);
362 // Append an arc to the queue ( cutting it into segments as needed )
363 void Robot::append_arc(Gcode
* gcode
, double target
[], double offset
[], double radius
, bool is_clockwise
){
366 double center_axis0
= this->current_position
[this->plane_axis_0
] + offset
[this->plane_axis_0
];
367 double center_axis1
= this->current_position
[this->plane_axis_1
] + offset
[this->plane_axis_1
];
368 double linear_travel
= target
[this->plane_axis_2
] - this->current_position
[this->plane_axis_2
];
369 double r_axis0
= -offset
[this->plane_axis_0
]; // Radius vector from center to current location
370 double r_axis1
= -offset
[this->plane_axis_1
];
371 double rt_axis0
= target
[this->plane_axis_0
] - center_axis0
;
372 double rt_axis1
= target
[this->plane_axis_1
] - center_axis1
;
374 // CCW angle between position and target from circle center. Only one atan2() trig computation required.
375 double angular_travel
= atan2(r_axis0
*rt_axis1
-r_axis1
*rt_axis0
, r_axis0
*rt_axis0
+r_axis1
*rt_axis1
);
376 if (angular_travel
< 0) { angular_travel
+= 2*M_PI
; }
377 if (is_clockwise
) { angular_travel
-= 2*M_PI
; }
379 // Find the distance for this gcode
380 gcode
->millimeters_of_travel
= hypot(angular_travel
*radius
, fabs(linear_travel
));
382 // We don't care about non-XYZ moves ( for example the extruder produces some of those )
383 if( gcode
->millimeters_of_travel
< 0.0001 ){ return; }
385 // Mark the gcode as having a known distance
386 this->distance_in_gcode_is_known( gcode
);
388 // Figure out how many segments for this gcode
389 uint16_t segments
= floor(gcode
->millimeters_of_travel
/this->mm_per_arc_segment
);
391 double theta_per_segment
= angular_travel
/segments
;
392 double linear_per_segment
= linear_travel
/segments
;
394 /* Vector rotation by transformation matrix: r is the original vector, r_T is the rotated vector,
395 and phi is the angle of rotation. Based on the solution approach by Jens Geisler.
396 r_T = [cos(phi) -sin(phi);
397 sin(phi) cos(phi] * r ;
398 For arc generation, the center of the circle is the axis of rotation and the radius vector is
399 defined from the circle center to the initial position. Each line segment is formed by successive
400 vector rotations. This requires only two cos() and sin() computations to form the rotation
401 matrix for the duration of the entire arc. Error may accumulate from numerical round-off, since
402 all double numbers are single precision on the Arduino. (True double precision will not have
403 round off issues for CNC applications.) Single precision error can accumulate to be greater than
404 tool precision in some cases. Therefore, arc path correction is implemented.
406 Small angle approximation may be used to reduce computation overhead further. This approximation
407 holds for everything, but very small circles and large mm_per_arc_segment values. In other words,
408 theta_per_segment would need to be greater than 0.1 rad and N_ARC_CORRECTION would need to be large
409 to cause an appreciable drift error. N_ARC_CORRECTION~=25 is more than small enough to correct for
410 numerical drift error. N_ARC_CORRECTION may be on the order a hundred(s) before error becomes an
411 issue for CNC machines with the single precision Arduino calculations.
412 This approximation also allows mc_arc to immediately insert a line segment into the planner
413 without the initial overhead of computing cos() or sin(). By the time the arc needs to be applied
414 a correction, the planner should have caught up to the lag caused by the initial mc_arc overhead.
415 This is important when there are successive arc motions.
417 // Vector rotation matrix values
418 double cos_T
= 1-0.5*theta_per_segment
*theta_per_segment
; // Small angle approximation
419 double sin_T
= theta_per_segment
;
421 double arc_target
[3];
428 // Initialize the linear axis
429 arc_target
[this->plane_axis_2
] = this->current_position
[this->plane_axis_2
];
431 for (i
= 1; i
<segments
; i
++) { // Increment (segments-1)
433 if (count
< this->arc_correction
) {
434 // Apply vector rotation matrix
435 r_axisi
= r_axis0
*sin_T
+ r_axis1
*cos_T
;
436 r_axis0
= r_axis0
*cos_T
- r_axis1
*sin_T
;
440 // Arc correction to radius vector. Computed only every N_ARC_CORRECTION increments.
441 // Compute exact location by applying transformation matrix from initial radius vector(=-offset).
442 cos_Ti
= cos(i
*theta_per_segment
);
443 sin_Ti
= sin(i
*theta_per_segment
);
444 r_axis0
= -offset
[this->plane_axis_0
]*cos_Ti
+ offset
[this->plane_axis_1
]*sin_Ti
;
445 r_axis1
= -offset
[this->plane_axis_0
]*sin_Ti
- offset
[this->plane_axis_1
]*cos_Ti
;
449 // Update arc_target location
450 arc_target
[this->plane_axis_0
] = center_axis0
+ r_axis0
;
451 arc_target
[this->plane_axis_1
] = center_axis1
+ r_axis1
;
452 arc_target
[this->plane_axis_2
] += linear_per_segment
;
454 // Append this segment to the queue
455 this->append_milestone(arc_target
, this->feed_rate
);
459 // Ensure last segment arrives at target location.
460 this->append_milestone(target
, this->feed_rate
);
463 // Do the math for an arc and add it to the queue
464 void Robot::compute_arc(Gcode
* gcode
, double offset
[], double target
[]){
467 double radius
= hypot(offset
[this->plane_axis_0
], offset
[this->plane_axis_1
]);
469 // Set clockwise/counter-clockwise sign for mc_arc computations
470 bool is_clockwise
= false;
471 if( this->motion_mode
== MOTION_MODE_CW_ARC
){ is_clockwise
= true; }
474 this->append_arc(gcode
, target
, offset
, radius
, is_clockwise
);
479 double Robot::theta(double x
, double y
){
480 double t
= atan(x
/fabs(y
));
481 if (y
>0) {return(t
);} else {if (t
>0){return(M_PI
-t
);} else {return(-M_PI
-t
);}}
484 void Robot::select_plane(uint8_t axis_0
, uint8_t axis_1
, uint8_t axis_2
){
485 this->plane_axis_0
= axis_0
;
486 this->plane_axis_1
= axis_1
;
487 this->plane_axis_2
= axis_2
;