golang scanner imp
This commit is contained in:
@@ -1,175 +1,188 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"image/color"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tarm/serial"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/google/gousb"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port string `json:"port"`
|
||||
URL string `json:"url"`
|
||||
BaudRate int `json:"baud"`
|
||||
Delimiter string `json:"delimiter"`
|
||||
FlowControl string `json:"flow_control"`
|
||||
const (
|
||||
VendorID = 0xFFFF
|
||||
ProductID = 0x0035
|
||||
)
|
||||
|
||||
var hidMap = map[byte]string{
|
||||
4: "a", 5: "b", 6: "c", 7: "d", 8: "e", 9: "f", 10: "g", 11: "h", 12: "i",
|
||||
13: "j", 14: "k", 15: "l", 16: "m", 17: "n", 18: "o", 19: "p", 20: "q",
|
||||
21: "r", 22: "s", 23: "t", 24: "u", 25: "v", 26: "w", 27: "x", 28: "y", 29: "z",
|
||||
30: "1", 31: "2", 32: "3", 33: "4", 34: "5", 35: "6", 36: "7", 37: "8", 38: "9", 39: "0",
|
||||
44: " ", 45: "-", 46: "=", 55: ".", 56: "/",
|
||||
}
|
||||
|
||||
var defaultConfig = Config{
|
||||
Port: "/dev/ttyACM0",
|
||||
URL: "https://scanner.sekidesu.xyz/scan",
|
||||
BaudRate: 115200,
|
||||
Delimiter: "\n",
|
||||
FlowControl: "none",
|
||||
type BridgeApp struct {
|
||||
urlEntry *widget.Entry
|
||||
status *canvas.Text
|
||||
logList *widget.List
|
||||
logs []string
|
||||
window fyne.Window
|
||||
}
|
||||
|
||||
const configPath = "config.json"
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
a := app.New()
|
||||
w := a.NewWindow("POS Hardware Bridge (Go)")
|
||||
|
||||
portName := flag.String("port", cfg.Port, "Serial port name")
|
||||
endpoint := flag.String("url", cfg.URL, "Target URL endpoint")
|
||||
baudRate := flag.Int("baud", cfg.BaudRate, "Baud rate")
|
||||
delim := flag.String("delim", cfg.Delimiter, "Line delimiter")
|
||||
flow := flag.String("flow", cfg.FlowControl, "Flow control: none, hardware, software")
|
||||
save := flag.Bool("save", false, "Save current parameters to config.json")
|
||||
flag.Parse()
|
||||
|
||||
cfg.Port = *portName
|
||||
cfg.URL = *endpoint
|
||||
cfg.BaudRate = *baudRate
|
||||
cfg.Delimiter = *delim
|
||||
cfg.FlowControl = *flow
|
||||
|
||||
if *save {
|
||||
saveConfig(cfg)
|
||||
fmt.Println("Settings saved to", configPath)
|
||||
bridge := &BridgeApp{
|
||||
urlEntry: widget.NewEntry(),
|
||||
status: canvas.NewText("Status: Booting...", color.Black),
|
||||
window: w,
|
||||
}
|
||||
|
||||
serialConfig := &serial.Config{
|
||||
Name: cfg.Port,
|
||||
Baud: cfg.BaudRate,
|
||||
ReadTimeout: time.Millisecond * 500,
|
||||
}
|
||||
bridge.status.TextSize = 14
|
||||
bridge.urlEntry.SetText("https://scanner.sekidesu.xyz/scan")
|
||||
|
||||
// tarm/serial uses boolean flags for flow control if available in the version used
|
||||
// If your version doesn't support these fields, you may need to update the package
|
||||
// or manage the lines manually via the file descriptor.
|
||||
/* Note: tarm/serial usually requires specific fork or version
|
||||
for full RTS/CTS hardware flow control support.
|
||||
*/
|
||||
// UI Layout
|
||||
bridge.logList = widget.NewList(
|
||||
func() int { return len(bridge.logs) },
|
||||
func() fyne.CanvasObject { return widget.NewLabel("template") },
|
||||
func(i widget.ListItemID, o fyne.CanvasObject) {
|
||||
o.(*widget.Label).SetText(bridge.logs[len(bridge.logs)-1-i])
|
||||
},
|
||||
)
|
||||
|
||||
port, err := serial.OpenPort(serialConfig)
|
||||
if err != nil {
|
||||
fmt.Printf("Error opening port %s: %v\n", cfg.Port, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer port.Close()
|
||||
content := container.NewBorder(
|
||||
container.NewVBox(
|
||||
widget.NewLabel("Target POS Endpoint:"),
|
||||
bridge.urlEntry,
|
||||
bridge.status,
|
||||
widget.NewLabel("Activity Log:"),
|
||||
),
|
||||
nil, nil, nil,
|
||||
bridge.logList,
|
||||
)
|
||||
|
||||
fmt.Printf("Listening on %s (Baud: %d, Flow: %s)...\n", cfg.Port, cfg.BaudRate, cfg.FlowControl)
|
||||
w.SetContent(content)
|
||||
w.Resize(fyne.NewSize(500, 400))
|
||||
|
||||
scanner := bufio.NewScanner(port)
|
||||
go bridge.usbListenLoop()
|
||||
|
||||
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
w.ShowAndRun()
|
||||
}
|
||||
|
||||
func (b *BridgeApp) addLog(msg string) {
|
||||
fyne.DoAndWait(func() {
|
||||
ts := time.Now().Format("15:04:05")
|
||||
b.logs = append([]string{fmt.Sprintf("[%s] %s", ts, msg)}, b.logs...)
|
||||
if len(b.logs) > 15 {
|
||||
b.logs = b.logs[:15]
|
||||
}
|
||||
if i := bytes.Index(data, []byte(cfg.Delimiter)); i >= 0 {
|
||||
return i + len(cfg.Delimiter), data[0:i], nil
|
||||
}
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
return 0, nil, nil
|
||||
b.logList.Refresh()
|
||||
})
|
||||
|
||||
for scanner.Scan() {
|
||||
rawContent := scanner.Text()
|
||||
content := strings.TrimFunc(rawContent, func(r rune) bool {
|
||||
return r < 32 || r > 126
|
||||
})
|
||||
|
||||
if content != "" {
|
||||
sendToEndpoint(cfg.URL, content)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Printf("Scanner error: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig() Config {
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
saveConfig(defaultConfig)
|
||||
return defaultConfig
|
||||
}
|
||||
|
||||
file, err := os.Open(configPath)
|
||||
func (b *BridgeApp) sendToPos(barcode string) {
|
||||
b.addLog(fmt.Sprintf("Captured: %s. Sending...", barcode))
|
||||
client := http.Client{Timeout: 3 * time.Second}
|
||||
resp, err := client.Get(b.urlEntry.Text + "?content=" + barcode)
|
||||
if err != nil {
|
||||
return defaultConfig
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var cfg Config
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(&cfg); err != nil {
|
||||
return defaultConfig
|
||||
}
|
||||
if cfg.Delimiter == "" {
|
||||
cfg.Delimiter = "\n"
|
||||
}
|
||||
if cfg.FlowControl == "" {
|
||||
cfg.FlowControl = "none"
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func saveConfig(cfg Config) {
|
||||
file, err := os.Create(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create/save config: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.Encode(cfg)
|
||||
}
|
||||
|
||||
func sendToEndpoint(baseURL, content string) {
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
fullURL := fmt.Sprintf("%s?content=%s", baseURL, url.QueryEscape(content))
|
||||
resp, err := client.Get(fullURL)
|
||||
if err != nil {
|
||||
fmt.Printf("Network Error: %v\n", err)
|
||||
b.addLog("HTTP Error: Backend unreachable")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading response: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Data: [%s] | Status: %s\n", content, resp.Status)
|
||||
if len(body) > 0 {
|
||||
fmt.Printf("Response: %s\n", string(body))
|
||||
}
|
||||
fmt.Println(strings.Repeat("-", 30))
|
||||
b.addLog(fmt.Sprintf("Success: POS returned %d", resp.StatusCode))
|
||||
}
|
||||
|
||||
func (b *BridgeApp) usbListenLoop() {
|
||||
ctx := gousb.NewContext()
|
||||
defer ctx.Close()
|
||||
|
||||
for {
|
||||
dev, err := ctx.OpenDeviceWithVIDPID(gousb.ID(VendorID), gousb.ID(ProductID))
|
||||
|
||||
if err != nil || dev == nil {
|
||||
fyne.DoAndWait(func() {
|
||||
b.status.Text = "Status: Scanner unplugged. Waiting..."
|
||||
b.status.Color = color.NRGBA{R: 200, G: 0, B: 0, A: 255}
|
||||
b.status.Refresh()
|
||||
})
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
fyne.DoAndWait(func() {
|
||||
b.status.Text = "Status: Scanner Locked & Ready"
|
||||
b.status.Color = color.NRGBA{R: 0, G: 180, B: 0, A: 255}
|
||||
b.status.Refresh()
|
||||
})
|
||||
|
||||
intf, done, err := dev.DefaultInterface()
|
||||
if err != nil {
|
||||
b.addLog("Error claiming interface")
|
||||
dev.Close()
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
var inEp *gousb.InEndpoint
|
||||
for _, epDesc := range intf.Setting.Endpoints {
|
||||
if epDesc.Direction == gousb.EndpointDirectionIn {
|
||||
inEp, _ = intf.InEndpoint(epDesc.Number)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if inEp == nil {
|
||||
b.addLog("No IN endpoint found")
|
||||
done()
|
||||
dev.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
currentBarcode := ""
|
||||
buf := make([]byte, inEp.Desc.MaxPacketSize)
|
||||
|
||||
for {
|
||||
n, err := inEp.Read(buf)
|
||||
if err != nil {
|
||||
// This usually happens when the device is unplugged
|
||||
break
|
||||
}
|
||||
if n < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
modifier := buf[0]
|
||||
keycode := buf[2]
|
||||
isShift := (modifier == 2 || modifier == 32)
|
||||
|
||||
if keycode == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if keycode == 40 { // Enter
|
||||
if currentBarcode != "" {
|
||||
// Capture the barcode to avoid race conditions
|
||||
code := currentBarcode
|
||||
go b.sendToPos(code)
|
||||
currentBarcode = ""
|
||||
}
|
||||
} else if val, ok := hidMap[keycode]; ok {
|
||||
if isShift && len(val) == 1 && val[0] >= 'a' && val[0] <= 'z' {
|
||||
val = string(val[0] - 32)
|
||||
}
|
||||
currentBarcode += val
|
||||
}
|
||||
}
|
||||
|
||||
done()
|
||||
dev.Close()
|
||||
b.addLog("Hardware connection lost. Reconnecting...")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user