First attempt at cross-platform support, lacks features, notably printing

This commit is contained in:
Ignacio Rivero 2025-06-24 21:05:10 -03:00
parent 845954597a
commit 994fe06bc2
2 changed files with 77 additions and 195 deletions

12
go.mod
View File

@ -6,14 +6,24 @@ require (
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333 github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333
github.com/makeworld-the-better-one/dither v1.0.0 github.com/makeworld-the-better-one/dither v1.0.0
tinygo.org/x/bluetooth v0.12.0
) )
require ( require (
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/mattn/go-colorable v0.1.6 // indirect github.com/mattn/go-colorable v0.1.6 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect github.com/mattn/go-isatty v0.0.12 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab // indirect github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab // indirect
github.com/pkg/errors v0.8.1 // indirect github.com/pkg/errors v0.8.1 // indirect
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af // indirect
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect
github.com/tinygo-org/cbgo v0.0.4 // indirect
github.com/tinygo-org/pio v0.2.0 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
golang.org/x/sys v0.0.0-20211204120058-94396e421777 // indirect golang.org/x/sys v0.11.0 // indirect
) )

260
main.go
View File

@ -11,7 +11,6 @@ You should have received a copy of the GNU General Public License along with Foo
package main package main
import ( import (
"context"
"flag" "flag"
"fmt" "fmt"
"image" "image"
@ -22,22 +21,29 @@ import (
"io" "io"
"log" "log"
"os" "os"
"os/signal"
"time" "time"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
ble "github.com/go-ble/ble" "github.com/go-ble/ble"
"github.com/go-ble/ble/linux"
dither "github.com/makeworld-the-better-one/dither" dither "github.com/makeworld-the-better-one/dither"
"tinygo.org/x/bluetooth"
) )
const minLines = 86 // firmware refuses to print anything shorter const minLines = 86 // firmware refuses to print anything shorter
func MustParseUUID(s string) bluetooth.UUID {
uuid, err := bluetooth.ParseUUID(s)
if err != nil {
panic("invalid UUID: " + err.Error())
}
return uuid
}
var ( var (
mainServiceUUID = ble.MustParse("ae30") mainServiceUUID = MustParseUUID("ae30")
printCharacteristic = ble.MustParse("ae01") printCharacteristic = MustParseUUID("ae01")
notifyCharacteristic = ble.MustParse("ae02") notifyCharacteristic = MustParseUUID("ae02")
dataCharacteristic = ble.MustParse("ae03") dataCharacteristic = MustParseUUID("ae03")
targetPrinterName = "MXW01" targetPrinterName = "MXW01"
scanTimeout = 10 * time.Second scanTimeout = 10 * time.Second
printCommandHeader = []byte{0x22, 0x21} printCommandHeader = []byte{0x22, 0x21}
@ -53,7 +59,7 @@ var (
ejectPaper uint ejectPaper uint
retractPaper uint retractPaper uint
outputPath string outputPath string
version = "dev" version = "dev"
) )
func init() { func init() {
@ -91,7 +97,7 @@ func init() {
flag.StringVar(&outputPath, "output", "", "Output PNG preview instead of printing (specify output path)") flag.StringVar(&outputPath, "output", "", "Output PNG preview instead of printing (specify output path)")
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Bleh! Cat Printer Utility for MXW01, version %s\n",version) fmt.Fprintf(os.Stderr, "Bleh! Cat Printer Utility for MXW01, version %s\n", version)
fmt.Fprintf(os.Stderr, "Usage: %s [options] <image_path or ->\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Usage: %s [options] <image_path or ->\n", os.Args[0])
fmt.Fprintln(os.Stderr, ` fmt.Fprintln(os.Stderr, `
Options: Options:
@ -440,72 +446,75 @@ func renderPreviewFrom4bpp(pixels []byte, width, height int) image.Image {
return img return img
} }
var adapter = bluetooth.DefaultAdapter
func initBLE() {
err := adapter.Enable()
if err != nil {
log.Fatalf("failed to enable BLE adapter: %v", err)
}
}
func main() { func main() {
flag.Parse() flag.Parse()
needNotifications := getStatus || getBattery || getVersion || getPrintType || getQueryCount || ejectPaper > 0 || retractPaper > 0 needNotifications := getStatus || getBattery || getVersion || getPrintType || getQueryCount || ejectPaper > 0 || retractPaper > 0
if needNotifications { if needNotifications {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) //ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop() //defer stop()
d, err := linux.NewDevice() initBLE()
if err != nil {
log.Fatalf("Failed to open BLE device: %v", err)
}
ble.SetDefaultDevice(d)
log.Println("Scanning for printer...") log.Println("Scanning for printer...")
log.Println("Connecting...") var printerAddr bluetooth.Address
var adv ble.Advertisement
ctxScan, cancel := context.WithTimeout(ctx, scanTimeout) err := adapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) {
err = ble.Scan(ctxScan, false, func(a ble.Advertisement) { fmt.Printf("Found device: %s, Name: %s\n", result.Address.String(), result.LocalName())
if a.LocalName() == targetPrinterName { if result.LocalName() == targetPrinterName {
adv = a printerAddr = result.Address
cancel() log.Printf("Found target printer: %s", targetPrinterName)
adapter.StopScan()
} }
}, nil) })
if err != nil && err != context.Canceled {
if err != nil {
log.Fatalf("Scan error: %v", err) log.Fatalf("Scan error: %v", err)
} }
if adv == nil {
log.Println("Printer not found.") if printerAddr == (bluetooth.Address{}) {
return log.Fatal("Printer not found.")
} }
client, err := ble.Dial(ctx, adv.Addr()) log.Println("Connecting...")
device, err := adapter.Connect(printerAddr, bluetooth.ConnectionParams{})
if err != nil { if err != nil {
log.Fatalf("Connect failed: %v", err) log.Fatalf("Connect failed: %v", err)
} }
defer client.CancelConnection()
mtu, err := client.ExchangeMTU(100) log.Println("Connected to printer:", device.Address)
if err != nil {
log.Printf("MTU negotiation failed: %v", err) var printChr, notifyChr *bluetooth.DeviceCharacteristic
} else { services, err := device.DiscoverServices([]bluetooth.UUID{mainServiceUUID})
log.Printf("Negotiated ATT MTU: %d", mtu)
}
var printChr, notifyChr *ble.Characteristic
services, err := client.DiscoverServices([]ble.UUID{mainServiceUUID})
if err != nil || len(services) == 0 { if err != nil || len(services) == 0 {
log.Fatalf("Service discovery failed: %v", err) log.Fatalf("Service discovery failed: %v", err)
} }
svc := services[0] svc := services[0]
chars, err := client.DiscoverCharacteristics(nil, svc) chars, err := svc.DiscoverCharacteristics(nil)
if err != nil { if err != nil {
log.Fatalf("Characteristic discovery failed: %v", err) log.Fatalf("Characteristic discovery failed: %v", err)
} }
for _, c := range chars { for i, c := range chars {
switch c.UUID.String() { switch c.UUID().String() {
case notifyCharacteristic.String(): case notifyCharacteristic.String():
notifyChr = c notifyChr = &chars[i]
case printCharacteristic.String(): case printCharacteristic.String():
printChr = c printChr = &chars[i]
} }
} }
if notifyChr != nil { if notifyChr != nil {
_, _ = client.DiscoverDescriptors(nil, notifyChr) err = notifyChr.EnableNotifications(func(b []byte) {
err = client.Subscribe(notifyChr, false, func(b []byte) {
parseNotification(b) parseNotification(b)
}) })
if err != nil { if err != nil {
@ -514,30 +523,33 @@ func main() {
log.Println("Subscribed to printer notifications.") log.Println("Subscribed to printer notifications.")
} }
} }
if printChr == nil {
log.Fatal("Print/command characteristic not found!")
}
if getStatus { if getStatus {
sendSimpleCommand(client, printChr, 0xA1) printChr.WriteWithoutResponse([]byte{0xA1})
} }
if getBattery { if getBattery {
sendSimpleCommand(client, printChr, 0xAB) printChr.WriteWithoutResponse([]byte{0xAB})
} }
if getVersion { if getVersion {
sendSimpleCommand(client, printChr, 0xB1) printChr.WriteWithoutResponse([]byte{0xB1})
} }
if getPrintType { if getPrintType {
sendSimpleCommand(client, printChr, 0xB0) printChr.WriteWithoutResponse([]byte{0xB0})
} }
if getQueryCount { if getQueryCount {
sendSimpleCommand(client, printChr, 0xA7) printChr.WriteWithoutResponse([]byte{0xA7})
} }
if ejectPaper > 0 { if ejectPaper > 0 {
sendLineCommand(client, printChr, 0xA3, ejectPaper) printChr.WriteWithoutResponse([]byte{0xA3, byte(ejectPaper)})
} }
if retractPaper > 0 { if retractPaper > 0 {
sendLineCommand(client, printChr, 0xA4, retractPaper) printChr.WriteWithoutResponse([]byte{0xA4, byte(retractPaper)})
} }
if getStatus || getBattery || getVersion || getPrintType || getQueryCount || ejectPaper > 0 || retractPaper > 0 { if getStatus || getBattery || getVersion || getPrintType || getQueryCount || ejectPaper > 0 || retractPaper > 0 {
log.Println("Waiting for notifications...") log.Println("Waiting for notifications...")
time.Sleep(2 * time.Second) time.Sleep(5 * time.Second)
} }
if flag.NArg() < 1 { if flag.NArg() < 1 {
return // no image to print, other commands may have run return // no image to print, other commands may have run
@ -546,146 +558,6 @@ func main() {
} }
} }
var printMode PrintMode
switch mode {
case "1bpp":
printMode = Mode1bpp
case "4bpp":
printMode = Mode4bpp
default:
fmt.Println("Invalid mode. Use '1bpp' or '4bpp'.")
return
}
imagePath := flag.Arg(0)
var pixels []byte
var height int
img, err := decodeImage(imagePath)
if err != nil {
log.Fatalf("Image load error: %v", err)
}
img = padImageToMinLines(img, minLines)
switch printMode {
case Mode1bpp:
pixels, height, err = loadImageMonoFromImage(img, ditherType)
case Mode4bpp:
pixels, height, err = loadImage4BitFromImage(img, ditherType)
}
if err != nil {
log.Fatalf("Image conversion error: %v", err)
}
if outputPath != "" {
var previewImg image.Image
switch printMode {
case Mode1bpp:
previewImg = renderPreviewFrom1bpp(pixels, linePixels, height)
case Mode4bpp:
previewImg = renderPreviewFrom4bpp(pixels, 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 = f
}
err = imaging.Encode(out, previewImg, imaging.PNG)
if err != nil {
log.Fatalf("Failed to write PNG preview: %v", err)
}
if outputPath != "-" {
fmt.Printf("Preview PNG written to %s\n", outputPath)
}
return
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
d, err := linux.NewDevice()
if err != nil {
log.Fatalf("Failed to open BLE device: %v", err)
}
ble.SetDefaultDevice(d)
log.Println("Scanning for printer...")
log.Println("Connecting...")
var adv ble.Advertisement
ctxScan, cancel := context.WithTimeout(ctx, scanTimeout)
err = ble.Scan(ctxScan, false, func(a ble.Advertisement) {
if a.LocalName() == targetPrinterName {
adv = a
cancel()
}
}, nil)
if err != nil && err != context.Canceled {
log.Fatalf("Scan error: %v", err)
}
if adv == nil {
log.Println("Printer not found.")
return
}
client, err := ble.Dial(ctx, adv.Addr())
if err != nil {
log.Fatalf("Connect failed: %v", err)
}
defer client.CancelConnection()
mtu, err := client.ExchangeMTU(100)
if err != nil {
log.Printf("MTU negotiation failed: %v", err)
} else {
log.Printf("Negotiated ATT MTU: %d", mtu)
}
var printChr, dataChr *ble.Characteristic
services, err := client.DiscoverServices([]ble.UUID{mainServiceUUID})
if err != nil || len(services) == 0 {
log.Fatalf("Service discovery failed: %v", err)
}
svc := services[0]
chars, err := client.DiscoverCharacteristics(nil, svc)
if err != nil {
log.Fatalf("Characteristic discovery failed: %v", err)
}
for _, c := range chars {
switch c.UUID.String() {
case dataCharacteristic.String():
dataChr = c
case printCharacteristic.String():
printChr = c
}
}
if printChr == nil {
log.Fatalf("Missing required print characteristic")
}
i := intensity
if i < 0 {
i = 0
}
if i > 100 {
i = 100
}
intensityByte := byte(i)
if dataChr == nil {
log.Fatalf("Missing required data characteristic")
}
err = sendImageBufferToPrinter(client, dataChr, printChr, pixels, height, printMode, intensityByte)
if err != nil {
log.Fatalf("Failed to print image: %v", err)
}
log.Println("Done!") log.Println("Done!")
} }