Generic Devices: Please give function analyzeDeviceInput() access to the previous state

The function analyzeDeviceInput(targetDevice, reportID, reportDataHex) is called whenever the data coming from a device has changed in at least one of the monitored bytes. With

let reportBuffer = buffer.Buffer.from(reportDataHex, 'hex');

the current state can be retrieved, but in order to be able to find out which bits have really changed, we'd need access to the previous report, too, like

let reportPreviousBuffer = buffer.Buffer.from(reportPreviousDataHex, 'hex');

Why? Let say I have a device with two buttons, A and B, which when pressed separately change byte 1 from 0x00 to 0x01 or 0x00 to 0x02, respectively. When both were pressed at exactly the very same time, byte 1 would change from 0x00 to 0x03, but this ideal transition does never quite happen; in practice one of the button will virtually always be pressed a tiny split second before the other one, and so byte 1 will first change from 0x00 to 0x01 and immediately thereafter to 0x03, or it will first change to 0x02 and then immediately thereafter to 0x03.

I.e. if we got the following code

    if((reportBuffer.readUInt8(1) & 0x01) == 0x01) {
        log('A');
        bttTriggerDeviceTrigger(targetDevice, 'A');
    } // <- an "else" here wouldn't really make things better but break them in a different way!
    if((reportBuffer.readUInt8(1) & 0x02) == 0x02) {
        log('B');
        bttTriggerDeviceTrigger(targetDevice, 'B');
    }

one of the buttons will virtually always be triggered twice even though it is only pressed once, i.e. we will see a trigger sequence of either A, A, B or B, A, B.

What I want to do is first find out if there was a change for a certain bit and only in this case process it further, like

let bit_A = 0x01;
let prev_A = (reportPreviousBuffer.readUInt8(1) & bit_A);
let curr_A = (reportBuffer.readUInt8(1) & bit_A);

if((curr_A ^ prev_A) != 0 && curr_A != 0) {
        log('A');
        bttTriggerDeviceTrigger(targetDevice, 'A');
    }
// and so forth for the other buttons

I hope I managed to describe this well enough, please feel free to ask if not!

You can define global variables to store any previous state if you write them before the analyzeDeviceInput(targetDevice, reportID, reportDataHex)

For example

let previousData = '';
function analyzeDeviceInput(targetDevice, reportID, reportDataHex) {  

  previousData = reportDataHex
}
1 Like

Awesome! Thank you very much, also for the incredibly quick reply. I was already thinking about

let result = await callBTT('set_number_variable', {variable_name: 'ReportByte1OfDeviceBlah' to: reportBuffer.readUInt8(1)});

but this wouldn't scale all to well. Great that "local global" variables can be used. Thanks again, this will help me proceed with my little project. :slight_smile:

Yep that would not scale well :slight_smile:

Just be careful if you have multiple device analyzer scripts - these variables are global for all device analyzers running in BTT. So you should give them some unique name to prevent conflicts. (For the analyzeDeviceInput function BTT takes care of this, but not for global variables)

1 Like

What happened if multiple analyzer scripts included a let previousData = ''; line? Would these equally-named variables clobber each other or would the whole JavaScript execution engine fail when a let statement is being executed on an already existing variable name?

