Input manager that supports Mai2 controllers in 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.
Full example of using the package is viewable here
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.
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)
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)
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 |
Currently this system supports three device types specified in MychIO.Device.DeviceClassification
.
Each device type has specified interaction zones (type Enums):
Interaction Zones:
MychIO.Device.TouchPanelZone
MychIO.Device.ButtonRingZone
Defined State (type Enum) of these zones:
MychIO.Device.InputState
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,
MychIO.Connection.IConnectionProperties
interface and can be easily serialized/unserialized using the instantiated IConnection class (IDictionary<string, dynamic> <==> concrete IConnection object)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.
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
);
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.
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)
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 |
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,
MychIO.Device.DeviceClassification
MychIO.Device.ButtonRingCommand
MychIO.Device.TouchPanelCommand
MychIO.Device.LedDeviceCommand
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.
public void AddTouchPanelInputSubscriptions(
IDictionary<TouchPanelZone, Action<TouchPanelZone, InputState>> inputSubscriptions,
string tag
)
public void AddButtonRingInputSubscriptions(
IDictionary<ButtonRingZone, Action<ButtonRingZone, InputState>> inputSubscriptions,
string tag
)
Where,
MychIO.IOManager.STANDARD_INPUT
(type: string)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.
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,
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
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:
public bool IsReading(DeviceClassification deviceClassification)
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)
Checks if device port is open and can start reading
public bool IsConnected(DeviceClassification deviceClassification)
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)
For integration of other controllers, testing, contributing there exists a unity development project that can be cloned here.
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...
};
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...
};
Each Device type has a Enum that specifies specific Commands:
LedDeviceCommand
ButtonRingCommand
TouchPanelCommand
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 } } },
};