Python – Remote HID Keystroke Converter
In de vorige post hebben we gezien hoe je met een Raspberry Pi Zero W een toetsenbord maakt welke remote aan te sturen is. Voor iedereen die weet wat een USB Rubber Ducky is, dit is in principe hetzelfde voor slechts 15 euro incl. remote control. Echter blijft er 1 ding wel erg lastig en dat is de onhandige manier van codes invoeren. Voor een simpel 5-letterig woord moet je al een heel script schrijven. Kan dat niet makkelijker?
In de vorige post hebben we gezien dat we een functie (write_report) kunnen maken om op deze manier keystrokes door te sturen naar het HID device. We kunnen dit doen in Bash, Python en andere scripting engine’s. In Bash maken we gebruik van hexadecimale notering. Zoals:
write_report "\x20\0\xb\0\0\0\0\0" |
In Python ziet gebruiken we een decimale opmaak:
NULL_CHAR = chr(0) write_report (chr(32)+NULL_CHAR+chr(11)+NULL_CHAR*5) |
Elke byte in de string representeert een karakter. De eerste byte representeert de modifier key (SHIFT, CTRL, ALT, WIN) keys. De 2e byte is gereserveerd en de volgende 6 bytes worden gebruikt voor keypresses.
Zoals we al zagen is het schrijven van een simpel woordje best een klus. Het woordje “Hallo” ziet er als volgt uit:
Bash
#!/bin/bash function write_report { echo -ne $1 > /dev/hidg0 } #h write_report "\x20\0\xb\0\0\0\0\0" #a write_report "\0\0\x4\0\0\0\0\0" #l write_report "\0\0\xf\0\0\0\0\0" #l write_report "\0\0\xf\0\0\0\0\0" #o write_report "\0\0\x12\0\0\0\0\0" #Release toetsen write_report "\0\0\0\0\0\0\0\0" |
Python:
#!/usr/bin/env python3 NULL_CHAR = chr(0) def write_report(report): with open('/dev/hidg0', 'rb+') as fd: fd.write(report.encode()) write_report (chr(32)+NULL_CHAR+chr(11)+NULL_CHAR*5) #a write_report (NULL_CHAR*2 +chr(4)+NULL_CHAR*5) #l write_report (NULL_CHAR*2 +chr(15)+NULL_CHAR*5) #l write_report (NULL_CHAR*2 +chr(15)+NULL_CHAR*5) #o write_report (NULL_CHAR*2 +chr(18)+NULL_CHAR*5) #Release toetsen write_report (NULL_CHAR*8) |
De eerste stap was om een Excel bestand te maken waar alle keystrokes (Bash & Python) in vermeld staan. Deze Excel sheet vindt je hier.
Dit is al wat makkelijker. We kunnen nu de juiste keystrokes kopiëren en plakken. Echter is dit nog steeds te omslachtig. Kan het niet makkelijker?
Natuurlijk! Laten we een generator maken. En wat gebruiken we daarvoor? Uiteraard mijn favoriete taal, Python!
Python HID Keystroke Converter
Laten we voorop stellen, ik ben niet goed in programmeren. Ik weet zeker dat jullie dit beter kunnen. Elke welkome aanvulling, verbetering of uitbreiding is dan ook welkom!
De basis (bovenstaande script) in Python hebben we al. Dit is geen generator. Maar we weten hieruit wel een aantal zaken.
- We moeten / kunnen een NULL_CHAR variabele definieren om beter inzichtelijk te maken wat 0-karakters zijn.
- We moeten een “write_report” functie schrijven.
Maar hoe gaan we keystrokes omzetten in “write_report” functies. Ik dacht aan een dictionary. Deze zou er dan ongeveer als volgt uit gaan zien.
'a':'write_report(NULL_CHAR*2 +chr(4)+NULL_CHAR*5)', 'b':'write_report(NULL_CHAR*2 +chr(5)+NULL_CHAR*5)', |
Het probleem is echter dat we elke toetsaanslag moeten registreren om deze om te zetten in een code. In de character tabel komen ook de volgende toetsen voor “SPATIE / TAB / ENTER”. Maar dit typen we niet zo in (de S zou vertaald worden naar “write_report(chr(2)+NULL_CHAR+chr(22)+NULL_CHAR*5)” i.p.v. dat we de kans zouden krijgen om het hele woord SPATIE voluit te typen.
Juist om deze reden heb ik besloten om de meest gebruikte keystrokes op te nemen in de dictionary en om symbolen die minder gebruikt worden toe te kennen aan een actie die uit meerdere letters bestaat zoals SPATIE. Dit ziet er dan als volgt uit.
'$':'write_report(NULL_CHAR*2 +chr(40)+NULL_CHAR*5)', #ENTER '%':'write_report(NULL_CHAR*2 +chr(41)+NULL_CHAR*5)', #ESC '~':'write_report(NULL_CHAR*2 +chr(43)+NULL_CHAR*5)', #TAB '_':'write_report(NULL_CHAR*2 +chr(44)+NULL_CHAR*5)', #SPATIE ']':'write_report(chr(5)+NULL_CHAR+chr(76)+NULL_CHAR*5)', #CTRL+ALT+DEL |
Zoals je ziet zal het “_” symbool het SPATIE teken gebruiken en het $ symbool een ENTER karakter. Uiteraard kun je hier zelf mee spelen en deze naar hartenlust veranderen.
We moeten echter na het indrukken van elke keystroke een releasecode meegeven. Dit voorkomt dat het script de laatste letter oneindig blijft uitvoeren en dat dubbele letters niet als 1 letter gelezen worden. Als we dit niet zouden doen dan zou “AAA” slechts uitgevoerd worden als “A”. Juist om deze reden hebben we de releasecode ook opgenomen in de dictionary wat er dan als volgt uitziet:
'a':'write_report(NULL_CHAR*2 +chr(4)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'b':'write_report(NULL_CHAR*2 +chr(5)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'c':'write_report(NULL_CHAR*2 +chr(6)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'd':'write_report(NULL_CHAR*2 +chr(7)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', |
Nu we de dictionary compleet hebben vragen we om de user input. Deze input wordt omgezet naar keystrokes.
user_input = input("Please tell me what you would like to translate: ") |
Dan definiëren we een lege lijst welke we later gaan vullen:
translated_list = [] |
En met een “for” loop gaan we de input “door” de dictionary gooien tot er een match is:
for letter in user_input: if letter in values: translated_list.append(values[letter]) |
Bovenstaande “.append” functie zorgt ervoor dat matches toegevoegd worde naan de lijst “translated_list”.
Vervolgens voeren we de code uit:
exec ("\n".join(translated_list)) |
De “join” zorgt ervoor dat het geheel wordt omgezet naar een string en de “\n” zorgt ervoor dat elke regel op een nieuwe lijn uitgevoerd wordt.
Als we ook meteen een script willen maken welke kunnen kopiëren kan dat ook. We gebruiken dan bovenstaande commando in combinatie met het “print” commando:
print("\n".join(translated_list)) |
Nu dat allemaal werkt completeren we de code en zorgen we ervoor dat we de directe uitvoer van commando’s aan en uit kunnen zetten evenals het genereren van de lijst. Beide mogelijkheden kunnen dus onafhankelijk van elkaar aan en uit worden gezet.
De hele code ziet er dan als volgt uit:
#!/usr/bin/python """ HID Keyboard Mapper Version: 1.0 By: Jarno Baselier Date: 2018 URL: www.jarnobaselier.nl This HID Keyboard Mapper is specially created to quickly translate keystrokes to Python, which execute it as HID output. Let's use a Raspberry Pi Easy as an example. Configure the USB port on the Raspberry Pi as an HID device and connected it to a computer. Now you can start this Python converter to create a keystroke script or to immediately send the keystrokes to the connected computer. """ #Imports #Program variables LIST = 1 #If LIST is 1 than complete keymap code is shown on screen EXECUTE = 1 #If EXECUTE is 1 than complete keymap code is send to the HID device and executed #Variables NULL_CHAR = chr(0) #Functions #Create write_report function to write HID codes to the HID Keyboard def write_report(report): with open('/dev/hidg0', 'rb+') as fd: fd.write(report.encode()) #Dictionary values = { 'a':'write_report(NULL_CHAR*2 +chr(4)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'b':'write_report(NULL_CHAR*2 +chr(5)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'c':'write_report(NULL_CHAR*2 +chr(6)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'd':'write_report(NULL_CHAR*2 +chr(7)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'e':'write_report(NULL_CHAR*2 +chr(8)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'f':'write_report(NULL_CHAR*2 +chr(9)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'g':'write_report(NULL_CHAR*2 +chr(10)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'h':'write_report(NULL_CHAR*2 +chr(11)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'i':'write_report(NULL_CHAR*2 +chr(12)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'j':'write_report(NULL_CHAR*2 +chr(13)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'k':'write_report(NULL_CHAR*2 +chr(14)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'l':'write_report(NULL_CHAR*2 +chr(15)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'm':'write_report(NULL_CHAR*2 +chr(16)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'n':'write_report(NULL_CHAR*2 +chr(17)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'o':'write_report(NULL_CHAR*2 +chr(18)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'p':'write_report(NULL_CHAR*2 +chr(19)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'q':'write_report(NULL_CHAR*2 +chr(20)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'r':'write_report(NULL_CHAR*2 +chr(21)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 's':'write_report(NULL_CHAR*2 +chr(22)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 't':'write_report(NULL_CHAR*2 +chr(23)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'u':'write_report(NULL_CHAR*2 +chr(24)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'v':'write_report(NULL_CHAR*2 +chr(25)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'w':'write_report(NULL_CHAR*2 +chr(26)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'x':'write_report(NULL_CHAR*2 +chr(27)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'y':'write_report(NULL_CHAR*2 +chr(28)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'z':'write_report(NULL_CHAR*2 +chr(29)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'A':'write_report(chr(2)+NULL_CHAR+chr(4)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'B':'write_report(chr(2)+NULL_CHAR+chr(5)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'C':'write_report(chr(2)+NULL_CHAR+chr(6)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'D':'write_report(chr(2)+NULL_CHAR+chr(7)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'E':'write_report(chr(2)+NULL_CHAR+chr(8)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'F':'write_report(chr(2)+NULL_CHAR+chr(9)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'G':'write_report(chr(2)+NULL_CHAR+chr(10)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'H':'write_report(chr(2)+NULL_CHAR+chr(11)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'I':'write_report(chr(2)+NULL_CHAR+chr(12)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'J':'write_report(chr(2)+NULL_CHAR+chr(13)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'K':'write_report(chr(2)+NULL_CHAR+chr(14)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'L':'write_report(chr(2)+NULL_CHAR+chr(15)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'M':'write_report(chr(2)+NULL_CHAR+chr(16)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'N':'write_report(chr(2)+NULL_CHAR+chr(17)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'O':'write_report(chr(2)+NULL_CHAR+chr(18)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'P':'write_report(chr(2)+NULL_CHAR+chr(19)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'Q':'write_report(chr(2)+NULL_CHAR+chr(20)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'R':'write_report(chr(2)+NULL_CHAR+chr(21)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'S':'write_report(chr(2)+NULL_CHAR+chr(22)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'T':'write_report(chr(2)+NULL_CHAR+chr(23)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'U':'write_report(chr(2)+NULL_CHAR+chr(24)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'V':'write_report(chr(2)+NULL_CHAR+chr(25)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'W':'write_report(chr(2)+NULL_CHAR+chr(26)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'X':'write_report(chr(2)+NULL_CHAR+chr(27)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'Y':'write_report(chr(2)+NULL_CHAR+chr(28)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', 'Z':'write_report(chr(2)+NULL_CHAR+chr(29)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '1':'write_report(NULL_CHAR*2 +chr(30)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '2':'write_report(NULL_CHAR*2 +chr(31)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '3':'write_report(NULL_CHAR*2 +chr(32)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '4':'write_report(NULL_CHAR*2 +chr(33)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '5':'write_report(NULL_CHAR*2 +chr(34)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '6':'write_report(NULL_CHAR*2 +chr(35)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '7':'write_report(NULL_CHAR*2 +chr(36)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '8':'write_report(NULL_CHAR*2 +chr(37)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '9':'write_report(NULL_CHAR*2 +chr(38)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '0':'write_report(NULL_CHAR*2 +chr(39)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '!':'write_report(chr(2)+NULL_CHAR+chr(30)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '@':'write_report(chr(2)+NULL_CHAR+chr(31)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '#':'write_report(chr(2)+NULL_CHAR+chr(32)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '&':'write_report(chr(2)+NULL_CHAR+chr(36)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '*':'write_report(chr(2)+NULL_CHAR+chr(37)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '(':'write_report(chr(2)+NULL_CHAR+chr(38)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', ')':'write_report(chr(2)+NULL_CHAR+chr(39)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '-':'write_report(NULL_CHAR*2 +chr(45)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '=':'write_report(NULL_CHAR*2 +chr(46)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '{':'write_report(NULL_CHAR*2 +chr(47)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '}':'write_report(NULL_CHAR*2 +chr(48)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '\\':'write_report(NULL_CHAR*2 +chr(49)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', ';':'write_report(NULL_CHAR*2 +chr(51)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '':'write_report(NULL_CHAR*2 +chr(52)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '`':'write_report(NULL_CHAR*2 +chr(53)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', ',':'write_report(NULL_CHAR*2 +chr(54)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '.':'write_report(NULL_CHAR*2 +chr(55)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '/':'write_report(NULL_CHAR*2 +chr(56)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '0':'write_report(chr(2)+NULL_CHAR+chr(50)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', ':':'write_report(chr(2)+NULL_CHAR+chr(51)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '"':'write_report(chr(2)+NULL_CHAR+chr(52)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '<':'write_report(chr(2)+NULL_CHAR+chr(54)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '>':'write_report(chr(2)+NULL_CHAR+chr(55)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '?':'write_report(chr(2)+NULL_CHAR+chr(56)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '}':'write_report(NULL_CHAR*2 +chr(79)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '{':'write_report(NULL_CHAR*2 +chr(80)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', ']':'write_report(chr(2)+NULL_CHAR+chr(79)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', '[':'write_report(chr(2)+NULL_CHAR+chr(80)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', #Next Signs are used for different keymappings - change if you want to '$':'write_report(NULL_CHAR*2 +chr(40)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', #ENTER '%':'write_report(NULL_CHAR*2 +chr(41)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', #ESC '^':'write_report(NULL_CHAR*2 +chr(42)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', #BACKSPACE '~':'write_report(NULL_CHAR*2 +chr(43)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', #TAB '_':'write_report(NULL_CHAR*2 +chr(44)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', #SPATIE '+':'write_report(NULL_CHAR*2 +chr(70)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', #PRINT SCREEN '[':'write_report(NULL_CHAR*2 +chr(76)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', #DELETE ']':'write_report(chr(5)+NULL_CHAR+chr(76)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', #CTRL+ALT+DEL '|':'write_report(NULL_CHAR*2 +chr(81)+NULL_CHAR*5)\nwrite_report(NULL_CHAR * 8)', #DOWN } user_input = input("Please tell me what you would like to translate: ") #Create an empty list which we will fill later translated_list = [] #FOR Loop, loops every letter trough dictionary and saves translation in translated_list for letter in user_input: if letter in values: translated_list.append(values[letter]) #Print the translated command #The current command removes the comma's and brackets by making the output a string and devides every string by a whiteline if LIST == 1: print ("\n") print ("\n") print ("Below is the translated script, ready to copy-paste:") print ("____________________________________________________") print ("#!/usr/bin/env python3") print ("NULL_CHAR = chr(0)") print ("") print ("def write_report(report):") print (" with open('/dev/hidg0', 'rb+') as fd:") print (" fd.write(report.encode())") print ("") print("\n".join(translated_list)) #Write keyboard codes to the HID device if EXECUTE == 1: exec ("\n".join(translated_list)) #End with a NULL command to release all keys and avoid a endless string write_report(NULL_CHAR * 8) |
De code is uiteraard ook hier te downloaden!
De HID Converter starten
Om de HID converter te starten type je:
sudo python3 HIDConverter.py |
Vervolgens vraagt het script wat je vertaald wilt hebben:
Om nu het volgende stuk te typen:
Hallo mijn naam is Jarno!
Hoe heet jij? |
Type je dus (denk aan de speciale karakters):
Hallo_mijn_naam_is_Jarno!$Hoe_heet_jij? |
Als je op ENTER drukt zal (afhankelijk of beide opties aan staan) de tekst omgezet worden naar een direct uitvoerbaar script (copy / paste):
En ook zal de ingevoerde content direct uitgevoerd worden:
Het is met de converter dus super makkelijk om scripts te maken en/of meteen uit te voeren. Scripts die je maakt kunnen bijvoorbeeld als Bash script opgeslagen worden waarna je ze kunt uitvoeren tijdens het booten van de Pi (net zoals een USB Rubber Ducky).
Het script kan uiteraard nog verbeterd worden met meerdere functies en met wachttijden maar voor nu scheelt het al heel veel werk t.o.v. handmatig invoeren.
Kun jij de code verbeteren? Laat het me weten!
Maak je gebruik van de converter of wil je gewoon zo een complimentje geven… doe dat dan zeker ook even. Deel het op je sociale kanalen en met vrienden! Ik zou dat enorm op prijs stellen.
Dankjewel!