commit ceeedf6d585c32286a2a14bc16ea657adb94ff10 Author: Terekhin Alexandr Date: Sun Dec 3 20:16:51 2023 +0300 feat: YAB-1785: Initial commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d778fa1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:20.04 + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y ca-certificates make git curl gcc libudev-dev dpkg-dev dh-make + +RUN sh -c 'curl -OL https://go.dev/dl/go1.21.3.linux-amd64.tar.gz && \ + tar -C /usr/local -xvf go1.21.3.linux-amd64.tar.gz' +ENV PATH="${PATH}:/usr/local/go/bin" +ENV CGO_ENABLED=1 + +ADD ./ /data +WORKDIR /data + +CMD ["make", "docker-deb"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aadf825 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +BINARY_NAME=keyboard-reader +LD_FLAGS="-s -w -buildid="$(shell git rev-parse HEAD) + +build: + #GOARCH=amd64 GOOS=linux go build -o ${BINARY_NAME} -trimpath -ldflags ${LD_FLAGS} -tags netgo + go build -o ${BINARY_NAME} -ldflags ${LD_FLAGS} -tags netgo + +run: build + ./${BINARY_NAME} --list + +clean: + go clean + rm ./debian/opt/ocppc/bin/${BINARY_NAME} || true + rm -rf ./build || true + docker container rm ${BINARY_NAME} || true + docker image rm ${BINARY_NAME} || true + +deb: clean build + cp ${BINARY_NAME} ./debian/opt/ocppc/bin/ + cd ./debian && dpkg-buildpackage -rfakeroot --no-sign --target-arch amd64 --host-arch amd64 + +docker-deb: clean + docker build -t ${BINARY_NAME} . + docker container run --name ${BINARY_NAME} ${BINARY_NAME} + docker container cp ${BINARY_NAME}:/data ./build diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/debian/debian/changelog b/debian/debian/changelog new file mode 100644 index 0000000..455ce43 --- /dev/null +++ b/debian/debian/changelog @@ -0,0 +1,6 @@ +yablochkov-keyboard-reader (0.1.0) release; urgency=low + + [ Alexander Terekhin ] + * Initial version + + -- Alexander Terekhin Sun, 03 Dec 2023 17:01:00 +0600 \ No newline at end of file diff --git a/debian/debian/compat b/debian/debian/compat new file mode 100644 index 0000000..9d60796 --- /dev/null +++ b/debian/debian/compat @@ -0,0 +1 @@ +11 \ No newline at end of file diff --git a/debian/debian/control b/debian/debian/control new file mode 100644 index 0000000..c8a453e --- /dev/null +++ b/debian/debian/control @@ -0,0 +1,11 @@ +Source: yablochkov-keyboard-reader +Section: misc +Priority: optional +Maintainer: Alexander Terekhin + +Package: yablochkov-keyboard-reader +Version: 0.1 +Architecture: amd64 +Installed-Size: 5008848 +Description: Yablochkov keyboard reader + diff --git a/debian/debian/postinst b/debian/debian/postinst new file mode 100644 index 0000000..6946cfc --- /dev/null +++ b/debian/debian/postinst @@ -0,0 +1,2 @@ +chmod +x /opt/ocppc/bin/keyboard-reader +systemctl enable keyboard-reader --now \ No newline at end of file diff --git a/debian/debian/rules b/debian/debian/rules new file mode 100644 index 0000000..2be5831 --- /dev/null +++ b/debian/debian/rules @@ -0,0 +1,23 @@ +#!/usr/bin/make -f + +build: + @ echo BUILD: nothing to do here + +binary: + @ echo BINARY + dh_testroot + dh_prep + dh_install + dh_installdocs + dh_installchangelogs + dh_installexamples + dh_installman + dh_link + dh_compress + dh_fixperms + dh_installdeb + dh_gencontrol + dh_md5sums + dh_builddeb + +.PHONY: build clean binary \ No newline at end of file diff --git a/debian/debian/yablochkov-keyboard-reader.install b/debian/debian/yablochkov-keyboard-reader.install new file mode 100644 index 0000000..a04b84c --- /dev/null +++ b/debian/debian/yablochkov-keyboard-reader.install @@ -0,0 +1,2 @@ +etc / +opt / \ No newline at end of file diff --git a/debian/etc/systemd/system/keyboard-reader.service b/debian/etc/systemd/system/keyboard-reader.service new file mode 100644 index 0000000..a168990 --- /dev/null +++ b/debian/etc/systemd/system/keyboard-reader.service @@ -0,0 +1,18 @@ +[Unit] +Description=Yablochkov keyboard reader + +[Service] +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=keyboard-reader +WorkingDirectory=/opt/ocppc +EnvironmentFile=/opt/conf/configuration_vars.env +EnvironmentFile=/opt/conf/configuration_vars_model.env +EnvironmentFile=/opt/conf/configuration_vars_personal.env +ExecStart=/opt/ocppc/bin/keyboard-reader --serial=OEM_TWN4_B1.64_NKF4.64_STD2.04 +User=debian +Group=input +ExecStop=/bin/kill -TERM $MAINPID + +[Install] +WantedBy=multi-user.target diff --git a/evdev/evdev.go b/evdev/evdev.go new file mode 100644 index 0000000..478b693 --- /dev/null +++ b/evdev/evdev.go @@ -0,0 +1,182 @@ +package evdev + +/* + #include + + static int _EVIOCGBIT(int ev, int len) {return EVIOCGBIT(ev, len);} +*/ +import "C" +import ( + "bytes" + "encoding/binary" + "os" + "syscall" + "unsafe" +) + +const ( + EVIOCGRAB = C.EVIOCGRAB // grab/release device +) + +var keyMap = map[uint16]rune{ + 41: '`', + 2: '1', + 3: '2', + 4: '3', + 5: '4', + 6: '5', + 7: '6', + 8: '7', + 9: '8', + 10: '9', + 11: '0', + 12: '-', + 13: '=', + 98: '/', + 55: '*', + 74: '-', + 16: 'Q', + 17: 'W', + 18: 'E', + 19: 'R', + 20: 'T', + 21: 'Y', + 22: 'U', + 23: 'I', + 24: 'O', + 25: 'P', + 26: '[', + 27: ']', + 71: '7', + 72: '8', + 73: '9', + 78: '+', + 30: 'A', + 31: 'S', + 32: 'D', + 33: 'F', + 34: 'G', + 35: 'H', + 36: 'J', + 37: 'K', + 38: 'L', + 39: ';', + 40: '\'', + 75: '4', + 76: '5', + 77: '6', + 44: 'Z', + 45: 'X', + 46: 'C', + 47: 'V', + 48: 'B', + 49: 'N', + 50: 'M', + 51: ',', + 52: '.', + 53: '/', + 43: '\\', + 79: '1', + 80: '2', + 81: '3', + 57: ' ', + 82: '0', + 83: '.', +} + +var eventSize = int(unsafe.Sizeof(InputEvent{})) + +type InputEvent struct { + Time syscall.Timeval // time in seconds since epoch at which event occurred + Type uint16 // event type - one of ecodes.EV_* + Code uint16 // event code related to the event type + Value int32 // event value related to the event type +} + +// InputDevice A Linux input device from which events can be read. +type InputDevice struct { + Fn string // path to input device (devnode) + bufLen int // read buffer size + + Name string // device name + Phys string // physical topology of device + File *os.File // an open file handle to the input device +} + +func EVIOCGBIT(ev, l int) int { return int(C._EVIOCGBIT(C.int(ev), C.int(l))) } // get event bits + +// Open an evdev input device. +func Open(devNode string, bufLen int) (*InputDevice, error) { + f, err := os.Open(devNode) + if err != nil { + return nil, err + } + + dev := InputDevice{} + dev.Fn = devNode + dev.File = f + + dev.bufLen = bufLen + + return &dev, nil +} + +func (dev *InputDevice) Close() { + if dev.File != nil { + dev.File.Close() + } +} + +func ioctl(fd uintptr, name uintptr, data unsafe.Pointer) syscall.Errno { + _, _, err := syscall.RawSyscall(syscall.SYS_IOCTL, fd, name, uintptr(data)) + return err +} + +// Grab the input device exclusively. +func (dev *InputDevice) Grab() error { + grab := int(1) + if err := ioctl(dev.File.Fd(), uintptr(EVIOCGRAB), unsafe.Pointer(&grab)); err != 0 { + return err + } + + return nil +} + +// Release a grabbed input device. +func (dev *InputDevice) Release() error { + if err := ioctl(dev.File.Fd(), uintptr(EVIOCGRAB), unsafe.Pointer(nil)); err != 0 { + return err + } + + return nil +} + +// Read and return a slice of input events from device. +func (dev *InputDevice) Read() ([]InputEvent, error) { + buffer := make([]byte, eventSize*dev.bufLen) + + count, err := dev.File.Read(buffer) + if err != nil { + return nil, err + } + + events := make([]InputEvent, count/eventSize) + + b := bytes.NewBuffer(buffer) + err = binary.Read(b, binary.LittleEndian, &events) + if err != nil { + return nil, err + } + + return events, err +} + +// GetKey Return appropriate key rune if present +func (event *InputEvent) GetKey() (rune, bool) { + if event.Type != 1 || event.Value != 1 { + return 0, false + } + + r, ok := keyMap[event.Code] + return r, ok +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..942fb89 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module keyboard-reader + +go 1.21.3 + +require github.com/farjump/go-libudev v0.0.0-20171109190736-8b0739cd6d0b + +require ( + github.com/jkeiser/iter v0.0.0-20200628201005-c8aa0ae784d1 // indirect + golang.org/x/sys v0.15.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e386031 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/farjump/go-libudev v0.0.0-20171109190736-8b0739cd6d0b h1:zMD1x/LqZnujKnuquz9rbl/P6HZomUj0YXD/yxEAXXM= +github.com/farjump/go-libudev v0.0.0-20171109190736-8b0739cd6d0b/go.mod h1:yzTdDrJ3rMj/15/ayeyb6HhTtu7VClWfiarB328Iew0= +github.com/jkeiser/iter v0.0.0-20200628201005-c8aa0ae784d1 h1:smvLGU3obGU5kny71BtE/ibR0wIXRUiRFDmSn0Nxz1E= +github.com/jkeiser/iter v0.0.0-20200628201005-c8aa0ae784d1/go.mod h1:fP/NdyhRVOv09PLRbVXrSqHhrfQypdZwgE2L4h2U5C8= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/keyboard-reader.go b/keyboard-reader.go new file mode 100644 index 0000000..f561d07 --- /dev/null +++ b/keyboard-reader.go @@ -0,0 +1,118 @@ +package main + +import ( + "flag" + udev "github.com/farjump/go-libudev" + "keyboard-reader/evdev" + "log" + "os" + "strings" + "time" +) + +const bufferLength = 32 + +func main() { + device := flag.String("dev", "", "Keyboard device") + vendor := flag.String("vendor", "", "Device vendor") + serial := flag.String("serial", "", "Device id serial") + list := flag.Bool("list", false, "List input devices") + connectorId := flag.Uint("connector", 0, "Connector id") + timeout := flag.Int("timeout", 5, "Http timeout, sec") + url := flag.String("url", "http://localhost:8914/state/idTag", "Endpoint url") + flag.Parse() + + if *list { + if devs, err := getUdevEnumerate().Devices(); err != nil { + log.Printf("Can't list devicies: %e", err) + } else { + for _, dev := range devs { + log.Println(dev.Properties()) + } + } + } + + if len(*vendor) == 0 && len(*device) == 0 && len(*serial) == 0 && !*list { + flag.Usage() + } + + if len(*vendor) > 0 { + device = getDevByProperty(*vendor, "ID_VENDOR") + } + + if len(*serial) > 0 { + device = getDevByProperty(*serial, "ID_SERIAL") + } + + if device == nil || len(*device) == 0 { + log.Println("Keyboard device not selected") + os.Exit(1) + } + + var dev *evdev.InputDevice + var events []evdev.InputEvent + var err error + + // Open our argv as a device and a file, so we can get the file descriptor. + if dev, err = evdev.Open(*device, bufferLength); err != nil { + // If the device can't be opened, spit out an error and exit. + log.Printf("Unable to open input device: %s\n", *device) + os.Exit(1) + } + defer dev.Close() + + if err = dev.Grab(); err != nil { + log.Printf("Can't grab input from device: %s, error: %e\n", *device, err) + os.Exit(2) + } + defer dev.Release() + + client := NewRest(*connectorId, *url, time.Duration(*timeout)*time.Second) + + // Run an infinite loop. + for { + // Read the device events. + if events, err = dev.Read(); err != nil { + log.Printf("Can't read input from device: %s, error: %e\n", *device, err) + os.Exit(3) + } + + var sb strings.Builder + + // Iterate through the events. + for _, event := range events { + if r, ok := event.GetKey(); ok { + sb.WriteRune(r) + } + } + + if sb.Len() > 0 { + id := sb.String() + log.Printf("Send '%s'", id) + client.Send(id) + } + } +} + +func getDevByProperty(value string, property string) *string { + e := getUdevEnumerate() + e.AddMatchProperty(property, value) + + if devices, err := e.Devices(); err == nil { + for _, item := range devices { + if name, found := item.Properties()["DEVNAME"]; found { + log.Printf("Found device by %s: %s\n", property, name) + return &name + } + } + } + return nil +} + +func getUdevEnumerate() *udev.Enumerate { + // Create Udev and Enumerate + u := udev.Udev{} + e := u.NewEnumerate() + e.AddMatchSysname("event*") + return e +} diff --git a/rest.go b/rest.go new file mode 100644 index 0000000..7cfd2c8 --- /dev/null +++ b/rest.go @@ -0,0 +1,146 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" + "time" +) + +type MonitorIdTag struct { + ConnectorId uint `json:"connectorId"` + IdTag string `json:"idTag"` +} + +type ResponseEntity struct { + Timestamp *time.Time `json:"timestamp"` + Status uint `json:"status"` + Error *string `json:"error"` + Path *string `json:"path"` +} + +type RestInterface interface { + Send(id string) +} + +type rest struct { + connectorId uint + url string + timeout time.Duration +} + +func NewRest(connectorId uint, url string, timeout time.Duration) RestInterface { + r := rest{connectorId: connectorId, url: url, timeout: timeout} + return &r +} + +func (r *rest) Send(id string) { + go r.send(id) +} + +func (r *rest) send(id string) { + tag := &MonitorIdTag{IdTag: id, ConnectorId: r.connectorId} + + //if r.connectorId > 0 { + // tag.ConnectorId = &r.connectorId + //} + + var b []byte + var err error + var request *http.Request + var response *http.Response + + if b, err = json.Marshal(tag); err != nil { + log.Printf("Can't marschal request: %e\n", err) + return + } + + ctx, _ := context.WithTimeout(context.Background(), r.timeout) + byteReader := bytes.NewReader(b) + + if request, err = http.NewRequestWithContext(ctx, "POST", r.url, byteReader); err != nil { + log.Printf("Can't build new request: %e\n", err) + return + } + + request.Header.Add("Content-Type", "application/json") + + if response, err = http.DefaultClient.Do(request); err != nil { + log.Printf("Error while rerforming request: %e\n", err) + return + } + + switch response.StatusCode { + case 200: + log.Printf("Result OK for id '%s'\n", id) + case 401: + log.Printf("Result UNAUTHORIZED for id '%s'\n", id) + default: + log.Printf("Recieve non sucess status code '%d' for id '%s'\n", response.StatusCode, id) + } + + if response.ContentLength == 0 { + log.Println("Response body empty") + return + } + + if b, err = io.ReadAll(response.Body); err != nil { + log.Printf("Can't read replay body: %e\n", err) + return + } + + if err = response.Body.Close(); err != nil { + log.Printf("Error while closing response body: %e\n", err) + return + } + + entity := &ResponseEntity{} + + if err = json.Unmarshal(b, entity); err != nil { + log.Printf("Can't unmarschal response body: %e\n", err) + return + } + + log.Printf("ResponseEntity: %v\n", entity) +} + +func (r *ResponseEntity) String() string { + var sb strings.Builder + + if r.Path != nil { + sb.WriteString("path = '") + sb.WriteString(*r.Path) + sb.WriteString("'") + } + + if r.Error != nil { + if sb.Len() > 0 { + sb.WriteString(", ") + } + sb.WriteString("error = '") + sb.WriteString(*r.Error) + sb.WriteString("'") + } + + if r.Timestamp != nil { + if sb.Len() > 0 { + sb.WriteString(", ") + } + sb.WriteString("timestamp = '") + sb.WriteString(r.Timestamp.String()) + sb.WriteString("'") + } + + if sb.Len() > 0 { + sb.WriteString(", ") + } + sb.WriteString("status = ") + sb.WriteString(fmt.Sprint(r.Status)) + + return sb.String() +}