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