commit
ceeedf6d58
@ -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"] |
@ -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
|
@ -0,0 +1,6 @@ |
||||
yablochkov-keyboard-reader (0.1.0) release; urgency=low |
||||
|
||||
[ Alexander Terekhin ] |
||||
* Initial version |
||||
|
||||
-- Alexander Terekhin <alex@bearns> Sun, 03 Dec 2023 17:01:00 +0600 |
@ -0,0 +1 @@ |
||||
11 |
@ -0,0 +1,11 @@ |
||||
Source: yablochkov-keyboard-reader |
||||
Section: misc |
||||
Priority: optional |
||||
Maintainer: Alexander Terekhin <alex@bearns.me> |
||||
|
||||
Package: yablochkov-keyboard-reader |
||||
Version: 0.1 |
||||
Architecture: amd64 |
||||
Installed-Size: 5008848 |
||||
Description: Yablochkov keyboard reader |
||||
|
@ -0,0 +1,2 @@ |
||||
chmod +x /opt/ocppc/bin/keyboard-reader |
||||
systemctl enable keyboard-reader --now |
@ -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 |
@ -0,0 +1,2 @@ |
||||
etc / |
||||
opt / |
@ -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 |
@ -0,0 +1,182 @@ |
||||
package evdev |
||||
|
||||
/* |
||||
#include <linux/input.h> |
||||
|
||||
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 |
||||
} |
@ -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 |
||||
) |
@ -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= |
@ -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 |
||||
} |
@ -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() |
||||
} |
Loading…
Reference in new issue