Is the targetDevice argument to analyzeDeviceInput() a unique id string which could serve as a unique dictionary key for each attached device? (But then there would need to be a let for this dictionary somewhere, too, so we're back to square one. Hmm.)

I haven't actually tested it, but I think it would throw an exception when using let because the variable is already defined.

The target device is a unique id, that would work.
Given that it's Java Script, you probably don't even need to define it via let / const / var first. I think you could just use it in your function.

So you could do something like this

function analyzeDeviceInput(targetDevice, reportID, reportDataHex) {  

if(!previousStates) {
 previousStates = {}
}

// DO SOMETHING
previousStates[targetDevice]

previousStates[tagetDevice] = reportDataHex

}
1 Like

Thank you! I'll try and report back. FWIW, the device I am working on is the Xbox Adaptive Controller. It is not only a game controller, but also has a lot of 3,5mm input sockets to which one can connect external switches. BTT has been the only tool that I've come across that lets me do something with it on the Mac, so allow me take this opportunity to thank you very much for this wonderful application. You rock!

oh that's really cool!

Let me know if you run into issues, the whole Generic Device support is still very beta, but I should be able to fix stuff quickly if necessary.

1 Like

I got it working! And quite elegantly so I think:

var xac_globals;
function analyzeDeviceInput(targetDevice, reportID, reportDataHex)
{
    let lastReport =
        ((xac_globals ||= {})[targetDevice] ||= {})["lastReport"] ||= 
            buffer.Buffer.alloc(reportDataHex.length, 0);
    
    let thisReport = reportDataHex;
    xac_globals[targetDevice]["lastReport"] = thisReport;

    let Buttons = [
        [ "A", 14, 0x01 ],
        [ "B", 14, 0x02 ]
    ];

    for (const [ b_name, b_byte, b_mask ] of Buttons) {      
        let last_b_state = (lastReport.readUInt8(b_byte) & b_mask);
        let this_b_state = (thisReport.readUInt8(b_byte) & b_mask);

        if (last_b_state ^ this_b_state) {
            let b_event = this_b_state ? b_name : b_name + "_UP";
            bttTriggerDeviceTrigger(targetDevice, b_event)
        }
    }

    // If you want to get the next report even though,
    // the data has not changed, call this function:
    // bttGetNextEvenWithoutChange(targetDevice, reportID)
}

The global variable (I chose xac_globals, feel free to come up with a convention) needs to be defined in order for the code to know about it, and var appears to do exactly the right thing, i.e. it will only create the variable if it does not already exist and otherwise leave it alone. Also note that your reportBuffer = buffer.Buffer.from(reportDataHex, 'hex'); from the sample code is not needed because reportDataHex is a buffer already.

Thanks again for your help @Andreas_Hegenberg!

Wait that's weird. I'm passing a hex string to the reportDataHex argument, how would that become a Buffer :thinking:

Ohh I think this will work while in "edit mode", but when the code runs outside of the webview, it will need the reportBuffer = buffer.Buffer.from(reportDataHex, 'hex')

Did you try whether it is able to trigger anything while the editor is closed?

No, I have only ever been in the editor so far. I'll change my code accordingly, no problem.

I'll fix it so that the webview version also receives the hex string to prevent confusion. (It doesn't cause an error because Buffer.from also accepts a buffer, but I can't pass a buffer to the code running outside the webview)

1 Like

Never mind, this does work both inside and outside the editor:

var xac_globals;
function analyzeDeviceInput(targetDevice, reportID, reportDataHex)
{
    const lastReportHex =
        ((xac_globals ||= {})[targetDevice] ||= {})["lastReport"] ||= 
            "00".repeat(reportDataHex.length);
    
    const lastReport = buffer.Buffer.from(lastReportHex, 'hex');

    const thisReport =
        (typeof reportDataHex === "string")
            ? buffer.Buffer.from(reportDataHex, 'hex')
            : reportDataHex;

    xac_globals[targetDevice]["lastReport"] = thisReport.toString("hex");

    const Buttons = [
        [ "A", 14, 0x01 ],
        [ "B", 14, 0x02 ]
    ];

    for (const [ b_name, b_byte, b_mask ] of Buttons) {      
        const last_b_state = (lastReport.readUInt8(b_byte) & b_mask);
        const this_b_state = (thisReport.readUInt8(b_byte) & b_mask);

        if (last_b_state ^ this_b_state) {
            const b_event = this_b_state ? b_name : b_name + "_UP";
            log("deviceTrigger: "+ b_event);
            bttTriggerDeviceTrigger(targetDevice, b_event);
        }
    }

    // If you want to get the next report even though,
    // the data has not changed, call this function:
    // bttGetNextEvenWithoutChange(targetDevice, reportID)
}
1 Like

Hey @Andreas_Hegenberg, today I got a Philips ACC2330/00 Foot Switch, which is a four-button USB HID device. I already confirmed it working with BTT 4.0.62, and since the buttons are all digital, I shall quickly be able to post a preset for it which makes use of the "previous state" technique the two of us developed in this thread here. :+1:

Just one question: For maximum user flexibility I want to provide both a "button pressed" and a "button released" trigger. What do you suggest as a good naming convention?

Do you prefer the "button released" trigger for a button named pedalLeft be called pedalLeftUp or pedalLeft_up or pedalLeft_UP or something completely different like with _released instead of _up?

I am planning to call the "button pressed" trigger just like I name the button itself, as this appears consistent with established conventions. Conventions should be a good practice as we are going to get a lot of Generic Devices integrated, so please advise!

I think you can use whatever you like best :slight_smile: As an early adapter you can kind of define the conventions.

I think I'd prefer one of the versions with an underscore, because they would be easier to process programmatically if there is ever a need to do that.

1 Like

OK, then I choose the convention to be _up — all lower case in the sense that no developer should be required to press the shift key unless there's a good reason. :smirk_cat:

Thank you once again for the super quick reply! :heart_eyes_cat:

FYI, here is a complete device implementation using this technique, feel free to copy and adapt for your own purposes:

Generic Devices: Philips Footcontrol ACC2330

1 Like