pull/5/head
Alexandre Bourget 2017-07-14 02:41:48 -04:00
commit cffc6fd992
8 zmienionych plików z 866 dodań i 0 usunięć

19
LICENSE 100644
Wyświetl plik

@ -0,0 +1,19 @@
Copyright (c) 2017 Alexandre Bourget
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

28
README.md 100644
Wyświetl plik

@ -0,0 +1,28 @@
Linux driver for Contour Design Shuttle Pro V2
==============================================
My goal is to set it up for the Lightworks Non-Linear Editor.
Buttons layout on the Contour Design Shuttle Pro v2:
F1 F2 F3 F4
F5 F6 F7 F8 F9
(Shuttle)
S-7 .. S-1 S0 S1 .. S7
M1 JogL JogR M2
B2 B3
B1 B4
See

133
config.go 100644
Wyświetl plik

@ -0,0 +1,133 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"regexp"
"strings"
)
var loadedConfiguration = &Config{}
var currentConfiguration *AppConfig
type Config struct {
Apps []*AppConfig `json:"apps"`
}
type AppConfig struct {
Name string `json:"name"`
MatchWindowTitles []string `json:"match_window_titles"`
windowTitleRegexps []*regexp.Regexp
Bindings map[string]string `json:"bindings"`
bindings []*deviceBinding
}
func (ac *AppConfig) parse() error {
if len(ac.MatchWindowTitles) == 0 {
ac.windowTitleRegexps = []*regexp.Regexp{
regexp.MustCompile(`.*`),
}
return nil
}
for _, window := range ac.MatchWindowTitles {
re, err := regexp.Compile(window)
if err != nil {
return fmt.Errorf("Invalid regexp in window match %q: %s", window, err)
}
ac.windowTitleRegexps = append(ac.windowTitleRegexps, re)
}
return nil
}
type deviceBinding struct {
// Input
heldButtons map[int]bool
buttonDown int
otherKey string
// Output
holdButtons []string
pressButton string
}
func (ac *AppConfig) parseBindings() error {
for key, value := range ac.Bindings {
newBinding := &deviceBinding{heldButtons: make(map[int]bool)}
// Input
input := strings.Split(key, "+")
for idx, part := range input {
cleanPart := strings.TrimSpace(part)
key := strings.ToUpper(cleanPart)
if shuttleKeys[key] == 0 && !otherShuttleKeysUpper[key] {
return fmt.Errorf("invalid shuttle device key map: %q doesn't exist", cleanPart)
}
if idx == len(input)-1 {
if shuttleKeys[key] != 0 {
newBinding.buttonDown = shuttleKeys[key]
} else {
newBinding.otherKey = key
}
} else {
keyID := shuttleKeys[key]
if keyID == 0 {
return fmt.Errorf("binding %q, expects a button press, not a shuttle or jog movement")
}
newBinding.heldButtons[keyID] = true
}
}
// Output
output := strings.Split(value, "+")
for idx, part := range output {
cleanPart := strings.TrimSpace(part)
buttonName := strings.ToUpper(cleanPart)
if keyboardKeysUpper[buttonName] == 0 {
return fmt.Errorf("keyboard key unknown: %q", cleanPart)
}
if idx == len(output)-1 {
newBinding.pressButton = buttonName
} else {
newBinding.holdButtons = append(newBinding.holdButtons, buttonName)
}
}
ac.bindings = append(ac.bindings, newBinding)
fmt.Printf("BINDING: %#v\n", newBinding)
}
return nil
}
func LoadConfig(filename string) error {
cnt, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
newConfig := &Config{}
err = json.Unmarshal(cnt, &newConfig)
if err != nil {
return err
}
for _, app := range newConfig.Apps {
if err := app.parse(); err != nil {
return fmt.Errorf("Error parsing app %q's matchers: %s", app.Name, err)
}
if err := app.parseBindings(); err != nil {
return fmt.Errorf("Error parsing app %q's bindings: %s", app.Name, err)
}
}
loadedConfiguration = newConfig
return nil
}

297
definitions.go 100644
Wyświetl plik

