4a0a055234
git-subtree-dir: faxmachine git-subtree-split: d23200bcfdedb9f8cc57e6a3c65b5ef93fcbfd19
274 lines
6.2 KiB
Go
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
|
|
}
|