├── client
├── utils.go
├── go.mod
├── swap_device.go
├── websocket_client.go
├── main.go
├── go.sum
└── icon
│ └── icon.go
├── server
├── go.mod
├── drivers
│ ├── blustream
│ │ ├── status-response.txt
│ │ ├── bluestream_status_reader.go
│ │ └── bluestream.go
│ ├── drivers.go
│ └── startech_kvm
│ │ └── startech_kvm.go
├── messages.go
├── go.sum
├── layout_test.go
├── http.go
├── hub.go
├── home.html
├── main.go
├── layout.go
└── client.go
├── docs
└── API_Endpoints.md
└── README.md
/client/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "time"
4 |
5 | func doEvery(d time.Duration, f func(time.Time)) {
6 | for x := range time.Tick(d) {
7 | f(x)
8 | }
9 | }
--------------------------------------------------------------------------------
/server/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/timgws/kvm-switch/server
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/gorilla/websocket v1.5.0
7 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
8 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
9 | )
10 |
--------------------------------------------------------------------------------
/client/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/timgws/kvm-switch/client
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/getlantern/systray v1.2.1
7 | github.com/go-vgo/robotgo v1.0.0-beta5.1
8 | github.com/jpillora/backoff v1.0.0 // indirect
9 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
10 | )
11 |
--------------------------------------------------------------------------------
/server/drivers/blustream/status-response.txt:
--------------------------------------------------------------------------------
1 | ================================================================
2 | HDMI CMX44AB Status
3 | FW Version: 2.22
4 |
5 | Power IR Key Beep
6 | On On On Off
7 |
8 | Input Edid HDMIcon
9 | 01 Force___11 On
10 | 02 Force___11 On
11 | 03 Force___11 Off
12 | 04 Force___11 Off
13 |
14 | Output FromIn HDMIcon OutputEn OSP Mute
15 | 01 01 Off Yes SNK Off
16 | 02 02 On Yes SNK Off
17 | ================================================================
18 |
--------------------------------------------------------------------------------
/server/messages.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type Direction int64
4 | const (
5 | Left Direction = iota
6 | Right
7 | Top
8 | Bottom
9 | )
10 |
11 | // IncomingAction is what is received by a client, connecting to a server.
12 | // Server is to determine what actions (if any) are to be performed after receiving an action/command.
13 | // { "action_name": "switch_direction", "action_direction": Left }
14 | type IncomingAction struct {
15 | ActionName string
16 | ActionDirection Direction `json:"optional"`
17 | }
18 |
19 | // BroadcastAction is what will be sent to all connected clients when an operation has been performed by the server
20 | // { "action_name": "active_computer", "value": "pc1" }
21 | type BroadcastAction struct {
22 | ActionName string
23 | }
--------------------------------------------------------------------------------
/server/go.sum:
--------------------------------------------------------------------------------
1 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
2 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
3 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
4 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
5 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
6 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
7 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
8 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
9 |
--------------------------------------------------------------------------------
/client/swap_device.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "reflect"
7 | "strings"
8 | )
9 |
10 | // SwapDevice is the action to broadcast to the server, requesting for actions to be performed.
11 | type SwapDevice struct {
12 | Device string `json:"device"`
13 | Direction string `json:"direction"`
14 | }
15 |
16 | // Unmarshal reads in SwapDevice commands that may have been sent from other clients.
17 | func (sd *SwapDevice) Unmarshal(data []byte) error {
18 | err := json.Unmarshal(data, sd)
19 | if err != nil {
20 | return err
21 | }
22 |
23 | fields := reflect.ValueOf(sd).Elem()
24 | for i := 0; i < fields.NumField(); i++ {
25 |
26 | dsTags := fields.Type().Field(i).Tag.Get("kvm")
27 | if strings.Contains(dsTags, "required") && fields.Field(i).IsZero() {
28 | return errors.New("required field is missing")
29 | }
30 |
31 | }
32 | return nil
33 | }
--------------------------------------------------------------------------------
/server/layout_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/timgws/kvm-switch/server/drivers/blustream"
6 | "github.com/timgws/kvm-switch/server/drivers/startech_kvm"
7 | "log"
8 | "testing"
9 | )
10 |
11 | func TestLayoutDirection(t *testing.T) {
12 | layout := BuildLayout()
13 | actions, err := layout.FindActions("home-computer", "left")
14 |
15 | if len(*actions) > 0 {
16 | t.Logf("Found %d actions...", len(*actions))
17 | }
18 |
19 | if err != nil {
20 | t.Fatalf("There was an error: %s", err)
21 | }
22 | }
23 |
24 | func TestLayoutWithDriver(t *testing.T) {
25 | Drivers.Drivers = append(Drivers.Drivers, blustream.NewInstance())
26 | Drivers.Drivers = append(Drivers.Drivers, startech_kvm.NewInstance())
27 |
28 | layout := BuildLayout()
29 |
30 | actions, _ := layout.FindActions("home-computer", "left")
31 | k, _ := json.Marshal(actions)
32 | log.Printf("%s", k)
33 |
34 | actions, err := layout.FindActions("home-computer", "right")
35 | k, _ = json.Marshal(actions)
36 | log.Printf("%s %s", k, err)
37 |
38 | //layout.Effect(actions)
39 | }
--------------------------------------------------------------------------------
/server/http.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "time"
9 | )
10 |
11 | func serveHome(w http.ResponseWriter, r *http.Request) {
12 | log.Println(r.URL)
13 | if r.URL.Path != "/" {
14 | http.Error(w, "Not found", http.StatusNotFound)
15 | return
16 | }
17 | if r.Method != http.MethodGet {
18 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
19 | return
20 | }
21 | http.ServeFile(w, r, "home.html")
22 | }
23 |
24 | func serveLayout(w http.ResponseWriter, r *http.Request) {
25 | log.Println(r.URL)
26 | w.Header().Add("Content-Type", "application/json")
27 | _, err := w.Write(JSONLayout)
28 | if err != nil {
29 | fmt.Println(err)
30 | }
31 | }
32 |
33 | func serveDriverStatus(w http.ResponseWriter, r *http.Request) {
34 | log.Println(r.URL)
35 | w.Header().Add("Content-Type", "application/json")
36 |
37 | drivers, _ := json.Marshal(Drivers)
38 |
39 | _, err := w.Write(drivers)
40 | if err != nil {
41 | fmt.Println(err)
42 | }
43 | }
44 |
45 | func serveRefreshStatus(w http.ResponseWriter, r *http.Request) {
46 | log.Println(r.URL)
47 | for _, driver := range Drivers.Drivers {
48 | driver.GetStatus()
49 | }
50 | }
51 |
52 | func serveSwap(w http.ResponseWriter, r *http.Request) {
53 | actions, _ := TheLayout.FindActions("home-computer", "left")
54 | TheLayout.Effect(actions)
55 |
56 | time.Sleep(500 * time.Millisecond)
57 | }
--------------------------------------------------------------------------------
/server/hub.go:
--------------------------------------------------------------------------------
1 |
2 | package main
3 |
4 | import (
5 | "fmt"
6 | )
7 |
8 | // Hub maintains the set of active clients and broadcasts messages to the
9 | // clients.
10 | type Hub struct {
11 | // Registered clients.
12 | clients map[*Client]bool
13 |
14 | // Inbound messages from the clients.
15 | broadcast chan []byte
16 |
17 | // Register requests from the clients.
18 | register chan *Client
19 |
20 | // Unregister requests from clients.
21 | unregister chan *Client
22 | }
23 |
24 | func newHub() *Hub {
25 | return &Hub{
26 | broadcast: make(chan []byte),
27 | register: make(chan *Client),
28 | unregister: make(chan *Client),
29 | clients: make(map[*Client]bool),
30 | }
31 | }
32 |
33 | func (h *Hub) run() {
34 | for {
35 | select {
36 | case client := <-h.register:
37 | h.clients[client] = true
38 | case client := <-h.unregister:
39 | if _, ok := h.clients[client]; ok {
40 | delete(h.clients, client)
41 | close(client.send)
42 | }
43 | case message := <-h.broadcast:
44 |
45 | var sd SwapDevice
46 | if err := sd.Unmarshal(message); err == nil {
47 | actions, _ := TheLayout.FindActions(sd.Device, sd.Direction)
48 | TheLayout.Effect(actions)
49 | //d, _ := json.Marshal(actions)
50 | //log.Printf("%s", d)
51 | }
52 |
53 | fmt.Printf("Clients: %d", len(h.clients))
54 | for client := range h.clients {
55 | select {
56 | case client.send <- message:
57 | default:
58 | close(client.send)
59 | delete(h.clients, client)
60 | }
61 | }
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/server/drivers/drivers.go:
--------------------------------------------------------------------------------
1 | package drivers
2 |
3 | type Driver struct {
4 | DriverInterface
5 | Name string
6 | ShortName string
7 |
8 | Inputs []Input
9 | Outputs []Output
10 | }
11 |
12 | type Input struct {
13 | DriverInput
14 | InputName string
15 | Active bool
16 | }
17 |
18 | type Output struct {
19 | DriverOutput
20 | OutputName string
21 | Active bool
22 | Input DriverInput
23 | }
24 |
25 | type DriverInput interface {
26 |
27 | }
28 |
29 | type DriverOutput interface {
30 |
31 | }
32 |
33 | type DriverInterface interface {
34 | DriverName() string
35 | GetShortName() string
36 |
37 | // IsRunning will query if a driver is currently operational.
38 | // If a driver is not running, it might mean that the device that it was connected to is gone (eg, USB Serial device)
39 | IsRunning() bool
40 |
41 | // SupportsInitState determines if a driver supports telling us about the state of the controlling device.
42 | // If we support knowing the state, we will query it and update our own state about the device.
43 | // If the device/driver does not implement querying state, we will simply reset all devices to the initial state.
44 | SupportsInitState() bool
45 |
46 | // Start running a driver. Attempt to Dial the device, and determine the state (if possible)
47 | Start() bool
48 |
49 | // Shutdown disconnects from a controlling device.
50 | Shutdown() bool
51 |
52 | GetStatus()
53 |
54 | LastError() error
55 |
56 | IsMatrix() bool
57 | }
58 |
59 | type OutputMatrix interface {
60 | SetOutput(outputName string, inputName string)
61 | }
62 |
63 | type OutputSingle interface {
64 | SetOutput(inputName string)
65 | }
--------------------------------------------------------------------------------
/client/websocket_client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/jpillora/backoff"
6 | "golang.org/x/net/websocket"
7 | "time"
8 | )
9 |
10 | var (
11 | websocketConnected = false
12 | websocketError = make (chan error)
13 | )
14 |
15 | func connectWebsocket() {
16 | b := &backoff.Backoff{
17 | Min: 100 * time.Millisecond,
18 | Max: 10 * time.Second,
19 | Factor: 2,
20 | Jitter: false,
21 | }
22 |
23 | debugLog("Starting Client")
24 |
25 | for {
26 | err := initWebsocketClient()
27 | if err != nil {
28 | d := b.Duration()
29 | debugLog("%s, reconnecting in %s", err, d)
30 | time.Sleep(d)
31 | continue
32 | } else {
33 | b.Reset()
34 | }
35 |
36 |
37 | select {
38 | case wserr := <-websocketError:
39 | if websocketConnected {
40 | debugLog("[Websocket]: error: %s... will reconnect", wserr)
41 | websocketConnected = false
42 | }
43 | }
44 | }
45 | }
46 |
47 | func initWebsocketClient() error {
48 | websocketServer := fmt.Sprintf("ws://%s/ws", *addr)
49 | ws, err := websocket.Dial(websocketServer, "", fmt.Sprintf("http://%s/", *addr))
50 | if err != nil {
51 | debugLog("Dial failed: %s\n", err.Error())
52 | return err
53 | }
54 |
55 | incomingMessages := make(chan string)
56 | go readClientMessages(ws, incomingMessages)
57 | i := 0
58 |
59 | websocketConnected = true
60 | debugLog("Connected to server: %s", websocketServer)
61 | for {
62 | select {
63 | case send := <-outgoingChanges:
64 | if websocketConnected {
65 | err = websocket.Message.Send(ws, send)
66 | }
67 | case <-time.After(time.Duration(2e9)):
68 | i++
69 | case message, ok := <-incomingMessages:
70 | if !ok {
71 | incomingMessages = nil
72 | outgoingChanges = nil
73 | }
74 | fmt.Println(`[websocket] Message Received:`, message)
75 |
76 | var sd SwapDevice
77 | if err := sd.Unmarshal([]byte(message)); err == nil {
78 | switched = false
79 | switching = false
80 | }
81 | }
82 |
83 | if incomingMessages == nil && outgoingChanges == nil {
84 | break
85 | }
86 | }
87 |
88 | return nil
89 | }
90 |
91 | func readClientMessages(ws *websocket.Conn, incomingMessages chan string) {
92 | for {
93 | var message string
94 | err := websocket.Message.Receive(ws, &message)
95 | if err != nil {
96 | debugLog("Websocket Error: %s\n", err.Error())
97 |
98 | //close(outgoingChanges)
99 | close(incomingMessages)
100 | websocketError <- err
101 | return
102 | }
103 |
104 | if message != "" {
105 | incomingMessages <- message
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/server/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | KVM Websocket Viewer
5 |
53 |
90 |
91 |
92 |
93 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "flag"
7 | "log"
8 | "net/http"
9 |
10 | "github.com/timgws/kvm-switch/server/drivers"
11 | "github.com/timgws/kvm-switch/server/drivers/blustream"
12 | "github.com/timgws/kvm-switch/server/drivers/startech_kvm"
13 | )
14 |
15 | const (
16 | maxBufferSize = 1024
17 | EnableDebugMode = true
18 | )
19 | var doneChan = make(chan error, 1)
20 | var buffer = make([]byte, maxBufferSize)
21 |
22 | var serverContext context.Context
23 | var serverContextCtx context.Context
24 | var serverContextCancel context.Context
25 |
26 | var addr = flag.String("addr", ":8787", "http service address")
27 |
28 | type allDrivers struct {
29 | Drivers []drivers.DriverInterface
30 | }
31 | var Drivers allDrivers
32 | var TheLayout *Layout
33 | var JSONLayout []byte
34 |
35 |
36 | func main() {
37 | generateLayout()
38 | registerDrivers()
39 | startDrivers()
40 |
41 | flag.Parse()
42 |
43 | hub := newHub()
44 | go hub.run()
45 |
46 | http.HandleFunc("/", serveHome)
47 | http.HandleFunc("/layout", serveLayout)
48 | http.HandleFunc("/driverStatus", serveDriverStatus)
49 | http.HandleFunc("/refreshStatus", serveRefreshStatus)
50 | http.HandleFunc("/swap/:driver/:input/:output", serveSwap)
51 | http.HandleFunc("/swap", serveSwap)
52 | http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
53 | serveWs(hub, w, r)
54 | })
55 |
56 | err := http.ListenAndServe(*addr, nil)
57 | if err != nil {
58 | log.Printf("Failed to start websocket: ListenAndServe: %s", err)
59 | }
60 | }
61 |
62 | // generateLayout should generate a layout from configuration.
63 | // TODO: fix the should.
64 | func generateLayout() {
65 | TheLayout = BuildLayout()
66 | if (EnableDebugMode) {
67 | JSONLayout, _ = json.Marshal(TheLayout)
68 | log.Printf("Layout JSON: %s", string(JSONLayout))
69 | }
70 | }
71 |
72 | // registerDrivers should go through config, and init the correct drivers required for the layout.
73 | // TODO: fix the 'should'
74 | func registerDrivers() {
75 | stkvm := startech_kvm.NewInstance()
76 | Drivers.Drivers = append(Drivers.Drivers, stkvm)
77 | bsmatrix := blustream.NewInstance()
78 | Drivers.Drivers = append(Drivers.Drivers, bsmatrix)
79 | }
80 |
81 | // startDrivers will start all registered drivers.
82 | func startDrivers() {
83 | for _, driver := range Drivers.Drivers {
84 | starting := driver.Start()
85 | if EnableDebugMode {
86 | log.Printf("Started driver: %s (did it attempt to start? %b)", driver.DriverName(), starting)
87 | }
88 | if driver.LastError() != nil {
89 | log.Printf("[%s]: ERROR: %e", driver.DriverName(), driver.LastError())
90 | }
91 | }
92 | }
93 |
94 | // findDriver will return an instance of a driver with the given shortName that the driver was configured with.
95 | func findDriver(shortName string) drivers.DriverInterface {
96 | for _, driver := range Drivers.Drivers {
97 | if driver.GetShortName() == shortName {
98 | return driver
99 | }
100 |
101 | switch d := driver.(type) {
102 | default:
103 | case drivers.Driver:
104 | if d.ShortName == shortName {
105 | return &d
106 | }
107 | }
108 | }
109 | return nil
110 | }
111 |
--------------------------------------------------------------------------------
/server/layout.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/timgws/kvm-switch/server/drivers"
6 | "strings"
7 | )
8 |
9 | // Layout defines where the machines are, and what the actions that will be performed when
10 | // the mouse is moved around different areas.
11 | type Layout struct {
12 | Computers []Computer
13 | }
14 |
15 | // Computer is a computer (or device) that will be swapped on the matrix.
16 | type Computer struct {
17 | Name string
18 | Directions Directions
19 | }
20 |
21 | // Directions for computers were actions will be performed when moving the mouse between different areas.
22 | type Directions struct {
23 | Left []Action
24 | Right []Action
25 | Top []Action
26 | Bottom []Action
27 | }
28 |
29 | // Action defines an individual _thing_ that will happen after an action is performed.
30 | type Action struct {
31 | DriverName string
32 | PerformAction string
33 | }
34 |
35 | // BuildLayout should build a layout that is usable by the clients.
36 | func BuildLayout() *Layout {
37 | return &Layout{
38 | Computers: []Computer{{
39 | Name: "work-computer",
40 | Directions: Directions{
41 | Right: []Action{{
42 | DriverName: "kvm",
43 | PerformAction: "1",
44 | }},
45 | },
46 | }, {
47 | Name: "home-computer",
48 | Directions: Directions{
49 | Left: []Action{{
50 | DriverName: "kvm",
51 | PerformAction: "2",
52 | }},
53 | Right: []Action{{
54 | DriverName: "matrix",
55 | PerformAction: "01-03",
56 | }, {
57 | DriverName: "matrix",
58 | PerformAction: "02-04",
59 | }, {
60 | DriverName: "kvm",
61 | PerformAction: "4",
62 | }},
63 | },
64 | }, {
65 | Name: "streaming-computer",
66 | Directions: Directions{
67 | Left: []Action{{
68 | DriverName: "matrix",
69 | PerformAction: "01-01",
70 | }, {
71 | DriverName: "matrix",
72 | PerformAction: "02-02",
73 | }, {
74 | DriverName: "kvm",
75 | PerformAction: "2",
76 | }},
77 | },
78 | }},
79 | }
80 | }
81 |
82 | // FindActions with a given computer/device, will find the actions that will be performed.
83 | func (l *Layout) FindActions(name string, direction string) (*[]Action, error) {
84 | var ourDevice *Computer
85 | for i, devices := range l.Computers {
86 | if devices.Name == name {
87 | ourDevice = &l.Computers[i]
88 | }
89 | }
90 |
91 | if ourDevice == nil {
92 | return nil, fmt.Errorf("device [%s] was not found", name)
93 | }
94 |
95 | var actions *[]Action
96 | switch direction {
97 | case "left":
98 | actions = &ourDevice.Directions.Left
99 | case "right":
100 | actions = &ourDevice.Directions.Right
101 | case "top":
102 | actions = &ourDevice.Directions.Top
103 | case "bottom":
104 | actions = &ourDevice.Directions.Bottom
105 | }
106 |
107 | if actions == nil {
108 | return nil, nil
109 | }
110 |
111 | if len(*actions) > 0 {
112 | return actions, nil
113 | }
114 |
115 | return nil, nil
116 | }
117 |
118 | // Effect tells the drivers to perform the actions for a given device in the matrix.
119 | func (l *Layout) Effect (actions *[]Action) {
120 | if actions == nil || len(*actions) < 1 {
121 | return
122 | }
123 | for _, item := range *actions {
124 | driver := findDriver(item.DriverName)
125 | action := item.PerformAction
126 |
127 | switch d := driver.(type) {
128 | case drivers.OutputSingle:
129 | d.SetOutput(action)
130 | case drivers.OutputMatrix:
131 | if strings.Contains(action, "-") {
132 | inOut := strings.Split(action, "-")
133 | d.SetOutput(inOut[0], inOut[1])
134 | }
135 | }
136 | }
137 | }
--------------------------------------------------------------------------------
/docs/API_Endpoints.md:
--------------------------------------------------------------------------------
1 | # /driverStatus
2 | ```
3 | curl http://localhost:8787/driverStatus | jq
4 | ```
5 |
6 | ```json
7 | {
8 | "Drivers": [
9 | {
10 | "DriverInterface": null,
11 | "Name": "Startech.com SV431DVIUDDMH2K B4.1",
12 | "ShortName": "kvm",
13 | "Inputs": null,
14 | "Outputs": null,
15 | "OutputSingle": null,
16 | "StartAttempted": true,
17 | "HasError": false,
18 | "Error": null,
19 | "NumOfInputs": 4,
20 | "NumOfOutputs": 1
21 | },
22 | {
23 | "DriverInterface": null,
24 | "Name": "Blustream CMX44AB v2.22",
25 | "ShortName": "matrix",
26 | "OutputMatrix": null,
27 | "StartAttempted": true,
28 | "HasError": false,
29 | "Error": null,
30 | "NumOfInputs": 4,
31 | "NumOfOutputs": 4,
32 | "Inputs": [
33 | {
34 | "DriverInput": null,
35 | "InputName": "01",
36 | "Active": true,
37 | "Edid": "Force___11"
38 | },
39 | {
40 | "DriverInput": null,
41 | "InputName": "02",
42 | "Active": true,
43 | "Edid": "Force___11"
44 | },
45 | {
46 | "DriverInput": null,
47 | "InputName": "03",
48 | "Active": false,
49 | "Edid": "Force___11"
50 | },
51 | {
52 | "DriverInput": null,
53 | "InputName": "04",
54 | "Active": false,
55 | "Edid": "Force___11"
56 | }
57 | ],
58 | "Outputs": [
59 | {
60 | "DriverOutput": null,
61 | "OutputName": "01",
62 | "Active": true,
63 | "Input": {
64 | "DriverInput": null,
65 | "InputName": "01",
66 | "Active": true,
67 | "Edid": "Force___11"
68 | },
69 | "Edid": "01"
70 | },
71 | {
72 | "DriverOutput": null,
73 | "OutputName": "02",
74 | "Active": true,
75 | "Input": {
76 | "DriverInput": null,
77 | "InputName": "02",
78 | "Active": true,
79 | "Edid": "Force___11"
80 | },
81 | "Edid": "02"
82 | },
83 | {
84 | "DriverOutput": null,
85 | "OutputName": "03",
86 | "Active": true,
87 | "Input": {
88 | "DriverInput": null,
89 | "InputName": "02",
90 | "Active": true,
91 | "Edid": "Force___11"
92 | },
93 | "Edid": "03"
94 | },
95 | {
96 | "DriverOutput": null,
97 | "OutputName": "04",
98 | "Active": true,
99 | "Input": {
100 | "DriverInput": null,
101 | "InputName": "02",
102 | "Active": true,
103 | "Edid": "Force___11"
104 | },
105 | "Edid": "04"
106 | }
107 | ]
108 | }
109 | ]
110 | }
111 | ```
112 |
113 | `Drivers` contains an array of all the loaded drivers that the Fence server is using to communicate.
114 |
115 | The `Inputs` array describes an input on the matrix (ie, a HDMI connection). `InputName` and `OutputName` are the
116 | names of the inputs/outputs.
117 |
118 | `HasError` and `Error` describes the current state of the driver.
119 |
120 | The `Output` describes what `Input` (if any) the output is currently connected to.
121 |
122 | In the above example:
123 | * Output `HDMI 1` is showing Input `HDMI 1`.
124 | * Output `HDMI 2`, `HDMI 3` and `HDMI 4` is showing Input `HDMI 2`.
125 |
126 | An input not being `Active` may mean that the device is not currently plugged in or powered on.
127 |
128 | # /layout
129 | This endpoint dumps a JSON representation of the computed layout.
130 |
131 | # /refreshStatus
132 | For any device (currently just the Blustream) that is supported, we will pull the latest output information from the
133 | device.
134 |
--------------------------------------------------------------------------------
/client/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "os"
9 | "time"
10 |
11 | "github.com/timgws/kvm-switch/client/icon"
12 |
13 | "github.com/getlantern/systray"
14 | "github.com/go-vgo/robotgo"
15 | )
16 |
17 | const (
18 | DisplayRefreshRate = 60
19 | EnableDebugMode = true
20 | )
21 |
22 | var messages chan string
23 | var outgoingChanges = make(chan string)
24 |
25 | var addr = flag.String("addr", "127.0.0.1:8787", "http service address")
26 | var deviceName = flag.String("name", "home-computer", "the name of the device in the layout")
27 |
28 | var cQuit <- chan bool
29 |
30 | var maxX, maxY int
31 | var lastX, lastY = 0, 0
32 | var switched = false
33 | var switching = false
34 |
35 | func main() {
36 | flag.Parse()
37 | systray.Run(onReady, onExit)
38 | go PollMousePosition()
39 |
40 | go func() {
41 | for {
42 | select {
43 | case quit := <-cQuit:
44 | if quit {
45 | os.Exit(1)
46 | }
47 | }
48 | }
49 | }()
50 | }
51 |
52 | func PollMousePosition() {
53 | go doEvery(time.Second/DisplayRefreshRate, pollMousePosition)
54 | maxX, maxY = robotgo.GetScreenSize()
55 | fmt.Println("[Glide] get screen size: ", maxX, maxY)
56 | }
57 |
58 | func deviceHasAtLeastOneDisplay() bool {
59 | numDisplays := robotgo.DisplaysNum()
60 | // macOS will segfault when finding the mouse position if there are no displays attached.
61 | // TODO: If this does not crash on Windows, maybe only run on macOS
62 | if numDisplays == 0 {
63 | // TODO: Fix me, we should only show this once every $X minutes. This will flood the console.
64 | debugLog("[WARNING]: SKIPPING GET PIXEL/MOUSE POS BECAUSE THERE ARE NO SCREENS ATTACHED.")
65 | return false
66 | }
67 |
68 | return true
69 | }
70 |
71 | func pollMousePosition(t time.Time) {
72 | if !deviceHasAtLeastOneDisplay() {
73 | return
74 | }
75 |
76 | x, y := robotgo.GetMousePos()
77 |
78 | if !(x != lastX || y != lastY) {
79 | return
80 | }
81 |
82 | mouseHasMoved(x, y)
83 | }
84 |
85 | func mouseHasMoved(x int, y int) {
86 | if x == 0 && lastX > 0 && switched == false {
87 | switched = true
88 | switchInput("left")
89 | } else if x == maxX-1 && lastX < maxX-2 && switched == false {
90 | switched = true
91 | switchInput("right")
92 | } else if y == 0 && lastY > 0 && switched == false {
93 | switched = true
94 | switchInput("top")
95 | } else if y == maxY-1 && lastY < maxY-2 && switched == false {
96 | switched = true
97 | switchInput("bottom")
98 | }
99 |
100 | /*
101 | color := robotgo.GetPixelColor(100, 200)
102 | log.Printf("(mouse pos: %d x %d) pixel color @ 100x200: #%s", x, y, color)
103 | */
104 |
105 | if EnableDebugMode {
106 | debugLog("[kvm-client] mouse pos (current: %d x %d, previous: %d x %d, max: %d x %d)", x, y, lastX, lastY, maxX, maxY)
107 | }
108 |
109 | lastX = x
110 | lastY = y
111 | }
112 |
113 | func switchInput(direction string) {
114 | if switching == false {
115 | debugLog("[Fence]: 📺 Mouse has moved to the %s desktop.", direction)
116 | switching = true
117 | }
118 |
119 | outgoingChanges <- makeSwap(SwapDevice{
120 | Device: *deviceName,
121 | Direction: direction,
122 | })
123 | }
124 |
125 | func makeSwap(swap SwapDevice) string {
126 | j, _ := json.Marshal(swap)
127 | debugLog("--> Sending to kvm-server %s", j)
128 | return string(j)
129 | }
130 |
131 | func onReady() {
132 | log.Printf("[GUI] The application has booted")
133 |
134 | go connectWebsocket()
135 | go PollMousePosition()
136 | //go processInput()
137 |
138 | systray.SetIcon(icon.Data)
139 | systray.SetTitle("Fence")
140 | systray.SetTooltip("Fence: Move to corner, swap your input")
141 | mRefresh := systray.AddMenuItem("Refresh", "Refresh the state")
142 | mQuit := systray.AddMenuItem("Quit", "Quit the whole app")
143 |
144 | // Sets the icon of a menu item. Only available on Mac and Windows.
145 | mQuit.SetIcon(icon.Data)
146 | go func() {
147 | <-mQuit.ClickedCh
148 | systray.Quit()
149 | }()
150 | go func() {
151 | <-mRefresh.ClickedCh
152 | systray.SetTitle("Refreshing...")
153 | fmt.Println("Refreshing..")
154 | }()
155 | }
156 |
157 | // onExit runs after the GUI has asked to exit.
158 | // TODO: clean up some sockets here.
159 | func onExit() {
160 | // clean up here
161 | }
162 |
163 | // debugLog will output something only if EnableDebugMode is true.
164 | func debugLog(msg string, v ...interface{}) {
165 | if EnableDebugMode {
166 | log.Printf(msg, v...)
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/server/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "github.com/gorilla/websocket"
8 | "log"
9 | "net/http"
10 | "reflect"
11 | "strings"
12 | "time"
13 | )
14 |
15 | const (
16 | // Time allowed to write a message to the peer.
17 | writeWait = 10 * time.Second
18 |
19 | // Time allowed to read the next pong message from the peer.
20 | pongWait = 60 * time.Second
21 |
22 | // Send pings to peer with this period. Must be less than pongWait.
23 | pingPeriod = (pongWait * 9) / 10
24 |
25 | // Maximum message size allowed from peer.
26 | maxMessageSize = 512
27 | )
28 |
29 | var (
30 | newline = []byte{'\n'}
31 | space = []byte{' '}
32 | )
33 |
34 | var upgrader = websocket.Upgrader{
35 | ReadBufferSize: 1024,
36 | WriteBufferSize: 1024,
37 | }
38 |
39 | type SwapDevice struct {
40 | Device string `json:"device" kvm:"required"`
41 | Direction string `json:"direction" kvm:"required"`
42 | }
43 |
44 | // Client is a middleman between the websocket connection and the hub.
45 | type Client struct {
46 | hub *Hub
47 |
48 | // The websocket connection.
49 | conn *websocket.Conn
50 |
51 | // Buffered channel of outbound messages.
52 | send chan []byte
53 | }
54 |
55 | // readPump pumps messages from the websocket connection to the hub.
56 | //
57 | // The application runs readPump in a per-connection goroutine. The application
58 | // ensures that there is at most one reader on a connection by executing all
59 | // reads from this goroutine.
60 | func (c *Client) readPump() {
61 | defer func() {
62 | c.hub.unregister <- c
63 | c.conn.Close()
64 | }()
65 | c.conn.SetReadLimit(maxMessageSize)
66 | c.conn.SetReadDeadline(time.Now().Add(pongWait))
67 | c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
68 | for {
69 | _, message, err := c.conn.ReadMessage()
70 | if err != nil {
71 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
72 | log.Printf("error: %v", err)
73 | }
74 | break
75 | }
76 |
77 | message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
78 | c.hub.broadcast <- message
79 | }
80 | }
81 |
82 | // writePump pumps messages from the hub to the websocket connection.
83 | //
84 | // A goroutine running writePump is started for each connection. The
85 | // application ensures that there is at most one writer to a connection by
86 | // executing all writes from this goroutine.
87 | func (c *Client) writePump() {
88 | ticker := time.NewTicker(pingPeriod)
89 | defer func() {
90 | ticker.Stop()
91 | c.conn.Close()
92 | }()
93 | for {
94 | select {
95 | case message, ok := <-c.send:
96 | c.conn.SetWriteDeadline(time.Now().Add(writeWait))
97 | if !ok {
98 | // The hub closed the channel.
99 | c.conn.WriteMessage(websocket.CloseMessage, []byte{})
100 | return
101 | }
102 |
103 | w, err := c.conn.NextWriter(websocket.TextMessage)
104 | if err != nil {
105 | return
106 | }
107 | w.Write(message)
108 |
109 | // Add queued chat messages to the current websocket message.
110 | n := len(c.send)
111 | for i := 0; i < n; i++ {
112 | w.Write(newline)
113 | w.Write(<-c.send)
114 | }
115 |
116 | if err := w.Close(); err != nil {
117 | return
118 | }
119 | case <-ticker.C:
120 | c.conn.SetWriteDeadline(time.Now().Add(writeWait))
121 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
122 | return
123 | }
124 | }
125 | }
126 | }
127 |
128 | // serveWs handles websocket requests from the peer.
129 | func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
130 | conn, err := upgrader.Upgrade(w, r, nil)
131 | if err != nil {
132 | log.Println(err)
133 | return
134 | }
135 | client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
136 | client.hub.register <- client
137 |
138 | // Allow collection of memory referenced by the caller by doing all work in
139 | // new goroutines.
140 | go client.writePump()
141 | go client.readPump()
142 | }
143 |
144 | func (sd *SwapDevice) Unmarshal(data []byte) error {
145 | err := json.Unmarshal(data, sd)
146 | if err != nil {
147 | return err
148 | }
149 |
150 | fields := reflect.ValueOf(sd).Elem()
151 | for i := 0; i < fields.NumField(); i++ {
152 |
153 | dsTags := fields.Type().Field(i).Tag.Get("kvm")
154 | if strings.Contains(dsTags, "required") && fields.Field(i).IsZero() {
155 | return errors.New("required field is missing")
156 | }
157 |
158 | }
159 | return nil
160 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fence
2 | ## What is Fence?
3 | Fence allows you to automate switching hardware devices when the mouse moves
4 | to the edge of the screen. The functionality is similar to Synergy, but we don't
5 | pass through mouse clicks and key presses.
6 |
7 | Sometimes also known as 'Glide and Switch', the Fence client runs on each PC connected to external
8 | devices through the Fence server. When the mouse moves to the edge of the screen, the client tells
9 | the server the name of the device (ie, the computer name), and the direction of the switch
10 | (left, right, top, bottom). The server then calculates the actions that needs to be performed.
11 |
12 | Both the server and the client are cross-platform.
13 |
14 | This gives you the convenience of Synergy (move your mouse to the edge of the screen
15 | to swap the computer receiving the inputs) with the stability of using USB DDM
16 | to redirect input devices. Lower latency, better(ish) security and much better
17 | application compatibility.
18 |
19 | Fence allows you to design your switching on a per-direction, per-device level,
20 | allowing full control over your switching layout.
21 |
22 | ## Where does it run?
23 | * macOS 11
24 | * Windows 10
25 | * Archlinux (with X11 - not Wayland)
26 |
27 | ## What devices can be controlled?
28 | * KVM controllers (such as the Startech SV431DVIUDDM or ConnectPRO UDP2-14AP)
29 | * HDMI matrix switches (such as the Blustream CMX44AB)
30 |
31 | ## How do I get up and running?
32 | Currently, the configuration is hardcoded into the `server` binary at build time.
33 |
34 | ### Step 1: Configure a server
35 | A Fence server is required to run on a machine that has access to all of the devices that
36 | are wanted to be controlled.
37 |
38 | A Raspberry Pi or similar low-powered device is ideal for this.
39 |
40 | For my setup, I use a SV431DVIUDDM and CMX44AB.
41 |
42 | * Update the `registerDrivers()` function inside `server/main.go` to register drivers for the devices you want to
43 | control.
44 | * Update the serial port in `server/drivers/startech_kvm/startech_kvm.go`
45 | and/or `server/drivers/blustream/blustream.go`
46 | * Define the correct layout in `server/layout.go` describing what you want performed when the mouse moves between
47 | screens
48 |
49 | Note that multiple instances of the matrix and KVM drivers can be started at the same time (see `server/main.go`),
50 | allowing for chains if control of a larger range of devices at once is desired.
51 |
52 | Start the server:
53 | ```shell
54 | # cd server
55 | # go build
56 | # ./server -addr :8787
57 | 2022/05/29 13:03:23 Started driver: Startech SV431DVIUDDM
58 | 2022/05/29 13:03:23 Started driver: Blustream
59 | 2022/05/29 13:03:23 [startech_kvm] Command #1/1: ERROR
60 | 2022/05/29 13:03:23 Ignore the first error, we are just initializing our state - looks like this device is correct
61 | 2022/05/29 13:03:23 [blustream]: New driver name is: Blustream CMX44AB
62 | 2022/05/29 13:03:23 [blustream]: New driver name is: Blustream CMX44AB v2.22
63 | 2022/05/29 13:03:24 [startech_kvm] Command #1/1: SV431DVIUDDM F/W Version :H2K B4.1
64 | 2022/05/29 13:03:24 [startech_kvm]: New driver name is: Startech.com SV431DVIUDDMH2K B4.1
65 | ```
66 |
67 | To see if the server is running successfully, you can use one of the [API Endpoints](docs/API_Endpoints.md)
68 |
69 | ### Step 2: Install client on all machines
70 | ```shell
71 | # cd client
72 | # go build
73 | # ./client -addr 10.2.2.2:8787 -name left
74 | 2022/05/29 13:06:23 [GUI] The application has booted
75 | 2022/05/29 13:06:23 [Websocket] Starting Client
76 | 2022/05/29 13:06:23 [Glide] get screen size: 3360x1890
77 | ```
78 |
79 | ## TODO/Upcoming Features
80 | * [x] Improve switching solution for Blustream devices.
81 | * [x] [#1](https://github.com/timgws/kvm-switch/issues/1)
82 | Fix the client, so that it reconnects when the server's connection goes away.
83 | * [ ] [#2](https://github.com/timgws/kvm-switch/issues/2)
84 | Enable using hotkeys to lock the current screen/not send glide commands to the server.
85 | * [ ] [#3](https://github.com/timgws/kvm-switch/issues/3)
86 | Use DDC/CI to control the monitor inputs, so a hardware matrix is not required.
87 | * [ ] [#3](https://github.com/timgws/kvm-switch/issues/3)
88 | Support Synergy, so DDC/CI commands can be issued for additional hardware-free solution.
89 | * [ ] [#4](https://github.com/timgws/kvm-switch/issues/4)
90 | Web interface to define the edges of different devices.
91 | * [ ] [#5](https://github.com/timgws/kvm-switch/issues/5)
92 | Implement SSL for connections.
93 | * [ ] [#6](https://github.com/timgws/kvm-switch/issues/6)
94 | Implement authentication for connections.
95 | * [ ] [#7](https://github.com/timgws/kvm-switch/issues/7)
96 | Investigate whats required to pass keypresses and mouse movements to the client.
97 |
--------------------------------------------------------------------------------
/server/drivers/blustream/bluestream_status_reader.go:
--------------------------------------------------------------------------------
1 | package blustream
2 |
3 | import (
4 | "github.com/timgws/kvm-switch/server/drivers"
5 | "log"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | // readStatus will read each line from the serial console after the `status` command is issued.
11 | func (d *BlustreamMatrix) readStatus(_msg string) {
12 | msg := strings.TrimSpace(_msg)
13 |
14 | if !d.statusIncoming {
15 | d.NumOfOutputs = len(d.Outputs)
16 | }
17 |
18 | // If our line contains the 'Status' word, it also contains the model name.
19 | // Replace the name in the driver.
20 | if d.statusReading == ReadingModel && strings.Contains(msg, "Status") {
21 | d.setModelState(msg)
22 | }
23 |
24 | if d.statusReading == ReadingVersion && strings.Contains(msg, "Version") {
25 | d.setFirmwareState(msg)
26 | d.statusReading = WaitingInput
27 | return
28 | }
29 |
30 | // Input Edid HDMIcon
31 | if d.statusReading == WaitingInput && strings.Contains(msg, "Input") {
32 | res := strings.Fields(msg)
33 | if strings.ToTitle(res[0]) == "INPUT" {
34 | d.statusReading = ReadingInput
35 | d.state.readingInputNumber = 0
36 | return
37 | }
38 | }
39 |
40 | if d.statusReading == ReadingInput {
41 | res := strings.Fields(msg)
42 |
43 | // Output FromIn HDMIcon OutputEn OSP Mute
44 | if strings.ToTitle(res[0]) == "OUTPUT" {
45 | d.statusReading = ReadingOutput
46 | d.state.readingOutputNumber = 0
47 | d.NumOfInputs = len(d.Inputs)
48 | return
49 | }
50 |
51 | // 01 Force___11 On
52 | if len(msg) > 0 {
53 | res := strings.Fields(msg)
54 | d.readInputLine(res)
55 | }
56 | }
57 |
58 | if d.statusReading == ReadingOutput {
59 | // 01 01 Off Yes SNK Off
60 | if len(msg) > 0 {
61 | res := strings.Fields(msg)
62 | d.readOutputLine(res)
63 | }
64 | }
65 | }
66 |
67 | // setModelState will attempt to set the model of the device
68 | func (d *BlustreamMatrix) setModelState(msg string) {
69 | modelSplit := strings.Split(msg, "Status")
70 | model := strings.TrimSpace(modelSplit[0])
71 | model = strings.Replace(model, "HDMI ", "", 1)
72 |
73 | d.state.ModelName = model
74 | if !strings.Contains(model, "Blustream") {
75 | d.state.Model = "Blustream " + model
76 | } else {
77 | d.state.Model = model
78 | }
79 |
80 | d.statusReading = ReadingVersion
81 | d.Driver.Name = d.state.Model
82 | d.setModel()
83 | }
84 |
85 | // setFirmwareState will attempt to find the FW Version of the device.
86 | func (d *BlustreamMatrix) setFirmwareState(msg string) {
87 | versionSplit := strings.Split(msg, ": ")
88 | if len(versionSplit) > 1 {
89 | version := strings.TrimSpace(versionSplit[1])
90 | d.state.CurrentVersion = version
91 | d.setModel()
92 | }
93 |
94 | d.statusReading = WaitingInput
95 | }
96 |
97 | // setModel will change the driverName based on the model retrieved from the status
98 | func (d *BlustreamMatrix) setModel() {
99 | if d.modelSet {
100 | return
101 | }
102 |
103 | if len(d.state.Model) > 0 {
104 | d.Driver.Name = d.state.Model
105 | if len(d.state.CurrentVersion) > 0 {
106 | d.Driver.Name = d.state.Model + " v" + d.state.CurrentVersion
107 | d.modelSet = true
108 | }
109 |
110 | log.Println("[blustream]: New driver name is: " + d.Driver.Name)
111 | }
112 | }
113 |
114 | // isActive is a helper function to turn "yes", "on" to true when reading status.
115 | func isActive(a string) bool {
116 | status := strings.ToLower(a)
117 | if status == "on" || status == "yes" {
118 | return true
119 | }
120 | return false
121 | }
122 |
123 | // readInputLine creates multiple BlustreamInput from inputs that are retrieved from the status.
124 | func (d *BlustreamMatrix) readInputLine(res []string) {
125 | i, err := strconv.Atoi(res[0])
126 | if err == nil {
127 | if d.state.readingInputNumber == 0 {
128 | d.state.readingInputNumber++
129 | }
130 |
131 | if i == d.state.readingInputNumber {
132 | if len(res) != 3 {
133 | return
134 | }
135 |
136 | if d.NumOfInputs < d.state.readingInputNumber {
137 | d.state.readingInputNumber++
138 | newInput := BlustreamInput{
139 | Input: &drivers.Input{
140 | InputName: res[0],
141 | Active: isActive(res[2]),
142 | },
143 | Edid: res[1],
144 | }
145 |
146 | d.Inputs = append(d.Inputs, newInput)
147 | } else {
148 | for k, input := range d.Inputs {
149 | if input.InputName == res[0] {
150 | updatedInput := false
151 | isSourceActive := isActive(res[2])
152 |
153 | if input.Active != isSourceActive {
154 | updatedInput = true
155 | input.Active = isSourceActive
156 | }
157 |
158 | if input.Edid != res[1] {
159 | updatedInput = true
160 | input.Edid = res[1]
161 | }
162 |
163 | if updatedInput {
164 | debugLog(input.InputName, "has changed")
165 | d.updatedInputs = append(d.updatedInputs, k)
166 | }
167 | }
168 | }
169 |
170 | d.state.readingInputNumber++
171 | }
172 | }
173 | }
174 | }
175 |
176 | func (d *BlustreamMatrix) readOutputLine(res []string) {
177 | i, err := strconv.Atoi(res[0])
178 | if err == nil {
179 | if d.state.readingOutputNumber == 0 {
180 | d.state.readingOutputNumber++
181 | }
182 |
183 | if i == d.state.readingOutputNumber {
184 | if len(res) != 6 {
185 | return
186 | }
187 |
188 | if d.NumOfOutputs < d.state.readingOutputNumber {
189 | d.state.readingOutputNumber++
190 | input := d.GetInput(res[1])
191 | newOutput := BlustreamOutput{
192 | Output: &drivers.Output{
193 | OutputName: res[0],
194 | Active: isActive(res[2]) && isActive(res[3]),
195 | Input: input,
196 | },
197 | Edid: res[1],
198 | }
199 |
200 | d.Outputs = append(d.Outputs, newOutput)
201 | } else {
202 | for k, output := range d.Outputs {
203 | if output.OutputName == res[0] {
204 | updatedInput := false
205 | isSourceActive := isActive(res[2])
206 |
207 | if output.Active != isSourceActive {
208 | updatedInput = true
209 | output.Active = isSourceActive
210 | }
211 |
212 | if output.Edid != res[1] {
213 | updatedInput = true
214 | output.Edid = res[1]
215 | }
216 |
217 | if updatedInput {
218 | log.Println(output.OutputName, "has changed")
219 | d.updatedInputs = append(d.updatedInputs, k)
220 | }
221 | }
222 | }
223 |
224 | d.state.readingOutputNumber++
225 | }
226 | }
227 | }
228 | }
--------------------------------------------------------------------------------
/server/drivers/startech_kvm/startech_kvm.go:
--------------------------------------------------------------------------------
1 | package startech_kvm
2 |
3 | import (
4 | "bytes"
5 | "github.com/tarm/serial"
6 | d "github.com/timgws/kvm-switch/server/drivers"
7 | "log"
8 | "strconv"
9 | "strings"
10 | "time"
11 | )
12 |
13 | const EnableDebugMode = false
14 |
15 | type StartechConfig struct {
16 | SerialDevice string
17 | SerialBaud int
18 | }
19 |
20 | type StartechState struct {
21 | CurrentDevice int
22 | }
23 |
24 | // StartechKvm has been developed with a SV431DVIUDDM
25 | // https://www.startech.com/en-au/server-management/sv431dviuddm
26 | // should work for a few other similar devices, but YMMV.
27 | type StartechKvm struct {
28 | d.Driver
29 | d.OutputSingle
30 |
31 | config StartechConfig
32 | isRunning bool
33 |
34 | StartAttempted bool
35 | HasError bool
36 | Error error
37 |
38 | NumOfInputs int
39 | NumOfOutputs int
40 |
41 | messages chan string
42 | serialResponse chan string
43 |
44 | state StartechState
45 | switching bool
46 | switched bool
47 | firstError bool
48 | }
49 |
50 | func NewInstance() *StartechKvm {
51 | return &StartechKvm{
52 | isRunning: false,
53 | Driver: d.Driver{
54 | Name: "Startech SV431DVIUDDM",
55 | ShortName: "kvm",
56 | },
57 | config: StartechConfig{
58 | SerialDevice: "/dev/tty.usbserial-141140",
59 | SerialBaud: 115200,
60 | },
61 | NumOfInputs: 4,
62 | NumOfOutputs: 1,
63 | firstError: true,
64 | state: StartechState{},
65 | }
66 | }
67 |
68 | // SupportsInitState is always false because these (and most other devices) do not support querying state.
69 | // A boy can dream.
70 | func (d *StartechKvm) SupportsInitState() bool {
71 | return false
72 | }
73 |
74 | // IsRunning will show if the device is being read from or not.
75 | func (d *StartechKvm) IsRunning() bool {
76 | return d.isRunning
77 | }
78 |
79 | func (d *StartechKvm) GetShortName() string {
80 | return d.ShortName
81 | }
82 |
83 | // Start will initialize the connection...
84 | func (d *StartechKvm) Start() bool {
85 | d.StartAttempted = true
86 | s_dev := d.config.SerialDevice
87 | s_baud := d.config.SerialBaud
88 |
89 | c := &serial.Config{Name: s_dev, Baud: s_baud}
90 | s, err := serial.OpenPort(c)
91 | if err != nil {
92 | d.Error = err
93 | return false
94 | }
95 |
96 | go d.readPort(s)
97 | go d.init(s)
98 |
99 | go d.processResponses()
100 |
101 | go d.writePort(s)
102 |
103 | return true
104 | }
105 |
106 | // init the device.
107 | func (d *StartechKvm) init(port *serial.Port) {
108 | // (Either) the startech is a bit dodge, or my USB->RS232 is a bit dodge.
109 | // let's send a fake command and wait for the error response.
110 | d.StartAttempted = true
111 | time.Sleep(time.Millisecond * 500)
112 | n, err := port.Write([]byte("HI!\r\n"))
113 | if err != nil {
114 | d.Error = err
115 | d.HasError = true
116 | log.Printf("Could not send %d bytes for driver.", n)
117 | }
118 | port.Flush()
119 | }
120 |
121 | func (d *StartechKvm) DriverName() string {
122 | return d.Name
123 | }
124 |
125 | // GetStatus does nothing here, because the method does not exist.
126 | func (d *StartechKvm) GetStatus() {
127 | }
128 |
129 |
130 | // writePort manages a channel that allows us to send & receive data to this serial connection.
131 | func (d *StartechKvm) writePort(port *serial.Port) {
132 | d.messages = make(chan string)
133 | log.Printf("WRITE PORT STARTED")
134 |
135 | go func() {
136 | for {
137 | select {
138 | case msg := <-d.messages:
139 | log.Printf("==> WRITE PORT MSG: %s", msg)
140 | n, err := port.Write([]byte(msg + "\r\n"))
141 | if err != nil {
142 | log.Printf("Error writing %d bytes: %s", n, err)
143 | d.HasError = true
144 | d.Error = err
145 | }
146 | }
147 | }
148 | }()
149 | }
150 |
151 | // readPort continuously reads data from the serial port, checks if there is some new data received
152 | // and sends it down to the serialResponse channel when we are g2g with a response from a command.
153 | //
154 | // TODO: This needs a big refactor. It's a little dodgy, but it does the trick.
155 | func (d *StartechKvm) readPort(port *serial.Port) {
156 | var command string
157 | for {
158 | buf := make([]byte, 60)
159 | n, err := port.Read(buf)
160 | if err != nil {
161 | log.Fatal(n, err)
162 | }
163 |
164 |
165 | b := bytes.Trim(buf, "\x00")
166 | tmpString := string(b)
167 | command = command + tmpString
168 |
169 | commands := strings.Split(command, "\r\n")
170 |
171 | if EnableDebugMode {
172 | log.Printf("BUFFER CONTENTS: %s tmpString %s", buf, tmpString)
173 | log.Printf("commands: %s", commands)
174 | }
175 |
176 | numCommands := len(commands)
177 | if numCommands > 1 {
178 | for i := range commands {
179 | // if we are at the last command, check that we have an empty command...
180 | if i == numCommands-1 {
181 | if commands[i] == "" {
182 | buf = []byte("")
183 | log.Printf("BUFFER: %s %q", buf, buf)
184 | continue
185 | }
186 | } else if i != numCommands {
187 | currentCommand := commands[0]
188 | if len(commands) > 1 {
189 | numCommands--
190 | commands = commands[1:]
191 |
192 | //buf = []byte(string(commands.joi))
193 | command = strings.Join(commands, "\r\n")
194 | buf = []byte(command)
195 | }
196 |
197 | d.serialResponse <- currentCommand
198 |
199 | log.Printf("[startech_kvm] Command #%d/%d: %s", i+1, len(commands), currentCommand)
200 | }
201 | }
202 | } else {
203 | if EnableDebugMode {
204 | log.Printf("Incomplete command from serial... expected newline")
205 | }
206 | }
207 |
208 | if EnableDebugMode {
209 | log.Printf("*-*-*-**-*-**-*-*-**-*-**-*-*-**-*-**-*-*-**-*-**-*-*-**-*-**-*-*-**-*-**-*-*-**-*-*")
210 | log.Printf("BUFFER CONTENTS: %s tmpString %s", buf, tmpString)
211 | log.Printf("commands: %s", commands)
212 | log.Printf("%q", buf)
213 | log.Printf("---------------------------")
214 | }
215 |
216 | //log.Printf("%s %q", buf, buf)
217 | }
218 | }
219 |
220 | // processResponses reads serial commands that have been fully read from the serial connection.
221 | func (d *StartechKvm) processResponses() {
222 | d.serialResponse = make(chan string)
223 |
224 | go func() {
225 | for {
226 | select {
227 | case msg := <-d.serialResponse:
228 | if EnableDebugMode {
229 | log.Printf("<== [STARTECH] READ SERIAL COMMAND: %s %q", msg, msg)
230 | }
231 |
232 | if msg == "ERROR" {
233 | if !d.firstError {
234 | d.HasError = true
235 | } else {
236 | log.Printf("Ignore the first error, we are just initializing our state - looks like this device is correct")
237 | d.firstError = false
238 | d.isRunning = true
239 | }
240 | continue
241 | }
242 |
243 | if strings.Contains(msg, "F/W Version") {
244 | // unlike Blustream, we only get to know the device when it boots.
245 | // SV431DVIUDDM F/W Version :H2K B4.1
246 | version := strings.Split(msg, " ")
247 | if len(version) == 5 {
248 | fwVersion := strings.Replace(strings.Join(version[3:], " "), ":", "", 1)
249 | d.Driver.Name = "Startech.com " + version[0] + fwVersion
250 | log.Println("[startech_kvm]: New driver name is: " + d.Driver.Name)
251 | }
252 | }
253 |
254 | if len(msg) == 3 {
255 | if msg[:2] == "CH" {
256 | log.Println(msg, msg[2:])
257 | chn, err := strconv.Atoi(msg[2:])
258 | if err != nil {
259 | d.HasError = true
260 | d.Error = err
261 | }
262 |
263 | d.state.CurrentDevice = chn
264 | }
265 | }
266 | }
267 | }
268 | }()
269 | }
270 |
271 | func (d *StartechKvm) SetOutput(inputName string) {
272 | //d.messages <- "CH" + inputName
273 | d.messages <- "K1P" + inputName
274 | }
275 |
276 | func (d *StartechKvm) LastError() error {
277 | return d.Error
278 | }
279 |
280 | func quickLog(str string) {
281 | log.Println(str)
282 | }
--------------------------------------------------------------------------------
/client/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
2 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
7 | github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
8 | github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
9 | github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
10 | github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
11 | github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
12 | github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
13 | github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
14 | github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
15 | github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
16 | github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
17 | github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
18 | github.com/getlantern/systray v1.2.1 h1:udsC2k98v2hN359VTFShuQW6GGprRprw6kD6539JikI=
19 | github.com/getlantern/systray v1.2.1/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
20 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
21 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
22 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
23 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
24 | github.com/go-vgo/robotgo v1.0.0-beta5.1 h1:5Lj9cViKleLP1m8SFkwljuaOKcQ9kWSwAeE8Xw9cpvE=
25 | github.com/go-vgo/robotgo v1.0.0-beta5.1/go.mod h1:Qrtib2lSWYnVZKEmt5K6TjXl5I2JAGQ4nbC6Ow6NFic=
26 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
27 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
28 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
29 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
30 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
31 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
32 | github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
33 | github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
34 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
35 | github.com/otiai10/gosseract v2.2.1+incompatible h1:Ry5ltVdpdp4LAa2bMjsSJH34XHVOV7XMi41HtzL8X2I=
36 | github.com/otiai10/gosseract v2.2.1+incompatible/go.mod h1:XrzWItCzCpFRZ35n3YtVTgq5bLAhFIkascoRo8G32QE=
37 | github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc=
38 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
39 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
40 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
41 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
43 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
44 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
45 | github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934 h1:2lhSR8N3T6I30q096DT7/5AKEIcf1vvnnWAmS0wfnNY=
46 | github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
47 | github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770 h1:2uX8QRLkkxn2EpAQ6I3KhA79BkdRZfvugJUzJadiJwk=
48 | github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU=
49 | github.com/shirou/gopsutil/v3 v3.21.12 h1:VoGxEW2hpmz0Vt3wUvHIl9fquzYLNpVpgNNB7pGJimA=
50 | github.com/shirou/gopsutil/v3 v3.21.12/go.mod h1:BToYZVTlSVlfazpDDYFnsVZLaoRG+g8ufT6fPQLdJzA=
51 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
52 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
53 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
54 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
55 | github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo=
56 | github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
57 | github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ=
58 | github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
59 | github.com/vcaesar/gops v0.22.0 h1:aWHWxY3fvUuaJGph+LegYUSEU/874WJT7MInd8OUmP0=
60 | github.com/vcaesar/gops v0.22.0/go.mod h1:GFNGo9xpCfjcfd/Hovi9RSrpd4FdaQt8V92TzpU22w4=
61 | github.com/vcaesar/imgo v0.30.1 h1:B7QMm2mZY+SGoEvvJwNi+eAv21ptvk2YT1IbP2OzyLE=
62 | github.com/vcaesar/imgo v0.30.1/go.mod h1:n4EluJIN/0UYYGPHBCY/BWlIjdRUdY5U6obtP2lrgdM=
63 | github.com/vcaesar/keycode v0.10.0 h1:Qx5QE8ZXHyRyjoA2QOxBp25OKMKB+zxMVqm0FWGV0d4=
64 | github.com/vcaesar/keycode v0.10.0/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ=
65 | github.com/vcaesar/tt v0.20.0 h1:9t2Ycb9RNHcP0WgQgIaRKJBB+FrRdejuaL6uWIHuoBA=
66 | github.com/vcaesar/tt v0.20.0/go.mod h1:GHPxQYhn+7OgKakRusH7KJ0M5MhywoeLb8Fcffs/Gtg=
67 | github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
68 | github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
69 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
70 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
71 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
72 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
73 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
74 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
75 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
76 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
77 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
78 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
79 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
80 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
81 | golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
82 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
83 | golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
84 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
85 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
86 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
87 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
88 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
89 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
90 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
91 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
92 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
93 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
94 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
95 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
96 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
97 |
--------------------------------------------------------------------------------
/server/drivers/blustream/bluestream.go:
--------------------------------------------------------------------------------
1 | package blustream
2 |
3 | import (
4 | "bytes"
5 | "github.com/tarm/serial"
6 | d "github.com/timgws/kvm-switch/server/drivers"
7 | "log"
8 | "regexp"
9 | "strings"
10 | "time"
11 | )
12 |
13 | const EnableDebugMode = false
14 |
15 | // Reading keeps track of where the statemachine is in reading the `status` command from the BlueStream.
16 | type Reading int
17 | const (
18 | NotReadingStatus Reading = iota
19 | ReadingModel
20 | ReadingVersion
21 | WaitingInput
22 | ReadingInput
23 | ReadingOutput
24 | )
25 |
26 | // BlustreamConfig holds the device-specific configuration.
27 | type BlustreamConfig struct {
28 | SerialDevice string
29 | SerialBaud int
30 | }
31 |
32 | // BlustreamInput represents a HDMI/DVI/USB-C input on a given Blustream matrix
33 | type BlustreamInput struct {
34 | *d.Input
35 | Edid string
36 | }
37 |
38 | // BlustreamOutput represents a HDMI/DVI/USB-C input on a given Blustream matrix
39 | type BlustreamOutput struct {
40 | *d.Output
41 | Edid string
42 | }
43 |
44 | // BlustreamState holds state about the current instance of a Blustream device.
45 | type BlustreamState struct {
46 | CurrentDevice int
47 |
48 | Model string
49 | ModelName string
50 | CurrentVersion string
51 |
52 | readingInputNumber int
53 | readingOutputNumber int
54 | }
55 |
56 | // BlustreamMatrix has been developed with a cmx44ab
57 | // https://www.blustream.com.au/cmx44ab
58 | // should work for a few other similar devices, but YMMV.
59 | type BlustreamMatrix struct {
60 | *d.Driver
61 | d.OutputMatrix
62 |
63 | config BlustreamConfig
64 | // isRunning defines whether we are connected to the rs232 port from the device, and it is working as expected.
65 | isRunning bool
66 |
67 | // StartAttempted defines if we have attempted to start connecting to the serial port, but the device has not init.
68 | StartAttempted bool
69 |
70 | // HasError is true if the device and/or connection has an error.
71 | HasError bool
72 |
73 | // Error contains the error of the device
74 | // WARNING: Error may not contain text! Check HasError.
75 | Error error
76 |
77 | // NumOfInputs: The number of inputs present on the device
78 | NumOfInputs int
79 | // NumOfOutputs: The number of outputs present on the device
80 | NumOfOutputs int
81 |
82 | // messages are commands that need to be sent down the channel to the serial device.
83 | messages chan string
84 |
85 | // serialResponse contains text that is coming inbound from the Blustream device.
86 | serialResponse chan string
87 |
88 | // finishedSwap makes sure that matrix swaps are synchronous
89 | finishedSwap chan bool
90 |
91 | state BlustreamState
92 | switching bool
93 | switched bool
94 | statusIncoming bool
95 | statusConfirmed bool
96 | statusStarted bool
97 | statusReading Reading
98 | modelSet bool
99 |
100 | // port contains the RS232 connection
101 | port *serial.Port
102 |
103 | // updatedInputs show inputs that have recently been updated (eg, new HDMI device coming online).
104 | updatedInputs []int
105 |
106 | // Inputs are a list of all the Driver.Inputs that are present on the matrix.
107 | Inputs []BlustreamInput
108 |
109 | // Outputs are a list of all the Driver.Outputs that are present on the matrix.
110 | Outputs []BlustreamOutput
111 | }
112 |
113 | // NewInstance create a new instance of a Blustream device.
114 | func NewInstance() *BlustreamMatrix {
115 | return &BlustreamMatrix{
116 | isRunning: false,
117 | Driver: &d.Driver{
118 | Name: "Blustream",
119 | ShortName: "matrix",
120 | },
121 | config: BlustreamConfig{
122 | SerialDevice: "/dev/tty.usbserial-141130",
123 | SerialBaud: 57600,
124 | },
125 | state: BlustreamState{},
126 | }
127 | }
128 |
129 | // SupportsInitState is always false because these (and most other devices) do not support querying state.
130 | // A boy can dream.
131 | func (d *BlustreamMatrix) SupportsInitState() bool {
132 | return true
133 | }
134 |
135 | func (d *BlustreamMatrix) GetShortName() string {
136 | return d.ShortName
137 | }
138 |
139 | func (d *BlustreamMatrix) DriverName() string {
140 | return d.Name
141 | }
142 |
143 | // IsRunning will show if the device is being read from or not.
144 | func (d *BlustreamMatrix) IsRunning() bool {
145 | return d.isRunning
146 | }
147 |
148 | // Start initializes the connection, sends first status command.
149 | func (d *BlustreamMatrix) Start() bool {
150 | d.StartAttempted = true
151 | s_dev := d.config.SerialDevice
152 | s_baud := d.config.SerialBaud
153 |
154 | c := &serial.Config{Name: s_dev, Baud: s_baud}
155 | s, err := serial.OpenPort(c)
156 | if err != nil {
157 | d.Error = err
158 | return false
159 | }
160 |
161 | d.port = s
162 |
163 | go d.readPort()
164 | go d.init()
165 |
166 | //go d.pingStatus(s)
167 |
168 | go d.processResponses()
169 |
170 | go d.writePort(s)
171 |
172 | return true
173 | }
174 |
175 | // GetStatus ask the Blustream matrix what the current state of the device is.
176 | // Call me to see if devices have changes (without notifying the switch)
177 | func (d *BlustreamMatrix) GetStatus() {
178 | d.pingStatus()
179 | }
180 |
181 | // SetOutput will change the output of a port to the given input port.
182 | func (d *BlustreamMatrix) SetOutput(outputName string, inputName string) {
183 | if d.switching {
184 | return
185 | }
186 | debugLog("OUTPUT MATRIX %s -> %s", outputName, inputName)
187 |
188 | d.finishedSwap = make(chan bool)
189 |
190 | var matrixOutput *BlustreamOutput
191 | var matrixInput *BlustreamInput
192 |
193 | debugLog("Out/In: %s, %s", d.Outputs, d.Inputs)
194 |
195 | for i, output := range d.Outputs {
196 | if output.OutputName == outputName {
197 | debugLog("<- OUTPUT %s == %s", output.OutputName, outputName)
198 | matrixOutput = &d.Outputs[i]
199 | }
200 | }
201 | for i, input := range d.Inputs {
202 | if input.InputName == inputName {
203 | debugLog("<- INPUT %s == %s", input.InputName, inputName)
204 | matrixInput = &d.Inputs[i]
205 | }
206 | }
207 |
208 | if matrixInput != nil && matrixOutput != nil {
209 | if !d.switching {
210 | d.switching = true
211 | d.messages <- "OUT" + outputName + "FR" + inputName
212 | }
213 | }
214 |
215 | select {
216 | case _, ok := <-d.finishedSwap:
217 | if ok {
218 | return
219 | }
220 | }
221 | }
222 |
223 | // init the device.
224 | func (d *BlustreamMatrix) init() {
225 | port := d.port
226 | // We are going to ask the device for the current status.
227 | time.Sleep(time.Millisecond * 500)
228 | d.statusIncoming = true
229 | d.statusReading = ReadingModel
230 | n, err := port.Write([]byte("STATUS\r\n"))
231 | if err != nil {
232 | d.statusReading = WaitingInput
233 | d.Error = err
234 | d.HasError = true
235 | debugLog("Could not send %d bytes for driver.", n)
236 | }
237 | port.Flush()
238 | }
239 |
240 | // init the device.
241 | func (d *BlustreamMatrix) pingStatus() {
242 | port := d.port
243 | // We are going to ask the device for the current status.
244 | time.Sleep(time.Millisecond * 500)
245 | d.statusIncoming = true
246 | n, err := port.Write([]byte("STATUS\r\n"))
247 | if err != nil {
248 | d.statusReading = WaitingInput
249 | d.Error = err
250 | d.HasError = true
251 | debugLog("Could not send %d bytes for driver.", n)
252 | }
253 | port.Flush()
254 | }
255 |
256 |
257 | // writePort manages a channel that allows us to send & receive data to this serial connection.
258 | func (d *BlustreamMatrix) writePort(port *serial.Port) {
259 | d.messages = make(chan string)
260 |
261 | go func() {
262 | for {
263 | select {
264 | case msg := <-d.messages:
265 | debugLog("==> WRITE PORT MSG: %s", msg)
266 | n, err := port.Write([]byte(msg + "\r\n"))
267 | if err != nil {
268 | debugLog("Error writing %d bytes: %s", n, err)
269 | d.HasError = true
270 | d.Error = err
271 | }
272 | }
273 | }
274 | }()
275 | }
276 |
277 | // readPort continuously reads data from the serial port, checks if there is some new data received
278 | // and sends it down to the serialResponse channel when we are g2g with a response from a command.
279 | //
280 | // TODO: This needs a big refactor. It's a little dodgy, but it does the trick.
281 | func (d *BlustreamMatrix) readPort() {
282 | port := *d.port
283 | var command string
284 | for {
285 | buf := make([]byte, 820)
286 | n, err := port.Read(buf)
287 | if err != nil {
288 | log.Fatal(n, err)
289 | }
290 |
291 |
292 | b := bytes.Trim(buf, "\x00")
293 | tmpString := string(b)
294 | command = command + tmpString
295 |
296 | commands := strings.Split(command, "\r\n")
297 |
298 | numCommands := len(commands)
299 | if numCommands > 1 {
300 | for i := range commands {
301 | // if we are at the last command, check that we have an empty command...
302 | if i == numCommands-1 {
303 | if commands[i] == "" {
304 | buf = []byte("")
305 | debugLog("BUFFER: %s %q", buf, buf)
306 | continue
307 | }
308 | } else if i != numCommands {
309 | currentCommand := commands[0]
310 | if len(commands) > 1 {
311 | numCommands--
312 | commands = commands[1:]
313 |
314 | //buf = []byte(string(commands.joi))
315 | command = strings.Join(commands, "\r\n")
316 | buf = []byte(command)
317 | }
318 |
319 | d.serialResponse <- currentCommand
320 |
321 | debugLog("==> Command #%d/%d: %s", i+1, len(commands), currentCommand)
322 | }
323 | }
324 |
325 | if EnableDebugMode == false{
326 | debugLog2("🧐 Inspecting the buffer contents")
327 | debugLog2("BUFFER CONTENTS: %s tmpString: %s", buf, tmpString)
328 | debugLog2("commands: %s - %q", commands, buf)
329 | }
330 | } else {
331 | //debugLog("Incomplete command from serial... expected newline")
332 | }
333 | }
334 | }
335 |
336 | // processResponses reads serial commands that have been fully read from the serial connection.
337 | func (d *BlustreamMatrix) processResponses() {
338 | d.serialResponse = make(chan string)
339 |
340 | go func() {
341 | for {
342 | select {
343 | case msg := <-d.serialResponse:
344 | debugLog("<== READ SERIAL COMMAND: %s %q", msg, msg)
345 |
346 | // Sometimes the output contains the model name. Hope we have it!
347 | if strings.Contains(msg, "> ") && len(d.state.ModelName) > 0 {
348 | s := strings.SplitN(msg, "> ", 2)
349 | if s[0] == d.state.ModelName {
350 | msg = strings.Join(s[1:], "> ")
351 | }
352 | }
353 |
354 | // NOTE status-response.txt to see what we are parsing.
355 | // If we see that the STATUS command is incoming, then we need to hold some state:
356 | if d.statusIncoming && msg == "STATUS" {
357 | // State 1: starting to read the status, but the command itself has not started to be received.
358 | // State 2: skip normal command processing, and parse the status command.
359 | // State 3: Finished reading the status command, go back to reading the command output as normal.
360 | debugLog("🍔 Eating the status command from Blustream")
361 | // Here we enter state 1.
362 | d.statusReading = ReadingModel
363 | d.statusStarted = true // get ready for state 2.
364 | d.statusIncoming = false
365 | } else if d.statusReading > NotReadingStatus && len(msg) > 2 {
366 | if msg[:2] == "==" {
367 | // for the first set of "==", we need to stay in the status incoming state.
368 | // for the second, we can leave this special state.
369 | if d.statusStarted {
370 | // Here we enter state 2.
371 | debugLog("🥇 The next line of should be the start of our statuses.")
372 | d.statusStarted = false
373 | continue
374 | }
375 |
376 | // Here we enter state 3.
377 | debugLog("🥇 We have finished reading the status.")
378 | d.statusStarted = true
379 | d.statusReading = NotReadingStatus // leave our status state
380 | continue
381 | }
382 |
383 | debugLog("😰 Status confirmed. Should it be?")
384 | d.readStatus(msg)
385 | continue
386 | }
387 |
388 | if len(msg) > 10 {
389 | // [SUCCESS]Set output 01 connect from input 02.
390 | if msg[:9] == "[SUCCESS]" {
391 | //match := []byte(msg[9:])
392 | r, _ := regexp.Compile(`Set output (\d*) connect from input (\d*)`)
393 | f := r.FindAllStringSubmatch(msg[9:], 12)
394 | if len(f[0]) == 3 {
395 | debugLog("Swapped input %s to output %s", f[0][2], f[0][1])
396 | }
397 | d.finishedSwap <- true
398 | d.switching = false
399 | d.switched = true
400 | }
401 | }
402 | }
403 | }
404 | }()
405 | }
406 |
407 | // LastError return the last
408 | func (d *BlustreamMatrix) LastError() error {
409 | return d.Error
410 | }
411 |
412 | // GetInput will return an input with a given name
413 | func (d *BlustreamMatrix) GetInput(inputName string) *BlustreamInput {
414 | for _, input := range d.Inputs {
415 | if input.InputName == inputName {
416 | return &input
417 | }
418 | }
419 | return nil
420 | }
421 |
422 | // debugLog will output something only if EnableDebugMode is true.
423 | func debugLog(msg string, v ...interface{}) {
424 | if EnableDebugMode {
425 | log.Printf("[blustream]: " + msg, v...)
426 | }
427 | }
--------------------------------------------------------------------------------
/client/icon/icon.go:
--------------------------------------------------------------------------------
1 | //+build linux darwin
2 |
3 | // File generated by 2goarray (http://github.com/cratonica/2goarray)
4 |
5 | package icon
6 |
7 | var Data []byte = []byte {
8 | 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
9 | 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20,
10 | 0x08, 0x06, 0x00, 0x00, 0x00, 0x73, 0x7a, 0x7a, 0xf4, 0x00, 0x00, 0x00,
11 | 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72,
12 | 0x65, 0x00, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x49, 0x6d, 0x61, 0x67,
13 | 0x65, 0x52, 0x65, 0x61, 0x64, 0x79, 0x71, 0xc9, 0x65, 0x3c, 0x00, 0x00,
14 | 0x03, 0x66, 0x69, 0x54, 0x58, 0x74, 0x58, 0x4d, 0x4c, 0x3a, 0x63, 0x6f,
15 | 0x6d, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x78, 0x6d, 0x70, 0x00,
16 | 0x00, 0x00, 0x00, 0x00, 0x3c, 0x3f, 0x78, 0x70, 0x61, 0x63, 0x6b, 0x65,
17 | 0x74, 0x20, 0x62, 0x65, 0x67, 0x69, 0x6e, 0x3d, 0x22, 0xef, 0xbb, 0xbf,
18 | 0x22, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x57, 0x35, 0x4d, 0x30, 0x4d, 0x70,
19 | 0x43, 0x65, 0x68, 0x69, 0x48, 0x7a, 0x72, 0x65, 0x53, 0x7a, 0x4e, 0x54,
20 | 0x63, 0x7a, 0x6b, 0x63, 0x39, 0x64, 0x22, 0x3f, 0x3e, 0x20, 0x3c, 0x78,
21 | 0x3a, 0x78, 0x6d, 0x70, 0x6d, 0x65, 0x74, 0x61, 0x20, 0x78, 0x6d, 0x6c,
22 | 0x6e, 0x73, 0x3a, 0x78, 0x3d, 0x22, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x3a,
23 | 0x6e, 0x73, 0x3a, 0x6d, 0x65, 0x74, 0x61, 0x2f, 0x22, 0x20, 0x78, 0x3a,
24 | 0x78, 0x6d, 0x70, 0x74, 0x6b, 0x3d, 0x22, 0x41, 0x64, 0x6f, 0x62, 0x65,
25 | 0x20, 0x58, 0x4d, 0x50, 0x20, 0x43, 0x6f, 0x72, 0x65, 0x20, 0x35, 0x2e,
26 | 0x30, 0x2d, 0x63, 0x30, 0x36, 0x30, 0x20, 0x36, 0x31, 0x2e, 0x31, 0x33,
27 | 0x34, 0x37, 0x37, 0x37, 0x2c, 0x20, 0x32, 0x30, 0x31, 0x30, 0x2f, 0x30,
28 | 0x32, 0x2f, 0x31, 0x32, 0x2d, 0x31, 0x37, 0x3a, 0x33, 0x32, 0x3a, 0x30,
29 | 0x30, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x3e, 0x20,
30 | 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44, 0x46, 0x20, 0x78, 0x6d, 0x6c,
31 | 0x6e, 0x73, 0x3a, 0x72, 0x64, 0x66, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70,
32 | 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72,
33 | 0x67, 0x2f, 0x31, 0x39, 0x39, 0x39, 0x2f, 0x30, 0x32, 0x2f, 0x32, 0x32,
34 | 0x2d, 0x72, 0x64, 0x66, 0x2d, 0x73, 0x79, 0x6e, 0x74, 0x61, 0x78, 0x2d,
35 | 0x6e, 0x73, 0x23, 0x22, 0x3e, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44,
36 | 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x72,
37 | 0x64, 0x66, 0x3a, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x22, 0x22, 0x20,
38 | 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3d,
39 | 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61,
40 | 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70,
41 | 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x6d, 0x6d, 0x2f, 0x22, 0x20, 0x78, 0x6d,
42 | 0x6c, 0x6e, 0x73, 0x3a, 0x73, 0x74, 0x52, 0x65, 0x66, 0x3d, 0x22, 0x68,
43 | 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f,
44 | 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31,
45 | 0x2e, 0x30, 0x2f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x2f, 0x52, 0x65, 0x73,
46 | 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, 0x23, 0x22, 0x20, 0x78,
47 | 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x6d, 0x70, 0x3d, 0x22, 0x68, 0x74,
48 | 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62,
49 | 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e,
50 | 0x30, 0x2f, 0x22, 0x20, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x4f, 0x72,
51 | 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65,
52 | 0x6e, 0x74, 0x49, 0x44, 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x64, 0x69,
53 | 0x64, 0x3a, 0x36, 0x37, 0x32, 0x34, 0x42, 0x45, 0x31, 0x35, 0x45, 0x44,
54 | 0x32, 0x30, 0x36, 0x38, 0x31, 0x31, 0x38, 0x38, 0x43, 0x36, 0x46, 0x32,
55 | 0x38, 0x31, 0x35, 0x44, 0x41, 0x33, 0x43, 0x35, 0x35, 0x35, 0x22, 0x20,
56 | 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65,
57 | 0x6e, 0x74, 0x49, 0x44, 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x64, 0x69,
58 | 0x64, 0x3a, 0x41, 0x33, 0x42, 0x34, 0x46, 0x42, 0x36, 0x36, 0x33, 0x41,
59 | 0x41, 0x38, 0x31, 0x31, 0x45, 0x32, 0x42, 0x32, 0x43, 0x41, 0x39, 0x37,
60 | 0x42, 0x44, 0x33, 0x34, 0x34, 0x31, 0x45, 0x46, 0x33, 0x32, 0x22, 0x20,
61 | 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e,
62 | 0x63, 0x65, 0x49, 0x44, 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x69, 0x69,
63 | 0x64, 0x3a, 0x41, 0x33, 0x42, 0x34, 0x46, 0x42, 0x36, 0x35, 0x33, 0x41,
64 | 0x41, 0x38, 0x31, 0x31, 0x45, 0x32, 0x42, 0x32, 0x43, 0x41, 0x39, 0x37,
65 | 0x42, 0x44, 0x33, 0x34, 0x34, 0x31, 0x45, 0x46, 0x33, 0x32, 0x22, 0x20,
66 | 0x78, 0x6d, 0x70, 0x3a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x54,
67 | 0x6f, 0x6f, 0x6c, 0x3d, 0x22, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x50,
68 | 0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x20, 0x43, 0x53, 0x35,
69 | 0x20, 0x4d, 0x61, 0x63, 0x69, 0x6e, 0x74, 0x6f, 0x73, 0x68, 0x22, 0x3e,
70 | 0x20, 0x3c, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x44, 0x65, 0x72, 0x69,
71 | 0x76, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x20, 0x73, 0x74, 0x52, 0x65,
72 | 0x66, 0x3a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x44,
73 | 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x69, 0x69, 0x64, 0x3a, 0x45, 0x36,
74 | 0x38, 0x31, 0x34, 0x43, 0x36, 0x41, 0x45, 0x45, 0x32, 0x30, 0x36, 0x38,
75 | 0x31, 0x31, 0x38, 0x38, 0x43, 0x36, 0x46, 0x32, 0x38, 0x31, 0x35, 0x44,
76 | 0x41, 0x33, 0x43, 0x35, 0x35, 0x35, 0x22, 0x20, 0x73, 0x74, 0x52, 0x65,
77 | 0x66, 0x3a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x44,
78 | 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x64, 0x69, 0x64, 0x3a, 0x36, 0x37,
79 | 0x32, 0x34, 0x42, 0x45, 0x31, 0x35, 0x45, 0x44, 0x32, 0x30, 0x36, 0x38,
80 | 0x31, 0x31, 0x38, 0x38, 0x43, 0x36, 0x46, 0x32, 0x38, 0x31, 0x35, 0x44,
81 | 0x41, 0x33, 0x43, 0x35, 0x35, 0x35, 0x22, 0x2f, 0x3e, 0x20, 0x3c, 0x2f,
82 | 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
83 | 0x69, 0x6f, 0x6e, 0x3e, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x52,
84 | 0x44, 0x46, 0x3e, 0x20, 0x3c, 0x2f, 0x78, 0x3a, 0x78, 0x6d, 0x70, 0x6d,
85 | 0x65, 0x74, 0x61, 0x3e, 0x20, 0x3c, 0x3f, 0x78, 0x70, 0x61, 0x63, 0x6b,
86 | 0x65, 0x74, 0x20, 0x65, 0x6e, 0x64, 0x3d, 0x22, 0x72, 0x22, 0x3f, 0x3e,
87 | 0x5d, 0xed, 0x35, 0xe2, 0x00, 0x00, 0x04, 0xee, 0x49, 0x44, 0x41, 0x54,
88 | 0x78, 0xda, 0xc4, 0x57, 0xcf, 0x6f, 0x55, 0x45, 0x18, 0x3d, 0xf3, 0xe3,
89 | 0xfe, 0xea, 0x7b, 0xaf, 0xa5, 0x6d, 0x0a, 0xd8, 0x34, 0xbe, 0x16, 0x83,
90 | 0x69, 0x8c, 0x2e, 0x04, 0xe2, 0x86, 0xb8, 0x70, 0xe1, 0x06, 0x35, 0x18,
91 | 0x13, 0x5d, 0x60, 0x8c, 0xd1, 0x68, 0xe2, 0xca, 0xb8, 0x33, 0x31, 0xf1,
92 | 0x6f, 0x70, 0x67, 0x5c, 0xb1, 0x62, 0xe1, 0x46, 0x42, 0x8c, 0x0b, 0xe3,
93 | 0x46, 0x34, 0x25, 0x11, 0x41, 0x14, 0xa4, 0x24, 0xa4, 0x08, 0x58, 0x0a,
94 | 0x29, 0x14, 0x0a, 0x6d, 0xe9, 0xeb, 0xbb, 0xef, 0xce, 0x9d, 0xf1, 0xcc,
95 | 0xbd, 0xaf, 0xa5, 0x44, 0x63, 0x49, 0xee, 0x4b, 0x78, 0xc9, 0xf7, 0xee,
96 | 0x9d, 0x3b, 0x33, 0x77, 0xce, 0x77, 0xbe, 0xf3, 0x7d, 0x33, 0x57, 0x38,
97 | 0xe7, 0xf0, 0x38, 0x7f, 0x7a, 0xab, 0x01, 0xe2, 0xd9, 0x37, 0xff, 0xeb,
98 | 0xb1, 0xa2, 0x7d, 0x46, 0xdb, 0xeb, 0x87, 0xd0, 0x8e, 0xd3, 0xbe, 0xf8,
99 | 0xd7, 0x28, 0x6b, 0xe1, 0x2e, 0x7c, 0xf3, 0xbf, 0xef, 0x97, 0x5b, 0x42,
100 | 0x34, 0x06, 0xf0, 0x2c, 0x6d, 0x36, 0xe0, 0x43, 0xda, 0x88, 0x90, 0xf2,
101 | 0x90, 0xea, 0x6f, 0xbc, 0x0b, 0x21, 0x9e, 0x67, 0xfb, 0x8d, 0xa2, 0xcf,
102 | 0x76, 0xc7, 0x70, 0x5e, 0xff, 0x40, 0x7f, 0x75, 0x06, 0xc6, 0x27, 0x9a,
103 | 0xb8, 0x76, 0x63, 0xbe, 0x70, 0x53, 0xf0, 0x2f, 0xe7, 0x02, 0xd6, 0xda,
104 | 0x71, 0x36, 0xaf, 0xd1, 0x0e, 0xcb, 0x38, 0x16, 0x5c, 0xf4, 0x7a, 0xbe,
105 | 0xbc, 0xd2, 0x14, 0x71, 0x04, 0x19, 0x68, 0xf8, 0xb0, 0x4a, 0xda, 0x2e,
106 | 0xce, 0xad, 0x0c, 0x60, 0xf7, 0x53, 0xbb, 0x90, 0x87, 0x11, 0x76, 0x8f,
107 | 0xee, 0x40, 0xa8, 0x35, 0xfe, 0x9a, 0xbf, 0x35, 0x36, 0x73, 0xf9, 0xea,
108 | 0x24, 0xd2, 0xf4, 0x20, 0x5d, 0x2d, 0xbc, 0x15, 0x49, 0x0c, 0x1d, 0x86,
109 | 0xdf, 0x09, 0x25, 0x7f, 0x80, 0x14, 0xd3, 0x8e, 0x20, 0x93, 0x30, 0x44,
110 | 0xa3, 0xd1, 0xd8, 0x12, 0x80, 0xdc, 0x3a, 0x02, 0x06, 0xa4, 0xba, 0xb0,
111 | 0x5a, 0x12, 0x2b, 0x2e, 0xf4, 0x35, 0xb4, 0x3e, 0x58, 0x84, 0xde, 0xb3,
112 | 0x91, 0xa6, 0x64, 0x86, 0xf7, 0x4a, 0xbe, 0x4a, 0xcf, 0x8f, 0xe4, 0x26,
113 | 0x0f, 0x42, 0xa5, 0xf0, 0xcc, 0xe8, 0x13, 0x50, 0xfe, 0x79, 0x65, 0x11,
114 | 0xf2, 0x1d, 0x86, 0x62, 0x9a, 0x9a, 0xbe, 0x88, 0xa7, 0x77, 0x8e, 0x04,
115 | 0x9d, 0x34, 0x1b, 0x45, 0xda, 0x81, 0xf7, 0xde, 0x53, 0x9d, 0x77, 0x32,
116 | 0x04, 0x49, 0x82, 0x88, 0x1e, 0x67, 0xb9, 0xa9, 0x37, 0xe2, 0x44, 0x3c,
117 | 0x39, 0x3c, 0x84, 0xa8, 0x1b, 0x8a, 0xca, 0x00, 0xba, 0xbf, 0x28, 0x35,
118 | 0xe6, 0xc0, 0x9f, 0x17, 0x2f, 0x7d, 0x20, 0xad, 0x6d, 0xc2, 0xe4, 0xd8,
119 | 0x10, 0x45, 0x10, 0x20, 0xd0, 0x01, 0xfa, 0x09, 0xa2, 0x16, 0x85, 0x13,
120 | 0xf5, 0x28, 0x3a, 0x1a, 0x28, 0x75, 0x98, 0x4b, 0x7f, 0xcf, 0xde, 0x56,
121 | 0xe5, 0x10, 0xf0, 0xf7, 0x36, 0x6d, 0x4a, 0x0a, 0x71, 0x14, 0x4a, 0x1d,
122 | 0xb0, 0x7e, 0xce, 0x3a, 0xb5, 0xbc, 0x2a, 0xea, 0x22, 0x50, 0x92, 0x11,
123 | 0x90, 0xd0, 0x8a, 0xdc, 0x0b, 0xbc, 0xc2, 0x1e, 0x9f, 0x7b, 0xbf, 0xd0,
124 | 0x3e, 0xea, 0x05, 0x80, 0x8f, 0x69, 0xfb, 0x7c, 0x76, 0x81, 0x8e, 0x23,
125 | 0x75, 0xa5, 0x75, 0x31, 0x68, 0x0f, 0x80, 0x66, 0xac, 0x44, 0x9a, 0x09,
126 | 0x38, 0x5b, 0xbe, 0x92, 0xdd, 0xcf, 0xd1, 0xde, 0xab, 0x1c, 0x82, 0x95,
127 | 0x54, 0xdf, 0x58, 0x5a, 0xe3, 0xab, 0x3a, 0x0e, 0xf5, 0xc8, 0x61, 0x72,
128 | 0x4c, 0x53, 0x5c, 0x0e, 0x27, 0x67, 0xb2, 0x62, 0x76, 0x28, 0x55, 0x51,
129 | 0x97, 0xf6, 0x8c, 0x4b, 0xf4, 0x45, 0xc0, 0xad, 0x7b, 0xe4, 0xbd, 0x23,
130 | 0xb0, 0xda, 0x21, 0x51, 0x10, 0x57, 0x2a, 0x03, 0x38, 0xb4, 0xf7, 0xef,
131 | 0x99, 0xe0, 0x85, 0x35, 0x34, 0x87, 0x15, 0x26, 0x77, 0x0e, 0xa3, 0x39,
132 | 0x5e, 0x73, 0xc7, 0x7e, 0x5a, 0xc5, 0x5b, 0x9f, 0xcf, 0x09, 0xd4, 0x15,
133 | 0x19, 0x50, 0x94, 0x84, 0xc4, 0xfb, 0x2f, 0x25, 0x78, 0x6d, 0x9f, 0xc4,
134 | 0xed, 0x85, 0x0c, 0x73, 0xf7, 0x52, 0xcc, 0x2e, 0x38, 0x5c, 0x5f, 0x74,
135 | 0xe7, 0x2a, 0x03, 0xf8, 0xe4, 0xe5, 0x9b, 0xe7, 0xa0, 0xb7, 0xd1, 0xc9,
136 | 0x41, 0x0a, 0xbf, 0x8f, 0x2e, 0xf7, 0x09, 0xa9, 0x38, 0x4d, 0xcc, 0x16,
137 | 0x9e, 0xfb, 0xb0, 0x07, 0x5a, 0x50, 0x8b, 0x75, 0x48, 0x1d, 0x61, 0x64,
138 | 0xb0, 0x8d, 0xed, 0x43, 0x6d, 0xec, 0x99, 0xa0, 0xfe, 0x4c, 0x6b, 0xba,
139 | 0x32, 0x80, 0xd4, 0xec, 0xb8, 0x2c, 0x5c, 0x9d, 0x45, 0xbd, 0x21, 0x21,
140 | 0x6b, 0x2c, 0x46, 0x35, 0xa6, 0x9d, 0x7d, 0xa0, 0x01, 0x0a, 0x30, 0x24,
141 | 0x0b, 0x51, 0x5c, 0x67, 0x23, 0x41, 0x26, 0x42, 0x6a, 0xc5, 0x9b, 0x36,
142 | 0x70, 0xe1, 0xb5, 0xb0, 0x72, 0x1d, 0x08, 0x86, 0x66, 0xa1, 0xe2, 0x25,
143 | 0xe8, 0x81, 0x41, 0xa1, 0x6a, 0x64, 0xa0, 0x1f, 0x41, 0xdc, 0x59, 0xef,
144 | 0xa5, 0xfa, 0x15, 0x42, 0x0f, 0xa2, 0x00, 0xe0, 0x59, 0x08, 0xe0, 0xf8,
145 | 0xcc, 0x09, 0x71, 0x9b, 0x20, 0x66, 0xab, 0xd7, 0x01, 0x99, 0xdc, 0xa4,
146 | 0xe7, 0x57, 0x84, 0x6e, 0x0c, 0xd2, 0x98, 0xf7, 0x83, 0xf4, 0x76, 0x6d,
147 | 0x23, 0x7f, 0x7c, 0x0a, 0xfa, 0x10, 0xc4, 0x31, 0xfb, 0x14, 0x37, 0x1f,
148 | 0x4d, 0xf1, 0x51, 0x13, 0xac, 0x42, 0x97, 0x9d, 0x50, 0x8b, 0xd5, 0x2b,
149 | 0x61, 0x30, 0x90, 0x41, 0x86, 0x97, 0xe8, 0xfd, 0x9e, 0x02, 0x80, 0xda,
150 | 0x46, 0x6f, 0x57, 0x8b, 0x52, 0xe0, 0x33, 0xd3, 0xe7, 0x3f, 0x0b, 0x0f,
151 | 0xa2, 0x64, 0x80, 0x8d, 0x3a, 0x84, 0x66, 0x85, 0x2c, 0xc2, 0x93, 0xcf,
152 | 0x08, 0x17, 0xd9, 0xea, 0x75, 0x40, 0x51, 0x78, 0x32, 0xfa, 0x83, 0x61,
153 | 0x80, 0xf0, 0xf7, 0x5c, 0x24, 0x8c, 0x06, 0x20, 0x65, 0x39, 0xd5, 0xd7,
154 | 0xfb, 0x52, 0x03, 0x04, 0xa7, 0xfd, 0xd8, 0x84, 0xe0, 0x22, 0xee, 0x1d,
155 | 0xd1, 0x19, 0x7f, 0xad, 0xce, 0x80, 0xf2, 0x2f, 0x91, 0x67, 0x79, 0xe3,
156 | 0xe9, 0xf0, 0x60, 0x10, 0x84, 0x0d, 0x36, 0x65, 0xb1, 0x1f, 0x48, 0xd1,
157 | 0x65, 0x20, 0x68, 0xa0, 0x18, 0x23, 0x03, 0x7f, 0x65, 0x47, 0x78, 0x1e,
158 | 0xc2, 0x55, 0xdf, 0x0d, 0xfd, 0x82, 0x7c, 0xe9, 0x79, 0xde, 0x2d, 0x95,
159 | 0xdb, 0xaf, 0x45, 0x18, 0xf8, 0xf4, 0x2b, 0xa7, 0x7a, 0x22, 0x22, 0xb6,
160 | 0x83, 0x90, 0x0b, 0x93, 0xfb, 0x32, 0x39, 0xe4, 0x02, 0x41, 0x9c, 0x2f,
161 | 0xc0, 0xf4, 0xa0, 0x14, 0x7b, 0x4f, 0xe7, 0xe0, 0xb2, 0x0b, 0xb0, 0x54,
162 | 0x7f, 0xbe, 0x46, 0x00, 0xe9, 0x03, 0x00, 0x3e, 0x04, 0xdc, 0xf9, 0x22,
163 | 0xc5, 0xca, 0xc8, 0x7e, 0xe7, 0x7c, 0xbd, 0xce, 0x09, 0x58, 0x2c, 0x6c,
164 | 0xe4, 0x6a, 0x25, 0x00, 0x8e, 0x2f, 0x76, 0xc6, 0x72, 0xe3, 0x3f, 0xed,
165 | 0xf2, 0x55, 0x96, 0xe4, 0x65, 0xc4, 0xaa, 0xc5, 0xb8, 0xcb, 0x82, 0x10,
166 | 0x81, 0x32, 0x0b, 0x22, 0xc1, 0xcc, 0x30, 0x34, 0xdb, 0x26, 0x88, 0xec,
167 | 0xd7, 0x62, 0x2f, 0x76, 0xb6, 0x3a, 0x00, 0x97, 0x67, 0xa5, 0x99, 0xd6,
168 | 0x94, 0x33, 0xcb, 0x04, 0x70, 0x17, 0x7d, 0x62, 0x85, 0x27, 0x9e, 0x2e,
169 | 0x80, 0x42, 0x84, 0xac, 0x07, 0xee, 0x3e, 0x01, 0xac, 0x70, 0x6c, 0xcb,
170 | 0x33, 0x71, 0x82, 0x13, 0x50, 0x58, 0xe5, 0x3a, 0x60, 0x56, 0xd7, 0xa1,
171 | 0x9c, 0x76, 0xb6, 0x73, 0x1f, 0xc2, 0xd4, 0x7d, 0x75, 0xeb, 0x5b, 0x07,
172 | 0x00, 0x0f, 0x80, 0xd9, 0x60, 0xb9, 0xb8, 0x3f, 0xc0, 0x9a, 0xb5, 0x3b,
173 | 0x44, 0x71, 0xa6, 0x67, 0xc7, 0x72, 0x97, 0xaf, 0xac, 0xdf, 0x5e, 0x45,
174 | 0xee, 0xce, 0x5a, 0xb4, 0xf7, 0x2b, 0xab, 0x91, 0x6c, 0xe8, 0x8b, 0x00,
175 | 0x28, 0x7a, 0x91, 0x2f, 0x77, 0x99, 0x4a, 0x7f, 0x23, 0xb1, 0x37, 0x7a,
176 | 0x06, 0x00, 0xd9, 0xbd, 0x8d, 0x53, 0xbe, 0x3f, 0x98, 0x38, 0xdb, 0xda,
177 | 0xaf, 0x84, 0x46, 0xac, 0x5d, 0xa9, 0x7a, 0x86, 0x40, 0x4b, 0x02, 0x30,
178 | 0x4b, 0x3c, 0x2d, 0xb3, 0xfc, 0x5b, 0x73, 0xbc, 0xa7, 0x1f, 0x26, 0x96,
179 | 0x9e, 0x6d, 0xa2, 0xe3, 0x47, 0x61, 0xe5, 0xa7, 0x42, 0x05, 0x3c, 0x03,
180 | 0x5a, 0x94, 0x25, 0xcf, 0x33, 0xc0, 0xfb, 0x9c, 0x59, 0x9a, 0x53, 0xac,
181 | 0xce, 0xfd, 0xfc, 0x28, 0xea, 0x7f, 0xf4, 0x10, 0x64, 0xf3, 0x9b, 0x9b,
182 | 0xa7, 0xb8, 0xc9, 0xcc, 0x91, 0x81, 0xb1, 0x7a, 0x68, 0x4a, 0x00, 0xc2,
183 | 0xef, 0x86, 0x06, 0x8a, 0xa1, 0xe2, 0xfa, 0x97, 0xa8, 0x86, 0xdf, 0x7b,
184 | 0xca, 0x80, 0xcb, 0x16, 0x36, 0x37, 0x17, 0x73, 0x67, 0x4e, 0xf2, 0x14,
185 | 0x34, 0x56, 0x8f, 0x92, 0xf2, 0x7c, 0x40, 0x6f, 0x7d, 0x0d, 0x10, 0xf9,
186 | 0x5d, 0x8e, 0x75, 0x27, 0x18, 0x8c, 0x56, 0x6f, 0x01, 0x98, 0xf9, 0x87,
187 | 0xdb, 0xce, 0x1c, 0x81, 0x69, 0xbf, 0x58, 0x8b, 0x9b, 0xdb, 0x81, 0x7a,
188 | 0x91, 0xc9, 0xa1, 0x6c, 0x51, 0x03, 0x77, 0xce, 0xb8, 0x5c, 0x7f, 0xe5,
189 | 0x8a, 0xcf, 0xc6, 0x1e, 0x02, 0x78, 0x38, 0x9e, 0xfe, 0xde, 0x1c, 0x83,
190 | 0x5d, 0x38, 0x55, 0x0b, 0x87, 0x5f, 0x67, 0xfb, 0x1d, 0x3e, 0x68, 0x2b,
191 | 0x61, 0xbe, 0x84, 0x6b, 0x7f, 0xeb, 0x50, 0x6b, 0x97, 0x63, 0x1e, 0xfd,
192 | 0x8b, 0x5b, 0x3c, 0xee, 0xcf, 0xf3, 0x7f, 0x04, 0x18, 0x00, 0xe0, 0x6e,
193 | 0xdd, 0x63, 0x24, 0x57, 0x80, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
194 | 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
195 | }
196 |
--------------------------------------------------------------------------------