Εικονικά USB περιφερειακά με το QEMU!

dimitris | Πέμ, 09/12/2013 - 10:04 | 26'

Σε αυτό το άρθρο κατασκευάζουμε το πρώτο μας εικονικό USB περιφερειακό για τον QEMU, μαθαίνοντας παράλληλα περισσότερα για τους USB descriptors και το πώς δουλεύει το “Plug and Play”.

Του Παντελή Κουκούσουλα

Νοσταλγείτε τις ένδοξες μέρες του Linux Coffee HOWTO [1], τότε που οι Έλληνες geeks ήταν πραγματικοί geeks και έφτιαχναν τις δικές τους προγραμματιζόμενες καφετιέρες; Θέλετε να ασχοληθείτε με hardware αλλά σας εκνευρίζουν τα κατσαβίδια, τα κολλητήρια και τα έξοδα για τα Board Support Packages; Θέλετε να μεγιστοποιήσετε τον παραλληλισμό στην ανάπτυξη του software και του hardware για ένα νέο περιφερειακό (αυτό που στη βιομηχανία hardware αποκαλούν “shift left”); Θέλετε ένα εύκολο τρόπο για να ανακαλύπτετε κενά ασφαλείας στην υποστήριξη “Plug and Play” διάφορων λειτουργικών ώστε να αναπτύσσετε συσκευές “Plug and Own”; Σε όλες αυτές τις περιπτώσεις (και πολλές άλλες!), ο QEMU και τα εικονικά περιφερειακά του αποτελούν τη λύση που ψάχνετε!

Αναμφίβολα ο QEMU είναι ένα από τα πιο ενδιαφέροντα προϊόντα που έχει να δείξει η κοινότητα του ΕΛΛΑΚ. Από το πρώτο άρθρο του Fabrice Bellard στο USENIX το 2005 [2] μέχρι το Linux-KVM, το Xen, το VirtualBox και τον emulator του Android (goldfish), οι ιδέες (και ο κώδικας) του QEMU έχουν διεισδύσει πραγματικά παντού και έχουν αλλάξει τον τρόπο που αναπτύσσεται το λογισμικό συστήματος, αλλά και το hardware στις μέρες μας προς το (πολύ) καλύτερο. Δεν είναι τυχαίο ότι για κάποιο καιρό τουλάχιστον ο Fabrice φιγουράριζε ως το νούμερο 4 στο ρόστερ των Free Software Hackers (μετά το Stallman, τον Linus και το δημιουργό της Perl, Lary Wall).

Εικονικά περιφερειακά στο QEMU

Τα τελευταία χρόνια η ανάπτυξη του QEMU υπήρξε ραγδαία, όχι μόνο σε χαρακτηριστικά αλλά και προς την κατεύθυνση της βελτίωσης της δομής και της ποιότητας του κώδικα. Στις τελευταίες εκδόσεις, χάρη σε βελτιώσεις όπως το εσωτερικό μητρώο συσκευών qdev [3], το QEMU Object Model (QOM) κλπ, η συγγραφή εικονικών περιφερειακών έχει απλοποιηθεί σημαντικά με αποτέλεσμα να ανοίγονται νέες δυνατότητες χρήσης τους όπως π.χ., rapid prototyping [4], functional / security testing [5] κλπ.

Στο άρθρο αυτό θα φτιάξουμε το πρώτο μας εικονικό USB περιφερειακό για τον QEMU, με ίσα-ίσα αρκετή λειτουργικότητα ώστε να μπορεί να φαίνεται στο lsusb εντός της εικονικής μηχανής. Αυτό θα μας επιτρέψει να δούμε τη διαδικασία ανάπτυξης αυτών των συσκευών στην πράξη, καθώς και να μάθουμε τα βασικά για το πώς δουλεύουν τα περιφερειακά του QEMU, οι USB descriptors και το αρχικό στάδιο του Plug and Play των USB συσκευών.

USB descriptors

Ο Universal Serial Bus (USB) δε χρειάζεται συστάσεις. Είναι ίσως η δημοφιλέστερη παγκοσμίως τεχνολογία διασύνδεσης περιφερειακών με δισεκατομμύρια USB να κυκλοφορούν στην παγκόσμια αγορά. Ένα από τα σημαντικά χαρακτηριστικά που επέτρεψαν αυτή την κυριαρχία είναι το λεγόμενο “Plug and Play” η δυνατότητα δηλαδή να γίνεται η αρχικοποίηση και παραμετροποίηση της συσκευής αυτόματα κατά τη σύνδεση με αποτέλεσμα να είναι κατευθείαν έτοιμη για χρήση χωρίς να απαιτείται κάποια ενέργεια από το χρήστη.