@ -0,0 +1,297 @@
package main
import "strings"
var shuttleKeys = map[string]int{
"F1": 256,
"F2": 257,
"F3": 258,
"F4": 259,
"F5": 260,
"F6": 261,
"F7": 262,
"F8": 263,
"F9": 264,
"B1": 267,
"B2": 265,
"B3": 266,
"B4": 268,
"M1": 269,
"M2": 270,
}
var otherShuttleKeys = map[string]bool{
"S-7": true,
"S-6": true,
"S-5": true,
"S-4": true,
"S-3": true,
"S-2": true,
"S-1": true,
"S0": true,
"S1": true,
"S2": true,
"S3": true,
"S4": true,
"S5": true,
"S6": true,
"S7": true,
"JogL": true,
"JogR": true,
}
var keyboardKeys = map[string]int{
"Esc": 1,
"1": 2,
"2": 3,
"3": 4,
"4": 5,
"5": 6,
"6": 7,
"7": 8,
"8": 9,
"9": 10,
"0": 11,
"Minus": 12,
"-": 12,
"Equal": 13,
"=": 13,
"Backspace": 14,
"Tab": 15,
"Q": 16,
"W": 17,
"E": 18,
"R": 19,
"T": 20,
"Y": 21,
"U": 22,
"I": 23,
"O": 24,
"P": 25,
"LeftBrace": 26,
"RightBrace": 27,
"{": 26,
"}": 27,
"Enter": 28,
"LeftCtrl": 29,
"Ctrl": 29,
"A": 30,
"S": 31,
"D": 32,
"F": 33,
"G": 34,
"H": 35,
"J": 36,
"K": 37,
"L": 38,
"Semicolon": 39,
";": 39,
"Apostrophe": 40,
"'": 40,
"Grave": 41,
"LeftShift": 42,
"Shift": 42,
"Backslash": 43,
"\\": 43,
"Z": 44,
"X": 45,
"C": 46,
"V": 47,
"B": 48,
"N": 49,
"M": 50,
"Comma": 51,
",": 51,
"Dot": 52,
".": 52,
"Slash": 53,
"/": 53,
"RightShift": 54,
"RShift": 54,
"KPAsterisk": 55,
"*": 55,
"LeftAlt": 56,
"Alt": 56,
"Space": 57,
"CapsLock": 58,
"F1": 59,
"F2": 60,
"F3": 61,
"F4": 62,
"F5": 63,
"F6": 64,
"F7": 65,
"F8": 66,
"F9": 67,
"F10": 68,
"NumLock": 69,
"ScrollLock": 70,
"KP7": 71,
"KP8": 72,
"KP9": 73,
"KPMinus": 74,
"KP4": 75,
"KP5": 76,
"KP6": 77,
"KPPlus": 78,
"KP1": 79,
"KP2": 80,
"KP3": 81,
"KP0": 82,
"KPDot": 83,
"F11": 87,
"F12": 88,
"KPEnter": 96,
"RightCtrl": 97,
"RCtrl": 97,
"RightAlt": 100,
"RAlt": 100,
"Linefeed": 101,
"Home": 102,
"Up": 103,
"PageUp": 104,
"PgUp": 104,
"Left": 105,
"Right": 106,
"End": 107,
"Down": 108,
"PageDown": 109,
"PgDown": 109,
"PgDn": 109,
"Insert": 110,
"Delete": 111,
"Macro": 112,
"Mute": 113,
"VolumeDown": 114,
"VolumeUp": 115,
"Power": 116, /*ScSystemPowerDown*/
"KPEqual": 117,
"KPPlusMinus": 118,
"Pause": 119,
"Scale": 120, /*AlCompizScale(Expose)*/
"KPComma": 121,
"LeftMeta": 125,
"Meta": 125,
"RightMeta": 126,
"RMeta": 126,
"Compose": 127,
"Stop": 128, /*AcStop*/
"Again": 129,
"Props": 130, /*AcProperties*/
"Undo": 131, /*AcUndo*/
"Front": 132,
"Copy": 133, /*AcCopy*/
"Open": 134, /*AcOpen*/
"Paste": 135, /*AcPaste*/
"Find": 136, /*AcSearch*/
"Cut": 137, /*AcCut*/
"Help": 138, /*AlIntegratedHelpCenter*/
"Menu": 139, /*Menu(ShowMenu)*/
"Calc": 140, /*AlCalculator*/
"Setup": 141,
"Sleep": 142, /*ScSystemSleep*/
"Wakeup": 143, /*SystemWakeUp*/
"File": 144, /*AlLocalMachineBrowser*/
"SendFile": 145,
"DeleteFile": 146,
"Xfer": 147,
"Prog1": 148,
"Prog2": 149,
"WWW": 150, /*AlInternetBrowser*/
"Coffee": 152, /*AlTerminalLock/Screensaver*/
"Direction": 153,
"CycleWindows": 154,
"Mail": 155,
"Bookmarks": 156, /*AcBookmarks*/
"Computer": 157,
"Back": 158, /*AcBack*/
"Forward": 159, /*AcForward*/
"CloseCD": 160,
"EjectCD": 161,
"EjectCloseCD": 162,
"NextSong": 163,
"PlayPause": 164,
"PreviousSong": 165,
"StopCD": 166,
"Record": 167,
"Rewind": 168,
"Phone": 169, /*MediaSelectTelephone*/
"ISO": 170,
"Config": 171, /*AlConsumerControlConfiguration*/
"Homepage": 172, /*AcHome*/
"Refresh": 173, /*AcRefresh*/
"Exit": 174, /*AcExit*/
"Move": 175,
"Edit": 176,
"ScrollUp": 177,
"ScrollDown": 178,
"KPLeftParen": 179,
"(": 179,
"KPRightParen": 180,
")": 180,
"New": 181, /*AcNew*/
"Redo": 182, /*AcRedo/Repeat*/
"F13": 183,
"F14": 184,
"F15": 185,
"F16": 186,
"F17": 187,
"F18": 188,
"F19": 189,
"F20": 190,
"F21": 191,
"F22": 192,
"F23": 193,
"F24": 194,
"PlayCD": 200,
"PauseCD": 201,
"Prog3": 202,
"Prog4": 203,
"Dashboard": 204, /*AlDashboard*/
"Suspend": 205,
"Close": 206, /*AcClose*/
"Play": 207,
"FastForward": 208,
"Print": 210, /*AcPrint*/
"Camera": 212,
"Sound": 213,
"Question": 214,
"Email": 215,
"Chat": 216,
"Search": 217,
"Connect": 218,
"Finance": 219, /*AlCheckbook/Finance*/
"Sport": 220,
"Shop": 221,
"AltErase": 222,
"Cancel": 223, /*AcCancel*/
"BrightnessDown": 224,
"BrightnessUp": 225,
"Media": 226,
"Send": 231, /*AcSend*/
"Reply": 232, /*AcReply*/
"ForwardMail": 233, /*AcForwardMsg*/
"Save": 234, /*AcSave*/
"Documents": 235,
"BrightnessCycle": 243, /*BrightnessUp,AfterMaxIsMin*/
"BrightnessZero": 244, /*BrightnessOff,UseAmbient*/
"DisplayOff": 245, /*DisplayDeviceToOffState*/
"Rfkill": 247, /*KeyThatControlsAllRadios*/
"Micmute": 248, /*Mute/UnmuteTheMicrophone*/
}
//var reverseShuttleKeys map[int]string
var keyboardKeysUpper = map[string]int{}
var otherShuttleKeysUpper = map[string]bool{}
func init() {
// for k, v := range shuttleKeys {
// reverseShuttleKeys[v] = k
// }
for k, v := range keyboardKeys {
keyboardKeysUpper[strings.ToUpper(k)] = v
}
for k, v := range otherShuttleKeys {
otherShuttleKeysUpper[strings.ToUpper(k)] = v
}
}

