Skip to content

Commit

Permalink
Merge pull request #34 from DeepBlueRobotics/add-support-for-can-devices
Browse files Browse the repository at this point in the history
Add support for CAN devices.
  • Loading branch information
brettle authored May 23, 2024
2 parents fe44c97 + 227d129 commit 7e0a079
Show file tree
Hide file tree
Showing 8 changed files with 749 additions and 14 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# WPIWebSockets
This API provides a Java interface to the [WPILib HALSim WebSockets API](https://github.com/wpilibsuite/allwpilib/blob/master/simulation/halsim_ws_core/doc/hardware_ws_api.md) which mirrors the standard simulation API.
# Building the API
To build the API, navigate to the root directory of this repository and run `./gradlew build`. This will use Gradle to build the API with Java 8. Note that the simulated device files (org.team199.wpiws.devices.*) are auto-generated using the [AsyncAPI Generator](https://github.com/asyncapi/generator) and [WPILib's machine readable WebSocket specification](https://github.com/wpilibsuite/allwpilib/blob/master/simulation/halsim_ws_core/doc/wpilib-ws.yaml). The AsyncAPI template used to generate these files is located at [./asyncapi-template](https://github.com/DeepBlueRobotics/WPIWebSockets/tree/master/asyncapi-template). These files are auto-generated during the `WPIWebSockets:build` task, however, they can be manually regenerated by running `./gradlew generateDeviceFiles`.
To build the API, navigate to the root directory of this repository and run `./gradlew build`. This will use Gradle to build the API with Java 8. Note that the simulated device files (org.team199.wpiws.devices.*) are auto-generated using the [AsyncAPI Generator](https://github.com/asyncapi/generator) and [an extended version](./wpilib-ws.yaml) of [WPILib's machine readable WebSocket specification](https://github.com/wpilibsuite/allwpilib/blob/master/simulation/halsim_ws_core/doc/wpilib-ws.yaml). The AsyncAPI template used to generate these files is located at [./asyncapi-template](./asyncapi-template). These files are auto-generated during the `WPIWebSockets:build` task, however, they can be manually regenerated by running `./gradlew generateDeviceFiles`.
# Using the API
Connecting to the running HALSim client and server instances can be accomplished by using the `connectHALSim` and `startHALSimServer` methods of [`org.team199.wpiws.connection.WSConnection`](https://github.com/DeepBlueRobotics/WPIWebSockets/blob/master/src/main/java/org/team199/wpiws/connection/WSConnection.java). Code for connecting to simulated devices can be found in the `org.team199.wpiws.devices` package. These classes can be initialized by providing the constructor with the device id with which it should associate itself. The instance will then provide getter, setter, and callback methods for all of the values exposed by the WebSocket API. (ex. `new PWMSim("5").setSpeed(5)`, `new DIOSim("0").getValue()`). Note: `static` methods apply globaly to the device type.
Connecting to the running HALSim client and server instances can be accomplished by using the `connectHALSim` and `startHALSimServer` methods of [`org.team199.wpiws.connection.WSConnection`](./src/main/java/org/team199/wpiws/connection/WSConnection.java). Code for connecting to simulated devices can be found in the `org.team199.wpiws.devices` package. These classes can be initialized by providing the constructor with the device id with which it should associate itself. The instance will then provide getter, setter, and callback methods for all of the values exposed by the WebSocket API. (ex. `new PWMSim("5").setSpeed(5)`, `new DIOSim("0").getValue()`). Note: `static` methods apply globaly to the device type.

For a more complete example, see [DeepBlueRobotics/DeepBlueSim](https://github.com/DeepBlueRobotics/DeepBlueSim) which uses this API to provide an interface to a [Webots](https://cyberbotics.com/) robot simulator.
3 changes: 3 additions & 0 deletions asyncapi-template/hooks/UpdateSchemaNames.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ const deviceFilter = require('../filters/DeviceFilter');

module.exports = {
'setFileTemplateName': (generator, originalFilename) => {
if(originalFilename["originalFilename"] === "simdeviceData") {
return "SimDeviceSim";
}
for(const [name, schema] of generator.asyncapi.allSchemas()) {
if(name === originalFilename["originalFilename"] && schema.property("type") !== null) {
return deviceFilter.formatName(schema.property("type").const()) + "Sim";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ public static void cancelDeviceCreatedCallback(Pair<String, SimDeviceCallback> c
* @param device the device identifier of the device sending the message
* @param data the data associated with the message
*/
public static void processMessage(String device, List<WSValue> data) {
public static void processMessage(String device, String type, List<WSValue> data) {
SimDeviceSim simDevice = new SimDeviceSim(device);
data.stream().filter(Objects::nonNull).forEach(value -> {
String key = value.getKey();
Expand Down
24 changes: 20 additions & 4 deletions asyncapi-template/template/$$schema$$.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{%- if schemaName === "simdeviceData" -%}
{% include "../partials/SimDeviceSim.java" -%}
{%- else -%}
{% set type = schema.property("type").const() -%}
{% set name = type | formatName -%}
{% set hasId = type | hasId -%}
Expand Down Expand Up @@ -49,6 +52,7 @@ public class {{ name }}Sim {
{% endif -%}
{% if hasId -%}
private static final Map<String, {{ name }}Sim.State> STATE_MAP = new ConcurrentHashMap<>();
private final String type;
{% else -%}
private static final {{ name }}Sim.State STATE = new State();
{% endif -%}
Expand All @@ -58,9 +62,15 @@ public class {{ name }}Sim {
/**
* Creates a new {{ name }}Sim
* @param id the device identifier of this {{ name }}Sim
* @param type the type of this {{ name }}Sim
*/
public {{ name }}Sim(String id) {
public {{ name }}Sim(String id, String type) {
super(id, STATE_MAP);
this.type = type;
}

public {{ name }}Sim(String id) {
this(id, "{{ type }}");
}
{%- endif -%}

Expand Down Expand Up @@ -153,7 +163,7 @@ private void setInitialized(boolean initialized, boolean notifyRobot) {
INITIALIZED_DEVICES.remove(id);
}
if(notifyRobot) {
ConnectionProcessor.broadcastMessage(id, "{{ type }}", new WSValue("<init", initialized));
ConnectionProcessor.broadcastMessage(id, type, new WSValue("<init", initialized));
}
}

Expand Down Expand Up @@ -242,7 +252,11 @@ protected State generateState() {
{%- if not varInfo.isRobotInput %}
System.err.println("WARNING: {{ name }}Sim#set{{ varInfo.pname }}({{ varInfo.pprimtype }}, true) was called, but {{ varInfo.pfname }} is not a robot input!");
{%- endif %}
{% if hasId -%}
ConnectionProcessor.broadcastMessage({{ cid }}, type, new WSValue("{{ varInfo.pfname }}", {{ varInfo.pnamel }}));
{%- else %}
ConnectionProcessor.broadcastMessage({{ cid }}, "{{ type }}", new WSValue("{{ varInfo.pfname }}", {{ varInfo.pnamel }}));
{%- endif %}
}
}

Expand All @@ -251,15 +265,16 @@ protected State generateState() {
/**
* An implementation of {@link org.team199.wpiws.interfaces.DeviceMessageProcessor} which processes WPI HALSim messages for {{ name }}Sims
* @param device the device identifier of the device sending the message
* @param type the type of the device sending the message
* @param data the data associated with the message
*/
public static void processMessage(String device, List<WSValue> data) {
public static void processMessage(String device, String type, List<WSValue> data) {
// Process all of the values, but save the "<init" value for last
// so that the rest of the state has been set when the initialize
// callback is called.
WSValue init = null;
{%- if hasId %}
{{ name }}Sim simDevice = new {{ name }}Sim(device);
{{ name }}Sim simDevice = new {{ name }}Sim(device, type);

for(WSValue value: data) {
if (value.getKey().equals("<init"))
Expand Down Expand Up @@ -338,3 +353,4 @@ public static class State {
}

}
{% endif -%}
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,19 @@ task archiveTemplate(type: Tar) {
// the asyncapi generator.
task generateDeviceFiles(type: NpxTask) {
def outputDir = "${buildDir}/generated/sources/asyncapi"
def specYaml = "${projectDir}/wpilib-ws.yaml"

// Define the command line that npx should use
workingDir = buildDir // Because templateDir can't be under it
command = '@asyncapi/generator@1.17.25'
args = ['--force-write',
'-o', "${outputDir}/org/team199/wpiws/devices",
"https://raw.githubusercontent.com/wpilibsuite/allwpilib/master/simulation/halsim_ws_core/doc/wpilib-ws.yaml",
specYaml,
file(archiveTemplate.archiveFile).toURI()]

// Define the inputs and outputs of this task so that gradle only runs it
// when necessary.
inputs.files(archiveTemplate.outputs)
inputs.files(archiveTemplate.outputs, file(specYaml))
outputs.dir(outputDir).withPropertyName("outputDir")

}
Expand Down
41 changes: 37 additions & 4 deletions src/main/java/org/team199/wpiws/connection/MessageProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import org.team199.wpiws.devices.AccelerometerSim;
import org.team199.wpiws.devices.AnalogInputSim;
import org.team199.wpiws.devices.CANEncoderSim;
import org.team199.wpiws.devices.CANMotorSim;
import org.team199.wpiws.devices.DIOSim;
import org.team199.wpiws.devices.DriverStationSim;
import org.team199.wpiws.devices.DutyCycleSim;
Expand Down Expand Up @@ -45,6 +47,14 @@ public final class MessageProcessor {
registerProcessor("RoboRIO", RoboRIOSim::processMessage);
registerProcessor("Solenoid", SolenoidSim::processMessage);
registerProcessor("SimDevice", SimDeviceSim::processMessage);
registerProcessor("CANMotor", CANMotorSim::processMessage);
registerProcessor("CANEncoder", CANEncoderSim::processMessage);
registerProcessor("CANDutyCycle", DutyCycleSim::processMessage);
registerProcessor("CANAccel", AccelerometerSim::processMessage);
registerProcessor("CANAIn", AnalogInputSim::processMessage);
registerProcessor("CANDIO", DIOSim::processMessage);
registerProcessor("CANGyro", GyroSim::processMessage);

}

/**
Expand All @@ -70,14 +80,37 @@ public static void registerProcessor(String type, DeviceMessageProcessor process
* @param data the values of that device which have been modified
*/
public static void process(String device, String type, List<WSValue> data) {
DeviceMessageProcessor processor = processors.get(type);
// Per the spec, some message with a type of SimDevice have a data
// format that is identical to hardware devices. In particular a
// SimDevice message with device=DutyCyle:Name has the same data format
// as a DutyCycle message, and a SimDevice message with
// device=CAN{Gyro,AI,Accel,DIO,DutyCycle}:Name has the same data format
// as a {Gyro,AI,Accel,DIO,DutyCycle} message. The
// CAN{Gyro,AI,Accel,DIO,DutyCycle} processors are registered as aliases
// for the the their non CAN counterparts. CANMotor and CANEncoder have
// their own processors because there is no Motor processor and the
// CANEncoder data format is different from the Encoder data format.
String dataType = type;
if (type.equals("SimDevice")) {
String[] deviceParts = device.split(":");
if (deviceParts.length > 1) {
dataType = deviceParts[0];
}
}

// Use the data type to find the appropriate processor. Fallback on SimDevice if the parsing above returned an invalid type.
DeviceMessageProcessor processor = processors.getOrDefault(dataType, type.equals("SimDevice") ? processors.get("SimDevice") : null);
if(processor == null) {
if(unknownTypes.add(type)) {
System.err.println("No processor found for device of type: \"" + type + "\" messages for devices of this type will be ignored");
if(unknownTypes.add(dataType)) {
System.err.println("No processor found for device with data type: \"" + dataType + "\". Messages with this data type will be ignored.");
}
return;
}
processor.processMessage(device, data);

// Pass the actual device type to the processor which will pass it on to
// the *Sim constructor so that the *Sim object creates messages with
// the correct type.
processor.processMessage(device, type, data);
}

private MessageProcessor() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ public interface DeviceMessageProcessor {
/**
* Processes a WPI HALSim message
* @param device the device identifier of the device sending the message
* @param type the type of the device sending the message
* @param data the data associated with the message
*/
public void processMessage(String device, List<WSValue> data);
public void processMessage(String device, String type, List<WSValue> data);

}
Loading

0 comments on commit 7e0a079

Please sign in to comment.