IO - Murray-Bridge-Bunyips/BunyipsLib GitHub Wiki
In BunyipsLib, there are various utility classes that handle inputs and outputs. Thes features span from external features such as FtcDashboard, to items such as encoders and reading from input devices. This article generalises some of the most important operations related to I/O.
Gamepad objects in BunyipsLib can be replaced with the more modern Controller class. This class provides customizability of bindings at the gamepad level, including setting your own unary functions for mapping, and predicates for button selection.
Important
gamepad1 and gamepad2 of BunyipsOpMode have been overridden to be Controller instances internally. No additional setup is required and this section will assume these gamepads are instances of Controller. For manual implementations, one will need to call the update()
method periodically.
Tip
Controller provides shorthand aliases for gamepad inputs. See them in the JavaDoc.
Unary functions can be mapped to gamepad inputs; for instance, gamepads are initially mapped to the standard identity/linear function (x) -> x:
gamepad1.set(Controls.Analog.LEFT_STICK_Y, (x) -> x); // default behaviourImportant
Unrelated to the Controller, but do note the y-sticks, including left_stick_y, and right_stick_y, are inverted, meaning pushing them fully up will return -1 and fully down 1. This is an SDK thing which I haven't quite worked out why it exists.
Tip
There exists some built-in UnaryFunction instances you can use, including IDENTITY, SQUARE, SQUARE_KEEP_SIGN, CUBE, CUBE_KEEP_SIGN, and NEGATE.
The most common mapping is to use quadratic controls, such that
This function is available as the built-in UnaryFunction.SQUARE_KEEP_SIGN instance.
To apply this function, it is passed into the set method with either an analog stick, as shown before, or you can use an AnalogGroup to define it for both sticks, both triggers, or all analog controls.
gamepad1.set(Controls.AnalogGroup.STICKS, UnaryFunction.SQUARE_KEEP_SIGN);
// function applied to all future gp1 left_stick_y, left_stick_x, right_stick_x, right_stick_y accessesFor buttons, this works similarly but instead uses a Predicate<Boolean> functional interface, for example:
gamepad2.set(Controls.A, (pressed) -> !pressed); // inverts gamepad2.aYou can also bind controls to other controls and even gamepads, for some reason. Might be useful for a system that switches around bindings depending on some condition.
gamepad1.set(Controls.X, (p) -> gamepad2.x); // not sure why you'd do this, but it's possible
gamepad1.set(Controls.Analog.LEFT_STICK_Y, (x) -> gamepad2.lsy);Controller instances also have the getDebounced method, which will run rising-edge detection on a particular button. This detection initially ignores the first call to the method, which is a caveat that can be used in command-based TeleOp to make a trigger bind. Read the accompanying JavaDoc for more info.
// e.g.
@Override
protected void activeLoop() {
if (gamepad1.a) {
// executes every loop that A is held
}
if (gamepad1.getDebounced(Controls.A)) {
// only executes once per press
}
if (gamepad1.aWasPressed()) {
// alternative to the getDebounced but stateless (SDK feature)
}
if (gamepad1.aWasReleased()) {
// executes once when A is released (SDK feature)
}
}There exists two Encoder classes that you might use throughout BunyipsLib for encoder operations. Do note that normally, you won't have to interface or instantiate these encoder classes yourself, as the Motor class internally uses an Encoder and other use cases rely on the built-in encoder interface via DcMotorEx.
Note
Unrelated to BunyipsLib, but high-resolution encoders, such as odometry pods, should be attached to ports 0 and 3, as Hubs only have two reliable quadrature encoder ports. Encoder counts will be missed for high resolution encoders on ports 1 and 2. You can read more about this here.
The Encoder class in BunyipsLib has several features, including:
- Independent setting of encoder direction (without setting motor power direction)
- Accumulation, meaning a "last known position" can be set and tracked from there
- Extraction of acceleration information (with a built-in low-pass filter)
The other encoder class, offered by RoadRunner, the RawEncoder, is used for RoadRunner drive instances. It is not used elsewhere. More information can be found in the RoadRunner section.
Note
Also unrelated to BunyipsLib directly, but if velocity readings on an encoder exceed 32767 ticks/sec, they will overflow. The Encoder interfaces presented here have correction methods (RoadRunner does it internally by wrapping the RawEncoder you supply into an OverflowEncoder), and the standard BunyipsLib encoder has the useEncoderOverflowCorrection() method to enable correction.
The Inertial Measurement Unit is essential for gathering heading information from a robot. It can be accessed with the IMU interface, as per the SDK.
An additional hardware device extension exists for advanced IMU operations, called the IMUEx.
The IMUEx provides field-level readings for IMU readings, as well as setting the desired domain for yaw (either -180,180, 0,360, or 0,inf). Optionally supports multithreaded reads, but this can damage performance due to bus locks, so it is strongly not recommended. IMUEx also supports the BunyipsLib-obsolete LazyImu interface from RoadRunner.
Lazy initialisation can still be performed for an IMU via IMUEx. To allow lazy initialisation, replace your initialize method with lazyInitialize, which will delay initialisation until a getter is called.
// In a RobotConfig
hw.imu = getHardware("imu", IMUEx.class, d -> {
d.lazyInitialize(...); // Same as initialize but will lazy-init
d.setRefreshRate(Milliseconds.of(300)); // Extra IMUEx specific features!
});IMUEx also allows for a "null IMU", available through the static IMUEx.none(), which will replicate an empty IMU for applications where an IMU is not required but still needed to be passed in.
For standard operations, the IMU interface from the SDK works perfectly well. For advanced operations such as lazy-init, refresh rate adjustments, and extended functionality, IMUEx is recommended. IMUEx implements IMU so it does not have any downsides.
Limit switches are hardware devices that can be accessed through the TouchSensor class. It is defined like any other HardwareDevice and initialised like one.
Note
Unrelated to BunyipsLib, but REV Touch Sensors use the N+1th port when configuring the digital channels.
Sometimes, the isPressed() method on the TouchSensor will be inverted, so not pressing the switch will return true and vice versa. Internal implementations that use the TouchSensor assume that pressed is always true. You can combat this using an InvertibleTouchSensor. As the name implies, it will invert the readings of a TouchSensor and re-expose it with the same TouchSensor interface.
Tip
InvertibleTouchSensor can be grabbed in a RobotConfig the same as you would a normal TouchSensor via getHardware().
Servos in BunyipsLib can be controlled either with the stock Servo interface, or a ServoEx can be used.
Note: from BunyipsLib v5.1.0 to v6.1.0,
ServoExis known as theProfiledServo.
A ServoEx is the same as the regular Servo interface, but adds these additional features:
- Motion profiling, allowing you to control the velocity and acceleration of a servo
- Position threshold for hardware optimisations
- Refresh rate for additional minor hardware optimisations
An example of limiting a ServoEx instance to half speed (0.5 pos/sec, 1 pos/sec/sec) is shown, which internally uses a TrapezoidalProfile:
// Units are measured in the servo position as given to setPosition() usually,
// where 0 and 1 are left and right on the servo programmer
servoEx.setConstraints(new TrapezoidProfile.Constraints(0.5, 1.0));
// Future calls to setPosition will follow the motion profileCaution
As ServoEx now uses a motion profile, it is important that setPosition is called periodically on every loop, otherwise the profile won't be able to update. BunyipsLib implementations that use servo instances already ensure that setPosition is executed periodically.
In FTC, one of the most important aspects of making proper robot code is optimising loop times. A loop time is the time taken for a single iteration of the main loop in an OpMode. This time is crucial for ensuring that the robot can respond to inputs quickly and accurately, while also updating PID controllers and odometry systems in a timely manner.
Tip
Slow loop times are not usually caused by software implementations and libraries, but instead hardware reads and writes.
Loop times are shown automatically in the presence of MovingAverageTimer and DualTelemetry instances, such as in standard operation of a BunyipsOpMode. More information on the standard telemetry output is available in the Telemetry section below.
BunyipsLib takes care of many optimisations internally, including using manual bulk caching for motor reads as part of the BunyipsOpMode. This is the main optimisation to ensure loop times are as fast as possible.
BunyipsLib offers a few caching options that you can use to optimise your loop times further:
The Motor class, described in more detail in the Motor section, also has a few optimisations that can be used to speed up loop times. The two main ones are power caching and refresh rate.
Power caching is a feature that caches the last power set to the motor, and defines a tolerance around the power that will not update the motor if a newly commanded power is within this tolerance. The SDK already ignores values that are the exact same as the last call, but not those that are near the last call.
By caching, unnecessary writes to the motor controller are ignored, as requesting a power of 0.5001 when the motor is already at 0.5 is not going to impact your motor output.
Tip
Power caching is disabled by default in a Motor, enable it with a tolerance using setPowerDeltaThreshold(double). This value should realistically be around 0.005-0.02, depending on how precise you need your motor power to be. Also note that input powers of -1, 0, and 1 will ignore this threshold for safety.
You can also choose to set a refresh rate (minimum period between writes) for the motor. This optimisation is not as useful as power caching (as it will introduce lag), but can be used to ensure that the motor controller is not written to too frequently. The method for this is setPowerRefreshRate(Measure<Time>).
Note
For CRServos, you can use the SimpleRotator to get these optimisations (Motor extends SimpleRotator internally so you don't lose out on any optimisations - you can also choose to use SimpleRotator alone if you don't need the Motor features).
As exemplified in the section above, ServoEx also has some optimisations that can be used to speed up loop times. These are similar to the Motor optimisations, but are instead applied to servo positions.
It is enabled through the setPositionDeltaThreshold(double) method, which sets a tolerance around the last position set to the servo.
Like power refresh rate, refresh rate for position can also be applied to a ServoEx to ensure writes have a minimum period between them. This is accessed through the setPositionRefreshRate(Measure<Time>) method.
Note
Unrelated to BunyipsLib, servo ranges can be increased to their full extent as the robot Hub only enforces a PWM range from 600-2400 microseconds. It can be expanded to 500-2500 microseconds which can give your servo the full range it was designed for. You can do this by calling setPwmRange(new PwmRange(500, 2500)) on a ServoEx or ServoImplEx instance.
For USB devices, like cameras, processing is done on another thread, thus loop time is not affected by camera processing.
Note
Unrelated to BunyipsLib, the USB 2.0 port should never be used. This port shares the ground with the Wi-Fi chip on the hub, and static shock by this USB device will kill your robot connection.
Another optimisation is a library called Photon, which can be integrated by following the instructions on the Photon GitHub page. Photon paralellizes hardware reads and writes, and is developed by Eeshwar, alum from FTC 7244.
Caution
Photon is extremely buggy and not recommended for stable use. Photon may cause servos to not function while the library is active, and may cause motors to behave erratically. It is recommended to not use Photon in any official capacity.
The Control and Expansion Hub have a singular Status LED attached to them. This light blinks in correspondence with the connection and voltage condition of the robot.
BunyipsLib hooks into these lights during the execution of BunyipsOpMode and other BunyipsLib-integrated OpModes, to provide safety warnings to others around and working with the robot. This adds functionality very similar to the FRC Robot Signal Light.
Note
The standard REV Status LED Blink Codes are listed here. Important blink codes, notably the low voltage one, override the custom blink patterns as defined in BunyipsLib.
| Light Pattern | Light Description | When | Status |
|---|---|---|---|
|
Blue or green, flashing or solid | Idle or in custom OpMode | Standard Status LED Blink Codes are in effect. The robot is switched on. |
![]() |
Flashing orange | Anytime | The robot is on low voltage and the battery should be recharged. This is a standard Status LED Blink Code. |
![]() |
Solid cyan | BunyipsOpMode initialisation | The robot is currently in static_init, initialising hardware and firing onInit(). This phase should not last long enough for it to be seen. The robot is able to issue hardware commands but is unlikely. |
![]() |
Flashing cyan | BunyipsOpMode initialisation | The robot is currently in dynamic_init, running initialisation loops such as waiting for a UserSelection or doing some other work such as vision. The robot may move during this phase to home actuators and servos. |
![]() |
Alternating green and cyan | BunyipsOpMode initialisation | The robot has completed initialisation and is ready waiting for the PLAY button to be pressed. Hardware loops have been inhibited to 5 Hz. |
![]() |
Solid yellow | BunyipsOpMode initialisation | The robot has completed initialisation and is waiting for the PLAY button to be pressed, but an exception was caught and handled during the initialisation routine. Check the Driver Station as this may be a gamepad zeroing fault (the DS will say "check gamepads") or programmer error. |
![]() |
Fast flashing green | BunyipsOpMode active loop | The robot is currently running. Do not work on the robot while this pattern is displayed as all actuator command systems are live. |
![]() |
Solid yellow | BunyipsOpMode active loop |
DebugMode has been enabled by the programmer and has tripped from a pre-set condition. Robot functionality may be inhibited or limited. |
![]() |
Solid light gray/white | BunyipsOpMode | The robot has finished execution via finish(). Hardware calls are actively inhibited at 2 Hz during this phase and the OpMode is awaiting STOP by the Driver Station. |
![]() |
Slow flashing light gray/white | BunyipsOpMode | The robot has finished execution via finish() and is no-oping until STOP. Hardware calls are not being inhibited during this time as the parameter to inhibit hardware was disabled by the programmer. |
![]() |
Solid red | BunyipsOpMode | A critical uncaught exception has occurred where BunyipsOpMode has terminated execution. A stacktrace is available on the Driver Station and Logcat. |
![]() |
Altn. green and light gray/white | HardwareTester, RoadRunnerTuningOpMode | Hardware Tester or RoadRunner tuning is active. Motors and actuators may move at any time at the operator's discretion, and interfering can impact tuning accuracy. |
FtcDashboard is an external dependency used within BunyipsLib to accelerate development. It is essential in the operation of several systems, including RoadRunner tuning, live vision previews, field overlay, and live adjustment of variables.
Important
FtcDashboard can be accessed on the URL http://192.168.43.1:8080/dash for a Control Hub while connected to the robot.
Configuration variables are special fields that the dashboard client can seamlessly modify while the app is running. To mark a field as a config variable, declare it `static` and not `final` and annotate the enclosing class with `@Config`.
@Config
public class RobotConstants {
public static int MAGIC_NUMBER = 32;
public static PIDCoefficients TURNING_PID = new PIDCoefficients();
// other constants
}It’s conventional to name variables in uppercase and treat them as constants inside the code. While saved dashboard changes instantly apply to the code fields, code-side changes only propagate to the client on explicit refresh.
Also, keep the copy semantics of Java primitives in mind when using this feature, value vs. reference behaviour that occurs when passing a value into functions.
FtcDashboard also supports gamepad input, however, the binding system can be axially incorrect and using a modified version of the client will be required. The wiki does not cover this process.
A live camera preview is possible by calling .startPreview() on a Vision instance, which will start a new SwitchableVisionSender.
Caution
FtcDashboard is not legal for use during official matches as per rule R706 (as of CM 2024 V6). It is otherwise permitted for use in the pits, or in any workshop environment. You can disable the dashboard from accepting connections by running the Enable/Disable Dashboard OpMode in the TeleOp list.
By default, telemetry is broadcasted from user code to the Driver Station. For many applications, this is perfectly fine behaviour; however, attempting to get this telemetry to also display on FtcDashboard, often for tests without a Driver Station, requires extra code.
DualTelemetry is a class that properly fuses FtcDashboard and Driver Station telemetry, unlike the built-in MultipleTelemetry alternative which is half-baked.
Tip
From contexts where access to DualTelemetry is ambiguous, such as in a subsystem, you can use the static utilities smartLog and smartAdd to add logs or telemetry to both the Driver Station and Dashboard. Using a DualTelemetry instance is not necessary to use these utilities.
DualTelemetry also comes with several features including integrated functional CSS styling for depth in messages, and the ability to use telemetry as a normal object while having smart parsing for FtcDashboard. It is expected that through the use of DualTelemetry, all Driver Station telemetry is also shown on the dashboard.
Tip
BunyipsOpMode instances override the telemetry field under the hood to supply a DualTelemetry instance. No additional setup is required to use this class, since DualTelemetry extends the Telemetry interface.
Using methods on DualTelemetry is the same as it would be for normal telemetry, exposing one additional method, add(). This method works similarly to addLine() and addData(), but removes the need to input a tag as used in addData(). The add() method returns an HtmlItem instance, which can use a builder-like pattern to apply CSS styles. (e.g. telemetry.add("hi!").bold();)
Important
All format strings in DualTelemetry (and across BunyipsLib) uses the Text utility for string formatting. Review it in the Utilities section.
DualTelemetry also prettifies telemetry, providing you with status bars for elapsed time, loop time, gamepad input, and OpMode state. No additional setup is needed to use these features on a BunyipsOpMode.
A figure is provided labelling the standard output of a BunyipsOpMode with no extra telemetry calls.
Warning
Some features on DualTelemetry, such as addLine() and accessing .log().add() are no longer supported. They will raise deprecation warnings (the method list here is not exhaustive). Instead, use the alternative as listed by the JavaDoc (such as using add() or .log(message)).
Many applications throughout FTC require the use of control efforts to regulate the state of a system. For example, a control algorithm that minimises positional error of a lift, or velocity of a flywheel are examples where control efforts are required.
BunyipsLib provides an interface, called the SystemController, that represents any type of open-loop or closed-loop control. This interface is passed into several of the built-in functions and tasks of BunyipsLib, especially those which perform correction on measured error.
You can review the API documentation of this interface here.
The SystemController is implemented through several algorithms. These implementations can be combined with the use of the composition API. An image is shown below of the difference between feedback and feedforward in a control loop. Specific implementations of the SystemController are explained after introducing the Motor class.
For effectively every application in FTC, you will be reaching for a motor object to interface with a motor connected to a port.
By default, you will be utilising the DcMotor (and extension variant DcMotorEx).
These motor interfaces come with four separate modes, as described in the official Javadoc.
Important
While the raw power mode is called RUN_WITHOUT_ENCODER, it does not imply the motor will run with encoders not tracking - it simply means it will not use encoders to regulate power output internally. Consult the table below for a description of each DcMotor.RunMode. Modes that use a SystemController will return true as being classified as a PID mode (via RunMode.isPIDMode()).
| Mode | Purpose |
|---|---|
RUN_WITHOUT_ENCODER |
Runs the motor at whatever speed is achieved by applying a fraction (in [-1, 1]) of the available voltage to the motor. |
RUN_USING_ENCODER |
Attempts to run the motor at the specified signed velocity, either through setVelocity or as a fraction of the maximum declared velocity of the motor in ticks per second through setPower. An encoder is required for this mode. |
RUN_TO_POSITION |
Rotates the motor in whatever direction to cause the encoder reading to advance or retreat to the setTargetPosition position. An encoder is required for this mode. |
STOP_AND_RESET_ENCODER |
Transient mode that resets the encoder reading back to 0 and removes power temporarily. Note that standard encoder readings persist until the robot is power cycled. Note for the Motor, the Motor.resetEncoder() method is internally called. |
In subsystems such as the HoldableActuator, the RUN_TO_POSITION mode is used to run a motor from its current position to a target position.
The DcMotor class regulates the default implementation of RUN_TO_POSITION. However, this run mode has a few major limiting factors.
- Limited update cycle locked at 20Hz
While the RUN_TO_POSITION PID controller is run at the firmware level, not affecting the main loop times, it is - for some reason - hard locked at a 20Hz update cycle. This is very slow for a PID controller, and can cause unexpected behaviours due to too slow of a polling time. This video describes how polling interval affects the output of a PID controller.
- Coefficients are limited
Since the controller is built into the hub, the coefficients for it can be changed but custom terms such as kG, kF, etc cannot be added to the system on the controller level. This means all you have for a RUN_TO_POSITION is a kP term that sets a velocity gain, which is very slow and hard to work with.
Solution: The Motor
Without having to rewrite the internal code structures using RUN_TO_POSITION, BunyipsLib provides a drop-in replacement for the DcMotor. It leverages the same API surface as the DcMotor to provide SystemController access, as described in the previous section.
Simply pass through a system controller using the methods setRunToPositionController or setRunUsingEncoderController for either mode (one is for the RUN_TO_POSITION positional tracking, one is for velocity tracking RUN_USING_ENCODERS), and calls to setPower will update these controllers.
Note
Since Motor is not bound by the locked 20Hz update loop, and instead your OpMode loop time, update cycles can easily be tripled! Blazing!
Using a RobotConfig allows an extremely simple way of getting a Motor instance up and running.
// You can access Motor directly as if you were to get a DcMotor!
protected void onRuntime() {
hw.motor = getHardware(Motor.class, "motor", d -> {
// perform initialisation as normal, while setting your system controllers!
// do note an exception will be thrown if you try to use a system controller on a Motor that has not been set.
});
}Caution
In order to update the internal system controllers, it is important that setPower is called periodically on every loop. BunyipsLib implementations that use motor instances already ensure that setPower is executed periodically.
Motor also supports several loop time optimisation techniques, including power caching, refresh rate, and other utilities such as power rescaling, voltage compensation, and gain scheduling using an interpolated lookup table. Read the API docs for more information on these methods.
Note
Motor works by taking the desired mode and otherwise ignoring it on the actual motor, the underlying object of the actual motor on the firmware is always running in RUN_WITHOUT_ENCODER mode, allowing maximum speed control by your control algorithm.
The primary implementations that you will likely use of the SystemController are the PID feedback controller and various feedforward controllers.
The main feedback controller is the PID(F) controller. The PID controller is the most well-known controller in control theory, supplying control efforts to minimise the error in a system. The F in the PIDF controller is an additional feedforward parameter which is built into PIDF, equivalent to the kV feedforward. It is not considered a feedback element. The three main gains are P for Proportional (error multiple), I for Integral (error sum) and D for Derivative (error rate).
Let
Tip
To learn more about the PID controller, have a look at this Game Manual 0 page.
The most important part of any system controller is the tuning process.
The SystemController interface exposes the setCoefficients method to set the coefficients for any system controller.
setCoefficients is a generic method that takes in a number of double arguments, with the specification of how many arguments being defined by the controller itself at runtime.
A common pattern is to have PID coefficients update a Motor instance based on inputs from FtcDashboard, live from any BunyipsOpMode via the RobotConfig as shown below. Note this is for a PID controller handling position controls.
@Config // for FtcDashboard tuning kP kI kD
public class Robot extends RobotConfig {
public static double kP = 1, kI = 0, kD = 0.001; // example coefficients
@Override
protected void onRuntime() {
motor = getHardware("motor", Motor.class, (d) -> {
// any standard motor setup...
PIDController pid = new PIDController(kP, kI, kD);
d.setRunToPositionController(pid); // note: for a position pid
BunyipsOpMode.ifRunning(o -> o.onActiveLoop(() -> {
// important line below! will update the PID coefficients dynamically on the execution of any BOM.
// Do note if using a composite controller (as described later),
// the coefficients will be the combination of all internal coefficients
pid.setCoefficients(kP, kI, kD, 0.0);
// optional but recommended: for graphing on FtcDashboard
Motor.debug(d, "motor", Motor.Scope.POSITION, Motor.Scope.TARGET); // minimum required scope
}));
});
}
}The Motor instance is described in the section below, which exposes the ability for a motor to respond directly to a SystemController via setRunToPositionController and setRunUsingEncoderController. Note that the example above is for a position PID, not a velocity PID.
Velocity controllers are tuned similarly and more information can be found here: https://docs.wpilib.org/en/stable/docs/software/advanced-controls/introduction/tuning-flywheel.html. Velocity controllers are used in the RUN_USING_ENCODER mode.
Caution
You may have noticed an extra 0.0 parameter on the setCoefficients call. This is because PIDController (and variants PDController and PController) are just instances of PIDFController, so the setCoefficients call will expect four parameters (for P, I, D, F).
With an above configuration, or similar with your own loop or reference to the controllers, you are ready to tune PID. Following the advice of logging "target vs. current" is also recommended via Motor.debug for a position PID. For tuning velocity PID, it is recommended to telemetry out the current and target velocity like:
BunyipsOpMode.ifRunning(o -> o.onActiveLoop(() -> {
o.telemetry.addData("currentVelocity", d.getVelocity());
o.telemetry.addData("targetVelocity", pid.getSetpoint());
}));Important
System controller tuning is a delicate process. Be careful and don't sporadically set your coefficients unreasonably high just because things are not moving. You probably messed up 10 minutes ago.
General tips for PID include:
- System not responding enough? Increase
kP. - System oscillates too much but
kPcan't be adjusted? IncreasekDby a very small factor, avoiding rattling oscillations. - Use
kIin moderation, a sufficiently tunedPDControllerwill operate better than aPIDControllerwithkItuned due to integral windup - System acts weirdly around certain setpoints? Add a feedforward (using a feedforward controller below, or use
kFfor a gain based on the setpoint)
Caution
Too high of a kD can cause severe rattling oscillations (triangle waves) since the system response rate is not fast enough to account for the system dynamics.
Note that loop times do affect PID, if you are having strange differences in PID performance, tune in an OpMode that has similar loop times to the one you plan to use the PID in (BunyipsOpMode.setLoopSpeed() is one way for slowing down tuning OpModes).
The PIDFController class also has some additional features that can be customised, along with composition into a CompositeController to add feedforward components (explained in Feedforward controllers below).
Note
Integrators introduce instability and hysteresis into feedback loop systems. It is strongly recommended that you avoid using integral gain unless absolutely no other solution will do - very often, problems that can be solved with an integrator can be better solved through use of a more-accurate feedforward.
A typical problem encountered when using integral feedback is excessive “wind-up” causing the system to wildly overshoot the setpoint. This can be alleviated in a number of ways - the PIDFController class enforces an integrator range limiter to help overcome this issue.
By default, the total output contribution from the integral gain is limited to be between -1.0 and 1.0.
The range limits may be increased or decreased using the setIntegrationBounds() method.
There also exists the setClearIntegralOnNewSetpoint() method, which will reset integration back to 0 whenever a new setpoint is provided.
Another way integral “wind-up” can be alleviated is by limiting the error range where integral gain is active. This can be achieved by setting the integration zone. If the error is more than the integration zone, the total accumulated error is reset, disabling integral gain. When the error is equal to or less than the integration zone, integral gain is enabled.
By default, the integration zone is disabled.
The integration zone may be set using the setIntegrationZone() method. To disable it, set it to Double.POSITIVE_INFINITY.
Warning
If your mechanism is not capable of fully continuous rotational motion (e.g. a turret without a slip ring, whose wires twist as it rotates), do not enable continuous input unless you have implemented an additional safety feature to prevent the mechanism from moving past its limit!
Some process variables (such as the angle of a turret) are measured on a circular scale, rather than a linear one - that is, each “end” of the process variable range corresponds to the same point in reality (e.g. 360 degrees and 0 degrees). In such a configuration, there are two possible values for any given error, corresponding to which way around the circle the error is measured. It is usually best to use the smaller of these errors.
To configure a PIDFController to automatically do this, use the enableContinuousInput() method.
Increasing the derivative term of a PIDFController can cause severe oscillations.
This is because the nature of the derivative when it attempts to slow down the rate of change of the system can create an unstable feedback loop resulting in oscillations that increase in amplitude. The same thing can happen when our source of data is unreliable and noisy. While we cannot perfectly fix noisy data without a perfect model we can use a series of filters to remove much of the high-frequency noise that appears in our measurements.
A low-pass filter is used internally to smooth out the derivative of error. This helps reduce oscillations based on one gain, the low-pass gain.
This gain must be within the range (0, 1) exclusive, and by default is 0.8 to smoothen out all PIDs. By increasing this value, the derivative is smoothed further, responding to less change and generating a smoother curve. Gains around to 0.8-0.9 provide a decent smoothing, and can be reduced if needed if a faster reaction time is desired.
You can adjust the used low-pass filter gain via the setDerivativeSmoothingGain() method.
The output of a PIDFController can be clamped via the setOutputClamps() method - this ensures your output never responds with a value greater in magnitude than the specified domain.
Advanced users may want to limit the velocity or acceleration of the PID response curve.
This is available through the ProfiledPIDController, an extension of the PIDFController. Internally, this controller uses a trapezoidal velocity profile to limit the speed of the response, which may be beneficial for systems that need smooth outputs.
While PID is a feedback controller, feedforward controllers take known kinematic properties of a system and assist the system's output to rely less on pure feedback. This can reduce oscillation as an error can be corrected before the error is created. In BunyipsLib, there are several "k-gain" feedforwards that can operate to model your system effectively.
Important
Feedforward controllers should be tuned before feedback controllers! It is also important to know that feedforward alone cannot bring a system to a precise setpoint. Feedforwards only assist in modelling system dynamics.
Feedforward is added to the end of your feedback output controller, such that output = pid + ff. A CompositeController will assist in the merging process of feedback and feedforward.
Tip
Be sure to use the same process as your feedback controllers using FtcDashboard to tune quickly! The composing controllers section below will elaborate on how setCoefficients calls work with multiple controllers.
Used to induce acceleration based on changes over time to the setpoint.
$$f(t)=K_a\cdot \frac{d}{dt}s(t)$$
The kA or acceleration gain applies an output proportional to the rate of change (or derivative) of the setpoint. This differs from a conventional kA gain for a target acceleration.
SystemController ff = new kA(kA);Tip
The kA gain is often unused for most single-target systems and is acceptable to omit.
Used to overcome non-linear dynamics in rotation.
$$f(t)=K_{cos}\cdot \cos(p_\theta(t)) \quad \text{ or }\quad f(t)=K_{cos}\cdot\cos(s_\theta(t))$$
The kCos or cosine gain applies an output proportional to the cosine of the angle of the system. This allows for additional "assistance" to be applied to a rotating arm for instance, which experiences gravity non-linearly as it rotates.
Note
This feedforward is the only one that requires the use of the BunyipsLib unit system! In the example provided below, an EncoderTicks.Generator will be used to convert encoder ticks into angles. Review the docs for EncoderTicks.Generator for more configuration options, such as adding a gear reduction.
EncoderTicks.Generator angleGenerator = EncoderTicks.createGenerator(motor);kCos accepts two constructors, either for encoder ticks, or to instead use the setpoint as
the reference angle. It is recommended to use the encoder tick constructor as shown below as it will be more reactive to change. If you choose to use the setpoint as the reference angle, you will need to supply an UnaryFunction that converts ticks to angle in radians as the second parameter.
// using the `angleGenerator` from above, recommended constructor
SystemController ff = new kCos(kCos, angleGenerator::getAngle);Tip
To tune kCos, set all other coefficients to zero and increase kCos slowly at an endpoint until the arm can hold itself relatively in place. Ensure that it is able to hold itself in place for all angles it can be suspended from.
You may also wish to "offset" the angle supplied to the feedforward if you would like to align it to a sine function instead (tuned from vertical). This is accomplished with new kCos(kCos, () -> angleGenerator.getAngle().plus(Degrees.of(90))) for 90 degrees.
Used to overcome constant forces such as gravity.
$$f(t)=K_g$$
Most useful on vertical lifts, the kG or gravity gain applies a constant output irrespective of the system state. This is useful for components always affected by gravity, such as vertical lifts.
SystemController ff = new kG(kG);Tip
To tune kG for a lift, set all other coefficients to zero and increase kG slowly at an endpoint until the lift can hold itself under its own weight. Tune the other coefficients as normal.
Used to overcome friction.
$$f(t)=K_s\cdot \text{sgn}(s(t)-p(t))$$
The static or friction gain kS applies a direct output in the direction (sign, positive or negative) of the error (desired output). This gain is used to overcome static friction in the system.
SystemController ff = new kS(kS);Tip
Tuning kS should be the first coefficient you tune, as it will be the basis of overcoming friction in your system.
Used to counter electromotive force and drag.
$$f(t)=K_v\cdot s(t)$$
The kV or velocity gain applies an output proportional to the setpoint of the system. This provides the force to overcome electromotive or back-forces in a DC motor, most effective for velocity controllers.
SystemController ff = new kV(kV);Tip
kS is exactly equivalent to the kF gain in the PIDFController. It is just named differently. It is also rare to use a kF/kV gain for a position controller.
The CompositeController can be used to compose different SystemController instances.
This new controller is in itself a SystemController instance, allowing you to pass it to the Motor class or wherever else as a singular instance.
This singular instance combines the outputs of internally composed controllers. For example, a PID and feedforward can be composed to output = pid + ff.
To compose, you can use the .compose() method on an existing SystemController, or use the standard constructor for CompositeController:
PIDController pid = new PIDController(/* ... */);
SystemController ff = new kS(/* ... */);
CompositeController c = pid.compose(ff, (a, b) -> a + b); // second arguments dictates
// how these controllers should be composed.
// usually you want to just add them so you
// can use this BiFunction here (or Double::sum)
// An optional overload omits this parameter and will default to Double::sum
motor.setRunToPositionController(c); // use c as you usually would for whichever mode
// motor.setRunUsingEncoderController(c); // ... alternatively
// Note: when calling setCoefficients on this new controller, the coefficients are now determined as the
// union of coefficients, much as if you were to call setCoefficients on both controllers one after the other.
c.setCoefficients(kP, kI, kD, kF, kS); // pidf + kS gain in sequence. Newly composed controllers expect their coefficients tacked on the end.This concept of sequenced parameters to setCoefficients also applies to multiple feedforward gains. The default Double::sum overload to compose is used and hence omitted in this example:
PIDFController pidf = new PIDFController(/* ... */);
SystemController ff = new kS(/* ... */).compose(new kA(/* ... */));
CompositeController c = pidf.compose(ff);
// use `c` wherever
// to set coefficients, the method expects the ordered parameters from composition, so:
c.setCoefficients(kP, kI, kD, kF, kS, kA);Note
Composing a PID controller with a feedforward controller will set the pidf() method of the SystemController to return the internal instance of the PID controller. Using two PIDs together is not supported and pidf() will throw an exception.










