New daemon architecture
This commit is contained in:
346
cmd/bleh/main.go
Normal file
346
cmd/bleh/main.go
Normal file
@@ -0,0 +1,346 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"bleh/internal/imageproc"
|
||||
"bleh/internal/ipc"
|
||||
"bleh/internal/mxw01"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
const defaultSock = "/run/bleh/blehd.sock"
|
||||
|
||||
var (
|
||||
intensity int
|
||||
mode string
|
||||
ditherType string
|
||||
getStatus bool
|
||||
getBattery bool
|
||||
getVersion bool
|
||||
getPrintType bool
|
||||
getQueryCount bool
|
||||
ejectPaper uint
|
||||
retractPaper uint
|
||||
outputPath string
|
||||
address string
|
||||
socketPath string
|
||||
jsonOutput bool
|
||||
version = "dev"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.IntVar(&intensity, "intensity", 80, "Print intensity (0-100)")
|
||||
flag.IntVar(&intensity, "i", 80, "Print intensity (0-100)")
|
||||
|
||||
flag.StringVar(&mode, "mode", "1bpp", "Print mode: 1bpp or 4bpp")
|
||||
flag.StringVar(&mode, "m", "1bpp", "Print mode: 1bpp or 4bpp")
|
||||
|
||||
flag.StringVar(&ditherType, "dither", "none", "Dither method: none, floyd, bayer2x2, bayer4x4, bayer8x8, bayer16x16, atkinson, jjn")
|
||||
flag.StringVar(&ditherType, "d", "none", "Dither method: none, floyd, bayer2x2, bayer4x4, bayer8x8, bayer16x16, atkinson, jjn")
|
||||
|
||||
flag.BoolVar(&getStatus, "status", false, "Query printer status")
|
||||
flag.BoolVar(&getStatus, "s", false, "Query printer status")
|
||||
|
||||
flag.BoolVar(&getBattery, "battery", false, "Query battery level")
|
||||
flag.BoolVar(&getBattery, "b", false, "Query battery level")
|
||||
|
||||
flag.BoolVar(&getVersion, "version", false, "Query printer version")
|
||||
flag.BoolVar(&getVersion, "v", false, "Query printer version")
|
||||
|
||||
flag.BoolVar(&getPrintType, "printtype", false, "Query print type")
|
||||
flag.BoolVar(&getPrintType, "p", false, "Query print type")
|
||||
|
||||
flag.BoolVar(&getQueryCount, "querycount", false, "Query internal counter")
|
||||
flag.BoolVar(&getQueryCount, "q", false, "Query internal counter")
|
||||
|
||||
flag.UintVar(&ejectPaper, "eject", 0, "Eject paper by N lines")
|
||||
flag.UintVar(&ejectPaper, "E", 0, "Eject paper by N lines")
|
||||
|
||||
flag.UintVar(&retractPaper, "retract", 0, "Retract paper by N lines")
|
||||
flag.UintVar(&retractPaper, "R", 0, "Retract paper by N lines")
|
||||
|
||||
flag.StringVar(&outputPath, "o", "", "Output PNG preview instead of printing (specify output path)")
|
||||
flag.StringVar(&outputPath, "output", "", "Output PNG preview instead of printing (specify output path)")
|
||||
|
||||
flag.StringVar(&address, "a", "", "Connect to printer by MAC address")
|
||||
flag.StringVar(&address, "address", "", "Connect to printer by MAC address")
|
||||
|
||||
flag.StringVar(&socketPath, "socket", "", "blehd Unix socket path (default: auto-detect)")
|
||||
flag.BoolVar(&jsonOutput, "json", false, "Output machine-readable JSON")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if outputPath != "-" {
|
||||
log.Println("Bleh! Cat Printer Utility for MXW01, version", version)
|
||||
}
|
||||
|
||||
needNotifications := getStatus || getBattery || getVersion || getPrintType || getQueryCount || ejectPaper > 0 || retractPaper > 0
|
||||
needPrinter := needNotifications || (flag.NArg() > 0 && outputPath == "")
|
||||
|
||||
if !needPrinter && outputPath == "" {
|
||||
log.Println("Nothing to do. Use -h for help.")
|
||||
log.Println("Done!")
|
||||
return
|
||||
}
|
||||
|
||||
// Print mode
|
||||
var printMode mxw01.PrintMode
|
||||
switch mode {
|
||||
case "1bpp":
|
||||
printMode = mxw01.Mode1bpp
|
||||
case "4bpp":
|
||||
printMode = mxw01.Mode4bpp
|
||||
default:
|
||||
log.Fatalf("Invalid mode. Use '1bpp' or '4bpp'.")
|
||||
}
|
||||
|
||||
imagePath := flag.Arg(0)
|
||||
var pixels []byte
|
||||
var height int
|
||||
var err error
|
||||
|
||||
if imagePath != "" {
|
||||
img, err := imageproc.DecodeImage(imagePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load image: %v", err)
|
||||
}
|
||||
img = imageproc.PadImageToMinLines(img, mxw01.MinLines)
|
||||
|
||||
switch printMode {
|
||||
case mxw01.Mode1bpp:
|
||||
pixels, height, err = imageproc.LoadImageMonoFromImage(img, ditherType)
|
||||
case mxw01.Mode4bpp:
|
||||
pixels, height, err = imageproc.LoadImage4BitFromImage(img, ditherType)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Image conversion error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if outputPath != "" {
|
||||
writePreview(printMode, pixels, height)
|
||||
return
|
||||
}
|
||||
|
||||
c, usedSock, err := dialAuto(socketPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to blehd: %v\nStart it with: ./blehd --socket %s", err, defaultSock)
|
||||
}
|
||||
_ = usedSock
|
||||
defer c.Close()
|
||||
|
||||
id := 1
|
||||
call := func(method string, params map[string]any) *ipc.Response {
|
||||
req := ipc.Request{ID: fmt.Sprintf("%d", id), Method: method, Params: params}
|
||||
id++
|
||||
if err := ipc.EncodeLine(c, req); err != nil {
|
||||
log.Fatalf("send failed: %v", err)
|
||||
}
|
||||
resp, err := readResp(c)
|
||||
if err != nil {
|
||||
log.Fatalf("read failed: %v", err)
|
||||
}
|
||||
if !resp.OK {
|
||||
log.Fatalf("%s failed: %s", method, resp.Error.Message)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
params := map[string]any{}
|
||||
if address != "" {
|
||||
params["address"] = address
|
||||
}
|
||||
|
||||
if needNotifications {
|
||||
if getStatus {
|
||||
resp := call("printer.status.get", params)
|
||||
printResult("printer.status.get", resp.Result)
|
||||
}
|
||||
if getBattery {
|
||||
resp := call("printer.battery.get", params)
|
||||
printResult("printer.battery.get", resp.Result)
|
||||
}
|
||||
if getVersion {
|
||||
resp := call("printer.version.get", params)
|
||||
printResult("printer.version.get", resp.Result)
|
||||
}
|
||||
if getPrintType {
|
||||
resp := call("printer.printtype.get", params)
|
||||
printResult("printer.printtype.get", resp.Result)
|
||||
}
|
||||
if getQueryCount {
|
||||
resp := call("printer.querycount.get", params)
|
||||
printResult("printer.querycount.get", resp.Result)
|
||||
}
|
||||
if ejectPaper > 0 {
|
||||
p := cloneMap(params)
|
||||
p["lines"] = int(ejectPaper)
|
||||
resp := call("paper.eject", p)
|
||||
printResult("paper.eject", resp.Result)
|
||||
}
|
||||
if retractPaper > 0 {
|
||||
p := cloneMap(params)
|
||||
p["lines"] = int(retractPaper)
|
||||
resp := call("paper.retract", p)
|
||||
printResult("paper.retract", resp.Result)
|
||||
}
|
||||
}
|
||||
|
||||
if flag.NArg() > 0 {
|
||||
i := intensity
|
||||
if i < 0 {
|
||||
i = 0
|
||||
}
|
||||
if i > 100 {
|
||||
i = 100
|
||||
}
|
||||
p := cloneMap(params)
|
||||
p["mode"] = mode
|
||||
p["intensity"] = i
|
||||
p["height"] = height
|
||||
p["pixels_b64"] = base64.StdEncoding.EncodeToString(pixels)
|
||||
resp := call("print.start", p)
|
||||
printResult("print.start", resp.Result)
|
||||
}
|
||||
|
||||
log.Println("Done!")
|
||||
}
|
||||
|
||||
func writePreview(printMode mxw01.PrintMode, pixels []byte, height int) {
|
||||
var previewImg image.Image
|
||||
switch printMode {
|
||||
case mxw01.Mode1bpp:
|
||||
previewImg = imageproc.RenderPreviewFrom1bpp(pixels, mxw01.LinePixels, height)
|
||||
case mxw01.Mode4bpp:
|
||||
previewImg = imageproc.RenderPreviewFrom4bpp(pixels, mxw01.LinePixels, height)
|
||||
}
|
||||
|
||||
var out io.Writer
|
||||
if outputPath == "-" {
|
||||
out = os.Stdout
|
||||
} else {
|
||||
f, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create output file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
out = bufio.NewWriter(f)
|
||||
defer func() { _ = out.(*bufio.Writer).Flush() }()
|
||||
}
|
||||
|
||||
if err := imaging.Encode(out, previewImg, imaging.PNG); err != nil {
|
||||
log.Fatalf("Failed to write PNG preview: %v", err)
|
||||
}
|
||||
if outputPath != "-" {
|
||||
log.Printf("Preview PNG written to %s\n", outputPath)
|
||||
}
|
||||
}
|
||||
|
||||
func dial(sock string) (net.Conn, error) {
|
||||
return net.DialTimeout("unix", sock, 2*time.Second)
|
||||
}
|
||||
|
||||
func dialAuto(sock string) (net.Conn, string, error) {
|
||||
if sock != "" {
|
||||
c, err := dial(sock)
|
||||
return c, sock, err
|
||||
}
|
||||
|
||||
candidates := []string{}
|
||||
if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
|
||||
candidates = append(candidates, xdg+"/bleh/blehd.sock")
|
||||
}
|
||||
candidates = append(candidates, defaultSock)
|
||||
|
||||
var lastErr error
|
||||
for _, p := range candidates {
|
||||
c, err := dial(p)
|
||||
if err == nil {
|
||||
return c, p, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("no socket candidates")
|
||||
}
|
||||
return nil, "", lastErr
|
||||
}
|
||||
|
||||
func readResp(c net.Conn) (*ipc.Response, error) {
|
||||
r := bufio.NewReader(c)
|
||||
line, err := ipc.ReadLine(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp ipc.Response
|
||||
if err := jsonUnmarshal(line, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func cloneMap(m map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(m)+1)
|
||||
for k, v := range m {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func jsonUnmarshal(b []byte, v any) error {
|
||||
// Small helper to avoid importing encoding/json at top-level with name conflicts.
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
func jsonPretty(v any) ([]byte, error) {
|
||||
return json.MarshalIndent(v, "", " ")
|
||||
}
|
||||
|
||||
func printResult(method string, result any) {
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
if jsonOutput {
|
||||
b, _ := jsonPretty(result)
|
||||
fmt.Println(string(b))
|
||||
return
|
||||
}
|
||||
|
||||
m, _ := result.(map[string]any)
|
||||
switch method {
|
||||
case "printer.status.get":
|
||||
fmt.Printf("Status: %v (%s), Battery: %v, Temp: %v\n",
|
||||
m["ok"], m["state"], m["battery"], m["temp"])
|
||||
case "printer.battery.get":
|
||||
fmt.Printf("Battery level: %v\n", m["battery"])
|
||||
case "printer.version.get":
|
||||
fmt.Printf("Version: %v, Print type: %v\n", m["version"], m["printType"])
|
||||
case "printer.printtype.get":
|
||||
fmt.Printf("Print type: %v\n", m["printType"])
|
||||
case "printer.querycount.get":
|
||||
fmt.Printf("Query count: %v\n", m["queryCount"])
|
||||
case "paper.eject":
|
||||
fmt.Printf("Ejecting paper (%v lines)...\n", m["eject"])
|
||||
case "paper.retract":
|
||||
fmt.Printf("Retracting paper (%v lines)...\n", m["retract"])
|
||||
case "print.start":
|
||||
fmt.Println("Printed.")
|
||||
default:
|
||||
b, _ := jsonPretty(result)
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
}
|
||||
424
cmd/blehd/main.go
Normal file
424
cmd/blehd/main.go
Normal file
@@ -0,0 +1,424 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"bleh/internal/ipc"
|
||||
"bleh/internal/mxw01"
|
||||
|
||||
ble "github.com/go-ble/ble"
|
||||
"github.com/go-ble/ble/linux"
|
||||
"os/user"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSock = "/run/bleh/blehd.sock"
|
||||
)
|
||||
|
||||
func autoSocketPath(explicit string) (string, error) {
|
||||
if explicit != "" {
|
||||
return explicit, nil
|
||||
}
|
||||
if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
|
||||
return filepath.Join(xdg, "bleh", "blehd.sock"), nil
|
||||
}
|
||||
return defaultSock, nil
|
||||
}
|
||||
|
||||
var printerMu sync.Mutex
|
||||
|
||||
func main() {
|
||||
var socketPath string
|
||||
var groupName string
|
||||
flag.StringVar(&socketPath, "socket", "", "Unix socket path (default: auto-detect)")
|
||||
flag.StringVar(&groupName, "group", "bleh", "Group name to own the socket")
|
||||
flag.Parse()
|
||||
|
||||
resolvedSock, err := autoSocketPath(socketPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to resolve socket path: %v", err)
|
||||
}
|
||||
socketPath = resolvedSock
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
// Init BLE device once.
|
||||
d, err := linux.NewDevice()
|
||||
if err != nil {
|
||||
log.Printf("Failed to open BLE device: %v", err)
|
||||
log.Printf("Hint: blehd needs CAP_NET_RAW and CAP_NET_ADMIN. Example:")
|
||||
log.Printf(" sudo setcap cap_net_raw,cap_net_admin=eip $(which blehd)")
|
||||
log.Fatalf("Exiting.")
|
||||
}
|
||||
ble.SetDefaultDevice(d)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(socketPath), 0o755); err != nil {
|
||||
log.Fatalf("Failed to create socket dir: %v", err)
|
||||
}
|
||||
// Remove stale socket.
|
||||
_ = os.Remove(socketPath)
|
||||
|
||||
ln, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Listen failed: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
// Set group ownership + perms.
|
||||
if err := setSocketPerms(socketPath, groupName); err != nil {
|
||||
log.Printf("Warning: failed to set socket permissions: %v", err)
|
||||
log.Printf("Hint: if running unprivileged, prefer an XDG runtime socket (set $XDG_RUNTIME_DIR)")
|
||||
}
|
||||
|
||||
log.Printf("blehd listening on %s", socketPath)
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = ln.Close()
|
||||
_ = os.Remove(socketPath)
|
||||
}()
|
||||
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) || ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
log.Printf("accept error: %v", err)
|
||||
continue
|
||||
}
|
||||
go handleConn(ctx, c)
|
||||
}
|
||||
}
|
||||
|
||||
func setSocketPerms(path, group string) error {
|
||||
grp, err := user.LookupGroup(group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gid, err := strconv.Atoi(grp.Gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Keep uid unchanged, set gid.
|
||||
if err := os.Chown(path, -1, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(path, 0o660)
|
||||
}
|
||||
|
||||
func handleConn(ctx context.Context, c net.Conn) {
|
||||
defer c.Close()
|
||||
r := bufio.NewReader(c)
|
||||
w := bufio.NewWriter(c)
|
||||
defer w.Flush()
|
||||
|
||||
for {
|
||||
line, err := ipc.ReadLine(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req, err := ipc.DecodeRequestLine(line)
|
||||
if err != nil {
|
||||
_ = ipc.EncodeLine(w, ipc.Response{OK: false, Error: &ipc.Error{Code: "EBADJSON", Message: err.Error()}})
|
||||
_ = w.Flush()
|
||||
continue
|
||||
}
|
||||
|
||||
resp := dispatch(ctx, req)
|
||||
_ = ipc.EncodeLine(w, resp)
|
||||
_ = w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func dispatch(ctx context.Context, req *ipc.Request) ipc.Response {
|
||||
if req.ID == "" {
|
||||
req.ID = "-"
|
||||
}
|
||||
method := req.Method
|
||||
|
||||
// Serialize access to the printer to avoid concurrent BLE sessions stomping each other.
|
||||
// (This can be relaxed later if the printer/stack supports it reliably.)
|
||||
printerMu.Lock()
|
||||
defer printerMu.Unlock()
|
||||
|
||||
switch method {
|
||||
case "printer.status.get":
|
||||
st, err := doQuery(ctx, req.Params, 0xA1)
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
s, err := mxw01.DecodeStatusNotification(st)
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
return ok(req.ID, s)
|
||||
|
||||
case "printer.battery.get":
|
||||
b, err := doQuery(ctx, req.Params, 0xAB)
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
pct, err := mxw01.DecodeBatteryNotification(b)
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
return ok(req.ID, map[string]any{"battery": pct})
|
||||
|
||||
case "printer.version.get":
|
||||
b, err := doQuery(ctx, req.Params, 0xB1)
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
v, err := mxw01.DecodeVersionNotification(b)
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
return ok(req.ID, v)
|
||||
|
||||
case "printer.printtype.get":
|
||||
b, err := doQuery(ctx, req.Params, 0xB0)
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
t, err := mxw01.DecodePrintTypeNotification(b)
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
return ok(req.ID, map[string]any{"printType": t})
|
||||
|
||||
case "printer.querycount.get":
|
||||
b, err := doQuery(ctx, req.Params, 0xA7)
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
qc, err := mxw01.DecodeQueryCountNotification(b)
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
return ok(req.ID, map[string]any{"queryCount": fmt.Sprintf("% X", qc)})
|
||||
|
||||
case "paper.eject":
|
||||
lines, _ := getInt(req.Params, "lines")
|
||||
err := doLineCommand(ctx, req.Params, 0xA3, uint(lines))
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
return ok(req.ID, map[string]any{"eject": lines})
|
||||
|
||||
case "paper.retract":
|
||||
lines, _ := getInt(req.Params, "lines")
|
||||
err := doLineCommand(ctx, req.Params, 0xA4, uint(lines))
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
return ok(req.ID, map[string]any{"retract": lines})
|
||||
|
||||
case "print.start":
|
||||
err := doPrint(ctx, req.Params)
|
||||
if err != nil {
|
||||
return fail(req.ID, err)
|
||||
}
|
||||
return ok(req.ID, map[string]any{"printed": true})
|
||||
|
||||
default:
|
||||
return ipc.Response{ID: req.ID, OK: false, Error: &ipc.Error{Code: "ENOIMPL", Message: "unknown method"}}
|
||||
}
|
||||
}
|
||||
|
||||
func ok(id string, result any) ipc.Response {
|
||||
return ipc.Response{ID: id, OK: true, Result: result}
|
||||
}
|
||||
|
||||
func fail(id string, err error) ipc.Response {
|
||||
return ipc.Response{ID: id, OK: false, Error: &ipc.Error{Code: "EFAIL", Message: err.Error()}}
|
||||
}
|
||||
|
||||
func getString(m map[string]any, key string) (string, bool) {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
s, ok := v.(string)
|
||||
return s, ok
|
||||
}
|
||||
|
||||
func getInt(m map[string]any, key string) (int, bool) {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
return int(t), true
|
||||
case int:
|
||||
return t, true
|
||||
case json.Number:
|
||||
i, err := t.Int64()
|
||||
return int(i), err == nil
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func doQuery(ctx context.Context, params map[string]any, cmd byte) ([]byte, error) {
|
||||
addr, _ := getString(params, "address")
|
||||
|
||||
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
adv, err := mxw01.FindPrinter(cctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := ble.Dial(cctx, adv.Addr())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.CancelConnection()
|
||||
|
||||
mtu, err := client.ExchangeMTU(100)
|
||||
if err == nil {
|
||||
_ = mtu
|
||||
}
|
||||
|
||||
printChr, notifyChr, _, err := mxw01.DiscoverChars(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if notifyChr == nil {
|
||||
return nil, fmt.Errorf("missing notification characteristic")
|
||||
}
|
||||
|
||||
notifs := make(chan []byte, 8)
|
||||
_, _ = client.DiscoverDescriptors(nil, notifyChr)
|
||||
err = client.Subscribe(notifyChr, false, func(b []byte) {
|
||||
select {
|
||||
case notifs <- append([]byte{}, b...):
|
||||
default:
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := mxw01.SendSimpleCommand(client, printChr, cmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deadline := time.NewTimer(2 * time.Second)
|
||||
defer deadline.Stop()
|
||||
for {
|
||||
select {
|
||||
case b := <-notifs:
|
||||
c, _, perr := mxw01.ParseNotification(b)
|
||||
if perr == nil && c == cmd {
|
||||
return b, nil
|
||||
}
|
||||
case <-deadline.C:
|
||||
return nil, fmt.Errorf("timeout waiting for notification 0x%02X", cmd)
|
||||
case <-cctx.Done():
|
||||
return nil, cctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doLineCommand(ctx context.Context, params map[string]any, cmd byte, lines uint) error {
|
||||
addr, _ := getString(params, "address")
|
||||
|
||||
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
adv, err := mxw01.FindPrinter(cctx, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := ble.Dial(cctx, adv.Addr())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.CancelConnection()
|
||||
|
||||
printChr, _, _, err := mxw01.DiscoverChars(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mxw01.SendLineCommand(client, printChr, cmd, lines)
|
||||
}
|
||||
|
||||
func doPrint(ctx context.Context, params map[string]any) error {
|
||||
addr, _ := getString(params, "address")
|
||||
modeStr, _ := getString(params, "mode")
|
||||
intensity, _ := getInt(params, "intensity")
|
||||
height, _ := getInt(params, "height")
|
||||
pixelsB64, _ := getString(params, "pixels_b64")
|
||||
|
||||
if height <= 0 {
|
||||
return fmt.Errorf("invalid height")
|
||||
}
|
||||
pixels, err := base64.StdEncoding.DecodeString(pixelsB64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode pixels: %v", err)
|
||||
}
|
||||
|
||||
pm := mxw01.Mode1bpp
|
||||
switch modeStr {
|
||||
case "1bpp":
|
||||
pm = mxw01.Mode1bpp
|
||||
case "4bpp":
|
||||
pm = mxw01.Mode4bpp
|
||||
default:
|
||||
return fmt.Errorf("invalid mode")
|
||||
}
|
||||
if intensity < 0 {
|
||||
intensity = 0
|
||||
}
|
||||
if intensity > 100 {
|
||||
intensity = 100
|
||||
}
|
||||
|
||||
cctx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
adv, err := mxw01.FindPrinter(cctx, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := ble.Dial(cctx, adv.Addr())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.CancelConnection()
|
||||
|
||||
mtu, err := client.ExchangeMTU(100)
|
||||
if err != nil {
|
||||
mtu = 23
|
||||
}
|
||||
|
||||
printChr, _, dataChr, err := mxw01.DiscoverChars(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if printChr == nil || dataChr == nil {
|
||||
return fmt.Errorf("missing required characteristics")
|
||||
}
|
||||
|
||||
return mxw01.SendImageBufferToPrinter(client, dataChr, printChr, pixels, height, pm, byte(intensity), mtu)
|
||||
}
|
||||
Reference in New Issue
Block a user