Generic Devices: Report repeated events even if monitored bytes don't change

Generic Devices are a great addition to the feature set. I used to use MacDial to monitor events of my SurfaceDial knob (a neat piece of hardware). From my experience hacking MacDial, I know that if I turn the knob right, it reports the same state for every "click" (a rotation of "n" degrees). Unfortunately, BTT only reports a single event, no matter how far right I turn the knob. Could you make that optional? That would make rotary knobs like the SurfaceDial actually useful for macOS.

thanks so much

ah yes I will add a function that you can call to make it recognize again. Will upload a version that includes this later!

In 4.005 you can now just call
bttGetNextEvenWithoutChange(targetDevice, reportID)
from your analyzer function, then it will get the next report even if there hasn't been a change in the data itself.

Works like a charm -- very nice, and thanks so much.

As soon as you get around to Support sending "Key Repeat" events for keyboard shortcut action, it will turn my SurfaceDial into a nice shuttle for Final Cut Pro :slight_smile:

Nice, would be great if you could share your script!

The key repeat thing is on my TODO list and I should get to it soon.

Sure, very simple, though:

function analyzeDeviceInput(targetDevice, reportID, reportDataHex) {
    let reportBuffer = buffer.Buffer.from(reportDataHex, 'hex');
    if(reportBuffer.readUInt8(1) === 0x03) {
        bttTriggerDeviceTrigger(targetDevice, 'button');
    } else if(reportBuffer.readUInt8(2) === 0x01) {
        bttTriggerDeviceTrigger(targetDevice, 'right');
    } else if(reportBuffer.readUInt8(2) === 0xff) {
        bttTriggerDeviceTrigger(targetDevice, 'left');
    }
    bttGetNextEvenWithoutChange(targetDevice, reportID)
}

The Dial actually has some other nice features, like haptic feedback and configurable sensitivity. I might try using the executeBTTCommand feature to enable those at some point.

1 Like

Thanks for sharing! Things like the Surface Dial are exactly the type of device I thought about when adding this generic device functionality. It's pretty nice being able to revive a small device with a few lines of code.

1 Like

Hi! did you ever get "button" working correctly? It seems to fire twice for me (e.g. I have it mapped to "play/pause" and pressing down and releasing causes media to play and then immediately pause again.

Maybe it's triggering for "press" and "release"? Does the data look exactly the same for both?

I think it has something to do with the next event listener:

If i remove that line, the "button" behaves as expected but the left/right rotation only triggers once (also as expected without that listener)

You could call this only for the dial:

function analyzeDeviceInput(targetDevice, reportID, reportDataHex) {
    let reportBuffer = buffer.Buffer.from(reportDataHex, 'hex');
    if(reportBuffer.readUInt8(1) === 0x03) {
        bttTriggerDeviceTrigger(targetDevice, 'button');
    } else if(reportBuffer.readUInt8(2) === 0x01) {
        bttTriggerDeviceTrigger(targetDevice, 'right');
        bttGetNextEvenWithoutChange(targetDevice, reportID)
    } else if(reportBuffer.readUInt8(2) === 0xff) {
        bttTriggerDeviceTrigger(targetDevice, 'left');
        bttGetNextEvenWithoutChange(targetDevice, reportID)
    }
}

If the button is being pressed while the wheel is still being turned, you'll want to act on both and you'll also want to be sure to call bttGetNextEvenWithoutChange() as you want to keep being notified about the wheel. So using if .. else ... else ... is generally a bad idea for buttons which aren't mutually exclusive, as multiple things could be happening at the same time and events would get lost.

The solution is to store the previous report and, in case of buttons, only act on changes. See Generic Devices: Philips Footcontrol ACC2330 for an example.

1 Like

Just replying to thank all involved in this thread. I don't need the Mac Dial app anymore and am now able to create my own dial workflow. Right now I have only one trigger for a turn of the wheel, but I'm going to try and create something that does this:

button: swich between modes
mode1 = zoom
mode2 = volume
mode3 = app switcher

1 Like

Thanks for this.. implemented same here.
For mode switching.. it would be good to reclaim the button press and add an additional command.

Would it be possible to enact something like key sequences for generic device button presses?
In particular I'd like to add double/triple press commands to the surface dial button press.

I've tried by mapping a keyboard shortcut to the surface dial button press, and then having a key sequence triggered upon multiple presses of that keyboard shortcut, but that doesn't seem to work.. perhaps Key Sequences only register real keyboard commands and not virtual keyboard shortcuts enacted by Better Touch Tool?

If so.. can this be done by scripting? Will appreciate help here please.

In your scripts you can keep state by defining variables outside of your analyzeDeviceInputMethod.

Maybe something like this

var lastPressTime = -1;
function analyzeDeviceInput(targetDevice, reportID, reportDataHex) {
    let reportBuffer = buffer.Buffer.from(reportDataHex, 'hex');
    if(reportBuffer.readUInt8(1) === 0x03) {
        lastPressTime = Date.now();

        setTimeout(()) => {
               if(lastPressTime - Date.now() < 0.3 ) {
                   bttTriggerDeviceTrigger(targetDevice, 'button-double-press');
                   lastPressTime = -1;
               } else {
                   bttTriggerDeviceTrigger(targetDevice, 'button-single-press');

               }
         }, 400);
        

    } else if(reportBuffer.readUInt8(2) === 0x01) {
        bttTriggerDeviceTrigger(targetDevice, 'right');
        bttGetNextEvenWithoutChange(targetDevice, reportID)
    } else if(reportBuffer.readUInt8(2) === 0xff) {
        bttTriggerDeviceTrigger(targetDevice, 'left');
        bttGetNextEvenWithoutChange(targetDevice, reportID)
    }
}
1 Like

