Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ This project did not use formal release tags through most of its history, so thi

### Added

- **SET_SPEED_FF sets the speed loop's feedforward** (`DCMT_OP_SET_SPEED_FF`,
`DCMT_CAP_SPEED_FEEDFORWARD`, bread-crumbs-contracts#75). Per motor: the
static PWM, the output RPM per PWM step above it (x1000) and the bound on
the PI trim. The firmware stores them and applies them on the next
speed-mode pass, converting RPM per PWM to DCMotorTacho's counts/s per
PWM with each motor's CPR. Until a controller sends it the loop is a
plain PI (no feedforward, trim 255, no bound). A payload that isn't 8
bytes is refused `MALFORMED`, and a build without the speed loop refuses
it `NOT_BUILT`; the capability bit is advertised only where the speed
loop is built. Serial `SPEEDFF=static1,rpp1,trim1,static2,rpp2,trim2`
does the same, with `rpp` in RPM per PWM step. `firmware/README.md`
describes calibrating it from an open-loop PWM sweep.

- **`docs/hardware-revisions.md` records the motors' PWM frequencies**
(#68). On the Nano, motor 1 (D6, Timer0) switches at 976.6 Hz and
motor 2 (D10, Timer1) at 490.2 Hz; on the Nano Every both are
Expand Down Expand Up @@ -301,6 +314,29 @@ This project did not use formal release tags through most of its history, so thi

### Changed

- **Breaking: the speed loop drives PWM directly (module major 3).**
Requires DCMotorServo `^2.0.0` (feastorg/dcmotorservo#26) and
bread-crumbs-contracts `^0.13.0` (`firmware/platformio.ini` and
`slice.yaml`). The speed loop is now feedforward (SET_SPEED_FF) plus a PI
trim on the measured speed, instead of a PID moving the position target.
`DCMT_MODULE_VER_MAJOR` is 3, from the contracts, so a controller built
for major 2 refuses this firmware rather than sending gains in the old
units. SET_PID's speed gains, and serial `PIDSPEED=`, are now Kp in PWM
per RPM and Ki in PWM per RPM-second; Kd is accepted and stored but
unused. The defaults are Kp 0.6, Ki 3.0, Kd 0 (were 0.15, 0.2, 0.01 in
the old units). In the host motor model with the model's feedforward and
trim 20, 60 RPM is within 5 % 210 ms after the command, from 1.29 s on
`main`; after a 0.9 s hold the peak 1 s speed is 60.8 RPM, from 61.8.
At CPR 848 the old loop drove backwards for one sample on entering speed
mode after some open-loop reverse timings (15 of 101 timings from 550 to
750 ms); with this loop none does (`test_speed_entry_62 sweep`). Host
model only; not yet run on a board.
- **The encoder counts per turn default to 848** (#103), from 798.
`MOTOR1_CPR` and `MOTOR2_CPR` are the E-S Motor 36PG-3429-19-EN's
measured counts per output turn
([record](docs/bench/2026-11-02-bus-lockup-and-cpr.md)); at 798 every
RPM read and commanded was about 6 % high. Another motor sets its own
with `-DMOTOR1_CPR=` / `-DMOTOR2_CPR=`.
- Requires bread-crumbs-contracts `^0.12.0` (`firmware/platformio.ini` and
`slice.yaml`), the release that defines GET_INPUTS and
`DCMT_CAP_INPUTS`. It is additive: the four default builds are
Expand Down Expand Up @@ -404,6 +440,18 @@ This project did not use formal release tags through most of its history, so thi
is incremented on the released->pressed transition in `processEStop()`,
mirroring `wdTripCount`.

### Removed

- **The speed loop's lead cap and the integral freeze** (#63, #89).
DCMotorServo 2.0 has no position target to cap, so `setMaxLead()` and
`DCMT_TACHO_MAX_LEAD` are gone, and its conditional integration replaces
the firmware's stopgap that ran the speed PID with Ki 0 while the cap
clamped (`lead_capped()`, `speed_tunings()`). The trim bound from
SET_SPEED_FF now limits what a held shaft banks. Stall detection (#64) is
unchanged. Their host tests, `test_max_lead` and `test_speed_freeze`, go
with them; `test_stall_release_89` still checks for no lunge after a
short hold.

### Fixed

- **A build without `DCMT_HW_GEN` now fails instead of building gen1** (#17).
Expand Down
96 changes: 81 additions & 15 deletions firmware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,22 +89,27 @@ terminal reaches which pin is in `docs/hardware-revisions.md`.
The runtime uses:

- `DCMotorServo` for closed-loop position control
- `DCMotorTacho` for cascaded closed-loop speed control
- `DCMotorTacho` (DCMotorServo 2.x) for closed-loop speed control: a PI on
PWM around a per-motor feedforward (see Speed Loop)

These defaults are a starting point for one motor, not properties of the
firmware. They come from a single bench motor: the gains and servo values
are the archive implementation's, and the speed Ki was chosen against a
first-order model fitted to that motor (#90; the model lives in the host
motor tests, `firmware/test/host/motor_sim.h`). Tune them for each motor:
the PID gains at runtime with `SET_PID` (or serial `PIDPOS=` /
`PIDSPEED=`), and every value per build with a flag such as
firmware. They come from a single bench motor: the position gains and servo
values are the archive implementation's, and the speed gains were checked
against a first-order model fitted to that motor (#90; the model lives in
the host motor tests, `firmware/test/host/motor_sim.h`). Tune them for each
motor: the gains at runtime with `SET_PID` (or serial `PIDPOS=` /
`PIDSPEED=`), the speed feedforward with `SET_SPEED_FF` (or serial
`SPEEDFF=`), and every value per build with a flag such as
`-DDCMT_SPEED_PID_KI=<ki>` (each is `#ifndef`-guarded in
`include/config_hardware.h`).

- `MOTOR1_CPR = 798`
- `MOTOR2_CPR = 798`
- `MOTOR1_CPR = 848`
- `MOTOR2_CPR = 848` (measured on the bench, #103)
- outer (position) PID defaults: `0.15 / 0.01 / 0.01`
- inner (speed) PID defaults: `0.15 / 0.2 / 0.01`
- speed PI defaults: Kp `0.6` PWM per RPM, Ki `3.0` PWM per RPM-second
(Kd `0.0`, accepted and unused)
- speed feedforward default: none (static 0, RPM per PWM 0, trim 255): a
plain PI until a controller sends `SET_SPEED_FF`
- servo PWM skip: `15`
- servo max PWM: `150`
- servo accuracy: `10`
Expand All @@ -113,11 +118,71 @@ the PID gains at runtime with `SET_PID` (or serial `PIDPOS=` /

Speed commands/capability are only available when `DCMT_ENABLE_SPEED_LOOP=1`.

## Speed Loop

Module major 3 (bread-crumbs-contracts 0.13.0, DCMotorServo 2.0.0). Every
speed interval (`DCMT_TACHO_INTERVAL_MS`) each motor is driven at

```text
pwm = feedforward + trim
feedforward = sign(target) * static + target / rpm_per_pwm
trim = Kp * error + integral, bounded to +-trim
```

with error the target RPM less the measured RPM, and the result bounded to
the servo max PWM. The integral doesn't accumulate while the trim or the
output is at its bound in the error's direction, so a held shaft banks at
most `trim` of drive and doesn't lunge on release (#89). Stall detection
(see Stall Faults) still latches a shaft held for the stall timeout.

- **Gains:** `SET_PID` with `DCMT_PID_LOOP_SPEED`, or serial
`PIDSPEED=kp,ki,kd`. Kp is PWM per RPM of speed error and Ki PWM per
RPM-second. Kd is accepted and stored, and unused: the loop is a PI. Gains
from module major 2 firmware (DCMotorServo 1.x) don't carry over; they
were in the old loop's units.
- **Feedforward:** `SET_SPEED_FF` (`DCMT_OP_SET_SPEED_FF`, advertised as
`DCMT_CAP_SPEED_FEEDFORWARD`, bit 13, only where the speed loop is built),
8 bytes `[static1:u8][rpm_per_pwm1:u16][trim1:u8][static2:u8][rpm_per_pwm2:u16][trim2:u8]`:
- `static`: PWM that just starts the shaft turning, 0 to 255;
- `rpm_per_pwm`: output RPM per PWM step above `static`, x1000 (0 to
65.535); 0 is no speed term. The firmware converts it to the library's
counts/s per PWM step with the motor's CPR;
- `trim`: the bound on the PI trim, PWM; 255 is no bound. About 20 keeps
a held shaft from banking more than that.

Stored per motor and applied on the next speed-mode pass. A payload that
isn't 8 bytes is refused `MALFORMED`, and a build without the speed loop
refuses it `NOT_BUILT`; either stores nothing. Like `SET_PID`, it is a
configuration write and is taken during a watchdog trip, e-stop or
fault. There is no readback.
- **Serial:** `SPEEDFF=static1,rpp1,trim1,static2,rpp2,trim2`, the same six
fields in the same order, with `rpp` in RPM per PWM step as a decimal
(`SPEEDFF=10,1.719,20,10,1.719,20`). Only where the speed loop is built.

### Calibrating the feedforward

The values come from the motor and its load, so there are no useful
defaults. In open loop, sweep the PWM in steps (`SET_OPEN_LOOP` or
`M1PWM=`) and at each step read the steady speed from GET_STATE (or `READ`)
once it settles. Fit a line, RPM = `rpm_per_pwm` * (PWM - `static`), over
the PWMs where the shaft turns: `rpm_per_pwm` is the slope and `static`
where the line crosses 0 RPM (the intercept on the PWM axis). Err towards
less drive: a lower `static` and a higher `rpm_per_pwm`. Feedforward that
under-drives costs a little start-up time, which the trim closes, while
over-drive overshoots at start-up. If the sweep is done in both directions,
take the lower `static` and the higher `rpm_per_pwm` of the two.

In the host motor model (24.3 counts/s per PWM above a deadband of 10 PWM,
at CPR 848) that is `static` 10 and `rpm_per_pwm` 24.3 * 60 / 848 = 1.719,
and with trim 20 and the default gains 60 RPM is within 5 % 210 ms after
the command (`test_speed_baseline_90`). Not yet checked on a board.

## Capability Mapping (`GET_CAPS`)

- Level 1: baseline (open loop and brakes); the `_noenc` and `_analog1` envs
- Level 2: baseline + closed-position + PID tuning
- Level 3: Level 2 + closed-speed
- Level 3: Level 2 + closed-speed, with `DCMT_CAP_SPEED_FEEDFORWARD`
(`SET_SPEED_FF`)

Controllers should gate behavior by capability flags, not by generation labels.

Expand Down Expand Up @@ -155,12 +220,12 @@ bread-crumbs-contracts `bread_refuse.h`:

| Reason | When |
| --- | --- |
| `BREAD_REFUSE_MALFORMED` | the payload does not unpack; SET_PID is not 13 bytes; SET_SETPOINT is not 4 or 5 bytes; SET_WATCHDOG lacks its u16; CLEAR_WATCHDOG_TRIP or CLEAR_FAULTS carries a payload |
| `BREAD_REFUSE_MALFORMED` | the payload does not unpack; SET_PID is not 13 bytes; SET_SPEED_FF is not 8 bytes; SET_SETPOINT is not 4 or 5 bytes; SET_WATCHDOG lacks its u16; CLEAR_WATCHDOG_TRIP or CLEAR_FAULTS carries a payload |
| `BREAD_REFUSE_ESTOP` | SET_OPEN_LOOP, SET_MODE or SET_SETPOINT while the hardware e-stop is held, or a SET_BRAKE that asks for a release then; this reason wins when a watchdog trip is held too |
| `BREAD_REFUSE_WATCHDOG_TRIP` | SET_OPEN_LOOP, SET_MODE or SET_SETPOINT while a trip is held, or a SET_BRAKE that asks for a release then; this reason wins over a latched fault |
| `BREAD_REFUSE_FAULT` | SET_OPEN_LOOP, SET_MODE or SET_SETPOINT while a stall fault is latched, or a SET_BRAKE that asks for a release then |
| `BREAD_REFUSE_UNDEFINED_VALUE` | a SET_MODE, SET_PID or SET_SETPOINT loop the contract does not define; a SET_WATCHDOG flag other than `BREAD_WATCHDOG_FLAG_COMMANDS_ONLY`; a SET_OPEN_LOOP PWM outside `-DCMT_PWM_MAX` to `DCMT_PWM_MAX` (255) |
| `BREAD_REFUSE_NOT_BUILT` | closed-loop speed (SET_MODE, or a SET_PID or SET_SETPOINT loop SPEED) when the speed loop is not built |
| `BREAD_REFUSE_NOT_BUILT` | closed-loop speed (SET_MODE, or a SET_PID or SET_SETPOINT loop SPEED) and SET_SPEED_FF when the speed loop is not built |
| `BREAD_REFUSE_WRONG_MODE` | SET_OPEN_LOOP outside open loop |
| `BREAD_REFUSE_UNKNOWN_OP` | an opcode with no registered command handler, including a GET opcode sent as a command frame instead of through SET_REPLY |

Expand Down Expand Up @@ -213,8 +278,9 @@ not stored: `SET_OPEN_LOOP`, `SET_SETPOINT`, `SET_MODE` and a `SET_BRAKE`
that would release a brake, and over serial `MODE=`, `M1PWM=`/`M2PWM=`,
`M1POS=`/`M2POS=`, `M1SPEED=`/`M2SPEED=`, `BRAKE1=0` and `BRAKE2=0`.
The I2C ones are refused with `BREAD_REFUSE_WATCHDOG_TRIP`, so GET_ACK says
so (see Refused Commands). Engaging a brake, PID tuning (`SET_PID`,
`PIDPOS=`, `PIDSPEED=`), `SET_WATCHDOG`/`WDOG=` and `READ` still work.
so (see Refused Commands). Engaging a brake, PID and feedforward tuning (`SET_PID`, `SET_SPEED_FF`,
`PIDPOS=`, `PIDSPEED=`, `SPEEDFF=`), `SET_WATCHDOG`/`WDOG=` and `READ` still
work.

Clearing a trip resumes nothing; the brakes stay engaged and no motor
moves. To resume:
Expand Down
39 changes: 19 additions & 20 deletions firmware/include/config_hardware.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
#define SERIAL_UPDATE_TIME_MS 1000

// Closed-loop tuning defaults. These, and the other motor-specific values
// below (counts/rev, PWM skip and cap, accuracy, the tacho interval and lead
// cap, the stall thresholds), come from a single bench motor: the PID gains
// and servo values are the archive implementation's, and the speed Ki was
// chosen against a first-order model fitted to that motor (#90, the host
// below (counts/rev, PWM skip and cap, accuracy, the tacho interval, the
// stall thresholds), come from a single bench motor: the position gains and
// servo values are the archive implementation's, and the speed gains were
// checked against a first-order model fitted to that motor (#90, the host
// motor tests). They are a starting point, not a property of the firmware:
// tune them per motor at runtime with SET_PID / PIDPOS= / PIDSPEED=, or per
// build with flags, e.g. -DDCMT_SPEED_PID_KI=<ki>.
Expand All @@ -55,17 +55,18 @@
#define DCMT_POS_PID_KD 0.01f
#endif

// Speed gains (module major 3, DCMotorServo 2.x): the speed loop is a PI on
// PWM around the feedforward SET_SPEED_FF sets. KP is PWM per RPM of speed
// error, KI PWM per RPM-second; KD is accepted and stored but not used.
// These are DCMotorTacho's own defaults.
#ifndef DCMT_SPEED_PID_KP
#define DCMT_SPEED_PID_KP 0.15f
#define DCMT_SPEED_PID_KP 0.6f
#endif
// 0.01 left the speed loop P-only for seconds: 60 RPM settled near 69 % of
// the target and took about 20 s to close the rest. 0.2 reaches it within
// 5 % in about 1.3 s in the model (#90).
#ifndef DCMT_SPEED_PID_KI
#define DCMT_SPEED_PID_KI 0.2f
#define DCMT_SPEED_PID_KI 3.0f
#endif
#ifndef DCMT_SPEED_PID_KD
#define DCMT_SPEED_PID_KD 0.01f
#define DCMT_SPEED_PID_KD 0.0f
#endif

#ifndef DCMT_SERVO_PWM_SKIP
Expand All @@ -83,13 +84,6 @@
#define DCMT_TACHO_INTERVAL_MS 5
#endif

// How far, in encoder counts, the speed loop may drive the position target
// ahead of the encoder. Without a cap a held shaft banks travel and lunges
// on release; 300 removed that on the 2026-10-22 bench (#63).
#ifndef DCMT_TACHO_MAX_LEAD
#define DCMT_TACHO_MAX_LEAD 300
#endif

// Stall faults (#64, DCMT_CAP_FAULTS). In closed loop, a motor driven for
// DCMT_STALL_TIMEOUT_MS without its encoder moving DCMT_STALL_MIN_COUNTS
// counts latches DCMT_FAULT_M1_STALL or _M2_STALL, which brakes both motors
Expand Down Expand Up @@ -171,12 +165,17 @@
#error "MOTOR2_PWM_PIN has no hardware timer on this MCU -- analogWrite would degrade to on/off. See docs/hardware-revisions.md"
#endif

// Encoder counts-per-rev (archive-proven closed-loop value).
// Encoder counts per output-shaft revolution. 848 is the E-S Motor
// 36PG-3429-19-EN measured on the bench on 2026-11-02 (#103): -16 008
// counts left a flag on its output shaft 1/8 turn from where it started,
// 18.875 turns (about +-1.2 counts). The datasheet's 12 PPR x 4 x 1:19 would
// be 912, and the earlier 798 read speeds about 6 % high. Another motor sets
// its own with -DMOTOR1_CPR= / -DMOTOR2_CPR=.
#ifndef MOTOR1_CPR
#define MOTOR1_CPR 798
#define MOTOR1_CPR 848
#endif
#ifndef MOTOR2_CPR
#define MOTOR2_CPR 798
#define MOTOR2_CPR 848
#endif

#endif // HARDWARE_CONFIG_H
16 changes: 16 additions & 0 deletions firmware/include/globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ struct PIDTunings
PIDTunings(float p, float i, float d) : kp(p), ki(i), kd(d) {}
};

// One motor's speed-loop feedforward, as SET_SPEED_FF carries it: static
// PWM, output RPM per PWM step above static x1000 (0 = no speed term), and
// the bound on the PI trim in PWM (DCMT_SPEED_FF_TRIM_UNBOUNDED = none).
// The defaults are no feedforward and no bound: a plain PI on PWM.
struct SpeedFeedforward
{
uint8_t pwmStatic = 0;
uint16_t rpmPerPwmX1000 = 0;
uint8_t trim = 255;
};

// ---- Shared state structs ----
struct DCMT_SLICE
{
Expand All @@ -47,6 +58,10 @@ struct DCMT_SLICE
PIDTunings posPid2 = {DCMT_POS_PID_KP, DCMT_POS_PID_KI, DCMT_POS_PID_KD};
PIDTunings speedPid1 = {DCMT_SPEED_PID_KP, DCMT_SPEED_PID_KI, DCMT_SPEED_PID_KD};
PIDTunings speedPid2 = {DCMT_SPEED_PID_KP, DCMT_SPEED_PID_KI, DCMT_SPEED_PID_KD};
#if DCMT_ENABLE_SPEED_LOOP
SpeedFeedforward speedFf1;
SpeedFeedforward speedFf2;
#endif
};

struct Timing
Expand Down Expand Up @@ -110,6 +125,7 @@ void handler_set_brake(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *dat
void handler_set_mode(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *data, uint8_t data_len, void *user_data);
void handler_set_setpoint(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *data, uint8_t data_len, void *user_data);
void handler_set_pid(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *data, uint8_t data_len, void *user_data);
void handler_set_speed_ff(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *data, uint8_t data_len, void *user_data);
void handler_set_watchdog(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *data, uint8_t data_len, void *user_data);
void handler_clear_watchdog_trip(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *data, uint8_t data_len, void *user_data);
void handler_clear_faults(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *data, uint8_t data_len, void *user_data);
Expand Down
4 changes: 2 additions & 2 deletions firmware/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ upload_speed = 115200
monitor_speed = 115200
lib_deps =
cameronbrooks11/CRUMBS @ ^0.17.0
cameronbrooks11/bread-crumbs-contracts @ ^0.12.0
cameronbrooks11/bread-crumbs-contracts @ ^0.13.0
cameronbrooks11/LMD18200 @ ^0.1.5
cameronbrooks11/DCMotorServo @ ^1.2.1
cameronbrooks11/DCMotorServo @ ^2.0.0
paulstoffregen/Encoder @ ^1.4.4
cameronbrooks11/bread-slice-core @ ^0.2.0
build_flags =
Expand Down
33 changes: 32 additions & 1 deletion firmware/src/dcmt_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ void handler_set_pid(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *data,
#if DCMT_ENABLE_SPEED_LOOP
else if (v.loop == DCMT_PID_LOOP_SPEED)
{
// Module major 3: kp PWM per RPM, ki PWM per RPM-second. kd is
// stored but the speed loop (a PI) doesn't use it.
slice.speedPid1 = {kp1, ki1, kd1};
slice.speedPid2 = {kp2, ki2, kd2};
}
Expand All @@ -309,6 +311,35 @@ void handler_set_pid(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *data,
#endif
}

// SET_SPEED_FF (DCMT_CAP_SPEED_FEEDFORWARD): the speed loop's per-motor
// feedforward and trim bound, as SET_PID is its gains. Like SET_PID it is a
// configuration write, taken during a hold: a malformed payload is refused
// MALFORMED, a build without the speed loop refuses it NOT_BUILT, and
// either stores nothing. Every value a field can hold is defined, so there
// is no UNDEFINED_VALUE. The loop applies it on the next speed-mode pass.
void handler_set_speed_ff(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *data, uint8_t data_len, void *user_data)
{
dcmt_set_speed_ff_t v;
(void)opcode;
(void)user_data;

if (dcmt_parse_set_speed_ff_payload(data, data_len, &v) != 0)
{
(void)crumbs_refuse(ctx, BREAD_REFUSE_MALFORMED);
return;
}
#if DCMT_ENABLE_SPEED_LOOP
slice.speedFf1.pwmStatic = v.static1;
slice.speedFf1.rpmPerPwmX1000 = v.rpm_per_pwm1_x1000;
slice.speedFf1.trim = v.trim1;
slice.speedFf2.pwmStatic = v.static2;
slice.speedFf2.rpmPerPwmX1000 = v.rpm_per_pwm2_x1000;
slice.speedFf2.trim = v.trim2;
#else
(void)crumbs_refuse(ctx, BREAD_REFUSE_NOT_BUILT);
#endif
}

void handler_set_watchdog(crumbs_context_t *ctx, uint8_t opcode, const uint8_t *data, uint8_t data_len, void *user_data)
{
uint16_t timeout_ms = 0;
Expand Down Expand Up @@ -479,7 +510,7 @@ void reply_get_caps(crumbs_context_t *ctx, crumbs_message_t *reply, void *user_d

#if DCMT_ENABLE_SPEED_LOOP
level = DCMT_CAP_LEVEL_3;
flags |= DCMT_CAP_CLOSED_LOOP_SPEED;
flags |= DCMT_CAP_CLOSED_LOOP_SPEED | DCMT_CAP_SPEED_FEEDFORWARD;
#endif
#if DCMT_HAS_ANALOG_INPUTS
flags |= DCMT_CAP_INPUTS;
Expand Down
Loading
Loading