TD Input Systems - theRAPTLab/gsgo GitHub Wiki

version dr01 Apr16-2021

This reference diagram will be useful as you work through the code examples below:
https://whimsical.com/input-system-dr01-Y2xuF7r1N1kxNJ2MyfzqZY

Note that **notify** is not yet implemented in the first draft, but will be coming shortly.

Quick Guide to Input Systems

The input system has the notion of device sources and device subscribers that are declared by any webapp that is on the URSYS network. All webapps maintain a copy of the network wide device directory.

A device is a kind of interface, and webapps can host more than one. Likewise, subscribers can hold multiple subscription to groups of devices if necessary.

Irregardless of being a source of a subscriber, devices communicate by sending control data objects. These are simply objects that have an id field and other properties, and conceptually the sending mechanism is similar to PTrack "tracks".

This Input System Diagram may be helpful to use as a reference for the following 4 sections of this document.

1. Creating Devices

You can create one or more device interfaces with two lines.

Here is a minimal code example that registers a device of type 'CharControl' on the network.

// mod-charcontrol-ui.js Initialize()
// called from CharControl.componentDidMount
async function Initialize(reactRoot) {
  const device = UR.NewDevice('CharControl');
  const { udid, status, error } = await UR.RegisterDevice(device)
  // status is set if success, error if not
  DEVICE_UDID = udid;
  MARKER_FRAMER = device.getControlFramer('markers');
}

There is a built-in template named CharControl that's defined in class-udevice in DEVICE_CLASS_TEMPLATES, which defines what inputs and outputs are supported by this type of device. In the case of device class "CharControl", this defines "inputs" named "markers" which is an array of { id, x, y } "control data objects". "Control Data" is the general term for any i/o related data that is sent/received by the device.

See Appendix A for how to create custom device interfaces.

We save DEVICE_UDID and MARKER_FRAMER for use later. The udid is an "unique device id" that is assigned by the system, and is used to lookup devices in the device directory. MARKER_FRAMER is a function that accepts an array of object and wraps them in a controlFrame that is distributed across the network; we use it in section 4 below.

2. Emitting Data from a Device

Devices emit "control data", which is a named array consisting of "control data objects" with an id property. Other than the id requirement, these data objects can contain any JSON-compatible data you want.

For our CharControl example, we are generating id, x, and y from the circle elements on the GUI. We package this into an array of control data objects to send. Here's minimal code that emits the cData objects.

// mod-charcontrol-us.js SendControlFrame
// cobjs is an array of [ {i, x, y}, ... ]
const controlFrame = MARKER_FRAMER(cobjs);
UR.SendControlFrame(controlFrame);

We created MARKER_FRAMER in the first block of code declaring the device. The getControlFramer("markers") returned a function that generates a "control frame" that contains all the control objects passed to it, so you don't have to manually create it yourself.

3. Subscribing to a Device Specification

Instead of subscribing to devices by name or address, URSYS uses a "device specification" object that will return the list of all matching devices. Each device spec defines filter functions to help you get what you need:

  • selectify = (device)=>true/false - if this device is part of what you want, return true. The device parameter is a UDevice that contains numerous properties that you can inspect.
  • quantify = deviceList=>filteredDeviceList - all passing devices from selectify can be counted or processed by this method. This is where you would check for minimum required devices, limit their number, or perform additional filtering.
  • notify = (event) => { } - when a change in conditions as defined by the selectify and quantity filters, your callback will be notified
// Tracker.jsx componentDidMount
const spec = {};
spec.selectify = (device) => device.meta.uclass === 'CharControl';
spec.quantify = (deviceList) => deviceList;
spec.notify = (changes) => console.log('notify is not supported yet');
API = UR.SubscribeDeviceSpec(spec);

This example filters for any devices with the device class 'CharControl', and passes the deviceList as-is. The SubscribeDeviceSpec() call returns a "device API" which we save in a global variable API for later use.

Note: you can subscribe to more than one device at a time in a single Device Spec. You can also subscribe to the same device multiple times using multiple Device Specs. Since each Device may implement different controls, this is useful for aggregating different pools of devices.

4. Reading Inputs from a Device Subscription

You use the API device to fetch functions that help you read/write/control the devices you have subscribed to via the Device Spec. The API has several methods, but the main one you need is getController(controlName). You use this method to specify which named control (e.g. "markers") you want to read.

