GPIO rotary volume
-
@subs said in GPIO rotary volume:
It's hard to tell. We just lack devices to test. Almost everything in recalbox is ready to handle it, except the gpio handling
Lacking devices to test, does that mean that you lack a rotary encoder to test? How much to the recalbox paypal would solve that? There's an issue withe the gpio handling? I thought recalbox could read gpio events already?
-
@dh04000 we don't have any rotary button such as yours we do handle gpio, hopefully lol
-
@subs said in GPIO rotary volume:
@dh04000 we don't have any rotary button such as yours we do handle gpio, hopefully lol
Post a link to the cheapest amazon listing for a compatible rotary encoder with free shipping in France (do you have Amazon PRIME?). I'll put the listing money in the Recalbox paypal after my next pay cycle.
-
It's not a question of price, but time (wire, code, test), finding something that fits the needs, being able to make a description of how the button works so that anyone can find a similar rortary button.
Let's take a shortcut : does it work like this
If so, that's as we would expect it and it is easy.
-
@subs said in GPIO rotary volume:
It's not a question of price, but time (wire, code, test), finding something that fits the needs, being able to make a description of how the button works so that anyone can find a similar rortary button.
Let's take a shortcut : does it work like this
If so, that's as we would expect it and it is easy.
I offered money because it is what I can offer Recalbox. I can't code, so the only things I can offer is time, and money. Time is me finding this and trying to talk about a feature that users (like me) might enjoy to have. I'd be willing to test it as well, tell me when and I'll purchase myself a rotary encoder. Money being to purchase the parts needed to allow you and others to attempt to implement it. If I could code, I would do it myself and contribute the code, but I can't.
Anyhoo, the github code matches an Adafruit rotary encoder, and from what I can gleam from the data sheet (https://cdn-shop.adafruit.com/datasheets/pec11.pdf), that image you posted matches the functionality, where the button is a simple on/off mechanism, and the left and right turns are a series of A or B connections to the common ground (middle post), depending on which direction is turned.
Seems to be what you expect.
-
Well @supernature2k can handle this, just leave him some time. But it should work one way or another if it's just a pair of switches
-
Hello @dh04000
look this script : https://gist.github.com/savetheclocktower/9b5f67c20f6c04e65ed88f2e594d43c1
may be you can create same for recalbox . -
NECRO'ing because there was an update on this rotary script by the author.
One of the author's said," the only things that need to be adjusted in this to make it python 2 compatible is change line #1 from: #!/usr/bin/env python3 to: #!/usr/bin/env python2, and change line #25 from from queue import Queue to from multiprocessing import Queue".
Ok, simple enough, ha!
Does recalbox run system.d and allow scripts to be started at start up?
Thanks.
-
-
@dh04000 I finally bought some rotary encoders, but had no time yet to work on that tbh
-
The next time the raspberry pi zero-w is available on adafruit, I'm buying and the rotary encoder suggested on the scripts githib page. I'll test it when I get it and report back.
-
Ordered my rotary encoder!
-
Rotary encoder received. Will try it out this weekend with the python script + author's modifications provided by the github page.
-
@dh04000 sadly i'll be off on holidays till 16th of April, so you may feel drown in a void if you're facing problems ...
-
@Substring I always feel like I'm drowning in the void. I'm a research scientist in real life. Poking holes in the darkness to reveal the light is what I do.
-
@dh04000 well hopefully the void shouldn't turn to a black hole neither, you may just lack a few changes required in the python scripts, but it should work.
One important thing i'm thinking of is the setting of the bounce time if the scripts are using the wiringPi library. The idea (wiringPi or not in fact) is to let a delay between 2 "clicks".
But I'm pretty confident, it should work.
-
I tried following the guide here (https://gist.github.com/savetheclocktower/9b5f67c20f6c04e65ed88f2e594d43c1#file-monitor-volume-L1), but recalbox's file system seems different to retropie's. Also, does recalbox have systemd? Here's my terminal output following the guide, so you see how I failed.
# mkdir ~/bin
# echo $PATH
/bin:/sbin:/usr/bin:/usr/sbin
-sh: /bin:/sbin:/usr/bin:/usr/sbin: No such file or directory
# nano ~/bin/monitor-volume
# chmod +x ~/bin/monitor-volume
# nano ~/monitor-volume.service
# monitor-volume
-sh: monitor-volume: command not found
-sh: -sh:: command not found
# nano ~/monitor-volume.service
# chmod +x ~/monitor-volume.service
mv ~/monitor-volume.service /etc/systemd/system
mv: can't rename '/recalbox/share/system/monitor-volume.service': No such file or directory
Maybe you can figure out whats wrong @Substring ? I'm sure its trivial.
-
Hello @dh04000
substring is in holidays.`But If I will had rotary volume , I try this idea.
you need to mount rewrite partition before :
mount -o remount,rw /
https://github.com/recalbox/recalbox-os/wiki/partition-en-ecriture-(FR)but you need to put python script
volume-monitor.py
orvolume-monitor
? in/recalbox/scripts
#!/usr/bin/env python3 """ The daemon responsible for changing the volume in response to a turn or press of the volume knob. The volume knob is a rotary encoder. It turns infinitely in either direction. Turning it to the right will increase the volume; turning it to the left will decrease the volume. The knob can also be pressed like a button in order to turn muting on or off. The knob uses two GPIO pins and we need some extra logic to decode it. The button we can just treat like an ordinary button. Rather than poll constantly, we use threads and interrupts to listen on all three pins in one script. """ import os import signal import subprocess import sys import threading from RPi import GPIO from queue import Queue DEBUG = False # SETTINGS # ======== # The two pins that the encoder uses (BCM numbering). GPIO_A = 26 GPIO_B = 19 # The pin that the knob's button is hooked up to. If you have no button, set # this to None. GPIO_BUTTON = 13 # The minimum and maximum volumes, as percentages. # # The default max is less than 100 to prevent distortion. The default min is # greater than zero because if your system is like mine, sound gets # completely inaudible _long_ before 0%. If you've got a hardware amp or # serious speakers or something, your results will vary. VOLUME_MIN = 60 VOLUME_MAX = 96 # The amount you want one click of the knob to increase or decrease the # volume. I don't think that non-integer values work here, but you're welcome # to try. VOLUME_INCREMENT = 1 # (END SETTINGS) # # When the knob is turned, the callback happens in a separate thread. If # those turn callbacks fire erratically or out of order, we'll get confused # about which direction the knob is being turned, so we'll use a queue to # enforce FIFO. The callback will push onto a queue, and all the actual # volume-changing will happen in the main thread. QUEUE = Queue() # When we put something in the queue, we'll use an event to signal to the # main thread that there's something in there. Then the main thread will # process the queue and reset the event. If the knob is turned very quickly, # this event loop will fall behind, but that's OK because it consumes the # queue completely each time through the loop, so it's guaranteed to catch up. EVENT = threading.Event() def debug(str): if not DEBUG: return print(str) class RotaryEncoder: """ A class to decode mechanical rotary encoder pulses. Ported to RPi.GPIO from the pigpio sample here: http://abyz.co.uk/rpi/pigpio/examples.html """ def __init__(self, gpioA, gpioB, callback=None, buttonPin=None, buttonCallback=None): """ Instantiate the class. Takes three arguments: the two pin numbers to which the rotary encoder is connected, plus a callback to run when the switch is turned. The callback receives one argument: a `delta` that will be either 1 or -1. One of them means that the dial is being turned to the right; the other means that the dial is being turned to the left. I'll be d**ned if I know yet which one is which. """ self.lastGpio = None self.gpioA = gpioA self.gpioB = gpioB self.callback = callback self.gpioButton = buttonPin self.buttonCallback = buttonCallback self.levA = 0 self.levB = 0 GPIO.setmode(GPIO.BCM) GPIO.setup(self.gpioA, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(self.gpioB, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.add_event_detect(self.gpioA, GPIO.BOTH, self._callback) GPIO.add_event_detect(self.gpioB, GPIO.BOTH, self._callback) if self.gpioButton: GPIO.setup(self.gpioButton, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.add_event_detect(self.gpioButton, GPIO.FALLING, self._buttonCallback, bouncetime=500) def destroy(self): GPIO.remove_event_detect(self.gpioA) GPIO.remove_event_detect(self.gpioB) GPIO.cleanup() def _buttonCallback(self, channel): self.buttonCallback(GPIO.input(channel)) def _callback(self, channel): level = GPIO.input(channel) if channel == self.gpioA: self.levA = level else: self.levB = level # Debounce. if channel == self.lastGpio: return # When both inputs are at 1, we'll fire a callback. If A was the most # recent pin set high, it'll be forward, and if B was the most recent pin # set high, it'll be reverse. self.lastGpio = channel if channel == self.gpioA and level == 1: if self.levB == 1: self.callback(1) elif channel == self.gpioB and level == 1: if self.levA == 1: self.callback(-1) class VolumeError(Exception): pass class Volume: """ A wrapper API for interacting with the volume settings on the RPi. """ MIN = VOLUME_MIN MAX = VOLUME_MAX INCREMENT = VOLUME_INCREMENT def __init__(self): # Set an initial value for last_volume in case we're muted when we start. self.last_volume = self.MIN self._sync() def up(self): """ Increases the volume by one increment. """ return self.change(self.INCREMENT) def down(self): """ Decreases the volume by one increment. """ return self.change(-self.INCREMENT) def change(self, delta): v = self.volume + delta v = self._constrain(v) return self.set_volume(v) def set_volume(self, v): """ Sets volume to a specific value. """ self.volume = self._constrain(v) output = self.amixer("set 'PCM' unmute {}%".format(v)) self._sync(output) return self.volume def toggle(self): """ Toggles muting between on and off. """ if self.is_muted: output = self.amixer("set 'PCM' unmute") else: # We're about to mute ourselves, so we should remember the last volume # value we had because we'll want to restore it later. self.last_volume = self.volume output = self.amixer("set 'PCM' mute") self._sync(output) if not self.is_muted: # If we just unmuted ourselves, we should restore whatever volume we # had previously. self.set_volume(self.last_volume) return self.is_muted def status(self): if self.is_muted: return "{}% (muted)".format(self.volume) return "{}%".format(self.volume) # Read the output of `amixer` to get the system volume and mute state. # # This is designed not to do much work because it'll get called with every # click of the knob in either direction, which is why we're doing simple # string scanning and not regular expressions. def _sync(self, output=None): if output is None: output = self.amixer("get 'PCM'") lines = output.readlines() if DEBUG: strings = [line.decode('utf8') for line in lines] debug("OUTPUT:") debug("".join(strings)) last = lines[-1].decode('utf-8') # The last line of output will have two values in square brackets. The # first will be the volume (e.g., "[95%]") and the second will be the # mute state ("[off]" or "[on]"). i1 = last.rindex('[') + 1 i2 = last.rindex(']') self.is_muted = last[i1:i2] == 'off' i1 = last.index('[') + 1 i2 = last.index('%') # In between these two will be the percentage value. pct = last[i1:i2] self.volume = int(pct) # Ensures the volume value is between our minimum and maximum. def _constrain(self, v): if v < self.MIN: return self.MIN if v > self.MAX: return self.MAX return v def amixer(self, cmd): p = subprocess.Popen("amixer {}".format(cmd), shell=True, stdout=subprocess.PIPE) code = p.wait() if code != 0: raise VolumeError("Unknown error") sys.exit(0) return p.stdout if __name__ == "__main__": gpioA = GPIO_A gpioB = GPIO_B gpioButton = GPIO_BUTTON v = Volume() def on_press(value): v.toggle() print("Toggled mute to: {}".format(v.is_muted)) EVENT.set() # This callback runs in the background thread. All it does is put turn # events into a queue and flag the main thread to process them. The # queueing ensures that we won't miss anything if the knob is turned # extremely quickly. def on_turn(delta): QUEUE.put(delta) EVENT.set() def consume_queue(): while not QUEUE.empty(): delta = QUEUE.get() handle_delta(delta) def handle_delta(delta): if v.is_muted: debug("Unmuting") v.toggle() if delta == 1: vol = v.up() else: vol = v.down() print("Set volume to: {}".format(vol)) def on_exit(a, b): print("Exiting...") encoder.destroy() sys.exit(0) debug("Volume knob using pins {} and {}".format(gpioA, gpioB)) if gpioButton != None: debug("Volume button using pin {}".format(gpioButton)) debug("Initial volume: {}".format(v.volume)) encoder = RotaryEncoder(GPIO_A, GPIO_B, callback=on_turn, buttonPin=GPIO_BUTTON, buttonCallback=on_press) signal.signal(signal.SIGINT, on_exit) while True: # This is the best way I could come up with to ensure that this script # runs indefinitely without wasting CPU by polling. The main thread will # block quietly while waiting for the event to get flagged. When the knob # is turned we're able to respond immediately, but when it's not being # turned we're not looping at all. # # The 1200-second (20 minute) timeout is a hack; for some reason, if I # don't specify a timeout, I'm unable to get the SIGINT handler above to # work properly. But if there is a timeout set, even if it's a very long # timeout, then Ctrl-C works as intended. No idea why. EVENT.wait(1200) consume_queue() EVENT.clear()
chmod +x /recalbox/scripts/volume-monitor
ou
chmod +x /recalbox/scripts/volume-monitor.py
and you need to create
S91MonitorVolume
(as service monitor-volume.service) in/etc/init.d/S91MonitorVolume
chmod +x /etc/init.d/S91MonitorVolume
ExecStart=/recalbox/scripts/monitor-volume
orExecStart=/recalbox/scripts/monitor-volume.py
?but I think you need to create service script like another scripts /etc/init.d/, but i dont know it's working
[Unit] Description=Volume knob monitor [Service] User=root Group=root ExecStart=/recalbox/scripts/monitor-volume [Install] WantedBy=multi-user.target
-
Ok, I can report that the script runs and works perfectly! I made the two modifications as the original author suggested to switch line 1 to "!/usr/bin/env python2" and to switch line 25 to "from multiprocessing import Queue".
But I have to start the script manually, "./volume-monitor.py" since it doesn't startup with the system. So, IF that was fixed then this would be working perfectly!
-
@dh04000 hi
The easy way to start it is totouch ~/custom.sh && chmod u+x ~/custom.sh
and then edit ~/custom.sh to start your python