Bug in the 3.2.0.5 API?

How to use Voicemeeter Remote API and control Voicemeeter Audio Engine
Post Reply
stoepie
Posts: 38
Joined: Thu Aug 25, 2022 8:58 pm

Bug in the 3.2.0.5 API?

Post by stoepie »

Think I found a bug.... on the 3.0.2.5. API. Perhaps it was already there in earlier versions, I never tried so I don't know.

I made a console app that loads the DLL, logs in, sets a button state, waits for a split second, logs out, unloads the DLL, and exits.

When using the API to set the 'Value' on a 'Mode 2P' Macro Button using either VBVMR_MACROBUTTON_MODE_DEFAULT or VBVMR_MACROBUTTON_MODE_TRIGGER, setting a value (either 0.0f or 1.0f, doesn't matter) doesn't clear (0) or set (1), but instead it TOGGLES it.

This makes it difficult to SET or CLEAR the button. I would expect a value of 1 to set it and a value of 0 to clear it. Or is this an incorrect assumption?

When just setting STATEONLY, 1 does set and 0 does clear, but I need "Trigger In" and "Trigger OUT" to run, to set the Button.Color appropriately as I can't do this remotely via the API.

When using this on MODE PUSH buttons, I do see the effect I expect where a 1 sets and a 0 clears.

My usecase is where I want to get the initial button states correct, so from the 'request for initial state', I run my console app that fetches a remote state and tries to set the Macro button state accordingly.

As a workaround, I GET the state and if it is not what I want it to be, I toggle it.

When I manually launch my app that works 10 times out of 10 but when I have the "Request for initial state" run my app, my app runs but the setting often doesn't work.

So actually 2 bugs, one where the set and clear both toggle, and the other where setting (or clearing) during startup often fails.



Any ideas?

Cheers,
David
stoepie
Posts: 38
Joined: Thu Aug 25, 2022 8:58 pm

Re: Bug in the 3.2.0.5 API?

Post by stoepie »

I am using the Macrobuttons to switch on/off equipment using an IP POWER 9258.

Point is that at startup of voicemeetermacrobuttons.exe, I have the "Request For Initial State" run an app or script, that reads the IP POWER port state (on/off) and tries to set the Macro Button State accordingly. The IP Power port state read works just fine, but setting the Macro Button state doesn't.

To make things a bit easier (I think) I changed from using a console app, to using a Python script.

Request for initial state runs:

Code: Select all

System.Execute("%windir%\system32\cmd.exe","C:\Users\David\Documents\Python\", "/min /C py .\PSwitch.py 192.168.1.100 1 ?3");
This launches the script, which reads port state of output 1 and tries to set Macro button 3 if not already set to the appropriate value.

___

The ON trigger runs:

Code: Select all

Button.Color=8;
System.Execute("%windir%\system32\cmd.exe","C:\Users\David\Documents\Python\", "/min /C py .\PSwitch.py 192.168.1.100 1 1");
The OFF trigger runs:

Code: Select all

Button.Color=3;
System.Execute("%windir%\system32\cmd.exe","C:\Users\David\Documents\Python\", "/min /C py .\PSwitch.py 192.168.1.100 1 0");
Yes, when the initial state triggers that in turn may trigger the ON or OFF scripts, but in this case it merely switches the IP Power Port on or off, which it already was. So that doesn't 'hurt'. Also it sets the button.color. Since there is no '?' it doesn't try to do anything with the API.

I need to set the initial color, as when I don't, I may have a disabled/off button with the wrong color (as nothing is triggered since I can only TOGGLE when it's in the wrong state).

Point is, when run externally, all is well. When run from the 'Request For Initial State' it most often does not work. And when I run the script on two buttons initial state, it seems it craps up a lot of states and it almost looks like the DLL isn't re-entrant/thread safe. It even manages to change existing IP POWER port states since the values read from the buttons are incorrect, firing the wrong macros.

I will think about making ONE function that sets all my Macro Buttons, adding a semaphore via a file, to prevent firing triggers from changing stuff, but this does seem like a fix for something that is essentially broken.

What I do not want to make is a backround task or service to get the macro button states right, nor do I want to edit and reload XML's. I just want to use the API. And ideally I want all my 'code' for a button to handle that button only, not to have dependencies.

My python script PSwitch.py (with loads of comments to explain):

Code: Select all

#
# David R, October 16th 2022. Feel free to use the code if it helps you in any way. No warranties!
#

# To SET a port output of the IP POWER 9258 device, use:
#
# py .\Pswitch.py IP PORT VALUE
#
# IP is the dotted IP address of the power switch
#
# PORT is the port number on the power switch, 1 to 4
#
# VALUE is 0 for OFF and 1 for ON
#
# To QUERY the port status of the IP POWER 9258 device, and SET the corresponding macro button state accordingly, use:
#
# py .\Pswitch.py IP PORT ?N
#
# IP is the dotted IP address of the power switch
#
# PORT is the port number on the power switch, 1 to 4
#
# The '?' tells the script to GET the status of the port, and SET the state of MACRO BUTTON 'N' accordingly.
#

# NOTE! For the sake of short code, I did not provide ANY error checking, so things could go south....

import urllib3
import argparse
import ctypes
import os
from ctypes import *
from time import sleep

# Minimize command window asap as I don't want to have a window visible
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 6)

# Get the commandline params using argparse
parser = argparse.ArgumentParser()
parser.add_argument("IP", type=str, help="PowerSwitch IP")
parser.add_argument("PORT", type=str, help="PowerSwitch PORT")
parser.add_argument("FUNC", type=str, help="PowerSwitch FUNCTION")

args = parser.parse_args()
IP = args.IP
PORT = args.PORT
FUNC = args.FUNC

# Use urllib3 to open the powerswitch URL
http = urllib3.PoolManager()

# Used the default login credentials for the IP POWER device
if FUNC[0] != "?":  # We want to SET the IP POWER port status
    url = "http://" + IP + "/set.cmd?user=admin+pass=12345678+cmd=setpower+p6" + PORT + "=" + FUNC
else:  # We want to read the IP POWER port status
    url = "http://" + IP + "/set.cmd?user=admin+pass=12345678+cmd=getpower"

r = http.request("GET", url, timeout=1)

if FUNC[0] == "?":  # We want to read the IP POWER port status
    # Allow to run from "Request for inital state"
    sleep(int(PORT))

    N = int(FUNC[1])
    retvalue = str(r.data)
    # Find the index to our port from the returnvalue
    idx = int(retvalue.index("p6" + PORT))

    # Get the status from the appropriate port
    actual = c_float(int(retvalue[idx + 4 : idx + 5]))

    # Now, get the MACRO BUTTON state, using the API

    # First, load the API
    # I'm on 64 bit Windows... you may need to change this. Yes, ugly, I know)
    mydll = cdll.LoadLibrary("C:\\Program Files (x86)\\VB\\Voicemeeter\\VoicemeeterRemote64.dll")

    # Login on the API
    result = mydll.VBVMR_Login()

    # Make a ctype float variable to use in the API call
    value = c_float(0.0)

    # Get the value for N
    N = int(FUNC[1])

    # Since the button color change needs time to trickle through, we need to wait a bit here....
    sleep(1)

    # Query MACRO BUTTON N
    # use VBVMR_MACROBUTTON_MODE_DEFAULT = 0
    result = mydll.VBVMR_MacroButton_GetStatus(N, pointer(value), 0)

    # If the MACRO BUTTON is not the same as the actual state, 'press' the button via the API
    if value.value != actual.value:
        # It seems pushing the button toggles it, rather than being able to SET or CLEAR it.
        result = mydll.VBVMR_MacroButton_SetStatus(N, actual, 0)
        sleep(1)

    # This value needs to be set, too (so it seems) otherwise the highlight of the button is missing / wrong?
    result = mydll.VBVMR_MacroButton_SetStatus(N, actual, 2)

    # It seems that when I release the API too soon, it does not work (does not flush pending commands, rather... does nothing)
    # I should not have to monitor to see if my value sticks.
    sleep(1)

    # Logout from the API
    result = mydll.VBVMR_Logout()
    
I welcome ideas and feedback.

Cheers,
David

edited the python script, adding a sleep
Last edited by stoepie on Wed Oct 19, 2022 7:58 pm, edited 1 time in total.
stoepie
Posts: 38
Joined: Thu Aug 25, 2022 8:58 pm

Re: Bug in the 3.2.0.5 API?

Post by stoepie »

Found a workaround that at least gets me the correct initial state, when firing the script from the "Request for initial state" :

My script now waits 'PORT*1000ms' (PORT being the IP POWER port number) before accessing the API (added it in the script in the previous post). This sees to it that:
  • The Macrobuttons app/api have some more time to 'wake up'
  • The scripts do not run at the same time, but the one after the other
I do not now if it's the wake up or the async or both, but at least now it always gives the proper initial state.

The two issues:

1 toggle instead of set/clear with the API

and

2 incorrect API/Macrobuttons workings when run simultaneously from the Request for initial state

Remain, but I can now work around both.

-David
Vincent Burel
Site Admin
Posts: 2019
Joined: Sun Jan 17, 2010 12:01 pm

Re: Bug in the 3.2.0.5 API?

Post by Vincent Burel »

you are maybe not using the API in the right way... first because the Login/Logout process has not been done to make single request, secondly because you must call xxx_IsDirty function as explained in user manual page 7: https://github.com/vburel2018/Voicemeet ... oteAPI.pdf
stoepie
Posts: 38
Joined: Thu Aug 25, 2022 8:58 pm

Re: Bug in the 3.2.0.5 API?

Post by stoepie »

Thank you for the help.

I've added:

Code: Select all

    # Call IsDirty to sync things up
    ComError = mydll.VBVMR_MacroButton_IsDirty()
before setting data. It returns 1 ('True'?)

That seems to solve the issue where states are mixed up when running the script in parallel for two different buttons.

I still see the 'toggle' behavior, and I must still wait a little bit (not sure how much, 1 second seems to work) before logout, otherwise the setting doesn't work.

I've tried adding the query to IsDirty, after setting and before logout. It returns 0 (False?), but still I have to add some delay for it to work so that doesn't help.

You say the API was not made for single requests... how would you advise to read some external states into the button states at startup?
Last edited by stoepie on Thu Oct 20, 2022 10:00 am, edited 1 time in total.
stoepie
Posts: 38
Joined: Thu Aug 25, 2022 8:58 pm

Re: Bug in the 3.2.0.5 API?

Post by stoepie »

Instead of waiting for some time (which is by default always wrong) I changed it to polling method:

Code: Select all

    a = 1
    while value.value != actual.value:
    	sleep(0.1)
        print("polling button, attempt ", a)
        a = a + 1
        ComError = mydll.VBVMR_MacroButton_IsDirty()
        result = mydll.VBVMR_MacroButton_GetStatus(N, pointer(value), 0)
Which (without the sleep) counts up to values between 40 and 112 before it's done. Then at exit all is fine.

Edit: With the sleep(0.1) it tries once or twice and then exists.

Perhaps this is the best/most elegant way?

(Of course, adding back in some checks on returnvalues and possibly an escape if I reach a 1000 attempts or so.)
Last edited by stoepie on Thu Oct 20, 2022 10:18 am, edited 1 time in total.
Vincent Burel
Site Admin
Posts: 2019
Joined: Sun Jan 17, 2010 12:01 pm

Re: Bug in the 3.2.0.5 API?

Post by Vincent Burel »

ok, but I would suggest to make a Sleep(10) instead.
stoepie
Posts: 38
Joined: Thu Aug 25, 2022 8:58 pm

Re: Bug in the 3.2.0.5 API?

Post by stoepie »

I'd like to share the end result. Perhaps as 'howto API in Python' it deserves it's own topic, but I did not want to create too much additional noise :)

Many thanks to Vincent for pointing out that calling VBVMR_MacroButton_IsDirty() right after a login is mandatory. It's in the manual :oops:

Here is a recap (with some improvements compared to what I posted before, to what I run from the macro buttons as well):

The "Request for initial state"-field reads:

Code: Select all

Button.Color=3;
System.Execute("c:\windows\pyw.exe","C:\Users\David\Documents\Python\","/C PSwitch.py 192.168.1.100 1 ?3");
This ensures the button starts as a color 3 button. Next, I use pyw.exe to start a Python script in minimized mode. There is no flashing command window, it all runs minimized this way.

The script is in my user docs Python directory and is named PSwitch.py. The commandline parameters are explained in the script.

The "Request for button on"-field reads:

Code: Select all

Button.Color=8;
System.Execute("c:\windows\pyw.exe","C:\Users\David\Documents\Python\","/C PSwitch.py 192.168.1.100 1 1");
And in the "Request for button off"-field I have:

Code: Select all

Button.Color=3;
System.Execute("c:\windows\pyw.exe","C:\Users\David\Documents\Python\","/C PSwitch.py 192.168.1.100 1 0");
The Python script reads:

Code: Select all

#
# David R, October 16th~20th 2022. Feel free to use the code if it helps you in any way. No warranties!
#
# To SET a port output of the IP POWER 9258 device, use:
#
# pyw .\Pswitch.py IP PORT[1-4] VALUE
#
# Where VALUE is 0 for OFF and 1 for ON
#
# To QUERY the port status of the IP POWER 9258 device, and SET the corresponding macro button state accordingly, use:
#
# pyw .\Pswitch.py IP PORT[1-4] ?N
#
# where '?' tells the script to GET the status of the port, and SET the state of MACRO BUTTON N accordingly.
#

# NOTE! For the sake of short code, I did not provide ANY error checking, so things could go south....

import urllib3
import argparse

import ctypes
import os

from ctypes import *
from time import sleep

# Get the commandline params using argparse
parser = argparse.ArgumentParser()
parser.add_argument("IP", type=str, help="PowerSwitch IP")
parser.add_argument("PORT", type=str, help="PowerSwitch PORT")
parser.add_argument("FUNC", type=str, help="PowerSwitch FUNCTION")

args = parser.parse_args()
IP = args.IP
PORT = args.PORT
FUNC = args.FUNC

# Use urllib3 to open the powerswitch URL
http = urllib3.PoolManager()

# Used the default login credentials for the IP POWER device
if FUNC[0] != "?":  # We want to SET the IP POWER port status
    url = "http://" + IP + "/set.cmd?user=admin+pass=12345678+cmd=setpower+p6" + PORT + "=" + FUNC
else:  # We want to read the IP POWER port status
    url = "http://" + IP + "/set.cmd?user=admin+pass=12345678+cmd=getpower"

r = http.request("GET", url, timeout=1)

if FUNC[0] == "?":  # We want to read the IP POWER port status
    N = int(FUNC[1])
    retvalue = str(r.data)
    
    # Find the index to our port from the returnvalue
    idx = int(retvalue.index("p6" + PORT))
    
    # Get the status from the appropriate port
    actual = c_float(int(retvalue[idx + 4 : idx + 5]))

    # I'm on 64 bit Windows... you may need to change this. Yes, ugly, I know)
    mydll = cdll.LoadLibrary("C:\\Program Files (x86)\\VB\\Voicemeeter\\VoicemeeterRemote64.dll")

    # Login on the API
    result = mydll.VBVMR_Login()

    # Call IsDirty to sync things up -> THIS IS A MUST-DO step!
    result = mydll.VBVMR_MacroButton_IsDirty()

    # Make a ctype float variable to use in the API call
    value = c_float(0.0)

    # Get the value for N
    N = int(FUNC[1])

    # Query MACRO BUTTON N
    result = mydll.VBVMR_MacroButton_GetStatus(N, pointer(value), 0)

    # If the MACRO BUTTON is not the same as the actual state, 'press' the button via the API
    if value.value != actual.value:
        # It seems pushing the button toggles it, rather than being able to SET or CLEAR it.
        result = mydll.VBVMR_MacroButton_SetStatus(N, actual, 0)

        retries = 10
        # Check if the API reports back the desired value so we can exit
        while value.value != actual.value:
            sleep(0.1)
            retries = retries - 1
            # Refresh the state
            result = mydll.VBVMR_MacroButton_IsDirty()
            # Read the state
            result = mydll.VBVMR_MacroButton_GetStatus(N, pointer(value), 0)
            # If we tried 10 times... and we're still not there.. exit
            if retries == 0:
                break

    # Logout from the API
    result = mydll.VBVMR_Logout()
    
I'm still a bit puzzled regarding the toggle instead of the set, but hey, things work!

Hopefully this post will help or inspire others to work with the API, using Python.

Oh, and the reason that 'Store Last Button State' doesn't work for me is that I swap between PC's during the day, and if I change it using the one PC it would not be in sync on the other, so I always want to read the actual state.

-David
Post Reply