// Tracker.jsx componentDidMount
const { getInputs } = API.getController('markers');

// periodically read inputs from our controller
const ReadInputs = () => {
  const cobjs = getInputs();
}
setInterval(ReadInputs, 100);

The example above uses the getController(controlName) API method to return a getInputs() function. Whenever you call this function, it will return a single array of ALL control data objects from the devices specified by your Device Spec. Each id in the cdObjs, though, is rewritten to be predictably unique (it prepends the URNET AddressNumber to each id).

Note: the getInputs() function has no parameters, as it is made to return the specific named control "markers" when you called API.getController('markers') This guarantees that all the control data objects will be an array of the same format.

As a device may implement more than one named control, you will have to fetch a getInputs() function for each name.

Appendix A: Creating a Custom Interface

If you are not using one of the templated control types (e.g. CharControl), setting up Device is a little different. You need to define the inputs and outputs data structures, so clients can inspect this information through the network-wide device directory.

The contents of these arrays are ControlDefs (cDefs), which are object that declare the names of controls and the structure of their Control Data Objects (cDataObj for single object, cData for an array of objects). For example, here is the declaration for the CharControl device class.

// in class-udevice
const TEMPLATES = {
    CharControl: {
        inputs: { 'markers': { x: 'axis', y: 'axis' } },
        outputs: { 'setGroup': { groups: 'array' } }
    }
}

The important part is the inputs and outputs declaration. This syntax means: "define a control named "markers", which will contain an array of objects with "x" and "y" properties encoded as 'axis' data types.

Here's the code to create this structure without using the CharControl template, while also adding a second control named "sliders".

// get a new UDevice instance
async () => {
    const device = UR.NewDevice('MyCharControl');
    device.addInputDef('markers',{ x:'axis', y:'axis' });
    device.addInputDef('sliders',{ name:'string', value:'float' }); 
    const { udid, status, error } = await UR.RegisterDevice(device)
}

Currently the system checks for a valid encoding type (e.g. the 'axis', 'float'), but does not enforce them.

We currently have these types defined for anticipated future use, but they are more descriptions of the expected type of data at this time. A goal of the encoding types is to ensure they can be represented by the shortest possible data representation; in the future we may add binary protocols for greater network efficiency as it becomes necessary.

// basic types 
'int',     // an integer
'float',   // a floating-point value
'string',  // an arbitrary string
'uint',    // positive integers only
'ufloat',  // positive floats only
'byte',    // 8-bit byte (hex)
'word',    // 16-bit word (hex)
'dword',   // 32-bit word (hex)
'qword',   // 64-bit word (hex)
{},        // "shape" of an object type
[],        // array of encoding type or "shape" of object type

// special types
'axis',    // a floating-point value between -1 and 1     
'vec2',    // a 2D vector [x y]                           
'vec3',    // a 3D vector [x y z]                         
'matrix3', // a 3D matrix (array of 9 floats)          
'matrix4', // a 4D matrix (array of 16 floats)         
'bistate', // a 0/1 continuous boolean signal          
'edge+',   // a momentary transition from 0 to 1 (bool)  
'edge-',   // a momentary transition from 1 to 0 (bool)  
'enum',    // one of a particular set of symbols (string)
'bits',    // an array of bits (hex)
'bits2',   // a 2D array of bits [hex-row, ...]
'fix2',    // fixed-point 2 decimal places (string)
'fix3',    // fixed-point 3 decimal places (string)

Appendix B: Miscellaneous Notes

The input system manages the collection of devices by itself so you don't have to. You shouldn't have to worry about asynchronous read/write operations or collating data from multiple sources.

The Control Data Object format is designed to mirror our other major module data types. These data types share the same id value, so it is easy to cross look-up associated entities in DATACORE. For example, if you know the id of an agent, you can also look-up its corresponding sprite and display object if they are defined locally.

  • instance definitions
  • agents
  • display objects
  • sprites

These datatypes are also compatible with DifferenceCache and SyncMap which use the unique id string values in to automatically manage changes in data sets.

The input system is a subset of the larger Device Identity and Device I/O specifications. The common data structure is defined in class-udevice.js. Also see the Input System DR01 diagram to see how all the data structures fit together.

⚠️ **GitHub.com Fallback** ⚠️