From bff4ed930c91139b3452b2371c20e2808801d2f9 Mon Sep 17 00:00:00 2001 From: MatMoul Date: Mon, 28 Nov 2016 00:32:39 +0100 Subject: [PATCH] Add spark support preview --- src/classes/Keyboard.cpp | 309 +++++++++++++----- src/classes/Keyboard.h | 13 +- .../g810-spectrum/set_boot_color.pcapng | Bin 0 -> 1052 bytes .../g810-spectrum/set_boot_rainbow.pcapng | Bin 0 -> 1052 bytes .../g810-spectrum/set_keys_colors.pcapng | Bin 0 -> 18512 bytes 5 files changed, 239 insertions(+), 83 deletions(-) create mode 100644 wireshark_dumps/g810-spectrum/set_boot_color.pcapng create mode 100644 wireshark_dumps/g810-spectrum/set_boot_rainbow.pcapng create mode 100644 wireshark_dumps/g810-spectrum/set_keys_colors.pcapng diff --git a/src/classes/Keyboard.cpp b/src/classes/Keyboard.cpp index b87734b..3d4d1e4 100644 --- a/src/classes/Keyboard.cpp +++ b/src/classes/Keyboard.cpp @@ -24,10 +24,15 @@ bool Keyboard::attach() { libusb_device_descriptor desc = {0}; libusb_get_device_descriptor(device, &desc); if (desc.idVendor == 0x046d) { - if (desc.idProduct == 0xc331) { pid = desc.idProduct; break; } // G810 - if (desc.idProduct == 0xc337) { pid = desc.idProduct; break; } // G810 - if (desc.idProduct == 0xc330) { pid = desc.idProduct; break; } // G410 - if (desc.idProduct == 0xc333) { pid = desc.idProduct; break; } // G610 + if (desc.idProduct == 0xc331) { pid = desc.idProduct; break; } // G810 spectrum + if (desc.idProduct == 0xc337) { pid = desc.idProduct; break; } // G810 spectrum + if (desc.idProduct == 0xc330) { pid = desc.idProduct; break; } // G410 spectrum + if (desc.idProduct == 0xc333) { pid = desc.idProduct; break; } // G610 spectrum + if (desc.idProduct == 0xc333) { // G910 spark + pid = desc.idProduct; + kbdProtocol = KeyboardProtocol::spark; + break; + } } } libusb_free_device_list(devs, 1); @@ -74,10 +79,23 @@ bool Keyboard::commit() { if (m_isAttached == false) return false; bool retval = false; unsigned char *data = new unsigned char[20]; - data[0] = 0x11; - data[1] = 0xff; - data[2] = 0x0c; - data[3] = 0x5a; + switch (kbdProtocol) { + case KeyboardProtocol::spectrum: + data[0] = 0x11; + data[1] = 0xff; + data[2] = 0x0c; + data[3] = 0x5a; + break; + case KeyboardProtocol::spark: + data[0] = 0x11; + data[1] = 0xff; + data[2] = 0x0c; //Need change + data[3] = 0x5a; //Need change + break; + default: + return false; + break; + } for(int i = 4; i < 20; i++) data[i] = 0x00; retval = sendDataInternal(data, 20); delete[] data; @@ -90,6 +108,10 @@ bool Keyboard::getKeyAddress(Key key, KeyAddress &keyAddress) { keyAddress.addressGroup = KeyAddressGroup::logo; keyAddress.id = 0x01; break; + case Key::logo2: + keyAddress.addressGroup = KeyAddressGroup::logo; + keyAddress.id = 0x02; + break; case Key::backlight: keyAddress.addressGroup = KeyAddressGroup::indicators; keyAddress.id = 0x01; @@ -130,6 +152,42 @@ bool Keyboard::getKeyAddress(Key key, KeyAddress &keyAddress) { keyAddress.addressGroup = KeyAddressGroup::multimedia; keyAddress.id = 0xe2; break; + case Key::g1: + keyAddress.addressGroup = KeyAddressGroup::gkeys; + keyAddress.id = 0x00; // Need change + break; + case Key::g2: + keyAddress.addressGroup = KeyAddressGroup::gkeys; + keyAddress.id = 0x00; // Need change + break; + case Key::g3: + keyAddress.addressGroup = KeyAddressGroup::gkeys; + keyAddress.id = 0x00; // Need change + break; + case Key::g4: + keyAddress.addressGroup = KeyAddressGroup::gkeys; + keyAddress.id = 0x00; // Need change + break; + case Key::g5: + keyAddress.addressGroup = KeyAddressGroup::gkeys; + keyAddress.id = 0x00; // Need change + break; + case Key::g6: + keyAddress.addressGroup = KeyAddressGroup::gkeys; + keyAddress.id = 0x00; // Need change + break; + case Key::g7: + keyAddress.addressGroup = KeyAddressGroup::gkeys; + keyAddress.id = 0x00; // Need change + break; + case Key::g8: + keyAddress.addressGroup = KeyAddressGroup::gkeys; + keyAddress.id = 0x00; // Need change + break; + case Key::g9: + keyAddress.addressGroup = KeyAddressGroup::gkeys; + keyAddress.id = 0x00; // Need change + break; default: keyAddress.addressGroup = KeyAddressGroup::keys; switch (key) { @@ -257,6 +315,7 @@ bool Keyboard::parseKey(std::string key, KeyAddress &keyAddress) { std::transform(key.begin(), key.end(), key.begin(), ::tolower); Key parsedKey; if (key == "logo") parsedKey = Key::logo; + else if (key == "logo2") parsedKey = Key::logo2; else if (key == "back_light" || key == "backlight" || key == "light") parsedKey = Key::backlight; else if (key == "game_mode" || key == "gamemode" || key == "game") parsedKey = Key::game; else if (key == "caps_indicator" || key == "capsindicator" || key == "caps") parsedKey = Key::caps; @@ -375,6 +434,15 @@ bool Keyboard::parseKey(std::string key, KeyAddress &keyAddress) { else if (key == "alt_right" || key == "altright" || key == "altr" || key == "altgr") parsedKey = Key::alt_right; else if (key == "win_right" || key == "winright" || key == "winr") parsedKey = Key::win_right; else if (key == "meta_right" || key == "metaright" || key == "metar") parsedKey = Key::win_right; + else if (key == "g1") parsedKey = Key::g1; + else if (key == "g2") parsedKey = Key::g2; + else if (key == "g3") parsedKey = Key::g3; + else if (key == "g4") parsedKey = Key::g4; + else if (key == "g5") parsedKey = Key::g5; + else if (key == "g6") parsedKey = Key::g6; + else if (key == "g7") parsedKey = Key::g7; + else if (key == "g8") parsedKey = Key::g8; + else if (key == "g9") parsedKey = Key::g9; else return false; return getKeyAddress(parsedKey, keyAddress); } @@ -389,6 +457,7 @@ bool Keyboard::parseKeyGroup(std::string key, KeyGroup &keyGroup) { else if (key == "numeric") keyGroup = KeyGroup::numeric; else if (key == "functions") keyGroup = KeyGroup::functions; else if (key == "keys") keyGroup = KeyGroup::keys; + else if (key == "gkeys") keyGroup = KeyGroup::gkeys; else return false; return true; } @@ -413,46 +482,110 @@ bool Keyboard::sendDataInternal(unsigned char *data, int data_size) { } bool Keyboard::populateAddressGroupInternal(KeyAddressGroup addressGroup, unsigned char *data) { - switch (addressGroup) { - case KeyAddressGroup::logo: - data[0] = 0x11; // Base address - data[1] = 0xff; // Base address - data[2] = 0x0c; // Base address - data[3] = 0x3a; // Base address - data[4] = 0x00; // Base address - data[5] = 0x10; // Base address - data[6] = 0x00; // Base address - data[7] = 0x01; // Base address + switch (kbdProtocol) { + case KeyboardProtocol::spectrum: + switch (addressGroup) { + case KeyAddressGroup::logo: + data[0] = 0x11; // Base address + data[1] = 0xff; // Base address + data[2] = 0x0c; // Base address + data[3] = 0x3a; // Base address + data[4] = 0x00; // Base address + data[5] = 0x10; // Base address + data[6] = 0x00; // Base address + data[7] = 0x01; // Base address + break; + case KeyAddressGroup::indicators: + data[0] = 0x12; // Base address + data[1] = 0xff; // Base address + data[2] = 0x0c; // Base address + data[3] = 0x3a; // Base address + data[4] = 0x00; // Base address + data[5] = 0x40; // Base address + data[6] = 0x00; // Base address + data[7] = 0x05; // Base address + break; + case KeyAddressGroup::multimedia: + data[0] = 0x12; // Base address + data[1] = 0xff; // Base address + data[2] = 0x0c; // Base address + data[3] = 0x3a; // Base address + data[4] = 0x00; // Base address + data[5] = 0x02; // Base address + data[6] = 0x00; // Base address + data[7] = 0x05; // Base address + break; + case KeyAddressGroup::keys: + data[0] = 0x12; // Base address + data[1] = 0xff; // Base address + data[2] = 0x0c; // Base address + data[3] = 0x3a; // Base address + data[4] = 0x00; // Base address + data[5] = 0x01; // Base address + data[6] = 0x00; // Base address + data[7] = 0x0e; // Base address + break; + default: + return false; + break; + } break; - case KeyAddressGroup::indicators: - data[0] = 0x12; // Base address - data[1] = 0xff; // Base address - data[2] = 0x0c; // Base address - data[3] = 0x3a; // Base address - data[4] = 0x00; // Base address - data[5] = 0x40; // Base address - data[6] = 0x00; // Base address - data[7] = 0x05; // Base address - break; - case KeyAddressGroup::multimedia: - data[0] = 0x12; // Base address - data[1] = 0xff; // Base address - data[2] = 0x0c; // Base address - data[3] = 0x3a; // Base address - data[4] = 0x00; // Base address - data[5] = 0x02; // Base address - data[6] = 0x00; // Base address - data[7] = 0x05; // Base address - break; - case KeyAddressGroup::keys: - data[0] = 0x12; // Base address - data[1] = 0xff; // Base address - data[2] = 0x0c; // Base address - data[3] = 0x3a; // Base address - data[4] = 0x00; // Base address - data[5] = 0x01; // Base address - data[6] = 0x00; // Base address - data[7] = 0x0e; // Base address + case KeyboardProtocol::spark: + switch (addressGroup) { + case KeyAddressGroup::logo: + data[0] = 0x11; // Base address + data[1] = 0xff; // Base address + data[2] = 0x0c; // Base address + data[3] = 0x3a; // Base address + data[4] = 0x00; // Base address + data[5] = 0x10; // Base address + data[6] = 0x00; // Base address + data[7] = 0x01; // Base address + break; + case KeyAddressGroup::indicators: + data[0] = 0x12; // Base address + data[1] = 0xff; // Base address + data[2] = 0x0c; // Base address + data[3] = 0x3a; // Base address + data[4] = 0x00; // Base address + data[5] = 0x40; // Base address + data[6] = 0x00; // Base address + data[7] = 0x05; // Base address + break; + case KeyAddressGroup::multimedia: + data[0] = 0x12; // Base address + data[1] = 0xff; // Base address + data[2] = 0x0c; // Base address + data[3] = 0x3a; // Base address + data[4] = 0x00; // Base address + data[5] = 0x02; // Base address + data[6] = 0x00; // Base address + data[7] = 0x05; // Base address + break; + case KeyAddressGroup::keys: + data[0] = 0x12; // Base address + data[1] = 0xff; // Base address + data[2] = 0x0c; // Base address + data[3] = 0x3a; // Base address + data[4] = 0x00; // Base address + data[5] = 0x01; // Base address + data[6] = 0x00; // Base address + data[7] = 0x0e; // Base address + break; + case KeyAddressGroup::gkeys: + data[0] = 0x12; // Base address + data[1] = 0xff; // Base address + data[2] = 0x0c; // Base address + data[3] = 0x3a; // Base address + data[4] = 0x00; // Base address + data[5] = 0x01; // Base address + data[6] = 0x00; // Base address + data[7] = 0x0e; // Base address + break; + default: + return false; + break; + } break; default: return false; @@ -562,11 +695,13 @@ bool Keyboard::setKeys(KeyValue keyValue[], int keyValueCount) { int multimediaCount = 0; KeyValue keys[200]; int keysCount = 0; + KeyValue gkeys[200]; + int gkeysCount = 0; for (int i = 0; i < keyValueCount; i++) { if(keyValue[i].key.addressGroup == KeyAddressGroup::logo) { - logo[0] = keyValue[i]; - logoCount = 1; + logo[logoCount] = keyValue[i]; + logoCount++; } else if(keyValue[i].key.addressGroup == KeyAddressGroup::indicators) { indicators[indicatorsCount] = keyValue[i]; indicatorsCount++; @@ -576,10 +711,13 @@ bool Keyboard::setKeys(KeyValue keyValue[], int keyValueCount) { } else if(keyValue[i].key.addressGroup == KeyAddressGroup::keys) { keys[keysCount] = keyValue[i]; keysCount++; + } else if(keyValue[i].key.addressGroup == KeyAddressGroup::gkeys) { + gkeys[gkeysCount] = keyValue[i]; + gkeysCount++; } } - if (logoCount > 0) setKey(logo[logoCount - 1]); + if (logoCount > 0) setKeysInternal(KeyAddressGroup::logo, logo, logoCount); if (indicatorsCount > 0) setKeysInternal(KeyAddressGroup::indicators, indicators, indicatorsCount); @@ -598,16 +736,18 @@ bool Keyboard::setKeys(KeyValue keyValue[], int keyValueCount) { } } + if (gkeysCount > 0) setKeysInternal(KeyAddressGroup::gkeys, gkeys, gkeysCount); + return true; } bool Keyboard::setAllKeys(KeyColors colors) { - KeyValue keyValues[117]; - for (int i = 0; i < 117; i++) { + KeyValue keyValues[127]; + for (int i = 0; i < 127; i++) { getKeyAddress((Key)i, keyValues[i].key); keyValues[i].colors = colors; } - setKeys(keyValues, 117); + setKeys(keyValues, 127); return true; } @@ -616,68 +756,81 @@ bool Keyboard::setGroupKeys(KeyGroup keyGroup, KeyColors colors) { int keyValuesCount = 0; switch (keyGroup) { case KeyGroup::logo: - setKey(Key::logo, colors); + for (int i = 0; i < 2; i++) { + getKeyAddress((Key)i, keyValues[i].key); + keyValues[i].colors = colors; + keyValuesCount++; + } + setKeys(keyValues, keyValuesCount); break; case KeyGroup::indicators: - for (int i = 1; i < 6; i++) { - getKeyAddress((Key)i, keyValues[i - 1].key); - keyValues[i - 1].colors = colors; + for (int i = 2; i < 7; i++) { + getKeyAddress((Key)i, keyValues[i - 2].key); + keyValues[i - 2].colors = colors; keyValuesCount++; } setKeys(keyValues, keyValuesCount); break; case KeyGroup::multimedia: - for (int i = 6; i < 11; i++) { - getKeyAddress((Key)i, keyValues[i - 6].key); - keyValues[i - 6].colors = colors; + for (int i = 7; i < 12; i++) { + getKeyAddress((Key)i, keyValues[i - 7].key); + keyValues[i - 7].colors = colors; keyValuesCount++; } setKeys(keyValues, keyValuesCount); break; case KeyGroup::fkeys: - for (int i = 11; i < 23; i++) { - getKeyAddress((Key)i, keyValues[i - 11].key); - keyValues[i - 11].colors = colors; + for (int i = 12; i < 24; i++) { + getKeyAddress((Key)i, keyValues[i - 12].key); + keyValues[i - 12].colors = colors; keyValuesCount++; } setKeys(keyValues, keyValuesCount); break; case KeyGroup::modifiers: - for (int i = 23; i < 32; i++) { - getKeyAddress((Key)i, keyValues[i - 23].key); - keyValues[i - 23].colors = colors; + for (int i = 24; i < 33; i++) { + getKeyAddress((Key)i, keyValues[i - 24].key); + keyValues[i - 24].colors = colors; keyValuesCount++; } setKeys(keyValues, keyValuesCount); break; case KeyGroup::arrows: - for (int i = 32; i < 36; i++) { - getKeyAddress((Key)i, keyValues[i - 32].key); - keyValues[i - 32].colors = colors; + for (int i = 33; i < 37; i++) { + getKeyAddress((Key)i, keyValues[i - 33].key); + keyValues[i - 33].colors = colors; keyValuesCount++; } setKeys(keyValues, keyValuesCount); break; case KeyGroup::numeric: - for (int i = 36; i < 53; i++) { - getKeyAddress((Key)i, keyValues[i - 36].key); - keyValues[i - 36].colors = colors; + for (int i = 37; i < 54; i++) { + getKeyAddress((Key)i, keyValues[i - 37].key); + keyValues[i - 37].colors = colors; keyValuesCount++; } setKeys(keyValues, keyValuesCount); break; case KeyGroup::functions: - for (int i = 53; i < 63; i++) { - getKeyAddress((Key)i, keyValues[i - 53].key); - keyValues[i - 53].colors = colors; + for (int i = 54; i < 64; i++) { + getKeyAddress((Key)i, keyValues[i - 54].key); + keyValues[i - 54].colors = colors; keyValuesCount++; } setKeys(keyValues, keyValuesCount); break; case KeyGroup::keys: - for (int i = 63; i < 117; i++) { - getKeyAddress((Key)i, keyValues[i - 63].key); - keyValues[i - 63].colors = colors; + for (int i = 64; i < 118; i++) { + getKeyAddress((Key)i, keyValues[i - 64].key); + keyValues[i - 64].colors = colors; + keyValuesCount++; + } + setKeys(keyValues, keyValuesCount); + break; + case KeyGroup::gkeys: + for (int i = 118; i < 127; i++) { + getKeyAddress((Key)i, keyValues[i - 118].key); + keyValues[i - 118].colors = colors; keyValuesCount++; } setKeys(keyValues, keyValuesCount); diff --git a/src/classes/Keyboard.h b/src/classes/Keyboard.h index f4dfbb4..25a88c3 100644 --- a/src/classes/Keyboard.h +++ b/src/classes/Keyboard.h @@ -8,10 +8,11 @@ class Keyboard { public: + enum class KeyboardProtocol { spectrum, spark }; enum class PowerOnEffect { rainbow, color }; - enum class KeyAddressGroup { logo, indicators, multimedia, keys }; - enum class Key { // 117 items - logo, + enum class KeyAddressGroup { logo, indicators, multimedia, keys, gkeys }; + enum class Key { // 127 items + logo, logo2, caps, num, scroll, game, backlight, mute, play, stop, prev, next, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, @@ -26,9 +27,10 @@ class Keyboard { tilde, minus, equal, open_bracket, close_bracket, backslash, semicolon, quote, dollar, - intl_backslash, comma, period, slash + intl_backslash, comma, period, slash, + g1, g2, g3, g4, g5, g6, g7, g8, g9 }; - enum class KeyGroup { logo, indicators, multimedia, fkeys, modifiers, arrows, numeric, functions, keys}; + enum class KeyGroup { logo, indicators, multimedia, fkeys, modifiers, arrows, numeric, functions, keys, gkeys}; struct KeyColors { char red; char green; char blue; }; struct KeyAddress { KeyAddressGroup addressGroup; char id; }; @@ -55,6 +57,7 @@ class Keyboard { bool m_isAttached = false; bool m_isKernellDetached = false; + KeyboardProtocol kbdProtocol = KeyboardProtocol::spectrum; libusb_device **devs; libusb_device_handle *dev_handle; libusb_context *ctx = NULL; diff --git a/wireshark_dumps/g810-spectrum/set_boot_color.pcapng b/wireshark_dumps/g810-spectrum/set_boot_color.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..ca2d889b0ce7796dadc5e9c7cdb3f4b730094853 GIT binary patch literal 1052 zcmbVLK}!Nb6n-Y+nkG79QqriUn3DUAvQiGmJwuUpW; zbN@i9pV6g1&_9Unz1dk^EGl~NcHVn4^L_KpybXpz!59GKt=S2Nqa%%Kl;_Z2(E(K8 zFbvJ3Qnh9;KmcclusQ-^SlzRmCuVXkHJ6Mh;%60Gtata)nxf1*NevR*(9@WcckgfS z*p=fHc$+H#*3%-L)#TxlkAk3MFF!5Q~ z32gzBmn!QG90%_)WzF<^Lvm(0amJTD&Xi&Z1mgS2ia0sg8$8$7)PCz+Q14~gIpOy- zaifp#=b(QOQ&YJS*`Ex)H&$l-zbe4x+3E!?o%<=C6?BU+yZfY0@$9D`~Uy| literal 0 HcmV?d00001 diff --git a/wireshark_dumps/g810-spectrum/set_boot_rainbow.pcapng b/wireshark_dumps/g810-spectrum/set_boot_rainbow.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..61e3c8eb257af54a466a29b211db0beedcef9ba6 GIT binary patch literal 1052 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