Documentation
Rechercher…
Codeur rotatif via GPIO (Contrôle du volume numérique)
(Ce bref guide et ce script python est adapté du projet de Savetheclocktower [anglais] trouvé sur GitHub).

But

Ce script est destiné à ceux qui veulent un bouton de volume physique sur un projet Recalbox, comme les machines d'arcade. Ce script est utile pour les bornes d'arcade avec des haut-parleurs qui n'ont pas leur propre bouton de volume physique, ou qui auraient des difficultés à déplacer le bouton de contrôle du haut-parleur près de l'utilisateur.
Ce script utilise un codeur rotatif standard à 5 broches et a été testé sur le codeur d'Adafruit. Cinq fils sont nécessaires pour ce codeur rotatif : trois pour la partie bouton (A, B et terre), et deux pour la partie touche (commun et terre). Voici une référence pour les broches GPIO de Raspberry Pi.
Description
BCM #
Board #
bouton A
GPIO 26
37
bouton B
GPIO 19
35
bouton de la terre
pin de la terre en-dessous du GPIO 26
39
touche commun
GPIO 13
33
touche de la terre
pin de la terre opposé au GPIO 13
34
Vous pouvez utiliser les broches que vous voulez ; il suffit de mettre à jour le script volume-monitor.sh si vous les modifiez. Si vous n'avez pas de bouton poussoir dans votre codeur rotatif, laissez les broches inoccupées. N'importe quelle broche de terre peut être utilisée, ces broches sont juste suggérées à cause de leur proximité.

Volume daemon

