label_printer/escpos/escpos.go
TT-392 4a0a055234 Squashed 'faxmachine/' content from commit d23200b
git-subtree-dir: faxmachine
git-subtree-split: d23200bcfdedb9f8cc57e6a3c65b5ef93fcbfd19
2024-09-12 20:45:00 +02:00

274 lines
6.2 KiB
Go

package escpos
import (
"context"
"fmt"
"image"
"io"
"math"
"runtime"
"git.sr.ht/~guacamolie/faxmachine/escpos/printer"
"git.sr.ht/~guacamolie/faxmachine/escpos/protocol"
)
type Printer struct {
printer printer.Printer
proto protocol.Protocol
ownsPrinter bool
lineDirty bool
ctx context.Context
cancelCtx context.CancelCauseFunc
statusResponse chan byte
asbStatus chan protocol.ASBStatus
}
func StartPrinter(printer printer.Printer, proto protocol.Protocol) (*Printer, error) {
p := &Printer{
printer: printer,
proto: proto,
ownsPrinter: false,
lineDirty: false,
statusResponse: make(chan byte),
asbStatus: make(chan protocol.ASBStatus),
}
if err := p.writeInstr(mustInstr(p.proto.InitializePrinter())); err != nil {
return nil, err
}
p.ctx, p.cancelCtx = context.WithCancelCause(context.Background())
go func() {
for p.ctx.Err() == nil {
var buf [4]byte
n, err := p.printer.Read(buf[:])
if err == io.EOF {
runtime.Gosched()
continue
}
if err != nil {
p.cancelCtx(fmt.Errorf("failed to read from printer: %w", err))
break
}
switch n {
case 1:
p.statusResponse <- buf[1]
case 4:
p.asbStatus <- p.proto.ParseASBStatus(buf)
}
}
close(p.statusResponse)
close(p.asbStatus)
}()
return p, nil
}
const (
FlagNone = 0
FlagDebug = 1 << 0
)
func StartUSBPrinter(path string, proto protocol.Protocol, flags int) (*Printer, error) {
usbPrinter, err := printer.OpenUSBPrinter(path, flags)
if err != nil {
return nil, err
}
p, err := StartPrinter(usbPrinter, proto)
if err != nil {
usbPrinter.Close()
return nil, err
}
p.ownsPrinter = true
return p, nil
}
func (p *Printer) Close() error {
p.cancelCtx(nil)
if p.ownsPrinter {
return p.printer.Close()
}
return nil
}
func mustInstr(instr []byte, err error) []byte {
if err != nil {
panic(fmt.Sprintf("library bug: %v", err))
}
return instr
}
func (p *Printer) writeInstr(instr []byte) error {
_, err := p.printer.Write(instr)
if err != nil {
return &IOError{err}
}
return nil
}
func (p *Printer) Write(data []byte) (int, error) {
return p.WriteString(string(data))
}
func (p *Printer) WriteString(s string) (n int, err error) {
err = p.Print(Text(s))
if err != nil {
return 0, err
}
// We can't just return the amount of written characters here, since that
// may not equal the amount of input characters after conversion. Code
// calling Write and WriteString often expect n to equal len(s) on success,
// to just return len(s) to satisfy this.
return len(s), nil
}
func (p *Printer) Print(c Component) error {
instructions, err := c.render(p.proto)
if err != nil {
return err
}
for _, inst := range instructions {
if _, err := p.printer.Write(inst); err != nil {
return &IOError{err}
}
if len(inst) > 0 && inst[len(inst)-1] == '\n' {
p.lineDirty = false
} else {
p.lineDirty = true
}
}
return nil
}
func (p *Printer) SetPrintSpeed(speed int) error {
if speed > math.MaxUint8 {
return fmt.Errorf("invalid print speed %d", speed)
}
instr, err := p.proto.SelectPrintSpeed(uint8(speed))
if err != nil {
return fmt.Errorf("printer does not support print speed %d: %v", speed, err)
}
return p.writeInstr(instr)
}
func (p *Printer) PrintImage(img image.Image) error {
if p.lineDirty {
return &LineDirtyError{"print image"}
}
x, y, data := getPrintImageData(img)
return p.printImage(x, y, data)
}
func (p *Printer) printImage(x int, y int, data []byte) error {
maxY := 1662
if y > maxY {
err := p.printImage(x, maxY, data[:(x*maxY)>>3])
if err != nil {
return fmt.Errorf("failed to print first half: %w", err)
}
// Wait until we finished to avoid overflowing the printer's buffer.
if err := p.Wait(); err != nil {
return fmt.Errorf("error printing first half: %w", err)
}
err = p.printImage(x, y-maxY, data[(x*maxY)>>3:])
if err := p.Wait(); err != nil {
return fmt.Errorf("error printing second half: %w", err)
}
return nil
}
storeInstr, err := p.proto.StoreGraphicsData(1, 1, protocol.Color1, uint16(x), uint16(y), data...)
if err != nil {
return fmt.Errorf("image does not confirm to limitations of printer: %w", err)
}
printInstr := mustInstr(p.proto.PrintGraphicsData())
if err := p.writeInstr(storeInstr); err != nil {
return fmt.Errorf("failed to send image data to printer: %w", err)
}
if err := p.writeInstr(printInstr); err != nil {
return fmt.Errorf("failed to send print instruction to printer: %w", err)
}
return nil
}
func (p *Printer) CutPaper() error {
if p.lineDirty {
return &LineDirtyError{"cut paper"}
}
instr := mustInstr(p.proto.CutPaper(protocol.FeedAndPartialCut, 0))
return p.writeInstr(instr)
}
func (p *Printer) Wait() error {
instr := mustInstr(p.proto.TransmitStatus(protocol.TransmitPaperSensorStatus))
if err := p.writeInstr(instr); err != nil {
return fmt.Errorf("failed to request status from printer: %w", err)
}
select {
case <-p.ctx.Done():
return context.Cause(p.ctx)
case <-p.statusResponse:
return nil
}
}
func (p *Printer) EnableASB(flags int) error {
instr, err := p.proto.SetAutomaticStatusBack(uint8(flags))
if err != nil {
return fmt.Errorf("failed to request Automatic Status Back (ASB) from printer: %w", err)
}
return p.writeInstr(instr)
}
func (p *Printer) DisableASB() error {
return p.EnableASB(protocol.ASBReportNothing)
}
func (p *Printer) ASBStatus() <-chan protocol.ASBStatus {
return p.asbStatus
}
// LineDirtyError is returned when the action cannot be performed because the
// printer needs to be in the "begining of the line" state for action to be
// performed, but it has been determined that the printer won't be in this
// state when it will process the command.
//
// This usually happens when character data is written to the printer without a
// linefeed ('\n') at the end.
type LineDirtyError struct {
attemptedAction string
}
func (err *LineDirtyError) Error() string {
return fmt.Sprintf("can only %s when at the beginning of the line", err.attemptedAction)
}
type IOError struct {
wrapped error
}
func (err *IOError) Error() string {
return fmt.Sprintf("io error when printing: %v", err.wrapped)
}
func (err *IOError) Unwrap() error {
return err.wrapped
}