Device Simulation
WPILib provides a way to manage simulation device data in the form of the SimDevice API.
Simulating Core WPILib Device Classes
Core WPILib device classes (i.e Encoder
, Ultrasonic
, etc.) have simulation classes named EncoderSim
, UltrasonicSim
, and so on. These classes allow interactions with the device data that wouldn’t be possible or valid outside of simulation. Constructing them outside of simulation likely won’t interfere with your code, but calling their functions and the like is undefined behavior - in the best case they will do nothing, worse cases might crash your code! Place functional simulation code in simulation-only functions (such as simulationPeriodic()
) or wrap them with RobotBase.isReal()
/ RobotBase::IsReal()
checks (which are constexpr
in C++).
Note
This example will use the EncoderSim
class as an example. Use of other simulation classes will be almost identical.
Creating Simulation Device objects
Simulation device object can be constructed in two ways:
a constructor that accepts the regular hardware object.
a constructor or factory method that accepts the port/index/channel number that the device is connected to. These would be the same number that was used to construct the regular hardware object. This is especially useful for unit testing.
// create a real encoder object on DIO 2,3
Encoder encoder = new Encoder(2, 3);
// create a sim controller for the encoder
EncoderSim simEncoder = new EncoderSim(encoder);
// create a real encoder object on DIO 2,3
frc::Encoder encoder{2, 3};
// create a sim controller for the encoder
frc::sim::EncoderSim simEncoder{encoder};
Reading and Writing Device Data
Each simulation class has getter (getXxx()
/GetXxx()
) and setter (setXxx(value)
/SetXxx(value)
) functions for each field Xxx
. The getter functions will return the same as the getter of the regular device class.
simEncoder.setCount(100);
encoder.getCount(); // 100
simEncoder.getCount(); // 100
simEncoder.SetCount(100);
encoder.GetCount(); // 100
simEncoder.GetCount(); // 100
Registering Callbacks
In addition to the getters and setters, each field also has a registerXxxCallback()
function that registers a callback to be run whenever the field value changes and returns a CallbackStore
object. The callbacks accept a string parameter of the name of the field and a HALValue
object containing the new value. Before retrieving values from a HALValue
, check the type of value contained. Possible types are HALValue.kBoolean
/HAL_BOOL
, HALValue.kDouble
/HAL_DOUBLE
, HALValue.kEnum
/HAL_ENUM
, HALValue.kInt
/HAL_INT
, HALValue.kLong
/HAL_LONG
.
In Java, call close()
on the CallbackStore
object to cancel the callback. Keep a reference to the object so it doesn’t get garbage-collected - otherwise the callback will be canceled by GC. To provide arbitrary data to the callback, capture it in the lambda or use a method reference.
In C++, save the CallbackStore
object in the right scope - the callback will be canceled when the object goes out of scope and is destroyed. Arbitrary data can be passed to the callbacks via the param
parameter.
Warning
Attempting to retrieve a value of a type from a HALValue
containing a different type is undefined behavior.
NotifyCallback callback = (String name, HALValue value) -> {
if (value.getType() == HALValue.kInt) {
System.out.println("Value of " + name + " is " + value.getInt());
}
}
CallbackStore store = simEncoder.registerCountCallback(callback);
store.close(); // cancel the callback
HAL_NotifyCallback callback = [](const char* name, void* param, const HALValue* value) {
if (value->type == HAL_INT) {
wpi::outs() << "Value of " << name << " is " << value->data.v_int << '\n';
}
};
frc::sim::CallbackStore store = simEncoder.RegisterCountCallback(callback);
// the callback will be canceled when ``store`` goes out of scope
Simulating Other Devices - The SimDeviceSim Class
Note
Vendors might implement their connection to the SimDevice API slightly different than described here. They might also provide a simulation class specific for their device class. See your vendor’s documentation for more information as to what they support and how.
The SimDeviceSim
(not SimDevice
!) class is a general device simulation object for devices that aren’t core WPILib devices and therefore don’t have specific simulation classes - such as vendor devices. These devices will show up in the Other Devices tab of the SimGUI.
The SimDeviceSim
object is created using a string key identical to the key the vendor used to construct the underlying SimDevice
in their device class. This key is the one that the device shows up with in the Other Devices tab, and is typically of the form Prefix:Device Name[index]
. If the key contains ports/index/channel numbers, they can be passed as separate arguments to the SimDeviceSim
constructor. The key contains a prefix that is hidden by default in the SimGUI, it can be shown by selecting the Show prefix option. Not including this prefix in the key passed to SimDeviceSim
will not match the device!
SimDeviceSim device = new SimDeviceSim(deviceKey, index);
frc::sim::SimDeviceSim device{deviceKey, index};
Once we have the SimDeviceSim
, we can get SimValue
objects representing the device’s fields. Type-specific SimDouble
, SimInt
, SimLong
, SimBoolean
, and SimEnum
subclasses also exist, and should be used instead of the type-unsafe SimValue
class. These are constructed from the SimDeviceSim
using a string key identical to the one the vendor used to define the field. This key is the one the field appears as in the SimGUI. Attempting to retrieve a SimValue
object outside of simulation or when either the device or field keys are unmatched will return null
- this can cause NullPointerException
in Java or undefined behavior in C++.
SimDouble field = device.getDouble(fieldKey);
field.get();
field.set(value);
hal::SimDouble field = device.GetDouble(fieldKey);
field.Get();
field.Set(value);