istareatscreens / MychIO

Unity washing machine input manager
3 stars 1 forks source link

MychIO

Input manager that supports Mai2 controllers in Unity.

Adding to Unity

To add this package to your project simply copy the clone link and go to Window->Package Manager. Hit the + in the window and select add package from git Url.

API

Full example of using the package is viewable here

Initialization

Add the following imports to your project

using MychIO.Device;
using MychIO;
using MychIO.Event;

Retrieve the ExecutionQueue (type ConcurrentQueue<Action>) from the IOManager and Instantiate it

_executionQueue = IOManager.ExecutionQueue;
_ioManager = new IOManager();

The executionQueue is used to store callbacks that will be executed on the main thread. Since this input manager uses threading you must execute all code in the context of the ExecutionQueue. Otherwise undefined/undesirable behaviour will likely be observed.

Event System

To subscribe to error events use IOEventType and generate a Dictionary of callbacks:

var eventCallbacks = new Dictionary<IOEventType, ControllerEventDelegate>{
            { IOEventType.Attach,
                (eventType, deviceType, message) =>
                {
                    # Note: appendEventText uses the IOManagers ExecutionQueue
                    appendEventText($"eventType: {eventType} type: {deviceType} message: {message.Trim()}");
                }
            },
            { IOEventType.ConnectionError,
                (eventType, deviceType, message) =>
                {
                    appendEventText($"eventType: {eventType} type: {deviceType} message: {message.Trim()}");
                }
            },
            { IOEventType.Debug,
                (eventType, deviceType, message) =>
                {
                    appendEventText($"eventType: {eventType} type: {deviceType} message: {message.Trim()}");
                }
            },
            { IOEventType.Detach,
                (eventType, deviceType, message) =>
                {
                    appendEventText($"eventType: {eventType} type: {deviceType} message: {message.Trim()}");
                }
            },
            { IOEventType.SerialDeviceReadError,
                (eventType, deviceType, message) =>
                {
                    appendEventText($"eventType: {eventType} type: {deviceType} message: {message.Trim()}");
                }
            }
        };

Then pass the callbacks into the instantiated IOManager

    _ioManager.SubscribeToEvents(eventCallbacks);

Using this method:

public void SubscribeToEvents(IDictionary<IOEventType, ControllerEventDelegate> eventSubscriptions)

Or to subscribe to an individual event or update the subscription:

public void SubscribeToEvent(IOEventType eventType, ControllerEventDelegate callback)

Alternatively you can subscribe to all events with a single callback using the following method:

public void SubscribeToAllEvents(ControllerEventDelegate callback)

Handling all fallback events

As an alternative to callbacks a hook class can be passed to handle events instead of callbacks directly. Implement the interface MychIO.IDeviceErrorHandler and pass it to the following method on IOManager:

public void AddDeviceErrorHandler(IDeviceErrorHandler errorHandler)

Types of events

Event types are represented as Enums in the MychIO.Eveent class under IOEventType

Event Enum Name Information
Attach Sent on controlled device connect
Detach Sent on controlled device disconnect
ConnectionError Sent on failure to establsh connecton
SerialDeviceReadError Sent on failure of read loop for serial port
HidDeviceReadError Sent on failure of read loop for hid device
ReconnectionError Sent on exception thrown during Reconnection attempt
Debug Used for debugging not to be used in production
InvalidDevicePropertyError Sent when user defined property does not exist for a device

Connecting To Devices

Currently this system supports three device types specified in MychIO.Device.DeviceClassification.

Each device type has specified interaction zones (type Enums):

Interaction Zones:

Defined State (type Enum) of these zones:

Using these enums we can construct callbacks that are executed only when a specific zones input state changes. Doing this we can specify any specific logic we want to trigger when these states are changed.

An example of generating these callbacks:

var touchPanelCallbacks = new Dictionary<TouchPanelZone, Action<TouchPanelZone, InputState>>();
foreach (TouchPanelZone touchZone in System.Enum.GetValues(typeof(TouchPanelZone)))
{
    // _touchIndicatorMap is a map of TouchPanelZone => GameObject
    if (!_touchIndicatorMap.TryGetValue(touchZone, out var touchIndicator))
    {
        throw new Exception($"Failed to find GameObject for {touchZone}");
    }

    touchPanelCallbacks[touchZone] = (TouchPanelZone input, InputState state) =>
    {
        _executionQueue.Enqueue(() =>
        {
            // In this execution queue callback we any changes we need to (This will happen on the MainThread)
            touchIndicator.SetActive(state == InputState.On);
        });
    };
}

