Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

3 changed files with 187 additions and 228 deletions

View File

@ -1,7 +1,7 @@
# Bleh! # Bleh!
<p align=center> <p align=center>
<img width=320 src="./demo.jpg"/> <img src="./bleh.jpg"/>
</p> </p>
**Bleh!** is a command-line utility to print images on the MXW01 Bluetooth thermal printer. **Bleh!** is a command-line utility to print images on the MXW01 Bluetooth thermal printer.
@ -30,12 +30,6 @@ Then:
go build go build
``` ```
If you want to be able to run it as a regular user, you'll need `setcap`:
```
sudo setcap cap_net_raw,cap_net_admin=eip ./bleh
```
## Usage ## Usage
```sh ```sh

BIN
demo.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

407
main.go
View File

@ -53,8 +53,7 @@ var (
ejectPaper uint ejectPaper uint
retractPaper uint retractPaper uint
outputPath string outputPath string
address string version = "dev"
version = "dev"
) )
func init() { func init() {
@ -91,16 +90,11 @@ func init() {
flag.StringVar(&outputPath, "o", "", "Output PNG preview instead of printing (specify output path)") 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(&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.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:
-h, --help Show this help message
-a, --address <mac> Connect to printer by MAC address
-i, --intensity int Print intensity (0-100) (default 80) -i, --intensity int Print intensity (0-100) (default 80)
-m, --mode string Print mode: 1bpp or 4bpp (default "1bpp") -m, --mode string Print mode: 1bpp or 4bpp (default "1bpp")
-d, --dither string Dither method: none, floyd, bayer2x2, bayer4x4, bayer8x8, bayer16x16, atkinson, jjn (default "none") -d, --dither string Dither method: none, floyd, bayer2x2, bayer4x4, bayer8x8, bayer16x16, atkinson, jjn (default "none")
@ -446,163 +440,112 @@ func renderPreviewFrom4bpp(pixels []byte, width, height int) image.Image {
return img return img
} }
func findPrinter(ctx context.Context) (ble.Advertisement, error) {
var addr ble.Addr
var adv ble.Advertisement
if address != "" {
log.Printf("Connecting directly to MAC address: %s", address)
addr = ble.NewAddr(address)
fmt.Printf("Using address: %s\n", addr)
}
ctxScan, cancel := context.WithTimeout(ctx, scanTimeout)
log.Println("Scanning for printer...")
err := ble.Scan(ctxScan, false, func(a ble.Advertisement) {
if address != "" {
if a.Addr().String() == addr.String() { // Wonder why this works and not direct comparison
adv = a
cancel()
}
} else if a.LocalName() == targetPrinterName {
adv = a
cancel()
}
}, nil)
if err != nil && err != context.Canceled {
return nil, fmt.Errorf("scan error, %v", err)
}
if adv == nil {
return nil, fmt.Errorf("printer not found")
}
log.Println("Found target printer with address:", adv.Addr().String())
return adv, nil
}
func discoverChars(client ble.Client) (*ble.Characteristic, *ble.Characteristic, *ble.Characteristic, error) {
var printChr, notifyChr, dataChr *ble.Characteristic
services, err := client.DiscoverServices([]ble.UUID{mainServiceUUID})
if err != nil || len(services) == 0 {
return nil, nil, nil, fmt.Errorf("service discovery failed: %v", err)
}
svc := services[0]
chars, err := client.DiscoverCharacteristics(nil, svc)
if err != nil {
return nil, nil, nil, fmt.Errorf("characteristic discovery failed: %v", err)
}
for _, c := range chars {
switch c.UUID.String() {
case printCharacteristic.String():
printChr = c
case notifyCharacteristic.String():
notifyChr = c
case dataCharacteristic.String():
dataChr = c
}
}
return printChr, notifyChr, dataChr, nil
}
func subToNotifs(client ble.Client, notifyChr *ble.Characteristic) error {
if notifyChr != nil {
_, _ = client.DiscoverDescriptors(nil, notifyChr)
err := client.Subscribe(notifyChr, false, func(b []byte) {
parseNotification(b)
})
if err != nil {
return fmt.Errorf("%v", err)
} else {
log.Println("Subscribed to printer notifications.")
}
} else {
return fmt.Errorf("missing notification characteristic")
}
return nil
}
func loadAndProcessImage(imagePath string, printMode PrintMode, ditherType string) ([]byte, int, error) {
img, err := decodeImage(imagePath)
if err != nil {
log.Fatalf("Image load error: %v", err)
}
img = padImageToMinLines(img, minLines)
var pixels []byte
var height int
// Convert image to the desired format
switch printMode {
case Mode1bpp:
pixels, height, err = loadImageMonoFromImage(img, ditherType)
case Mode4bpp:
pixels, height, err = loadImage4BitFromImage(img, ditherType)
}
if err != nil {
return nil, 0, fmt.Errorf("image conversion error: %v", err)
}
return pixels, height, nil
}
func loadPrinter() (ble.Client, *ble.Characteristic, *ble.Characteristic, *ble.Characteristic, error) {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// Initialize BLE device
d, err := linux.NewDevice()
if err != nil {
log.Fatalf("Failed to open BLE device: %v", err)
}
ble.SetDefaultDevice(d)
// Find printer
adv, err := findPrinter(ctx)
if err != nil {
log.Fatalf("Failed to find printer: %v", err)
}
// Connect to printer
log.Println("Connecting...")
client, err := ble.Dial(ctx, adv.Addr())
if err != nil {
log.Fatalf("Connect failed: %v", err)
}
// Negotiate large MTU if possible
mtu, err := client.ExchangeMTU(100)
if err != nil {
log.Printf("MTU negotiation failed: %v", err)
} else {
log.Printf("Negotiated ATT MTU: %d", mtu)
}
// Discover services and characteristics
printChr, notifyChr, dataChr, err := discoverChars(client)
if err != nil {
log.Fatalf("Characteristic discovery failed: %v", err)
}
return client, printChr, notifyChr, dataChr, nil
}
func main() { func main() {
flag.Parse() flag.Parse()
if outputPath != "-" {
log.Println("Bleh! Cat Printer Utility for MXW01, version", version)
}
needNotifications := getStatus || getBattery || getVersion || getPrintType || getQueryCount || ejectPaper > 0 || retractPaper > 0 needNotifications := getStatus || getBattery || getVersion || getPrintType || getQueryCount || ejectPaper > 0 || retractPaper > 0
needPrinter := needNotifications || (flag.NArg() > 0 && outputPath == "") if needNotifications {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
if !needPrinter && outputPath == "" { d, err := linux.NewDevice()
log.Println("Nothing to do. Use -h for help.") if err != nil {
log.Println("Done!") log.Fatalf("Failed to open BLE device: %v", err)
return }
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, notifyChr *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 notifyCharacteristic.String():
notifyChr = c
case printCharacteristic.String():
printChr = c
}
}
if notifyChr != nil {
_, _ = client.DiscoverDescriptors(nil, notifyChr)
err = client.Subscribe(notifyChr, false, func(b []byte) {
parseNotification(b)
})
if err != nil {
log.Printf("Subscribe failed: %v notifications will be ignored", err)
} else {
log.Println("Subscribed to printer notifications.")
}
}
if getStatus {
sendSimpleCommand(client, printChr, 0xA1)
}
if getBattery {
sendSimpleCommand(client, printChr, 0xAB)
}
if getVersion {
sendSimpleCommand(client, printChr, 0xB1)
}
if getPrintType {
sendSimpleCommand(client, printChr, 0xB0)
}
if getQueryCount {
sendSimpleCommand(client, printChr, 0xA7)
}
if ejectPaper > 0 {
sendLineCommand(client, printChr, 0xA3, ejectPaper)
}
if retractPaper > 0 {
sendLineCommand(client, printChr, 0xA4, retractPaper)
}
if getStatus || getBattery || getVersion || getPrintType || getQueryCount || ejectPaper > 0 || retractPaper > 0 {
log.Println("Waiting for notifications...")
time.Sleep(2 * time.Second)
}
if flag.NArg() < 1 {
return // no image to print, other commands may have run
} else if flag.NArg() >= 1 && needNotifications {
log.Fatalf("Refusing to print and query at the same time due to a firmware bug. Please run print and query commands separately.")
}
} }
// Get print mode
var printMode PrintMode var printMode PrintMode
switch mode { switch mode {
case "1bpp": case "1bpp":
@ -613,17 +556,25 @@ func main() {
fmt.Println("Invalid mode. Use '1bpp' or '4bpp'.") fmt.Println("Invalid mode. Use '1bpp' or '4bpp'.")
return return
} }
// Get image path
imagePath := flag.Arg(0) imagePath := flag.Arg(0)
pixels, height, err := []byte(nil), int(0), error(nil) var pixels []byte
var height int
img, err := decodeImage(imagePath)
if imagePath != "" { if err != nil {
pixels, height, err = loadAndProcessImage(imagePath, printMode, ditherType) log.Fatalf("Image load error: %v", err)
if err != nil { }
log.Fatalf("Failed to load and process image: %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 != "" { if outputPath != "" {
@ -650,75 +601,89 @@ func main() {
log.Fatalf("Failed to write PNG preview: %v", err) log.Fatalf("Failed to write PNG preview: %v", err)
} }
if outputPath != "-" { if outputPath != "-" {
log.Printf("Preview PNG written to %s\n", outputPath) fmt.Printf("Preview PNG written to %s\n", outputPath)
} }
return return
} }
if needPrinter { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
client, printChr, notifyChr, dataChr, err := loadPrinter() defer stop()
defer client.CancelConnection() d, err := linux.NewDevice()
if err != nil {
log.Fatalf("Failed to open BLE device: %v", err)
}
ble.SetDefaultDevice(d)
if err != nil { log.Println("Scanning for printer...")
log.Fatalf("Failed to load printer: %v", err) 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
}
if needNotifications { client, err := ble.Dial(ctx, adv.Addr())
// Subscribe to notifications if err != nil {
err := subToNotifs(client, notifyChr) log.Fatalf("Connect failed: %v", err)
if err != nil { }
log.Fatalf("Failed to subscribe to notifications: %v", err) defer client.CancelConnection()
}
// TODO: check if the firmware allows more than one command at a time mtu, err := client.ExchangeMTU(100)
// Also find a neater way to handle this if err != nil {
if getStatus { log.Printf("MTU negotiation failed: %v", err)
sendSimpleCommand(client, printChr, 0xA1) } else {
} log.Printf("Negotiated ATT MTU: %d", mtu)
if getBattery { }
sendSimpleCommand(client, printChr, 0xAB) var printChr, dataChr *ble.Characteristic
} services, err := client.DiscoverServices([]ble.UUID{mainServiceUUID})
if getVersion { if err != nil || len(services) == 0 {
sendSimpleCommand(client, printChr, 0xB1) log.Fatalf("Service discovery failed: %v", err)
} }
if getPrintType { svc := services[0]
sendSimpleCommand(client, printChr, 0xB0) chars, err := client.DiscoverCharacteristics(nil, svc)
} if err != nil {
if getQueryCount { log.Fatalf("Characteristic discovery failed: %v", err)
sendSimpleCommand(client, printChr, 0xA7) }
} for _, c := range chars {
if ejectPaper > 0 { switch c.UUID.String() {
sendLineCommand(client, printChr, 0xA3, ejectPaper) case dataCharacteristic.String():
} dataChr = c
if retractPaper > 0 { case printCharacteristic.String():
sendLineCommand(client, printChr, 0xA4, retractPaper) printChr = c
}
log.Println("Waiting for notifications...")
time.Sleep(2 * time.Second)
if flag.NArg() < 1 {
return // no image to print
} else {
log.Fatalf("Refusing to print and query at the same time due to a firmware bug. Please run print and query commands separately.")
}
}
if printChr == nil {
log.Fatalf("Missing required print characteristic")
} }
}
i := max(intensity, 0) if printChr == nil {
i = min(i, 100) log.Fatalf("Missing required print characteristic")
intensityByte := byte(i) }
if dataChr == nil { i := intensity
log.Fatalf("Missing required data characteristic") if i < 0 {
} i = 0
}
if i > 100 {
i = 100
}
intensityByte := byte(i)
err = sendImageBufferToPrinter(client, dataChr, printChr, pixels, height, printMode, intensityByte) if dataChr == nil {
if err != nil { log.Fatalf("Missing required data characteristic")
log.Fatalf("Failed to print image: %v", err) }
}
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!")