Subversion Repositories Tronxy-X3A-Marlin

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 ron 1
/**
2
 * Marlin 3D Printer Firmware
3
 * Copyright (C) 2016, 2017 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
4
 *
5
 * Based on Sprinter and grbl.
6
 * Copyright (C) 2011 Camiel Gubbels / Erik van der Zalm
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
 *
21
 */
22
 
23
//todo:  add support for multiple encoders on a single axis
24
//todo:    add z axis auto-leveling
25
//todo:  consolidate some of the related M codes?
26
//todo:  add endstop-replacement mode?
27
//todo:  try faster I2C speed; tweak TWI_FREQ (400000L, or faster?); or just TWBR = ((CPU_FREQ / 400000L) - 16) / 2;
28
//todo:    consider Marlin-optimized Wire library; i.e. MarlinWire, like MarlinSerial
29
 
30
 
31
#include "MarlinConfig.h"
32
 
33
#if ENABLED(I2C_POSITION_ENCODERS)
34
 
35
  #include "Marlin.h"
36
  #include "temperature.h"
37
  #include "stepper.h"
38
  #include "I2CPositionEncoder.h"
39
  #include "parser.h"
40
 
41
  #include <Wire.h>
42
 
43
 
44
  void I2CPositionEncoder::init(const uint8_t address, const AxisEnum axis) {
45
    encoderAxis = axis;
46
    i2cAddress = address;
47
 
48
    initialised++;
49
 
50
    SERIAL_ECHOPAIR("Setting up encoder on ", axis_codes[encoderAxis]);
51
    SERIAL_ECHOLNPAIR(" axis, addr = ", address);
52
 
53
    position = get_position();
54
  }
55
 
56
  void I2CPositionEncoder::update() {
57
    if (!initialised || !homed || !active) return; //check encoder is set up and active
58
 
59
    position = get_position();
60
 
61
    //we don't want to stop things just because the encoder missed a message,
62
    //so we only care about responses that indicate bad magnetic strength
63
 
64
    if (!passes_test(false)) { //check encoder data is good
65
      lastErrorTime = millis();
66
      /*
67
      if (trusted) { //commented out as part of the note below
68
        trusted = false;
69
        SERIAL_ECHOPGM("Fault detected on ");
70
        SERIAL_ECHO(axis_codes[encoderAxis]);
71
        SERIAL_ECHOLNPGM(" axis encoder. Disengaging error correction until module is trusted again.");
72
      }
73
      */
74
      return;
75
    }
76
 
77
    if (!trusted) {
78
      /**
79
       * This is commented out because it introduces error and can cause bad print quality.
80
       *
81
       * This code is intended to manage situations where the encoder has reported bad magnetic strength.
82
       * This indicates that the magnetic strip was too far away from the sensor to reliably track position.
83
       * When this happens, this code resets the offset based on where the printer thinks it is. This has been
84
       * shown to introduce errors in actual position which result in drifting prints and poor print quality.
85
       * Perhaps a better method would be to disable correction on the axis with a problem, report it to the
86
       * user via the status leds on the encoder module and prompt the user to re-home the axis at which point
87
       * the encoder would be re-enabled.
88
       */
89
 
90
      /*
91
        // If the magnetic strength has been good for a certain time, start trusting the module again
92
 
93
        if (millis() - lastErrorTime > I2CPE_TIME_TRUSTED) {
94
          trusted = true;
95
 
96
          SERIAL_ECHOPGM("Untrusted encoder module on ");
97
          SERIAL_ECHO(axis_codes[encoderAxis]);
98
          SERIAL_ECHOLNPGM(" axis has been fault-free for set duration, reinstating error correction.");
99
 
100
          //the encoder likely lost its place when the error occured, so we'll reset and use the printer's
101
          //idea of where it the axis is to re-initialise
102
          float position = planner.get_axis_position_mm(encoderAxis);
103
          int32_t positionInTicks = position * get_ticks_unit();
104
 
105
          //shift position from previous to current position
106
          zeroOffset -= (positionInTicks - get_position());
107
 
108
          #ifdef I2CPE_DEBUG
109
            SERIAL_ECHOPGM("Current position is ");
110
            SERIAL_ECHOLN(position);
111
 
112
            SERIAL_ECHOPGM("Position in encoder ticks is ");
113
            SERIAL_ECHOLN(positionInTicks);
114
 
115
            SERIAL_ECHOPGM("New zero-offset of ");
116
            SERIAL_ECHOLN(zeroOffset);
117
 
118
            SERIAL_ECHOPGM("New position reads as ");
119
            SERIAL_ECHO(get_position());
120
            SERIAL_CHAR('(');
121
            SERIAL_ECHO(mm_from_count(get_position()));
122
            SERIAL_ECHOLNPGM(")");
123
          #endif
124
        }
125
      */
126
      return;
127
    }
128
 
129
    lastPosition = position;
130
    const millis_t positionTime = millis();
131
 
132
    //only do error correction if setup and enabled
133
    if (ec && ecMethod != I2CPE_ECM_NONE) {
134
 
135
      #ifdef I2CPE_EC_THRESH_PROPORTIONAL
136
        const millis_t deltaTime = positionTime - lastPositionTime;
137
        const uint32_t distance = ABS(position - lastPosition),
138
                       speed = distance / deltaTime;
139
        const float threshold = constrain((speed / 50), 1, 50) * ecThreshold;
140
      #else
141
        const float threshold = get_error_correct_threshold();
142
      #endif
143
 
144
      //check error
145
      #if ENABLED(I2CPE_ERR_ROLLING_AVERAGE)
146
        float sum = 0, diffSum = 0;
147
 
148
        errIdx = (errIdx >= I2CPE_ERR_ARRAY_SIZE - 1) ? 0 : errIdx + 1;
149
        err[errIdx] = get_axis_error_steps(false);
150
 
151
        LOOP_L_N(i, I2CPE_ERR_ARRAY_SIZE) {
152
          sum += err[i];
153
          if (i) diffSum += ABS(err[i-1] - err[i]);
154
        }
155
 
156
        const int32_t error = int32_t(sum / (I2CPE_ERR_ARRAY_SIZE + 1)); //calculate average for error
157
 
158
      #else
159
        const int32_t error = get_axis_error_steps(false);
160
      #endif
161
 
162
      //SERIAL_ECHOPGM("Axis error steps: ");
163
      //SERIAL_ECHOLN(error);
164
 
165
      #ifdef I2CPE_ERR_THRESH_ABORT
166
        if (ABS(error) > I2CPE_ERR_THRESH_ABORT * planner.axis_steps_per_mm[encoderAxis]) {
167
          //kill("Significant Error");
168
          SERIAL_ECHOPGM("Axis error greater than set threshold, aborting!");
169
          SERIAL_ECHOLN(error);
170
          safe_delay(5000);
171
        }
172
      #endif
173
 
174
      #if ENABLED(I2CPE_ERR_ROLLING_AVERAGE)
175
        if (errIdx == 0) {
176
          // In order to correct for "error" but avoid correcting for noise and non-skips
177
          // it must be > threshold and have a difference average of < 10 and be < 2000 steps
178
          if (ABS(error) > threshold * planner.axis_steps_per_mm[encoderAxis] &&
179
              diffSum < 10 * (I2CPE_ERR_ARRAY_SIZE - 1) && ABS(error) < 2000) { // Check for persistent error (skip)
180
            errPrst[errPrstIdx++] = error; // Error must persist for I2CPE_ERR_PRST_ARRAY_SIZE error cycles. This also serves to improve the average accuracy
181
            if (errPrstIdx >= I2CPE_ERR_PRST_ARRAY_SIZE) {
182
              float sumP = 0;
183
              LOOP_L_N(i, I2CPE_ERR_PRST_ARRAY_SIZE) sumP += errPrst[i];
184
              const int32_t errorP = int32_t(sumP * (1.0f / (I2CPE_ERR_PRST_ARRAY_SIZE)));
185
              SERIAL_ECHO(axis_codes[encoderAxis]);
186
              SERIAL_ECHOPAIR(" - err detected: ", errorP * planner.steps_to_mm[encoderAxis]);
187
              SERIAL_ECHOLNPGM("mm; correcting!");
188
              thermalManager.babystepsTodo[encoderAxis] = -LROUND(errorP);
189
              errPrstIdx = 0;
190
            }
191
          }
192
          else
193
            errPrstIdx = 0;
194
        }
195
      #else
196
        if (ABS(error) > threshold * planner.axis_steps_per_mm[encoderAxis]) {
197
          //SERIAL_ECHOLN(error);
198
          //SERIAL_ECHOLN(position);
199
          thermalManager.babystepsTodo[encoderAxis] = -LROUND(error / 2);
200
        }
201
      #endif
202
 
203
      if (ABS(error) > I2CPE_ERR_CNT_THRESH * planner.axis_steps_per_mm[encoderAxis]) {
204
        const millis_t ms = millis();
205
        if (ELAPSED(ms, nextErrorCountTime)) {
206
          SERIAL_ECHOPAIR("Large error on ", axis_codes[encoderAxis]);
207
          SERIAL_ECHOPAIR(" axis. error: ", (int)error);
208
          SERIAL_ECHOLNPAIR("; diffSum: ", diffSum);
209
          errorCount++;
210
          nextErrorCountTime = ms + I2CPE_ERR_CNT_DEBOUNCE_MS;
211
        }
212
      }
213
    }
214
 
215
    lastPositionTime = positionTime;
216
  }
