Repository: JSubelj/g910-gkey-macro-support (language: python)

Issue:
[Issue] Line ending with \n is not working with keymapping de
**Describe the bug**
When I setup a GKey to typeout "\n/remaining\n", I get the following error:

2023-08-28 19:28:48,138 [WARNING] {lib.data_mappers.config_reader} - Error(s) in config: {'m1': {'g5': {'do': {'typeout': 'Character \n on position 10 for keyboard mapping: de does not exist! '}}}}

When I change the keymapping to en it works, but then the "/"-character is identified as "-"-character. I didn't found out which character I need on the German keyboard to let the application identify it as "/"-character.

**To Reproduce**
Steps to reproduce the behavior:
1. Open configuration
2. change keyboard_mapping to "de"
3. Set a gkey to the following action:
      "g5": {
        "hotkey_type": "typeout",
        "do": "\n/remaining\n"
        }
4. See error in logfile

**Expected behavior**
It should work the same way as with keymapping in en.

**Screenshots**
https://imgur.com/a/otdStN4

**Additional info**
 - did you run in daemon or normal mode -> both
 - logs from /var/log/g910-gkeys.log  -> See screenshot above
 - logs from journalctl -u g910-gkeys  -> See screenshot above
 - configuration in /etc/g910-gkeys/config.json  --> https://pastebin.com/7FMXwtP0

**Desktop (please complete the following information):**
 - OS: Arch Linux
 - Kernel version 6.4.12-zen1-1-zen
 - desktop enviroment KDE Plasma 5.27.7
 - g910-gkeys version g910-gkeys-git 0.2.5.r0.g600610b-1
 - python version Python 3.11.3

**Additional context**
Add any other context about the problem here.



Discussion / hints:

Fix patch:
diff --git a/.ON_UPDATE_UPDATE b/.ON_UPDATE_UPDATE
index c171b49..6a71d96 100644
--- a/.ON_UPDATE_UPDATE
+++ b/.ON_UPDATE_UPDATE
@@ -2,8 +2,6 @@ If you are not me, you can ignore this file.
 
 Raise the version number in:
     - reboot and try
-    - README.md
-    - wiki/HOME.md
     - PROJECT_INFO.py
     - PKGBUILD
 
@@ -12,7 +10,6 @@ Update:
     - PKGBUILD md5 hash
 
 Before release:
-    - Check documentation for correct version numbers
     - Check PKG-INFO (generated with python setup.py build) for correct version numbers
     - Check g910-gkeys --version
     - Check pacman for correct version numbers
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index d652c6b..055c05f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,8 +10,15 @@ install_and_run.sh
 wiki/
 g910_gkeys.egg-info/
 g910-gkey-macro-support.wiki/
+/config/
+notes
 
 # build
 /build/
 /.pybuild/
 /debian/
