From 885d814c70c7186cead9b76b92b199348bec0389 Mon Sep 17 00:00:00 2001 From: Ignacio Rivero Date: Fri, 20 Jun 2025 08:04:27 -0300 Subject: [PATCH] Initial commit --- cat.png | Bin 0 -> 763 bytes go.mod | 15 +++ go.sum | 43 ++++++ main.go | 333 +++++++++++++++++++++++++++++++++++++++++++++++ netpaws_test.png | Bin 0 -> 8913 bytes 5 files changed, 391 insertions(+) create mode 100644 cat.png create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 netpaws_test.png diff --git a/cat.png b/cat.png new file mode 100644 index 0000000000000000000000000000000000000000..5f2440b4321c5aab68a013fb6893a29e3827412e GIT binary patch literal 763 zcmYk4QAiVU9LK-soj02|b`R1_$qgfYsdj_F44a$Wm?N~3UVPe8q^JiM8i>2>n1U+{ z3y}&0#e#yUu!oAswX4K}URDIAR44YZ!r&e(#+2;$x0g8X?)U$Ff8WpdcW@VvwuT+{ z3OfKtq*-kPIGyT_5|N(m4MLJ$!pUGu5SSS)-8fN9XRfC$+{Ar)yXOKOJe|#LEkJ(_ zpbr5BYUw%PoC5ra0S9jYuK2{WV+|yT9f!jzHrX{bduNO~+v3gbJwVY%c5|7R-Mgu2 z>y5O8Y~RH)yHeKm@X;b^jS)3?yl?U6=b*jFS5|BY&o{?czKo~dDKGBr^E}PDR#NK| zJ=^hQDG@F7d%NocyYeDD6&erOk>-#RzXIxDH!y8%YPk=9boo!+k$fd|u683j@V*N$ zoFN)Yx+R)d8=19aK=~~~Uhi9Tz>_!3N(8K1Mjd@%n4yeOMoqd|z-vD0I&Xl`{Y+{^45MFi>!Nft3r0W>({ z(j=tPW+wrQiIeme%GuyN^o5Z_BGf45c=9pC;#P?1P7yVs=&BEL{c5! cbyCsrs`BP?#jtR8DM5c1BB56GK~pUG7k_~Bh5!Hn literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..44e1e16 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module catprinter-ble + +go 1.22 + +require ( + github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333 + github.com/disintegration/imaging v1.6.2 // indirect + github.com/mattn/go-colorable v0.1.6 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab // indirect + github.com/pkg/errors v0.8.1 // indirect + golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect + golang.org/x/sys v0.0.0-20211204120058-94396e421777 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3380553 --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/JuulLabs-OSS/cbgo v0.0.1/go.mod h1:L4YtGP+gnyD84w7+jN66ncspFRfOYB5aj9QSXaFHmBA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333 h1:bQK6D51cNzMSTyAf0HtM30V2IbljHTDam7jru9JNlJA= +github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333/go.mod h1:fFJl/jD/uyILGBeD5iQ8tYHrPlJafyqCJzAyTHNJ1Uk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab h1:n8cgpHzJ5+EDyDri2s/GC7a9+qK3/YEGnBsd0uS/8PY= +github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab/go.mod h1:y1pL58r5z2VvAjeG1VLGc8zOQgSOzbKN7kMHPvFXJ+8= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99/go.mod h1:CxaUhijgLFX0AROtH5mluSY71VqpjQBw9JXE2UKZmc4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211204120058-94396e421777 h1:QAkhGVjOxMa+n4mlsAWeAU+BMZmimQAaNiMu+iUi94E= +golang.org/x/sys v0.0.0-20211204120058-94396e421777/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c433576 --- /dev/null +++ b/main.go @@ -0,0 +1,333 @@ +// catprinter_ble.go +// A Go reimplementation of the CatPrinterBLE utility +// using Go's BLE stack and Image processing libraries + +package main + +import ( + "context" + "fmt" + "image/color" + "github.com/disintegration/imaging" + "log" + "os" + "os/signal" + "time" + ble "github.com/go-ble/ble" + "github.com/go-ble/ble/linux" +) + + +var ( + mainServiceUUID = ble.MustParse("ae30") + printCharacteristic = ble.MustParse("ae01") + notifyCharacteristic = ble.MustParse("ae02") + dataCharacteristic = ble.MustParse("ae03") + targetPrinterName = "MXW01" + scanTimeout = 10 * time.Second + printCommandHeader = []byte{0x22, 0x21} + printCommandFooter = byte(0xFF) +) + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func reverseBitsInByte(b byte) byte { + b = (b&0xF0)>>4 | (b&0x0F)<<4 + b = (b&0xCC)>>2 | (b&0x33)<<2 + b = (b&0xAA)>>1 | (b&0x55)<<1 + return b +} + +func dumpCharacteristics(client ble.Client, svc *ble.Service) { + chars, err := client.DiscoverCharacteristics(nil, svc) + if err != nil { + log.Fatalf("Error discovering characteristics: %v", err) + } + + log.Printf("Discovered %d characteristics:", len(chars)) + for _, c := range chars { + props := "" + if c.Property&ble.CharRead != 0 { + props += "Read " + } + if c.Property&ble.CharWrite != 0 { + props += "Write " + } + if c.Property&ble.CharWriteNR != 0 { + props += "WriteWithoutResponse " + } + if c.Property&ble.CharNotify != 0 { + props += "Notify " + } + if c.Property&ble.CharIndicate != 0 { + props += "Indicate " + } + + log.Printf(" UUID: %s, Properties: %s", c.UUID, props) + } +} + +func parseNotification(data []byte) { + if len(data) < 10 || data[0] != 0x22 || data[1] != 0x21 { + fmt.Println("Invalid notification header") + return + } + + cmd := data[2] + switch cmd { + case 0xA1: // GetStatus + battery := data[9] + temp := data[10] + statusOk := data[12] == 0 + statusCode := data[6] + errCode := data[13] + + statusMsg := "Unknown" + if statusOk { + switch statusCode { + case 0x0: + statusMsg = "Standby" + case 0x1: + statusMsg = "Printing" + case 0x2: + statusMsg = "Feeding paper" + case 0x3: + statusMsg = "Ejecting paper" + } + } else { + switch errCode { + case 0x1, 0x9: + statusMsg = "No paper" + case 0x4: + statusMsg = "Overheated" + case 0x8: + statusMsg = "Low battery" + } + } + + fmt.Printf("Status: %v (%s), Battery: %d, Temp: %d\n", statusOk, statusMsg, battery, temp) + default: + fmt.Printf("Unhandled command: 0x%02X\n", cmd) + } +} + +const ( + linePixels = 384 + bytesPerLine = linePixels / 8 +) + +// Convert image to 1bpp, dithered, and packed bytes +func loadImageMono(path string) ([]byte, int, error) { + img, err := imaging.Open(path, imaging.AutoOrientation(true)) + if err != nil { + return nil, 0, fmt.Errorf("image open error: %v", err) + } + + ratio := float64(img.Bounds().Dx()) / float64(img.Bounds().Dy()) + height := int(float64(linePixels) / ratio) + img = imaging.Resize(img, linePixels, height, imaging.Lanczos) + img = imaging.Grayscale(img) + img = imaging.AdjustContrast(img, 10) + + pixels := make([]byte, (linePixels*height)/8) + for y := 0; y < height; y++ { + for x := 0; x < linePixels; x++ { + gray := color.GrayModel.Convert(img.At(x, y)).(color.Gray) + if gray.Y < 127 { + idx := (y*linePixels + x) / 8 + bit := 7 - (x % 8) + pixels[idx] |= 1 << bit + } + } + } + + return pixels, height, nil +} + +func sendImageToPrinter(client ble.Client, dataChr, printChr *ble.Characteristic, path string) error { + pixels, height, err := loadImageMono(path) + if err != nil { + return err + } + + fmt.Printf("Sending image: %dx%d lines\n", linePixels, height) + + cmd := buildCommand(0xA2, []byte{80}) // intensity 80 + err = client.WriteCharacteristic(printChr, cmd, true) + if err != nil { + return fmt.Errorf("intensity set failed: %v", err) + } + + lines := height + param := []byte{ + byte(lines & 0xFF), byte(lines >> 8), + 0x30, // reserved + 0x00, // 1bpp mode + } + cmd = buildCommand(0xA9, param) + err = client.WriteCharacteristic(printChr, cmd, true) + if err != nil { + return fmt.Errorf("print command failed: %v", err) + } + + for y := 0; y < lines; y++ { + slice := pixels[y*bytesPerLine : (y+1)*bytesPerLine] + reversed := make([]byte, bytesPerLine) + for i := 0; i < bytesPerLine; i++ { + reversed[i] = reverseBitsInByte(slice[i]) + } + + err := client.WriteCharacteristic(dataChr, reversed, true) + if err != nil { + return fmt.Errorf("line %d write failed: %v", y, err) + } + time.Sleep(6 * time.Millisecond) + } + + cmd = buildCommand(0xAD, []byte{0x00}) // flush + err = client.WriteCharacteristic(printChr, cmd, true) + if err != nil { + return fmt.Errorf("flush failed: %v", err) + } + + return nil +} + + +func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: catprinter-ble ") + return + } + imagePath := os.Args[1] + + 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...") + + var adv ble.Advertisement + ctxScan, cancel := context.WithTimeout(ctx, 10*time.Second) + 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 + } + + log.Println("Connecting...") + 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, continuing with default MTU. Reason: %v", err) + } else { + log.Printf("Negotiated ATT MTU: %d", mtu) + } + + + 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("Char discovery failed: %v", err) + } + + var printChr, dataChr *ble.Characteristic + for _, c := range chars { + switch c.UUID.String() { + case printCharacteristic.String(): + printChr = c + case dataCharacteristic.String(): + dataChr = c + } + } + if printChr == nil || dataChr == nil { + log.Fatalf("Required characteristics missing") + } + + err = sendImageToPrinter(client, dataChr, printChr, imagePath) + if err != nil { + log.Fatalf("Failed to print image: %v", err) + } + + log.Println("Done!") +} + +func buildCommand(cmdId byte, payload []byte) []byte { + cmd := append([]byte{}, printCommandHeader...) + cmd = append(cmd, cmdId) + cmd = append(cmd, 0x00) // reserved + cmd = append(cmd, byte(len(payload)&0xFF), byte(len(payload)>>8)) + cmd = append(cmd, payload...) + cmd = append(cmd, calculateCRC8(payload)) + cmd = append(cmd, printCommandFooter) + return cmd +} + +func calculateCRC8(data []byte) byte { + table := [256]byte{ + 0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, + 0x38, 0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d, + 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65, + 0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, + 0xe0, 0xe7, 0xee, 0xe9, 0xfc, 0xfb, 0xf2, 0xf5, + 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd, + 0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, + 0xa8, 0xaf, 0xa6, 0xa1, 0xb4, 0xb3, 0xba, 0xbd, + 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2, + 0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, + 0xb7, 0xb0, 0xb9, 0xbe, 0xab, 0xac, 0xa5, 0xa2, + 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a, + 0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, + 0x1f, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0d, 0x0a, + 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42, + 0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, + 0x89, 0x8e, 0x87, 0x80, 0x95, 0x92, 0x9b, 0x9c, + 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4, + 0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, + 0xc1, 0xc6, 0xcf, 0xc8, 0xdd, 0xda, 0xd3, 0xd4, + 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c, + 0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, + 0x19, 0x1e, 0x17, 0x10, 0x05, 0x02, 0x0b, 0x0c, + 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34, + 0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, + 0x76, 0x71, 0x78, 0x7f, 0x6a, 0x6d, 0x64, 0x63, + 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b, + 0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, + 0xae, 0xa9, 0xa0, 0xa7, 0xb2, 0xb5, 0xbc, 0xbb, + 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8d, 0x84, 0x83, + 0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, + 0xe6, 0xe1, 0xe8, 0xef, 0xfa, 0xfd, 0xf4, 0xf3} + crc := byte(0) + for _, b := range data { + crc = table[crc^b] + } + return crc +} + diff --git a/netpaws_test.png b/netpaws_test.png new file mode 100644 index 0000000000000000000000000000000000000000..23943a3704d7c85d2104091af74a184532eea92a GIT binary patch literal 8913 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