217
 
218
  void I2CPositionEncoder::set_homed() {
219
    if (active) {
220
      reset();  // Reset module's offset to zero (so current position is homed / zero)
221
      delay(10);
222
 
223
      zeroOffset = get_raw_count();
224
      homed++;
225
      trusted++;
226
 
227
      #ifdef I2CPE_DEBUG
228
        SERIAL_ECHO(axis_codes[encoderAxis]);
229
        SERIAL_ECHOPAIR(" axis encoder homed, offset of ", zeroOffset);
230
        SERIAL_ECHOLNPGM(" ticks.");
231
      #endif
232
    }
233
  }
234
 
235
  bool I2CPositionEncoder::passes_test(const bool report) {
236
    if (report) {
237
      if (H != I2CPE_MAG_SIG_GOOD) SERIAL_ECHOPGM("Warning. ");
238
      SERIAL_ECHO(axis_codes[encoderAxis]);
239
      SERIAL_ECHOPGM(" axis ");
240
      serialprintPGM(H == I2CPE_MAG_SIG_BAD ? PSTR("magnetic strip ") : PSTR("encoder "));
241
      switch (H) {
242
        case I2CPE_MAG_SIG_GOOD:
243
        case I2CPE_MAG_SIG_MID:
244
          SERIAL_ECHOLNPGM("passes test; field strength ");
245
          serialprintPGM(H == I2CPE_MAG_SIG_GOOD ? PSTR("good.\n") : PSTR("fair.\n"));
246
          break;
247
        default:
248
          SERIAL_ECHOLNPGM("not detected!");
249
      }
250
    }
251
    return (H == I2CPE_MAG_SIG_GOOD || H == I2CPE_MAG_SIG_MID);
252
  }
253
 
254
  float I2CPositionEncoder::get_axis_error_mm(const bool report) {
255
    float target, actual, error;
256
 
257
    target = planner.get_axis_position_mm(encoderAxis);
258
    actual = mm_from_count(position);
259
    error = actual - target;
260
 
261
    if (ABS(error) > 10000) error = 0; // ?
262
 
263
    if (report) {
264
      SERIAL_ECHO(axis_codes[encoderAxis]);
265
      SERIAL_ECHOPAIR(" axis target: ", target);
266
      SERIAL_ECHOPAIR(", actual: ", actual);
267
      SERIAL_ECHOLNPAIR(", error : ",error);
268
    }
269
 
270
    return error;
271
  }
272
 