Le script ci-dessous fonctionne comme suit : il utilise les broches spécifiées, et lorsque le bouton est tourné d'une manière ou d'une autre, il utilise les états des broches A et B pour savoir si le bouton a été tourné vers la gauche ou vers la droite. Ainsi, il sait s'il faut augmenter ou diminuer le volume du système en réponse, ce qu'il fait avec le programme de ligne de commande Amixer.
    1.
    Pour installer le script dans votre Recalbox : vous devez d'abord vous connecter à votre Recalbox via ssh.
    2.
    Remonter la partition en lecture-écriture : mount -o remount, rw /
    3.
    Créez/éditez votre script volume-monitor.py dans /recalbox/scripts via nano : nano /recalbox/scripts/volume-monitor.py
    4.
    Copiez et collez le script du bas dans le fichier, puis enregistrez le fichier par la commande Ctrl+X :
    1
    #!/usr/bin/env python2
    2
    3
    """
    4
    The daemon responsible for changing the volume in response to a turn or press
    5
    of the volume knob.
    6
    The volume knob is a rotary encoder. It turns infinitely in either direction.
    7
    Turning it to the right will increase the volume; turning it to the left will
    8
    decrease the volume. The knob can also be pressed like a button in order to
    9
    turn muting on or off.
    10
    The knob uses two GPIO pins and we need some extra logic to decode it. The
    11
    button we can just treat like an ordinary button. Rather than poll
    12
    constantly, we use threads and interrupts to listen on all three pins in one
    13
    script.
    14
    """
    15
    16
    import os
    17
    import signal
    18
    import subprocess
    19
    import sys
    20
    import threading
    21
    22
    from RPi import GPIO
    23
    from multiprocessing import Queue
    24
    25
    DEBUG = False
    26
    27
    # SETTINGS
    28
    # ========
    29
    30
    # The two pins that the encoder uses (BCM numbering).
    31
    GPIO_A = 26
    32
    GPIO_B = 19
    33
    34
    # The pin that the knob's button is hooked up to. If you have no button, set
    35
    # this to None.
    36
    GPIO_BUTTON = 13
    37
    38
    # The minimum and maximum volumes, as percentages.
    39
    #
    40
    # The default max is less than 100 to prevent distortion. The default min is
    41
    # greater than zero because if your system is like mine, sound gets
    42
    # completely inaudible _long_ before 0%. If you've got a hardware amp or
    43
    # serious speakers or something, your results will vary.
    44
    VOLUME_MIN = 60
    45
    VOLUME_MAX = 96
    46
    47
    # The amount you want one click of the knob to increase or decrease the
    48
    # volume. I don't think that non-integer values work here, but you're welcome
    49
    # to try.
    50
    VOLUME_INCREMENT = 1
    51
    52
    # (END SETTINGS)
    53
    #
    54
    55
    56
    # When the knob is turned, the callback happens in a separate thread. If
    57
    # those turn callbacks fire erratically or out of order, we'll get confused
    58
    # about which direction the knob is being turned, so we'll use a queue to
    59
    # enforce FIFO. The callback will push onto a queue, and all the actual
    60
    # volume-changing will happen in the main thread.
    61
    QUEUE = Queue()
    62
    63
    # When we put something in the queue, we'll use an event to signal to the
    64
    # main thread that there's something in there. Then the main thread will
    65
    # process the queue and reset the event. If the knob is turned very quickly,
    66
    # this event loop will fall behind, but that's OK because it consumes the
    67
    # queue completely each time through the loop, so it's guaranteed to catch up.
    68
    EVENT = threading.Event()
    69
    70
    def debug(str):
    71
    if not DEBUG:
    72
    return
    73
    print(str)
    74
    75
    class RotaryEncoder:
    76
    """
    77
    A class to decode mechanical rotary encoder pulses.
    78
    Ported to RPi.GPIO from the pigpio sample here:
    79
    http://abyz.co.uk/rpi/pigpio/examples.html
    80
    """
    81
    82
    def __init__(self, gpioA, gpioB, callback=None, buttonPin=None, buttonCallback=None):
    83
    """
    84
    Instantiate the class. Takes three arguments: the two pin numbers to
    85
    which the rotary encoder is connected, plus a callback to run when the
    86
    switch is turned.
    87
    88
    The callback receives one argument: a `delta` that will be either 1 or -1.
    89
    One of them means that the dial is being turned to the right; the other
    90
    means that the dial is being turned to the left. I'll be damned if I know
    91
    yet which one is which.
    92
    """
    93
    94
    self.lastGpio = None
    95
    self.gpioA = gpioA
    96
    self.gpioB = gpioB
    97
    self.callback = callback
    98
    99
    self.gpioButton = buttonPin
    100
    self.buttonCallback = buttonCallback
    101
    102
    self.levA = 0
    103
    self.levB = 0
    104
    105
    GPIO.setmode(GPIO.BCM)
    106
    GPIO.setup(self.gpioA, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    107
    GPIO.setup(self.gpioB, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    108
    109
    GPIO.add_event_detect(self.gpioA, GPIO.BOTH, self._callback)
    110
    GPIO.add_event_detect(self.gpioB, GPIO.BOTH, self._callback)
    111
    112
    if self.gpioButton:
    113
    GPIO.setup(self.gpioButton, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    114
    GPIO.add_event_detect(self.gpioButton, GPIO.FALLING, self._buttonCallback, bouncetime=500)
    115
    116
    117
    def destroy(self):
    118
    GPIO.remove_event_detect(self.gpioA)
    119
    GPIO.remove_event_detect(self.gpioB)
    120
    GPIO.cleanup()
    121
    122
    def _buttonCallback(self, channel):
    123
    self.buttonCallback(GPIO.input(channel))
    124
    125
    def _callback(self, channel):
    126
    level = GPIO.input(channel)
    127
    if channel == self.gpioA:
    128
    self.levA = level
    129
    else:
    130
    self.levB = level
    131
    132
    # Debounce.
    133
    if channel == self.lastGpio:
    134
    return
    135
    136
    # When both inputs are at 1, we'll fire a callback. If A was the most
    137
    # recent pin set high, it'll be forward, and if B was the most recent pin
    138
    # set high, it'll be reverse.
    139
    self.lastGpio = channel
    140
    if channel == self.gpioA and level == 1:
    141
    if self.levB == 1:
    142
    self.callback(1)
    143
    elif channel == self.gpioB and level == 1:
    144
    if self.levA == 1:
    145
    self.callback(-1)
    146
    147
    class VolumeError(Exception):
    148
    pass
    149
    150
    class Volume:
    151
    """
    152
    A wrapper API for interacting with the volume settings on the RPi.
    153
    """
    154
    MIN = VOLUME_MIN
    155
    MAX = VOLUME_MAX
    156
    INCREMENT = VOLUME_INCREMENT
    157
    158
    def __init__(self):
    159
    # Set an initial value for last_volume in case we're muted when we start.
    160
    self.last_volume = self.MIN
    161
    self._sync()
    162
    163
    def up(self):
    164
    """
    165
    Increases the volume by one increment.
    166
    """
    167
    return self.change(self.INCREMENT)
    168
    169
    def down(self):
    170
    """
    171
    Decreases the volume by one increment.
    172
    """
    173
    return self.change(-self.INCREMENT)
    174
    175
    def change(self, delta):
    176
    v = self.volume + delta
    177
    v = self._constrain(v)
    178
    return self.set_volume(v)
    179
    180
    def set_volume(self, v):
    181
    """
    182
    Sets volume to a specific value.
    183
    """
    184
    self.volume = self._constrain(v)
    185
    output = self.amixer("set 'PCM' unmute {}%".format(v))
    186
    self._sync(output)
    187
    return self.volume
    188
    189
    def toggle(self):
    190
    """
    191
    Toggles muting between on and off.
    192
    """
    193
    if self.is_muted:
    194
    output = self.amixer("set 'PCM' unmute")
    195
    else:
    196
    # We're about to mute ourselves, so we should remember the last volume
    197
    # value we had because we'll want to restore it later.
    198
    self.last_volume = self.volume
    199
    output = self.amixer("set 'PCM' mute")
    200
    201
    self._sync(output)
    202
    if not self.is_muted:
    203
    # If we just unmuted ourselves, we should restore whatever volume we
    204
    # had previously.
    205
    self.set_volume(self.last_volume)
    206
    return self.is_muted
    207
    208
    def status(self):
    209
    if self.is_muted:
    210
    return "{}% (muted)".format(self.volume)
    211
    return "{}%".format(self.volume)
    212
    213
    # Read the output of `amixer` to get the system volume and mute state.
    214
    #
    215
    # This is designed not to do much work because it'll get called with every
    216
    # click of the knob in either direction, which is why we're doing simple
    217
    # string scanning and not regular expressions.
    218
    def _sync(self, output=None):
    219
    if output is None:
    220
    output = self.amixer("get 'PCM'")
    221
    222
    lines = output.readlines()
    223
    if DEBUG:
    224
    strings = [line.decode('utf8') for line in lines]
    225
    debug("OUTPUT:")
    226
    debug("".join(strings))
    227
    last = lines[-1].decode('utf-8')
    228
    229
    # The last line of output will have two values in square brackets. The
    230
    # first will be the volume (e.g., "[95%]") and the second will be the
    231
    # mute state ("[off]" or "[on]").
    232
    i1 = last.rindex('[') + 1
    233
    i2 = last.rindex(']')
    234
    235
    self.is_muted = last[i1:i2] == 'off'
    236
    237
    i1 = last.index('[') + 1
    238
    i2 = last.index('%')
    239
    # In between these two will be the percentage value.
    240
    pct = last[i1:i2]
    241
    242
    self.volume = int(pct)
    243
    244
    # Ensures the volume value is between our minimum and maximum.
    245
    def _constrain(self, v):
    246
    if v < self.MIN:
    247
    return self.MIN
    248
    if v > self.MAX:
    249
    return self.MAX
    250
    return v
    251
    252
    def amixer(self, cmd):
    253
    p = subprocess.Popen("amixer {}".format(cmd), shell=True, stdout=subprocess.PIPE)
    254
    code = p.wait()
    255
    if code != 0:
    256
    raise VolumeError("Unknown error")
    257
    sys.exit(0)
    258
    259
    return p.stdout
    260
    261
    262
    if __name__ == "__main__":
    263
    264
    gpioA = GPIO_A
    265
    gpioB = GPIO_B
    266
    gpioButton = GPIO_BUTTON
    267
    268
    v = Volume()
    269
    270
    def on_press(value):
    271
    v.toggle()
    272
    print("Toggled mute to: {}".format(v.is_muted))
    273
    EVENT.set()
    274
    275
    # This callback runs in the background thread. All it does is put turn
    276
    # events into a queue and flag the main thread to process them. The
    277
    # queueing ensures that we won't miss anything if the knob is turned
    278
    # extremely quickly.
    279
    def on_turn(delta):
    280
    QUEUE.put(delta)
    281
    EVENT.set()
    282
    283
    def consume_queue():
    284
    while not QUEUE.empty():
    285
    delta = QUEUE.get()
    286
    handle_delta(delta)
    287
    288
    def handle_delta(delta):
    289
    if v.is_muted:
    290
    debug("Unmuting")
    291
    v.toggle()
    292
    if delta == 1:
    293
    vol = v.up()
    294
    else:
    295
    vol = v.down()
    296
    print("Set volume to: {}".format(vol))
    297
    298
    def on_exit(a, b):
    299
    print("Exiting...")
    300
    encoder.destroy()
    301
    sys.exit(0)
    302
    303
    debug("Volume knob using pins {} and {}".format(gpioA, gpioB))
    304
    305
    if gpioButton != None:
    306
    debug("Volume button using pin {}".format(gpioButton))
    307
    308
    debug("Initial volume: {}".format(v.volume))
    309
    310
    encoder = RotaryEncoder(GPIO_A, GPIO_B, callback=on_turn, buttonPin=GPIO_BUTTON, buttonCallback=on_press)
    311
    signal.signal(signal.SIGINT, on_exit)
    312
    313
    while True:
    314
    # This is the best way I could come up with to ensure that this script
    315
    # runs indefinitely without wasting CPU by polling. The main thread will
    316
    # block quietly while waiting for the event to get flagged. When the knob
    317
    # is turned we're able to respond immediately, but when it's not being
    318
    # turned we're not looping at all.
    319
    #
    320
    # The 1200-second (20 minute) timeout is a hack; for some reason, if I
    321
    # don't specify a timeout, I'm unable to get the SIGINT handler above to
    322
    # work properly. But if there is a timeout set, even if it's a very long
    323
    # timeout, then Ctrl-C works as intended. No idea why.
    324
    EVENT.wait(1200)
    325
    consume_queue()
    326
    EVENT.clear()
    Copied!
    5.
    Marquer le script comme un fichier exécutable : chmod +x /recalbox/scripts/volume-monitor.py
    6.
    Pour que le script de contrôle du volume démarre avec votre système, procédez comme suit :touch ~/custom.sh && chmod u+x ~/custom.sh
    7.
    Ouvrez le script custom.sh dans l'éditeur nano :nano ~/custom.sh
    8.
    Enfin, copiez et collez ce qui suit dans le fichier custom.sh et enregistrez avec Ctrl+X :python /recalbox/scripts/volume-monitor.py
    9.
    Redémarrez votre Recalbox en utilisant la commande de redémarrage : reboot
    10.
    Profitez du nouveau contrôle de volume de votre matériel.

Modifications du script

Ces modifications se trouvent toutes sous la rubrique "Settings" du script Volume-monitor.py
ASTUCE ! Veuillez patienter 3 secondes après l'apparition du menu d'Emulationstation AVANT de toucher le codeur rotatif. Tourner le codeur plus tôt peut bloquer le script et rendre le codeur insensible.
Si vous souhaitez modifier les deux broches par défaut utilisées par le codeur, modifiez les lignes suivantes dans le script. Veillez à utiliser les codes de numérotation BCM.
GPIO_A = 26
GPIO_B = 19
Si vous voulez changer la broche à laquelle le bouton poussoir est accroché, alors modifiez la ligne correspondante dans le script ci-dessous. Veillez à utiliser les codes de numérotation BCM. Si vous n'avez pas de bouton, réglez ce paramètre sur Aucun.
GPIO_BUTTON = 13
Si vous souhaitez modifier la plage (c'est-à-dire : min & max) que le script module le volume du Raspberry Pi, alors éditez la ligne correspondante dans le script ci-dessous. Les chiffres sont exprimés en pourcentage. Le max par défaut est inférieur à 100 pour éviter toute distorsion. Le min par défaut est supérieur à zéro car si votre système est comme le mien, le son devient complètement inaudible bien avant 0%. Si vous avez un amplificateur matériel ou des haut-parleurs de qualité ou autre, vos résultats varieront.
VOLUME_MIN = 60
VOLUME_MAX = 96
Si vous souhaitez que le volume de votre système change plus rapidement et soit plus sensible, modifiez la ligne correspondante dans le script ci-dessous. Le paramètre par défaut est 1, passez à 2 pour doubler le taux de changement de volume.
VOLUME_INCREMENT = 1

Comment désinstaller le script

Répétez les étapes 7 et 8 ci-dessus, mais en supprimant la ligne ajoutée à l'étape 8 ou en la commentant par l'ajout d'un hachage, par exemple la ligne se lit comme suit :
#python /recalbox/scripts/volume-monitor.py

Crédits

Je tiens à remercier Substring pour l'aide qu'il nous a apporté en rendant possible ce guide et la modification de Recalbox. J'aimerais également remercier les autres développeurs de Recalbox pour avoir rendu ce merveilleux projet disponible. Et je voudrais remercier savetheclocktower pour le code du projet original et pour son aide dans la conversion en Python2.
Dernière mise à jour 7mo ago