Skru' op!
Ovre i min dertil-indrettede potentiometer skuffe fandt jeg mig en 50k variabel modstand - et potentiometer - samt 3 nĂŠrmest pinligt tynde ledninger, og en Arduino Pro Micro og har lavet mig en fysisk volume kontrol til min computer.
Jeg har set mange forskellige varianter af lignende projekter, og hvor mange andre har lavet det med sÄkaldte rotary encoders som er digitale, og derfor nÊrmest endnu nemmere at have med at gÞre, end et analog komponent som en modstand er, sÄ var det ikke dét jeg ledte efter.
Jeg kan nemlig godt lide at min volume kontrol har stops, sÄledes at nÄr man rammer 0% eller 100% volume (These go to eleven), at man ikke kan dreje volume kontrolleren yderligere. PrÊcis som pÄ et old school stereoanlÊg, guitarforstÊrker eller hvad ved jeg.
SÄ uden at tÊnke alverden mere over diverse microcontrollers' begrÊnsninger, var min tankte at jeg blot ville lÊse potentiometerets vÊrdi, mappe vÊrdien til et sted imellem 0-100, og derfra bare fÄ arduinoen til at sende computeren samme kommando som jeg kan bruge til at sÊtte volumen til en specifik vÊrdi i terminalen. pactl
bruger jeg typisk til dét-
-Lidt hurtig sÞgning pÄ nettet fik mig hurtigt til at indse, hvad jeg jo egentlig i forvejen udmÊrket vidste: det er bare ikke lige helt sÄdan arduinos kan interagere med et vÊrtssystem - ogsÄ selvom det havde vÊret praktisk.
Hm, nÄ! NÊste tanke var sÄ, at, ligesom fÞr, mÄle potentiometerets nuvÊrende vÊrdi sammenligne med forrige mÄling, og med en pro micro, som i modsÊtning til en klassisk Arduiono UNO ogsÄ kan agere HID (Human Input Device)/tastatur. Med et HID
-bibliotek ville man sÄ kunne eksekvere samme kommando som tidligere nÊvnt, men denne gang ved at sende keystrokes der ville taste kommandoen for mig, eks i terminalen i stedet.
Ja - mine egne tanker om lÞsningen var det samme! "Brother eww!" Det bliver rigtig hurtigt rigtig grimt pÄ nippet til ulÊkkert, og ville mest af alt minde om et eller andet ducky script/script-kitty payload. Det var allerede udelukket!
Det ville ogsÄ resultere i at der sÄ snart man justerede lyden, ville poppe et terminal vindue op pÄ ens skÊrm, som sÄ ville skulle vÊre der i n
-antal milliseconder, imens pro micro'en sendte de nĂždvendige keystrokes der tilsammen udgĂžr kommandoen vi prĂžver at kĂžre, og herefter lukke ned igen...đ
Og hvad sĂ„ hvis man skruer rigtig hurtigt rigtig meget op? Ja, sĂ„ ville den skulle sende kommandoen for hver mĂ„ling pro microen registrerede imens jeg skruede op, og det ville bombardere enhver computer med terminal vinduer đ
Men! Dét jeg kunne gÞre var sÄ at mÄle potentiometerets vÊrdi, sammenligne med den forrige mÄling, og i tilfÊlde af at var en difference pÄ mere end +/- 1% pÄ de to, at bruge pro micro'ens keyboard emulerings-egenskaber til at sÄ sende keystrokes for de volume taster der jo findes pÄ de fleste tastature - MEDIA_VOLUME_UP
og MEDIA_VOLUME_DOWN
.
Her er udfordringen jo sÄ, at man med de taster jo ikke sÊtter volumen til en specifik vÊrdi, men istedet justerer den op eller ned, typisk med 2% af gangen. Det har ogsÄ den uheldige resultat, at hvis man tilslutter enheden til en computer hvor volumen er pÄ 100%, imens volume-knob'et er pÄ 0%, at jeg fÞrst ville skulle skrue op, fÞr jeg ville kunne skrue ned - pga. de stops, som jeg jo var sÄ insisterende pÄ at jeg ville have.
Men faktisk er det den overordnede idé, som firmwaren er endt med, i hvert fald i skrivende stund - og efter lidt justen frem og tilbage pÄ lidt grace time/hvor ofte jeg sender keystrokes i pro microens main loop, virker det faktisk vÊsentlig bedre end jeg ville have turdet hÄbe pÄ.
Jeg valgte at gÞre det pÄ den mÄde til trods for mine "strenge" krav, for at gÞre enheden 100% "OS-agnostisk", ligesom jeg jo selv er. Tilslutter jeg den til en vilkÄrlig Windows eller Mac computer, vil det fortsat fungere out-of-the-box, da de jo ligesom Linux blot ser enheden som et tastatur, der trykker pÄ den ene eller anden tast..
Men det havde lidt samme udfordring som fÞr - hvad mon hvis jeg igen skruer for hurtigt op, eller ned? Der ville alt efter hvor hurtigt jeg gjorde det, mÄske blive sendt et keystroke eller to undervejs, imens potentiometeret reelt kan have roteret eks. 75% eller hvad ved jeg, hvis jeg virkeligt giver den gas.
SÄ ville de to igen hurtigt ende ude af sync, hvor volumen eks kunne vÊre hÞj, imens potentiometeret var pÄ 0% eller for den sags skyld omvendt.
SÄ i stedet for at blot bruge viden om, at "der var en difference <+/-1" til at sende et enkeltstÄende keystroke, kalkulerer vi selvfÞlgeligt den reele difference pÄ de to vÊrdier, og i et loop i stedet sender ét MEDIA_VOLUME_UP
eller MEDIA_VOLUME_DOWN
keystroke for hver anden difference i vÊrdierne der var pÄ denne mÄling og den forrige.
Vi sender kun hver anden gang, da tastatur volume tasterne typisk justerer med 2% af gangen, imens vores potentiomer jo er mappet til samtlige vĂŠrdier imellem 0-101.
Og som altid, prÞver jeg slet ikke at lade som om at jeg har opfundet den dybe tallerken eller skrevet et nyt framework, eller hvad ved jeg (i bunden af siden, linker jeg endda til en instructables how-to, der gÞr mere eller mindre det samme), men mere bare fortÊlle om udfordringerne man kan mÞde pÄ selv simple projekter som det her, og lidt om selve tankeprocessen der ender med at fÄ mig i mÄladd .
Den simple Êndring har dog nÊrmest fjernet alt "slÞr" der gjorde at computeren og kontrolleren af og til endte ud af sync - jeg kan dog godt nogle gange ramme 0% stoppet pÄ kontrolleren, hvor den reelle volume stÄr pÄ 2% og en sjÊlden gang 4%.. men sÄ er vi lidt i marginalerne.
Men stadig.. nÄr nu jeg selv havde sat succeskriterierne, og selv ville have et 0% stop og et 100% stop, sÄ var den eneste rigtige lÞsning i min bog, at fÄ det gjort - jeg ville kunne sÊtte en specifik vÊrdi, sÄledes at nÄr jeg pÄ den endelige enhed havde skruet 50% op, at volumen pÄ min computer ogsÄ var 50%!
SÄ ud over at lade kontrolleren klare nogle keystrokes den ene vej, eller den anden, mÄtte jeg finde en mÄde at faktisk interagere med selve styresystemet.
Kommandoen jeg skal kÞre for at sÊtte min volume til eks 50% kunne eks. se sÄledes ud:
pactl set-sink-volume @DEFAULT_SINK@ 50%
Det er noget jeg snildt ville kunne gĂžre med python og dets indbyggede os
-bibliotek. Jeg ville egentlig ogsÄ fra start af helst slippe for at have det dér "server/client" forhold, hvor der skulle kÞre noget som helst software pÄ computeren for at det virkede. Men jeg ville samtidig opnÄ prÊcissionen jeg havde sat mig for, og mÄtte derfor gÄ pÄ kompromis ét eller andet sted.
Jeg endte derfor ogsÄ med at lave et python "companion" script, der nÄr en enhed tilsluttes /dev/ttyACM0
starter af sig selv og overvÄger data der sendes til serial porten via pythons serial
-bibliotek, imens jeg selvfĂžlgeligt fik pro micro'en til at outputte selve volume-vĂŠrdien vi ledte efter til serial-porten.
Og sÄ var vi dér! Super responsiv, reagere prÊcist sÄ hurtigt som jeg skruer op eller ned, ligesom den fungerer bÄde med og uden det ekstra python software, for at holde det simpelt og universelt.
Det gÞr at jeg nu kan tilslutte det til en vilkÄrlig anden enhed, der understÞttet et USB tastatur og justere lydstyrken med en ret stor prÊcission, hvor jeg pÄ min arbejdscomputer har den prÊcise kontrol jeg satte mig for at opnÄ. Der skulle i python-land blot trÊkkes lidt fra og lÊgges lidt til nÄr jeg satte lydstyrken til en specifik vÊrdi, da kontrolleren i sig selv, jo fortsat sendte de keystrokes der gÞr at den kan kÞre selv.
Schematic + BOM
BOM:
- Arduino Pro Micro
- 50K potentiometer
- Jumper wires
Loddet sammen sÄlede:
A0
- Analog GPIO 0, og 3.3v
til venstre ben og GND
det hÞjre nÄr man ser potentiometeret ovenfra, som pÄ billedet ovenfor, med benene peget imod dig selv.
C++ kode
Jeg koder jo mine embedded projekter/hardware projekter i PlatformIO (pio), som krĂŠver filene er c++ (.cpp
) filer, i stedet arduino formatet (.ino
). Arduino er jo et subset af C++ i forvejen, der blot tilfÞjer noget ekstra funktionalitet, sÄ koden herunder skulle vist meget gerne vÊre 100% kompatibel med Arduino IDE hvis du bruger dét, og blot Êndrer endelsen. Du skal ogsÄ installere HID-Project af NicoHood fra Arduino library-manageren.
#include <HID-Project.h>
#include <HID-Settings.h>
#define REVERSED true
int currentVolume = 0;
int previousVolume = 0;
int volumeAdjustment = 0;
void setup()
{
Serial.begin(115200);
Consumer.begin();
delay(1000);
for (int i = 0; i < 52; i++)
{
Consumer.write(MEDIA_VOLUME_DOWN);
delay(2);
}
}
void loop()
{
currentVolume = analogRead(A0);
currentVolume = map(currentVolume, 0, 1023, 0, 101);
if (REVERSED)
{
currentVolume = 101 - currentVolume;
}
if (abs(currentVolume - previousVolume) > 1)
{
int volumeDifference = (currentVolume / 2) - (previousVolume / 2);
previousVolume = currentVolume;
if (volumeDifference > 0)
{
for (int i = 0; i < volumeDifference; i++)
{
Consumer.write(MEDIA_VOLUME_UP);
Serial.println(volumeAdjustment + i + 1);
delay(2);
}
}
else if (volumeDifference < 0)
{
for (int i = 0; i < abs(volumeDifference); i++)
{
Consumer.write(MEDIA_VOLUME_DOWN);
Serial.println(volumeAdjustment - i - 1);
delay(2);
}
}
volumeAdjustment += volumeDifference;
}
delay(35);
}
Python "companion script" (valgfri)
Ja, som nĂŠvnt er companion-scriptet her ikke overhovedet nogen nĂždvendighed, for at styre sin volume, men Ăžnsker du den 100% prĂŠcise kontrol kĂžrer du blot filen her.
import serial
import os
import time
port = '/dev/volume-knob' if os.path.exists('/dev/volume-knob') else '/dev/ttyACM0'
baudrate = 115200
def initialize_serial_connection(port, baudrate):
try:
connection = serial.Serial(port, baudrate, timeout=1)
return connection
except (serial.SerialException, OSError) as e:
print(f"Failed to connect to {port}: {e}")
exit(1)
def read_line_from_serial(connection):
try:
line = connection.readline().strip().decode('utf-8')
return line
except serial.SerialException:
print("SerialException: Device disconnected")
connection.close()
exit(0)
except OSError as e:
print(f"OSError: {e}")
connection.close()
exit(1)
def main():
connection = initialize_serial_connection(port, baudrate)
previous_volume = None
while True:
line = read_line_from_serial(connection)
if not line:
time.sleep(0.1)
continue
try:
volume = int(line) * 2
print(volume)
if previous_volume is not None:
if volume > previous_volume:
volume -= 2
elif volume < previous_volume:
volume += 2
if previous_volume != volume:
os.system(f'pactl set-sink-volume @DEFAULT_SINK@ {volume}%')
previous_volume = volume
except ValueError:
print("Error: Invalid integer received")
continue
if __name__ == "__main__":
main()
Bruger din enhed en anden port end /dev/ttyACM0
ĂŠndrer du selvfĂžlgeligt blot port
variablen til at vÊre dén dit system har tildelt.
Jeg angiver port
som /dev/volume-knob
da jeg i nÊste trin fÄr udev
til at lave et symlink til dén path, og eksisterer den ikke defaulter scriptet til /dev/ttyACM0
, sÄledes at det virker med og uden udev
.
I projektets Github repo er der ogsÄ en debugging udgave af scriptet, der logger hvad der sker, hvis der er noget der driller.
KÞr kun nÄr enheden er tilsluttet
Som nÊvnt kÞrer jeg udelukkende scriptet, nÄr enheden er tilsluttet - de smÄ 50 LOCs er nok ikke noget man pÄ noget som helst tidspunkt ville bemÊrke kÞrte i baggrunden, hvis man blot kaldte scriptet ved boot, men jeg ser ingen grund til det - heller ikke selvom at enheden kommer til at vÊre tilsluttet min computer fra boot, hver gang den starter. Her bruger jeg udev
sammen med systemd
, som selvfÞlgeligt gÞr det til en *nix-only mÄde at gribe det an pÄ
Opret udev
regel for dit board
- Brug
lsusb
kommandoen til at finde din enhedsidVendor
ogidProduct
og noter begge dele. F(Fire-cifrede tal, mine var hhv. 2341 og 8036) - Opret en
udev
-regel for din enhed, sÄledes
sudo vim /etc/udev/rules.d/99-arduino-leonardo.rules
- Og tilfĂžj fĂžlgende linje, hvor du selvfĂžlgeligt udskifter mit
idVendor
ogidProduct
, sÄ det matcher dit, ligesom du skal ÊndreDIT-BRUGERNAVN
og/PATH/TIL/DIT/VOLUME/COMPANION/SCRIPT.py
til at matche din bruger og lokationen af dit script - det er vigtigt det er en komplet path - ikke noget~/mappe/script.py
,$HOME/mappe/script.py
eller lignende! .
SUBSYSTEM=="tty", ATTRS{idVendor}=="2341", ATTRS{idProduct}=="8036", SYMLINK+="volume-knob", RUN+="/bin/su DIT-BRUGERNAVN -c '/usr/bin/python /PATH/TIL/DIT/VOLUME/COMPANION/SCRIPT.py'"
Her ĂŠndrer du selvfĂžlgeligt DIT-BRUGERNAVN
og /PATH/TIL/DIT/VOLUME/COMPANION/SCRIPT.py
til at matche din bruger og lokationen af dit script - det er vigtigt det er en komplet path - ikke noget ~/mappe/script.py
, $HOME/mappe/script.py
eller lignende!
- Gem og luk (
:wq
)
GenindlĂŠs nu udev
udev
kan man fÄ til at genindlÊse sine regler med fÞlgende kommandoer:
sudo udevadm control --reload-rules
sudo udevadm trigger
- Tilslut kontrolleren - BOOM, nu skulle dit script gerne kÞre, sÄ vÊr OBS pÄ om nu volumen stÄr pÄ 100%, hvis du samtidigt lytter til et musik e.l!
SÄ er OS-delen af dit set ogsÄ fÊrdig - der findes andre mÄder at gribe det an pÄ, og nedenfor gÄr jeg ogsÄ lidt igennem, hvorfor jeg valgte at gÄ den vej som jeg gjorde.
Konklussion/How did it go?
Well... jeg har skrevet i beskrivelsen/meta tags til indlĂŠgget her at det er et "10 minutters projekt" - og hvor det er 100% rigtigt med denne lille "how-to", var det meget langt fra 10 minutter for mig.
IsÊr dét med at kÞre scriptet automatisk ved tilslutning noget der tog mig noget tid at komme igennem - de enheder jeg typisk laver, nÄr jeg tinker med hardware er ikke tiltÊnkt at skulle sluttes til en computer efterfÞlgende og er ofte selvstÊndige enheder.
Men jeg vidste at udev
har med tilslutning af enheder at gÞre, efter jeg fÞrste gang satte PlatformIO op, sÄ i fÞrste ville jeg bruge udev
til at kĂžre en systemd
service - det gik egentlig fint, men sÄ var der en udfordring i at pactl
(pÄ mit system, i hvert fald) ikke kan styres af root-brugeren som systemd
-services typisk bruger, da det er min bruger der kalder det ved login.
Yderligere havde jeg en udfordring at fÄ servicen til at stoppe igen, pÄ en mÄde sÄ den ikke bare stod som inactive/dead, selv nÊste gang jeg tilsluttede enheden igen. Dette brugte jeg sÄ noget tid pÄ, at prÞve at fikse i selve python scriptet, men jeg kunne ikke finde nogen gylden mellemvej, hvor scriptet sÄ fungerede som forventet bÄde ved manuel eksekvering og via udev
/systemd
ved tilslutning.
SĂ„ jeg droppede systemd
og ville blot eksekvere scriptet nÄr enheden tilsluttes direkte i udev
-reglen, som endte med, men her bÞvlede jeg sÄ rigtig meget med, at pÄ forskellige mÄder at igen eksekvere med min bruger, frem for root udev
, akkurat som systemd
er en systemprocess, og det er derfor normen at bruge root.
Men det er derfor at kommandoen der kĂžres via udev
er lidt f
'd at se pÄ - typisk nÄr man kalder et python script, kan det klares med et simpelt kald:
python her/er/mit/script.py
NÄr systemet skal eksekvere det, skal den dog bruge en 100% lokation for bÄde programmet vi kalder og relative paths gÄr heller ikke, og vi ender i stedet pÄ noget a la:
/usr/bin/python /home/bruger/scripts/her/er/mit/script.py
Da vi oven i hatten skulle have systemet til at eksekvere pÄ brugerens vejne mÄtte jeg fÞrst bruge su
, som pÄ *nix systemer bruges til at skifte bruger med, sammen med -u
flaget til at definere hvem man eksekvere det som, efterfulgt af -c
for at fortĂŠlle hvilken kommando der skal kĂžres, og ender derfor med det her rod.
/bin/su -u mr -c /usr/bin/python /home/mr/Repos/volume-server/server.py
Og for at, som jeg jo altid sigter efter at gĂžre; citere Tom Hanks i rollen som Forest Gump: "And that all I have to say about that (đ€ź)
Men ift selve enheden, sÄ er jeg rigtig glad for resultatet! OgsÄ selvom at jeg pÄ AKKO Alice Pro tastaturet som jeg skriver pÄ lige nu, reelt set har dedikerede volume taster som gÞr AKKOrat (hÞhÞ) det samme. Jeg skifter dog ofte keyboards med forskellige layouts og antal af taster, sÄ at have den fysiske mulighed altid tilgÊngelig er fantastisk!
Og selvfÞlgelkigt - her er da ogsÄ lige et billede af, hvordan den ser ud:
Som altid, nĂ„r jeg laver sĂ„danne projekter kigger jeg rundt pĂ„ mit kontor og tĂŠnker "hvad kan jeg proppe dig ind i? đ€", og fandt en gammel Virginia Flake pibe tobak dĂ„se, jeg borede et par huller i til potentiometeret og pro micro'ens USB kabel
Hvis du tjekker linket nedenfor "USB Volume Controller - Potentiometer Based", vil du ogsÄ kunne en Instructables how-to, der indeholder 3D print/stl filer til et lignende projekt, der indeholde de samme komponenter.
Links og dokumentation
Det her har vÊret noget mere et trial and error-projekt, end sÄ meget andet, men her er dokumentationen til nogle af de ting der fik mig i mÄl, samt links til hvor man kan kÞbe hvad der skal bruges.
Links:
- Fritzing - Schematics og hardware illustrationer
- PlatformIO - Embedded programmering i din yndlings editor
- Arduino - Open source prototype platform med egen IDE
Hardware:
- Pro Micro USB-C version
- Potentiometer
Dokumentation
- Projektet pÄ Github
- Udev manual
- HID/HID-Project arduino bibliotek wiki
- Arduino Control - Windows Volume (lattepanda.com)
- USB Volume Controller - Potentiometer Based : 9 Steps (with Pictures)
- systemd manual
- systemd
Der er ikke tale om affiliate links, det er blot for at gĂžre projektet nemmere at komme i gang med. For your tinkering pleasure!