273
  int32_t I2CPositionEncoder::get_axis_error_steps(const bool report) {
274
    if (!active) {
275
      if (report) {
276
        SERIAL_ECHO(axis_codes[encoderAxis]);
277
        SERIAL_ECHOLNPGM(" axis encoder not active!");
278
      }
279
      return 0;
280
    }
281
 
282
    float stepperTicksPerUnit;
283
    int32_t encoderTicks = position, encoderCountInStepperTicksScaled;
284
    //int32_t stepperTicks = stepper.position(encoderAxis);
285
 
286
    // With a rotary encoder we're concerned with ticks/rev; whereas with a linear we're concerned with ticks/mm
287
    stepperTicksPerUnit = (type == I2CPE_ENC_TYPE_ROTARY) ? stepperTicks : planner.axis_steps_per_mm[encoderAxis];
288
 
289
    //convert both 'ticks' into same units / base
290
    encoderCountInStepperTicksScaled = LROUND((stepperTicksPerUnit * encoderTicks) / encoderTicksPerUnit);
291
 
292
    int32_t target = stepper.position(encoderAxis),
293
            error = (encoderCountInStepperTicksScaled - target);
294
 
295
    //suppress discontinuities (might be caused by bad I2C readings...?)
296
    const bool suppressOutput = (ABS(error - errorPrev) > 100);
297
 
298
    if (report) {
299
      SERIAL_ECHO(axis_codes[encoderAxis]);
300
      SERIAL_ECHOPAIR(" axis target: ", target);
301
      SERIAL_ECHOPAIR(", actual: ", encoderCountInStepperTicksScaled);
302
      SERIAL_ECHOLNPAIR(", error : ", error);
303
 
304
      if (suppressOutput) SERIAL_ECHOLNPGM("Discontinuity detected, suppressing error.");
305
    }
306
 
307
    errorPrev = error;
308
 
309
    return (suppressOutput ? 0 : error);
310
  }
311
 
312
  int32_t I2CPositionEncoder::get_raw_count() {
313
    uint8_t index = 0;
314
    i2cLong encoderCount;
315
 
316
    encoderCount.val = 0x00;
317
 
318
    if (Wire.requestFrom((int)i2cAddress, 3) != 3) {
319
      //houston, we have a problem...
320
      H = I2CPE_MAG_SIG_NF;
321
      return 0;
322
    }
323
 
324
    while (Wire.available())
325
      encoderCount.bval[index++] = (uint8_t)Wire.read();
326
 
327
    //extract the magnetic strength
328
    H = (B00000011 & (encoderCount.bval[2] >> 6));
329
 
330
    //extract sign bit; sign = (encoderCount.bval[2] & B00100000);
331
    //set all upper bits to the sign value to overwrite H
332
    encoderCount.val = (encoderCount.bval[2] & B00100000) ? (encoderCount.val | 0xFFC00000) : (encoderCount.val & 0x003FFFFF);
333
 
334
    if (invert) encoderCount.val *= -1;
335
 
336
    return encoderCount.val;
337
  }
338
 
339
  bool I2CPositionEncoder::test_axis() {
340
    //only works on XYZ cartesian machines for the time being
341
    if (!(encoderAxis == X_AXIS || encoderAxis == Y_AXIS || encoderAxis == Z_AXIS)) return false;
342
 
343
    float startCoord[NUM_AXIS] = { 0 }, endCoord[NUM_AXIS] = { 0 };
344
 
345
    const float startPosition = soft_endstop_min[encoderAxis] + 10,
346
                endPosition = soft_endstop_max[encoderAxis] - 10,
347
                feedrate = FLOOR(MMM_TO_MMS((encoderAxis == Z_AXIS) ? HOMING_FEEDRATE_Z : HOMING_FEEDRATE_XY));
348
 
349
    ec = false;
350
 
351
    LOOP_NA(i) {
352
      startCoord[i] = planner.get_axis_position_mm((AxisEnum)i);
353
      endCoord[i] = planner.get_axis_position_mm((AxisEnum)i);
354
    }
355
 
356
    startCoord[encoderAxis] = startPosition;
357
    endCoord[encoderAxis] = endPosition;
358
 
359
    planner.synchronize();
360
 
361
    planner.buffer_line(startCoord[X_AXIS], startCoord[Y_AXIS], startCoord[Z_AXIS],
362
                        planner.get_axis_position_mm(E_AXIS), feedrate, 0);
363
    planner.synchronize();
364
 
365
    // if the module isn't currently trusted, wait until it is (or until it should be if things are working)
366
    if (!trusted) {
367
      int32_t startWaitingTime = millis();
368
      while (!trusted && millis() - startWaitingTime < I2CPE_TIME_TRUSTED)
369
        safe_delay(500);
370
    }
371
 
372
    if (trusted) { // if trusted, commence test
373
      planner.buffer_line(endCoord[X_AXIS], endCoord[Y_AXIS], endCoord[Z_AXIS],
374
                          planner.get_axis_position_mm(E_AXIS), feedrate, 0);
375
      planner.synchronize();
376
    }
377
 
378
    return trusted;
379
  }
380
 