60
main.go 100644
Wyświetl plik

@ -0,0 +1,60 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"github.com/bendahl/uinput"
"github.com/gvalkov/golang-evdev"
)
var configFile = flag.String("config", filepath.Join(os.Getenv("HOME"), ".shuttle-go.json"), "Location to the .shuttle-go.json configuration")
func main() {
flag.Parse()
if len(flag.Args()) != 1 {
fmt.Println("Missing device name as parameter.\nExample: [program] /dev/input/by-id/usb-Contour_Design_ShuttlePRO_v2-event-if00\n")
os.Exit(1)
}
err := LoadConfig(*configFile)
if err != nil {
fmt.Println("Error reading configuration:", err)
os.Exit(10)
}
// X-window title change watcher
watcher := NewWindowWatcher()
if err := watcher.Setup(); err != nil {
fmt.Println("Error watching X window:", err)
os.Exit(3)
}
go watcher.Run()
// Virtual keyboard
vk, err := uinput.CreateKeyboard("/dev/uinput", []byte("Shuttle Pro V2"))
if err != nil {
log.Println("Can't open dev:", err)
}
// Shuttle device event receiver
dev, err := evdev.Open(flag.Arg(0))
if err != nil {
fmt.Println("Couldn't open Shuttle device:", err)
os.Exit(2)
}
fmt.Println("ready")
mapper := NewMapper(vk, dev)
for {
if err := mapper.Process(); err != nil {
fmt.Println("Error processing input events (continuing):", err)
}
}
}

