First attempt at cross-platform support, lacks features, notably printing
This commit is contained in:
parent
845954597a
commit
994fe06bc2
12
go.mod
12
go.mod
@ -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
260
main.go
@ -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!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user