381
  void I2CPositionEncoder::calibrate_steps_mm(const uint8_t iter) {
382
    if (type != I2CPE_ENC_TYPE_LINEAR) {
383
      SERIAL_ECHOLNPGM("Steps per mm calibration is only available using linear encoders.");
384
      return;
385
    }
386
 
387
    if (!(encoderAxis == X_AXIS || encoderAxis == Y_AXIS || encoderAxis == Z_AXIS)) {
388
      SERIAL_ECHOLNPGM("Automatic steps / mm calibration not supported for this axis.");
389
      return;
390
    }
391
 
392
    float old_steps_mm, new_steps_mm,
393
          startDistance, endDistance,
394
          travelDistance, travelledDistance, total = 0,
395
          startCoord[NUM_AXIS] = { 0 }, endCoord[NUM_AXIS] = { 0 };
396
 
397
    float feedrate;
398
 
399
    int32_t startCount, stopCount;
400
 
401
    feedrate = MMM_TO_MMS((encoderAxis == Z_AXIS) ? HOMING_FEEDRATE_Z : HOMING_FEEDRATE_XY);
402
 
403
    bool oldec = ec;
404
    ec = false;
405
 
406
    startDistance = 20;
407
    endDistance = soft_endstop_max[encoderAxis] - 20;
408
    travelDistance = endDistance - startDistance;
409
 
410
    LOOP_NA(i) {
411
      startCoord[i] = planner.get_axis_position_mm((AxisEnum)i);
412
      endCoord[i] = planner.get_axis_position_mm((AxisEnum)i);
413
    }
414
 
415
    startCoord[encoderAxis] = startDistance;
416
    endCoord[encoderAxis] = endDistance;
417
 
418
    planner.synchronize();
419
 
420
    LOOP_L_N(i, iter) {
421
      planner.buffer_line(startCoord[X_AXIS], startCoord[Y_AXIS], startCoord[Z_AXIS],
422
                          planner.get_axis_position_mm(E_AXIS), feedrate, 0);
423
      planner.synchronize();
424
 
425
      delay(250);
426
      startCount = get_position();
427
 
428
      //do_blocking_move_to(endCoord[X_AXIS],endCoord[Y_AXIS],endCoord[Z_AXIS]);
429
 
430
      planner.buffer_line(endCoord[X_AXIS], endCoord[Y_AXIS], endCoord[Z_AXIS],
431
                          planner.get_axis_position_mm(E_AXIS), feedrate, 0);
432
      planner.synchronize();
433
 
434
      //Read encoder distance
435
      delay(250);
436
      stopCount = get_position();
437
 
438
      travelledDistance = mm_from_count(ABS(stopCount - startCount));
439
 
440
      SERIAL_ECHOPAIR("Attempted to travel: ", travelDistance);
441
      SERIAL_ECHOLNPGM("mm.");
442
 
443
      SERIAL_ECHOPAIR("Actually travelled:  ", travelledDistance);
444
      SERIAL_ECHOLNPGM("mm.");
445
 
446
      //Calculate new axis steps per unit
447
      old_steps_mm = planner.axis_steps_per_mm[encoderAxis];
448
      new_steps_mm = (old_steps_mm * travelDistance) / travelledDistance;
449
 
450
      SERIAL_ECHOLNPAIR("Old steps per mm: ", old_steps_mm);
451
      SERIAL_ECHOLNPAIR("New steps per mm: ", new_steps_mm);
452
 
453
      //Save new value
454
      planner.axis_steps_per_mm[encoderAxis] = new_steps_mm;
455
 
456
      if (iter > 1) {
457
        total += new_steps_mm;
458
 
459
        // swap start and end points so next loop runs from current position
460
        float tempCoord = startCoord[encoderAxis];
461
        startCoord[encoderAxis] = endCoord[encoderAxis];
462
        endCoord[encoderAxis] = tempCoord;
463
      }
464
    }
465
 
466
    if (iter > 1) {
467
      total /= (float)iter;
468
      SERIAL_ECHOLNPAIR("Average steps per mm: ", total);
469
    }
470
 
471
    ec = oldec;
472
 
473
    SERIAL_ECHOLNPGM("Calculated steps per mm has been set. Please save to EEPROM (M500) if you wish to keep these values.");
474
  }
475
 
476
  void I2CPositionEncoder::reset() {
477
    Wire.beginTransmission(i2cAddress);
478
    Wire.write(I2CPE_RESET_COUNT);
479
    Wire.endTransmission();
480
 
481
    #if ENABLED(I2CPE_ERR_ROLLING_AVERAGE)
482
      ZERO(err);
483
    #endif
484
  }
485
 
486
 
487
  bool I2CPositionEncodersMgr::I2CPE_anyaxis;
488
  uint8_t I2CPositionEncodersMgr::I2CPE_addr,
489
          I2CPositionEncodersMgr::I2CPE_idx;
490
  I2CPositionEncoder I2CPositionEncodersMgr::encoders[I2CPE_ENCODER_CNT];
491
 
