Rotary Encoder plugin very unreliable

Hear me out:
I have an IQaudio cosmic controller, this already has some hw debounce built in, short of a hex-schmitt inverter I think. With this the volume control was extremely skippy and unreliable in Volumio.
I then built my own HW debounce circuit with 10k ohm resistors, 0.1uf caps and a hex-schmitt inverter. With that it’s still pretty shit in Volumio by all means: a fast spin recognizes, one at most two steps, sometimes it jumps back a bit, etc. It’s only marginally better than before, and IMO unuseable.

I tested it with an arduino over serial, printing values from code without any SW debounce: the result is clear. Raw encoder is very bad, skips constantly. built-in circuit is very good, my own circuit has 0 skips (the code tracks skips/inversions in one direction). I don’t have an oscilloscope so can’t use that.

My question is: what is going on with this plugin? How can it perform so much worse than raw, dumb code on an arduino if the hardware is flawless? My volumio image is a few weeks old, and the rotary encoder plugin is the latest version as well.

We should see at how the rotary encoder plugin has been developed… In any case a Volumio limitation is on the number of consecutive volume inputs it can take in a short time (async code…). Maybe contact the plugin dev to see if we can give him a hint on how to improve his code?

I’ll try to get in touch. I think I need to try a simple python script to check the difference between the two ways to set volumio volume as well.

But the async code might explain some. I understand that’s due to network maccess, but shouldn’t there be a way for a plugin running hardware locally to set volume directly ?
Also, i’ve only briefly seen the volumio code to set volume: it looks like you can only pass commands to increase a single step? If you could pass multiple steps it would solve some of the issues with async code: you could just track 10 steps in one spin and pass them on as a single command.

edit: should have looked better, above is all answered here: volumio.github.io/docs/API/Comm … lient.html

Hi there,

If you search the forum and the corresponding github project I think you will find all your answers. If you can find a solution I’m sure we’re all more than happy to fix it and have it patched asap, there was some erratic behaviour, but that has been fixed some time ago.

You could indeed track steps and only update the volume once every x detents/seconds, this would influence how you can use the knob. It would also mean we would need to keep track of volume, effectively giving up its statelessness.

Have you tried enabling the debug logging in the plugin and matching the log lines with your hardware actions?

Hey sorry if I sound like a dick Saiyato. Just want to get this working well (for everyone).
First: i’m still a bit of a noob with Linux, so I have no idea how to view the log from your plugin. There is no mention of it in the readme and I can’t trace it down in your code all that quickly., so I haven’t done that yet.

I’ve coded my own python script in standard Raspbian to control ALSA audio levels, worked on it until it felt smooth, responsive and clear. It became obvious to me when writing this that you want quite a bit of customization depending on your hardware and personal prefs. I obviously did not do any SW debounce.
I mention this to point out I did gain insight in programming the encoder for responsiveness.
I am familiar with Python and C, but have never writen javascript like your plugin. I did look at your code and did see a few things to mirror with my own experience.

I was inspired by moOde audio exposing 3 values: update cycle length (100ms default), accel rate (if more than a set amount of steps are gained in a cycle) and an accelerated step/gain rate for the volume (increased over standard if accel condition is met). Result is that you can finetune in small steps when going slow, or you can whizz through in faster increments if you spin fast. It does still allow you to customize the responsiveness to a high degree by exposing these 3 parameters (ok their UI is pretty bad for it, but still i got it working flawless in a few seconds with the same hardware).

So comparing my successfull effort to emulate this, to your code:
You seem to be only tracking an single upward or a downward step, your tick then sets this in single increment steps (volume plus/minus). theoretically it should work, but as Michelangelo said, perhap the async code messes with this, Timings are all very important for the responsive feeling I’ve noticed.
What I do instead:
keep track of encoder absolute position increments: in python this is a callback with a very small bounce/repeat time, 20ms works in my case. It’s only called if the encoder changes position and adds or subtracts 1 depending on direction.
A separate loop runs at a longer interval, 100ms works well (ideally customizable), and checks the DELTA/ difference between the current encoder abolute position and the one from the previous tick. It then sets the volume in a batch: not by doing single increments, but getting the current level, and adding or subtracting the delta multiplied by the rate. By using delta only, we’ve still got the position independence. I guess it would only break if you manage to overflow the integer or something (extremely unlikely).
The default gain rate can be 1 or 2 (ideally customizable) if more than the defined acceleration steps were made in a loop (customizable parameter), the rate is higher, 5 worked well for me (customizable param again).

Here’s my code:

import RPi.GPIO as GPIO
from time import sleep
import os
try:
    import alsaaudio
except:
    #this probably won't work but at least here's the commands
    os.system("sudo apt-get install libasound2-dev")
    os.system("sudo -H pip3 install pyalsaaudio")
    import alsaaudio