+
+*.log
+
+# layout helper
+/char_uinput_mapping_*
\ No newline at end of file
diff --git a/README.md b/README.md
index 0322988..084a3c6 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,48 @@
-# Logitech G910 keyboard gkey support for GNU/Linux (Project version: v0.2.5)
+# Logitech G910 keyboard gkey support for GNU/Linux (Project version: v0.3.0)
 
 Because I didn't find any GKey support for Logitech G910 keyboard I decided to create this GKey mapper.
 Code is based on an [issue](https://github.com/CReimer/g910-gkey-uinput/issues/3)
 in [g910-gkey-uinput](https://github.com/CReimer/g910-gkey-uinput) project. I expanded the code, so that it is more 
-user friendly to add functionality to GKeys.
+user-friendly to add functionality to GKeys.
 
 Everything is described in great depth (and actually much better) on [WIKI](https://github.com/JSubelj/g910-gkey-macro-support/wiki).
 
-Note: From version 0.2.0 onwards the g810-led controller is no longer required because Gkey to Fkey mapping is disabled inside the driver.
-
 ## Requirements
 
 To use this project you need:
  - Python >=3.7
- - git
  - uinput kernel module (more on this [here](http://tjjr.fi/sw/python-uinput/#Usage))
  - pip requirements are stored in requirements.txt
- - g810-led for color changing per profile, https://github.com/MatMoul/g810-led
  
+#### Note: 
+From version 0.2.0 onwards the g810-led controller is no longer required because G-key to F-key mapping is disabled inside the driver.
+
 ## Installation \[[AUR](https://aur.archlinux.org/packages/g910-gkeys-git/)\]
- - ~~install [g810-led-git](https://github.com/MatMoul/g810-led) for your distro (for Arch based distros: [aur](https://aur.archlinux.org/packages/g810-led-git/))~~
- - ~~disable Gkeys to Fkeys mapping: `g910-led -gkm 1` (probably will include that on program start)~~
  - load uinput kernel module: `modprobe uinput` (on Manjaro is loaded by default afaik) 
- - clone repo: `git clone https://github.com/JSubelj/g910-gkey-macro-support.git`
- - move to cloned repo: `cd g910-gkey-macro-support`
+ - clone repo:  
+   ```
+   git clone https://github.com/JSubelj/g910-gkey-macro-support.git
+   ```  
+   or download & unzip:  
+   ```
+   wget https://github.com/JSubelj/g910-gkey-macro-support/archive/refs/heads/master.zip; unzip master.zip
+   ```
+ - move to driver directory: `cd g910-gkey-macro-support`
  - run the installer shell: `chmod +x installer.sh; sudo ./installer.sh`
  - This will install the module, set a command `g910-gkeys`, add a service file to `/usr/lib/systemd/system/g910-gkeys.service` or `/lib/systemd/system/g910-gkeys.service`, install a default configuration file in `/etc/g910-gkeys/config.json` if none exists, and finally enable and start this service.
 
-If you do not want to enable g910-gkeys automatically, use the `-n` switch: `sudo ./installer.sh -n`. You will have to do it yourself in this case: `sudo systemctl enable --now g910-gkeys.service`.
+If you do not want to enable g910-gkeys automatically, use the `-n` switch: `sudo ./installer.sh -n`. If you want to enable it later you can use: `sudo systemctl enable --now g910-gkeys.service`. To just start the service one till next reboot use: `sudo systemctl start g910-gkeys`
  
 ## Update
  If you update from version <= v0.2.4 you will need to make some manual changes to your `/etc/g910-gkeys/config.json` to make use of the new gkey profile feature.
  You don't need to do anything if you don't want to use the profile feature your old config will be loaded as default profile m1.
 
 ## Uninstalling
-Run the uninstalling script: `chmod +x uninstall.sh; sudo ./uninstall.sh`. If you prefer to do it manually, these are the commands :
+### By script (recommended)
+Run the uninstalling script: `chmod +x uninstall.sh; sudo ./uninstall.sh`. 
+
+### Manual uninstall
+If you prefer to do it manually, these are the commands :
 
  1. Disable and stop the service: `sudo systemctl disable --now g910-gkeys`
  2. Remove installed files (list is in files.txt): `sudo xargs --arg-file=files.txt rm -rf`
@@ -55,12 +63,15 @@ configuration can be found in docs folder: [ex_config](docs/ex_config/ex_config.
  * `"fr"` - french
  * `"si"` - slovenian
 
+Even if your language is not supported you can still use the driver with hotkey_type run. Other types won't work as expected with the wrong language configured.
+
 ### Hotkey types
 The mapper supports five types of hotkeys (also described in [hotkey_types.txt](docs/hotkey_types.txt)):
  * `"typeout"` - Type out (ex. clicking on GKey types out a string)
  * `"shortcut"` - Shortcuts (ex. clicking on GKey presses shift+f4)
  * `"run"` - Starting a program (anything you can start from shell)
  * `"python"` - Execute a snippet of Python code and print provided output (see ex. below)
+ * `"uinput"` - Direct mapping with uinput.KEY_XY
  * `"nothing"` - Do nothing (unbound key)
 
 To add a hotkey add to `config.json` the following code:
@@ -73,11 +84,14 @@ To add a hotkey add to `config.json` the following code:
 
 Depending on the hotkey command, the syntax for "do" is different (supported characters for typeout and 
 hotkeys are listed in [supported_keys.txt](docs/supported_keys.txt)):
- * `"typeout"` - Typeout syntax is same as you would type text out (ex. "tyPe Me Out!")
+ * `"typeout"` - Type out syntax is same as you would type text out (ex. "tyPe Me Out!")
  * `"shortcut"` - Shortcuts are separated by a plus sign and a comma (ex. "ctrl+c,ctrl+v")
  * `"nothing"` - If `hotkey_type` is set to `"nothing"` then "do" key need not exist or can be anything.
  * `"run"` - Run has the same syntax as you would type a cli program in command line (ex. "systemctl daemon-reload")
  * `"python"` - A Python one-line script. If output is desired, the script should define a global variable named `output_string` and set it to the string to be output.
+ * `"uinput"` - A key defined by python3-uinput to emit (ex. KEY_F13 or KEY_KATAKANA)
+
+**Important: Commands and python code is executed as root**
 
 ### Profiles
 There are four profiles you can use and set up different gkey macros in config. Select the profile with M[1-3|R] key on your keyboard.
@@ -86,29 +100,53 @@ If you use the profile feature you can also use the following parameter:
  * `username` - Username to send notifications with (required if notify is used)
  * `profiles` - Define a profile which is bound to the matching mkey
 
-The following example shows how to set slovenian layout and define the g1 key for profile m1 and m2 and shows how to run a app like firefox, chrome or similar. Please replace **\<username\>** with your username.
+The following example shows how to set slovenian layout and define the g1-5 macro keys for profile m1 and m2 and shows an example of every `hotkey_type`.
  ```
 {
     "notify": "True",
     "username": "<username>",
     "keyboard_mapping": "si",
     "profiles": {
-        "m1": {
-            "g1": {
+        "MEMORY_1": {
+            "MACRO_1": {
+                "hotkey_type": "uinput",
+                "do": "KEY_F13"
+            },
+            "MACRO_2": {
+                "hotkey_type": "shortcut",
+                "do": "ctrl+c"
+            },
+            "MACOR_3": {
+                "hotkey_type": "typeout",
+                "do": "averylongemailadressyoudontwanttowriteoutyourself@gmail.com"
+            },
+            "MACRO_4": {
                 "hotkey_type": "run",
-                "do": "su <username> -c 'DISPLAY=:0 nohup firefox' & 2>&1 > /dev/null"
+                "do": "/path/script_run_by_root"
             },
-            "g2": {
+            "MACRO_5": {
                 "hotkey_type": "python",
                 "do": "import datetime; global output_string; output_string = datetime.now().strftime('%Y-%m-%d %H:%M:%S')"
             }
         },
-        "m2": {
-            "g1": {
+        "MEMORY_2": {
+            "MACRO_1": {
+                "hotkey_type": "uinput",
+                "do": "KEY_KATAKANA"
+            },
+            "MACRO_2": {
+                "hotkey_type": "shortcut",
+                "do": "ctrl+v"
+            },
+            "MACRO_3": {
+                "hotkey_type": "typeout",
+                "do": "ssh -p1234 -i ~/.ssh/id_rsa user@domain\n"
+            },
+            "MACRO_4": {
                 "hotkey_type": "run",
-                "do": "su <username> -c 'DISPLAY=:0 nohup chrome' & 2>&1 > /dev/null"
+                "do": "su <username> -c 'DISPLAY=:0 nohup firefox' & 2>&1 > /dev/null"
             },
-            "g2": {
+            "MACRO_5": {
                 "hotkey_type": "python",
                 "do": "import datetime; global output_string; output_string = datetime.datetime.now().isoformat()"
             }
@@ -116,10 +154,10 @@ The following example shows how to set slovenian layout and define the g1 key fo
     }
 }
  ```
+To run an app like firefox, chrome or similar you need to replace **\<username\>** with your username. The DISPLAY variable defines the screen to open the app on a multi-screen-setup.
 
 The code is tested on Logitech G910 keyboard with
-- OS: Manjaro, 4.19.1 Linux kernel, DE: kde plasma 5.14.3
-- OS: Ubuntu 20.04.5 LTS, Linux 5.4.0-131-generic, GNOME: 3.36.8
+- OS: Ubuntu 20.04.6 LTS, Linux 5.4.0-159-generic, GNOME: 3.36.9
 
 ### Disclaimer
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
diff --git a/cli_entry_point.py b/cli_entry_point.py
index 6e2a3d4..ab37c35 100644
--- a/cli_entry_point.py
+++ b/cli_entry_point.py
@@ -3,31 +3,30 @@
 
 import argparse
 from lib import g910_gkey_mapper, PROJECT_INFO
+from lib.misc.config import Config
+
 
 def main():
     parser = argparse.ArgumentParser(description=PROJECT_INFO.DESCRIPTION)
-    parser.add_argument("--create-config", help="Creates config in /etc/g910-gkeys", action='store_true', default=False)
-    parser.add_argument("-c","--set-config", help="Set the config file to use", default='/etc/g910-gkeys/config.json', dest="config_file")
-    parser.add_argument("-v","--version", help="Displays the information about the driver", action='store_true', default=False)
+    parser.add_argument("--create-config", help="Creates config in /etc/g910-gkeys",
+                        action='store_true', default=False)
+    parser.add_argument("-s", "--set-config", help="Set the config file to use",
+                        default='/etc/g910-gkeys/config.json', dest="config_file")
+    parser.add_argument("-v", "--version", help="Displays the information about the driver",
+                        action='version', version=f"%(prog)s {PROJECT_INFO.VERSION} by {PROJECT_INFO.AUTHOR}")
     args = parser.parse_args()
     if args.create_config:
-        from lib.misc import create_config
-
-        create_config.create()
-    elif args.version:
-        print(PROJECT_INFO.NAME)
-        print()
-        print(PROJECT_INFO.DESCRIPTION)
-        print()
-        print("Created by", PROJECT_INFO.AUTHOR)
-        print("Version", PROJECT_INFO.VERSION)
+        from lib.usb_device import USBDevice
+        config = Config()
+        device = USBDevice()  # init usb device and keyboard interface
+        config.create(device.keyboard)  # create config with keyboard interface
+        device.__exit__()  # clean up usb connection
     elif args.config_file != "/etc/g910-gkeys/config.json":
-        from lib.misc import paths
-
-        paths.config_path = args.config_file
+        Config.config_path = args.config_file
         g910_gkey_mapper.main()
     else:
         g910_gkey_mapper.main()
 
-if __name__=="__main__":
+
+if __name__ == "__main__":
     main()
diff --git a/config/config.json b/config/config.json
deleted file mode 100644
index 22f821a..0000000
--- a/config/config.json
+++ /dev/null
@@ -1,76 +0,0 @@
-{
-  "__comment" : "following hotkey types are supported: nothing, typeout, shortcut and run; only en, fr, de and si keyboard mappings are currently supported",
-  "keyboard_mapping": "en",
-  "notify": "True",
-  "username": "",
-  "profiles": {
-    "m1": {
-      "g1": {
-        "hotkey_type": "typeout",
-        "do": "This is profile m1"
-      },
-      "g2": {
-        "hotkey_type": "shortcut",
-        "do": "alt+F4"
-      },
-      "g3": {
-        "hotkey_type": "run",
-        "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
-      },
-      "g4": {
-        "hotkey_type": "nothing"
-      }
-    },
-    "m2": {
-      "g1": {
-        "hotkey_type": "typeout",
-        "do": "This is profile m2"
-      },
-      "g2": {
-        "hotkey_type": "shortcut",
-        "do": "alt+F4"
-      },
-      "g3": {
-        "hotkey_type": "run",
-        "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
-      },
-      "g4": {
-        "hotkey_type": "nothing"
-      }
-    },
-    "m3": {
-      "g1": {
-        "hotkey_type": "typeout",
-        "do": "This is profile m3"
-      },
-      "g2": {
-        "hotkey_type": "shortcut",
-        "do": "alt+F4"
-      },
-      "g3": {
-        "hotkey_type": "run",
-        "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
-      },
-      "g4": {
-        "hotkey_type": "nothing"
-      }
-    },
-    "mr": {
-      "g1": {
-        "hotkey_type": "typeout",
-        "do": "This is profile mr"
-      },
-      "g2": {
-        "hotkey_type": "shortcut",
-        "do": "alt+F4"
-      },
-      "g3": {
-        "hotkey_type": "run",
-        "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
-      },
-      "g4": {
-        "hotkey_type": "nothing"
-      }
-    }
-  }
-}
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 079750f..d596471 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -1,4 +1,4 @@
-As of version 0.2.5 the default language set in keyboard_mapping is `en`. In earlier versions the default keyboard_mapping was set to `si` for slovene, because the author of this package is from [Slovenia](https://en.wikipedia.org/wiki/Slovenia). If you have an other keyboard layout configured in your system you need to change this in the configuration accordingly.
+As of version 0.2.5 the default language set in keyboard_mapping is `en`. In earlier versions the default keyboard_mapping was set to `si` for slovene, because the author of this package is from [Slovenia](https://en.wikipedia.org/wiki/Slovenia). If you have another keyboard layout configured in your system you need to change this in the configuration accordingly.
 
 ## Location
 Config created on setup is located in: `/etc/g910-gkeys/config.json`
@@ -12,71 +12,71 @@ The driver is set up so that it will work without any data in config (using the
   "notify": "True",
   "username": "",
   "profiles": {
-    "m1": {
-      "g1": {
+    "MEMORY_1": {
+      "MACRO_1": {
         "hotkey_type": "typeout",
         "do": "This is profile m1"
       },
-      "g2": {
+      "MACRO_2": {
         "hotkey_type": "shortcut",
         "do": "alt+F4"
       },
-      "g3": {
+      "MACRO_3": {
         "hotkey_type": "run",
         "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
       },
-      "g4": {
+      "MACRO_4": {
         "hotkey_type": "nothing"
       }
     },
-    "m2": {
-      "g1": {
+    "MEMORY_2": {
+      "MACRO_1": {
         "hotkey_type": "typeout",
         "do": "This is profile m2"
       },
-      "g2": {
+      "MACRO_2": {
         "hotkey_type": "shortcut",
         "do": "alt+F4"
       },
-      "g3": {
+      "MACRO_3": {
         "hotkey_type": "run",
         "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
       },
-      "g4": {
+      "MACRO_4": {
         "hotkey_type": "nothing"
       }
     },
-    "m3": {
-      "g1": {
+    "MEMORY_3": {
+      "MACRO_1": {
         "hotkey_type": "typeout",
         "do": "This is profile m3"
       },
-      "g2": {
+      "MACRO_2": {
         "hotkey_type": "shortcut",
         "do": "alt+F4"
       },
-      "g3": {
+      "MACRO_3": {
         "hotkey_type": "run",
         "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
       },
-      "g4": {
+      "MACRO_4": {
         "hotkey_type": "nothing"
       }
     },
-    "mr": {
-      "g1": {
+    "MEMORY_RECORD": {
+      "MACRO_1": {
         "hotkey_type": "typeout",
         "do": "This is profile mr"
       },
-      "g2": {
+      "MACRO_2": {
         "hotkey_type": "shortcut",
         "do": "alt+F4"
       },
-      "g3": {
+      "MACRO_3": {
         "hotkey_type": "run",
         "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
       },
-      "g4": {
+      "MACRO_4": {
         "hotkey_type": "nothing"
       }
     }
@@ -95,14 +95,14 @@ The driver is set up so that it will work without any data in config (using the
 
 `username` - used to show notification (optional)  
 
-`profiles` - use up to 4 profiles, define `m[1-3|r]` as entity of profile  
+`profiles` - use up to 4 profiles, define `MEMORY_[1-3|RECORD]` as entity of profile  
 
-`m[1-3|r]` - switch profile (only if you have more then one profile in config.json)  
+`MEMORY_[1-3|RECORD]` - switch profile (only if you have more than one profile in config.json)  
 
-`g[1-9]` - G-keys macro definition, defined in root of config if no profiles are needed or as entity of the assigned profile. In each `g*` object you have two entities `hotkey_type` and `do` which set what to do when a gkey is pressed.
+`MACRO_[1-N]` - G-keys macro definition, defined in root of config if no profiles are needed or as entity of the assigned profile. In each `MACRO_[1-N]` object you have two entities `hotkey_type` and `do` which set what to do when a gkey is pressed.
 
 ### Hotkey types
-There are currently 5 hotkey types: `nothing`, `shortcut`, `typeout`, `run` and `python` their functionality is as follows:
+There are currently 6 hotkey types: `nothing`, `shortcut`, `typeout`, `run` and `python`, `uinput` their functionality is as follows:
 
 #### Type out - `"typeout"`  
 Types out what is written in "do" key by key, as you would on a keyboard. New line is `\n` and tab is `\t`.
@@ -119,9 +119,13 @@ If you want to run something that interacts with gnome or your desktop service y
 #### Execute a snippet of Python code - `"python"`  
 A Python one-line script. If output is desired, the script should define a global variable named `output_string` and set it to the string to be output.
 
+#### Map a G-Key to a different key - `"uinput"`
+A key defined by python3-uinput to emit (ex. `KEY_F13` or `KEY_KATAKANA`)  
+A full list of all available keys you can find [here](https://github.com/tuomasjjrasanen/python-uinput/blob/master/src/ev.py).
+
 #### Do nothing - `"nothing"`
 
-Also, not all possible characters and control keys are supported (yeah no emojis!). Supported characters and control keys can be viewed in [supported_keys.txt](https://github.com/JSubelj/g910-gkey-macro-support/blob/master/docs/supported_keys.txt) (or if you like to dwell in python dicts (and more consistent): [char_uinput_mapper.py](https://github.com/JSubelj/g910-gkey-macro-support/blob/master/lib/data_mappers/char_uinput_mapper.py)).
+Also, not all possible characters and control keys are supported (yeah no emojis!). Supported characters and control keys can be viewed in [char_uinput_mapper.py](https://github.com/JSubelj/g910-gkey-macro-support/blob/master/lib/data_mappers/char_uinput_mapper.py).
 
 
 >It is also worth pointing out that Enter and Tab keys are pressed while typing out `\n` and `\t`, so you can use this when controlling a program like vim or terminal without using `shortcut`.
@@ -130,29 +134,33 @@ Also, not all possible characters and control keys are supported (yeah no emojis
 ```
 {
     "keyboard_mapping": "si",
-    "g1": {
-        "hotkey_type": "shortcut",
-        "do": "ctrl+alt+t"
-    },
-    "g2": {
-        "hotkey_type": "typeout",
-        "do": "vim /tmp/test-gkey.txt\n"
-    },
-    "g3": {
-        "hotkey_type": "typeout",
-        "do": "iThis Is a Test \n \t also %$&$\"& characters."
-    },
-    "g4": {
-        "hotkey_type": "shortcut",
-        "do": "esc"
-    },
-    "g5": {
-        "hotkey_type": "typeout",
-        "do": ":wq\n"
-    },
-    "g6": {
-        "hotkey_type": "typeout",
-        "do": "cat /tmp/test-gkey.txt\n"
+    "profiles": {
+        "MEMORY_1": {
+            "MACRO_1": {
+                "hotkey_type": "shortcut",
+                "do": "ctrl+alt+t"
+            },
+            "MACRO_2": {
+                "hotkey_type": "typeout",
+                "do": "vim /tmp/test-gkey.txt\n"
+            },
+            "MACRO_3": {
+                "hotkey_type": "typeout",
+                "do": "iThis Is a Test \n \t also %$&$\"& characters."
+            },
+            "MACRO_4": {
+                "hotkey_type": "shortcut",
+                "do": "esc"
+            },
+            "MACRO_5": {
+                "hotkey_type": "typeout",
+                "do": ":wq\n"
+            },
+            "MACRO_6": {
+                "hotkey_type": "typeout",
+                "do": "cat /tmp/test-gkey.txt\n"
+            }
+        }
     }
 }   
 ```
diff --git a/docs/Detailed-installation.md b/docs/Detailed-installation.md
index f7fbe9b..f9b155c 100644
--- a/docs/Detailed-installation.md
+++ b/docs/Detailed-installation.md
@@ -2,7 +2,6 @@
 First check that you have the needed requirements:
  - Python 3.7 (3.6 should work)
  - uinput kernel module (more on this [here](http://tjjr.fi/sw/python-uinput/#Usage))
- - [g810-led controller](https://github.com/MatMoul/g810-led) (optional) switch the lights on m keys to indicate active profile
 
 For Arch based distros: It's live on [AUR](https://aur.archlinux.org/packages/g910-gkeys-git/)
 
diff --git a/docs/Home.md b/docs/Home.md
index 788911f..4d59cc0 100644
--- a/docs/Home.md
+++ b/docs/Home.md
@@ -27,5 +27,4 @@ Information about how to remove driver from you system are [here](https://github
 If you like to contribute to this project please start [here](https://github.com/JSubelj/g910-gkey-macro-support/wiki/Contributing)
 
 The code is tested on Logitech G910 keyboard with
-- OS: Manjaro, 4.19.1 Linux kernel, DE: kde plasma 5.14.3
-- OS: Ubuntu 20.04.5 LTS, Linux 5.4.0-131-generic, GNOME: 3.36.8
\ No newline at end of file
+- OS: Ubuntu 20.04.6 LTS, Linux 5.4.0-159-generic, GNOME: 3.36.9
\ No newline at end of file
diff --git a/docs/ex_config/ex_config.json b/docs/ex_config/ex_config.json
index 2b2e9fe..f11cd00 100644
--- a/docs/ex_config/ex_config.json
+++ b/docs/ex_config/ex_config.json
@@ -4,74 +4,74 @@
   "notify": "True",
   "username": "",
   "profiles": {
-    "m1": {
-      "g1": {
+    "MEMORY_1": {
+      "MACRO_1": {
         "hotkey_type": "typeout",
         "do": "This is profile m1"
       },
-      "g2": {
+      "MACRO_2": {
         "hotkey_type": "shortcut",
         "do": "alt+F4"
       },
-      "g3": {
+      "MACRO_3": {
         "hotkey_type": "run",
         "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
       },
-      "g4": {
+      "MACRO_4": {
         "hotkey_type": "python",
         "do": "import uuid; global output_string; output_string = str(uuid.uuid4());"
       }
     },
-    "m2": {
-      "g1": {
+    "MEMORY_2": {
+      "MACRO_1": {
         "hotkey_type": "typeout",
         "do": "This is profile m2"
       },
-      "g2": {
+      "MACRO_2": {
         "hotkey_type": "shortcut",
         "do": "alt+F4"
       },
-      "g3": {
+      "MACRO_3": {
         "hotkey_type": "run",
         "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
       },
-      "g4": {
+      "MACRO_4": {
         "hotkey_type": "python",
         "do": "import uuid; global output_string; output_string = str(uuid.uuid4());"
       }
     },
-    "m3": {
-      "g1": {
+    "MEMORY_3": {
+      "MACRO_1": {
         "hotkey_type": "typeout",
         "do": "This is profile m3"
       },
-      "g2": {
+      "MACRO_2": {
         "hotkey_type": "shortcut",
         "do": "alt+F4"
       },
-      "g3": {
+      "MACRO_3": {
         "hotkey_type": "run",
         "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
       },
-      "g4": {
+      "MACRO_4": {
         "hotkey_type": "python",
         "do": "import uuid; global output_string; output_string = str(uuid.uuid4());"
       }
     },
-    "mr": {
-      "g1": {
+    "MEMORY_RECORD": {
+      "MACRO_1": {
         "hotkey_type": "typeout",
         "do": "This is profile mr"
       },
-      "g2": {
+      "MACRO_2": {
         "hotkey_type": "shortcut",
         "do": "alt+F4"
       },
-      "g3": {
+      "MACRO_3": {
         "hotkey_type": "run",
         "do": "su <username> -c 'DISPLAY=:0 nohup gnome-terminal' & 2>&1 > /dev/null"
       },
-      "g4": {
+      "MACRO_4": {
         "hotkey_type": "python",
         "do": "import uuid; global output_string; output_string = str(uuid.uuid4());"
       }
diff --git a/etc/60-g910-gkeys.rules b/etc/60-g910-gkeys.rules
new file mode 100644
index 0000000..9ea74ac
--- /dev/null
+++ b/etc/60-g910-gkeys.rules
@@ -0,0 +1,7 @@
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c335", TAG+="uaccess"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c33f", TAG+="uaccess"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c33e", TAG+="uaccess"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c32b", TAG+="uaccess"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c24d", TAG+="uaccess"
+
+KERNEL=="uinput", TAG+="uaccess"
\ No newline at end of file
diff --git a/g910-gkeys.service b/etc/g910-gkeys.service
similarity index 100%
rename from g910-gkeys.service
rename to etc/g910-gkeys.service
diff --git a/etc/uinput-g910-gkeys.conf b/etc/uinput-g910-gkeys.conf
new file mode 100644
index 0000000..23e28ca
--- /dev/null
+++ b/etc/uinput-g910-gkeys.conf
@@ -0,0 +1,2 @@
+# we need the uinput kernel module to be loaded in order to work properly
+uinput
\ No newline at end of file
diff --git a/installer.sh b/installer.sh
index f97a076..b853428 100755
--- a/installer.sh
+++ b/installer.sh
@@ -10,10 +10,9 @@
     exit 1
 
 # get system unit directory
-DEST=$(pkg-config systemd --variable=systemdsystemunitdir)
-CONFDIR=/etc/g910-gkeys
+DESTINATION=$(pkg-config systemd --variable=systemdsystemunitdir)
 FILES=files.txt
-STARTSERVICE=y
+START_SERVICE=y
 SUPPORTED_KEYBOARDS=(de en fr si)
 KEYBOARD=$(locale | grep LANG= | cut -d= -f2 | cut -d_ -f1)
 
@@ -23,7 +22,7 @@ usage() {
 
 while getopts nl: opt ; do
     case "$opt" in
-        n) STARTSERVICE=n
+        n) START_SERVICE=n
            ;;
         *) usage
            exit 1
@@ -31,8 +30,11 @@ while getopts nl: opt ; do
     esac
 done
 
-[[ ! " ${SUPPORTED_KEYBOARDS[@]} " =~ " ${KEYBOARD} " ]] &&
+[[ ! " ${SUPPORTED_KEYBOARDS[*]} " =~ ${KEYBOARD} ]] &&
 	echo "Your system default language is $KEYBOARD, but it's not supported." &&
+	echo "You can try to run: sudo python3 cli_entry_point.py -l create" &&
+	echo "You need to add the created mapping to lib.data_mappers.char_uinput_mapper and" &&
+	echo "add your locale to lib.data_mappers.supported_configs, before installing again." &&
 	echo "Please open a feature request on github with your language:" &&
 	echo "https://github.com/JSubelj/g910-gkey-macro-support/issues/new/choose" &&
 	exit 1
@@ -40,13 +42,13 @@ done
 python3 setup.py install --record "$FILES"
 
 # systemd service file
-echo "$DEST"/g910-gkeys.service >> "$FILES"
-cp g910-gkeys.service "$DEST"/g910-gkeys.service
+echo "$DESTINATION"/g910-gkeys.service >> "$FILES"
+cp etc/g910-gkeys.service "$DESTINATION"/g910-gkeys.service
 
 systemctl daemon-reload
 
 # enable and start service if necessary
-if [[ $STARTSERVICE = y ]]; then
+if [[ $START_SERVICE = y ]]; then
     systemctl enable --now g910-gkeys.service
 else
     echo "g910-gkeys service was not started."
diff --git a/layout_config_helpers.py b/layout_config_helpers.py
new file mode 100644
index 0000000..73a08a2
--- /dev/null
+++ b/layout_config_helpers.py
@@ -0,0 +1,320 @@
+"""
+    This file contains functions that can help you get the right config for your layout.
+"""
+import signal
+import sys
+import tty
+import termios
+import uinput  # used to eval keys
+import lib.data_mappers.char_uinput_mapper as uinput_mapper
+from lib.data_mappers.char_uinput_mapper import keys as locale_key_mapping
+from lib.usb_device import USBDevice
+from lib.keyboard import Keyboard, KeyInputTimeoutException
+from lib.data_mappers.bytearrays import keys as uinput_key_map, commands as uinput_if1
+from lib.misc.helper import Helper
+from lib.misc.config import Config
+
+
+class LayoutHelper:
+    device: USBDevice = None
+    keyboard: Keyboard = None
+
+    WRAPPING_CLICK: int = 0  # only use key click
+    WRAPPING_SHIFT: int = 1  # key click and wrapped shift key click
+    WRAPPING_ALTGR: int = 2  # key click, wrapped shift key click and wrapped alt key click
+
+    skip_keys_altgr: list = [
+        "uinput.KEY_EQUAL",
+        "uinput.KEY_LEFTBRACE",
+        "uinput.KEY_SEMICOLON",
+        "uinput.KEY_APOSTROPHE",
+        "uinput.KEY_GRAVE"
+    ]
+
+    def __init__(self, command: str):
+        """
+        Setup signals and run
+        :param command:
+        """
+        signal.signal(signal.SIGINT, self.signal_handler)
+        signal.signal(signal.SIGTERM, self.signal_handler)
+        signal.signal(signal.SIGQUIT, self.signal_handler)
+
+        if command == "read1":
+            self.read_raw_bytes(1)
+        elif command == "read0":
+            self.read_raw_bytes(0)
+        elif command == "create":
+            self.create()
+        elif command == "test":
+            self.test()
+        elif command == "uinput":
+            self.uinput_key()
+        else:
+            print(f"Syntax: g910-gkey [--help|--read0|--read1|--create|--test|--uinput]")
+            if command != "help":
+                print(f"No matching helper '{command}' found!")
+
+        self.__exit__()
+
+    def signal_handler(self, sig, frame):
+        """
+        Terminating signale handler
+        :param sig:
+        :param frame:
+        """
+        print(f"Got signal, {signal.Signals(sig).name} terminating!")
+        print("The application will end the current process and needs to disconnect your usb and uinput device.")
+
+    def read_raw_bytes(self, interface: int):
+        """
+        Read raw bytes send over usb from keyboard and try to get matching command or key from command_bytearray
+        :param interface: 0 for all normal keys, 1 for g-, m- and media-keys
+        """
+        self.device = USBDevice(interface)
+        if interface == 0:
+            byte_map = uinput_key_map
+        else:
+            byte_map = uinput_if1
+        bytes_received = None
+        while bytes_received is None:
+            bytes_received = self.device.read()
+            if bytes_received is not None:
+                try:
+                    uinput_key = list(byte_map.keys())[list(byte_map.values()).index(bytes(bytes_received))]
+                except ValueError:
+                    uinput_key = "NO_MATCH"
+                print(f"{uinput_key}: {bytes(bytes_received)}")
+
+    def uinput_key(self):
+        """
+        Get uinput key by pressing a key on the keyboard
+        """
+        self.device = USBDevice(0)  # init keyboard interface 0 to read default keys
+        write_config = ''
+        print(f"Press a key you want to get the uinput key definition:")
+        user_input = False
+        while not user_input:
+            bytes_received = self.device.read()
+            if bytes_received is not None:
+                if bytes(bytes_received) == uinput_key_map.get('KEY_LEFTSHIFT') or \
+                        bytes(bytes_received) == uinput_key_map.get('KEY_RIGHTSHIFT') or \
+                        bytes(bytes_received) == uinput_key_map.get('KEY_LEFTALT') or \
+                        bytes(bytes_received) == uinput_key_map.get('KEY_RIGHTALT'):
+                    pass
+                elif bytes(bytes_received) != b'\x00\x00\x00\x00\x00\x00\x00\x00':
+                    wrap_byte = bytes_received[0]
+                    bytes_received[0] = 0  # remove shift wrap from byte array
+                    try:
+                        uinput_key = list(uinput_key_map.keys())[list(uinput_key_map.values()).index(bytes(bytes_received))]
+                        if wrap_byte == uinput_key_map.get('KEY_LEFTSHIFT')[0]:
+                            # wrapped with left shift
+                            write_config = f"wrap_shift(click(uinput.{uinput_key}))"
+                        elif wrap_byte == uinput_key_map.get('KEY_RIGHTALT')[0]:
+                            # wrapped with right alt (altgr)
+                            write_config = f"wrap_altgr(click(uinput.{uinput_key}))"
+                        else:
+                            write_config = f"click(uinput.{uinput_key})"
+                    except ValueError:
+                        pass  # just keep on going if a key is not found
+                elif bytes(bytes_received) == b'\x00\x00\x00\x00\x00\x00\x00\x00':
+                    user_input = True
+        print(write_config)
+
+    def create(self):
+        """
+        Create localized uinput key map
+        """
+        user_lang = Helper.get_locale()
+        config = Config()
+        try:
+            self.keyboard = Keyboard(config)
+            locale_map = self.get_keys()
+            filename = f"char_uinput_mapping_{user_lang}"
+            with open(filename, "w") as fd:
+                fd.write("'" + user_lang + "': {\n")
+                for row in locale_map:
+                    fd.write(f"{row}\n")
+                fd.write("}")
+            print(f"Your local char to uinput map was saved to {filename}.")
+        except Exception as e:
+            print(repr(e))
+
+    @staticmethod
+    def get_key_timeout_handler(signum, frame):
+        """
+        Timeout handler for char to uinput mapping raise a KeyInputTimeoutException
+        :param signum:
+        :param frame:
+        """
+        raise KeyInputTimeoutException("No key emitted.")
+
+    def get_emitted_char(self, event: list):
+        """
+        Get character written down to stdout on emit of given key
+        :param event: uinput event
+        :return: character written down in stdout on key emit
+        :rtype: str
+        """
+        # set alarm signal to 1s to go on if key emit doesn't output any character
+        signal.signal(signal.SIGALRM, self.get_key_timeout_handler)
+        signal.alarm(1)
+        file_descriptors = termios.tcgetattr(sys.stdin)  # save old file descriptors to reset later
+        tty.setcbreak(sys.stdin)  # set cbreak to stdin (we will need no enter to read from stdin)
+        if event == uinput_mapper.click((eval("uinput.KEY_GRAVE"))):
+            # KEY_GRAVE have to be pressed two times
+            event += event
+        elif event == uinput_mapper.click((eval("uinput.KEY_EQUAL"))) or \
+                event == uinput_mapper.wrap_shift(uinput_mapper.click(eval("uinput.KEY_EQUAL"))):
+            # KEY_EQUAL have to be pressed two times alone and also together with shift
+            event += event
+        self.keyboard.execute_events(event)
+        try:
+            char = sys.stdin.read(1)[0]  # read the char which gets writen by uinput keyboard
+            signal.alarm(0)  # remove the alarm signal
+        except KeyInputTimeoutException:
+            # if we capture an alarm, we set an empty character to remove from mapping later
+            char = ''
+        # reset file descriptor on stdin
+        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, file_descriptors)
+        return char
+
+    @staticmethod
+    def get_other_keys():
+        """
+        Get none alphanumeric keys
+        :return: Return a list of keys with a string interpretation of uinput key (ex. ["uinput.KEY_COMMA", ...])
+        :rtype: list
+        """
+        keys = [
+            "uinput.KEY_EQUAL",
+            "uinput.KEY_MINUS",
+            "uinput.KEY_LEFTBRACE",
+            "uinput.KEY_RIGHTBRACE",
+            "uinput.KEY_SEMICOLON",
+            "uinput.KEY_APOSTROPHE",
+            "uinput.KEY_GRAVE",
+            "uinput.KEY_BACKSLASH",
+            "uinput.KEY_COMMA",
+            "uinput.KEY_DOT",
+            "uinput.KEY_SLASH"
+        ]
+        return keys
+
+    def get_keys(self):
+        """
+        Get config list entries for all keys
+        :return: config list entries for all keys
+        :rtype: list
+        """
+        key_config = []
+        for i in range(48, 58):  # digits 0-9
+            key_config += self.get_key_config("uinput.KEY_" + chr(i), self.WRAPPING_ALTGR)
+
+        for i in range(65, 91):  # characters a-z
+            key_config += self.get_key_config("uinput.KEY_" + chr(i), self.WRAPPING_ALTGR)
+
+        for key in self.get_other_keys():  # add other keys (no control keys)
+            key_config += self.get_key_config(key, self.WRAPPING_ALTGR)
+
+        return key_config
+
+    def get_key_config(self, key: str, wrapping: int = 0):
+        """
+        Get key configuration by given uinput key
+        :param key: uinput key as string (ex. uinput.KEY_0)
+        :param wrapping: one of WRAPPING_CLICK | WRAPPING_SHIFT | WRAPPING_ALTGR
+        :return: list entry/ies for key for use in the driver
+        :rtype: list
+        """
+        locale_keys = []
+        char = self.get_emitted_char(uinput_mapper.click((eval(key))))
+        if char != '':
+            locale_keys.append(f"\t'{char}': click({key}),")
+        if wrapping > 0:  # wrap key with shift
+            char = self.get_emitted_char(uinput_mapper.wrap_shift(uinput_mapper.click((eval(key)))))
+            if char != '':
+                locale_keys.append(f"\t'{char}': wrap_shift(click({key})),")
+        if wrapping > 1 and key not in self.skip_keys_altgr:  # wrap key with right alt
+            char = self.get_emitted_char(uinput_mapper.wrap_altgr(uinput_mapper.click((eval(key)))))
+            if char != '':
+                locale_keys.append(f"\t'{char}': wrap_altgr(click({key})),")
+
+        return locale_keys
+
+    def test(self):
+        """
+        Test the char to uinput map with current locale
+        A virtual keyboard is initialized and all keys from the map will be emitted.
+        After each key is emitted the written char gets checked against the key map.
+        """
+        user_lang = Helper.get_locale()
+        config = Config()
+        self.keyboard = Keyboard(config)
+        events = locale_key_mapping[user_lang]
+        error = False
+        for key, event in events.items():
+            if error:
+                break
+            try:
+                char = self.get_emitted_char(event)
+                if key != char:
+                    error = True
+                    print(f"{key} != {char}")
+            except Exception as e:
+                print(repr(e))
+                error = True
+
+        if error:
+            print("Error in key map: Please correct char_uinput_mapper.py")
+            return False
+        else:
+            print(f"'{user_lang}' Key map tested successfully.")
+            return True
+
+    def __exit__(self):
+        """
+        Disconnect the usb- and uinput-device on exit
+        """
+        try:
+            self.device.__exit__()
+        except AttributeError:
+            pass  # pass if no usb device was initialized
+        try:
+            self.keyboard.__exit__()
+        except AttributeError:
+            pass  # pass if no uinput device was initialized
+
+
+if __name__ == "__main__":
+    import argparse
+    from lib import PROJECT_INFO
+
+    parser = argparse.ArgumentParser(description=PROJECT_INFO.DESCRIPTION)
+    parser.add_argument("--read0", help="Read the raw bytes from interface 0 (default keys)",
+                        action='store_true', default=False)
+    parser.add_argument("--read1", help="Read the raw bytes from interface 1 (macro- and media-keys)",
+                        action='store_true', default=False)
+    parser.add_argument("-c", "--create", help="Create a keyboard mapping for your locale keyboard",
+                        action='store_true', default=False)
+    parser.add_argument("-t", "--test", help="Test the keyboard mapping set in config.json",
+                        action='store_true', default=False)
+    parser.add_argument("-u", "--uinput", help="Get uinput key from key press",
+                        action='store_true', default=False)
+    parser.add_argument("-v", "--version", help="Displays version of the driver",
+                        action='version', version='%(prog)s ' + PROJECT_INFO.VERSION)
+    args = parser.parse_args()
+
+    command = "help"
+    if args.read0:
+        command = "read0"
+    elif args.read1:
+        command = "read1"
+    elif args.create:
+        command = "create"
+    elif args.test:
+        command = "test"
+    elif args.uinput:
+        command = "uinput"
+
+    LayoutHelper(command)
diff --git a/lib/PROJECT_INFO.py b/lib/PROJECT_INFO.py
index ddf69aa..2e60529 100644
--- a/lib/PROJECT_INFO.py
+++ b/lib/PROJECT_INFO.py
@@ -4,4 +4,4 @@
 EMAIL = 'jan.subelj010@gmail.com'
 AUTHOR = 'Jan Šubelj'
 REQUIRES_PYTHON = '>=3.7.0'
-VERSION = '0.2.5'
\ No newline at end of file
+VERSION = '0.3.0'
diff --git a/lib/data_mappers/bytearrays.py b/lib/data_mappers/bytearrays.py
new file mode 100644
index 0000000..756e975
--- /dev/null
+++ b/lib/data_mappers/bytearrays.py
@@ -0,0 +1,99 @@
+# we still use the command in layout_config_helpers, but it's redundant and should be removed
+# todo: remove this if it is no longer needed
+commands = {
+    'release': bytearray(b'\x11\xff\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'g1': bytearray(b'\x11\xff\x08\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'g2': bytearray(b'\x11\xff\x08\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'g3': bytearray(b'\x11\xff\x08\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'g4': bytearray(b'\x11\xff\x08\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'g5': bytearray(b'\x11\xff\x08\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'g6': bytearray(b'\x11\xff\x08\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'g7': bytearray(b'\x11\xff\x08\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'g8': bytearray(b'\x11\xff\x08\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'g9': bytearray(b'\x11\xff\x08\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'm1': bytearray(b'\x11\xff\t\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'm2': bytearray(b'\x11\xff\t\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'm3': bytearray(b'\x11\xff\t\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'mr': bytearray(b'\x11\xff\n\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'm1-3_key_release': bytearray(b'\x11\xff\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'mr_key_release': bytearray(b'\x11\xff\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'vol_down': bytearray(b'\x02 '),
+    'vol_up': bytearray(b'\x02\x10'),
+    'media_release': bytearray(b'\x02\x00'),
+    'vol_mute': bytearray(b'\x02@'),
+    'play_pause': bytearray(b'\x02\x08'),
+    'stop': bytearray(b'\x02\x04'),
+    'next_song': bytearray(b'\x02\x01'),
+    'prev_song': bytearray(b'\x02\x02'),
+    'm1-3_led_off': bytearray(b'\x11\xff\t\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'm1_led_on': bytearray(b'\x11\xff\t\x1e\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'm2_led_on': bytearray(b'\x11\xff\t\x1e\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'm3_led_on': bytearray(b'\x11\xff\t\x1e\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'm4_led_on': bytearray(b'\x11\xff\t\x1e\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'm5_led_on': bytearray(b'\x11\xff\t\x1e\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'm6_led_on': bytearray(b'\x11\xff\t\x1e\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'm7_led_on': bytearray(b'\x11\xff\t\x1e\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'mr_led_off': bytearray(b'\x11\xff\n\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
+    'mr_led_on': bytearray(b'\x11\xff\n\x0e\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
+}
+
+# g910 byte array > uinput map
+keys = {
+    'KEY_LEFTCTRL':     bytearray(b'\x01\x00\x00\x00\x00\x00\x00\x00'),
+    'KEY_RIGHTCTRL':    bytearray(b'\x10\x00\x00\x00\x00\x00\x00\x00'),
+    'KEY_LEFTSHIFT':    bytearray(b'\x02\x00\x00\x00\x00\x00\x00\x00'),
+    'KEY_RIGHTSHIFT':   bytearray(b' \x00\x00\x00\x00\x00\x00\x00'),
+    'KEY_LEFTALT':      bytearray(b'\x04\x00\x00\x00\x00\x00\x00\x00'),
+    'KEY_RIGHTALT':     bytearray(b'@\x00\x00\x00\x00\x00\x00\x00'),
+    
+    'KEY_A':            bytearray(b'\x00\x00\x04\x00\x00\x00\x00\x00'),
+    'KEY_B':            bytearray(b'\x00\x00\x05\x00\x00\x00\x00\x00'),
+    'KEY_C':            bytearray(b'\x00\x00\x06\x00\x00\x00\x00\x00'),
+    'KEY_D':            bytearray(b'\x00\x00\x07\x00\x00\x00\x00\x00'),
+    'KEY_E':            bytearray(b'\x00\x00\x08\x00\x00\x00\x00\x00'),
+    'KEY_F':            bytearray(b'\x00\x00\x09\x00\x00\x00\x00\x00'),
+    'KEY_G':            bytearray(b'\x00\x00\n\x00\x00\x00\x00\x00'),
+    'KEY_H':            bytearray(b'\x00\x00\x0b\x00\x00\x00\x00\x00'),
+    'KEY_I':            bytearray(b'\x00\x00\x0c\x00\x00\x00\x00\x00'),
+    'KEY_J':            bytearray(b'\x00\x00\r\x00\x00\x00\x00\x00\x00'),
+    'KEY_K':            bytearray(b'\x00\x00\x0e\x00\x00\x00\x00\x00'),
+    'KEY_L':            bytearray(b'\x00\x00\x0f\x00\x00\x00\x00\x00'),
+    'KEY_M':            bytearray(b'\x00\x00\x10\x00\x00\x00\x00\x00'),
+    'KEY_N':            bytearray(b'\x00\x00\x11\x00\x00\x00\x00\x00'),
+    'KEY_O':            bytearray(b'\x00\x00\x12\x00\x00\x00\x00\x00'),
+    'KEY_P':            bytearray(b'\x00\x00\x13\x00\x00\x00\x00\x00'),
+    'KEY_Q':            bytearray(b'\x00\x00\x14\x00\x00\x00\x00\x00'),
+    'KEY_R':            bytearray(b'\x00\x00\x15\x00\x00\x00\x00\x00'),
+    'KEY_S':            bytearray(b'\x00\x00\x16\x00\x00\x00\x00\x00'),
+    'KEY_T':            bytearray(b'\x00\x00\x17\x00\x00\x00\x00\x00'),
+    'KEY_U':            bytearray(b'\x00\x00\x18\x00\x00\x00\x00\x00'),
+    'KEY_V':            bytearray(b'\x00\x00\x19\x00\x00\x00\x00\x00'),
+    'KEY_W':            bytearray(b'\x00\x00\x1a\x00\x00\x00\x00\x00'),
+    'KEY_X':            bytearray(b'\x00\x00\x1b\x00\x00\x00\x00\x00'),
+    'KEY_Y':            bytearray(b'\x00\x00\x1c\x00\x00\x00\x00\x00'),
+    'KEY_Z':            bytearray(b'\x00\x00\x1d\x00\x00\x00\x00\x00'),
+
+    'KEY_?':            bytearray(b'\x00\x00d\x00\x00\x00\x00\x00'),
+    'KEY_LEFTBRACE':    bytearray(b'\x00\x00/\x00\x00\x00\x00\x00'),
+    'KEY_RIGHTBRACE':   bytearray(b'\x00\x000\x00\x00\x00\x00\x00'),
+    'KEY_SEMICOLON':    bytearray(b'\x00\x003\x00\x00\x00\x00\x00'),
+    'KEY_APOSTROPHE':   bytearray(b'\x00\x004\x00\x00\x00\x00\x00'),
+    'KEY_BACKSLASH':    bytearray(b'\x00\x002\x00\x00\x00\x00\x00'),
+    'KEY_COMMA':        bytearray(b'\x00\x006\x00\x00\x00\x00\x00'),
+    'KEY_DOT':          bytearray(b'\x00\x007\x00\x00\x00\x00\x00'),
+    'KEY_SLASH':        bytearray(b'\x00\x008\x00\x00\x00\x00\x00'),
+
+    'KEY_GRAVE':        bytearray(b'\x00\x005\x00\x00\x00\x00\x00'),
+    'KEY_1':            bytearray(b'\x00\x00\x1e\x00\x00\x00\x00\x00'),
+    'KEY_2':            bytearray(b'\x00\x00\x1f\x00\x00\x00\x00\x00'),
+    'KEY_3':            bytearray(b'\x00\x00 \x00\x00\x00\x00\x00'),
+    'KEY_4':            bytearray(b'\x00\x00!\x00\x00\x00\x00\x00'),
+    'KEY_5':            bytearray(b'\x00\x00"\x00\x00\x00\x00\x00'),
+    'KEY_6':            bytearray(b'\x00\x00#\x00\x00\x00\x00\x00'),
+    'KEY_7':            bytearray(b'\x00\x00$\x00\x00\x00\x00\x00'),
+    'KEY_8':            bytearray(b'\x00\x00%\x00\x00\x00\x00\x00'),
+    'KEY_9':            bytearray(b'\x00\x00&\x00\x00\x00\x00\x00'),
+    'KEY_0':            bytearray(b'\x00\x00\'\x00\x00\x00\x00\x00'),
+    'KEY_MINUS':        bytearray(b'\x00\x00-\x00\x00\x00\x00\x00'),
+    'KEY_EQUAL':        bytearray(b'\x00\x00.\x00\x00\x00\x00\x00')
+}
diff --git a/lib/data_mappers/char_uinput_mapper.py b/lib/data_mappers/char_uinput_mapper.py
index 216d95e..3f2e913 100644
--- a/lib/data_mappers/char_uinput_mapper.py
+++ b/lib/data_mappers/char_uinput_mapper.py
@@ -16,7 +16,7 @@ def wrap_altgr(key_array):
     return [(uinput.KEY_RIGHTALT, 1)] + key_array + [(uinput.KEY_RIGHTALT, 0)]
 
 
-# types of return events: list(touple(uinputkey, [0,1,3])) ex: [((1,12),0),((1,12),1), ((1,12),3)]
+# types of return events: list(touple(uinputkey, [0|1|3])) ex: [((1, 12), 0),((1, 12), 1), ((1, 12), 3)]
 # 1 means keypress
 # 0 means keyrelease
 # 3 means click key
@@ -31,6 +31,7 @@ def wrap_altgr(key_array):
         'pageup': click(uinput.KEY_PAGEUP),
         'pagedown': click(uinput.KEY_PAGEDOWN),
         "shift": click(uinput.KEY_LEFTSHIFT),
+        "tab": click(uinput.KEY_TAB),
         'esc': click(uinput.KEY_ESC),
         "enter": click(uinput.KEY_ENTER),
         "capslock": click(uinput.KEY_CAPSLOCK),
@@ -92,7 +93,6 @@ def wrap_altgr(key_array):
         'right': click(uinput.KEY_RIGHT),
     },
     "si": {
-
         # autogenerated
         '0': click(uinput.KEY_0),
         '1': click(uinput.KEY_1),
@@ -583,7 +583,10 @@ def wrap_altgr(key_array):
         ";": wrap_shift(click(uinput.KEY_COMMA)),
         '.': click(uinput.KEY_DOT),
         ":": wrap_shift(click(uinput.KEY_DOT)),
-        '€': wrap_altgr(click(uinput.KEY_E))
+        '€': wrap_altgr(click(uinput.KEY_E)),
+
+        "\n": click(uinput.KEY_ENTER),
+        "\t": click(uinput.KEY_TAB)
     }
 }
 
diff --git a/lib/data_mappers/command_bytearray.py b/lib/data_mappers/command_bytearray.py
deleted file mode 100644
index d89695b..0000000
--- a/lib/data_mappers/command_bytearray.py
+++ /dev/null
@@ -1,36 +0,0 @@
-commands = {
-    'release': bytearray(b'\x11\xff\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'g1': bytearray(b'\x11\xff\x08\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'g2': bytearray(b'\x11\xff\x08\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'g3': bytearray(b'\x11\xff\x08\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'g4': bytearray(b'\x11\xff\x08\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'g5': bytearray(b'\x11\xff\x08\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'g6': bytearray(b'\x11\xff\x08\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'g7': bytearray(b'\x11\xff\x08\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'g8': bytearray(b'\x11\xff\x08\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'g9': bytearray(b'\x11\xff\x08\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'm1': bytearray(b'\x11\xff\t\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'm2': bytearray(b'\x11\xff\t\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'm3': bytearray(b'\x11\xff\t\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'mr': bytearray(b'\x11\xff\n\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'm1-3_key_release': bytearray(b'\x11\xff\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'mr_key_release': bytearray(b'\x11\xff\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'vol_down': bytearray(b'\x02 '),
-    'vol_up': bytearray(b'\x02\x10'),
-    'media_release': bytearray(b'\x02\x00'),
-    'vol_mute': bytearray(b'\x02@'),
-    'play_pause': bytearray(b'\x02\x08'),
-    'stop': bytearray(b'\x02\x04'),
-    'next_song': bytearray(b'\x02\x01'),
-    'prev_song': bytearray(b'\x02\x02'),
-    'm1-3_led_off': bytearray(b'\x11\xff\t\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'm1_led_on': bytearray(b'\x11\xff\t\x1e\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'm2_led_on': bytearray(b'\x11\xff\t\x1e\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'm3_led_on': bytearray(b'\x11\xff\t\x1e\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'm4_led_on': bytearray(b'\x11\xff\t\x1e\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'm5_led_on': bytearray(b'\x11\xff\t\x1e\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'm6_led_on': bytearray(b'\x11\xff\t\x1e\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'm7_led_on': bytearray(b'\x11\xff\t\x1e\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'mr_led_off': bytearray(b'\x11\xff\n\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'),
-    'mr_led_on': bytearray(b'\x11\xff\n\x0e\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
-}
\ No newline at end of file
diff --git a/lib/data_mappers/config_reader.py b/lib/data_mappers/config_reader.py
deleted file mode 100644
index 6b578fe..0000000
--- a/lib/data_mappers/config_reader.py
+++ /dev/null
@@ -1,155 +0,0 @@
-import json
-from json.decoder import JSONDecodeError
-from lib.misc import paths, logger
-from lib.data_mappers import supported_configs, char_uinput_mapper
-from lib import g910_gkey_mapper
-
-log = logger.logger(__name__)
-
-config = None
-
-
-def validate_hotkey_action(do, hotkey_action, keyboard_mapping):
-    if hotkey_action == "nothing" or hotkey_action == "run":
-        return None
-    if hotkey_action == "typeout":
-        error = {}
-        i = 0
-        error["typeout"] = ""
-        for c in do:
-            try:
-                char_uinput_mapper.keys[keyboard_mapping][c]
-            except:
-                error["typeout"] += "Character "+c+" on position "+str(i)+" for keyboard mapping: "+keyboard_mapping+" does not exist! "
-            i+=1
-        if error["typeout"] == "":
-            return None
-        return error
-    if hotkey_action == "shortcut":
-        error = {}
-        press_together = []
-
-        together = do.split(",")
-        for hotkey in together:
-            keys_str = hotkey.split("+")
-            press_together.append(keys_str)
-
-        error["shortcut"] = ""
-        i = 0
-        for combo in press_together:
-            for key_string in combo:
-                i+=1
-                if len(key_string) == 1:
-                    try:
-                        char_uinput_mapper.keys[keyboard_mapping][key_string]
-                    except:
-                        error["shortcut"]+="Character "+key_string+" on position "+str(i)+" for keyboard mapping: "+keyboard_mapping+" does not exist! "
-
-                else:
-                    try:
-                        char_uinput_mapper.keys["control"][key_string]
-                    except:
-                        error["shortcut"]+="Control key "+key_string+" on position "+str(i)+" does not exist! "
-        if error["shortcut"] == "":
-            return None
-        else:
-            return error
-
-
-def validate_config(config_dic : dict):
-    errors = {}
-    keyboard_mapping = config_dic.get("keyboard_mapping", supported_configs.default_keyboard_mapping)
-    return_config = {"keyboard_mapping": keyboard_mapping}
-    if keyboard_mapping not in supported_configs.keyboard_mappings:
-        return {"keyboard_mapping": keyboard_mapping+" does not exist!"}, None
-
-    return_config["notify"] = config_dic.get("notify", False)
-    return_config["username"] = config_dic.get("username", "")
-
-    profile_start_range = 1
-    return_config["profiles"] = {}
-    return_config["profiles"]["m1"] = {}
-    backward_compatibility = False
-    errors["backward_compatibility"] = {}
-    # add backward compatibility
-    for i in range(1,10):
-        setting_for_gkey = config_dic.get("g" + str(i), {})
-        hotkey_type = setting_for_gkey.get("hotkey_type",supported_configs.default_hotkey_type)
-        errors["backward_compatibility"]["g" + str(i)] = {}
-        if hotkey_type not in supported_configs.hotkey_types:
-            errors["backward_compatibility"]["g" + str(i)]["hotkey_type"] = hotkey_type + " does not exist!"
-        do = setting_for_gkey.get("do",supported_configs.default_hotkey_do)
-        do_validation = validate_hotkey_action(do, hotkey_type, keyboard_mapping)
-        if do_validation:
-            errors["backward_compatibility"]["g" + str(i)]["do"] = do_validation.copy()
-            do = ""
-            hotkey_type = "nothing"
-        if len(errors["backward_compatibility"]["g" + str(i)]) == 0:
-            errors["backward_compatibility"].pop("g" + str(i))
-        if do != "":
-            backward_compatibility = True
-            profile_start_range = 2
-        return_config["profiles"]["m1"]["g" + str(i)] = {"hotkey_type": hotkey_type, "do": do}
-
-    if len(errors["backward_compatibility"]) == 0:
-        errors.pop("backward_compatibility")
-
-    for profile_index in range(profile_start_range,4):
-        if profile_index == 4:
-            profile_index = "r"
-
-        return_config["profiles"]["m" + str(profile_index)] = {}
-        errors["m" + str(profile_index)] = {}
-        for i in range(1,10):
-            setting_for_gkey = config_dic.get("profiles", {}).get("m" + str(profile_index), {}).get("g" + str(i), {})
-            hotkey_type = setting_for_gkey.get("hotkey_type",supported_configs.default_hotkey_type)
-            errors["m" + str(profile_index)]["g" + str(i)] = {}
-            if hotkey_type not in supported_configs.hotkey_types:
-                errors["m" + str(profile_index)]["g" + str(i)]["hotkey_type"] = hotkey_type + " does not exist!"
-            do = setting_for_gkey.get("do",supported_configs.default_hotkey_do)
-            do_validation = validate_hotkey_action(do, hotkey_type, keyboard_mapping)
-            if do_validation:
-                errors["m" + str(profile_index)]["g" + str(i)]["do"] = do_validation.copy()
-                do = ""
-                hotkey_type = "nothing"
-            if len(errors["m" + str(profile_index)]["g" + str(i)]) == 0:
-                errors["m" + str(profile_index)].pop("g" + str(i))
-            return_config["profiles"]["m" + str(profile_index)]["g" + str(i)] = {"hotkey_type": hotkey_type, "do": do}
-
-        if len(errors["m" + str(profile_index)]) == 0:
-            errors.pop("m" + str(profile_index))
-
-    if len(errors) == 0:
-        return None, return_config
-
-    return errors, return_config
-
-
-
-
-def update_config():
-    global config
-    config = None
-    config = read()
-
-def read():
-    global config
-    if config:
-        return config
-
-    log.debug("Reading config from " + paths.config_path)
-    try:
-        with open(paths.config_path, "r") as f:
-            
-            err, config = validate_config(json.load(f))
-            log.debug("using config: " + str(config))
-            if err:
-                log.warning("Error(s) in config: "+str(err))
-            return config
-
-    except JSONDecodeError as e:
-        log.error(f"JSONDecodeError: {str(e)} in {paths.config_path}")
-    except Exception as e:
-        log.error(f"{type(e).__name__}: {str(e)}")
-        log.error(f"NO CONFIG FOUND! Create config.json in {paths.config_path}\n To create default config run: g910-gkeys --create-config\n")
-    g910_gkey_mapper.program_running = False
diff --git a/lib/data_mappers/supported_configs.py b/lib/data_mappers/supported_configs.py
index ec972ef..f6a24d4 100644
--- a/lib/data_mappers/supported_configs.py
+++ b/lib/data_mappers/supported_configs.py
@@ -3,9 +3,10 @@
 
 default_hotkey_type = "nothing"
 default_hotkey_do = ""
-hotkey_types = ["nothing", "typeout", "shortcut", "run", "python"]
+default_key_action = {
+    "hotkey_type": default_hotkey_type,
+    "do": default_hotkey_do
+}
+hotkey_types = ["nothing", "typeout", "shortcut", "run", "python", "uinput"]
 
-profile = "m1"
-profiles = ["m1", "m2", "m3", "mr"]
-
-valid_keys = ["g1", "g2", "g3", "g4", "g5", "g6", "g7", "g8", "g9", "m1", "m2", "m3", "mr"]
+default_profile = "MEMORY_1"
diff --git a/lib/data_mappers/supported_devices.py b/lib/data_mappers/supported_devices.py
new file mode 100644
index 0000000..baf688b
--- /dev/null
+++ b/lib/data_mappers/supported_devices.py
@@ -0,0 +1,397 @@
+from dataclasses import dataclass, field
+
+
+@dataclass(frozen=True)
+class DeviceEventInterface:
+    macroKeys: dict  # dict[bytes, str]
+    memoryKeys: dict  # dict[bytes, str]
+    memoryKeysLEDs = dict()  # dict[str, bytes]
+    mediaKeys: dict  # dict[bytes, str] | only in use when in libUSB mode (kernel driver unloaded)
+    releaseEvents: dict  # dict[str, bytes]
+
+    # Following is sent to disable the default G keys mapping
+    disableGKeys = list()  # list[bytes]
+    disableGKeysResponse = list()  # list[bytes]
+    # enable the g-key to f-key mapping
+    enableGKeys = list()  # list[bytes]
+    enableGKeysResponse = list()  # list[bytes]
+
+
+@dataclass(frozen=True)
+class LogitechG910EventInterface(DeviceEventInterface):
+    macroKeys = {
+        b'\x11\xff\x08\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_1",
+        b'\x11\xff\x08\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_2",
+        b'\x11\xff\x08\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_3",
+        b'\x11\xff\x08\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_4",
+        b'\x11\xff\x08\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_5",
+        b'\x11\xff\x08\x00\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_6",
+        b'\x11\xff\x08\x00\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_7",
+        b'\x11\xff\x08\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_8",
+        b'\x11\xff\x08\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_9"
+    }
+
+    memoryKeys = {
+        b'\x11\xff\x09\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_1",
+        b'\x11\xff\x09\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_2",
+        b'\x11\xff\x09\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_3",
+        b'\x11\xff\x0a\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_RECORD"
+    }
+
+    memoryKeysLEDs = {
+        "MEMORY_1": b'\x11\xff\x09\x1b\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        "MEMORY_2": b'\x11\xff\x09\x1b\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        "MEMORY_3": b'\x11\xff\x09\x1b\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        "MEMORY_RECORD": [
+            b'\x11\xff\x0a\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+            b'\x11\xff\x0a\x0e\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        ]
+    }
+
+    mediaKeys = {
+        b'\x02\x08': "KEY_PLAYPAUSE",
+        b'\x02\x04': "KEY_STOP",
+        b'\x02\x02': "KEY_PREVIOUS",
+        b'\x02\x01': "KEY_NEXT",
+        b'\x02\x40': "KEY_MUTE",
+        b'\x02\x10': "KEY_VOLUMEUP",
+        b'\x02\x20': "KEY_VOLUMEDOWN"
+    }
+
+    releaseEvents = {
+        b'\x11\xff\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': 'MACRO_RELEASE',
+        b'\x02\x00': 'MEDIA_RELEASE',
+        b'\x11\xff\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': 'MEMORY_RELEASE',
+        b'\x11\xff\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': 'MEMORY_RECORD_RELEASE'
+    }
+
+    disableGKeys = [
+        b'\x11\xff\x08\x2e\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+    ]
+
+    disableGKeysResponse = [
+        b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        b'\x02\x00',  # media key release
+        disableGKeys[0],
+        b'\x11\xff\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',  # macro key release
+        b'\x11\xff\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',  # memory key release
+        b'\x11\xff\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'   # memory record release
+    ]
+
+    enableGKeys = [b'\x11\xff\x08\x2e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']
+
+    enableGKeysResponse = [
+        b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        b'\x02\x00',  # media key release
+        disableGKeys[0],
+        b'\x11\xff\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',  # macro key release
+        b'\x11\xff\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',  # memory key release
+        b'\x11\xff\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'   # memory record release
+    ]
+
+
+@dataclass(frozen=True)
+class LogitechG815EventInterface(DeviceEventInterface):
+    macroKeys = {
+        b'\x11\xff\x0a\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_1",
+        b'\x11\xff\x0a\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_2",
+        b'\x11\xff\x0a\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_3",
+        b'\x11\xff\x0a\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_4",
+        b'\x11\xff\x0a\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MACRO_5",
+        b'\x11\xff\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_RECORD"
+    }
+
+    memoryKeys = {
+        b'\x11\xff\x0b\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_1",
+        b'\x11\xff\x0b\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_2",
+        b'\x11\xff\x0b\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_3"
+    }
+
+    memoryKeysLEDs = {
+        "MEMORY_1": b'\x11\xff\x0b\x1a\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',  # M1 LED
+        "MEMORY_2": b'\x11\xff\x0b\x1a\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',  # M2 LED
+        "MEMORY_3": b'\x11\xff\x0b\x1a\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'   # M3 LED
+    }
+
+    mediaKeys = {
+        b'\x03\x08': "KEY_PLAYPAUSE",
+        b'\x03\x04': "KEY_STOP",
+        b'\x03\x02': "KEY_PREVIOUS",
+        b'\x03\x01': "KEY_NEXT",
+        b'\x03\x10': "KEY_VOLUMEUP",
+        b'\x03\x20': "KEY_VOLUMEDOWN",
+        b'\x03\x40': "KEY_MUTE"
+    }
+
+    releaseEvents = {
+        'KEY_G': b'\x11\xff\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',      # G release
+        'KEY_MEDIA': b'\x03\x00',  # media key release
+        'KEY_M': b'\x11\xff\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',    # M release
+        'KEY_MR': b'\x11\xff\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'     # MR release
+    }
+
+    disableGKeys = [
+        # switch from keyboard memory mode to client mode - disables everything but the "normal" keys,
+        # so we need to enable it again
+        b'\x11\xff\x11\x1a\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        # enable G-keys
+        b'\x11\xff\x0a\x2a\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        # enable LED manual control (not 100% sure but without this package the following
+        # two packages will not do anything)
+        b'\x11\xff\x0f\x5a\x01\x03\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        # enable GLogo light
+        b'\x11\xff\x0f\x1a\x00\x02\x00\x00\x00\x00\x00\x08\x34\x64\x00\x00\x01\x00\x00\x00',
+        # enable default background light
+        b'\x11\xff\x0f\x1a\x01\x04\x00\x00\x00\x00\x00\x00\x34\x01\x64\x08\x01\x00\x00\x00'
+    ]
+
+
+@dataclass(frozen=True)
+class LogitechG915EventInterface(LogitechG815EventInterface):
+    macroKeys = {
+        b'\x02\x01\x00\x00\x00\x00\x00\x00\x00': "MACRO_1",
+        b'\x02\x02\x00\x00\x00\x00\x00\x00\x00': "MACRO_2",
+        b'\x02\x04\x00\x00\x00\x00\x00\x00\x00': "MACRO_3",
+        b'\x02\x08\x00\x00\x00\x00\x00\x00\x00': "MACRO_4",
+        b'\x02\x10\x00\x00\x00\x00\x00\x00\x00': "MACRO_5",
+        b'\x11\x01\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_RECORD"
+    }
+
+    memoryKeys = {
+        b'\x11\x01\x0b\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_1",
+        b'\x11\x01\x0b\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_2",
+        b'\x11\x01\x0b\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': "MEMORY_3"
+    }
+
+    memoryKeysLEDs = {
+        "MEMORY_1": b'\x11\x01\x0b\x1a\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',  # M1 LED
+        "MEMORY_2": b'\x11\x01\x0b\x1a\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',  # M2 LED
+        "MEMORY_3": b'\x11\x01\x0b\x1a\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'   # M3 LED
+    }
+
+    releaseEvents = {
+        'KEY_G': b'\x02\x00\x00\x00\x00\x00\x00\x00\x00'  # G release
+    }
+
+    disableGKeys = [
+        b'\x11\x01\x11\x1a\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        b'\x11\x01\x0a\x2a\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        b'\x11\x01\x0f\x5a\x01\x03\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        b'\x11\x01\x0f\x1a\x00\x02\x00\x00\x00\x00\x00\x08\x34\x64\x00\x00\x01\x00\x00\x00',
+        b'\x11\x01\x0f\x1a\x01\x04\x00\x00\x00\x00\x00\x00\x34\x01\x64\x08\x01\x00\x00\x00'
+    ]
+
+
+@dataclass(frozen=True)
+class LogitechG710EventInterface(DeviceEventInterface):
+    macroKeys = {
+        b'\x03\x01\x00\x00': "MACRO_1",
+        b'\x03\x02\x00\x00': "MACRO_2",
+        b'\x03\x04\x00\x00': "MACRO_3",
+        b'\x03\x08\x00\x00': "MACRO_4",
+        b'\x03\x10\x00\x00': "MACRO_5",
+        b'\x03\x20\x00\x00': "MACRO_6",
+        b'\x03\x00\x80\x00': "MEMORY_RECORD"
+    }
+
+    memoryKeys = {
+        b'\x03\x00\x10\x00': "MEMORY_1",
+        b'\x03\x00\x20\x00': "MEMORY_2",
+        b'\x03\x00\x40\x00': "MEMORY_3"
+    }
+
+    memoryKeysLEDs = {
+        "MEMORY_1": b'\x06\x10',  # M1 LED
+        "MEMORY_2": b'\x06\x20',  # M2 LED
+        "MEMORY_3": b'\x06\x40'   # M3 LED
+    }
+
+    mediaKeys = {
+        b'\x02\x08': "KEY_PLAYPAUSE",
+        b'\x02\x04': "KEY_STOP",
+        b'\x02\x02': "KEY_PREVIOUS",
+        b'\x02\x01': "KEY_NEXT",
+        b'\x02\x40': "KEY_MUTE",
+        b'\x02\x10': "KEY_VOLUMEUP",
+        b'\x02\x20': "KEY_VOLUMEDOWN"
+    }
+
+    releaseEvents = {
+        'KEY_G': b'\x03\x00\x00\x00',    # G release
+        'KEY_MEDIA': b'\x02\x00',            # media key release
+        'KEY_M': b'\x03\x00\x00\x00',    # M release
+        'KEY_MR': b'\x03\x00\x00\x00'     # MR release
+    }
+
+    disableGKeys = [b'\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']
+
+
+@dataclass(frozen=True)
+class LogitechG510EventInterface(DeviceEventInterface):
+    macroKeys = {
+        b'\x03\x01\x00\x00\x00': "MACRO_1",
+        b'\x03\x02\x00\x00\x00': "MACRO_2",
+        b'\x03\x04\x00\x00\x00': "MACRO_3",
+        b'\x03\x08\x00\x00\x00': "MACRO_4",
+        b'\x03\x10\x00\x00\x00': "MACRO_5",
+        b'\x03\x20\x00\x00\x00': "MACRO_6",
+        b'\x03\x40\x00\x00\x00': "MACRO_7",
+        b'\x03\x80\x00\x00\x00': "MACRO_8",
+        b'\x03\x00\x01\x00\x00': "MACRO_9",
+        b'\x03\x00\x02\x00\x00': "MACRO_10",
+        b'\x03\x00\x04\x00\x00': "MACRO_11",
+        b'\x03\x00\x08\x00\x00': "MACRO_12",
+        b'\x03\x00\x10\x00\x00': "MACRO_13",
+        b'\x03\x00\x20\x00\x00': "MACRO_14",
+        b'\x03\x00\x40\x00\x00': "MACRO_15",
+        b'\x03\x00\x80\x00\x00': "MACRO_16",
+        b'\x03\x00\x00\x01\x00': "MACRO_17",
+        b'\x03\x00\x00\x02\x00': "MACRO_18",
+        b'\x03\x00\x00\x80\x00': "MEMORY_RECORD"
+    }
+
+    memoryKeys = {
+        b'\x03\x00\x00\x10\x00': "MEMORY_1",
+        b'\x03\x00\x00\x20\x00': "MEMORY_2",
+        b'\x03\x00\x00\x40\x00': "MEMORY_3"
+    }
+
+    mediaKeys = {
+        b'\x02\x08': "KEY_PLAYPAUSE",
+        b'\x02\x04': "KEY_STOP",
+        b'\x02\x02': "KEY_PREVIOUS",
+        b'\x02\x01': "KEY_NEXT",
+        b'\x02\x10': "KEY_MUTE",  # not 100% sure about those last 3
+        b'\x02\x20': "KEY_VOLUMEDOWN",
+        b'\x02\x40': "KEY_VOLUMEUP"
+    }
+
+    releaseEvents = {
+        'KEY_G': b'\x03\x00\x00\x00\x00',   # M and G release
+        'KEY_MEDIA': b'\x02\x00'            # media key release
+    }
+
+    disableGKeys = [
+        b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+        b'\x07\x03\x00',
+        b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+    ]
+
+
+@dataclass(frozen=True)
+class KeyboardInterface:
+
+    deviceName: str
+
+    usbVendor: int
+    usbProduct: int
+    usbConfiguration: int
+    usbInterface: tuple  # tuple[int, int] | (index of interface, index of alternate setting)
+    usbEndpoint: int
+
+    numMacroKeys: int
+    numMemoryKeys: int  # number of memory / profile keys
+
+    events: DeviceEventInterface
+
+    disableGKeysInterface: int
+    disableGKeysUseWrite: bool = field(default=True)
+
+
+@dataclass(frozen=True)
+class LogitechG910OrionSpectrum(KeyboardInterface):
+
+    deviceName = "Logitech G910 Orion Spectrum"
+
+    usbVendor = 0x046d
+    usbProduct = 0xc335
+    usbConfiguration = 0
+    usbInterface = (1, 0)
+    usbEndpoint = 0
+
+    numMacroKeys = 9
+    numMemoryKeys = 3
+
+    disableGKeysInterface = 1
+
+    events = LogitechG910EventInterface
+
+
+@dataclass(frozen=True)
+class LogitechG910OrionSpark(LogitechG910OrionSpectrum):
+    deviceName = "Logitech G910 Orion Spark"
+    usbProduct = 0xc32b
+    disableGKeysInterface = 0
+
+
+@dataclass(frozen=True)
+class LogitechG710p(KeyboardInterface):
+    deviceName = "Logitech G710+"
+
+    usbVendor = 0x046d
+    usbProduct = 0xc24d
+    usbConfiguration = 0
+    usbInterface = (1, 0)
+    usbEndpoint = 0
+
+    numMacroKeys = 6
+    numMemoryKeys = 3
+
+    disableGKeysInterface = 1
+    disableGKeysUseWrite = False
+
+    events = LogitechG710EventInterface
+
+
+@dataclass(frozen=True)
+class LogitechG510(KeyboardInterface):
+    deviceName = "Logitech G510"
+
+    usbVendor = 0x046d
+    usbProduct = 0xc22d
+    usbConfiguration = 0
+    usbInterface = (1, 0)
+    usbEndpoint = 0
+
+    numMacroKeys = 18
+    numMemoryKeys = 3
+
+    disableGKeysInterface = 1
+    disableGKeysUseWrite = False
+
+    events = LogitechG510EventInterface
+
+
+@dataclass(frozen=True)
+class LogitechG815(KeyboardInterface):
+    deviceName = "Logitech G815"
+
+    usbVendor = 0x046d
+    usbProduct = 0xc33f
+    usbConfiguration = 0
+    usbInterface = (1, 0)
+    usbEndpoint = 0
+
+    numMacroKeys = 5
+    numMemoryKeys = 3
+
+    disableGKeysInterface = 1
+
+    events = LogitechG815EventInterface
+
+
+@dataclass(frozen=True)
+class LogitechG915(LogitechG815):
+    deviceName = "Logitech G915 (wired)"
+    usbProduct = 0xc33e
+    events = LogitechG915EventInterface
+
+
+SUPPORTED_DEVICES = [
+    LogitechG915,
+    LogitechG910OrionSpectrum,
+    LogitechG910OrionSpark,
+    LogitechG815,
+    LogitechG710p,
+    LogitechG510
+]
diff --git a/lib/data_mappers/uinput_all_keys.py b/lib/data_mappers/uinput_all_keys.py
index 665e6f6..e6e6ec5 100644
--- a/lib/data_mappers/uinput_all_keys.py
+++ b/lib/data_mappers/uinput_all_keys.py
@@ -1,7 +1,7 @@
 import uinput
 
-uinput_all_keys = \
-    [uinput.KEY_RESERVED,
+uinput_all_keys = [
+    uinput.KEY_RESERVED,
     uinput.KEY_ESC,
     uinput.KEY_1,
     uinput.KEY_2,
@@ -426,6 +426,3 @@
     uinput.KEY_MIN_INTERESTING,
     uinput.KEY_MAX
 ]
-
-
-
diff --git a/lib/develop_wo_keyboard.py b/lib/develop_wo_keyboard.py
deleted file mode 100644
index cf4c68e..0000000
--- a/lib/develop_wo_keyboard.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from lib.functionalities import gkey_functionality
-from lib.keyboard_initialization import usb_and_keyboard_device_init
-import time
-
-def main():
-    device = usb_and_keyboard_device_init.init_uinput_device()
-
-    time.sleep(1)
-    gkey_functionality.g1(device)
-    gkey_functionality.g2(device)
-
diff --git a/lib/functionalities/g910_led.py b/lib/functionalities/g910_led.py
deleted file mode 100644
index 3c8c9fa..0000000
--- a/lib/functionalities/g910_led.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import subprocess
-
-installed = None
-
-def is_installed():
-    global installed
-    if installed is None:
-        which_led_process = subprocess.run(['which', 'g910-led'], capture_output=True)
-        if len(which_led_process.stdout) > 0:
-            installed = True
-        else:
-            installed = False
-
-    return installed
\ No newline at end of file
diff --git a/lib/functionalities/gkey_functionality.py b/lib/functionalities/gkey_functionality.py
deleted file mode 100644
index 333a610..0000000
--- a/lib/functionalities/gkey_functionality.py
+++ /dev/null
@@ -1,120 +0,0 @@
-import ast
-import subprocess
-import inspect
-import sys
-from lib.data_mappers import config_reader, supported_configs
-from lib.misc import logger
-from lib.uinput_keyboard import keyboard
-from lib.functionalities import g910_led
-
-log = logger.logger(__name__)
-
-
-def execute_writing(string_to_write: str, device):
-    keyboard.writeout(string_to_write, config_reader.read()["keyboard_mapping"], device)
-
-
-def execute_hotkey(string_for_hotkey: str, device):
-    keyboard.shortcut(
-        string_for_hotkey, config_reader.read()["keyboard_mapping"], device
-    )
-
-
-def execute_command(command):
-    subprocess.Popen(['/bin/bash', '-c', command], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-
-
-def execute_python(command: str, device):
-    try:
-        global output_string
-        output_string = None
-        ast.parse(command)
-        exec(command)
-        if output_string:
-            keyboard.writeout(output_string,config_reader.read()['keyboard_mapping'],device)
-    except Exception as e:
-        log.error(f"{type(e).__name__} when running python command '{command}'\nDetails: '{str(e)}'")
-
-
-def execute_change_profile(key):
-    supported_configs.profile = key
-
-    if g910_led.is_installed():
-        n = key[1]
-        if n == 'r':
-            subprocess.run(['g910-led', '-mn', '0'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-            subprocess.run(['g910-led', '-mr', '1'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-        elif n == '1' or n == '2':
-            subprocess.run(['g910-led', '-mr', '0'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-            subprocess.run(['g910-led', '-mn', n], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-        else:
-            subprocess.run(['g910-led', '-mr', '0'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-            subprocess.run(['g910-led', '-mn', '4'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-
-    config = config_reader.read()
-    notify = config['notify']
-    username = config['username']
-    if notify == 'True' and username != '':
-        command = f"su {username} -c 'notify-send -i keyboard Logitech-G910 \"Switched profile to {key}\"'"
-        log.debug('/bin/bash -c' + command)
-        subprocess.Popen(['/bin/bash', '-c', command], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-
-
-def release(device):
-    keyboard.release(device)
-
-
-def resolve_config(key):
-    config = config_reader.read()['profiles'].get(supported_configs.profile, "m1")
-
-    if key in [ "m1", "m2", "m3", "mr"]:
-        log.info(f"{key} pressed, change profile to {key}")
-        return lambda _: execute_change_profile(key)
-
-    if key not in config:
-        log.info(f"{key} pressed, unbound in config, doing nothing!")
-        return lambda _: None
-
-    key_config: dict = config[key]
-
-    do = key_config.get('do', supported_configs.default_hotkey_do)
-    if not do:
-        log.info(f"{key} pressed, but do is empty or not set, doing nothing!")
-        return lambda _: None
-
-    command = key_config.get("hotkey_type", supported_configs.default_hotkey_type)
-    if command not in supported_configs.hotkey_types:
-        raise Exception(
-            f'hotkey_type: "{command}" for key "{key}" not known! hotkey_types '
-            f"can only be one of: {supported_configs.hotkey_types}"
-        )
-
-    if command == 'typeout':
-        log.info(f"{key} pressed, typing out: {repr(do)}")
-        return lambda device: execute_writing(do, device)
-    if command == 'shortcut':
-        log.info(f"{key} pressed, pressing: {do}")
-        return lambda device: execute_hotkey(do, device)
-    if command == 'run':
-        log.info(f"{key} pressed, running: {do}")
-        return lambda _: execute_command(do)
-    if command == 'python':
-        log.info(f"{key} pressed, running: {do}")
-        return lambda device: execute_python(do, device)
-
-    # only nothing key config remains
-    log.info(f"{key} pressed, doing nothing!")
-    return lambda _: None
-
-
-def handle_gkey_press(device, key: str):
-    """
-    Handles a key press
-    :param device: the usb device pressed
-    :param key: the string of the gkey pressed (g1-g9)
-    :return: True if keypress was handled, false if it was not
-    """
-    if key in supported_configs.valid_keys:
-        resolve_config(key)(device)
-        return True
-    return False
diff --git a/lib/functionalities/media_static_keys_functionality.py b/lib/functionalities/media_static_keys_functionality.py
deleted file mode 100644
index d288371..0000000
--- a/lib/functionalities/media_static_keys_functionality.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import uinput
-
-
-def volume_up(device):
-    device.emit_click(uinput.KEY_VOLUMEUP)
-
-
-def volume_down(device):
-    device.emit_click(uinput.KEY_VOLUMEDOWN)
-
-
-def volume_mute(device):
-    device.emit_click(uinput.KEY_MUTE)
-
-
-def play_pause(device):
-    device.emit_click(uinput.KEY_PLAYPAUSE)
-
-
-def stop(device):
-    device.emit_click(uinput.KEY_STOPCD)
-
-
-def next_song(device):
-    device.emit_click(uinput.KEY_NEXTSONG)
-
-
-def prev_song(device):
-    device.emit_click(uinput.KEY_PREVIOUSSONG)
-
-
-def resolve_key(device, key):
-    if key == 'vol_down':
-        volume_down(device)
-        return True
-
-    elif key == 'vol_up':
-        volume_up(device)
-        return True
-
-    elif key == 'vol_mute':
-        volume_mute(device)
-        return True
-
-    elif key == 'play_pause':
-        play_pause(device)
-        return True
-
-    elif key == 'stop':
-        stop(device)
-        return True
-
-    elif key == 'prev_song':
-        prev_song(device)
-        return True
-
-    elif key == 'next_song':
-        next_song(device)
-        return True
-    return False
diff --git a/lib/g910_gkey_mapper.py b/lib/g910_gkey_mapper.py
index 4d64b09..e815bbe 100644
--- a/lib/g910_gkey_mapper.py
+++ b/lib/g910_gkey_mapper.py
@@ -1,27 +1,20 @@
-# Taken from https://github.com/CReimer/g910-gkey-uinput/issues/3 and expanded
-
 import fcntl
 import os
-import usb.core
-import usb.util
 import signal
 import time
-from lib.functionalities import gkey_functionality, media_static_keys_functionality
-from lib.data_mappers import command_bytearray, config_reader
-from lib.keyboard_initialization import usb_and_keyboard_device_init
-from lib.misc import logger, paths
-
-log = logger.logger(__name__)
+import uinput
+from lib.misc.config import Config
+from lib.usb_device import USBDevice
+from lib.keyboard import Keyboard
+from lib.misc import notify, memory_leds
+from lib.misc.logger import Logger
 
-program_running=True
-
-def emitKeys(device, key):
-    if gkey_functionality.handle_gkey_press(device, key):
-        return
-    elif key == "release":
-        gkey_functionality.release(device)
-    elif media_static_keys_functionality.resolve_key(device, key):
-        pass
+config: Config = Config()
+log = Logger().logger(__name__)
+notifier = notify.Notification(config)
+program_running: bool = True
+device: USBDevice
+keyboard: Keyboard
 
 
 def signal_handler(sig, frame):
@@ -31,13 +24,21 @@ def signal_handler(sig, frame):
 
 
 def config_changed_handler(sig, frame):
-    log.info("config changed")
+    log.info(f"Got signal, {signal.Signals(sig).name} config changed!")
     time.sleep(0.5)
-    config_reader.update_config()
+    config.update_config()
+
+
+def change_profile(dev: USBDevice, profile: str):
+    log.info(f"Change profile to {profile}.")
+    config.profile = profile
+    if memory_leds.change_profile(dev, profile):
+        log.debug(f"Changed memory key led for {profile}.")
+    notifier.send_notification(profile)
 
 
 def main():
-    global program_running
+    global program_running, device, keyboard
     log.info("--------------------------------------------------------------------------------")
     log.info(f"----------------------STARTED g910-keys-pid:{str(os.getpid())}------------------------------")
     log.info("--------------------------------------------------------------------------------")
@@ -47,65 +48,72 @@ def main():
     signal.signal(signal.SIGQUIT, signal_handler)
     signal.signal(signal.SIGIO, config_changed_handler)
 
-    # sends signal if config is changed (or really anything in the config directory)
-    fd = os.open(paths.config_dir, os.O_RDONLY)
-    fcntl.fcntl(fd, fcntl.F_SETSIG, 0)
-    fcntl.fcntl(fd, fcntl.F_NOTIFY,
-                fcntl.DN_MODIFY | fcntl.DN_CREATE | fcntl.DN_MULTISHOT)
-    # To see if config exists
-    config_reader.read()
-    
     if program_running:
-        # uinput device
-        log.debug("gathering uinput device")
-        device = usb_and_keyboard_device_init.init_uinput_device()
-        # usb device
-        dev, endpoint, USB_TIMEOUT, USB_IF = usb_and_keyboard_device_init.init_g910_keyboard()
+        # usb keyboard
+        device = USBDevice()
+        # check paths, create config directory and default config if not exists
+        config.check_paths(config, device)
+        # sends signal if config is changed (or really anything in the config directory)
+        fd = os.open(config.config_dir, os.O_RDONLY)
+        fcntl.fcntl(fd, fcntl.F_SETSIG, 0)
+        fcntl.fcntl(fd, fcntl.F_NOTIFY, fcntl.DN_MODIFY | fcntl.DN_CREATE | fcntl.DN_MULTISHOT)
+        # To see if config exists
+        config.read()
+        # disable macro key default f key binding on interface 0
+        device.disable_fkey_to_gkey_binding()
+        # uinput keyboard
+        keyboard = Keyboard(config)
+        keyboard.set_keyboard(device.keyboard)
+        # set first profile out of event interface
+        change_profile(device, list(keyboard.keyboard.events.memoryKeys.values())[0])
 
     while program_running:
-        try:
-            usb.util.claim_interface(dev, USB_IF)
-            # log.debug("reading control values")
-            control = dev.read(endpoint.bEndpointAddress, endpoint.wMaxPacketSize, USB_TIMEOUT)
-            usb.util.release_interface(dev, USB_IF)
-
-            if control:
-                b = bytearray(control)
-                if b in command_bytearray.commands.values():
-                    key = list(command_bytearray.commands.keys())[
-                        list(command_bytearray.commands.values()).index(b)]
-                    log.debug(f"Pressed {key}, bytecode: {b}")
-                    emitKeys(device, key)
-                elif b[:3] in (bytearray(b'\x11\xff\x0f'), bytearray(b'\x11\xff\x10'), bytearray(b'\x11\xff\xff')):
-                    #Suppress warnings on these values, these are return values from LEDs being set.
-                    pass
-                else:
-                    log.warning(str(b) + ' no match')
-        except SystemExit:
-            program_running = False
-        except Exception as e:
-            if e.args[0] == 110:
-                pass
-            elif e.args[0] == 19 or e.args[0] == 5:
-                try:
-                    dev, endpoint, USB_TIMEOUT, USB_IF = usb_and_keyboard_device_init.init_g910_keyboard()
-                except:
-                    pass
-            else:
-                log.exception(e)
+        loop()
 
-        time.sleep(0.001)
+    try:
+        keyboard.__exit__()
+        device.__exit__()
+    except AttributeError:
+        pass  # pass if no uinput or usb keyboard is assigned
+
+    log.info("------------------------------------EXITING-------------------------------------")
 
+
+def loop():
     try:
-        if device:
-            device.__exit__()
-            log.info("Removed uinput device")
-    except UnboundLocalError:
-        pass # pass if no device is assigned
+        control = device.read()  # listen for incoming traffic from usb device
+
+        if control:
+            b = bytes(control)
+            if b in device.keyboard.events.macroKeys.keys():
+                key = list(device.keyboard.events.macroKeys.values())[
+                    list(device.keyboard.events.macroKeys.keys()).index(b)]
+                log.debug(f"Event {key}, bytecode: {b}")
+                keyboard.emit_keys(key)
+            elif b in device.keyboard.events.memoryKeys.keys():
+                profile = list(device.keyboard.events.memoryKeys.values())[
+                    list(device.keyboard.events.memoryKeys.keys()).index(b)]
+                change_profile(device, profile)
+            elif b in device.keyboard.events.mediaKeys.keys():
+                key = list(device.keyboard.events.mediaKeys.values())[
+                    list(device.keyboard.events.mediaKeys.keys()).index(b)]
+                log.debug(f"Media key {key} pressed, bytecode: {b}")
+                keyboard.device.emit_click(eval(f"uinput.{key}"))
+            elif b in device.keyboard.events.releaseEvents.keys():
+                release = list(device.keyboard.events.releaseEvents.values())[
+                    list(device.keyboard.events.releaseEvents.keys()).index(b)]
+                log.debug(f"Release event {release}.")
+                keyboard.release()
+            elif b[:3] in (bytearray(b'\x11\xff\x0f'), bytearray(b'\x11\xff\x10'), bytearray(b'\x11\xff\xff')):
+                pass  # Suppress warnings on these values, these are return values from LEDs being set.
+            else:
+                log.warning(str(b) + ' no match')
+    except SystemExit:
+        signal.raise_signal(signal.SIGQUIT)
     except Exception as e:
-        log.error("Could not remove uinput device.",e)
+        log.exception(e)
+        signal.raise_signal(signal.SIGQUIT)
 
-    log.info("------------------------------------EXITING-------------------------------------")
 
 if __name__ == "__main__":
     main()
diff --git a/lib/keyboard.py b/lib/keyboard.py
new file mode 100644
index 0000000..34a8f5a
--- /dev/null
+++ b/lib/keyboard.py
@@ -0,0 +1,168 @@
+import ast
+import time
+import uinput
+import subprocess
+from lib.data_mappers import supported_configs
+from lib.data_mappers.char_uinput_mapper import keys, reverse_keys
+from lib.data_mappers.uinput_all_keys import uinput_all_keys
+from lib.data_mappers.supported_devices import KeyboardInterface
+from lib.misc.logger import Logger
+
+output_string = ''
+
+
+class Keyboard:
+    log = None
+    # todo: this wouldn't be needed, so the whole release function
+    press_release_fifo = []
+    device = None
+    keyboard: KeyboardInterface = None
+    locale: str = 'en'
+    config = None
+
+    def __init__(self, config):
+        self.log = Logger().logger(__name__)
+        self.config = config
+        self.log.debug("gathering uinput keyboard")
+        self.device = uinput.Device(uinput_all_keys)
+        self.log.debug("got uinput keyboard: " + str(self.device))
+        self.locale = self.config.read().get("keyboard_mapping", supported_configs.default_keyboard_mapping)
+        self.log.debug(f"Set {self.locale} uinput mapping.")
+        time.sleep(1)  # wait till keyboard is fully initialized
+
+    def __enter__(self):
+        return self
+
+    def set_locale(self, locale: str):
+        self.locale = locale
+
+    def set_keyboard(self, keyboard: KeyboardInterface):
+        self.keyboard = keyboard
+
+    def emit_keys(self, key):
+        # config = self.config.read()['profiles'].get(supported_configs.profile, "MEMORY_1")
+        config = self.config.get_profile()
+
+        if key not in config:
+            self.log.info(f"{key} pressed, unbound in config, doing nothing!")
+            return lambda _: None
+
+        key_config: dict = config[key]
+
+        do = key_config.get('do', supported_configs.default_hotkey_do)
+        if not do:
+            self.log.info(f"{key} pressed, but do is empty or not set, doing nothing!")
+            return lambda _: None
+
+        command = key_config.get("hotkey_type", supported_configs.default_hotkey_type)
+        if command not in supported_configs.hotkey_types:
+            raise Exception(
+                f'hotkey_type: "{command}" for key "{key}" not known! hotkey_types '
+                f"can only be one of: {supported_configs.hotkey_types}"
+            )
+
+        if command == 'typeout':
+            self.log.info(f"{key} pressed, typing out: {repr(do)}")
+            self.execute_writing(do)
+        elif command == 'shortcut':
+            self.log.info(f"{key} pressed, pressing: {do}")
+            self.execute_hotkey(do)
+        elif command == 'run':
+            self.log.info(f"{key} pressed, running: {do}")
+            self.execute_command(do)
+        elif command == 'python':
+            self.log.info(f"{key} pressed, running: {do}")
+            self.execute_python(do)
+        elif command == 'uinput':
+            self.log.info(f"{key} pressed, mapped to: {do}")
+            self.execute_events([(eval(f"uinput.{do}"), 3)])
+        else:
+            # only nothing key config remains
+            self.log.info(f"{key} pressed, doing nothing!")
+
+    def execute_events(self, events):
+        for event in events:
+            if event[1] == 3:
+                self.device.emit_click(event[0])
+            else:
+                # todo: check why emit is used instead of emit_combo?
+                #       This will only trigger pressed or released event.
+                self.device.emit(*event)
+
+            time.sleep(0.02)
+
+    def execute_writing(self, string_to_write: str):
+        for c in string_to_write:
+            self.execute_events(keys[self.locale][c])
+
+    def execute_hotkey(self, string_for_hotkey: str):
+        press_together = []
+        together = string_for_hotkey.split(",")
+
+        for hotkey in together:
+            keys_str = hotkey.split("+")
+            press_together.append(keys_str)
+
+        uinput_groups = []
+        for combo in press_together:
+            uinput_combo = []
+            for key_string in combo:
+                if len(key_string) == 1:
+                    uinput_combo.append(keys[self.locale][key_string][0][0])
+                else:
+                    uinput_combo.append(keys["control"][key_string][0][0])
+
+            uinput_groups.append(uinput_combo)
+
+        if len(uinput_groups) == 1:
+            # todo: this could be one line, why is emit used here?
+            # self.keyboard.emit_combo(uinput_groups[0])
+            keys_to_fifo = []
+            for uinput_key in uinput_groups[0]:
+                keys_to_fifo.insert(0, uinput_key)
+                self.device.emit(uinput_key, 1)
+                time.sleep(0.02)
+            self.press_release_fifo.insert(0, keys_to_fifo)
+        else:
+            for group in uinput_groups:
+                self.device.emit_combo(group)
+
+    @staticmethod
+    def execute_command(command):
+        subprocess.Popen(['/bin/bash', '-c', command], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+    def execute_python(self, command: str):
+        try:
+            global output_string
+            output_string = ''
+            ast.parse(command)
+            exec(command)
+            if output_string:
+                self.execute_writing(output_string)
+        except Exception as e:
+            self.log.error(f"{type(e).__name__} when running python command '{command}'\nDetails: '{str(e)}'")
+
+    def release(self):
+        if not len(self.press_release_fifo):
+            return
+        keys_to_release = self.press_release_fifo.pop(0)
+        to_log = ""
+        for key in keys_to_release:
+            self.device.emit(key, 0)
+            to_log += " " + reverse_keys[key] + ","
+
+        self.log.info("Released keys:" + to_log[:-1] + " Shortcuts to release: " + str(
+            len(self.press_release_fifo)) + " (Should always be 0)")
+
+    def __exit__(self):
+        try:
+            self.device.__exit__()
+            self.log.info("Removed uinput device")
+        except UnboundLocalError:
+            pass
+        except Exception as e:
+            self.log.error("Could not remove uinput device: " + str(e))
+
+
+class KeyInputTimeoutException(Exception):
+    pass
diff --git a/lib/keyboard_initialization/__init__.py b/lib/keyboard_initialization/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/keyboard_initialization/usb_and_keyboard_device_init.py b/lib/keyboard_initialization/usb_and_keyboard_device_init.py
deleted file mode 100644
index 1008571..0000000
--- a/lib/keyboard_initialization/usb_and_keyboard_device_init.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import time
-import uinput
-import usb
-from lib.data_mappers import uinput_all_keys
-from lib.misc import logger
-from lib import g910_gkey_mapper
-
-log = logger.logger(__name__)
-
-def init_uinput_device():
-    device = uinput.Device(uinput_all_keys.uinput_all_keys)
-    log.debug("got uinput device: " + str(device))
-    return device
-
-
-def init_usb_dev():
-    USB_VENDOR = 0x046d
-    USB_PRODUCT = (0x0c32b, 0xc335)
-    USB_IF = 1  # Interface
-    USB_TIMEOUT = 5  # Timeout in MS
-
-    select_product = 0
-    dev = usb.core.find(idVendor=USB_VENDOR, idProduct=USB_PRODUCT[select_product])
-    while not dev:
-        select_product = abs(select_product - 1)
-        log.debug("usb dev: not found. retrying with other product id")
-        dev = usb.core.find(idVendor=USB_VENDOR, idProduct=USB_PRODUCT[select_product])
-        time.sleep(0.1)
-
-    log.debug("usb dev: " + str(dev[0][(1, 0)]))
-    endpoint = dev[0][(1, 0)][0]
-
-    if dev.is_kernel_driver_active(USB_IF) is True:
-        log.debug("detaching kernel driver")
-        dev.detach_kernel_driver(USB_IF)
-
-    return dev, endpoint, USB_TIMEOUT, USB_IF
-
-
-def disable_fkey_to_gkey_binding(dev, endpoint, USB_TIMEOUT, USB_IF):
-    usb.util.claim_interface(dev, USB_IF)
-    log.info("Trying to disable mapping")
-    log.debug("Sending disable")
-    packet = [0x11, 0xff, 0x08, 0x2e, 0x2]
-    for _ in range(len(packet), 20):
-        packet.append(0)
-    disable_return = dev.ctrl_transfer(0x21, 0x09, 0x0212, 1, packet, 2000)  # write(1,packet,USB_TIMEOUT))
-    log.debug("Got: " + str(disable_return))
-    time.sleep(0.001)
-
-    buffer = []
-    for _ in range(64):
-        buffer.append(0)
-
-    # Don't really know why this has to be send but its included in the original g810-leds
-    ret = dev.write(0x82, buffer, 2000)
-    log.debug("Got: " + str(ret))
-
-    operation_successful = [17, 255, 8, 46, 2]
-    while 1:
-        time.sleep(0.001)
-        confirmation_bytes = dev.read(endpoint.bEndpointAddress, endpoint.wMaxPacketSize, USB_TIMEOUT)
-        if list(confirmation_bytes[:5]) == operation_successful:
-            log.info("Mapping disabled successfully")
-            break
-    usb.util.release_interface(dev, USB_IF)
-    return True
-
-
-def init_g910_keyboard():
-    dev = endpoint = USB_TIMEOUT = USB_IF = None
-    counter = 0
-    log.debug("gathering usb device")
-    while not dev:
-        try:
-            dev, endpoint, USB_TIMEOUT, USB_IF = init_usb_dev()
-        except SystemExit:
-            g910_gkey_mapper.program_running = False
-        except Exception as e:
-            if e.args[0] == 5:
-                counter += 1
-                if counter >= 1000:
-                    log.warning("USBError: Input/Output Error")
-                    counter = 0
-                time.sleep(0.01)
-            else:
-                log.warning("Getting keyboard error: " + str(e))
-
-    successfully_disabled_mapping = False
-    counter = 0
-    while not successfully_disabled_mapping:
-        try:
-            successfully_disabled_mapping = disable_fkey_to_gkey_binding(dev, endpoint, USB_TIMEOUT, USB_IF)
-        except SystemExit:
-            g910_gkey_mapper.program_running = False
-        except Exception as e:
-            if e.args[0] == 110:
-                counter += 1
-                if counter >= 5:
-                    log.warning("Can't disable mapping")
-                    counter = 0
-                time.sleep(0.01)
-            else:
-                log.warning("Disable mapping error: " + str(e))
-                try:
-                    dev, endpoint, USB_TIMEOUT, USB_IF = init_usb_dev()
-                except SystemExit:
-                    g910_gkey_mapper.program_running = False
-                except:
-                    pass
-
-    return dev, endpoint, USB_TIMEOUT, USB_IF
diff --git a/lib/misc/config.py b/lib/misc/config.py
new file mode 100644
index 0000000..f2bdac5
--- /dev/null
+++ b/lib/misc/config.py
@@ -0,0 +1,227 @@
+import json
+import os
+import signal
+from json.decoder import JSONDecodeError
+from lib.misc.helper import Helper
+from lib.data_mappers import supported_configs, char_uinput_mapper
+from lib.misc.logger import Logger
+
+
+class ConfigException(Exception):
+    pass
+
+
+class Config:
+
+    config: dict = None
+
+    profile: str = None
+
+    config_dir: str
+
+    config_path: str
+
+    logs_path: str
+
+    def __init__(self):
+        self.profile = supported_configs.default_profile
+        # set config path
+        if Helper.is_installed():
+            self.config_dir = "/etc/g910-gkeys"
+            self.config_path = self.config_dir + "/config.json"
+        else:
+            main_dir = Helper.get_base_path()
+            self.config_dir = os.path.join(main_dir, "config")
+            self.config_path = self.config_dir + "/config.json"
+        # set log path
+        if os.geteuid() != 0:
+            self.logs_path = "g910-gkeys.log"
+        else:
+            self.logs_path = "/var/log/g910-gkeys.log"
+
+    @staticmethod
+    def validate_hotkey_action(do, hotkey_action, keyboard_mapping):
+        if hotkey_action == "nothing" or hotkey_action == "run":
+            return None
+        if hotkey_action == "typeout":
+            error = {}
+            i = 0
+            error["typeout"] = ""
+            for c in do:
+                try:
+                    char_uinput_mapper.keys[keyboard_mapping][c]
+                except KeyError:
+                    error["typeout"] += f"Character \"{c}\" on position {str(i)} does not exist for keyboard mapping " \
+                                        f"{keyboard_mapping}!"
+                i += 1
+            if error["typeout"] == "":
+                return None
+            return error
+        if hotkey_action == "shortcut":
+            error = {}
+            press_together = []
+
+            together = do.split(",")
+            for hotkey in together:
+                keys_str = hotkey.split("+")
+                press_together.append(keys_str)
+
+            error["shortcut"] = ""
+            i = 0
+            for combo in press_together:
+                for key_string in combo:
+                    i += 1
+                    if len(key_string) == 1:
+                        try:
+                            char_uinput_mapper.keys[keyboard_mapping][key_string]
+                        except KeyError:
+                            error["shortcut"] += f"Character {key_string} on position {str(i)} for keyboard mapping: " \
+                                                 f"{keyboard_mapping} does not exist! "
+
+                    else:
+                        try:
+                            char_uinput_mapper.keys["control"][key_string]
+                        except KeyError:
+                            error["shortcut"] += "Control key "+key_string+" on position "+str(i)+" does not exist! "
+            if error["shortcut"] == "":
+                return None
+            else:
+                return error
+
+    def validate_config(self, config_dic: dict):
+        errors = []
+        keyboard_mapping = config_dic.get("keyboard_mapping", supported_configs.default_keyboard_mapping)
+        return_config = {"keyboard_mapping": keyboard_mapping}
+        if keyboard_mapping not in supported_configs.keyboard_mappings:
+            return {"keyboard_mapping": keyboard_mapping+" does not exist!"}, None
+
+        return_config["notify"] = config_dic.get("notify", False)
+        return_config["username"] = config_dic.get("username", "")
+
+        profile_start_range = 1
+        return_config["profiles"] = {}
+        return_config["profiles"]["MEMORY_1"] = {}
+        # backward compatibility if no profile is set in config default is used
+        if config_dic.get("profiles", {}) is {}:
+            profile_start_range = 2
+            for i in range(1, 10):
+                try:
+                    return_config["profiles"]["MEMORY_1"]["MACRO_" + str(i)] = \
+                        self.get_key_action(config_dic, "backward_compatibility", i)
+                except ConfigException as e:
+                    errors += [e]
+
+        for profile_index in range(profile_start_range, 5):
+            if profile_index == 4:
+                profile_index = "RECORD"
+
+            return_config["profiles"]["MEMORY_" + str(profile_index)] = {}
+            for i in range(1, 10):
+                try:
+                    return_config["profiles"][f"MEMORY_{str(profile_index)}"][f"MACRO_{str(i)}"] = \
+                        self.get_key_action(config_dic, profile_index, i)
+                except ConfigException as e:
+                    errors += [e]
+
+        if len(errors) > 0:
+            raise ConfigException(errors)
+        else:
+            return return_config
+
+    def get_key_action(self, config_dic: dict, profile_index: str, i: int):
+        keyboard_mapping = config_dic.get("keyboard_mapping", supported_configs.default_keyboard_mapping)
+        key = "MACRO_" + str(i)
+        if profile_index != "backward_compatibility":
+            if config_dic.get("profiles", {}).get(f"MEMORY_{str(profile_index)}", {}) == {}:
+                # compatibility < 0.3.0
+                key = "g" + str(i)
+                setting_for_gkey = config_dic.get("profiles", {}) \
+                    .get("m" + str(profile_index), {}) \
+                    .get(key, {})
+            else:  # > 0.3.0
+                setting_for_gkey = config_dic.get("profiles", {}) \
+                    .get("MEMORY_" + str(profile_index), {}) \
+                    .get(key, {})
+        else:  # compatibility mode for old configs without profiles < 0.2.5
+            key = "g" + str(i)
+            setting_for_gkey = config_dic.get(key, {})
+
+        hotkey_type = setting_for_gkey.get("hotkey_type", supported_configs.default_hotkey_type)
+        if hotkey_type not in supported_configs.hotkey_types:
+            raise ConfigException(f"{key} - hotkey_type {hotkey_type} does not exist!")
+        do = setting_for_gkey.get("do", supported_configs.default_hotkey_do)
+        do_validation = self.validate_hotkey_action(do, hotkey_type, keyboard_mapping)
+        if do_validation:
+            raise ConfigException(f"{key} - Do validation failed: {do_validation}")
+        return {"hotkey_type": hotkey_type, "do": do}
+
+    def update_config(self):
+        self.config = self.read()
+
+    def read(self):
+        log = Logger().logger(__name__)
+        if self.config:
+            return self.config
+
+        log.info("Reading config from " + self.config_path)
+        try:
+            with open(self.config_path, "r") as f:
+                self.config = self.validate_config(json.load(f))
+                return self.config
+        except JSONDecodeError as e:
+            log.error(f"JSONDecodeError: {str(e)} in {self.config_path}")
+        except ConfigException as e:
+            log.warning(str(e))
+        except FileNotFoundError:
+            # this should no longer occur since config is created at start of the service if not exists
+            log.error("No config found.")
+        except Exception as e:
+            log.exception(e)
+        signal.raise_signal(signal.SIGQUIT)
+
+    def load(self):
+        with open(self.config_path, "r") as f:
+            return json.load(f)
+
+    def get_profile(self):
+        return self.read().get('profiles', {}).get(self.profile, {})
+
+    def exists(self):
+        return os.path.exists(self.config_path)
+
+    # Creates config
+    def create(self, keyboard):
+        log = Logger().logger(__name__)
+        if self.exists():  # backup config if there is already one
+            log.info("Backing up existing config to: " + self.config_path + ".bak")
+            os.rename(self.config_path, self.config_path + ".bak")
+
+        # detect current locale and set as keyboard mapping
+        user_lang = Helper.get_locale()
+        # check for support and otherwise set to default
+        if user_lang not in supported_configs.keyboard_mappings:
+            log.warning(f"No support for {user_lang} keyboard layout. Using default (en).")
+            user_lang = "en"
+        config = {
+            "keyboard_mapping": user_lang,
+            "notify": "False",
+            "username": "",
+            "profiles": {}
+        }
+        for profile in keyboard.events.memoryKeys.values():
+            config["profiles"].update({profile: {}})
+
+            for macro_key in keyboard.events.macroKeys.values():
+                key = "KEY_F" + str(12 + int(macro_key.split("_")[1]))
+                config["profiles"][profile][macro_key] = {"hotkey_type": "uinput", "do": key}
+
+        with open(self.config_path, "w") as f:
+            log.info("Creating default config: " + self.config_path)
+            f.write(json.dumps(config, indent=4))
+
+    def check_paths(self, config, device):
+        if not os.path.exists(self.config_dir):
+            print("Creating directory: "+self.config_dir)
+            os.makedirs(self.config_dir)
+        if not os.path.exists(self.config_path):
+            config.create(device.keyboard)  # create config with keyboard interface
diff --git a/lib/misc/create_config.py b/lib/misc/create_config.py
deleted file mode 100644
index 03b4885..0000000
--- a/lib/misc/create_config.py
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-import os
-import json
-from lib.misc import paths
-
-
-def initialize_config():
-    config = {}
-    config["__comment"] = "following hotkey types are supported: nothing, typeout, shortcut, run and python; only en, fr, de and si keyboard mappings are currently supported"
-    config["keyboard_mapping"] = "en"
-    config["profiles"] = {
-        "m1": {}, "m2": {}, "m3": {}, "mr": {}
-    }
-    for mkey in range(1, 4):
-        config["profiles"]["m" + str(mkey)]["g1"] = {"hotkey_type": "typeout", "do": "Its WORKING!!!"}
-
-        for i in range(2, 10):
-            config["profiles"]["m" + str(mkey)]["g" + str(i)] = {"hotkey_type": "nothing", "do": ""}
-
-    return config
-
-
-def does_config_exists():
-    return os.path.exists(paths.config_path)
-
-
-# Creates config at home
-def create():
-    if does_config_exists():
-        print("Backuping existing config to: " + paths.config_path + ".bak")
-        os.rename(paths.config_path, paths.config_path + ".bak")
-
-    with open(paths.config_path, "w") as f:
-        print("Writting config at: " + paths.config_path)
-        f.write(json.dumps(initialize_config(), indent=4))
diff --git a/lib/misc/helper.py b/lib/misc/helper.py
new file mode 100644
index 0000000..9f56873
--- /dev/null
+++ b/lib/misc/helper.py
@@ -0,0 +1,22 @@
+import locale
+import os
+import sys
+
+
+class Helper:
+
+    @staticmethod
+    def is_installed():
+        command = sys.argv[0].split("/")[-1]
+        return command == "g910-gkeys"
+
+    @staticmethod
+    def get_locale():
+        # detect current locale and set as keyboard mapping
+        user_lang, encoding = locale.getdefaultlocale()
+        return user_lang.split("_")[0]
+
+    @staticmethod
+    def get_base_path():
+        main_dir = os.path.abspath(os.getcwd())
+        return main_dir
diff --git a/lib/misc/is_installed.py b/lib/misc/is_installed.py
deleted file mode 100644
index 36c4fb8..0000000
--- a/lib/misc/is_installed.py
+++ /dev/null
@@ -1,5 +0,0 @@
-import sys
-
-def is_installed():
-    command = sys.argv[0].split("/")[-1]
-    return command!="launcher.py"
diff --git a/lib/misc/layout_config_helpers.py b/lib/misc/layout_config_helpers.py
deleted file mode 100644
index e6f82e9..0000000
--- a/lib/misc/layout_config_helpers.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# this file has function that can help you get the right config for your layout. it is not optimized i dont even promise
-# that it will work when u run it, if there's interest i can beautify it so it will be simpler to use. open an issue or smth idk.
-import uinput
-from lib.data_mappers import uinput_all_keys
-from lib.data_mappers.char_uinput_mapper import keys
-
-def click(uinput_key):
-    return [(uinput_key, 3)]
-
-def wrap_shift(key_array):
-    return [(uinput.KEY_LEFTSHIFT, 1)]+key_array+[(uinput.KEY_LEFTSHIFT, 0)]
-
-
-def wrap_altgr(key_array):
-    return [(uinput.KEY_RIGHTALT, 1)]+key_array+[(uinput.KEY_RIGHTALT, 0)]
-
-def get_shift_keys():
-    import sys
-    import time
-    time.sleep(5)
-    device = uinput.Device(uinput_all_keys.uinput_all_keys)
-    for i in range(48, 58):
-        sys.stdout.write("'")
-        sys.stdout.flush()
-
-        for key in wrap_shift(click(getattr(uinput, "KEY_" + chr(i)))):
-            time.sleep(0.1)
-            device.emit(*key)
-        sys.stdout.write("': wrap_shift(click(uinput.KEY_" + chr(i) + ")),\n")
-        sys.stdout.flush()
-
-def get_altgr_keys():
-    import sys
-    import time
-    time.sleep(5)
-    device = uinput.Device(uinput_all_keys.uinput_all_keys)
-    for i in range(48, 58):
-        sys.stdout.write("'")
-        sys.stdout.flush()
-
-        for key in wrap_altgr(click(getattr(uinput, "KEY_" + chr(i)))):
-            time.sleep(0.1)
-            device.emit(*key)
-        device.emit_click(uinput.KEY_SPACE)
-        time.sleep(0.1)
-        sys.stdout.write("': wrap_altgr(click(uinput.KEY_" + chr(i) + ")+click(uinput.KEY_SPACE)),\n")
-        sys.stdout.flush()
-
-def get_other_keys():
-    keys = {"uinput.KEY_MINUS" : uinput.KEY_MINUS,
-"uinput.KEY_EQUAL" : uinput.KEY_EQUAL,
-"uinput.KEY_BACKSPACE" : uinput.KEY_BACKSPACE,
-"uinput.KEY_TAB" : uinput.KEY_TAB,
-"uinput.KEY_Q" : uinput.KEY_Q,
-"uinput.KEY_W" : uinput.KEY_W,
-"uinput.KEY_E" : uinput.KEY_E,
-"uinput.KEY_R" : uinput.KEY_R,
-"uinput.KEY_T" : uinput.KEY_T,
-"uinput.KEY_Y" : uinput.KEY_Y,
-"uinput.KEY_U" : uinput.KEY_U,
-"uinput.KEY_I" : uinput.KEY_I,
-"uinput.KEY_O" : uinput.KEY_O,
-"uinput.KEY_P" : uinput.KEY_P,
-"uinput.KEY_LEFTBRACE" : uinput.KEY_LEFTBRACE,
-"uinput.KEY_RIGHTBRACE" : uinput.KEY_RIGHTBRACE,
-"uinput.KEY_ENTER" : uinput.KEY_ENTER,
-"uinput.KEY_LEFTCTRL" : uinput.KEY_LEFTCTRL,
-"uinput.KEY_A" : uinput.KEY_A,
-"uinput.KEY_S" : uinput.KEY_S,
-"uinput.KEY_D" : uinput.KEY_D,
-"uinput.KEY_F" : uinput.KEY_F,
-"uinput.KEY_G" : uinput.KEY_G,
-"uinput.KEY_H" : uinput.KEY_H,
-"uinput.KEY_J" : uinput.KEY_J,
-"uinput.KEY_K" : uinput.KEY_K,
-"uinput.KEY_L" : uinput.KEY_L,
-"uinput.KEY_SEMICOLON" : uinput.KEY_SEMICOLON,
-"uinput.KEY_APOSTROPHE" : uinput.KEY_APOSTROPHE,
-"uinput.KEY_GRAVE" : uinput.KEY_GRAVE,
-"uinput.KEY_LEFTSHIFT" : uinput.KEY_LEFTSHIFT,
-"uinput.KEY_BACKSLASH" : uinput.KEY_BACKSLASH,
-"uinput.KEY_Z" : uinput.KEY_Z,
-"uinput.KEY_X" : uinput.KEY_X,
-"uinput.KEY_C" : uinput.KEY_C,
-"uinput.KEY_V" : uinput.KEY_V,
-"uinput.KEY_B" : uinput.KEY_B,
-"uinput.KEY_N" : uinput.KEY_N,
-"uinput.KEY_M" : uinput.KEY_M,
-"uinput.KEY_COMMA" : uinput.KEY_COMMA,
-"uinput.KEY_DOT" : uinput.KEY_DOT,
-"uinput.KEY_SLASH" : uinput.KEY_SLASH,
-"uinput.KEY_RIGHTSHIFT" : uinput.KEY_RIGHTSHIFT,
-"uinput.KEY_KPASTERISK" : uinput.KEY_KPASTERISK,
-"uinput.KEY_LEFTALT" : uinput.KEY_LEFTALT,
-"uinput.KEY_SPACE" : uinput.KEY_SPACE,
-"uinput.KEY_CAPSLOCK" : uinput.KEY_CAPSLOCK}
-    import sys
-    import time
-    time.sleep(5)
-    device = uinput.Device(uinput_all_keys.uinput_all_keys)
-    for val,key in keys.items():
-        sys.stdout.write("'")
-        sys.stdout.flush()
-        for key_event in click(key):
-            time.sleep(0.1)
-            device.emit(*key_event)
-        #device.emit_click(uinput.KEY_SPACE)
-        #time.sleep(0.1)
-        sys.stdout.write("': click("+val+"),\n")
-        sys.stdout.flush()
-
-
-def get_keys():
-    for i in range(48,58):
-        print("'"+chr(i)+"': click(uinput.KEY_"+chr(i)+"),")
-
-    for i in range(65,91):
-        #if chr(i) == 'Z':
-        #    print("'y': click(uinput.KEY_Z),")
-        #    print("'Y': wrap_shift(click(uinput.KEY_Z)),")
-        #elif chr(i) == 'Y':
-        #    print("'z': click(uinput.KEY_Y),")
-        #    print("'Z': wrap_shift(click(uinput.KEY_Y)),")
-
-        #else:
-        print("'"+chr(i).lower()+"': click(uinput.KEY_"+chr(i)+"),")
-        print("'"+chr(i)+"': wrap_shift(click(uinput.KEY_"+chr(i)+")),")
-
-def get_fkeys():
-    for i in range(1,13):
-        print("'F"+str(i)+"': click(uinput.KEY_F"+str(i)+"),")
-
-
-def execute_events(events, device):
-    import time
-    for event in events:
-        if event[1] == 3:
-            device.emit_click(event[0])
-        else:
-            device.emit(*event)
-
-        time.sleep(0.02)
-
-
-def writeout(string, charset, device):
-    for c in string:
-        execute_events(keys[charset][c],device)
-
-def test():
-    import sys
-    import time
-
-    device = uinput.Device(uinput_all_keys.uinput_all_keys)
-    time.sleep(4)
-    events = keys['en']
-    for key, event in events.items():
-        sys.stdout.write("'"+key+"'=='")
-        sys.stdout.flush()
-        time.sleep(0.1)
-        execute_events(event,device)
-        time.sleep(0.1)
-        sys.stdout.write("'\n")
-
-if __name__ == "__main__":
-    #print(keys)
-    #get_keys()
-    #get_shift_keys()
-    #get_altgr_keys()
-    #get_other_keys()
-    #test()
-    import uinput_all_keys
-
-    import time
-    device = uinput.Device(uinput_all_keys.uinput_all_keys)
-
-    time.sleep(4)
-    #writeout("šđčćžŠĐČĆŽ\n","si", device)
-    #get_fkeysfdsaf( )
diff --git a/lib/misc/logger.py b/lib/misc/logger.py
index da351d1..bae6410 100644
--- a/lib/misc/logger.py
+++ b/lib/misc/logger.py
@@ -1,16 +1,47 @@
 import logging
-from lib.misc import paths
+import os
 
 
-fh = logging.FileHandler(paths.logs_path, "a")
-fh.setLevel(logging.DEBUG)
-formatter = logging.Formatter('%(asctime)s [%(levelname)s] {%(name)s} - %(message)s')
-fh.setFormatter(formatter)
+class Logger:
 
+    file_handler = None
 
-def logger(name):
-    log = logging.getLogger(name)
-    log.setLevel(logging.INFO)
-    log.propagate = False
-    log.addHandler(fh)
-    return log
+    log_level: int
+
+    log_level_map = {
+        'DEBUG': logging.DEBUG,
+        'INFO': logging.INFO,
+        'WARNING': logging.WARNING,
+        'ERROR': logging.ERROR
+    }
+
+    def __init__(self, log_level: int = logging.DEBUG):
+        from lib.misc.config import Config
+        config = Config()
+        log_level = config.load().get('log_level', 'INFO')
+        self.log_level = self.log_level_map[log_level]
+        self.log_to_file(config.logs_path, self.log_level)
+
+    def logger(self, name=None):
+        log = logging.getLogger(name)
+        log.setLevel(self.log_level)
+        log.propagate = False
+        if self.file_handler is not None:
+            log.addHandler(self.file_handler)
+        return log
+
+    def log_to_file(self, filename, log_level):
+        self.log_level = log_level
+        self.file_handler = logging.FileHandler(filename, "a")
+        self.file_handler.setLevel(log_level)
+        formatter = logging.Formatter('%(asctime)s [%(levelname)s] {%(name)s} - %(message)s')
+        self.file_handler.setFormatter(formatter)
+
+        try:
+            # simple log rotation to avoid growing infinite
+            size = os.path.getsize(filename)
+            if size > 5000000:  # More than 5MB
+                print(f"Moving {filename} (size {str(size / 1000000)} MB) to {filename}.old")
+                os.rename(filename, filename + ".old")
+        except:
+            pass
diff --git a/lib/misc/memory_leds.py b/lib/misc/memory_leds.py
new file mode 100644
index 0000000..886dde4
--- /dev/null
+++ b/lib/misc/memory_leds.py
@@ -0,0 +1,21 @@
+import usb
+from lib.usb_device import USBDevice
+from lib.misc.logger import Logger
+
+
+def change_profile(device: USBDevice, profile: str):
+    log = Logger().logger(__name__)
+    # set memory key led to corresponding profile
+    try:
+        device.dev.ctrl_transfer(0x21, 0x09, 0x0211, 1, device.keyboard.events.memoryKeysLEDs[profile], device.usb_timeout)
+        response = device.dev.read(device.endpoint, device.endpoint.wMaxPacketSize, device.usb_timeout)
+        return bytes(response) == device.keyboard.events.memoryKeysLEDs[profile]
+    except usb.core.USBError as e:
+        if e.errno == 110:  # Operation timed out
+            pass
+        elif e.errno == 19 or e.errno == 5:
+            # 19 - No such keyboard
+            # 5 - Input/Output Error
+            log.error(f"USBError {e.errno}")
+        else:
+            log.exception(e)
diff --git a/lib/misc/notify.py b/lib/misc/notify.py
new file mode 100644
index 0000000..dd231d5
--- /dev/null
+++ b/lib/misc/notify.py
@@ -0,0 +1,33 @@
+import subprocess
+import os
+from lib.misc.config import Config
+from lib.misc.logger import Logger
+
+
+class Notification:
+
+    log = None
+
+    config: Config = None
+
+    def __init__(self, config: Config):
+        self.log = Logger().logger(__name__)
+        self.config = config
+
+    def send_notification(self, profile: str):
+        config = self.config.read()
+        if config['notify'] == "True":
+            inner_command = f"notify-send -i keyboard Logitech-G910 \"Switched profile to {profile}\""
+            command = None
+            if os.geteuid() == 0:
+                if config['username'] != "":
+                    # if run as root service use su <username> wrapper
+                    command = f"su {config['username']} -c '{inner_command}'"
+                else:
+                    self.log.warn("Notifications enabled but no username set in config!")
+            else:
+                command = inner_command  # if run as user we can use notify-send directly
+
+            if command is not None:
+                self.log.debug(f"Sending notification: /bin/bash -c {command}")
+                subprocess.Popen(['/bin/bash', '-c', command], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
diff --git a/lib/misc/paths.py b/lib/misc/paths.py
deleted file mode 100644
index a2bab81..0000000
--- a/lib/misc/paths.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import os
-import sys
-from lib.misc import is_installed
-from lib.misc import create_config
-
-
-main_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
-if is_installed.is_installed():
-    config_dir = "/etc/g910-gkeys"
-    config_path = config_dir + "/config.json"
-
-    if not os.path.exists(config_dir):
-        print("Creating directory: "+config_dir)
-        os.makedirs(config_dir)
-        create_config.create()
-
-else:
-    config_dir = os.path.join(main_dir, "config")
-    config_path = config_dir + "/config.json"
-
-logs_path = "/var/log/g910-gkeys.log"
-
-try:
-    size = os.path.getsize(logs_path)
-    if size > 5000000: # More then 5MB
-        print("Moving "+logs_path+" (size "+str(size/1000000)+"MB) to "+logs_path+".old")
-        os.rename(logs_path, logs_path+".old")
-except:
-    pass
-
-pid_path = config_dir+"/g910-keys.pid"
-
diff --git a/lib/uinput_keyboard/__init__.py b/lib/uinput_keyboard/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/uinput_keyboard/keyboard.py b/lib/uinput_keyboard/keyboard.py
deleted file mode 100644
index a7f00ef..0000000
--- a/lib/uinput_keyboard/keyboard.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from lib.data_mappers.char_uinput_mapper import keys, reverse_keys
-import time
-from lib.misc import logger
-
-log = logger.logger(__name__)
-
-
-def execute_events(events, device):
-    for event in events:
-        if event[1] == 3:
-            device.emit_click(event[0])
-        else:
-            device.emit(*event)
-
-        time.sleep(0.02)
-
-
-def writeout(string, charset, device):
-    for c in string:
-        execute_events(keys[charset][c], device)
-
-
-press_release_fifo = []
-
-
-def release(device):
-    if not len(press_release_fifo):
-        return
-    keys_to_release = press_release_fifo.pop(0)
-    to_log = ""
-    for key in keys_to_release:
-        device.emit(key, 0)
-        to_log += " " + reverse_keys[key] + ","
-
-    log.info("Released keys:" + to_log[:-1] + " Shortcuts to release: " + str(
-        len(press_release_fifo)) + " (Should always be 0)")
-
-
-def shortcut(value, keymap, device):
-    press_together = []
-
-    together = value.split(",")
-
-    for hotkey in together:
-        keys_str = hotkey.split("+")
-        press_together.append(keys_str)
-
-    uinput_groups = []
-    for combo in press_together:
-        uinput_combo = []
-        for key_string in combo:
-            if len(key_string) == 1:
-                uinput_combo.append(keys[keymap][key_string][0][0])
-            else:
-                uinput_combo.append(keys["control"][key_string][0][0])
-
-        uinput_groups.append(uinput_combo)
-
-    if len(uinput_groups) == 1:
-        keys_to_fifo = []
-        for uinput_key in uinput_groups[0]:
-            keys_to_fifo.insert(0, uinput_key)
-            device.emit(uinput_key, 1)
-            time.sleep(0.02)
-        press_release_fifo.insert(0, keys_to_fifo)
-    else:
-        for group in uinput_groups:
-            device.emit_combo(group)
-
-
-if __name__ == "__main__":
-    import uinput, time
-    from lib.data_mappers.uinput_all_keys import uinput_all_keys
-
-    dev = uinput.Device(uinput_all_keys)
-    time.sleep(4)
-    shortcut("ctrl+c,f,d,s,a,f,ctrl+v", "si", dev)
diff --git a/lib/usb_device.py b/lib/usb_device.py
new file mode 100644
index 0000000..f1c3a58
--- /dev/null
+++ b/lib/usb_device.py
@@ -0,0 +1,104 @@
+import time
+import usb
+import signal
+from lib.data_mappers.supported_devices import KeyboardInterface, SUPPORTED_DEVICES
+from lib.misc.logger import Logger
+
+
+class USBDevice:
+    log = None
+    keyboard: KeyboardInterface = None
+    dev: usb.core.Device = None
+    endpoint: usb.core.Endpoint = None
+
+    usb_if: int = 1  # Interface
+    usb_timeout: int = 1000  # Timeout in MS
+
+    def __init__(self, usb_if: int = 1):
+        self.log = Logger().logger(__name__)
+        if usb_if is not None:
+            self.usb_if = usb_if
+        self.init_usb_dev()
+        if self.dev.is_kernel_driver_active(self.usb_if) is True:
+            self.log.debug("detaching kernel driver")
+            self.dev.detach_kernel_driver(self.usb_if)
+
+    def init_usb_dev(self):
+        for keyboard in SUPPORTED_DEVICES:
+            self.dev = usb.core.find(idVendor=keyboard.usbVendor, idProduct=keyboard.usbProduct)
+            if self.dev is not None:
+                self.keyboard = keyboard  # set keyboard interface
+                break
+            time.sleep(0.1)
+
+        if self.dev is None:
+            self.log.warning(f"No supported keyboard found.")
+        else:
+            self.log.info(f"Keyboard {self.keyboard.deviceName} found.")
+
+        self.log.debug("usb dev: " + str(self.dev[self.keyboard.usbConfiguration][(self.usb_if, 0)]))
+        self.endpoint = self.dev[self.keyboard.usbConfiguration][(self.usb_if, 0)][self.keyboard.usbEndpoint]
+        self.usb_timeout = self.dev.default_timeout
+        self.log.debug(f"Device default timeout: {self.dev.default_timeout}ms")
+
+    def disable_fkey_to_gkey_binding(self):
+        time.sleep(0.5)
+        self.log.info("Trying to disable f-key to g-key binding")
+
+        if self.keyboard.disableGKeysInterface:
+            for packet in self.keyboard.events.disableGKeys:
+                self.log.debug(f"Sending ctrl command to keyboard {str(packet)}...")
+                if self.keyboard.disableGKeysUseWrite:
+                    byte_written = self.dev.ctrl_transfer(0x21, 0x09, 0x0212, 1, packet, self.usb_timeout)
+                    # byte_written = self.dev.write(self.endpoint.bEndpointAddress, packet, self.usb_timeout)
+                    self.log.debug(f"Completed (sent {byte_written} byte)")
+                else:
+                    # self.dev.ctrl_transfer(0x21, 0x09, 0x0212, 1, packet, self.usb_timeout)
+                    pass
+
+                response_count = len(self.keyboard.events.disableGKeysResponse)
+                while response_count:
+                    try:
+                        confirmation_bytes = self.dev.read(self.endpoint.bEndpointAddress,
+                                                           self.endpoint.wMaxPacketSize, self.usb_timeout)
+                        if bytes(confirmation_bytes) in self.keyboard.events.disableGKeysResponse:
+                            self.log.debug(f"G-key-mode - response: {str(confirmation_bytes)}")
+                            if bytes(confirmation_bytes) == packet:
+                                self.log.info("G-key-mode - Disabled successfully")
+                        else:
+                            self.log.warning(f"Warning - G-key-mode - Unknown response: {str(confirmation_bytes)}")
+                            return False
+                        response_count = response_count - 1
+                    except usb.core.USBError as e:
+                        self.log.debug(str(e))
+
+                time.sleep(0.2)
+        return True
+
+    def read(self):
+        try:
+            if self.dev:
+                return self.dev.read(self.endpoint.bEndpointAddress, self.endpoint.wMaxPacketSize, self.usb_timeout)
+        except usb.core.USBError as e:
+            if e.errno == 110:  # Operation timed out
+                pass  # nothing to read from keyboard
+            elif e.errno == 19 or e.errno == 5:
+                # 19 - No such keyboard
+                # 5 - Input/Output Error
+                self.log.error(f"USBError {e.errno}")
+                time.sleep(0.5)
+                self.init_usb_dev()
+            else:
+                self.log.exception(e)
+                signal.raise_signal(signal.SIGQUIT)  # raise sig quit to exit
+
+    def __exit__(self):
+        try:
+            if self.dev and self.dev.is_kernel_driver_active(self.usb_if) is False:
+                usb.util.release_interface(self.dev, self.usb_if)
+                self.log.debug("attaching kernel driver")
+                self.dev.attach_kernel_driver(self.usb_if)
+        except usb.core.USBError as e:
+            self.log.error("Could not re-attach kernel driver: " + str(e))
+        except UnboundLocalError:
+            pass
diff --git a/pkgbuild/PKGBUILD b/pkgbuild/PKGBUILD
index 0e2a16c..f86dbd8 100644
--- a/pkgbuild/PKGBUILD
+++ b/pkgbuild/PKGBUILD
@@ -3,7 +3,7 @@
 pkgname=g910-gkeys-git
 _appname=g910-gkeys
 _gitname=g910-gkey-macro-support
-pkgver=0.2.5
+pkgver=0.3.0
 pkgrel=1
 pkgdesc='GKey support for Logitech G910 Keyboard on GNU/Linux'
 url='https://github.com/JSubelj/g910-gkey-macro-support'
diff --git a/requirements.txt b/requirements.txt
index 3679e29..73d17c2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 python-uinput==0.11.2
-pyusb==1.0.2
\ No newline at end of file
+pyusb==1.0.2
+setuptools>=60.2.0
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 3f6c09e..a74132b 100644
--- a/setup.py
+++ b/setup.py
@@ -38,8 +38,7 @@
     long_description = DESCRIPTION
 
 # Load the package's __version__.py module as a dictionary.
-about = {}
-about['__version__'] = VERSION
+about = {'__version__': VERSION}
 
 # Where the magic happens:
 setup(
@@ -52,7 +51,7 @@
     author_email=EMAIL,
     python_requires=REQUIRES_PYTHON,
     url=URL,
-    packages=find_packages(exclude=('layout_config_helpers',)),
+    packages=find_packages(),
     # If your package is a single module, use this instead of 'packages':
     py_modules=['cli_entry_point'],
 
@@ -60,7 +59,7 @@
         'console_scripts': ['g910-gkeys=cli_entry_point:main'],
     },
     install_requires=REQUIRED,
-    #extras_require=EXTRAS,
+    # extras_require=EXTRAS,
     include_package_data=True,
     license='GPL3',
     classifiers=[


Test patch:
diff --git a/lib/functionalities/__init__.py b/tests/__init__.py
similarity index 100%
rename from lib/functionalities/__init__.py
rename to tests/__init__.py
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644
index 0000000..2a11670
--- /dev/null
+++ b/tests/test_config.py
@@ -0,0 +1,133 @@
+import os
+from lib.data_mappers import supported_configs
+from lib.data_mappers.config_reader import ConfigException, Config
+from lib.misc.helper import Helper
+from lib.usb_device import USBDevice
+
+config: Config
+
+config_key_action = {
+    "hotkey_type": "shortcut",
+    "do": "ctrl+c"
+}
+
+config_dict = {
+    "keyboard_mapping": Helper.get_locale(),
+    "notify": "False",
+    "profiles": {
+        "MEMORY_1": {
+            "MACRO_1": config_key_action
+        }
+    }
+}
+
+invalid_config_dict = {
+    "keyboard_mapping": Helper.get_locale(),
+    "notify": "False",
+    "profiles": {
+        "MEMORY_1": {
+            "MACRO_1": {
+                "hotkey_type": "invalid",
+                "do": "invalid"
+            }
+        }
+    }
+}
+
+
+class TestConfig:
+
+    def test_config(self):
+        """
+        Initialize config class
+        """
+        global config
+        config = Config()
+        assert config is not None
+
+    def test_config_delete(self):
+        """
+        Delete config for next test
+        """
+        if os.path.exists(config.config_path):
+            os.remove(config.config_path)
+        assert os.path.exists(config.config_path) is False
+
+    def test_config_create(self):
+        """
+        Test creating config with found keyboard interface
+        """
+        global config
+        # usb keyboard
+        device = USBDevice()
+        config.create(device.keyboard)
+        assert config is not None and os.path.exists(config.config_path)
+
+    def test_config_read(self):
+        """
+        Test reading from config
+        """
+        global config
+        # detect current locale and set as keyboard mapping
+        user_lang = Helper.get_locale()
+        assert config is not None and config.read()["keyboard_mapping"] == user_lang
+
+    def test_config_get_key_action(self):
+        """
+        Test get key action for assigned macro key
+        """
+        global config
+        key_action = config.get_key_action(config_dict, "1", 1)
+        assert key_action == config_key_action
+
+    def test_config_get_key_action_not_defined(self):
+        """
+        Test get key action for not assigned macro key
+        """
+        global config
+        key_action = config.get_key_action(config_dict, "1", 5)
+        assert key_action != config_key_action and key_action == supported_configs.default_key_action
+
+    def test_validate_config(self):
+        """
+        Test validation of config json
+        """
+        global config
+        validated_config = config.validate_config(config_dict)
+        assert type(validated_config) == dict
+
+    def test_validate_config_not_valid(self):
+        """
+        Test detection of invalid config json
+        """
+        global config
+        success = False
+        try:
+            config.validate_config(invalid_config_dict)
+        except ConfigException:
+            success = True
+        assert success
+
+    def test_validate_hotkey_action_typeout(self):
+        """
+        Test validation of hotkey type "typeout"
+        """
+        global config
+        valid = config.validate_hotkey_action("This is a test!", "typeout", Helper.get_locale())
+        assert valid is None
+
+    def test_validate_hotkey_action_shortcut(self):
+        """
+        Test validation of hotkey type "shortcut"
+        """
+        global config
+        valid = config.validate_hotkey_action("a,ctrl+c,ctrl+v", "shortcut", Helper.get_locale())
+        assert valid is None
+
+    def test_validate_hotkey_action_shortcut_unknown_control_key(self):
+        """
+        Test validation of hotkey type "shortcut" wit invalid control key
+        """
+        global config
+        valid = config.validate_hotkey_action("ctrl+gaga", "shortcut", Helper.get_locale())
+        assert valid == {'shortcut': 'Control key gaga on position 2 does not exist! '}
diff --git a/tests/test_de.py b/tests/test_de.py
deleted file mode 100644
index eb1507b..0000000
--- a/tests/test_de.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from lib.uinput_keyboard import keyboard
-import time
-from lib.keyboard_initialization import usb_and_keyboard_device_init
-
-device = usb_and_keyboard_device_init.init_uinput_device()
-time.sleep(1)
-layout="de"
-def shortcut(string):
-    keyboard.shortcut(string, layout, device)
-    keyboard.release(device)
-    time.sleep(0.5)
-    if len(keyboard.press_release_fifo):
-        print("PRESS_RELEASE_FIFO is not empty!")
-
-def writeout(string):
-    keyboard.writeout(string, layout, device)
-    time.sleep(0.5)
-
-
-string="0123456789aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZäÄöÖüÜ!\"§$%&/()=²³¼½¬{[]}#'´`-_@~ß?\\^°*,;.:€"
-path = "/tmp/test_g910.txt"
-shortcut("ctrl+shift+t")
-writeout("rm "+path)
-shortcut("enter")
-writeout("vim "+path)
-shortcut("enter")
-writeout("i"+string)
-shortcut("esc")
-writeout(":wq")
-shortcut("enter")
-writeout("exit")
-shortcut("enter")
-
-print("testing",layout,"layout")
-
-with open(path,"r") as f:
-    read_string = f.read()
-
-is_correct = True
-for i in range(len(string)):
-    if string[i] != read_string[i]:
-        is_correct = False
-        print("should be:",repr(string[i]),"; is:",repr(read_string[i]))
-
-print("Everything ok:",is_correct)
diff --git a/tests/test_en.py b/tests/test_en.py
deleted file mode 100644
index 34fdc39..0000000
--- a/tests/test_en.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from lib.uinput_keyboard import keyboard
-import time
-from lib.keyboard_initialization import usb_and_keyboard_device_init
-
-device = usb_and_keyboard_device_init.init_uinput_device()
-time.sleep(1)
-layout="en"
-def shortcut(string):
-    keyboard.shortcut(string, layout, device)
-    keyboard.release(device)
-    time.sleep(0.5)
-    if len(keyboard.press_release_fifo):
-        print("PRESS_RELEASE_FIFO is not empty!")
-
-def writeout(string):
-    keyboard.writeout(string, layout, device)
-    time.sleep(0.5)
-
-
-string="0123456789aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ)!@#$%^&(-=\t[]\n;\"`\\./*, '"
-path = "/tmp/test_g910.txt"
-shortcut("ctrl+alt+t")
-writeout("rm "+path+"\n")
-writeout("vim "+path+"\n")
-writeout("i"+string)
-shortcut("esc")
-writeout(":wq\n")
-writeout("exit\n")
-
-print("testing",layout,"layout")
-
-with open(path,"r") as f:
-    read_string = f.read()
-
-is_correct = True
-for i in range(len(string)):
-    if string[i] != read_string[i]:
-        is_correct = False
-        print("should be:",repr(string[i]),"; is:",repr(read_string[i]))
-
-print("Everything ok:",is_correct)
diff --git a/tests/test_fr.py b/tests/test_fr.py
deleted file mode 100644
index 27cf8b2..0000000
--- a/tests/test_fr.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from lib.uinput_keyboard import keyboard
-import time
-from lib.keyboard_initialization import usb_and_keyboard_device_init
-
-device = usb_and_keyboard_device_init.init_uinput_device()
-time.sleep(1)
-layout = "fr"
-
-
-def shortcut(string):
-    keyboard.shortcut(string, layout, device)
-    keyboard.release(device)
-    time.sleep(0.5)
-    if len(keyboard.press_release_fifo):
-        print("PRESS_RELEASE_FIFO is not empty!")
-
-
-def writeout(string):
-    keyboard.writeout(string, layout, device)
-    time.sleep(0.5)
-
-
-string = " ²&é\"'(-è_çà)=0123456789~#{[|`\\^@]}aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ^¨$£øù%*µ,?;.:/!§\t\n"
-path = "/tmp/test_g910.txt"
-shortcut("ctrl+alt+t")
-writeout("rm " + path + "\n")
-writeout("vim " + path + "\n")
-writeout("i" + string)
-shortcut("capslock+a,a,capslock+a,a")
-print("pressing esc")
-time.sleep(5)
-shortcut("esc")
-print("pressing :wq entr")
-time.sleep(5)
-shortcut("shift+.,w,q,enter")
-print("typing exit")
-time.sleep(5)
-writeout("exit\n")
-string+="AAAa"
-
-print("testing", layout, "layout")
-
-with open(path, "r") as f:
-    read_string = f.read()
-
-is_correct = True
-for i in range(len(string)):
-    if string[i] != read_string[i]:
-        is_correct = False
-        print("should be:", repr(string[i]), "; is:", repr(read_string[i]))
-
-print("Everything ok:", is_correct)
-
-device.__exit__()
diff --git a/tests/test_si.py b/tests/test_si.py
deleted file mode 100644
index 39fe07c..0000000
--- a/tests/test_si.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from lib.uinput_keyboard import keyboard
-import time
-from lib.keyboard_initialization import usb_and_keyboard_device_init
-
-device = usb_and_keyboard_device_init.init_uinput_device()
-time.sleep(1)
-layout = "si"
-
-
-def shortcut(string):
-    keyboard.shortcut(string, layout, device)
-    keyboard.release(device)
-    time.sleep(0.5)
-    if len(keyboard.press_release_fifo):
-        print("PRESS_RELEASE_FIFO is not empty!")
-
-
-def writeout(string):
-    keyboard.writeout(string, layout, device)
-    time.sleep(0.5)
-
-
-string = " ,'0123456789aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXzZyY=!\"#$%&/()˝~ˇ^˘°˛`˙""¸čćšžđ.-*?ŠĐČĆ¨Ž;:_\\|€¶ŧ←↓→øþ÷×æ„“[]ħłß¤‘’¢@{}<>—\t\n"
-path = "/tmp/test_g910.txt"
-shortcut("ctrl+alt+t")
-writeout("rm " + path + "\n")
-writeout("vim " + path + "\n")
-writeout("i" + string)
-shortcut("capslock+a,a,capslock+a,a")
-print("pressing esc")
-time.sleep(5)
-shortcut("esc")
-print("pressing :wq entr")
-time.sleep(5)
-shortcut("shift+.,w,q,enter")
-print("typing exit")
-time.sleep(5)
-writeout("exit\n")
-string+="AAAa"
-
-print("testing", layout, "layout")
-
-with open(path, "r") as f:
-    read_string = f.read()
-
-is_correct = True
-for i in range(len(string)):
-    if string[i] != read_string[i]:
-        is_correct = False
-        print("should be:", repr(string[i]), "; is:", repr(read_string[i]))
-
-print("Everything ok:", is_correct)
-
-device.__exit__()
diff --git a/tests/test_uinput.py b/tests/test_uinput.py
new file mode 100644
index 0000000..aa1717e
--- /dev/null
+++ b/tests/test_uinput.py
@@ -0,0 +1,14 @@
+from lib.keyboard import Keyboard
+# from lib.usb_device import USBDevice
+from lib.data_mappers.config_reader import Config
+
+class TestUInput:
+
+    def test_uinput(self):
+        config = Config()
+        # usb keyboard
+        # device = USBDevice()
+        # uinput keyboard
+        keyboard = Keyboard(config)
+        # keyboard.set_keyboard(device.keyboard)
+        assert keyboard.device is not None
diff --git a/tests/test_usb.py b/tests/test_usb.py
new file mode 100644
index 0000000..23895b1
--- /dev/null
+++ b/tests/test_usb.py
@@ -0,0 +1,25 @@
+from lib.data_mappers.supported_devices import KeyboardInterface
+from lib.usb_device import USBDevice
+
+device = None
+
+
+class TestUSB:
+
+    def test_usb(self):
+        global device
+        # usb keyboard
+        device = USBDevice()
+        assert issubclass(device.keyboard, KeyboardInterface)
+
+    def test_gkeymode(self):
+        global device
+        assert device is not None and device.disable_fkey_to_gkey_binding()
+
+    def test_read(self):
+        global device
+        assert device is not None and device.read() is None
+
+    def test_exit(self):
+        global device
+        assert device is not None and device.__exit__() is None