diff --git a/hid/controller.go b/hid/controller.go index e0fbcdc..46ac237 100644 --- a/hid/controller.go +++ b/hid/controller.go @@ -217,25 +217,25 @@ func (ctl *HIDController) CancelAllVMs() error { } //Function declarations for master VM -//ToDo: Global mutex for VM callbacks (or better for atomar part of Keyboard.SendString) +//ToDo: Global mutex for VM callbacks (or better for atomar part of Keyboard.StringToPressKeySequence) func (ctl *HIDController) jsKbdWriteString(call otto.FunctionCall) (res otto.Value) { arg0 := call.Argument(0) - //fmt.Printf("JS kString called with: `%s` (%s)\n", arg0, arg0) + //fmt.Printf("JS type() called with: `%s` (%s)\n", arg0, arg0) if !arg0.IsString() { - log.Printf("JavaScript: Wrong argument for `kString`. `kString` accepts a single String argument. Error location: %v\n", call.CallerLocation()) + log.Printf("JavaScript: Wrong argument for `type`. `type` accepts a single String argument. Error location: %v\n", call.CallerLocation()) return } outStr,err := arg0.ToString() if err != nil { - log.Printf("kString error: couldn't convert `%s` to UTF-8 string\n", arg0) + log.Printf("type error: couldn't convert `%s` to UTF-8 string\n", arg0) return } log.Printf("Typing `%s` on HID keyboard device `%s`\n", outStr, ctl.Keyboard.DevicePath) - err = ctl.Keyboard.SendString(outStr) + err = ctl.Keyboard.StringToPressKeySequence(outStr) if err != nil { - log.Printf("kString error: Couldn't type out `%s` on %v\n", outStr, ctl.Keyboard.DevicePath) + log.Printf("type error: Couldn't type out `%s` on %v\n", outStr, ctl.Keyboard.DevicePath) return } return @@ -244,7 +244,7 @@ func (ctl *HIDController) jsKbdWriteString(call otto.FunctionCall) (res otto.Val func (ctl *HIDController) jsDelay(call otto.FunctionCall) (res otto.Value) { arg0 := call.Argument(0) - //fmt.Printf("JS kString called with: `%s` (%s)\n", arg0, arg0) + //fmt.Printf("JS delay() called with: `%s` (%s)\n", arg0, arg0) if !arg0.IsNumber() { log.Printf("JavaScript: Wrong argument for `delay`. `delay` accepts a single Number argument. Error location: %v\n", call.CallerLocation()) @@ -263,13 +263,42 @@ func (ctl *HIDController) jsDelay(call otto.FunctionCall) (res otto.Value) { return } +//for pressing key combos +func (ctl *HIDController) jsPress(call otto.FunctionCall) (res otto.Value) { + + arg0 := call.Argument(0) + //fmt.Printf("JS delay() called with: `%s` (%s)\n", arg0, arg0) + + if !arg0.IsString() { + log.Printf("JavaScript: Wrong argument for 'press'. 'press' accepts a single argument of type string.\n\tError location: %v\n", call.CallerLocation()) + return + } + + comboStr,err := arg0.ToString() + if err != nil { + log.Printf("Javascript 'press' error: couldn't convert '%v' to string\n", arg0) + return + } + log.Printf("Pressing combo '%s'\n", comboStr) + err = ctl.Keyboard.StringToPressKeyCombo(comboStr) + if err != nil { + log.Printf("Javascript `delay` error: couldn't convert `%v` to string\n", arg0) + oErr,vErr := otto.ToValue(err) + if vErr == nil { return oErr} + return + } + return +} + func (ctl *HIDController) initMasterVM() (err error) { ctl.vmMaster = otto.New() - err = ctl.vmMaster.Set("kString", ctl.jsKbdWriteString) + err = ctl.vmMaster.Set("type", ctl.jsKbdWriteString) if err != nil { return err } err = ctl.vmMaster.Set("delay", ctl.jsDelay) if err != nil { return err } + err = ctl.vmMaster.Set("press", ctl.jsPress) + if err != nil { return err } return nil } diff --git a/hid/keyboard.go b/hid/keyboard.go index 947899d..b2e021c 100644 --- a/hid/keyboard.go +++ b/hid/keyboard.go @@ -11,6 +11,7 @@ import ( "time" "math/rand" "regexp" + "path/filepath" ) var ( @@ -46,10 +47,8 @@ func NewKeyboard(devicePath string, resourcePath string) (keyboard *HIDKeyboard, keyboard.KeyDelay = 0 keyboard.KeyDelayJitter = 0 - //ToDo: Load whole language map folder, for now single layout testing - err = keyboard.LoadLanguageMapFromFile(resourcePath + "/common.json") - if err != nil {return nil, err} - err = keyboard.LoadLanguageMapFromFile(resourcePath + "/DE_ASCII.json") + //Load available language maps + err = keyboard.LoadLanguageMapDir(resourcePath) if err != nil {return nil, err} //Init LED sate @@ -59,10 +58,86 @@ func NewKeyboard(devicePath string, resourcePath string) (keyboard *HIDKeyboard, return } +func (kbd *HIDKeyboard) LoadLanguageMapDir(dirpath string) (err error) { + folder,err := filepath.Abs(dirpath) + if err != nil { return err } + var mapFiles []string + err = filepath.Walk(string(folder), func(path string, info os.FileInfo, err error) error { + if err != nil { return err } // prevent panic due to access failures + if !info.IsDir() && (strings.ToLower(filepath.Ext(info.Name())) == ".json") { + fp := path + abs,pErr := filepath.Abs(fp) + if pErr == nil { + mapFiles = append(mapFiles, abs) + } else { + //print warning + log.Printf("Ignoring map file '%s', retrieving absolute path failed!\n", fp) + } + } + return nil + }) + if err != nil { return err } + + //mapFiles contains all absolute path of files with extension ".json" + var commonMAP *HIDKeyboardLanguageMap + for _,mapFile := range mapFiles { + kbdmap, lErr := loadKeyboardLanguageMapFromFile(mapFile) + if lErr != nil { + //Warn on error + log.Printf("Skipping language map file '%s' due to load error: %v\n", mapFile, lErr) + continue + } + if strings.ToUpper(kbdmap.Name) == "COMMON" { + //this is the map with common keys + //mapping in this file will be reflected to all other maps, in case the ARE NOT ALREADY PRESENT + commonMAP = kbdmap + } else { + if kbd.LanguageMaps == nil { + kbd.LanguageMaps = make(map[string]*HIDKeyboardLanguageMap) + } + kbd.LanguageMaps[strings.ToUpper(kbdmap.Name)] = kbdmap + + if kbd.ActiveLanguageLayout == nil && kbdmap.Name != "COMMON" { + kbd.ActiveLanguageLayout = kbdmap + } + } + } + + // If no language maps beside "COMMON" have been loaded, return error + if len(kbd.LanguageMaps) == 0 { + if commonMAP == nil { + return errors.New("Couldn't load any language map") + } else { + return errors.New("Couldn't load any language map, beside 'COMMON' map") + } + + } + + // At this point, all map files not named "COMMON" should be added to kbd.LanguageMaps + // In case a map with name "COMMON" was found, it should be stored in commonMap + // If commonMap was found, the contained mappings are added to the other language maps, + // in case the dedicated mapping doesn't exist already. F.e. the mapping for "F1" is only + // needed in map "COMMON" and added to all other maps, except they specify the "F1" mapping + // themselves. + if commonMAP != nil { + //iterate over all common mappings + for name,reports := range commonMAP.Mapping { + //iterate over all loaded maps + for _,lMap := range kbd.LanguageMaps { + //check if the mapping isn't already present and add it if needed + if _,alreadyPresent := lMap.Mapping[name]; !alreadyPresent { + lMap.Mapping[name] = reports + } + } + } + } + + return +} func (kbd *HIDKeyboard) LoadLanguageMapFromFile(filepath string) (err error) { //if this is the first map loaded, set as active Map - kbdmap, err := LoadKeyboardLanguageMapFromFile(filepath) + kbdmap, err := loadKeyboardLanguageMapFromFile(filepath) if err != nil { return err } if kbd.LanguageMaps == nil { @@ -70,10 +145,11 @@ func (kbd *HIDKeyboard) LoadLanguageMapFromFile(filepath string) (err error) { } kbd.LanguageMaps[strings.ToUpper(kbdmap.Name)] = kbdmap - if kbd.ActiveLanguageLayout == nil { + if kbd.ActiveLanguageLayout == nil && kbdmap.Name != "COMMON" { kbd.ActiveLanguageLayout = kbdmap } + return nil } @@ -97,10 +173,56 @@ func (kbd *HIDKeyboard) SetActiveLanguageMap(name string) (err error) { return nil } -func (kbd *HIDKeyboard) SendString(str string) (err error) { +func (kbd *HIDKeyboard) StringToPressKeyCombo(comboStr string) (err error) { + report,err := kbd.StringToKeyCombo(comboStr) + if err != nil { return err } + seq := []KeyboardOutReport{*report} //convert to single report sequence + err = kbd.PressKeySequence(seq) + return +} + + +func (kbd *HIDKeyboard) StringToKeyCombo(comboStr string) (result *KeyboardOutReport, err error) { //ToDo: Check if keyboard device file exists if kbd.ActiveLanguageLayout == nil { - return errors.New("No language mapping active, couldn't send string!") + return nil,errors.New("No language mapping active, couldn't send key combo!") + } + + //split key sequence describe by string into single key names + keyNames := rpSplit.Split(comboStr, -1) + if len(keyNames) == 0 { + return nil,errors.New("No keys to press") + } + if len(keyNames) == 1 && len(keyNames[0]) == 0 { + return nil,errors.New("No keys to press") + } + + //fmt.Printf("KeyNames %d: %+v\n", len(keyNames), keyNames) + + //try to convert splitted keynames to reports + comboReports := make([]*KeyboardOutReport,0) + for _,keyname := range keyNames { + if len(keyname) == 0 { continue } //ignore empty keynames + report,err := kbd.mapKeyNameToReport(keyname) + if err == nil { + //fmt.Printf("Keyname '%s' mapped to report %+v\n", keyname, report) + comboReports = append(comboReports, report) + } else { + return nil,errors.New(fmt.Sprintf("Couldn't build key combo '%s' because of mapping error in keyname '%s': %v", comboStr, keyname, err)) + } + } + + //fmt.Printf("Combo reports for '%s': %+v\n", comboStr, comboReports) + + //combine reports + result,err = combineReports(comboReports) + return +} + +func (kbd *HIDKeyboard) StringToPressKeySequence(str string) (err error) { + //ToDo: Check if keyboard device file exists + if kbd.ActiveLanguageLayout == nil { + return errors.New("No language mapping active, couldn't send key sequence!") } for _,runeVal := range str { strRune := string(runeVal) @@ -120,55 +242,102 @@ func (kbd *HIDKeyboard) SendString(str string) (err error) { return nil } -// mapKeyStringToReports tries to translate a key expressed by a string description to a SINGLE +// mapKeyNameToReport tries to translate a key expressed by a string description to a SINGLE // report (with respect to the chosen language map), which could be sent to a keyboard device. // Most printable characters like 'a' or 'A' could be represented by a single rune (f.e. `a` or `A`). -// mapKeyStringToReports translates uppercase alphabetical keys [A-Z] to the respective lower case, +// The parameter `keyName` is of type string (instead of rune) because there're keys which +// couldn't be described with a single rune, for example: 'F1', 'ESCAPE' ... +// +// mapKeyNameToReport translates uppercase alphabetical keys [A-Z] to the respective lower case, // before trying to map, in order to avoid fetching reports with [SHIFT] modifiers (this assures // that 'A' gets mapped to the USB key KEY_A, not to the USB key KEY_A combined with the [SHIFT] // modifier). -// The parameter `keyDescription` is of type string (instead of rune) because there're keys which -// couldn't be described with a single rune, for example: 'F1', 'ESCAPE' ... // -// mapKeyStringToReports could return a report, containing only modifiers (f.e. if -// keyDescription = 'CTRL'). Such reports could be used to build KeyCombos, by mixing them together. +// There're result reports containing only modifiers (f.e. if keyName = 'CTRL'). Such reports could be used +// to build KeyCombos, by mixing them together. // -// The language maps could contain mappings, containing multiple reports, as they sometime represent -// printable runes consisting of a sequence of multiple keys (f.e. `^` in the German layout maps -// to a report slice of the key for [^] followed by the key [SPACE], which is needed to print the character). -// mapKeyStringToReports returns ONLY THE FIRST REPORT of such slices, as this is closer to the representation. -// Additionally, single reports could be combimned into a key-combo, which wouldn't be possible with a ordered -// sequence of reports. -func (kbd *HIDKeyboard) TmapKeyStringToReports(keyDescription string) (report *KeyboardOutReport,err error) { - // Assure keyDescription contains no spaces, else error - r := rpSplit.Split(keyDescription, -1) +// The language maps consist mappings from UTF-8 runes to reports sequences and keyNames to single reports. +// Examples for sequences are mostly printable runes which are built from multiple sequential key presses +// (mostly started with DEAD KEYS) like the `^` rune on a german keyboard layout, which has to be created by +// pressing [^] followed by [SPACE]. +// The purpose of mapKeyNameToReport is to resolve the given keyname to A SINGLE REPORT, thus report sequences +// are truncated to the first report only, before returned. This is the trade-off between assuring to return a +// single report per keyname versus managing separated mapping files for rune-mapping and key-mapping. +func (kbd *HIDKeyboard) mapKeyNameToReport(keyName string) (report *KeyboardOutReport,err error) { + // Assure keyName contains no spaces, else error + r := rpSplit.Split(keyName, -1) if len(r) > 1 { - return nil, errors.New("keyDescription mustn't contain spaces") + return nil, errors.New(fmt.Sprintf("Error mapping keyName '%s', unallowed contains spaces!", keyName)) } - keyDescription = strings.ToUpper(r[0]) //reassign trimmed, upper case version + keyName = strings.ToUpper(r[0]) //reassign trimmed, upper case version - // If keyDescription consists of a single upper case letter, translate to lowercase - if rpSingleUpperLetter.MatchString(keyDescription) { keyDescription = strings.ToLower(keyDescription)} + // If keyName consists of a single upper case letter, translate to lowercase + if rpSingleUpperLetter.MatchString(keyName) { keyName = strings.ToLower(keyName)} - // Try to find a matching mapping in 1) current language map, followed by 2) common map (the latter - // holds mappings like 'F1', 'CTRL' etc which are more or less language independent. The common - // map is only accessed, if there was no successful mapping in the chosen language map (priority - // as more specialized) - c,ok := kbd.LanguageMaps["COMMON"] - if !ok { return nil,errors.New("Keyboardmap 'common' not found")} - common := c.Mapping - - reports := common[keyDescription] - if len(reports) > 1 { - //store first report as result - report = &reports[0] + // Try to find a matching mapping in current language map + if kbd.ActiveLanguageLayout == nil { + return nil, errors.New("No language mapping selected") + } + if reports,found := kbd.ActiveLanguageLayout.Mapping[keyName]; found { + //report(s) found, return only first one + if len(reports) > 0 { + return &reports[0], nil + } else { + return nil, errors.New(fmt.Sprintf("Mapping for key '%s' found in language map named '%s', but mapping is empty!", keyName, kbd.ActiveLanguageLayout.Name)) + } + } else { + return nil, errors.New(fmt.Sprintf("Couldn't find mapping for key '%s' in language map named '%s'!", keyName, kbd.ActiveLanguageLayout.Name)) } - - fmt.Printf("Descr '%s': %+v\n", keyDescription, ) - return } +// combineReports combines a slice of output reports into a single report (for key combinations). +// The following rules apply: +// 1) Modifiers are combined with logical or +// 2) Unique keys are filled into the keys array, one-by-one +// 3) Duplicated keys are ignore +// 4) Only the first 6 keys are regarded (without duplicates), the rest is ignored +func combineReports(reports []*KeyboardOutReport) (result *KeyboardOutReport, err error) { + r := KeyboardOutReport{} + keys := make(map[byte]bool) + keyCount := 0 + maxKeys := 6 + + ADDREPORTLOOP: + for _,report := range reports { + //Add modifiers + r.Modifiers |= report.Modifiers + + // add keys to map + // Note: This could be interrupted in the middle of a report if too many keys are contained, while the + // modifiers of this report are already applied. This is a corner case, we don't take care of (happens f.e. + // if the first report contains 2 keys and the second one 6 keys with modifiers) + for _,key := range report.Keys { + if key != 0 { //Ignore "no key" + if !keys[key] { + keys[key] = true + keyCount++ + if keyCount >= maxKeys { + break ADDREPORTLOOP + } + } + } + } + } + + //keys should contain maxKeys at max + keyCount = 0 + for k,_ := range keys { + r.Keys[keyCount] = k + keyCount++ + if keyCount >= maxKeys { break } + } + + return &r, nil +} + + + // PressKeySequence writes the output reports given in `reports` to the keyboard device in sequential // order. A all empty report is automatically appended in order to release all keys after finishing // the sequence (press in contrast to hold). @@ -224,7 +393,7 @@ func (klm *HIDKeyboardLanguageMap) StoreToFile(filePath string) (err error) { return ioutil.WriteFile(filePath, mapJson, os.ModePerm) } -func LoadKeyboardLanguageMapFromFile(filePath string) (result *HIDKeyboardLanguageMap, err error) { +func loadKeyboardLanguageMapFromFile(filePath string) (result *HIDKeyboardLanguageMap, err error) { result = &HIDKeyboardLanguageMap{} mapJson, err := ioutil.ReadFile(filePath) if err != nil { return nil,err } @@ -316,6 +485,15 @@ func (kr *KeyboardOutReport) UnmarshalJSON(b []byte) error { return nil } +func (kr KeyboardOutReport) String() string { + bytes,err := kr.MarshalJSON() + if err == nil { + return string(bytes) + } else { + return fmt.Sprintf("%+v", kr) //ToDo: check if this works or calls a loop + } +} + func (kr *KeyboardOutReport) MarshalJSON() ([]byte, error) { keys := []string{} diff --git a/keymaps/common.json b/keymaps/common.json index 822bbe2..9d3afa5 100755 --- a/keymaps/common.json +++ b/keymaps/common.json @@ -68,8 +68,24 @@ "NUM": [{"Modifiers": [], "Keys": ["KEY_NUMLOCK"]}], "CAPSLOCK": [{"Modifiers": [], "Keys": ["KEY_CAPSLOCK"]}], "CAPS": [{"Modifiers": [], "Keys": ["KEY_CAPSLOCK"]}], + "TABULATOR": [{"Modifiers": [], "Keys": ["KEY_TAB"]}], + "TAB": [{"Modifiers": [], "Keys": ["KEY_TAB"]}], "COMPOSE": [{"Modifiers": [], "Keys": ["KEY_COMPOSE"]}], - "102ND": [{"Modifiers": [], "Keys": ["KEY_102ND"]}] - } + "102ND": [{"Modifiers": [], "Keys": ["KEY_102ND"]}], + + "F13": [{"Modifiers": [], "Keys": ["KEY_F13"]}], + "F14": [{"Modifiers": [], "Keys": ["KEY_F14"]}], + "F15": [{"Modifiers": [], "Keys": ["KEY_F15"]}], + "F16": [{"Modifiers": [], "Keys": ["KEY_F16"]}], + "F17": [{"Modifiers": [], "Keys": ["KEY_F17"]}], + "F18": [{"Modifiers": [], "Keys": ["KEY_F18"]}], + "F19": [{"Modifiers": [], "Keys": ["KEY_F19"]}], + "F20": [{"Modifiers": [], "Keys": ["KEY_F20"]}], + "F21": [{"Modifiers": [], "Keys": ["KEY_F21"]}], + "F22": [{"Modifiers": [], "Keys": ["KEY_F22"]}], + "F23": [{"Modifiers": [], "Keys": ["KEY_F23"]}], + "F24": [{"Modifiers": [], "Keys": ["KEY_F24"]}] + + } } diff --git a/testhid.go b/testhid.go index 040f49f..98a3c2d 100644 --- a/testhid.go +++ b/testhid.go @@ -51,7 +51,7 @@ func main() { err := mapDeASCII.StoreToFile("/tmp/DE_ASCII.json") if err != nil { log.Fatal(err)} - testmap, err := hid.LoadKeyboardLanguageMapFromFile("keymaps/DE_ASCII.json") + testmap, err := hid.loadKeyboardLanguageMapFromFile("keymaps/DE_ASCII.json") if err != nil { log.Fatal(err)} fmt.Println(testmap) */ @@ -59,16 +59,25 @@ func main() { hidCtl, err := hid.NewHIDController("/dev/hidg0", "keymaps", "") if err != nil {panic(err)} - _,err = hidCtl.Keyboard.TmapKeyStringToReports("INS") - if err != nil {panic(err)} - _,err = hidCtl.Keyboard.TmapKeyStringToReports("F1") - if err != nil {panic(err)} - _,err = hidCtl.Keyboard.TmapKeyStringToReports("F13") - if err != nil {panic(err)} - _,err = hidCtl.Keyboard.TmapKeyStringToReports(" F3 ") - if err != nil {panic(err)} + testcombos := []string {"SHIFT 1", "ENTER", "ALT TAB", "ALT TABULATOR", " WIN ", "GUI "} + for _,comboStr := range testcombos { + fmt.Printf("Pressing combo '%s'\n", comboStr) + err := hidCtl.Keyboard.StringToPressKeyCombo(comboStr) + if err == nil { + fmt.Printf("... '%s' pressed sleeping 2s\n", comboStr) + time.Sleep(2000 * time.Millisecond) + } else { + fmt.Printf("Error pressing combo '%s': %v\n", comboStr, err) + } + } + + fmt.Printf("Chosen keyboard language mapping '%s'\n", hidCtl.Keyboard.ActiveLanguageLayout.Name) + + fmt.Println("Initial sleep to test if we capture LED state changes from the past, as soon as we start waiting (needed at boot)") + + time.Sleep(3 * time.Second) //ToDo: Test multiple waits in separate goroutines @@ -122,17 +131,22 @@ func main() { // ascii := " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" // special := "§°üÜöÖäĵ€ß¹²³⁴⁵⁶⁷⁸⁹⁰¼½¬„“¢«»æſðđŋħĸł’¶ŧ←↓→øþ" -// err = keyboard.SendString("Test:" + ascii + "\t" + special) +// err = keyboard.StringToPressKeySequence("Test:" + ascii + "\t" + special) if err != nil { fmt.Println(err)} script := ` - kString(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~"); - kString("\n") - kString("Waiting 500ms ...\n"); + for (i=0; i<10; i++) { + press("CAPS") + delay(500) + } + + type(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~"); + type("\n") + type("Waiting 500ms ...\n"); delay(500) - kString("... done\n"); - kString("§°üÜöÖäĵ€ß¹²³⁴⁵⁶⁷⁸⁹⁰¼½¬„“¢«»æſðđŋħĸł’¶ŧ←↓→øþ"); - kString("\n") + type("... done\n"); + type("§°üÜöÖäĵ€ß¹²³⁴⁵⁶⁷⁸⁹⁰¼½¬„“¢«»æſðđŋħĸł’¶ŧ←↓→øþ"); + type("\n") console.log("Log message from JS"); `