Για να μπορέσει αυτό να λειτουργήσει, διατηρώντας παράλληλα χαμηλά το κόστος των περιφερειακών, ο USB σχεδιάστηκε ως ασύμμετρος (master-slave) δίαυλος, όπου όλη την πολυπλοκότητα της αρχικής διαπραγμάτευσης (handshake) και παραμετροποίησης (configuration) κάθε συσκευής την αναλαμβάνει εξολοκλήρου ο υπολογιστής (host). Η διαδικασία ονομάζεται απαρίθμηση (enumeration) και ξεκινά αυτόματα με τη σύνδεση της συσκευής. Αν όλα πάνε καλά ο host αποδίδει στη συσκευή έναν αριθμό από 1-127 μέσω του οποίου γίνεται στη συνέχεια η επικοινωνία με τη συσκευή. Το μόνο που χρειάζεται να κάνουν τα περιφερειακά είναι να μπορούν να περιγράψουν με ακρίβεια στο host τις λειτουργίες/δυνατότητες (π.χ., “ποντίκι”, “πληκτρολόγιο”, “USB flash stick”, κλπ) αλλά και τις απαιτήσεις τους (π.χ., σε ρεύμα για bus powered συσκευές).

Αυτό γίνεται στέλνοντας στο host δομές δεδομένων που ονομάζονται περιγραφείς (descriptors) οι οποίοι μάλιστα είναι διαρθρωμένοι σε μία ιεραρχία όπως στην παρακάτω εικόνα.

Τα είδη των descriptors έχουν ως εξής:

Device Descriptor: Υπάρχει μόνο ένας ανά συσκευή και περιλαμβάνει πληροφορία όπως π.χ., το λεγόμενο USB ID (VendorID:ProductID) της συσκευής που την ξεχωρίζει από τις υπόλοιπες και επιτρέπει στο λειτουργικό να αντιστοιχήσει τον κατάλληλο driver στη συσκευή, για συσκευές με μη τυποποιημένη λειτουργικότητα. Επίσης περιλαμβάνει μια λίστα από Configuration Descriptors.

Configuration Descriptor: Ο configuration descriptor περιγράφει παραμέτρους όπως π.χ., τα σχετικά με την κατανάλωση ρεύματος. Αν και οι περισσότερες συσκευές έχουν μόνο ένα Configuration Descriptor, είναι δυνατόν μια συσκευή να έχει και πάνω από έναν (π.χ., ένα Configuration για bus powered και ένα για mains powered). Επίσης περιλαμβάνει τη λίστα με τα Interfaces που αντιστοιχούν στο Configuration.

Interface Descriptor: Κάθε Interface μίας συσκευής αντιστοιχεί σε μία “τυποποιημένη λειτουργία” (class: π.χ., HID ή Mass Storage). Μία συσκευή μπορεί να έχει πάνω από ένα Interface π.χ., ένα πολυμηχάνημα μπορεί να έχει άλλο Interface για το σαρωτή, άλλο για τον εκτυπωτή και άλλο για το FAX, ή ένα τηλέφωνο μπορεί να είναι υλοποιημένο ως ένα USB Audio Interface για τον ήχο και ένα USB και ένα USB HID Interface για τα keypad, LCD, buzzer κλπ. Το σημαντικότερο κομμάτι ενός Ιnterface φυσικά είναι η λίστα με τα Endpoints.