492
  void I2CPositionEncodersMgr::init() {
493
    Wire.begin();
494
 
495
    #if I2CPE_ENCODER_CNT > 0
496
      uint8_t i = 0;
497
 
498
      encoders[i].init(I2CPE_ENC_1_ADDR, I2CPE_ENC_1_AXIS);
499
 
500
      #ifdef I2CPE_ENC_1_TYPE
501
        encoders[i].set_type(I2CPE_ENC_1_TYPE);
502
      #endif
503
      #ifdef I2CPE_ENC_1_TICKS_UNIT
504
        encoders[i].set_ticks_unit(I2CPE_ENC_1_TICKS_UNIT);
505
      #endif
506
      #ifdef I2CPE_ENC_1_TICKS_REV
507
        encoders[i].set_stepper_ticks(I2CPE_ENC_1_TICKS_REV);
508
      #endif
509
      #ifdef I2CPE_ENC_1_INVERT
510
        encoders[i].set_inverted(I2CPE_ENC_1_INVERT);
511
      #endif
512
      #ifdef I2CPE_ENC_1_EC_METHOD
513
        encoders[i].set_ec_method(I2CPE_ENC_1_EC_METHOD);
514
      #endif
515
      #ifdef I2CPE_ENC_1_EC_THRESH
516
        encoders[i].set_ec_threshold(I2CPE_ENC_1_EC_THRESH);
517
      #endif
518
 
519
      encoders[i].set_active(encoders[i].passes_test(true));
520
 
521
      #if I2CPE_ENC_1_AXIS == E_AXIS
522
        encoders[i].set_homed();
523
      #endif
524
    #endif
525
 
526
    #if I2CPE_ENCODER_CNT > 1
527
      i++;
528
 
529
      encoders[i].init(I2CPE_ENC_2_ADDR, I2CPE_ENC_2_AXIS);
530
 
531
      #ifdef I2CPE_ENC_2_TYPE
532
        encoders[i].set_type(I2CPE_ENC_2_TYPE);
533
      #endif
534
      #ifdef I2CPE_ENC_2_TICKS_UNIT
535
        encoders[i].set_ticks_unit(I2CPE_ENC_2_TICKS_UNIT);
536
      #endif
537
      #ifdef I2CPE_ENC_2_TICKS_REV
538
        encoders[i].set_stepper_ticks(I2CPE_ENC_2_TICKS_REV);
539
      #endif
540
      #ifdef I2CPE_ENC_2_INVERT
541
        encoders[i].set_inverted(I2CPE_ENC_2_INVERT);
542
      #endif
543
      #ifdef I2CPE_ENC_2_EC_METHOD
544
        encoders[i].set_ec_method(I2CPE_ENC_2_EC_METHOD);
545
      #endif
546
      #ifdef I2CPE_ENC_2_EC_THRESH
547
        encoders[i].set_ec_threshold(I2CPE_ENC_2_EC_THRESH);
548
      #endif
549
 
550
      encoders[i].set_active(encoders[i].passes_test(true));
551
 
552
      #if I2CPE_ENC_2_AXIS == E_AXIS
553
        encoders[i].set_homed();
554
      #endif
555
    #endif
556
 
557
    #if I2CPE_ENCODER_CNT > 2
558
      i++;
559
 
560
      encoders[i].init(I2CPE_ENC_3_ADDR, I2CPE_ENC_3_AXIS);
561
 
562
      #ifdef I2CPE_ENC_3_TYPE
563
        encoders[i].set_type(I2CPE_ENC_3_TYPE);
564
      #endif
565
      #ifdef I2CPE_ENC_3_TICKS_UNIT
566
        encoders[i].set_ticks_unit(I2CPE_ENC_3_TICKS_UNIT);
567
      #endif
568
      #ifdef I2CPE_ENC_3_TICKS_REV
569
        encoders[i].set_stepper_ticks(I2CPE_ENC_3_TICKS_REV);
570
      #endif
571
      #ifdef I2CPE_ENC_3_INVERT
572
        encoders[i].set_inverted(I2CPE_ENC_3_INVERT);
573
      #endif
574
      #ifdef I2CPE_ENC_3_EC_METHOD
575
        encoders[i].set_ec_method(I2CPE_ENC_3_EC_METHOD);
576
      #endif
577
      #ifdef I2CPE_ENC_3_EC_THRESH
578
        encoders[i].set_ec_threshold(I2CPE_ENC_3_EC_THRESH);
579
      #endif
580
 
581
    encoders[i].set_active(encoders[i].passes_test(true));
582
 
583
      #if I2CPE_ENC_3_AXIS == E_AXIS
584
        encoders[i].set_homed();
585
      #endif
586
    #endif
587
 
588
    #if I2CPE_ENCODER_CNT > 3
589
      i++;
590
 
591
      encoders[i].init(I2CPE_ENC_4_ADDR, I2CPE_ENC_4_AXIS);
592
 
593
      #ifdef I2CPE_ENC_4_TYPE
594
        encoders[i].set_type(I2CPE_ENC_4_TYPE);
595
      #endif
596
      #ifdef I2CPE_ENC_4_TICKS_UNIT
597
        encoders[i].set_ticks_unit(I2CPE_ENC_4_TICKS_UNIT);
598
      #endif
599
      #ifdef I2CPE_ENC_4_TICKS_REV
600
        encoders[i].set_stepper_ticks(I2CPE_ENC_4_TICKS_REV);
601
      #endif
602
      #ifdef I2CPE_ENC_4_INVERT
603
        encoders[i].set_inverted(I2CPE_ENC_4_INVERT);
604
      #endif
605
      #ifdef I2CPE_ENC_4_EC_METHOD
606
        encoders[i].set_ec_method(I2CPE_ENC_4_EC_METHOD);
607
      #endif
608
      #ifdef I2CPE_ENC_4_EC_THRESH
609
        encoders[i].set_ec_threshold(I2CPE_ENC_4_EC_THRESH);
610
      #endif
611
 
612
      encoders[i].set_active(encoders[i].passes_test(true));
613
 
614
      #if I2CPE_ENC_4_AXIS == E_AXIS
615
        encoders[i].set_homed();
616
      #endif
617
    #endif
618
 
619
    #if I2CPE_ENCODER_CNT > 4
620
      i++;
621
 
622
      encoders[i].init(I2CPE_ENC_5_ADDR, I2CPE_ENC_5_AXIS);
623
 
624
      #ifdef I2CPE_ENC_5_TYPE
625
        encoders[i].set_type(I2CPE_ENC_5_TYPE);
626
      #endif
627
      #ifdef I2CPE_ENC_5_TICKS_UNIT
628
        encoders[i].set_ticks_unit(I2CPE_ENC_5_TICKS_UNIT);
629
      #endif
630
      #ifdef I2CPE_ENC_5_TICKS_REV
631
        encoders[i].set_stepper_ticks(I2CPE_ENC_5_TICKS_REV);
632
      #endif
633
      #ifdef I2CPE_ENC_5_INVERT
634
        encoders[i].set_inverted(I2CPE_ENC_5_INVERT);
635
      #endif
636
      #ifdef I2CPE_ENC_5_EC_METHOD
637
        encoders[i].set_ec_method(I2CPE_ENC_5_EC_METHOD);
638
      #endif
639
      #ifdef I2CPE_ENC_5_EC_THRESH
640
        encoders[i].set_ec_threshold(I2CPE_ENC_5_EC_THRESH);
641
      #endif
642
 
643
      encoders[i].set_active(encoders[i].passes_test(true));
644
 
645
      #if I2CPE_ENC_5_AXIS == E_AXIS
646
        encoders[i].set_homed();
647
      #endif
648
    #endif
649
  }
650
 
651
  void I2CPositionEncodersMgr::report_position(const int8_t idx, const bool units, const bool noOffset) {
652
    CHECK_IDX();
653
 
654
    if (units)
655
      SERIAL_ECHOLN(noOffset ? encoders[idx].mm_from_count(encoders[idx].get_raw_count()) : encoders[idx].get_position_mm());
656
    else {
657
      if (noOffset) {
658
        const int32_t raw_count = encoders[idx].get_raw_count();
659
        SERIAL_ECHO(axis_codes[encoders[idx].get_axis()]);
660
        SERIAL_CHAR(' ');
661
 
662
        for (uint8_t j = 31; j > 0; j--)
663
          SERIAL_ECHO((bool)(0x00000001 & (raw_count >> j)));
664
 
665
        SERIAL_ECHO((bool)(0x00000001 & raw_count));
666
        SERIAL_CHAR(' ');
667
        SERIAL_ECHOLN(raw_count);
668
      }
669
      else
670
        SERIAL_ECHOLN(encoders[idx].get_position());
671
    }
672
  }
673
 