Thank you and for your fantastic work on BTT which is an integral part of my productivity! much appreciated.

Unfortunately I couldn't get this to work (I'm not a coder, but took a crack at it).

  • Initially no button presses were logging. I deleted a closing bracket from setTimeout and that fixed it.

  • right and left now log correctly, but all button presses are registered as 'button-double-press' rather than distinguishing between a single and double press.

  • I assume this is because the code above doesn't save a second button press to compare it to the time elapsed since the first button press. and so lastPressTime - Date.now will always be 0. which is smaller than 0.3 - hence triggering button-double-press.

  • So feel there needs to be a register of a second press and comparison to the time of the first to see if enough time has elapsed to register it as a single press, or to register as a double press.

In short, I have little idea, but I hope you take this attempt to understand from a mere mortal as sufficient sacrifice to warrant a pity code fix/ explanation of what I'm doing wrong to suit my request :sweat_smile:

Thanks

Took another shot and getting there :slight_smile:

  • Now registers double press, and single press. Problem is that when the double press is registered, it also logs a single press.. should fix this soon, think it's just a matter of nesting the else statement of the single press.

var lastPressTime = -1;
function analyzeDeviceInput(targetDevice, reportID, reportDataHex) {
let reportBuffer = buffer.Buffer.from(reportDataHex, 'hex');
if(reportBuffer.readUInt8(1) === 0x03) {
lastPressTime = Date.now();

    setTimeout(() => {
           if(reportBuffer.readUInt8(1) === 0x03) {
               secondPressTime = Date.now();
           } if(secondPressTime - lastPressTime < 300 ) {
               log('button-double-press!');
               bttTriggerDeviceTrigger(targetDevice, 'button-double-press');
               lastPressTime = -1;
           } else {
               log('button-single-press!');
               bttTriggerDeviceTrigger(targetDevice, 'button-single-press');
               lastPressTime = -1;

           }
     }, 400);
    

} else if(reportBuffer.readUInt8(2) === 0x01) {
    log('right!');
    bttTriggerDeviceTrigger(targetDevice, 'right');
    bttGetNextEvenWithoutChange(targetDevice, reportID)
} else if(reportBuffer.readUInt8(2) === 0xff) {
    log('left!');
    bttTriggerDeviceTrigger(targetDevice, 'left');
    bttGetNextEvenWithoutChange(targetDevice, reportID)
}

}

Posting here in case it's useful to anyone..
I ended up finding a workaround in BTT settings rather than fixing the script.

Microsoft Surface Dial.bttpreset (35.0 KB)

The script under Generic Devices > Surface Dial provides for double press, single press, right jog, left jog, hold press and jog right (nextTrack trigger), and hold press and jog left (previousTrack trigger).

Double press is also a top level trigger. By default it async delays next action by 0.3 seconds. This is as the script is flawed and recognises a double press followed by a single press whenever there is a double press.. so the delay allows the single press to be registered before opening Roon where it would pause/play (can configure whatever app you want to open).

I then have Roon specific triggers. When Roon is open, these map Roon keyboard to the triggers:
single press = play/pause
right = volume up keyboard shortcut - rotation linked to volume, left = volume down, press and hold whilst turning right = next track, press and hold whilst turning left = previous track.

The press and hold actions have a delay next action by 1.5 seconds.. again this is a work around for the broken script, as otherwise pressing is recognised as multiple button presses, and triggers unwanted actions. so you have 1.5 seconds to release the button when changing track, during which other actions won't be recognised. seems to work ok though.

A Double Press when Roon is opens the Menu Bar Controller for Sonos app. Here double pressing returns to Roon.

So basically the Double Press rotates between applications, starting with Roon.

This works well for my old Mac that is used specifically for Roon server, but may be problematic for your use case due to the script limitations.

Hope it's beneficial to someone, and please post any improvements you may make

1 Like