You can connect to the three supported devices (TouchPanel, ButtonRing and LedDevice) using the following functions as follows:

_ioManager
    .AddTouchPanel(
        AdxTouchPanel.GetDeviceName(),
        inputSubscriptions: touchPanelCallbacks
    );
_ioManager.AddButtonRing(
    AdxIO4ButtonRing.GetDeviceName(),
    inputSubscriptions: buttonRingCallbacks
);
_ioManager.AddLedDevice(
   AdxLedDevice.GetDeviceName()
);

Method Signatures for these methods are as follows:

public void AddTouchPanel(
    string deviceName,
    IDictionary<string, dynamic> connectionProperties = null,
    IDictionary<TouchPanelZone, Action<TouchPanelZone, InputState>> inputSubscriptions = null
)
public void AddButtonRing(
    string deviceName,
    IDictionary<string, dynamic> connectionProperties = null,
    IDictionary<ButtonRingZone, Action<ButtonRingZone, InputState>> inputSubscriptions = null
)
public void AddLedDevice(
    string deviceName,
    IDictionary<string, dynamic> connectionProperties = null
)

Where,

Connection/Device Properties

The current implemented devices have the following connection objects:

Device Concrete Class ConnectionProperties Object
AdxTouchPanel SerialDeviceProperties
AdxLedDevice SerialDeviceProperties
AdxIO4ButtonRing HidDeviceProperties
AdxHIDButtonRing HidDeviceProperties

ConnectionProperties implement the standard interface IConnectionproperties that methods for serialization/unserialziation of properties. Allowing for generic handling of device properties.

        IDictionary<string, dynamic> GetProperties();
        IConnectionProperties UpdateProperties(IDictionary<string, dynamic> updateProperties);

IConnectionProperties objects can emit InvalidDevicePropertyError events when a user provides invalid device properties.

Adding Custom Connection Properties to A Device

To add connection properties to a device you instantiate a new ConnectionProperties class specific to the device you would like to attach. Then instantiate an appropriate IConnnectionProperties class and use its copy constructor. You can call the GetDefaultDeviceProperties method on the specific device you want to get the standard properties set for the device and then pass whatever other properties you wish to change. Then call the GetProperties method on this properties object to serialize the properties into a type agnostic dictionary.

var propertiesTouchPanel = new SerialDeviceProperties(
    AdxTouchPanel.GetDefaultDeviceProperties(),
    comPortNumber: "COM10"
).GetProperties();

_ioManager
    .AddTouchPanel(
        AdxTouchPanel.GetDeviceName(),
        propertiesTouchPanel,
        inputSubscriptions: touchPanelCallbacks
);

// Another way to add custom properties
_ioManager.AddButtonRing(
    AdxIO4ButtonRing.GetDeviceName(),
    new Dictionary<string, dynamic>(){
        { "PollingRateMs", 0 },
        { "DebounceTimeMs", 5}
    },
    inputSubscriptions: buttonRingCallbacks
);

Retreiving Device Properties

A Device class (e.g. AdxIO4ButtonRing) has the following methods for retrieving/creating device properties:

public static Type GetDevicePropertiesType()

Returns the IConnectionProperties concrete type for casting purposes

public static T3 GetDefaultDeviceProperties()

Returns the default concrete IConnectionProperties class (e.g. SerialDeviceConnectionProperties). This will return a hard copy of the standard device connection properties for that device.

public static IConnectionProperties GetDefaultConnectionProperties()

Does the same as GetDefaultDeviceProperties except it returns an IConnectionProperties interface.

Retrieving Set Device Properties

On IOManager the following method can be used to retrieve the current set IConnectionProperties (Device properties) for a device:

public IDictionary<string, dynamic>? GetDeviceProperties(DeviceClassification deviceClassification)

Polling/Debounce Properties

All devices that give input have debounce and polling settings.

Delay Type Connection Class Interaction
Polling SerialDeviceConnection Delay happens directly after a read from the device (read is blocking)
Debounce SerialDeviceConnection Delay happens for each individual zone interaction
Polling HidDeviceConnection Delay happens after every unique read (read is non-blocking)
Debounce HidDeviceConnection Delay happens for each individual zone interaction

Writing to Devices

Once the connection to a device is established, send commands to execute specific functions using the follow IOManager method:

public async Task WriteToDevice(DeviceClassification deviceClassification, params Enum[] command)

Where,

Changing Interaction Subscriptions

Controller input needs to be dynamic based on scene (i.e. menu versus in game) callbacks can be changed and reloaded using the following functions.

Adding/Replacing Input (Subscription) callbacks

