Knob Layout

Hello,

I have a custom HID device and am creating a DeviceState for it.

The device has a knob where each position corresponds to an exclusive state.

The input is a bit-field and comes in on a byte with other input, where the last four bits signify which state/position the knob is in.

Looking at the docs and playing around with different [InputControl] attributes, I’m unsure how to implement this use case. It seems “Dpad” will work, but the knob state does not correspond to up, down, left, right. I also have another knob that has more than 4 states.

Thanks.

Recommend taking a look at how DualShock4HIDInputReport sets up the dpad. From what you describe, you’re looking at a similar setup, i.e. basically an enum. The key ingredient is the DiscreteButton layout with the minValue, maxValue, nullValue, and wrapAtValue parameters. If there’s no range of values (i.e. no diagonals), it’s simpler. Each value is just one state hardcoded on the DiscreteButton.

I’m seeing errors:
“Could not re-recreate input device ‘{deviceState.description}’ with layout ‘{deviceState.layout}’ and variants ‘{deviceState.variants}’ after domain reload”
“Cannot find layout matching device description ‘{description}’”, nameof(description)"

When trying to do the following:

[InputControl( name = "WINDOW_COVER_STATE", layout = "Button", bit = 0, displayName = "Cover State" )]
[InputControl( name = "RESET_HEADING_BUTTON", layout = "Button", bit = 1, displayName = "Reset Heading")]
// bits 2 & 3 are unused
[InputControl(name = "dpad", format = "BIT", layout = "Dpad", sizeInBits = 4, defaultState = 0)]
[InputControl(name = "dpad/up", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 4, sizeInBits = 1)]
[InputControl(name = "dpad/right", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 5, sizeInBits = 1)]
[InputControl(name = "dpad/down", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 6, sizeInBits = 1)]
[InputControl(name = "dpad/left", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0, maxValue=1", bit = 7, sizeInBits = 1)]
public byte inputByte0;

What I’d really like to do is:

[InputControl( name = "KNOB", format = "BIT", layout = "DiscreteButton", sizeInBits = 4, displayName = "Knob" )]
[InputControl( name = "KNOB/off", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 4, sizeInBits = 1, displayName = "Knob off" )]
[InputControl( name = "KNOB/stdby", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 5, sizeInBits = 1, displayName = "Knob stdby" )]
[InputControl( name = "KNOB/rdy", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 6, sizeInBits = 1, displayName = "Knob rdy" )]
[InputControl( name = "KNOB/ovrd", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 7, sizeInBits = 1, displayName = "Knob ovrd" )]

That’s usually a sign of the layout not becoming available in time (usually before the first input update). What’s the values of the fields in the error message here? Is it from your layout?

If you have discrete bits corresponding to each control, you do not need DiscreteButton.

[InputControl( name = "KnobOff", layout = "Button", bit = 4, sizeInBits = 1, displayName = "Knob off" )]

[InputControl( name = "KnobStdby", layout = "Button", bit = 5, sizeInBits = 1, displayName = "Knob stdby" )]

[InputControl( name = "KnobRdy", layout = "Button", bit = 6, sizeInBits = 1, displayName = "Knob rdy" )]

[InputControl( name = "KnobOvrd", layout = "Button", bit = 7, sizeInBits = 1, displayName = "Knob ovrd" )]

To introduce a “KNOB” control at the top with “off”, “stdby”, “rdy”, and “ovrd” as children, you need a custom control layout. However, doing so really only makes sense if indeed it makes sense to treat the entire knob as a single control with a single value for the entire knob. If that is indeed the case, you can create a custom InputControl-derived control in C# (same way that Dpad is set up). Something like (just sketched out; haven’t tried to compile or run this so I may have made mistakes):

[Flags]
public enum KnobState
{
    Off = 1 << 0,
    Stdby = 1 << 1,
    Rdy = 1 << 2,
    Ovrd = 1 << 3,
}

public class KnobControl : InputControl<KnobState>
{
    public ButtonControl off { get; private set; }
    public ButtonControl stdby { get; private set; }
    public ButtonControl rdy { get; private set; }
    public ButtonControl ovrd { get; private set; }

    protected void override void FinishSetup()
    {
        base.FinishSetup();

        off = GetChildControl<ButtonControl>("off");
        stdby = GetChildControl<ButtonControl>("stdby");
        rdy = GetChildControl<ButtonControl>("rdy");
        ovrd = GetChildControl<ButtonControl>("ovrd");
    }

    public override unsafe KnobState ReadUnprocessedValueFromState(void* statePtr)
    {
        return (KnobState) stateBlock.ReadInt(statePtr);
    }
}

With that registered, you can add a “Knob” control with something like this:

[InputControl(name = "KNOB", layout = "Knob", bit = 4, sizeInBits = 4)]
[InputControl(name = "KNOB/off", bit = 4)]
[InputControl(name = "KNOB/stdby", bit = 5)]
[InputControl(name = "KNOB/rdy", bit = 6)]
[InputControl(name = "KNOB/ovrd", bit = 7)]

Note that using slashes in paths has special meaning. It signifies a control setting that does not introduce a new control but rather modifies the settings of a child control introduced by another layout.

1 Like

Yes, the values are from my layout.

“Could not re-recreate input device ‘myDeviceName’ with ‘MyInputDevice’ and variants ‘Default’ after domain reload”
“Cannot find layout matching device description ‘myDeviceName’”
“parameter name: description”

The custom Knob control is exactly what I’m looking for, but after trying to implement it I’m seeing the following errors:

Layout ‘MyInputDevice’ matches existing device ‘MyDeviceName’ but failed to instantiate: UnityEngine.InputSystem.Layouts.InputControlLayout+LayoutNotFoundException: Cannot find layout ‘Knob’ used in control ‘KNOB’ of layout ‘MyInputDevice’ —>
LayoutNotFoundException: Cannot find control layout ‘Knob’

```csharp
*using System;

using UnityEngine.Scripting;

[Flags]
public enum KnobState
{
Off = 1 << 0,
Stdby = 1 << 1,
Rdy = 1 << 2,
Ovrd = 1 << 3,
}

namespace UnityEngine.InputSystem.Controls
{
[Preserve]
public class KnobControl : InputControl
{
public ButtonControl off { get; private set; }
public ButtonControl stdby { get; private set; }
public ButtonControl rdy { get; private set; }
public ButtonControl ovrd { get; private set; }

    public KnobControl() { }

    protected override void FinishSetup()
    {
        base.FinishSetup();

        off = GetChildControl<ButtonControl>( "off" );
        stdby = GetChildControl<ButtonControl>( "stdby" );
        rdy = GetChildControl<ButtonControl>( "rdy" );
        ovrd = GetChildControl<ButtonControl>( "ovrd" );
    }

    public override unsafe KnobState ReadUnprocessedValueFromState( void* statePtr )
    {
        return (KnobState)stateBlock.ReadInt( statePtr );
    }
}

}*
```

You need to register the control layout. Same procedure as registering your device layout, i.e. calling InputSystem.RegisterLayout<…>. You can do it in the same place where you currently register the device layout.

InputSystem.RegisterLayout<KnobControl>("Knob");

“Layout ‘MyInputDevice’ matches existing device ‘MyDeviceName’ but failed to instantiate: System.InvalidOperationException: Control ‘/MyInputDevice/KNOB’ with layout ‘Knob’ has no size set and has no children to compute size from”

Can you show me the snippet where you add the knob to your device layout?

#if UNITY_EDITOR
[InitializeOnLoad] // Call static class constructor in editor.
#endif
[InputControlLayout( stateType = typeof( MyInputDeviceState ) )]
public class MyInputDevice : InputDevice
{
#if UNITY_EDITOR
    static MyInputDevice() { Initialize(); }
#endif

    [RuntimeInitializeOnLoadMethod]
    private static void Initialize()
    {
        InputSystem.RegisterLayout<KnobControl>( "Knob" );

        InputSystem.RegisterLayout<MyInputDevice>(
            matches: new InputDeviceMatcher()
                .WithInterface( "HID" )
                .WithCapability( "vendorId", 8263 )
                .WithCapability( "productId", 991 )
                );
    }

    public ButtonControl coverOpen { get; private set; }
    public ButtonControl resetHeading { get; private set; }
    public KnobControl knob { get; private set; }

    protected override void FinishSetup()
    {
        base.FinishSetup();

        coverOpen = GetChildControl<ButtonControl>( "WINDOW_COVER_STATE" );
        resetHeading = GetChildControl<ButtonControl>( "RESET_HEADING_BUTTON" );
        knob = GetChildControl<KnobControl>( "KNOB" );
    }

You’re missing the configuration of the knob control from above on the device.

[InputControl(name = "KNOB", bit = 4, sizeInBits = 4)]
[InputControl(name = "KNOB/off", bit = 4)]
[InputControl(name = "KNOB/stdby", bit = 5)]
[InputControl(name = "KNOB/rdy", bit = 6)]
[InputControl(name = "KNOB/ovrd", bit = 7)]
public KnobControl knob { get; private set; }

That should shut the layouter up and get rid of the exceptions.

Actually, sorry, in your case this needs to go on the knob in your MyInputDeviceState struct. And the “KNOB” control will need an explicit layout=“Knob” setting.

I have that in the device state. Will that not work?

public struct MyInputDeviceState : IInputStateTypeInfo
{
    public FourCC format => new FourCC( "HID" );

    public byte reportId;
    public byte reportLength;

    [InputControl( name = "WINDOW_COVER_STATE", layout = "Button", bit = 0, displayName = "Cover State" )]
    [InputControl( name = "RESET_HEADING_BUTTON", layout = "Button", bit = 1, displayName = "Reset Heading" )]

    [InputControl( name = "KNOB", layout = "Knob", bit = 4, sizeInBits = 4 )]
    [InputControl( name = "KNOB/off", bit = 4)]
    [InputControl( name = "KNOB/stdby", bit = 5)]
    [InputControl( name = "KNOB/rdy", bit = 6)]
    [InputControl( name = "KNOB/ovrd", bit = 7)]
    public byte inputByte0;
}

Ah doh, sorry, forgot a vital bit. We changed it some time ago so that properties need an explicit [InputControl] attribute instead of anything that is InputControl-derived getting picked up automatically. Adding [InputControl] to each KnobControl child control should do the trick.

        [InputControl]
        public ButtonControl off { get; private set; }
        [InputControl]
        public ButtonControl stdby { get; private set; }
        [InputControl]
        public ButtonControl rdy { get; private set; }
        [InputControl]
        public ButtonControl ovrd { get; private set; }
1 Like

Ah perfect now everything works. Thank you for the assistance!

For any future readers:

using System;

using UnityEngine.InputSystem.Layouts;

[Flags]
public enum KnobState
{
    Off = 1 << 0,
    Stdby = 1 << 1,
    Rdy = 1 << 2,
    Ovrd = 1 << 3,
}

namespace UnityEngine.InputSystem.Controls
{
    public class KnobControl : InputControl<KnobState>
    {
        [InputControl]
        public ButtonControl off { get; private set; }
        [InputControl]
        public ButtonControl stdby { get; private set; }
        [InputControl]
        public ButtonControl rdy { get; private set; }
        [InputControl]
        public ButtonControl ovrd { get; private set; }

        protected override void FinishSetup()
        {
            base.FinishSetup();

            off = GetChildControl<ButtonControl>( "off" );
            stdby = GetChildControl<ButtonControl>( "stdby" );
            rdy = GetChildControl<ButtonControl>( "rdy" );
            ovrd = GetChildControl<ButtonControl>( "ovrd" );
        }

        public override unsafe KnobState ReadUnprocessedValueFromState( void* statePtr )
        {
            return (KnobState)stateBlock.ReadInt( statePtr );
        }
    }
}

I’ve fully implemented my device and everything looks great in the InputDebug tool, but when running/compiling I see:

Could not re-recreate input device ‘MyDevice’ with layout ‘MyInputDevice’ and variants ‘Default’ after domain reload
ArgumentException: Cannot find layout matching device description ‘MyDevice’
Parameter name: description

Looking at it, this is a problem in the core system that we need to fix. I’ve filed a bug.

To explain, what the system does when it initializes is to run one initial update that only re-creates devices. It does so to make sure that MonoBehaviour.Start methods have devices available. However, this means that if system initialization is triggered from somewhere else (e.g. someone else running InitializeOnLoad/RuntimeInitializeOnLoadMethod code that accesses InputSystem) before your RegisterLayout code, your device won’t get recreated properly.

I’ll try to have it fixed for the next package (1.0.0-preview.2) which shouldn’t be too far out.

1 Like

Fix pending. Expected to be in 1.0.0-preview.2.

1 Like

Hey Rene,

I’ve updated to preview 2 and the exceptions have been fixed, but when subscribing to InputSystem.onDeviceChange I receive 3 device added callbacks for my device - one for each layout I’m registering (2 knobs and the device itself).

Hmm, on domain reload or also first time around?

Is it still this code?

    [RuntimeInitializeOnLoadMethod]
    private static void Initialize()
    {
        InputSystem.RegisterLayout<KnobControl>( "Knob" );
        InputSystem.RegisterLayout<MyInputDevice>(
            matches: new InputDeviceMatcher()
                .WithInterface( "HID" )
                .WithCapability( "vendorId", 8263 )
                .WithCapability( "productId", 991 )
                );
    }

And within that, you’re seeing three InputDeviceChange.Added messages?

Device recreation due to layout changes can be a bit surprising and depending on the code, it may do the right thing here. But it sure sounds fishy.

Yep its still that code. I’m listening for devices in an OnEnable(). Happens when I hit play.

        private void OnEnable()
        {
            InputSystem.onDeviceChange +=
                ( device, change ) =>
                    {
                        switch( change )
                        {
                            case InputDeviceChange.Reconnected:
                                Debug.Log( device + " Connected" );
                                break;

                            case InputDeviceChange.Disconnected:
                                Debug.Log( device + " Disconnected" );
                                break;

                            case InputDeviceChange.Added:
                            {
                                Debug.Log( device + " Added" );
                                break;
                            }
                        }
                    };
        }

If I disconnect my device while running and connect it again I only see added once. I need the added when the app first runs though because I have a simulated (UI) version of the device and I want to choose which one to use.