Saiyato / volumio-rotary-encoder-plugin

Simple dual rotary encoder plugin for Volumio 2.x
MIT License
19 stars 6 forks source link

Could not get it to work, resolved by using external program #12

Open Pastinakel opened 6 years ago

Pastinakel commented 6 years ago

Hi, as said before by others, it is very hard to get it to work. So I used a different approach (also used by others): a python script that catches the rotary events and changes the volume in Volumio. Problem here is that Volumio is not reponding fast enough to process all movement. This script keeps adjusting the volume until it matches the one set by the rotary.

Maybe it helps others.

#!/usr/bin/python3.4

import RPi.GPIO as GPIO
import time
import subprocess
from socketIO_client import SocketIO, LoggingNamespace

GPIO.setmode(GPIO.BCM)
socketIO = SocketIO('localhost', 3000)

rotA = 24
rotB = 23
buttons = [17, 27]

status = 'pause'
service = ''
volatile = True
actVolume = 50          # actual volume
pushVolume = 50         # to be volume
volumePushed = False    # new volume pushed
volumeRot = False       # new volume set by rotary

lastRot = None
levA = 0
levB = 0

def button_pressed(channel):
  global status
  if channel == 17:
    print ('next')
    socketIO.emit('next')
  elif channel == 27:
    print ('play/pause/stop')
    print('state', status)
    if status == 'play':
      if service == 'mpd':
        socketIO.emit('pause')
      else:
        socketIO.emit('stop')
    else:
      socketIO.emit('play')
  else:
    print ("unknown button", channel)

def rotary_turned(channel):
  global rotA, rotB, lastRot, levA, levB, pushVolume, volumePushed, volumeRot

  level = GPIO.input(channel)

  if channel == rotA:
    if levA == level: return # no change in level -> skip. Debounce on reversing direction
    levA = level
  else:
    if levB == level: return # no change in level -> skip. Debounce on reversing direction
    levB = level

#  print(channel, lastRot ,GPIO.input(rotA),GPIO.input(rotB),levA,levB)

  if channel == lastRot:
    return

  lastRot = channel
  oldVolume = pushVolume

  if channel == rotA and level == 1:
    if levB == 1:
      pushVolume += 1
  elif channel == rotB and level == 1:
    if levA == 1:
      pushVolume -= 1
  if pushVolume > 100: pushVolume = 100
  if pushVolume < 0: pushVolume = 0

  if pushVolume == oldVolume:
    return

  volumeRot = True

  if volumePushed:
    volumePushed = False
    print(channel, levA, levB, 'pushVolume:', pushVolume)
    equalize_volume()

def setup_channel(channel):
  try:
    print ("register %d" %channel)
    GPIO.setup(channel, GPIO.IN, GPIO.PUD_UP)
    GPIO.add_event_detect(channel, GPIO.FALLING, callback = button_pressed, bouncetime = 400)
    print ('success')
  except (ValueError, RuntimeError) as e:
    print ('ERROR:', e)

def setup_rotary(channel):
  try:
    print ("register %d" %channel)
    GPIO.setup(channel, GPIO.IN, GPIO.PUD_UP)
    GPIO.add_event_detect(channel, GPIO.BOTH, callback = rotary_turned)
    print ('success')
  except (ValueError, RuntimeError) as e:
    print ('ERROR:', e)

def equalize_volume():
  global rotA, rotB, lastRot, levA, levB, pushVolume, actVolume, volumePushed, volumeRot

#  print('Equalize_volume Start','act:',actVolume,'push:',pushVolume)

  if actVolume != pushVolume:
    if volumeRot:
      socketIO.emit('volume',pushVolume)
    else:
      pushVolume = actVolume
  else:
    volumePushed = True
    volumeRot = False

  print('Equalize_volume Ended','act:',actVolume,'push:',pushVolume)

def on_push_state(*args):
#  print('state', args)
  global status, service, volatile, actVolume
#  status = args[0]['status'].encode('ascii', 'ignore')
  status = str(args[0]['status'])
  if ('volatile' in args[0]):
    volatile = str(args[0]['volatile']) == "True"
  else: volatile = False
  if ('service' in args[0]) and args[0]['service'] != '':
    service = str(args[0]['service'])
  else:
    service = ''
  if ('volume' in args[0]):
    try:
      actVolume = int(args[0]['volume'])
    except:
      actVolume = 0
  else:
    volume = 0
  if ('stream' in args[0]):
    print ('stream',str(args[0]['stream']))

  print ('Push:','Status:', status, 'Service:', service, 'Volatile:', volatile, 'Volume:', actVolume)

  equalize_volume()

for x in buttons:
  setup_channel(x)

for x in [rotA,rotB]:
  setup_rotary(x)

socketIO.on('pushState', on_push_state)

# get initial state
socketIO.emit('getState', '', on_push_state)
pushVolume = actVolume
volumePushed = True

try:
  socketIO.wait()
except KeyboardInterrupt:
  pass

GPIO.cleanup()
Saiyato commented 6 years ago

I've not read the script entirely and Python is definitely not my strong suit. But it looks like you are saving up the changes and "commit" then on the button push.

I'm now exploring whether or not ALSA can be called directly, this would make it more snappy. The only thing needed is to sync it to the Volumio UI. I'm guessing that's where the delay is at.

HW -> node JS -> UI

If you enable logging you will see that there is a snappy response from node JS when turning the knob, but calling Volumio (over the command line or socket) causes delays in the system.

The most sensible approach would be to have some visualisation (LCD) and buffer the changes before sending them to Volumio. My suggestion would be to update ALSA and have Volumio poll ALSA volume levels or subscribe to changes.

Note: this only fixes volume, there are other functions for the encoder ;)

Pastinakel commented 6 years ago

Hi Saiyato,

Thanks for your reply.

The problem with the ALSA approach is that changes in ALSA mixer are not picked up by Volumio. Eg: If the volume is 80 and I change it to 50 in ALSA, it is still 80 in Volumio. Next time I decrease the volume by 10, it will be 70 if I do it in Volumio, and it will be 40 if I do it in Alsa. Quite a difference.

Direct control over ALSA mixer: https://github.com/iqaudio/tools/blob/master/IQ_rot.c

Pastinakel commented 6 years ago

[...] HW -> node JS -> UI

If you enable logging you will see that there is a snappy response from node JS when turning the knob, but calling Volumio (over the command line or socket) causes delays in the system. [...] Maybe I had a slow Raspberry Pi 2 but in my experience there can be a delay in node JS. Big enough to miss most of the rotary movement.

Saiyato commented 6 years ago

Yeah, thats why I want to propagate the new volume to Volumio asynchronous, this will give you snappy response in terms of volume control, the GUI just lags behind a bit.

When I was working on the solution I did get good results from the log on my Pi3, not sure if I tried it on the Pi2 as well... I had to count detents carefully, but I didn't seem to miss any.

The direct control uses the same logic as I do for detent and direction detection, so that's a relieve for me :) Since I didn't experience misfire I was looking for node JS libraries to control alsa directly and subscribe to ALSA events. But that's mostly because I get javascript, Python however... not so much ;)