URSYS Devices - dsriseah/ursys GitHub Wiki

PORT IN PROGRESS from GEMSTEP

The URSYS Device system is built on top of the message system. The device system allows any URSYS application to define any number of remote controller devices with named properties and send data. The data is received by clients who have subscribed devices that meet their conditions, creating a controller object. The controller object maintains a buffer of all inputs that is refreshed whenever data is received asynchronously.

NOTE: this is not a local controller object as for a single-user game. It's designed to allow specialized apps to act as groups of "controllers" that provide input into a game loop during its READ_INPUTS phase.

Basic API Usage

The old GEMSTEP API has different semantics, but this is the general shape of how you send data

CREATE A DEVICE

const deviceSpec = {...}
const device = NewDevice(deviceSpec)
RegisterDevice(device)

SEND DEVICE OUTPUT

// using device declaration from above
const cData = { markers:[{x:0, y:0}, {x:10,y:10} ] };
const cFrame = device.createFrame(cData)
SendControlFrame(cFrame)

USE A DEVICE

const subscriptionSpec = {...}; // receive 
const controller = SubscribeDeviceSpec(subscriptionSpec)
const pieces = controller.getInputs()
// contains { markers:[{x:0, y:0}, {x:10,y:10} ] }

Device Specification

A UDevice is defined by its "device descriptor"

type Prefix = string; // uppercase
type UniqueInt = string;
type UAddress = `${Prefix}-${UniqueInt}`
type UDeviceID = `UDID:${UniqueInt}`

type UDeviceMeta = {
    // device identifiers
    uaddr: UAddress; // address of the device
    uclass: string; // named type of device
    uapp: string; // appserver route identifier
    uname: string; // short descriptive device type
    // display and selection properties
    uapp_label: string; // public name to use
    uapp_tags: string[]; // optional tags
};

type BasicTypes = 'int' | 'float' | 'string' | 'uint' | 'ufloat' | 'byte' | 'word' | 'dword' | 'qword';
type SpecialTypes = | 'axis' | 'vec2' | 'vec3' | 'matrix3' | 'matrix4' | 'bistate' | 'edge+' | 'edge-' | 'enum' | 'bits' | 'bits2' | 'fix2' | 'fix3' | Encoding;
type UEncoding = BasicTypes | SpecialTypes;

type UControlDef = { [group:string]: { [propName:string]:UEncoding } }

type UDevice = {
  udid: UDeviceID;
  meta: UDeviceMeta;
  inputs: UControlDef;
  outputs: UControlDef;
}

type UDeviceClass = {
    [class:string]: {
        inputs: UControlDef;
        outputs: UControlDef;
    }
}

Device Instrastructure Services

The device server is based on URSYS messaging with these reserved messages.

# requests from clients to server

'NET:SRV_DEVICE_DIR' -> PKT_DeviceDirectory
'NET:SRV_DEVICE_REG' -> PKT_RegisterDevice
'NET:SRV_CONTROL_ON' -> PKT_ControlFrameIn

# events from server to clients

device directory change -> 'NET:UR_DEVICES' 
control frame broadcast -> 'NET:UR_CFRAME'

Device Definitions

A client that wants to create a device creates a UDevice declaration and submits it to the server to register it. Then, the client can send data as a control frame that is the described in the UDevice declaration.

// tracker data is a marker id and x, y coordinates (normalized) 
// { id, x, y } 

// create and register
const device = NewDevice('CharControl');
const registrationStatus = await UR.RegisterDevice(dev);
// expose results of registration
const { udid, status, error } = registrationStatus;
// create control frame utility object for "markers" set
const ControlFrameMaker = device.getControlFramer('markers');
// send data as a controller
const markerData = [ { id, x, y }, ... ];
const cFrame = ControlFrameMaker(markerData);
SendControlFrame(cFrame)

A controller app can send data whenever it feels like it, and the system will buffer it for clients.

Device Clients

A client that wants to receive control data from a device creates a device subscription by providing three functions that select, quantify and optionally notify of changes. This allows you to logically define the conditions that you require for a controller to be considered active as well as be informed with the condition is no longer met.

After you create the subscription, you receive a controller object that allows you to read/write the device as well as unsubscribe.

// define filters
const selectify = device => device.meta.uclass = "Sim";
const quantify = selectedDeviceList => selectedDeviceList
// define notication function for subscription state changes
const notify = subscriptionStatus => {
  const { selected, quantified, valid } = subscriptionStatus;
  if (valid) console.log('subscription criterial met')
}
// get a deviceAPI using the filters and notification functions
const deviceAPI = UR.SubscribeDeviceSpec(selectify, quantify, notifiy);
// expose the deviceAPI methods
const { unsubscribe, getController, subscriptionID } = deviceAPI
// get a controller object from the deviceAPI
const controller = getController();
// expose controller methods
const { getInputs, putOutputs, getChanges } = controller;
// read inputs every 500 ms
setInterval(()=>console.log(getInputs()),500);

There are two main ways to read inputs:

  • getInputs() reads the complete list of control objects
  • getChanges() returns { all, added, updated, removed } that happened since the last call to getChanges()

NOTE: Devices are designed to also be written-to, but this isn't implemented in any version of URSYS.

Notes

  • The client code does a lot of housekeeping to ensure that users of a device just have to use getInputs() or getChanges() and be sure that everything is up to date at the time of invocation; the control frames are buffered asynchronously so you don't have to worry about partial updates, etc. This is designed to work with a typical game loop where inputs are read at the beginning of a frame.
  • The Basic and Advanced encoding types are not currently enforced; it's up to the client to interpret them correctly
  • The system includes entity aging, which is useful for tracker systems with ids that aren't persistent and must be removed.
  • The system is designed for remote controllers being read by the subscribing apps; there is not yet a "local controller" model thought that should be easy enough to add.
⚠️ **GitHub.com Fallback** ⚠️