Merge pull request #385 from wolfmanjm/upstreamedge
[clinton/Smoothieware.git] / src / modules / tools / zprobe / ZProbe.cpp
CommitLineData
88443c6b
JM
1/*
2 This file is part of Smoothie (http://smoothieware.org/). The motion control part is heavily based on Grbl (https://github.com/simen/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/>.
6*/
7
8#include "ZProbe.h"
9
10#include "Kernel.h"
11#include "BaseSolution.h"
12#include "Config.h"
13#include "Robot.h"
14#include "StepperMotor.h"
15#include "StreamOutputPool.h"
16#include "Gcode.h"
17#include "Conveyor.h"
18#include "Stepper.h"
19#include "checksumm.h"
20#include "ConfigValue.h"
21#include "SlowTicker.h"
22#include "Planner.h"
037c350d 23#include "SerialMessage.h"
88443c6b 24
681a62d7
JM
25#include <tuple>
26#include <algorithm>
27
88443c6b
JM
28#define zprobe_checksum CHECKSUM("zprobe")
29#define enable_checksum CHECKSUM("enable")
30#define probe_pin_checksum CHECKSUM("probe_pin")
31#define debounce_count_checksum CHECKSUM("debounce_count")
681a62d7
JM
32#define slow_feedrate_checksum CHECKSUM("slow_feedrate")
33#define fast_feedrate_checksum CHECKSUM("fast_feedrate")
34#define probe_radius_checksum CHECKSUM("probe_radius")
681a62d7 35#define probe_height_checksum CHECKSUM("probe_height")
88443c6b 36
681a62d7 37// from endstop section
b7cd847e 38#define delta_homing_checksum CHECKSUM("delta_homing")
681a62d7 39#define arm_radius_checksum CHECKSUM("arm_radius")
b7cd847e 40
88443c6b
JM
41#define alpha_steps_per_mm_checksum CHECKSUM("alpha_steps_per_mm")
42#define beta_steps_per_mm_checksum CHECKSUM("beta_steps_per_mm")
43#define gamma_steps_per_mm_checksum CHECKSUM("gamma_steps_per_mm")
44
45#define X_AXIS 0
46#define Y_AXIS 1
47#define Z_AXIS 2
48
49void ZProbe::on_module_loaded()
50{
51 // if the module is disabled -> do nothing
52 this->enabled = THEKERNEL->config->value( zprobe_checksum, enable_checksum )->by_default(false)->as_bool();
53 if( !(this->enabled) ) {
54 // as this module is not needed free up the resource
55 delete this;
56 return;
57 }
681a62d7 58 this->running = false;
88443c6b
JM
59
60 // load settings
61 this->on_config_reload(this);
62 // register event-handlers
63 register_for_event(ON_CONFIG_RELOAD);
64 register_for_event(ON_GCODE_RECEIVED);
65 register_for_event(ON_IDLE);
66
67 THEKERNEL->slow_ticker->attach( THEKERNEL->stepper->acceleration_ticks_per_second , this, &ZProbe::acceleration_tick );
68}
69
70void ZProbe::on_config_reload(void *argument)
71{
681a62d7
JM
72 this->pin.from_string( THEKERNEL->config->value(zprobe_checksum, probe_pin_checksum)->by_default("nc" )->as_string())->as_input();
73 this->debounce_count = THEKERNEL->config->value(zprobe_checksum, debounce_count_checksum)->by_default(0 )->as_number();
74
037c350d
JM
75 // see what type of arm solution we need to use
76 this->is_delta = THEKERNEL->config->value(delta_homing_checksum)->by_default(false)->as_bool();
77 if(this->is_delta) {
78 // default is probably wrong
79 this->probe_radius = THEKERNEL->config->value(zprobe_checksum, probe_radius_checksum)->by_default(100.0F)->as_number();
80 }
681a62d7 81
681a62d7 82 this->probe_height = THEKERNEL->config->value(zprobe_checksum, probe_height_checksum)->by_default(5.0F)->as_number();
88443c6b
JM
83
84 this->steppers[0] = THEKERNEL->robot->alpha_stepper_motor;
85 this->steppers[1] = THEKERNEL->robot->beta_stepper_motor;
86 this->steppers[2] = THEKERNEL->robot->gamma_stepper_motor;
87
88 // we need to know steps per mm
681a62d7
JM
89 // FIXME we need to get this after config loaded from robot as the config settings can be overriden or trap M92
90 this->steps_per_mm[0] = THEKERNEL->config->value(alpha_steps_per_mm_checksum)->as_number();
91 this->steps_per_mm[1] = THEKERNEL->config->value(beta_steps_per_mm_checksum)->as_number();
92 this->steps_per_mm[2] = THEKERNEL->config->value(gamma_steps_per_mm_checksum)->as_number();
88443c6b 93
681a62d7
JM
94 this->slow_feedrate = THEKERNEL->config->value(zprobe_checksum, slow_feedrate_checksum)->by_default(5)->as_number(); // feedrate in mm/sec
95 this->fast_feedrate = THEKERNEL->config->value(zprobe_checksum, fast_feedrate_checksum)->by_default(100)->as_number(); // feedrate in mm/sec
88443c6b
JM
96}
97
681a62d7 98bool ZProbe::wait_for_probe(int steps[3])
88443c6b
JM
99{
100 unsigned int debounce = 0;
101 while(true) {
102 THEKERNEL->call_event(ON_IDLE);
103 // if no stepper is moving, moves are finished and there was no touch
104 if( !this->steppers[X_AXIS]->moving && !this->steppers[Y_AXIS]->moving && !this->steppers[Z_AXIS]->moving ) {
105 return false;
106 }
107
108 // if the touchprobe is active...
109 if( this->pin.get() ) {
110 //...increase debounce counter...
111 if( debounce < debounce_count) {
112 // ...but only if the counter hasn't reached the max. value
113 debounce++;
114 } else {
115 // ...otherwise stop the steppers, return its remaining steps
116 for( int i = X_AXIS; i <= Z_AXIS; i++ ) {
117 steps[i] = 0;
118 if ( this->steppers[i]->moving ) {
119 steps[i] = this->steppers[i]->stepped;
120 this->steppers[i]->move(0, 0);
121 }
122 }
123 return true;
124 }
125 } else {
126 // The probe was not hit yet, reset debounce counter
127 debounce = 0;
128 }
129 }
130}
131
132void ZProbe::on_idle(void *argument)
133{
134}
135
136// single probe and report amount moved
681a62d7 137bool ZProbe::run_probe(int& steps, bool fast)
88443c6b
JM
138{
139 // Enable the motors
140 THEKERNEL->stepper->turn_enable_pins_on();
681a62d7 141 this->current_feedrate = (fast ? this->fast_feedrate : this->slow_feedrate) * this->steps_per_mm[Z_AXIS]; // steps/sec
88443c6b
JM
142
143 // move Z down
681a62d7 144 this->running = true;
88443c6b 145 this->steppers[Z_AXIS]->set_speed(0); // will be increased by acceleration tick
681a62d7 146 this->steppers[Z_AXIS]->move(true, 1000 * this->steps_per_mm[Z_AXIS]); // always probes down, no more than 1000mm TODO should be 2*maxz
b7cd847e
JM
147 if(this->is_delta) {
148 // for delta need to move all three actuators
149 this->steppers[X_AXIS]->set_speed(0);
681a62d7 150 this->steppers[X_AXIS]->move(true, 1000 * this->steps_per_mm[X_AXIS]);
b7cd847e 151 this->steppers[Y_AXIS]->set_speed(0);
681a62d7 152 this->steppers[Y_AXIS]->move(true, 1000 * this->steps_per_mm[Y_AXIS]);
b7cd847e
JM
153 }
154
681a62d7
JM
155 int s[3];
156 bool r = wait_for_probe(s);
157 steps= s[2]; // only need z
158 this->running = false;
88443c6b
JM
159 return r;
160}
161
681a62d7
JM
162bool ZProbe::return_probe(int steps)
163{
164 // move probe back to where it was
165 this->current_feedrate = this->fast_feedrate * this->steps_per_mm[Z_AXIS]; // feedrate in steps/sec
166 bool dir= steps < 0;
167 steps= abs(steps);
168
169 this->running = true;
170 this->steppers[Z_AXIS]->set_speed(0); // will be increased by acceleration tick
171 this->steppers[Z_AXIS]->move(dir, steps);
172 if(this->is_delta) {
173 this->steppers[X_AXIS]->set_speed(0);
174 this->steppers[X_AXIS]->move(dir, steps);
175 this->steppers[Y_AXIS]->set_speed(0);
176 this->steppers[Y_AXIS]->move(dir, steps);
177 }
178 while(this->steppers[X_AXIS]->moving || this->steppers[Y_AXIS]->moving || this->steppers[Z_AXIS]->moving) {
179 // wait for it to complete
180 THEKERNEL->call_event(ON_IDLE);
181 }
182
183 this->running = false;
184
185 return true;
186}
187
188// calculate the X and Y positions for the three towers given the radius from the center
189static std::tuple<float, float, float, float, float, float> getCoordinates(float radius)
190{
191 float px = 0.866F * radius; // ~sin(60)
192 float py = 0.5F * radius; // cos(60)
193 float t1x = -px, t1y = -py; // X Tower
194 float t2x = px, t2y = -py; // Y Tower
195 float t3x = 0.0F, t3y = radius; // Z Tower
196 return std::make_tuple(t1x, t1y, t2x, t2y, t3x, t3y);
197}
198
199bool ZProbe::probe_delta_tower(int& steps, float x, float y)
200{
201 int s;
202 // move to tower
203 coordinated_move(x, y, NAN, this->fast_feedrate);
204 if(!run_probe(s)) return false;
205
206 // return to original Z
207 return_probe(s);
208 steps= s;
209
210 return true;
211}
212
fc7b9a7b
JM
213/* Run a calibration routine for a delta
214 1. Home
215 2. probe for z bed
681a62d7
JM
216 3. probe initial tower positions
217 4. set initial trims such that trims will be minimal negative values
218 5. home, probe three towers again
219 6. calculate trim offset and apply to all trims
220 7. repeat 5, 6 4 times to converge on a solution
221 8. home, Probe center
fc7b9a7b
JM
222 9. calculate delta radius and apply it
223 10. check level
224*/
225
037c350d 226bool ZProbe::calibrate_delta_endstops(Gcode *gcode)
fc7b9a7b 227{
037c350d
JM
228 // get probe points
229 float t1x, t1y, t2x, t2y, t3x, t3y;
230 std::tie(t1x, t1y, t2x, t2y, t3x, t3y) = getCoordinates(this->probe_radius);
231
681a62d7
JM
232 // zero trim values
233 set_trim(0, 0, 0, &(StreamOutput::NullStream));
234
235 // home
236 home();
237
238 // find bed, run at fast rate
239 int s;
240 if(!run_probe(s, true)) return false;
241
242 // how far to move down from home before probe
243 int probestart = s - (this->probe_height*this->steps_per_mm[Z_AXIS]);
244 gcode->stream->printf("Probe start ht is %f mm\n", probestart/this->steps_per_mm[Z_AXIS]);
245
681a62d7
JM
246
247 // move to start position
248 home();
249 return_probe(-probestart);
250
037c350d
JM
251
252 gcode->stream->printf("Calibrating Endstops\n");
681a62d7
JM
253 // get initial probes
254 // probe the base of the X tower
255 if(!probe_delta_tower(s, t1x, t1y)) return false;
256 float t1z= s / this->steps_per_mm[Z_AXIS];
257 gcode->stream->printf("T1-1 Z:%1.4f C:%d\n", t1z, s);
258
259 // probe the base of the Y tower
260 if(!probe_delta_tower(s, t2x, t2y)) return false;
261 float t2z= s / this->steps_per_mm[Z_AXIS];
262 gcode->stream->printf("T2-1 Z:%1.4f C:%d\n", t2z, s);
263
264 // probe the base of the Z tower
265 if(!probe_delta_tower(s, t3x, t3y)) return false;
266 float t3z= s / this->steps_per_mm[Z_AXIS];
267 gcode->stream->printf("T3-1 Z:%1.4f C:%d\n", t3z, s);
268
269 float trimscale= 1.2522F; // empirically determined
270
271 // set initial trims to worst case so we always have a negative trim
272 float min= std::min({t1z, t2z, t3z});
273 float trimx= (min-t1z)*trimscale, trimy= (min-t2z)*trimscale, trimz= (min-t3z)*trimscale;
274
275 // set initial trim
276 set_trim(trimx, trimy, trimz, gcode->stream);
277
278 for (int i = 1; i <= 4; ++i) {
279 // home and move probe to start position just above the bed
280 home();
281 return_probe(-probestart);
282
283 // probe the base of the X tower
284 if(!probe_delta_tower(s, t1x, t1y)) return false;
285 t1z= s / this->steps_per_mm[Z_AXIS];
286 gcode->stream->printf("T1-2-%d Z:%1.4f C:%d\n", i, t1z, s);
287
288 // probe the base of the Y tower
289 if(!probe_delta_tower(s, t2x, t2y)) return false;
290 t2z= s / this->steps_per_mm[Z_AXIS];
291 gcode->stream->printf("T2-2-%d Z:%1.4f C:%d\n", i, t2z, s);
292
293 // probe the base of the Z tower
294 if(!probe_delta_tower(s, t3x, t3y)) return false;
295 t3z= s / this->steps_per_mm[Z_AXIS];
296 gcode->stream->printf("T3-2-%d Z:%1.4f C:%d\n", i, t3z, s);
297
298 auto mm= std::minmax({t1z, t2z, t3z});
299 if((mm.second-mm.first) < 0.03F) break; // probably as good as it gets, TODO set 0.02 as config value
300
301 // set new trim values based on min difference
302 min= mm.first;
303 trimx += (min-t1z)*trimscale;
304 trimy += (min-t2z)*trimscale;
305 trimz += (min-t3z)*trimscale;
306
307 // set trim
308 set_trim(trimx, trimy, trimz, gcode->stream);
309
310 // flush the output
311 THEKERNEL->call_event(ON_IDLE);
312 }
313
314 // move probe to start position just above the bed
315 home();
316 return_probe(-probestart);
317
318 // probe the base of the three towers again to see if we are level
319 int dx= 0, dy= 0, dz= 0;
320 if(!probe_delta_tower(dx, t1x, t1y)) return false;
321 gcode->stream->printf("T1-final Z:%1.4f C:%d\n", dx / this->steps_per_mm[Z_AXIS], dx);
322 if(!probe_delta_tower(dy, t2x, t2y)) return false;
323 gcode->stream->printf("T2-final Z:%1.4f C:%d\n", dy / this->steps_per_mm[Z_AXIS], dy);
324 if(!probe_delta_tower(dz, t3x, t3y)) return false;
325 gcode->stream->printf("T3-final Z:%1.4f C:%d\n", dz / this->steps_per_mm[Z_AXIS], dz);
326
327 // compare the three and report
328 auto mm= std::minmax({dx, dy, dz});
329 gcode->stream->printf("max endstop delta= %f\n", (mm.second-mm.first)/this->steps_per_mm[Z_AXIS]);
330
037c350d
JM
331 return true;
332}
333
334bool ZProbe::calibrate_delta_radius(Gcode *gcode)
335{
336 gcode->stream->printf("Calibrating delta radius\n");
337
338 // get probe points
339 float t1x, t1y, t2x, t2y, t3x, t3y;
340 std::tie(t1x, t1y, t2x, t2y, t3x, t3y) = getCoordinates(this->probe_radius);
341
342 home();
343 // find bed, then move to a point 5mm above it
344 int s;
345 if(!run_probe(s, true)) return false;
346 float bedht= s/this->steps_per_mm[Z_AXIS] - this->probe_height; // distance to move from home to 5mm above bed
347 gcode->stream->printf("Bed ht is %f mm\n", bedht);
348
349 home();
350 coordinated_move(NAN, NAN, -bedht, this->fast_feedrate, true); // do a relative move from home to the point above the bed
351
352 // probe the base of the three towers to get reference point at this Z height
353 int dx= 0, dy= 0, dz= 0, dc= 0;
354 if(!probe_delta_tower(dx, t1x, t1y)) return false;
355 gcode->stream->printf("T1 Z:%1.3f C:%d\n", dx / this->steps_per_mm[Z_AXIS], dx);
356 if(!probe_delta_tower(dy, t2x, t2y)) return false;
357 gcode->stream->printf("T2 Z:%1.3f C:%d\n", dy / this->steps_per_mm[Z_AXIS], dy);
358 if(!probe_delta_tower(dz, t3x, t3y)) return false;
359 gcode->stream->printf("T3 Z:%1.3f C:%d\n", dz / this->steps_per_mm[Z_AXIS], dz);
681a62d7 360 if(!probe_delta_tower(dc, 0, 0)) return false;
037c350d 361 gcode->stream->printf("CT Z:%1.3f C:%d\n", dc / this->steps_per_mm[Z_AXIS], dc);
681a62d7 362
037c350d 363 float cmm= dc / this->steps_per_mm[Z_AXIS];
681a62d7 364
037c350d
JM
365 // get current delta radius
366 float delta_radius= 0.0F;
367 BaseSolution::arm_options_t options;
368 if(THEKERNEL->robot->arm_solution->get_optional(options)) {
369 delta_radius= options['R'];
370 }
371 if(delta_radius == 0.0F) {
372 gcode->stream->printf("This appears to not be a delta arm solution\n");
373 return false;
374 }
375 options.clear();
fc7b9a7b 376
037c350d
JM
377 // probe t1, but use coordinated moves, probing center won't change
378 float drinc= 2.5F; // approx
379 for (int i = 1; i <= 10; ++i) {
380 // set the new delta radius
381 options['R']= delta_radius;
382 THEKERNEL->robot->arm_solution->set_optional(options);
383 gcode->stream->printf("Setting delta radius to: %1.4f\n", delta_radius);
384
385 home();
386 coordinated_move(NAN, NAN, -bedht, this->fast_feedrate, true); // needs to be a relative coordinated move
387 if(!probe_delta_tower(dx, t1x, t1y)) return false;
388
389 // now look at the difference and reduce it by adjusting delta radius
390 float m= dx / this->steps_per_mm[Z_AXIS];
391 float d= cmm-m;
392 gcode->stream->printf("T1-%d Z:%1.4f C:%d delta: %1.3f\n", i, m, dx, d);
393 if(abs(d) < 0.03F) break; // resolution of success TODO should be in config
394 // increase delta radius to adjust for low center
395 // decrease delta radius to adjust for high center
396 delta_radius += (d*drinc);
397 }
fc7b9a7b
JM
398 return true;
399}
400
88443c6b
JM
401void ZProbe::on_gcode_received(void *argument)
402{
403 Gcode *gcode = static_cast<Gcode *>(argument);
88443c6b
JM
404
405 if( gcode->has_g) {
406 // G code processing
681a62d7 407 if( gcode->g == 30 ) { // simple Z probe
bd96f4d7 408 gcode->mark_as_taken();
88443c6b
JM
409 // first wait for an empty queue i.e. no moves left
410 THEKERNEL->conveyor->wait_for_empty_queue();
411
681a62d7
JM
412 int steps;
413 if(run_probe(steps)) {
414 gcode->stream->printf("Z:%1.4f C:%d\n", steps / this->steps_per_mm[Z_AXIS], steps);
bd96f4d7
JM
415 // move back to where it started, unless a Z is specified
416 if(gcode->has_letter('Z')) {
417 // set Z to the specified value, and leave probe where it is
418 THEKERNEL->robot->reset_axis_position(gcode->get_value('Z'), Z_AXIS);
681a62d7
JM
419 } else {
420 return_probe(steps);
bd96f4d7 421 }
681a62d7 422 } else {
bd96f4d7 423 gcode->stream->printf("ZProbe not triggered\n");
88443c6b 424 }
fc7b9a7b 425
681a62d7
JM
426 } else if( gcode->g == 32 ) { // auto calibration for delta, Z bed mapping for cartesian
427 // first wait for an empty queue i.e. no moves left
428 THEKERNEL->conveyor->wait_for_empty_queue();
fc7b9a7b
JM
429 gcode->mark_as_taken();
430 if(is_delta) {
037c350d
JM
431 if(!gcode->has_letter('R')){
432 if(!calibrate_delta_endstops(gcode)) {
433 gcode->stream->printf("Calibration failed to complete, probe not triggered\n");
434 return;
435 }
681a62d7 436 }
037c350d
JM
437 if(!gcode->has_letter('E')){
438 if(!calibrate_delta_radius(gcode)) {
439 gcode->stream->printf("Calibration failed to complete, probe not triggered\n");
440 return;
441 }
442 }
443 gcode->stream->printf("Calibration complete, save settings with M500\n");
444
681a62d7
JM
445 } else {
446 // TODO create Z height map for bed
447 gcode->stream->printf("Not supported yet\n");
fc7b9a7b 448 }
88443c6b
JM
449 }
450
451 } else if(gcode->has_m) {
452 // M code processing here
bd96f4d7 453 if(gcode->m == 119) {
681a62d7 454 int c = this->pin.get();
bd96f4d7
JM
455 gcode->stream->printf(" Probe: %d", c);
456 gcode->add_nl = true;
457 gcode->mark_as_taken();
681a62d7 458
bd96f4d7 459 }
88443c6b
JM
460 }
461}
462
463#define max(a,b) (((a) > (b)) ? (a) : (b))
464// Called periodically to change the speed to match acceleration
465uint32_t ZProbe::acceleration_tick(uint32_t dummy)
466{
467 if(!this->running) return(0); // nothing to do
468
469 // foreach stepper that is moving
b7cd847e 470 for ( int c = X_AXIS; c <= Z_AXIS; c++ ) {
88443c6b
JM
471 if( !this->steppers[c]->moving ) continue;
472
473 uint32_t current_rate = this->steppers[c]->steps_per_second;
681a62d7 474 uint32_t target_rate = int(floor(this->current_feedrate));
88443c6b 475
681a62d7
JM
476 if( current_rate < target_rate ) {
477 uint32_t rate_increase = int(floor((THEKERNEL->planner->acceleration / THEKERNEL->stepper->acceleration_ticks_per_second) * this->steps_per_mm[c]));
88443c6b
JM
478 current_rate = min( target_rate, current_rate + rate_increase );
479 }
681a62d7
JM
480 if( current_rate > target_rate ) {
481 current_rate = target_rate;
482 }
88443c6b
JM
483
484 // steps per second
485 this->steppers[c]->set_speed(max(current_rate, THEKERNEL->stepper->minimum_steps_per_second));
486 }
487
488 return 0;
489}
681a62d7
JM
490
491// issue a coordinated move directly to robot, and return when done
492// Only move the coordinates that are passed in as not nan
037c350d 493void ZProbe::coordinated_move(float x, float y, float z, float feedrate, bool relative)
681a62d7
JM
494{
495 char buf[32];
037c350d
JM
496 char cmd[64];
497
498 if(relative) strcpy(cmd, "G91 G0 ");
499 else strcpy(cmd, "G0 ");
500
681a62d7 501 if(!isnan(x)) {
037c350d 502 int n = snprintf(buf, sizeof(buf), " X%1.3f", x);
681a62d7
JM
503 strncat(cmd, buf, n);
504 }
505 if(!isnan(y)) {
037c350d 506 int n = snprintf(buf, sizeof(buf), " Y%1.3f", y);
681a62d7
JM
507 strncat(cmd, buf, n);
508 }
509 if(!isnan(z)) {
037c350d 510 int n = snprintf(buf, sizeof(buf), " Z%1.3f", z);
681a62d7
JM
511 strncat(cmd, buf, n);
512 }
513
514 // use specified feedrate (mm/sec)
037c350d 515 int n = snprintf(buf, sizeof(buf), " F%1.1f", feedrate * 60); // feed rate is converted to mm/min
681a62d7 516 strncat(cmd, buf, n);
037c350d
JM
517 if(relative) strcat(cmd, " G90");
518
519 //THEKERNEL->streams->printf("DEBUG: move: %s\n", cmd);
681a62d7 520
037c350d
JM
521 // send as a command line as may have multiple G codes in it
522 struct SerialMessage message;
523 message.message = cmd;
524 message.stream = &(StreamOutput::NullStream);
525 THEKERNEL->call_event(ON_CONSOLE_LINE_RECEIVED, &message );
681a62d7
JM
526 THEKERNEL->conveyor->wait_for_empty_queue();
527}
528
529// issue home command
530void ZProbe::home()
531{
532 Gcode gc("G28", &(StreamOutput::NullStream));
533 THEKERNEL->call_event(ON_GCODE_RECEIVED, &gc);
534}
535
536void ZProbe::set_trim(float x, float y, float z, StreamOutput *stream)
537{
538 char buf[40];
539 int n = snprintf(buf, sizeof(buf), "M666 X%1.8f Y%1.8f Z%1.8f", x, y, z);
540 Gcode gc(string(buf, n), stream);
541 THEKERNEL->call_event(ON_GCODE_RECEIVED, &gc);
542}