674
  void I2CPositionEncodersMgr::change_module_address(const uint8_t oldaddr, const uint8_t newaddr) {
675
    // First check 'new' address is not in use
676
    Wire.beginTransmission(newaddr);
677
    if (!Wire.endTransmission()) {
678
      SERIAL_ECHOPAIR("?There is already a device with that address on the I2C bus! (", newaddr);
679
      SERIAL_ECHOLNPGM(")");
680
      return;
681
    }
682
 
683
    // Now check that we can find the module on the oldaddr address
684
    Wire.beginTransmission(oldaddr);
685
    if (Wire.endTransmission()) {
686
      SERIAL_ECHOPAIR("?No module detected at this address! (", oldaddr);
687
      SERIAL_ECHOLNPGM(")");
688
      return;
689
    }
690
 
691
    SERIAL_ECHOPAIR("Module found at ", oldaddr);
692
    SERIAL_ECHOLNPAIR(", changing address to ", newaddr);
693
 
694
    // Change the modules address
695
    Wire.beginTransmission(oldaddr);
696
    Wire.write(I2CPE_SET_ADDR);
697
    Wire.write(newaddr);
698
    Wire.endTransmission();
699
 
700
    SERIAL_ECHOLNPGM("Address changed, resetting and waiting for confirmation..");
701
 
702
    // Wait for the module to reset (can probably be improved by polling address with a timeout).
703
    safe_delay(I2CPE_REBOOT_TIME);
704
 
705
    // Look for the module at the new address.
706
    Wire.beginTransmission(newaddr);
707
    if (Wire.endTransmission()) {
708
      SERIAL_ECHOLNPGM("Address change failed! Check encoder module.");
709
      return;
710
    }
711
 
712
    SERIAL_ECHOLNPGM("Address change successful!");
713
 
714
    // Now, if this module is configured, find which encoder instance it's supposed to correspond to
715
    // and enable it (it will likely have failed initialisation on power-up, before the address change).
716
    const int8_t idx = idx_from_addr(newaddr);
717
    if (idx >= 0 && !encoders[idx].get_active()) {
718
      SERIAL_ECHO(axis_codes[encoders[idx].get_axis()]);
719
      SERIAL_ECHOLNPGM(" axis encoder was not detected on printer startup. Trying again.");
720
      encoders[idx].set_active(encoders[idx].passes_test(true));
721
    }
722
  }
723
 
724
  void I2CPositionEncodersMgr::report_module_firmware(const uint8_t address) {
725
    // First check there is a module
726
    Wire.beginTransmission(address);
727
    if (Wire.endTransmission()) {
728
      SERIAL_ECHOPAIR("?No module detected at this address! (", address);
729
      SERIAL_ECHOLNPGM(")");
730
      return;
731
    }
732
 
733
    SERIAL_ECHOPAIR("Requesting version info from module at address ", address);
734
    SERIAL_ECHOLNPGM(":");
735
 
736
    Wire.beginTransmission(address);
737
    Wire.write(I2CPE_SET_REPORT_MODE);
738
    Wire.write(I2CPE_REPORT_VERSION);
739
    Wire.endTransmission();
740
 
741
    // Read value
742
    if (Wire.requestFrom((int)address, 32)) {
743
      char c;
744
      while (Wire.available() > 0 && (c = (char)Wire.read()) > 0)
745
        SERIAL_ECHO(c);
746
      SERIAL_EOL();
747
    }
748
 
749
    // Set module back to normal (distance) mode
750
    Wire.beginTransmission(address);
751
    Wire.write(I2CPE_SET_REPORT_MODE);
752
    Wire.write(I2CPE_REPORT_DISTANCE);
753
    Wire.endTransmission();
754
  }
755
 
756
  int8_t I2CPositionEncodersMgr::parse() {
757
    I2CPE_addr = 0;
758
 
759
    if (parser.seen('A')) {
760
 
761
      if (!parser.has_value()) {
762
        SERIAL_PROTOCOLLNPGM("?A seen, but no address specified! [30-200]");
763
        return I2CPE_PARSE_ERR;
764
      };
765
 
766
      I2CPE_addr = parser.value_byte();
767
      if (!WITHIN(I2CPE_addr, 30, 200)) { // reserve the first 30 and last 55
768
        SERIAL_PROTOCOLLNPGM("?Address out of range. [30-200]");
769
        return I2CPE_PARSE_ERR;
770
      }
771
 
772
      I2CPE_idx = idx_from_addr(I2CPE_addr);
773
      if (I2CPE_idx >= I2CPE_ENCODER_CNT) {
774
        SERIAL_PROTOCOLLNPGM("?No device with this address!");
775
        return I2CPE_PARSE_ERR;
776
      }
777
    }
778
    else if (parser.seenval('I')) {
779
 
780
      if (!parser.has_value()) {
781
        SERIAL_PROTOCOLLNPAIR("?I seen, but no index specified! [0-", I2CPE_ENCODER_CNT - 1);
782
        SERIAL_PROTOCOLLNPGM("]");
783
        return I2CPE_PARSE_ERR;
784
      };
785
 
786
      I2CPE_idx = parser.value_byte();
787
      if (I2CPE_idx >= I2CPE_ENCODER_CNT) {
788
        SERIAL_PROTOCOLLNPAIR("?Index out of range. [0-", I2CPE_ENCODER_CNT - 1);
789
        SERIAL_ECHOLNPGM("]");
790
        return I2CPE_PARSE_ERR;
791
      }
792
 
793
      I2CPE_addr = encoders[I2CPE_idx].get_address();
794
    }
795
    else
796
      I2CPE_idx = 0xFF;
797
 
798
    I2CPE_anyaxis = parser.seen_axis();
799
 
800
    return I2CPE_PARSE_OK;
801
  };
802
 
803
  /**
804
   * M860:  Report the position(s) of position encoder module(s).
805
   *
806
   *   A<addr>  Module I2C address.  [30, 200].
807
   *   I<index> Module index.  [0, I2CPE_ENCODER_CNT - 1]
808
   *   O        Include homed zero-offset in returned position.
809
   *   U        Units in mm or raw step count.
810
   *
811
   *   If A or I not specified:
812
   *    X       Report on X axis encoder, if present.
813
   *    Y       Report on Y axis encoder, if present.
814
   *    Z       Report on Z axis encoder, if present.
815
   *    E       Report on E axis encoder, if present.
816
   *
817
   */
818
  void I2CPositionEncodersMgr::M860() {
819
    if (parse()) return;
820
 
821
    const bool hasU = parser.seen('U'), hasO = parser.seen('O');
822
 
823
    if (I2CPE_idx == 0xFF) {
824
      LOOP_XYZE(i) {
825
        if (!I2CPE_anyaxis || parser.seen(axis_codes[i])) {
826
          const uint8_t idx = idx_from_axis(AxisEnum(i));
827
          if ((int8_t)idx >= 0) report_position(idx, hasU, hasO);
828
        }
829
      }
830
    }
831
    else
832
      report_position(I2CPE_idx, hasU, hasO);
833
  }
834
 
835
  /**
836
   * M861:  Report the status of position encoder modules.
837
   *
838
   *   A<addr>  Module I2C address.  [30, 200].
839
   *   I<index> Module index.  [0, I2CPE_ENCODER_CNT - 1]
840
   *
841
   *   If A or I not specified:
842
   *    X       Report on X axis encoder, if present.
843
   *    Y       Report on Y axis encoder, if present.
844
   *    Z       Report on Z axis encoder, if present.
845
   *    E       Report on E axis encoder, if present.
846
   *
847
   */