194
mapper.go 100644
Wyświetl plik

@ -0,0 +1,194 @@
package main
import (
"fmt"
"reflect"
"strings"
"github.com/bendahl/uinput"
evdev "github.com/gvalkov/golang-evdev"
)
// Mapper receives events from the Shuttle devices, and maps (through
// configuration) to the Virtual Keyboard events.
type Mapper struct {
virtualKeyboard uinput.Keyboard
inputDevice *evdev.InputDevice
state buttonsState
}
type buttonsState struct {
jog int
shuttle int
buttonsHeld map[int]bool
}
func NewMapper(virtualKeyboard uinput.Keyboard, inputDevice *evdev.InputDevice) *Mapper {
m := &Mapper{
virtualKeyboard: virtualKeyboard,
inputDevice: inputDevice,
}
m.state.buttonsHeld = make(map[int]bool)
m.state.jog = -1
return m
}
func (m *Mapper) Process() error {
evs, err := m.inputDevice.Read()
if err != nil {
return err
}
fmt.Println("---")
m.dispatch(evs)
return nil
}
func (m *Mapper) dispatch(evs []evdev.InputEvent) {
newJogVal := jogVal(evs)
if m.state.jog != newJogVal {
if m.state.jog != -1 {
// Trigger JL or JR if we're advancing or not..
delta := newJogVal - m.state.jog
if (delta > 0 || delta < -200) && (delta < 200) {
if err := m.EmitOther("JogR"); err != nil {
fmt.Println("Jog right:", err)
}
} else {
if err := m.EmitOther("JogL"); err != nil {
fmt.Println("Jog left:", err)
}
}
}
m.state.jog = newJogVal
}
newShuttleVal := shuttleVal(evs)
if m.state.shuttle != newShuttleVal {
keyName := fmt.Sprintf("S%d", newShuttleVal)
if err := m.EmitOther(keyName); err != nil {
fmt.Println("Shuttle movement %q: %s\n", keyName, err)
}
m.state.shuttle = newShuttleVal
}
for _, ev := range evs {
if ev.Type != 1 {
continue
}
heldButtons, lastDown := buttonVals(m.state.buttonsHeld, ev)
if lastDown != 0 {
modifiers := buttonsToModifiers(heldButtons, lastDown)
if err := m.EmitKeys(modifiers, lastDown); err != nil {
fmt.Println("Button press:", err)
}
// fmt.Printf("OUTPUT: Modifiers: %v, Just pressed: %d\n", modifiers, lastDown)
}
m.state.buttonsHeld = heldButtons
}
//fmt.Printf("TYPE: %d\tCODE: %d\tVALUE: %d\n", ev.Type, ev.Code, ev.Value)
// TODO: Lock on configuration changes
return
}
func (m *Mapper) EmitOther(key string) error {
conf := currentConfiguration
if conf == nil {
return fmt.Errorf("No configuration for this Window")
}
upperKey := strings.ToUpper(key)
for _, binding := range conf.bindings {
if binding.otherKey == upperKey {
return m.executeBinding(binding.holdButtons, binding.pressButton)
}
}
return fmt.Errorf("No bindings for those movements")
}
func (m *Mapper) EmitKeys(modifiers map[int]bool, keyDown int) error {
conf := currentConfiguration
if conf == nil {
return fmt.Errorf("No configuration for this Window")
}
for _, binding := range conf.bindings {
if reflect.DeepEqual(binding.heldButtons, modifiers) && binding.buttonDown == keyDown {
return m.executeBinding(binding.holdButtons, binding.pressButton)
}
}
return fmt.Errorf("No binding for these keys")
}
func (m *Mapper) executeBinding(holdButtons []string, pressButton string) error {
fmt.Println("Executing bindings:", holdButtons, pressButton)
for _, button := range holdButtons {
if err := m.virtualKeyboard.KeyDown(keyboardKeysUpper[button]); err != nil {
return err
}
}
if err := m.virtualKeyboard.KeyPress(keyboardKeysUpper[pressButton]); err != nil {
return err
}
for _, button := range holdButtons {
if err := m.virtualKeyboard.KeyUp(keyboardKeysUpper[button]); err != nil {
return err
}
}
return nil
}
func jogVal(evs []evdev.InputEvent) int {
for _, ev := range evs {
if ev.Type == 2 && ev.Code == 7 {
return int(ev.Value)
}
}
return 0
}
func shuttleVal(evs []evdev.InputEvent) int {
for _, ev := range evs {
if ev.Type == 2 && ev.Code == 8 {
return int(ev.Value)
}
}
return 0
}
func buttonVals(current map[int]bool, ev evdev.InputEvent) (out map[int]bool, lastDown int) {
out = current
if ev.Value == 1 {
current[int(ev.Code)] = true
} else {
delete(current, int(ev.Code))
}
if ev.Value == 1 {
lastDown = int(ev.Code)
}
return
}
func buttonsToModifiers(held map[int]bool, buttonDown int) (out map[int]bool) {
out = make(map[int]bool)
for k := range held {
if k == buttonDown {
continue
}
out[k] = true
}
return
}