Endpoint: Μπορείτε να φανταστείτε ένα Endpoint σαν ένα socket στο unix. Κάθε φορά που κάποιος λέει “στέλνω ένα πακέτο σε μία USB συσκευή” στην πραγματικότητα εννοεί ότι στέλνει το πακέτο σε ένα συγκεκριμένο Endpoint της συσκευής. Δυστυχώς ο χώρος του άρθρου αυτού δε φτάνει για να αναπτύξουμε επαρκώς τα Endpoints και τις κατηγορίες τους. Οπότε, ας αρκεστούμε στο ότι υπάρχει πάντα τουλάχιστον ένα Endpoint (το Endpoint #0) στο οποίο στέλνονται τα λεγόμενα πακέτα ελέγχου (control packets), μέσω των οποίων γίνεται η διαδικασία της απαρίθμησης που περιγράψαμε μεταξύ άλλων.

 

Φυσικά τα παραπάνω αγγίζουν απλά την “επιφάνεια του παγόβουνου” όσον αφορά το USB και τη λειτουργικότητα / ευελιξία του. Όποιος ενδιαφέρεται για ανάλυση σε περισσότερο βάθος μπορεί να απευθυνθεί στις πολύ κατατοπιστικές πηγές [6] και [7].

Η συσκευή μας

Όπως είπαμε, στόχος του άρθρου είναι να αναπτύξουμε μία εικονική συσκευή με αρκετή λειτουργικότητα ώστε να περνά επιτυχώς από τη διαδικασία της απαρίθμησης (enumeration) που αναφέραμε παραπάνω. Προηγουμένως όμως, πρέπει να βεβαιωθούμε ότι η συσκευή ενσωματώνεται σωστά και στην υποδομή του QEMU. Δηλαδή αφενός πρέπει το αρχείο της συσκευής να μεταγλωττίζεται κανονικά και αφετέρου πρέπει να μπορούμε να ζητήσουμε να συμπεριληφθεί η συσκευή μας σε μια εικονική μηχανή μέσω γραμμής εντολών, με την παράμετρο -device (νέα μέθοδος καθορισμού συσκευών) ή/και με την παράμετρο -usbdevice (παλιά μέθοδος) κλπ. Αυτά γενικά απλά συνεπάγονται την προσθήκη επιπλέον δεδομένων στο αρχείο του κώδικα της συσκευής μας. Οι παράμετροι που επιλέξαμε για τη συσκευή είναι να έχει όνομα dummyusb, θα είναι USB 1.1 συσκευή, θα έχει VendorID=0xdead και ProductID=0xbeef, Version=1.0, ένα μόνο Configuration το οποίο θα είναι bus powered και θα είναι προσβάσιμη από τη γραμμή εντολών με “-device usb-dummy” ή “-usbdevice dummy”.

Με βάση αυτές τις επιλογές, ο κώδικας της συσκευής μας διαμορφώνεται ως εξής (δείτε τον πλήρη κώδικα στο http://goo.gl/IYeEW):

#include "hw/hw.h"
#include "hw/usb.h"
#include "hw/usb/desc.h"
typedef struct USBDummyState {
    USBDevice dev;
} USBDummyState;
enum {
    STR_MANUFACTURER = 1,
    STR_PRODUCT,
    STR_SERIALNUMBER,
};
static const USBDescStrings desc_strings = {
    [STR_MANUFACTURER]     = "QEMU",
    [STR_PRODUCT]          = "USBDUMMY",
    [STR_SERIALNUMBER]     = "1",
};
static const VMStateDescription
       vmstate_usb_dummy = {
    .name                  = "usb-dummy",
    .unmigratable          = 1,
};
static const USBDescIface desc_iface_dummy = {
    .bInterfaceNumber      = 0,
    .bNumEndpoints         = 0,
    .bInterfaceClass       = USB_CLASS_VENDOR_SPEC,
};
static const USBDescDevice desc_device_dummy = {
    .bcdUSB                        = 0x0110,
    .bMaxPacketSize0               = 8,
    .bNumConfigurations            = 1,
    .confs = (USBDescConfig[]) {
        {
            .bNumInterfaces        = 1,
            .bConfigurationValue   = 1,
            .bmAttributes          = 0x80,
            .bMaxPower             = 40,
            .nif = 1,
            .ifs = &desc_iface_dummy,
        },
    },
};
static const USBDesc desc_usbdummy = {
    .id = {
        .idVendor          = 0xdead,
        .idProduct         = 0xbeef,
        .bcdDevice         = 0x0100,
        .iManufacturer     = STR_MANUFACTURER,
        .iProduct          = STR_PRODUCT,
        .iSerialNumber     = STR_SERIALNUMBER,
    },
    .full = &desc_device_dummy,
    .str  = desc_strings,
};
static int usb_dummy_initfn(USBDevice *dev)
{
    usb_desc_create_serial(dev);
    usb_desc_init(dev);
    return 0;
}
static int usb_dummy_handle_control(
USBDevice *dev, USBPacket *p, int request,
int value, int index, int length, uint8_t *data)
{
    int ret;
    ret = usb_desc_handle_control(dev, p, request,
                                  value, index,
                                  length, data);
    if (ret >= 0) {
        return ret;
    }
    return 0;
}
static void usb_dummy_class_init(
             ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    USBDeviceClass *uc = USB_DEVICE_CLASS(klass);
    uc->product_desc   = "A dummy USB Device";
    uc->usb_desc       = &desc_usbdummy;
    uc->init           = usb_dummy_initfn;
    uc->handle_control = usb_dummy_handle_control;
    dc->desc = "Dummy USB Device for QEMU";
    dc->vmsd = &vmstate_usb_dummy;
}
static TypeInfo dummy_info = {
    .name          = "usb-dummy",
    .parent        = TYPE_USB_DEVICE,
    .instance_size = sizeof(USBDummyState),
    .class_init    = usb_dummy_class_init,
};
static void usb_dummy_register_types(void)
{
  type_register_static(&dummy_info);
  usb_legacy_register("usb-dummy", "dummy", NULL);
}
type_init(usb_dummy_register_types)

Όπως βλέπετε ο κώδικας είναι ιδιαίτερα απλός και κατανοητός. Δεν υπάρχουν καθόλου loops, nested ifs και γενικά “αλγόριθμοι” αλλά απλή ανάθεση δεδομένων. Ίσως ο τρόπος που γίνεται η ανάθεση μέσα στις δομές να ξενίσει τους τυχόν αρχάριους στη C. Αν ανήκετε σε αυτή την κατηγορία ψάξτε στο google για “C99 static initializers”. Ίσως παρατηρήσατε ότι το στυλ αυτό προγραμματισμού με τις static δομές και τους function pointers θυμίζει πολύ το πώς ο πυρήνας Linux (και το GObject ως ένα βαθμό) επίσης υλοποιεί το object model του και έχετε δίκιο, ο πυρήνας πιθανότατα ήταν η πηγή έμπνευσης αυτού του μοντέλου.

Η base class του dummy device μας μάλιστα (USBDevice) περιλαμβάνει τον απαραίτητο κώδικα για να στείλει τους descriptors στο host και έτσι απλά την επικαλούμαστε στη μέθοδο

usb_dummy_handle_control

(θυμηθείτε ότι control packets είναι τα πακέτα μέσω των οποίων γίνεται η απαρίθμηση).

Κάτι άλλο που ίσως να ξενίσει είναι η δομή

vmstate_usb_dummy

την οποία όμως μπορείτε να αγνοήσετε προς το παρόν και να την αφήσετε ως έχει (.unmigratable = 1) εκτός αν σας ενδιαφέρει να φτιάχνετε συσκευές που συνεχίζουν να δουλεύουν ακόμα και σε περίπτωση live migration, δηλαδή όταν ένας Virtual Guest αλλάζει φυσικό Host.

Μεταγλώττιση και δοκιμή

Μπορούμε πλέον να δοκιμάσουμε τη νέα μας συσκευή! Για την ανάκτηση του κώδικα του qemu και τη μεταγλώττιση θα χρειαστούμε το git καθώς φυσικά και τα build dependencies του qemu. Υποθέτοντας ότι τρέχετε την τελευταία έκδοση της διανομής σας πιθανότατα μπορείτε απλά να κάνετε κάτι σαν sudo apt-get build-dep / yum-builddep / zypper si -d / κλπ για αυτές (υποθέτω ότι κάποιος που διαβάζει αυτό το άρθρο δε χρειάζεται καθοδήγηση σχετικά με το πώς να εγκαθιστά πακέτα στη διανομή του).

Θα χρειαστούμε ακόμα ένα πράγμα, το οποίο είναι ένα disk image ή livecd ISO το οποίο θα τρέξει μέσα στην εικονική μηχανή που θα περιέχει τη συσκευή μας. Προσωπική μου προτίμηση γι αυτό το άρθρο είναι το GRML, ένα debian-based livecd μόλις 150Mb που όμως περιέχει ssh, σχετικά πρόσφατο kernel (3.0), το πραγματικό lsusb (όχι την έκδοση του busybox όπως άλλα αντίστοιχα livecd) κλπ και άρα είναι παραπάνω από επαρκές για τις ανάγκες μας:

wget http://goo.gl/6gHBc -O grml32.iso

Έχοντας εγκαταστήσει τα προαπαιτούμενα, μπορούμε πλέον να κατεβάσουμε και να μεταγλωττίσουμε τον κώδικα του QEMU:

git clone git://git.qemu.org/qemu.git
git checkout -b usbdemo 71ea2e016131a9fcd
./configure --target-list=x86_64-softmmu &&
make

Το 71ea2e016131a9fcd είναι απλά ένα commit που έχω δοκιμάσει ότι δουλεύει, μια και με git master ποτέ δεν ξέρει κανείς. Μπορείτε κάλλιστα να δοκιμάσετε και με το master όμως (απλά παραλείψτε το βήμα με το git checkout) και πιθανότατα θα δουλέψει μια χαρά επίσης. Όσο για το x86_64-softmmu είναι απλά για εξοικονόμηση χρόνου, μια και αλλιώς ο QEMU θα μεταγλωττίσει όλα τα δυνατά targets. Αν δε σας αρέσει το x86-64 σαν target μπορείτε χωρίς πρόβλημα να το αντικαταστήσετε με i386-softmmu.

Άπαξ και ο QEMU μεταγλωττίστηκε χωρίς πρόβλημα, ήρθε η ώρα να δημιουργήσουμε το αρχείο hw/usb/dev-dummy.c και να του προσθέσουμε τον κώδικα της συσκευής που παραθέσαμε προηγουμένως. Για να μεταγλωττιστεί το αρχείο κάνουμε απλά:

echo 'common-obj-y += dev-dummy.o'  >> hw/usb/Makefile.objs && make

Μπορούμε πλέον να δοκιμάσουμε τη συσκευή μας με:

./x86_64-softmmu/qemu-system-x86_64 \
-cdrom ../grml32.iso -usb \
-usbdevice dummy

όπου αντί για grml32.iso μπορείτε να βάλετε το δικό σας livecd iso, αλλιώς αν έχετε ένα disk image (π.χ., disk.img) μπορείτε να βάλετε -hda disk.img αντί για το -cdrom.

Μέσα από το virtual machine πλέον μπορούμε να τρέξουμε

lsusb -v -d dead:beef

και με λίγη τύχη θα δούμε κάτι σαν την εικόνα.

Αυτό ήταν! Η συσκευή μας αναγνωρίζεται κανονικά! Μπορείτε πλέον να “παίξετε” αλλάζοντας τις παραμέτρους στον κώδικα και βλέποντας το πώς επηρρεάζεται η έξοδος του lsusb μέσα στο VM, όπως και να αρχίσετε να διαβάζετε τον κώδικα των άλλων προσομοιωμένων συσκευών στο φάκελο hw/usb και να εφαρμόσετε όποιες ιδέες θέλετε από αυτές.

Εντάξει, δούλεψε. Και τώρα τι;

Το προφανές επόμενο βήμα θα ήταν π.χ., να φτιάξετε μία USB HID συσκευή (κάτι απλό, π.χ., θερμόμετρο). Σε αυτό θα βοηθήσει πολύ το να διαβάσετε τον κώδικα του αρχείου dev-hid.c το οποίο περιέχει τις ήδη υποστηριζόμενες HID συσκευές (πληκτρολόγιο, ποντίκι, tablet). Όπως θα δείτε, οι HID συσκευές έχουν ένα ακόμα είδος descriptor, τους “report descriptors”. Ο χώρος του άρθρου και πάλι δεν επαρκεί για να μπούμε σε λεπτομέρειες για τις HID συσκευές (δείτε τις πηγές που είπαμε), αλλά μπορούμε να δώσουμε ένα απλό hint:

Οι report descriptors των HID συσκευών περιγράφονται συνήθως με μια τυποποιημένη γλώσσα και έτσι θα τους δείτε σε όλα τα άρθρα. Π.χ., για ένα θερμόμετρο που έχει και ένα LED για “ένδειξη ετοιμότητας”, οι report descriptors θα είναι κάπως σαν το κείμενο στην εικόνα:

Για να τους μετατρέψετε σε πίνακα με αριθμούς όπως αυτοί που υπάρχουν στο dev-hid.c, μπορείτε να χρησιμοποιήσετε το εργαλείο που προσφέρει το ίδιο το USB.org γι αυτή τη δουλειά. Δυστυχώς είναι γραμμένο για windows αλλά δουλεύει μια χαρά στο wine οπότε μικρό το κακό. Από αυτό το εργαλείο μπορείτε μετά απλά να αποθηκεύσετε σε μορφή .h και θα έχετε τον πίνακα που χρειάζεστε. Καλή τύχη!

Από εκεί και πέρα, το μόνο όριο είναι πραγματικά η φαντασία σας. Προσπαθήστε π.χ., να δημιουργήσετε τη δική σας συσκευή “Plug to Own”, βρίσκοντας και αξιοποιώντας τυχόν security holes στο Plug and Play stack κάποιου λειτουργικού (αν το όνομα του οποίου αρχίζει από W μάλλον θα το διασκεδάσετε περισσότερο hint hint). Ή προσπαθήστε να βοηθήσετε στην ανάπτυξη/αποσφαλμάτωση του USB stack κάποιου από τα λειτουργικά που δεν έχουν μεγάλη ομάδα ανάπτυξης (π.χ., Haiku, Plan9, ReactOS κλπ). Ή φτιάξτε κάτι εντελώς δικό σας που κανένας από εμάς δεν είχε φανταστεί καν ότι χρειαζόταν! Το πιο σημαντικό είναι να το διασκεδάσετε!

Όσο προχωράτε βαθύτερα στην ανάπτυξη του QEMU και εικονικού hardware, πιθανότατα θα χρειαστείτε περισσότερες πληροφορίες για το πώς δουλεύει. Για αυτά, εκτός από τις τυπικές πηγές (mailing lists, wikis, google κλπ), πολύ χρήσιμα και κατατοπιστικά είναι τα άρθρα στο blog του Stefan Hajnoczi (π.χ., http://goo.gl/1WS19 και http://goo.gl/nKB8h) που θα σας δώσουν την απαραίτητη “high level” εικόνα ώστε να μπορείτε να διαβάσετε τον κώδικα εύκολα.

 

USB Interfaces και Linux

Η υλοποίηση της λειτουργικότητας μέσω τυποποιημένων Interfaces βοηθά και στη γρηγορότερη υποστήριξη μιας συσκευής στο Linux! Είναι σημαντικό να κατανοήσουμε ότι ένας “class driver” στο Linux συνδέεται όχι με ένα USB Configuration ή Device αλλά με ένα Interface. Αυτό σημαίνει ότι ακόμα και για συσκευές χωρίς πλήρη υποστήριξη, μπορεί να υπάρχει driver για ένα από τα Interfaces. Π.χ., ο γράφων πρόσφατα απέκτησε ένα USB VoIP τηλέφωνο της Crypto (στην πραγματικότητα rebranded από τη Yealink). Το τηλέφωνο αυτό είναι υλοποιημένο ως composite συσκευή με 2 Interfaces: Audio και HID. Χωρίς να χρειαστεί καθόλου κώδικας, το Audio κομμάτι δούλεψε αμέσως με τον usb-audio driver του Linux καθιστώντας τη συσκευή εν μέρει λειτουργική και μειώνοντας την απαιτούμενη ποσότητα reverse engineering! Κάτι παρόμοιο συμβαίνει και με το μετρητή επιπέδων γλυκόζης που αποτελεί το τρέχον reverse engineering project του γράφοντος.

 

Σύνδεσμοι

[1] Linux Coffee HOWTO: goo.gl/5c7a4

[2] Fabrice Bellard, USENIX 2005 QEMU άρθρο: goo.gl/YTKVP

[3] qdev: μοντέλο συσκευών για τον QEMU: goo.gl/R1EPJ και goo.gl/OR2sw

[4] Γράφοντας και δοκιμάζοντας drivers χωρίς το hardware: goo.gl/7IiEn

[5] Διασκεδάζοντας με “Plug to Own” συσκευές: goo.gl/FSN9s

[6] “Usb in a nutshell” δωρεάν html βιβλίο: goo.gl/4emJY

[7] Το USB απλοποιημένο, σειρά άρθρων: goo.gl/O2fvn
[8] Το εργαλείο του USB.org για τους HID report descriptors: goo.gl/0Gzog

 

 

Λίγα λόγια για τον Παντελή Κουκούσουλα

Ο Παντελής ασχολείται με Linux System Administration και προγραμματισμό

 

Δώσε αστέρια!

MO: 5 (ψήφοι: 1)