848
  void I2CPositionEncodersMgr::M861() {
849
    if (parse()) return;
850
 
851
    if (I2CPE_idx == 0xFF) {
852
      LOOP_XYZE(i) {
853
        if (!I2CPE_anyaxis || parser.seen(axis_codes[i])) {
854
          const uint8_t idx = idx_from_axis(AxisEnum(i));
855
          if ((int8_t)idx >= 0) report_status(idx);
856
        }
857
      }
858
    }
859
    else
860
      report_status(I2CPE_idx);
861
  }
862
 
863
  /**
864
   * M862:  Perform an axis continuity test for position encoder
865
   *        modules.
866
   *
867
   *   A<addr>  Module I2C address.  [30, 200].
868
   *   I<index> Module index.  [0, I2CPE_ENCODER_CNT - 1]
869
   *
870
   *   If A or I not specified:
871
   *    X       Report on X axis encoder, if present.
872
   *    Y       Report on Y axis encoder, if present.
873
   *    Z       Report on Z axis encoder, if present.
874
   *    E       Report on E axis encoder, if present.
875
   *
876
   */
877
  void I2CPositionEncodersMgr::M862() {
878
    if (parse()) return;
879
 
880
    if (I2CPE_idx == 0xFF) {
881
      LOOP_XYZE(i) {
882
        if (!I2CPE_anyaxis || parser.seen(axis_codes[i])) {
883
          const uint8_t idx = idx_from_axis(AxisEnum(i));
884
          if ((int8_t)idx >= 0) test_axis(idx);
885
        }
886
      }
887
    }
888
    else
889
      test_axis(I2CPE_idx);
890
  }
891
 
892
  /**
893
   * M863:  Perform steps-per-mm calibration for
894
   *        position encoder modules.
895
   *
896
   *   A<addr>  Module I2C address.  [30, 200].
897
   *   I<index> Module index.  [0, I2CPE_ENCODER_CNT - 1]
898
   *   P        Number of rePeats/iterations.
899
   *
900
   *   If A or I not specified:
901
   *    X       Report on X axis encoder, if present.
902
   *    Y       Report on Y axis encoder, if present.
903
   *    Z       Report on Z axis encoder, if present.
904
   *    E       Report on E axis encoder, if present.
905
   *
906
   */
907
  void I2CPositionEncodersMgr::M863() {
908
    if (parse()) return;
909
 
910
    const uint8_t iterations = constrain(parser.byteval('P', 1), 1, 10);
911
 
912
    if (I2CPE_idx == 0xFF) {
913
      LOOP_XYZE(i) {
914
        if (!I2CPE_anyaxis || parser.seen(axis_codes[i])) {
915
          const uint8_t idx = idx_from_axis(AxisEnum(i));
916
          if ((int8_t)idx >= 0) calibrate_steps_mm(idx, iterations);
917
        }
918
      }
919
    }
920
    else
921
      calibrate_steps_mm(I2CPE_idx, iterations);
922
  }
923
 
924
  /**
925
   * M864:  Change position encoder module I2C address.
926
   *
927
   *   A<addr>  Module current/old I2C address.  If not present,
928
   *            assumes default address (030).  [30, 200].
929
   *   S<addr>  Module new I2C address. [30, 200].
930
   *
931
   *   If S is not specified:
932
   *    X       Use I2CPE_PRESET_ADDR_X (030).
933
   *    Y       Use I2CPE_PRESET_ADDR_Y (031).
934
   *    Z       Use I2CPE_PRESET_ADDR_Z (032).
935
   *    E       Use I2CPE_PRESET_ADDR_E (033).
936
   */
937
  void I2CPositionEncodersMgr::M864() {
938
    uint8_t newAddress;
939
 
940
    if (parse()) return;
941
 
942
    if (!I2CPE_addr) I2CPE_addr = I2CPE_PRESET_ADDR_X;
943
 
944
    if (parser.seen('S')) {
945
      if (!parser.has_value()) {
946
        SERIAL_PROTOCOLLNPGM("?S seen, but no address specified! [30-200]");
947
        return;
948
      };
949
 
950
      newAddress = parser.value_byte();
951
      if (!WITHIN(newAddress, 30, 200)) {
952
        SERIAL_PROTOCOLLNPGM("?New address out of range. [30-200]");
953
        return;
954
      }
955
    }
956
    else if (!I2CPE_anyaxis) {
957
      SERIAL_PROTOCOLLNPGM("?You must specify S or [XYZE].");
958
      return;
959
    }
960
    else {
961
           if (parser.seen('X')) newAddress = I2CPE_PRESET_ADDR_X;
962
      else if (parser.seen('Y')) newAddress = I2CPE_PRESET_ADDR_Y;
963
      else if (parser.seen('Z')) newAddress = I2CPE_PRESET_ADDR_Z;
964
      else if (parser.seen('E')) newAddress = I2CPE_PRESET_ADDR_E;
965
      else return;
966
    }
967
 
968
    SERIAL_ECHOPAIR("Changing module at address ", I2CPE_addr);
969
    SERIAL_ECHOLNPAIR(" to address ", newAddress);
970
 
971
    change_module_address(I2CPE_addr, newAddress);
972
  }
973
 
974
  /**
975
   * M865:  Check position encoder module firmware version.
976
   *
977
   *   A<addr>  Module I2C address.  [30, 200].
978
   *   I<index> Module index.  [0, I2CPE_ENCODER_CNT - 1].
979
   *
980
   *   If A or I not specified:
981
   *    X       Check X axis encoder, if present.
982
   *    Y       Check Y axis encoder, if present.
983
   *    Z       Check Z axis encoder, if present.
984
   *    E       Check E axis encoder, if present.
985
   */
986
  void I2CPositionEncodersMgr::M865() {
987
    if (parse()) return;
988
 
989
    if (!I2CPE_addr) {
990
      LOOP_XYZE(i) {
991
        if (!I2CPE_anyaxis || parser.seen(axis_codes[i])) {
992
          const uint8_t idx = idx_from_axis(AxisEnum(i));
993
          if ((int8_t)idx >= 0) report_module_firmware(encoders[idx].get_address());
994
        }
995
      }
996
    }
997
    else
998
      report_module_firmware(I2CPE_addr);
999
  }
