├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── internal ├── bt │ ├── bluetooth.go │ ├── scan.go │ ├── uuid_darwin.go │ ├── uuid_linux.go │ └── uuids.go ├── commands │ ├── checksum.go │ └── commands.go └── graphics │ └── graphics.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | *.jpeg 3 | *.jpg 4 | /gattoprint 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Daniel Kertesz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gattoprint 2 | 3 | A Go client for the thermal printer knows as "cat printer". 4 | 5 | This project implements the "cat printer" protocol in Go and is based on the amazing work 6 | done by the authors of the projects linked below. 7 | 8 | Bluetooth support is brought by the [Go Bluetooth](https://github.com/tinygo-org/bluetooth) library, 9 | which is part of the [TinyGo](https://tinygo.org/) project. 10 | 11 | Installation: 12 | 13 | ``` 14 | $ go install github.com/piger/gattoprint@latest 15 | ``` 16 | 17 | ## A word of warning 18 | 19 | This project is based on the reverse engineering of the Android app iPrint, which is 20 | the official client for this printer; using this program instead of the official client might 21 | damage your printer. 22 | 23 | ## OS support 24 | 25 | Tested on: 26 | 27 | - Debian 11 (Raspbian GNU/Linux 11 (bullseye)) on a Raspberry Pi 4b. 28 | - macOS Monterey (12.5) 29 | 30 | Due to the requirements of the [Go Bluetooth](https://github.com/tinygo-org/bluetooth) library 31 | cross-compiling this program is not supported, or at least is not as straightforward as it's usually 32 | for Go programs. 33 | 34 | ### macOS privacy settings 35 | 36 | On macOS this program must be _allowed_ to use Bluetooth; in the "Security & Privacy" section 37 | of the "System Preferences" app, in the "Privacy" section, you must add this program to the 38 | apps allowed to use Bluetooth. 39 | 40 | If you run this program from a terminal like iTerm2 then the program to be added is iTerm itself. 41 | 42 | ## Additional resources 43 | 44 | - [WerWolv/PythonCatPrinter](https://github.com/WerWolv/PythonCatPrinter) -- the original project that seem to have [started it all](https://werwolv.net/blog/cat_printer). 45 | - [amber-sixel/gb01print](https://github.com/amber-sixel/gb01print) -- a fork of the above project. 46 | - [catprinter](https://github.com/rbaron/catprinter) -- a Python client for the printer. 47 | - [Thermal Printer wiki](https://github.com/bitbank2/Thermal_Printer/wiki/Cat-Rabbit-printer-info) -- some information about the printing protocol. 48 | - [iPrint Utility](https://mywk.net/software/iprint-utility) -- a Windows client for the printer. 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/piger/gattoprint 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/anthonynsimon/bild v0.13.0 7 | github.com/esimov/dithergo v0.0.0-20210215145655-7f9ddf55e848 8 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e 9 | golang.org/x/term v0.5.0 10 | tinygo.org/x/bluetooth v0.5.0 11 | ) 12 | 13 | require ( 14 | github.com/JuulLabs-OSS/cbgo v0.0.2 // indirect 15 | github.com/fatih/structs v1.1.0 // indirect 16 | github.com/go-ole/go-ole v1.2.4 // indirect 17 | github.com/godbus/dbus/v5 v5.0.3 // indirect 18 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect 19 | github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d // indirect 20 | github.com/sirupsen/logrus v1.6.0 // indirect 21 | golang.org/x/sys v0.13.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/JuulLabs-OSS/cbgo v0.0.2 h1:gCDyT0+EPuI8GOFyvAksFcVD2vF4CXBAVwT6uVnD9oo= 3 | github.com/JuulLabs-OSS/cbgo v0.0.2/go.mod h1:L4YtGP+gnyD84w7+jN66ncspFRfOYB5aj9QSXaFHmBA= 4 | github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8= 5 | github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE= 6 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 7 | github.com/bgould/http v0.0.0-20190627042742-d268792bdee7/go.mod h1:BTqvVegvwifopl4KTEDth6Zezs9eR+lCWhvGKvkxJHE= 8 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 9 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 10 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 11 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= 16 | github.com/esimov/dithergo v0.0.0-20210215145655-7f9ddf55e848 h1:4mfWkM1T8YZsWyuuIQcV0lskFRCkCMPnKBWKcBOTJUs= 17 | github.com/esimov/dithergo v0.0.0-20210215145655-7f9ddf55e848/go.mod h1:KQYJwxnA4taJBkCet7blGupIXRROaNk+5mvpcIRSjZA= 18 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 19 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 20 | github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= 21 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 22 | github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= 23 | github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= 24 | github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= 25 | github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 26 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 28 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 29 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 30 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 31 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 32 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 33 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 34 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 35 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 36 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 37 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 38 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 39 | github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d h1:EG/xyWjHT19rkUpwsWSkyiCCmyqNwFovr9m10rhyOxU= 40 | github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0= 41 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 42 | github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk= 43 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 44 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 45 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 46 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 47 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 48 | github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= 49 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 50 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 51 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 52 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 53 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 54 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 55 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 56 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 57 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 58 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 59 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 60 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 61 | github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8= 62 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 63 | github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= 64 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 65 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 66 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 67 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 68 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 69 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 70 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= 71 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= 72 | golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 73 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 74 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 75 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 76 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 77 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 78 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 79 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 80 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 81 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 82 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 84 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 85 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 86 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 87 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 88 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 89 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 90 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 91 | golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= 92 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 93 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 94 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 95 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 96 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 97 | golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 98 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 99 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 100 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 101 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 102 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 103 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 104 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 105 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 106 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 107 | tinygo.org/x/bluetooth v0.5.0 h1:UftQTmx/snuTbeoS/R6+ZixmxSl5d6BvyfxlmD8eDng= 108 | tinygo.org/x/bluetooth v0.5.0/go.mod h1:3rm7IKtmhP7aU2XRJI/Ods3J9Lqc3BAPPTNZmTtb42Q= 109 | tinygo.org/x/drivers v0.14.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= 110 | tinygo.org/x/drivers v0.15.1/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= 111 | tinygo.org/x/drivers v0.16.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= 112 | tinygo.org/x/drivers v0.20.0/go.mod h1:uJD/l1qWzxzLx+vcxaW0eY464N5RAgFi1zTVzASFdqI= 113 | tinygo.org/x/tinyfont v0.2.1/go.mod h1:eLqnYSrFRjt5STxWaMeOWJTzrKhXqpWw7nU3bPfKOAM= 114 | tinygo.org/x/tinyfs v0.1.0/go.mod h1:ysc8Y92iHfhTXeyEM9+c7zviUQ4fN9UCFgSOFfMWv20= 115 | tinygo.org/x/tinyterm v0.1.0/go.mod h1:/DDhNnGwNF2/tNgHywvyZuCGnbH3ov49Z/6e8LPLRR4= 116 | -------------------------------------------------------------------------------- /internal/bt/bluetooth.go: -------------------------------------------------------------------------------- 1 | package bt 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "tinygo.org/x/bluetooth" 9 | ) 10 | 11 | const ( 12 | printWaitTime = 30 * time.Second // time to wait for the printer to finish printing. 13 | sendDelay = 10 * time.Millisecond // time to wait between each write command sent to the printer. 14 | ) 15 | 16 | // chunks split the slice `s` in chunks of the given size. 17 | func chunks(s []byte, size int) [][]byte { 18 | var result [][]byte 19 | l := len(s) 20 | 21 | for i := 0; i < l; i += size { 22 | end := i + size 23 | if end > l { 24 | end = l 25 | } 26 | result = append(result, s[i:end]) 27 | } 28 | 29 | return result 30 | } 31 | 32 | // SendCommands reads from the `commands` channel and send commands to the printer, 33 | // ensuring a certain chunk size (20 bytes) and a small delay between each write. 34 | func SendCommands(adapter *bluetooth.Adapter, address bluetooth.Addresser, commands chan []byte) error { 35 | device, err := adapter.Connect(address, bluetooth.ConnectionParams{}) 36 | if err != nil { 37 | return fmt.Errorf("failed to connect to printer: %w", err) 38 | } 39 | defer func() { 40 | if err := device.Disconnect(); err != nil { 41 | fmt.Printf("warning: error disconnecting from printer: %s", err) 42 | } 43 | }() 44 | 45 | services, err := device.DiscoverServices([]bluetooth.UUID{*PrintServiceUUID}) 46 | if err != nil { 47 | return fmt.Errorf("failed to get print service: %w", err) 48 | } 49 | 50 | printService := services[0] 51 | 52 | chs, err := printService.DiscoverCharacteristics([]bluetooth.UUID{*WriteUUID, *NotificationUUID}) 53 | if err != nil { 54 | return fmt.Errorf("failed to get write characteristic: %w", err) 55 | } 56 | 57 | tx, notif := chs[0], chs[1] 58 | notifChan := make(chan struct{}, 1) 59 | 60 | if err := notif.EnableNotifications(func(buf []byte) { 61 | notifChan <- struct{}{} 62 | }); err != nil { 63 | return fmt.Errorf("error enabling notifications: %w", err) 64 | } 65 | 66 | log.Println("sending commands to printer") 67 | 68 | for cmd := range commands { 69 | fmt.Print(".") 70 | 71 | for _, chunk := range chunks(cmd, 20) { 72 | fmt.Print("+") 73 | if _, err := tx.WriteWithoutResponse(chunk); err != nil { 74 | return err 75 | } 76 | time.Sleep(sendDelay) 77 | } 78 | } 79 | 80 | fmt.Println() 81 | log.Println("waiting for the printer to finish printing") 82 | 83 | t := time.NewTimer(printWaitTime) 84 | defer t.Stop() 85 | 86 | select { 87 | case <-t.C: 88 | log.Printf("%s passed but the printer didn't signal that finished printing", printWaitTime) 89 | case <-notifChan: 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/bt/scan.go: -------------------------------------------------------------------------------- 1 | package bt 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "golang.org/x/term" 11 | "tinygo.org/x/bluetooth" 12 | ) 13 | 14 | // FindDevice search for the named BLE device and returns its address; it 15 | // search indefinitely but can be stopped by pressing any key. 16 | // The `adapter` parameter is a Bluetooth adapter that must be enabled 17 | // before calling this function. 18 | func FindDevice(name string, adapter *bluetooth.Adapter) (bluetooth.Addresser, error) { 19 | oldState, err := term.MakeRaw(int(os.Stdin.Fd())) 20 | if err != nil { 21 | return nil, fmt.Errorf("error configuring terminal: %w", err) 22 | } 23 | defer func() { 24 | if err := term.Restore(int(os.Stdin.Fd()), oldState); err != nil { 25 | fmt.Printf("error restoring terminal state: %s", err) 26 | } 27 | }() 28 | 29 | keyPress := make(chan struct{}, 1) 30 | go func() { 31 | b := make([]byte, 1) 32 | if _, err := os.Stdin.Read(b); err != nil { 33 | // let's continue instead of returning here, so that 34 | // the channel is not left hanging forever. 35 | fmt.Printf("error reading stdin: %s", err) 36 | } 37 | keyPress <- struct{}{} 38 | }() 39 | 40 | ticker := time.NewTicker(500 * time.Millisecond) 41 | defer ticker.Stop() 42 | 43 | errChan := make(chan error, 1) 44 | jobChan := make(chan bluetooth.Addresser, 1) 45 | go func() { 46 | err := adapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) { 47 | if result.LocalName() == name { 48 | if err := adapter.StopScan(); err != nil { 49 | fmt.Printf("error stopping scan: %s", err) 50 | } 51 | jobChan <- result.Address 52 | } 53 | }) 54 | if err != nil { 55 | errChan <- fmt.Errorf("error scanning for BLE: %w", err) 56 | } 57 | }() 58 | 59 | // progress bar 60 | chars := []string{"|", "/", "-", "\\"} 61 | l := len(chars) 62 | idx := 0 63 | 64 | // clear progress bar 65 | defer func() { 66 | fmt.Printf("\r%s\r", strings.Repeat(" ", 50)) 67 | }() 68 | 69 | for { 70 | select { 71 | case <-keyPress: 72 | return nil, errors.New("scan cancelled") 73 | 74 | case result := <-jobChan: 75 | return result, nil 76 | 77 | case <-ticker.C: 78 | fmt.Printf("\r%s Scanning...", chars[idx]) 79 | idx++ 80 | if idx >= l { 81 | idx = 0 82 | } 83 | 84 | case err := <-errChan: 85 | return nil, err 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/bt/uuid_darwin.go: -------------------------------------------------------------------------------- 1 | package bt 2 | 3 | // It looks like the `cbgo` library doesn't support 16 bits UUID, leading 4 | // to macOS reading different UUIDs from the printer than what is read on 5 | // Linux; see also: https://github.com/tinygo-org/bluetooth/issues/68 6 | 7 | func init() { 8 | PrintServiceUUID = mustParseUUID("ae300000-0000-0000-0000-000000000000") 9 | WriteUUID = mustParseUUID("ae010000-0000-0000-0000-000000000000") 10 | NotificationUUID = mustParseUUID("ae020000-0000-0000-0000-000000000000") 11 | } 12 | -------------------------------------------------------------------------------- /internal/bt/uuid_linux.go: -------------------------------------------------------------------------------- 1 | package bt 2 | 3 | func init() { 4 | PrintServiceUUID = mustParseUUID("0000ae30-0000-1000-8000-00805f9b34fb") 5 | WriteUUID = mustParseUUID("0000ae01-0000-1000-8000-00805f9b34fb") 6 | NotificationUUID = mustParseUUID("0000ae02-0000-1000-8000-00805f9b34fb") 7 | } 8 | -------------------------------------------------------------------------------- /internal/bt/uuids.go: -------------------------------------------------------------------------------- 1 | package bt 2 | 3 | import "tinygo.org/x/bluetooth" 4 | 5 | var ( 6 | PrintServiceUUID *bluetooth.UUID 7 | WriteUUID *bluetooth.UUID 8 | NotificationUUID *bluetooth.UUID 9 | ) 10 | 11 | func mustParseUUID(s string) *bluetooth.UUID { 12 | uuid, err := bluetooth.ParseUUID(s) 13 | if err != nil { 14 | panic(err) 15 | } 16 | return &uuid 17 | } 18 | -------------------------------------------------------------------------------- /internal/commands/checksum.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | var table = []byte{ 4 | 0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, 0x38, 0x3f, 0x36, 0x31, 5 | 0x24, 0x23, 0x2a, 0x2d, 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65, 6 | 0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, 0xe0, 0xe7, 0xee, 0xe9, 7 | 0xfc, 0xfb, 0xf2, 0xf5, 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd, 8 | 0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, 0xa8, 0xaf, 0xa6, 0xa1, 9 | 0xb4, 0xb3, 0xba, 0xbd, 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2, 10 | 0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, 0xb7, 0xb0, 0xb9, 0xbe, 11 | 0xab, 0xac, 0xa5, 0xa2, 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a, 12 | 0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, 0x1f, 0x18, 0x11, 0x16, 13 | 0x03, 0x04, 0x0d, 0x0a, 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42, 14 | 0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, 0x89, 0x8e, 0x87, 0x80, 15 | 0x95, 0x92, 0x9b, 0x9c, 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4, 16 | 0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, 0xc1, 0xc6, 0xcf, 0xc8, 17 | 0xdd, 0xda, 0xd3, 0xd4, 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c, 18 | 0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, 0x19, 0x1e, 0x17, 0x10, 19 | 0x05, 0x02, 0x0b, 0x0c, 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34, 20 | 0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, 0x76, 0x71, 0x78, 0x7f, 21 | 0x6a, 0x6d, 0x64, 0x63, 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b, 22 | 0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, 0xae, 0xa9, 0xa0, 0xa7, 23 | 0xb2, 0xb5, 0xbc, 0xbb, 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8d, 0x84, 0x83, 24 | 0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, 0xe6, 0xe1, 0xe8, 0xef, 25 | 0xfa, 0xfd, 0xf4, 0xf3, 26 | } 27 | 28 | func crc8(data []byte) byte { 29 | var crc byte 30 | 31 | for _, val := range data { 32 | crc = table[(crc^val)&0xFF] 33 | } 34 | return crc & 0xFF 35 | } 36 | -------------------------------------------------------------------------------- /internal/commands/commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | 7 | "golang.org/x/exp/constraints" 8 | ) 9 | 10 | // see sources/com/blueUtils/BluetoothOrder.java 11 | 12 | var ( 13 | cmdSetQuality byte = 0xA4 14 | cmdControlLattice byte = 0xA6 15 | cmdSetEnergy byte = 0xAF 16 | cmdDrawingMode byte = 0xBE // 1 for text, 0 for images 17 | cmdOtherFeedPaper byte = 0xBD 18 | cmdDrawBitmap byte = 0xA2 // Line to draw. 0 bit -> don't draw pixel, 1 bit -> draw pixel 19 | cmdFeedPaper byte = 0xA1 20 | cmdGetDevState byte = 0xA3 21 | 22 | cmdPrintLattice = []byte{0xAA, 0x55, 0x17, 0x38, 0x44, 0x5F, 0x5F, 0x5F, 0x44, 0x38, 0x2C} 23 | cmdImgPrintSpeed = []byte{0x23} 24 | cmdFinishLattice = []byte{0xAA, 0x55, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17} 25 | cmdBlankSpeed = []byte{0x19} 26 | 27 | qualityStandard = []byte{0x33} 28 | 29 | cmdStart = []byte{0x51, 0x78} 30 | cmdEnd = []byte{0xFF} 31 | 32 | // QUALITIES 33 | // 0x31 0x32 0x33 0x34 0x35 (49, 50, 51, 52, 53 in Java) 34 | 35 | // PRINT_COMMANDS 36 | // 0xA2 0xBF (fixed length, run-length) 37 | ) 38 | 39 | // formatMessage encodes a printer command in the required format. 40 | func formatMessage(cmd byte, data []byte) []byte { 41 | msg := new(bytes.Buffer) 42 | 43 | msg.Write(cmdStart) 44 | msg.WriteByte(cmd) 45 | msg.WriteByte(0x00) 46 | msg.WriteByte(byte(len(data))) 47 | msg.WriteByte(0x00) 48 | msg.Write(data) 49 | msg.WriteByte(crc8(data)) 50 | msg.Write(cmdEnd) 51 | 52 | return msg.Bytes() 53 | } 54 | 55 | // binary.Write(buf, binary.LittleEndian, i) 56 | func printerShort(i int) []byte { 57 | result := []byte{ 58 | byte(i & 0xFF), byte((i >> 8) & 0xFF), 59 | } 60 | return result 61 | } 62 | 63 | /* contrast 64 | 65 | energy = { 66 | 0: printer_short(8000), 67 | 1: printer_short(12000), 68 | 2: printer_short(17500) 69 | } 70 | contrast = 1 71 | */ 72 | 73 | // encodeImgRows encodes each row of an image as an array of bytes, where 74 | // each byte contains 8 pixels. 75 | func encodeImgRows(img *image.Gray) chan []byte { 76 | out := make(chan []byte) 77 | 78 | go func() { 79 | bounds := img.Bounds() 80 | 81 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 82 | row := new(bytes.Buffer) 83 | var pixels byte 84 | index := 0 85 | 86 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 87 | r, g, b, _ := img.At(x, y).RGBA() 88 | 89 | if r == 0 && g == 0 && b == 0 { 90 | pixels |= 1 << index 91 | } else { 92 | pixels |= 0 93 | } 94 | 95 | index++ 96 | 97 | if index == 8 { 98 | row.WriteByte(pixels) 99 | index = 0 100 | pixels = 0 101 | } 102 | } 103 | out <- row.Bytes() 104 | } 105 | close(out) 106 | }() 107 | return out 108 | } 109 | 110 | // PrintImage is a "generator" (like in Python) function that returns a channel 111 | // where it writes the printer commands necessary to print an image. 112 | func PrintImage(img *image.Gray) chan []byte { 113 | cmds := make(chan []byte) 114 | 115 | go func() { 116 | // set quality to standard 117 | cmds <- formatMessage(cmdSetQuality, qualityStandard) 118 | 119 | // start and/or set up the lattice, whatever that is 120 | cmds <- formatMessage(cmdControlLattice, cmdPrintLattice) 121 | 122 | // Set energy used 123 | var contrast int = 12000 124 | cmds <- formatMessage(cmdSetEnergy, printerShort(contrast)) 125 | 126 | // Set mode to image mode 127 | cmds <- formatMessage(cmdDrawingMode, []byte{0}) 128 | 129 | // not entirely sure what this does 130 | cmds <- formatMessage(cmdOtherFeedPaper, cmdImgPrintSpeed) 131 | 132 | // encode image, one row at a time 133 | for row := range encodeImgRows(img) { 134 | cmds <- formatMessage(cmdDrawBitmap, row) 135 | } 136 | 137 | // finish the lattice, whatever that means 138 | cmds <- formatMessage(cmdControlLattice, cmdFinishLattice) 139 | 140 | // feed some empty lines 141 | // feed_lines = 112 142 | cmds <- formatMessage(cmdOtherFeedPaper, cmdBlankSpeed) 143 | 144 | count := 112 145 | for count > 0 { 146 | feed := min(count, 0xFF) 147 | cmds <- formatMessage(cmdFeedPaper, printerShort(feed)) 148 | count -= feed 149 | } 150 | 151 | // use a GetDevState request as a way for the printer to signal that it finished 152 | // printing its current job. 153 | cmds <- formatMessage(cmdGetDevState, []byte{0x00}) 154 | 155 | close(cmds) 156 | }() 157 | 158 | return cmds 159 | } 160 | 161 | func min[T constraints.Ordered](a, b T) T { 162 | if a < b { 163 | return a 164 | } 165 | return b 166 | } 167 | -------------------------------------------------------------------------------- /internal/graphics/graphics.go: -------------------------------------------------------------------------------- 1 | package graphics 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "image" 7 | _ "image/jpeg" 8 | _ "image/png" 9 | "log" 10 | "os" 11 | 12 | "github.com/anthonynsimon/bild/transform" 13 | dither "github.com/esimov/dithergo" 14 | ) 15 | 16 | const ( 17 | PrintWidth = 384 // the width of the printed image, required by the printer. 18 | errorMultiplier float32 = 1.18 // error multiplier for dithering. 19 | ) 20 | 21 | var ditherers map[string]dither.Dither 22 | 23 | func init() { 24 | // setup ditherers 25 | ditherers = make(map[string]dither.Dither) 26 | 27 | fs := dither.Dither{ 28 | Type: "FloydSteinberg", 29 | Settings: dither.Settings{ 30 | Filter: [][]float32{ 31 | {0.0, 0.0, 0.0, 7.0 / 48.0, 5.0 / 48.0}, 32 | {3.0 / 48.0, 5.0 / 48.0, 7.0 / 48.0, 5.0 / 48.0, 3.0 / 48.0}, 33 | {1.0 / 48.0, 3.0 / 48.0, 5.0 / 48.0, 3.0 / 48.0, 1.0 / 48.0}, 34 | }, 35 | }, 36 | } 37 | 38 | ditherers["FloydSteinberg"] = fs 39 | } 40 | 41 | // ConvertImage converts the given image file in a grayscale dithered image, 42 | // scaled down to the width required by the printer. 43 | func ConvertImage(filename string) (*image.Gray, error) { 44 | fh, err := os.Open(filename) 45 | if err != nil { 46 | return nil, fmt.Errorf("error opening file: %w", err) 47 | } 48 | defer fh.Close() 49 | 50 | img, imgFmt, err := image.Decode(fh) 51 | if err != nil { 52 | return nil, fmt.Errorf("error decoding image: %w", err) 53 | } 54 | 55 | log.Printf("decoded image %s as: %s", filename, imgFmt) 56 | 57 | b := img.Bounds() 58 | width := b.Dx() 59 | height := b.Dy() 60 | log.Printf("image size: %dx%d", width, height) 61 | 62 | factor := float64(PrintWidth) / float64(width) 63 | newHeight := int(float64(height) * factor) 64 | 65 | imgResized := transform.Resize(img, PrintWidth, newHeight, transform.Lanczos) 66 | b = imgResized.Bounds() 67 | log.Printf("resized to: %dx%d", b.Dx(), b.Dy()) 68 | 69 | // XXX: imaging has greyscaling methods as well! 70 | // imgGray := imaging.Grayscale(imgResized) 71 | imgGray := image.NewGray(b) 72 | for y := b.Min.Y; y < b.Max.Y; y++ { 73 | for x := b.Min.X; x < b.Max.X; x++ { 74 | imgGray.Set(x, y, imgResized.At(x, y)) 75 | } 76 | } 77 | 78 | ditherer, ok := ditherers["FloydSteinberg"] 79 | if !ok { 80 | return nil, fmt.Errorf("ditherer FloydSteinberg not found") 81 | } 82 | 83 | imgDithered := ditherer.Monochrome(imgGray, errorMultiplier) 84 | b = imgDithered.Bounds() 85 | 86 | log.Printf("dithered size: %dx%d", b.Dx(), b.Dy()) 87 | 88 | if result, ok := imgDithered.(*image.Gray); ok { 89 | return result, nil 90 | } 91 | 92 | return nil, errors.New("ditherer did not return the right type") 93 | } 94 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "image/png" 7 | "log" 8 | "os" 9 | 10 | "github.com/piger/gattoprint/internal/bt" 11 | "github.com/piger/gattoprint/internal/commands" 12 | "github.com/piger/gattoprint/internal/graphics" 13 | 14 | "tinygo.org/x/bluetooth" 15 | ) 16 | 17 | var ( 18 | flagDeviceName = flag.String("printer-name", "GB03", "Name advertised by the printer") 19 | flagOutput = flag.String("output", "output.png", "Output file name, for preview") 20 | flagNoPrint = flag.Bool("no-print", false, "Disable printing, just create the preview") 21 | ) 22 | 23 | func run(filename string) error { 24 | goo, err := graphics.ConvertImage(filename) 25 | if err != nil { 26 | return fmt.Errorf("error converting image: %w", err) 27 | } 28 | 29 | out, err := os.Create(*flagOutput) 30 | if err != nil { 31 | return fmt.Errorf("error opening preview file for writing: %w", err) 32 | } 33 | defer out.Close() 34 | 35 | if err := png.Encode(out, goo); err != nil { 36 | return fmt.Errorf("error encoding preview: %w", err) 37 | } 38 | 39 | if *flagNoPrint { 40 | return nil 41 | } 42 | 43 | cmds := commands.PrintImage(goo) 44 | 45 | var adapter = bluetooth.DefaultAdapter 46 | if err := adapter.Enable(); err != nil { 47 | return fmt.Errorf("error enabling Bluetooth adapter: %w", err) 48 | } 49 | 50 | addr, err := bt.FindDevice(*flagDeviceName, adapter) 51 | if err != nil { 52 | return fmt.Errorf("couldn't find printer: %w", err) 53 | } 54 | 55 | log.Printf("found %s: %s", *flagDeviceName, addr) 56 | 57 | if err := bt.SendCommands(adapter, addr, cmds); err != nil { 58 | return fmt.Errorf("error sending commands to printer: %w", err) 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func main() { 65 | flag.Parse() 66 | 67 | args := flag.Args() 68 | if len(args) == 0 { 69 | fmt.Printf("error: pass an image file\n") 70 | os.Exit(1) 71 | } 72 | 73 | filename := args[0] 74 | 75 | if err := run(filename); err != nil { 76 | log.Print(err) 77 | os.Exit(1) 78 | } 79 | } 80 | --------------------------------------------------------------------------------