public void AddTouchPanelInputSubscriptions(
    IDictionary<TouchPanelZone, Action<TouchPanelZone, InputState>> inputSubscriptions,
    string tag
)

public void AddButtonRingInputSubscriptions(
    IDictionary<ButtonRingZone, Action<ButtonRingZone, InputState>> inputSubscriptions,
    string tag
)

Where,

Important Note: this will only update the internal state of the IOManager itself, if you overwrite a tag that already exists and is currently loaded it will not change what the device is currently using for inputSubscriptions. You must call the Change methods. This is intentional as to prevent side effects.

Switch Input (Subscription) callbacks

To change what callbacks your zones are subscribed to (i.e. update a devices subscription callbacks) you can call the following functions. Internally this will halt reading on the device, then replace the callbacks then start reading again:

public void ChangeTouchPanelInputSubscriptions(string tag)
public void ChangeButtonRingInputSubscriptions(string tag)

Where,

Executing input callbacks

To execute the Subscription callbacks simply add the following to your Update() method in unity:

    while (_executionQueue.TryDequeue(out var action))
    {
        action();
    }

This will execute all callbacks passed to the queue every frame

Fallback/Status methods

Warning: these methods introduce a lot of overhead (particularly for HID devices) and should only be called in emergency situations

In case of failure you can use the following methods to determine a devices status or attempt to restore the connection/read loop:

IsReading

public bool IsReading(DeviceClassification deviceClassification)

StartReading

Attempts to restart the read loop for the connected device (If device is not connected this will fail and return false)

public bool StartReading(DeviceClassification deviceClassification)

IsConnected

Checks if device port is open and can start reading

public bool IsConnected(DeviceClassification deviceClassification)

Reconnect

If called this will recreate the device as if you are calling AddTouchDevice, AddButtonRing, etc methods, it will also start reading automatically so no need to call StartReading(). Only to be used as a last ditch effort before just Destorying and recreating the IOManager. If there is a hard exception in this method it will throw a IOEventType.ReconnectionError Event

public bool ReConnect(DeviceClassification deviceClassification)

Testing and Development

For integration of other controllers, testing, contributing there exists a unity development project that can be cloned here.

Adding a Device

To integrate a Serial or HID device create a class file in ButtonRing, LedDevice or TouchPanel and inheriting from abstract class MychIO.Device.Device<T1, T2, T3>, where:

This abstract class is split into two partial classes (BuildProperties and Base). You must override all static methods located in Device.BuildProperties for proper instantiation of the device to occur.

After creation of your custom controller interface class simply go to DeviceFactory and add your deviceName to the following Dictionary to facilitate loading it using IOManager:

private static Dictionary<string, Type> _deviceNameToType = new()
{
    { AdxTouchPanel.GetDeviceName(), typeof(AdxTouchPanel) },
    { AdxIO4ButtonRing.GetDeviceName(), typeof(AdxIO4ButtonRing) },
    { AdxHIDButtonRing.GetDeviceName(), typeof(AdxHIDButtonRing) },
    { AdxLedDevice.GetDeviceName(), typeof(AdxLedDevice) },
    // Add other devices here...
};

Adding a Connection

To create a new connection class inherit from the abstract class MychIO.Connection.Connection. The abstract Connection class is split into two partial classes (Base and BuildProperties). Note all static methods in the partial class BuildProperties must be overridden in the child class for proper instantiation of it.

After creation of your custom connection class you must add your class to the MychIO.Connection.ConnectionFactory dictionary:

private static Dictionary<ConnectionType, Type> _connectionTypeToConnection = new()
{
    { ConnectionType.HID, typeof(HidDeviceConnection) },
    { ConnectionType.SerialDevice, typeof(SerialDeviceConnection) }
    // Add other connections here...
};

Adding a Device Command

Each Device type has a Enum that specifies specific Commands:

These commands are setup in their respected device classes and can be made as complex as required for example TouchPanelCommands are placed in the concerete class as follows:

public static readonly IDictionary<TouchPanelCommand, byte[][]> Commands = new Dictionary<TouchPanelCommand, byte[][]>
{
    { TouchPanelCommand.Start, new byte[][] { new byte[] { 0x7B, 0x53, 0x54, 0x41, 0x54, 0x7D } } },
    { TouchPanelCommand.Reset, new byte[][] { new byte[] { 0x7B, 0x52, 0x53, 0x45, 0x54, 0x7D } } },
    { TouchPanelCommand.Halt, new byte[][] { new byte[] { 0x7B, 0x48, 0x41, 0x4C, 0x54, 0x7D } } },
};