1000
 
1001
  /**
1002
   * M866:  Report or reset position encoder module error
1003
   *        count.
1004
   *
1005
   *   A<addr>  Module I2C address.  [30, 200].
1006
   *   I<index> Module index.  [0, I2CPE_ENCODER_CNT - 1].
1007
   *   R        Reset error counter.
1008
   *
1009
   *   If A or I not specified:
1010
   *    X       Act on X axis encoder, if present.
1011
   *    Y       Act on Y axis encoder, if present.
1012
   *    Z       Act on Z axis encoder, if present.
1013
   *    E       Act on E axis encoder, if present.
1014
   */
1015
  void I2CPositionEncodersMgr::M866() {
1016
    if (parse()) return;
1017
 
1018
    const bool hasR = parser.seen('R');
1019
 
1020
    if (I2CPE_idx == 0xFF) {
1021
      LOOP_XYZE(i) {
1022
        if (!I2CPE_anyaxis || parser.seen(axis_codes[i])) {
1023
          const uint8_t idx = idx_from_axis(AxisEnum(i));
1024
          if ((int8_t)idx >= 0) {
1025
            if (hasR)
1026
              reset_error_count(idx, AxisEnum(i));
1027
            else
1028
              report_error_count(idx, AxisEnum(i));
1029
          }
1030
        }
1031
      }
1032
    }
1033
    else if (hasR)
1034
      reset_error_count(I2CPE_idx, encoders[I2CPE_idx].get_axis());
1035
    else
1036
      report_error_count(I2CPE_idx, encoders[I2CPE_idx].get_axis());
1037
  }
1038
 
1039
  /**
1040
   * M867:  Enable/disable or toggle error correction for position encoder modules.
1041
   *
1042
   *   A<addr>  Module I2C address.  [30, 200].
1043
   *   I<index> Module index.  [0, I2CPE_ENCODER_CNT - 1].
1044
   *   S<1|0>   Enable/disable error correction. 1 enables, 0 disables.  If not
1045
   *            supplied, toggle.
1046
   *
1047
   *   If A or I not specified:
1048
   *    X       Act on X axis encoder, if present.
1049
   *    Y       Act on Y axis encoder, if present.
1050
   *    Z       Act on Z axis encoder, if present.
1051
   *    E       Act on E axis encoder, if present.
1052
   */
1053
  void I2CPositionEncodersMgr::M867() {
1054
    if (parse()) return;
1055
 
1056
    const int8_t onoff = parser.seenval('S') ? parser.value_int() : -1;
1057
 
1058
    if (I2CPE_idx == 0xFF) {
1059
      LOOP_XYZE(i) {
1060
        if (!I2CPE_anyaxis || parser.seen(axis_codes[i])) {
1061
          const uint8_t idx = idx_from_axis(AxisEnum(i));
1062
          if ((int8_t)idx >= 0) {
1063
            const bool ena = onoff == -1 ? !encoders[I2CPE_idx].get_ec_enabled() : !!onoff;
1064
            enable_ec(idx, ena, AxisEnum(i));
1065
          }
1066
        }
1067
      }
1068
    }
1069
    else {
1070
      const bool ena = onoff == -1 ? !encoders[I2CPE_idx].get_ec_enabled() : !!onoff;
1071
      enable_ec(I2CPE_idx, ena, encoders[I2CPE_idx].get_axis());
1072
    }
1073
  }
1074
 
1075
  /**
1076
   * M868:  Report or set position encoder module error correction
1077
   *        threshold.
1078
   *
1079
   *   A<addr>  Module I2C address.  [30, 200].
1080
   *   I<index> Module index.  [0, I2CPE_ENCODER_CNT - 1].
1081
   *   T        New error correction threshold.
1082
   *
1083
   *   If A not specified:
1084
   *    X       Act on X axis encoder, if present.
1085
   *    Y       Act on Y axis encoder, if present.
1086
   *    Z       Act on Z axis encoder, if present.
1087
   *    E       Act on E axis encoder, if present.
1088
   */
1089
  void I2CPositionEncodersMgr::M868() {
1090
    if (parse()) return;
1091
 
1092
    const float newThreshold = parser.seenval('T') ? parser.value_float() : -9999;
1093
 
1094
    if (I2CPE_idx == 0xFF) {
1095
      LOOP_XYZE(i) {
1096
        if (!I2CPE_anyaxis || parser.seen(axis_codes[i])) {
1097
          const uint8_t idx = idx_from_axis(AxisEnum(i));
1098
          if ((int8_t)idx >= 0) {
1099
            if (newThreshold != -9999)
1100
              set_ec_threshold(idx, newThreshold, encoders[idx].get_axis());
1101
            else
1102
              get_ec_threshold(idx, encoders[idx].get_axis());
1103
          }
1104
        }
1105
      }
1106
    }
1107
    else if (newThreshold != -9999)
1108
      set_ec_threshold(I2CPE_idx, newThreshold, encoders[I2CPE_idx].get_axis());
1109
    else
1110
      get_ec_threshold(I2CPE_idx, encoders[I2CPE_idx].get_axis());
1111
  }
1112
 
1113
  /**
1114
   * M869:  Report position encoder module error.
1115
   *
1116
   *   A<addr>  Module I2C address.  [30, 200].
1117
   *   I<index> Module index.  [0, I2CPE_ENCODER_CNT - 1].
1118
   *
1119
   *   If A not specified:
1120
   *    X       Act on X axis encoder, if present.
1121
   *    Y       Act on Y axis encoder, if present.
1122
   *    Z       Act on Z axis encoder, if present.
1123
   *    E       Act on E axis encoder, if present.
1124
   */
1125
  void I2CPositionEncodersMgr::M869() {
1126
    if (parse()) return;
1127
 
1128
    if (I2CPE_idx == 0xFF) {
1129
      LOOP_XYZE(i) {
1130
        if (!I2CPE_anyaxis || parser.seen(axis_codes[i])) {
1131
          const uint8_t idx = idx_from_axis(AxisEnum(i));
1132
          if ((int8_t)idx >= 0) report_error(idx);
1133
        }
1134
      }
1135
    }
1136
    else
1137
      report_error(I2CPE_idx);
1138
  }
1139
 
1140
#endif // I2C_POSITION_ENCODERS