package hid import ( "sync" "os" "time" "log" "fmt" "context" ) var ( //privateCurrentLEDWatcher *HIDKeyboardLEDStateWatcher = nil privateCurrentKeyboardLEDWatcher *KeyboardLEDStateWatcher = nil ) const ( MaskNumLock = 1 << 0 MaskCapsLock = 1 << 1 MaskScrollLock = 1 << 2 MaskCompose = 1 << 3 MaskKana = 1 << 4 MaskNone = 1 << 7 //not really a mask, indicates no change MaskAny = MaskNumLock | MaskCapsLock | MaskScrollLock | MaskCompose | MaskKana MaskAnyOrNone = MaskNumLock | MaskCapsLock | MaskScrollLock | MaskCompose | MaskKana | MaskNone ) type HIDLEDState struct { NumLock bool CapsLock bool ScrollLock bool Compose bool Kana bool } func (s *HIDLEDState) fillState(stateByte byte) { if stateByte & MaskNumLock > 0 { s.NumLock = true } if stateByte & MaskCapsLock > 0 { s.CapsLock = true } if stateByte & MaskScrollLock > 0 { s.ScrollLock = true } if stateByte & MaskCompose > 0 { s.Compose = true } if stateByte & MaskKana > 0 { s.Kana = true } return } func (s HIDLEDState) AnyOn() bool { return s.NumLock || s.CapsLock || s.ScrollLock || s.Compose || s.Kana } func (s HIDLEDState) Mask(mask HIDLEDState) (result HIDLEDState) { result.NumLock = s.NumLock && mask.NumLock result.CapsLock = s.CapsLock && mask.CapsLock result.ScrollLock = s.ScrollLock && mask.ScrollLock result.Compose = s.Compose && mask.Compose result.Kana = s.Kana && mask.Kana return } func (s HIDLEDState) Changes(other HIDLEDState) (result HIDLEDState) { result.NumLock = s.NumLock != other.NumLock result.CapsLock = s.CapsLock != other.CapsLock result.ScrollLock = s.ScrollLock != other.ScrollLock result.Compose = s.Compose != other.Compose result.Kana = s.Kana != other.Kana return } type lockableListenerMap struct { sync.Mutex m map[*KeyboardLEDStateListener]bool } type KeyboardLEDStateWatcher struct { ledState *HIDLEDState //latest global LED state listeners *lockableListenerMap //map of registered listeners ledStateFile *os.File hasInitialState bool //marks if the initial state is define (gets true after first LED state ha been read) //listener which should be registered are put into a zero length channel, to allow blocking till a listener is added //in case there isn't already a listener (avoid reading LED states, without having a single listener consuming them) listenersToAdd chan *KeyboardLEDStateListener readerToDispatcher chan HIDLEDState ctx context.Context cancelFunc context.CancelFunc isUsable bool } func NewLEDStateWatcher(ctx context.Context, devFilePath string) (res *KeyboardLEDStateWatcher,err error) { //try to open the devFile devFile, err := os.Open(devFilePath) if err != nil { return } if privateCurrentKeyboardLEDWatcher != nil { privateCurrentKeyboardLEDWatcher.Stop() } ctx,cancel := context.WithCancel(ctx) res = &KeyboardLEDStateWatcher{ ledState: &HIDLEDState{}, hasInitialState: false, ledStateFile: devFile, listeners: &lockableListenerMap{ m: make(map[*KeyboardLEDStateListener]bool), }, // Buffer at least one listener, to avoid blocking when one is added, the channel is only used to block the // dispatchLoop, in case there's no registered listener (by reading from the listenerToAdd chanel listenersToAdd: make(chan *KeyboardLEDStateListener,1), readerToDispatcher: make(chan HIDLEDState), //communicates new LED states to from file reader loop to dispatcher loop, blocks till consumed ctx: ctx, cancelFunc: cancel, /* ledState: &HIDLEDState{}, listeners: &listenersmap{m: make(map[*HIDKeyboardLEDListener]*HIDKeyboardLEDListener)}, addListeners: make(chan *HIDKeyboardLEDListener,1), //Buffer at least one, to avoid blocking `CreateAndAddNewListener` (we only want to block `dispatchListeners` in case there's no listener) */ } go res.readLoop() go res.dispatchLoop() privateCurrentKeyboardLEDWatcher = res res.isUsable = true return } func (w *KeyboardLEDStateWatcher) RetrieveNewListener() (l *KeyboardLEDStateListener, err error) { if !w.isUsable { return nil, ErrNotAllowed } //create listener and assgin watcher as parent l = &KeyboardLEDStateListener{ isMarkedForDeletion: false, changedLeds: make(chan HIDLEDState), //interrupt: make(chan struct{}), ledWatcher: w, } //addListener to map w.listenersToAdd <- l return l,nil } // - unregisters all listeners // - listeners which are still processed receive an interrupt on their interrupt channel, it is the responsibility // of the listener to deal with this interrupt and close the channel after the interrupt is consumed (wrting to the channel // happens only once) // - close ledStateFile func (w *KeyboardLEDStateWatcher) Stop() (err error) { //ToDo: A crash occurs from time to time, if the underlying file is already gone (gadget destroyed), this has to be called before the file is removed err = w.ledStateFile.Close() //produces an error in readLoop which gets translated to an interrupt return nil } //reads the LED state from device file, till os.File object is closed func (w *KeyboardLEDStateWatcher) readLoop() { fmt.Println("***Starting LED reader") defer w.ledStateFile.Close() //Assure File object is closed after this loop, if the closed file wasn't the reason for loop abort buf := make([]byte, 1) for { //fmt.Println("-----------\nLED READ LOOP\n-------------------") n,err := w.ledStateFile.Read(buf) if err != nil { log.Printf("Keyboard LED watcher: LED file seems to be closed %s: %v\n...interrupting dispatcher loop!\n", w.ledStateFile.Name(), err) //mark watcher as unusable w.isUsable = false //interrupt the dispatcher loop, by cancelling the context w.cancelFunc() break } for i:=0; i timeout { return nil, ErrTimeout } else { remaining = timeout - passedBy } //Wait for report of LED change select { case ledsChanged := <- l.changedLeds: //we have a state change, check relevance //fmt.Printf("LEDListener received state change on following LEDs %+v\n", ledsChanged) relevantChanges := ledsChanged.Mask(intendedChangeStruct) if relevantChanges.AnyOn() { //We have an intended state change //fmt.Printf("LEDListener: the following changes have been relevant %+v\n", relevantChanges) return &relevantChanges, nil } // special case - MaskNone is enabled we report back an LED change, even if nothing changed // this could be used to trigger on Keyboard re-attachment on Windows/som Linox distros, as a new // LED state is reported everytime the keyboard is attached, even if it doesn't differ from the old one if intendedChange & MaskNone > 0 { //fmt.Printf("LEDListener: no changes, reporting back anyway %+v\n", relevantChanges) return &relevantChanges, nil } //If here, there was a LED state change, but not one we want to use for triggering (continue outer loop, consuming channel data) case <-l.ledWatcher.ctx.Done(): fmt.Println("...LEDWatcher aborted") return nil, ErrAbort case irq:=<-irqFunc: fmt.Println("...WaitLEDStateChange received Irq") irq() return nil, ErrIrq case <- time.After(remaining): return nil, ErrTimeout } } } func (kbd *HIDKeyboard) WaitLEDStateChangeRepeated(irqFunc <-chan func(), intendedChange byte, repeatCount int, minRepeatDelay time.Duration, timeout time.Duration) (changed *HIDLEDState,err error) { //register state change listener l,err := kbd.LEDWatcher.RetrieveNewListener() if err!= nil { return nil,err } //defer kbd.LEDWatcher.removeListener(l) defer l.Unregister() startTime := time.Now() remaining := timeout intendedChangeStruct := HIDLEDState{} intendedChangeStruct.fillState(intendedChange) lastNum,lastCaps,lastScroll,lastCompose,lastKana := startTime,startTime,startTime,startTime,startTime countNum,countCaps,countScroll,countCompose,countKana := 0,0,0,0,0 for { //calculate remaining timeout (error out if already reached passedBy := time.Since(startTime) if passedBy > timeout { return nil, ErrTimeout } else { remaining = timeout - passedBy } //Wait for report of LED change select { case ledsChanged := <- l.changedLeds: //we have a state change, check relevance //fmt.Printf("LEDListener received state change on following LEDs %+v\n", ledsChanged) relevantChanges := ledsChanged.Mask(intendedChangeStruct) // fmt.Printf("LEDs changed: %v\n", ledsChanged) // fmt.Printf("Changes relevant: %v\n", relevantChanges) if relevantChanges.AnyOn() { now := time.Now() //log.Printf("Duration: NUM %v, CAPS %v, SCROLL %v, COMPOSE %v, KANA %v\n", now.Sub(lastNum), now.Sub(lastCaps), now.Sub(lastScroll), now.Sub(lastCompose), now.Sub(lastKana)) //We have an intended state change, check if any was in intended delay and increment counter in case if relevantChanges.NumLock { if now.Sub(lastNum) < minRepeatDelay { countNum += 1 } else { countNum = 0 } lastNum = now } if relevantChanges.CapsLock { if now.Sub(lastCaps) < minRepeatDelay { countCaps += 1 } else { countCaps = 0 } lastCaps = now } if relevantChanges.ScrollLock { if now.Sub(lastScroll) < minRepeatDelay { countScroll += 1 } else { countScroll = 0 } lastScroll = now } if relevantChanges.Compose { if now.Sub(lastCompose) < minRepeatDelay { countCompose += 1 } else { countCompose = 0 } lastCompose = now } if relevantChanges.Kana { if now.Sub(lastKana) < minRepeatDelay { countKana += 1 } else { countKana = 0 } lastKana = now } //log.Printf("\tRelevant LED changes after applying mask (interval %v) NUM: %v CAPS: %v SCROLL: %v COMPOSE: %v KANA: %v\n", minRepeatDelay, countNum, countCaps, countScroll, countCompose, countKana) //check counters result := &HIDLEDState{} if countNum >= repeatCount { result.NumLock = true } if countCaps >= repeatCount { result.CapsLock = true } if countScroll >= repeatCount { result.ScrollLock = true } if countCompose >= repeatCount { result.Compose = true } if countKana >= repeatCount { result.Kana = true } if result.AnyOn() { return result, nil } //return &relevantChanges, nil } //If here, there was a LED state change, but not one we want to use for triggering (continue outer loop, consuming channel data) case <-l.ledWatcher.ctx.Done(): return nil, ErrAbort case irq:=<-irqFunc: irq() return nil, ErrIrq case <- time.After(remaining): return nil, ErrTimeout } } }