class MediaControl:
    
    CLOCKWISE = 1
    ANTICLOCKWISE = 0
    
    def __init__(self, p_controlmode = "default"):
        self.m_alsamixer = alsaaudio.Mixer()
        self.rotary1Pin = 23
        self.rotary2Pin = 24
        self.switchPin = 10
        self.m_controlmode = p_controlmode
        self.m_currpos = 0
        self.m_prevpos = 0
        self.m_accel = 3
        self.m_slow = 2
        self.m_fast = 5
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.rotary1Pin, GPIO.IN)
        GPIO.setup(self.rotary2Pin, GPIO.IN)
        GPIO.setup(self.switchPin, GPIO.IN, pull_up_down = GPIO.PUD_UP)
        
    def start(self):
        GPIO.add_event_detect(self.rotary1Pin,
                              GPIO.FALLING,
                              callback=self._clockCallback,
                              bouncetime=50)
        GPIO.add_event_detect(self.switchPin,
                              GPIO.FALLING,
                              callback=self._switchCallback,
                              bouncetime=300)

    def stop(self):
        GPIO.remove_event_detect(self.rotary1Pin)
        GPIO.remove_event_detect(self.switchPin)
        GPIO.cleanup()
        print("GPIO released and cleaned")
    
    def ChangeVolume(self, p_value):
        l_currvol = int(self.m_alsamixer.getvolume()[0])
        l_vol = l_currvol + p_value
        if l_vol >= 100:
            #print("setting volume with " + str(p_value) + " to " + str(l_vol))
            self.m_alsamixer.setvolume(100)
        elif l_vol <= 0:
            self.m_alsamixer.setvolume(0)
        else:
            self.m_alsamixer.setvolume(l_vol)
    
    def _clockCallback(self, pin):
        data = GPIO.input(self.rotary2Pin)
        if data == 1:
            #ANTICLOCKWISE
            self.m_currpos -=1
        else:
            #CLOCKWISE
            self.m_currpos +=1
        
                
    
    def _switchCallback(self, pin):
        if GPIO.input(self.switchPin) == 0:
            print("switch pressed")
                     
    def Check(self):
        l_diff = self.m_prevpos - self.m_currpos
        l_step = self.m_slow
        if (abs(l_diff) >= self.m_accel):
            l_step = self.m_fast
        if l_diff != 0:
            self.ChangeVolume(l_diff*l_step)
        print(self.m_currpos) 
        self.m_prevpos = self.m_currpos
            
    
if __name__ == "__main__":

    
    MediaMan = MediaControl()

    MediaMan.start()

    try:
        while True:
            MediaMan.Check()
            sleep(0.1)
    finally:
        MediaMan.stop()
        

Hi,

I think we’re after the same goal here :wink: The plugin uses the central logging method, implemented in the core, which outputs to the journal. I.e. you can read the journal, or tail it if you want to live debug.

When you say you downloaded the latest version, did you use the one from the store (i.e. from the plugins collection) or the one from Github? Because only the latter contains stability fixes which could explain your behaviour/experience. See this PR, it has been reverted (21 days ago), but never accepted again. So the version in the store (June 2018) is a bit outdated. You can find the issue on the Github page here.

As for controlling ALSA directly, this is possible and might even improve the feel/responsiveness, however, if you want to WUI (web user interface) to match the volume set by the plugin you’d need to find a way to listen to ALSA changes (or fetch them, but that is more costly in terms of performance I guess).

Another thing to bear in mind is the path of information, as Volumio is running on NodeJS and MoOde (correct me if I’m wrong) runs on PHP. The choice of using NodeJS libraries comes from the desire to make the plugin platform independent, the idea is to handle everything as generic as possible with a few writes as possible (reduce wear-and-tear of SD cards). When using the NodeJS libraries I’m able to quickly add functionality and fully integrate into the Volumio ecosystem, no extra scripts or processes required, which makes it rather easy to maintain. Plus I’m no Python hero :wink:

So back to the essence, the idea to buffer interrupts appeals to me, however I would have to dive into the MoOde code to see if I can easily incorporate that practice into the current plugin.

Currently the plugin ticks each and every detent and it will send the corresponding (configured) command to the Volumio instance. As Michelangelo pointed out, async handlers might cause problems when flooded with commands. For example when volume +1 commands are handled against a previously (and at that point no longer valid) fetched current volume. As you said, timing is everything.

I’ve just checked the Volumio CLI, it’s indeed to possible to increment to a certain volume, this would indeed require the plugin to fetch the volume prior to incrementing it with the detents detected in a single tick.

Hi Saiyato, thanks for getting back to me.

I might not have the latest version then… By now I’ve gone pretty far and wrote my own python control scripts that also trigger my touchscreen to dim and undim (events from the rotary encoder reset its idletime), so I’m beyond being able to use your plugin. I would however like to see this fixed in a generic way for everybody with a rotary encoder.

As for moOde and php vs NodeJS being platform independent: I fully understand, makes sense. I’m merely illustrating the buffering idea in python here, i think moOde’s code is irrelevant even as the idea is fairly simple. My own encoder works absolutely 100% perfect with this acceleration code in place, even controlling Volumio through commandline (haven’t bothered with socket.io). Key again was to not flood Volumio with commands: the volume update loop runs every 100ms and this is sufficient for responsiveness. Heck, it even looks smoother with my encoder and code than using the touchscreen!

As for writing plugins, I’m kind of drawn to trying it myself, I can’t imagine NodeJS will be so hard, it’s just the whole structure and dependencies thing that puts me off. I got any generic media-key input working by monitoring kernel input events, which means any keyboard, mouse, remote that is registered as an input device (even bluetooth once you get it running) can control Volumio if it has media control keys. I’d imagine that would be very useful for a lot of people if it came as an (official) plugin.
Maybe we should talk about it, like how you get set up for remote dev with NodeJS and so.

We added media key control in Volumio since a couple of releases :wink:

Hmm, it didn’t work at all for me! I’ve been so wrapped up in scripting my own stuff I didn’t do updates for a while, i’m on 2.457. Updating now!
edit: nope, no deal. both of my keyboards, USB wired and bluetooth are not triggering any action through their media keys.