32
sample_config.json 100644
Wyświetl plik

@ -0,0 +1,32 @@
{
"apps": [
{
"name": "Lightworks",
"match_window_titles": [
"^Lightworks$", ".*"
],
"bindings": {
"F5": "a",
"F6": "s",
"F7": "i",
"JogL": "KPLeftParen",
"JogR": "KPRightParen",
"S-7": "KP1+KP2",
"S-6": "KP1+KP3",
"S-5": "KP1+KP4",
"S-4": "KP1+KP5",
"S-3": "j",
"S-2": "KP1+KP7",
"S-1": "KP1+KP8",
"S0": "k",
"S1": "KP2+KP3",
"S2": "KP2+KP4",
"S3": "l",
"S4": "KP2+KP6",
"S5": "KP2+KP7",
"S6": "KP2+KP8",
"S7": "KP2+KP9"
}
}
]
}

103
watch.go 100644
Wyświetl plik

@ -0,0 +1,103 @@
package main
import (
"fmt"
"log"
"time"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
)
type watcher struct {
conn *xgb.Conn
root xproto.Window
activeAtom, nameAtom xproto.Atom
prevWindowName string
}
func NewWindowWatcher() *watcher {
return &watcher{}
}
func (w *watcher) Setup() error {
X, err := xgb.NewConn()
if err != nil {
return err
}
// Get the window id of the root window.
setup := xproto.Setup(X)
w.conn = X
w.root = setup.DefaultScreen(X).Root
// Get the atom id (i.e., intern an atom) of "_NET_ACTIVE_WINDOW".
aname := "_NET_ACTIVE_WINDOW"
activeAtom, err := xproto.InternAtom(X, true, uint16(len(aname)),
aname).Reply()
if err != nil {
return fmt.Errorf("Couldn't get _NET_ACTIVE_WINDOW atom: %s", err)
}
// Get the atom id (i.e., intern an atom) of "_NET_WM_NAME".
aname = "_NET_WM_NAME"
nameAtom, err := xproto.InternAtom(X, true, uint16(len(aname)),
aname).Reply()
if err != nil {
return fmt.Errorf("Couldn't get _NET_WM_NAME atom: %s", err)
}
w.activeAtom = activeAtom.Atom
w.nameAtom = nameAtom.Atom
return nil
}
func (w *watcher) Run() {
for {
w.watch()
time.Sleep(2 * time.Second)
}
}
func (w *watcher) watch() {
// From github.com/BurntSushi/xgb's examples.
reply, err := xproto.GetProperty(w.conn, false, w.root, w.activeAtom,
xproto.GetPropertyTypeAny, 0, (1<<32)-1).Reply()
if err != nil {
log.Fatal(err)
}
windowID := xproto.Window(xgb.Get32(reply.Value))
reply, err = xproto.GetProperty(w.conn, false, windowID, w.nameAtom,
xproto.GetPropertyTypeAny, 0, (1<<32)-1).Reply()
if err != nil {
log.Fatal(err)
}
windowName := string(reply.Value)
if w.prevWindowName != windowName {
w.prevWindowName = windowName
w.loadWindowConfiguration(windowName)
}
}
func (w *watcher) loadWindowConfiguration(windowName string) {
if loadedConfiguration == nil {
fmt.Println("Window name switched, but no configuration:", windowName)
return
}
for _, conf := range loadedConfiguration.Apps {
for _, re := range conf.windowTitleRegexps {
if re.MatchString(windowName) {
currentConfiguration = conf
fmt.Printf("Applying configuration for app %q\n", conf.Name)
return
}
}
}
currentConfiguration = nil
}