How to set the text on a button bound to a custom HostValueVariable?

I’m building a MIDI Remote script (Cubase Pro 15.0.21, macOS, API v1.3).

I have three square buttons that work as one-shot triggers: each one forces a transport state — Punch In, Pre-Roll, Post-Roll — to always ON (fixed value 1, never toggle), using DirectAccess on mTransport.

This follows the approach @m.c described here: Get Punch In/Out and Pre/Post-roll state with Midi Remote? - #12 by m.c

To get a valid activeMapping for the daTransport.setParameterProcessValue() call, I bind each button’s mSurfaceValue to a dummy HostValueVariable (page.mCustom.makeHostValueVariable(…)) and run the DirectAccess write inside mOnValueChange. Functionally it works perfectly.

The problem: in the surface editor (and in the hover tooltip) each of these buttons shows the text “undefined value (not defined)”. I assume this is because the bound HostValueVariable has no real host parameter/title behind it.

My questions:

Is there any way to set a custom title/text on the button (or on the HostValueVariable) so it shows something meaningful instead of “undefined value”?

Or is there a cleaner pattern to trigger a DirectAccess write from an incoming CC without binding the visible button to a dummy HostValueVariable that produces this label?

Minimal reproducible script below. Thanks in advance!

// Minimal reproducible script: three square buttons that force the transport's
// Punch In / Pre-Roll / Post-Roll to ON via DirectAccess. Each button shows
// "undefined value (not defined)" in the surface editor (the issue I'm asking about).

var midiremote_api = require('midiremote_api_v1')
var driver = midiremote_api.makeDeviceDriver('Repro', 'PunchRollRepro', 'jordikt')

var midiInput = driver.mPorts.makeMidiInput('Repro In')
var surface   = driver.mSurface
var page      = driver.mMapping.makePage('Main')

// DirectAccess on the transport
var daTransport = page.mHostAccess.makeDirectAccess(page.mHostAccess.mTransport)

// DirectAccess parameter tags
var DA_TAG_TRANSPORT_PUNCH_IN  = 4020
var DA_TAG_TRANSPORT_PRE_ROLL  = 4037
var DA_TAG_TRANSPORT_POST_ROLL = 4038

var CHANNEL_TRANSPORT = 0   // MIDI channel 1

// --- Three square buttons ---
var btnPunchInOn  = surface.makeButton(0, 0, 1, 1)
var btnPreRollOn  = surface.makeButton(0, 1, 1, 1)
var btnPostRollOn = surface.makeButton(0, 2, 1, 1)

// --- MIDI input bindings (CC 99 / 101 / 103, channel 1) ---
btnPunchInOn.mSurfaceValue.mMidiBinding.setInputPort(midiInput).bindToControlChange(CHANNEL_TRANSPORT, 99)
btnPreRollOn.mSurfaceValue.mMidiBinding.setInputPort(midiInput).bindToControlChange(CHANNEL_TRANSPORT, 101)
btnPostRollOn.mSurfaceValue.mMidiBinding.setInputPort(midiInput).bindToControlChange(CHANNEL_TRANSPORT, 103)

// --- Value bindings to dummy HostValueVariables ---
// This dummy binding is what makes the button display "undefined value (not defined)".
// It is needed only to obtain a valid activeMapping inside mOnValueChange, so the
// DirectAccess write below can run.
var hvvPunchInOn  = page.mCustom.makeHostValueVariable('hvvPunchInOn')
var hvvPreRollOn  = page.mCustom.makeHostValueVariable('hvvPreRollOn')
var hvvPostRollOn = page.mCustom.makeHostValueVariable('hvvPostRollOn')

page.makeValueBinding(btnPunchInOn.mSurfaceValue, hvvPunchInOn).mOnValueChange = function(activeDevice, activeMapping, value, diff) {
    if (value == 1) {
        btnPunchInOn.mSurfaceValue.setProcessValue(activeDevice, 0)
        var id = daTransport.getBaseObjectID(activeMapping)
        daTransport.setParameterProcessValue(activeMapping, id, DA_TAG_TRANSPORT_PUNCH_IN, 1)
    }
}

page.makeValueBinding(btnPreRollOn.mSurfaceValue, hvvPreRollOn).mOnValueChange = function(activeDevice, activeMapping, value, diff) {
    if (value == 1) {
        btnPreRollOn.mSurfaceValue.setProcessValue(activeDevice, 0)
        var id = daTransport.getBaseObjectID(activeMapping)
        daTransport.setParameterProcessValue(activeMapping, id, DA_TAG_TRANSPORT_PRE_ROLL, 1)
    }
}

page.makeValueBinding(btnPostRollOn.mSurfaceValue, hvvPostRollOn).mOnValueChange = function(activeDevice, activeMapping, value, diff) {
    if (value == 1) {
        btnPostRollOn.mSurfaceValue.setProcessValue(activeDevice, 0)
        var id = daTransport.getBaseObjectID(activeMapping)
        daTransport.setParameterProcessValue(activeMapping, id, DA_TAG_TRANSPORT_POST_ROLL, 1)
    }
}

// DirectAccess lifecycle
page.mOnActivate = function(activeDevice, activeMapping) {
    daTransport.activate(activeMapping)
}
page.mOnDeactivate = function(activeDevice, activeMapping) {
    daTransport.deactivate(activeMapping)
}

In order to avoid the “Undefined Value” labeling, indeed you need to use custom vars instead of buttons.

var aCustomVar=surface.makeCustomValueVariable("aCustomVar")
//Bind this, instead of the mSurfaceValue of a button

Depending on the occasion, I sometimes add lamps and labels while using these custom vars.

Example:

var lamp=surface.makeLamp(0,0,1,1)
var label=surface.makeLabelField(1,0,4,1)

Upon creating the page, I set the label’s text to whatever the lamp next to it is about. Example:

page.setLabelFieldText(label,"Punch-In")

Then inside the customHostValue mOnValueChange event, I get the value of the DA parameter and update the lamp accordingly. Example:

lamp.mSurfaceValue.setProcessValue(activeDevice,autoPunchInValue)

Thanks a lot, @m.c !

The custom-variable-instead-of-button technique (plus the lamp + label pattern) is really useful to know.

For these particular controls (Punch In / Pre-Roll / Post-Roll) I’m using them purely as one-shot “always ON” triggers, so I don’t read their state back. To drive a lamp with faithful feedback I’d have to poll the DirectAccess parameter values, and that’s exactly the kind of polling I’d rather avoid here if I can (I’m controlling the feedback of these controls with the legacy Generic Device).

So I’ll keep my current setup (the buttons bound to dummy host value variables) and simply place a descriptive label next to each button so it’s clear what each one does.

The “undefined value” text is only shown inside the button of the surface editor anyway, so it’s a minor cosmetic detail I’m happy to live with in exchange for keeping the code simple and polling-free.

Thanks again for taking the time to explain it!

If/when our script gets full of elements, it makes sense to poll in the mOnIdle event. Note that DA polling is extremely fast, no real overhead to the DAW. But yeah, whatever suits you best :slight_smile: