Files
2026-09-30 10:22:42 +02:00

778 lines
19 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package escpos
import (
"bytes"
"strings"
)
const (
nul = 0x00
ht = 0x09
lf = 0x0A
ff = 0x0C
cr = 0x0D
dle = 0x10
can = 0x18
esc = 0x1B
fs = 0x1C
gs = 0x1D
)
// step interpreta el comando que empieza en b[0]. Devuelve los bytes
// consumidos, o 0 si el comando está incompleto y hay que esperar más datos.
func (p *Printer) step(s *Session, b []byte) int {
switch c := b[0]; {
case c >= 0x20:
return p.text(s, b)
case c == lf:
p.cmd(s, b[:1], "LF — imprime la línea y avanza %d puntos", max(p.st.lineSpacing, p.lineHeight()))
p.printLine(p.st.lineSpacing)
case c == cr:
p.cmd(s, b[:1], "CR — ignorado (esta impresora no hace retorno automático)")
case c == ht:
p.tab()
p.cmd(s, b[:1], "HT — tabulador: columna en punto %d", p.line.x)
case c == esc:
return p.escCommand(s, b)
case c == gs:
return p.gsCommand(s, b)
case c == dle:
return p.dleCommand(s, b)
case c == fs:
return p.fsCommand(s, b)
case c == ff || c == can:
p.cmd(s, b[:1], "0x%02X — solo tiene efecto en modo página: ignorado", c)
default:
p.warnRaw(s, b[:1], "Byte de control 0x%02X sin significado: ignorado", c)
}
return 1
}
func (p *Printer) text(s *Session, b []byte) int {
n := 0
for n < len(b) && b[n] >= 0x20 {
n++
}
var sb strings.Builder
for _, c := range b[:n] {
r := p.decode(c)
p.addChar(r)
sb.WriteRune(r)
}
p.addLog(s, LogText, b[:n], "Texto: «"+sb.String()+"»")
return n
}
// Longitud total de los comandos ESC de longitud fija.
var escFixedLen = map[byte]int{
'@': 2, 'E': 3, 'G': 3, '-': 3, 'a': 3, '!': 3, 'M': 3, 'd': 3, 'J': 3,
'2': 2, '3': 3, ' ': 3, 't': 3, 'R': 3, 'p': 5, 'i': 2, 'm': 2, '$': 4,
'\\': 4, '{': 3, 'V': 3, 'c': 4, '=': 3, 'v': 2, 'u': 3, 'U': 3, 'L': 2,
'S': 2, 'T': 3, 'W': 10, 'r': 3, 'e': 3, '%': 3, '?': 3, 'K': 3,
}
// Longitud total de los comandos GS de longitud fija.
var gsFixedLen = map[byte]int{
'!': 3, 'B': 3, 'L': 4, 'W': 4, 'I': 3, 'r': 3, 'a': 3, 'h': 3, 'w': 3,
'H': 3, 'f': 3, 'P': 4, 'b': 3, '/': 3, '$': 4, '\\': 4, 'T': 3, 'E': 3,
':': 2, '^': 5, 'j': 3, 'g': 3, 'z': 3,
}
func (p *Printer) escCommand(s *Session, b []byte) int {
if len(b) < 2 {
return 0
}
fixed := escFixedLen
if n, ok := fixed[b[1]]; ok && len(b) < n {
return 0
}
switch b[1] {
case '@':
p.st = defaultMode(p.profile.WidthDots)
p.line = lineBuf{}
p.cmd(s, b[:2], "ESC @ — inicializar: se borran el buffer de línea y todos los modos")
return 2
case 'E':
p.st.bold = b[2]&1 == 1
p.cmd(s, b[:3], "ESC E %d — negrita %s", b[2], onOff(p.st.bold))
return 3
case 'G':
p.st.doubleStrike = b[2]&1 == 1
p.cmd(s, b[:3], "ESC G %d — doble impacto %s", b[2], onOff(p.st.doubleStrike))
return 3
case '-':
switch b[2] {
case 0, '0':
p.st.underline = 0
case 1, '1':
p.st.underline = 1
case 2, '2':
p.st.underline = 2
}
p.cmd(s, b[:3], "ESC - %d — subrayado de %d puntos", b[2], p.st.underline)
return 3
case 'a':
switch b[2] {
case 0, '0':
p.st.align = 0
case 1, '1':
p.st.align = 1
case 2, '2':
p.st.align = 2
}
msg := "ESC a %d — alineación: %s"
if len(p.line.cells) > 0 {
msg += " (a mitad de línea: se aplicará en la siguiente)"
}
p.cmd(s, b[:3], msg, b[2], []string{"izquierda", "centro", "derecha"}[p.st.align])
return 3
case '!':
n := b[2]
p.st.fontB = n&0x01 != 0
p.st.bold = n&0x08 != 0
p.st.heightMul = 1 + int(n>>4&1)
p.st.widthMul = 1 + int(n>>5&1)
p.st.underline = int(n >> 7 & 1)
p.cmd(s, b[:3], "ESC ! 0x%02X — modo: fuente %s, negrita %v, alto×%d, ancho×%d, subrayado %v",
n, map[bool]string{false: "A", true: "B"}[p.st.fontB], p.st.bold, p.st.heightMul, p.st.widthMul, p.st.underline > 0)
return 3
case 'M':
p.st.fontB = b[2] == 1 || b[2] == '1'
p.cmd(s, b[:3], "ESC M %d — fuente %s", b[2], map[bool]string{false: "A (12×24)", true: "B (9×17)"}[p.st.fontB])
return 3
case 'd':
n := int(b[2])
p.cmd(s, b[:3], "ESC d %d — imprime y avanza %d líneas", n, n)
if n == 0 {
p.printLine(0)
} else {
p.printLine(p.st.lineSpacing)
p.paper.grow((n - 1) * p.st.lineSpacing)
}
return 3
case 'J':
p.cmd(s, b[:3], "ESC J %d — imprime y avanza %d puntos (%.1f mm)", b[2], b[2], float64(b[2])/8)
p.printLine(int(b[2]))
return 3
case '2':
p.st.lineSpacing = 30
p.cmd(s, b[:2], "ESC 2 — interlineado por defecto (30 puntos)")
return 2
case '3':
p.st.lineSpacing = int(b[2])
p.cmd(s, b[:3], "ESC 3 %d — interlineado de %d puntos", b[2], b[2])
return 3
case ' ':
p.st.rightSpacing = int(b[2])
p.cmd(s, b[:3], "ESC SP %d — espacio a la derecha de cada carácter: %d puntos", b[2], b[2])
return 3
case 't':
if cp, ok := codePages[b[2]]; ok {
p.st.codePage = b[2]
p.cmd(s, b[:3], "ESC t %d — página de códigos %s", b[2], cp.name)
} else {
p.warnRaw(s, b[:3], "ESC t %d — página de códigos no soportada por el emulador: se mantiene la actual", b[2])
}
return 3
case 'R':
p.st.intl = b[2]
name, ok := intlNames[b[2]]
if !ok {
name = "sin tabla en el emulador: se usa EE.UU."
}
p.cmd(s, b[:3], "ESC R %d — juego de caracteres internacional: %s", b[2], name)
return 3
case 'p':
p.status.DrawerOpen = true
p.sendASB()
p.cmd(s, b[:5], "ESC p %d %d %d — pulso al cajón (pin %d): encendido %d ms, apagado %d ms → CAJÓN ABIERTO",
b[2], b[3], b[4], map[bool]int{false: 2, true: 5}[b[2]&1 == 1], int(b[3])*2, int(b[4])*2)
return 5
case 'i', 'm':
p.cmd(s, b[:2], "ESC %c — corte %s (comando antiguo)", b[1], map[byte]string{'i': "total", 'm': "parcial"}[b[1]])
if b[1] == 'i' {
p.doCut(s, "total", 0)
} else {
p.doCut(s, "parcial", 0)
}
return 2
case '$':
pos := int(b[2]) | int(b[3])<<8
if pos <= p.areaWidth() {
p.line.x = pos
}
p.cmd(s, b[:4], "ESC $ — posición absoluta: punto %d", pos)
return 4
case '\\':
d := int(int16(uint16(b[2]) | uint16(b[3])<<8))
if x := p.line.x + d; x >= 0 && x <= p.areaWidth() {
p.line.x = x
}
p.cmd(s, b[:4], "ESC \\ — posición relativa: %+d puntos", d)
return 4
case '*':
if len(b) < 5 {
return 0
}
m := b[2]
k := int(b[3]) | int(b[4])<<8
bpc := 1
if m == 32 || m == 33 {
bpc = 3
}
total := 5 + k*bpc
if len(b) < total {
return 0
}
p.addBitImage(m, k, b[5:total])
p.cmd(s, b[:total], "ESC * %d — imagen de bits: %d columnas de %d puntos", m, k, 8*bpc)
return total
case 'D':
var tabs []int
for i := 2; i < len(b); i++ {
if b[i] == nul || len(tabs) == 32 {
n := i
if b[i] == nul {
n = i + 1
}
p.st.tabs = tabs
p.cmd(s, b[:n], "ESC D — tabuladores en las columnas %v", tabs)
return n
}
tabs = append(tabs, int(b[i]))
}
return 0
case '&':
if len(b) < 5 {
return 0
}
y, i := int(b[2]), 5
for c := int(b[3]); c <= int(b[4]); c++ {
if len(b) <= i {
return 0
}
i += 1 + y*int(b[i])
if len(b) < i {
return 0
}
}
p.warnRaw(s, b[:i], "ESC & — definir caracteres de usuario: no emulado")
return i
case '(':
if len(b) < 5 {
return 0
}
total := 5 + (int(b[3]) | int(b[4])<<8)
if len(b) < total {
return 0
}
p.warnRaw(s, b[:total], "ESC ( %c — no emulado", b[2])
return total
case '{':
p.st.upsideDown = b[2]&1 == 1
p.warnRaw(s, b[:3], "ESC { %d — impresión boca abajo: no emulada", b[2])
return 3
case 'v':
p.cmd(s, b[:2], "ESC v — enviar estado del sensor de papel")
p.respond(s, p.paperSensorByte())
return 2
case 'u':
p.cmd(s, b[:3], "ESC u %d — enviar estado del periférico (cajón)", b[2])
p.respond(s, p.drawerByte())
return 3
case 'L', 'S', 'T', 'W':
n := map[byte]int{'L': 2, 'S': 2, 'T': 3, 'W': 10}[b[1]]
p.warnRaw(s, b[:n], "ESC %c — comando de modo página: no emulado", b[1])
return n
}
if n, ok := fixed[b[1]]; ok {
p.warnRaw(s, b[:n], "ESC %c — sin efecto en el emulador", b[1])
return n
}
p.warnRaw(s, b[:2], "ESC 0x%02X — comando desconocido", b[1])
return 2
}
func (p *Printer) gsCommand(s *Session, b []byte) int {
if len(b) < 2 {
return 0
}
fixed := gsFixedLen
if n, ok := fixed[b[1]]; ok && len(b) < n {
return 0
}
switch b[1] {
case '!':
n := b[2]
p.st.widthMul = 1 + int(n>>4&7)
p.st.heightMul = 1 + int(n&7)
p.cmd(s, b[:3], "GS ! 0x%02X — tamaño: ancho ×%d, alto ×%d", n, p.st.widthMul, p.st.heightMul)
return 3
case 'B':
p.st.reverse = b[2]&1 == 1
p.cmd(s, b[:3], "GS B %d — blanco sobre negro %s", b[2], onOff(p.st.reverse))
return 3
case 'L':
p.st.leftMargin = int(b[2]) | int(b[3])<<8
p.cmd(s, b[:4], "GS L — margen izquierdo de %d puntos", p.st.leftMargin)
return 4
case 'W':
p.st.areaWidth = int(b[2]) | int(b[3])<<8
p.cmd(s, b[:4], "GS W — ancho del área de impresión: %d puntos", p.st.areaWidth)
return 4
case 'V':
return p.gsCut(s, b)
case 'v':
if len(b) < 3 {
return 0
}
if b[2] != '0' {
p.warnRaw(s, b[:3], "GS v %c — desconocido", b[2])
return 3
}
if len(b) < 8 {
return 0
}
x := int(b[4]) | int(b[5])<<8
y := int(b[6]) | int(b[7])<<8
total := 8 + x*y
if len(b) < total {
return 0
}
p.printRaster(b[3], x, y, b[8:total])
p.cmd(s, b[:total], "GS v 0 %d — imagen raster de %d×%d puntos (%d bytes de datos)", b[3], x*8, y, x*y)
return total
case '(':
if len(b) < 5 {
return 0
}
total := 5 + (int(b[3]) | int(b[4])<<8)
if len(b) < total {
return 0
}
if b[2] == 'k' {
p.gsParenK(s, b[:total])
} else {
p.warnRaw(s, b[:total], "GS ( %c — no emulado", b[2])
}
return total
case '8':
if len(b) < 7 {
return 0
}
n := int64(b[3]) | int64(b[4])<<8 | int64(b[5])<<16 | int64(b[6])<<24
if n > 8<<20 {
p.warnRaw(s, b[:7], "GS 8 %c — longitud absurda (%d): se descarta la cabecera", b[2], n)
return 7
}
total := 7 + int(n)
if len(b) < total {
return 0
}
p.warnRaw(s, b[:total], "GS 8 %c — gráficos extendidos: no emulado", b[2])
return total
case '*':
if len(b) < 4 {
return 0
}
total := 4 + int(b[2])*int(b[3])*8
if len(b) < total {
return 0
}
p.warnRaw(s, b[:total], "GS * — imagen descargable: no emulada")
return total
case 'I':
p.cmd(s, b[:3], "GS I %d — identificación de la impresora", b[2])
p.gsI(s, b[2])
return 3
case 'r':
switch b[2] {
case 1, '1':
p.cmd(s, b[:3], "GS r %d — enviar estado del sensor de papel", b[2])
p.respond(s, p.paperSensorByte())
case 2, '2':
p.cmd(s, b[:3], "GS r %d — enviar estado del cajón", b[2])
p.respond(s, p.drawerByte())
default:
p.warnRaw(s, b[:3], "GS r %d — tipo de estado desconocido", b[2])
}
return 3
case 'a':
p.asbMask = b[2]
p.asbTo = s
if b[2] == 0 {
p.asbTo = nil
}
p.cmd(s, b[:3], "GS a 0x%02X — estado automático (ASB) %s", b[2], onOff(b[2] != 0))
p.sendASB()
return 3
case 'k':
return p.gsBarcode(s, b)
case 'h':
p.st.barcodeHeight = max(1, int(b[2]))
p.cmd(s, b[:3], "GS h %d — altura del código de barras", b[2])
return 3
case 'w':
p.st.barcodeWidth = int(b[2])
p.cmd(s, b[:3], "GS w %d — ancho de módulo del código de barras", b[2])
return 3
case 'H':
p.st.hriPos = b[2]
p.cmd(s, b[:3], "GS H %d — posición del texto legible (HRI)", b[2])
return 3
case 'f':
p.cmd(s, b[:3], "GS f %d — fuente del texto legible (HRI)", b[2])
return 3
}
if n, ok := fixed[b[1]]; ok {
p.warnRaw(s, b[:n], "GS %c — sin efecto en el emulador", b[1])
return n
}
p.warnRaw(s, b[:2], "GS 0x%02X — comando desconocido", b[1])
return 2
}
func (p *Printer) gsCut(s *Session, b []byte) int {
if len(b) < 3 {
return 0
}
m := b[2]
n, feed := 3, 0
kind := ""
switch m {
case 0, '0':
kind = "total"
case 1, '1':
kind = "parcial"
case 65, 66, 97, 98, 103, 104:
if len(b) < 4 {
return 0
}
n = 4
kind = map[bool]string{true: "total", false: "parcial"}[m%2 == 1]
feed = p.profile.CutterOffset + int(b[3])
default:
p.warnRaw(s, b[:3], "GS V %d — modo de corte desconocido", m)
return 3
}
if n == 4 {
p.cmd(s, b[:n], "GS V %d %d — avanza hasta la cuchilla + %d puntos y corte %s", m, b[3], b[3], kind)
} else {
p.cmd(s, b[:n], "GS V %d — corte %s en la posición actual", m, kind)
}
p.doCut(s, kind, feed)
return n
}
func (p *Printer) doCut(s *Session, kind string, feed int) {
if !p.profile.HasCutter {
p.flushLine()
p.paper.grow(feed)
p.warn(s, "Este modelo no tiene cuchilla: el corte se ignora (usa «Arrancar papel»)")
return
}
if p.status.CutterError {
p.warn(s, "Error de cuchilla: no se corta")
return
}
p.cut(s, kind, feed)
}
// gsParenK procesa GS ( k pL pH cn fn [parámetros]: símbolos 2D.
func (p *Printer) gsParenK(s *Session, b []byte) {
if len(b) < 7 {
p.warnRaw(s, b, "GS ( k — demasiado corto")
return
}
cn, fn := b[5], b[6]
if cn != 49 {
p.warnRaw(s, b, "GS ( k cn=%d — símbolo 2D no emulado (solo QR, cn=49)", cn)
return
}
if !p.profile.NativeQR {
p.warnRaw(s, b, "GS ( k — este modelo no soporta QR nativo: comando ignorado")
return
}
switch fn {
case 65:
p.cmd(s, b, "GS ( k QR fn 65 — seleccionar modelo (siempre se usa el modelo 2)")
case 67:
if len(b) >= 8 && b[7] >= 1 && b[7] <= 16 {
p.st.qrModule = b[7]
}
p.cmd(s, b, "GS ( k QR fn 67 — tamaño de módulo: %d puntos", p.st.qrModule)
case 69:
if len(b) >= 8 && b[7] >= 48 && b[7] <= 51 {
p.st.qrECC = b[7]
}
p.cmd(s, b, "GS ( k QR fn 69 — corrección de errores %c", "LMQH"[p.st.qrECC-48])
case 80:
if len(b) >= 8 {
p.st.qrData = append([]byte(nil), b[8:]...)
}
p.cmd(s, b, "GS ( k QR fn 80 — almacenar %d bytes: «%s»", len(p.st.qrData), strings.ToValidUTF8(string(p.st.qrData), "?"))
case 81:
p.cmd(s, b, "GS ( k QR fn 81 — imprimir el QR almacenado")
p.printQR(s)
default:
p.warnRaw(s, b, "GS ( k QR fn %d — no emulado", fn)
}
}
func (p *Printer) gsBarcode(s *Session, b []byte) int {
if len(b) < 3 {
return 0
}
m := b[2]
var data []byte
total := 0
switch {
case m <= 6:
i := bytes.IndexByte(b[3:], nul)
if i < 0 {
if len(b) > 3+255 {
p.warnRaw(s, b[:3], "GS k %d — datos sin NUL final: se descarta", m)
return 3
}
return 0
}
data, total = b[3:3+i], 3+i+1
case m >= 65 && m <= 79:
if len(b) < 4 {
return 0
}
total = 4 + int(b[3])
if len(b) < total {
return 0
}
data = b[4:total]
default:
p.warnRaw(s, b[:3], "GS k %d — tipo de código de barras desconocido", m)
return 3
}
p.warnRaw(s, b[:total], "GS k %d — código de barras %s: se dibuja un marcador (no emulado)", m, barcodeNames[m])
p.printBarcode(m, data)
return total
}
func (p *Printer) gsI(s *Session, n byte) {
pr := p.profile
switch n {
case 1, 49:
p.respond(s, pr.ModelID)
return
case 2, 50:
p.respond(s, pr.TypeID)
return
case 3, 51:
p.respond(s, pr.VersionID)
return
}
info := map[byte]string{65: pr.Firmware, 66: pr.Maker, 67: pr.Model, 68: pr.Serial, 69: "ANK"}[n]
if info == "" || !pr.ExtendedGSI {
p.warn(s, "GS I %d — este modelo no responde (el host se quedará esperando hasta su timeout)", n)
return
}
resp := append([]byte{0x5F}, info...)
p.respond(s, append(resp, nul)...)
}
func (p *Printer) dleCommand(s *Session, b []byte) int {
if len(b) < 2 {
return 0
}
switch b[1] {
case 0x04:
if len(b) < 3 {
return 0
}
if b[2] == 7 || b[2] == 8 {
if len(b) < 4 {
return 0
}
p.warnRaw(s, b[:4], "DLE EOT %d — no emulado", b[2])
return 4
}
p.dleEOT(s, b[:3])
return 3
case 0x05:
if len(b) < 3 {
return 0
}
p.cmd(s, b[:3], "DLE ENQ %d — petición en tiempo real: sin efecto", b[2])
return 3
case 0x14:
if len(b) < 3 {
return 0
}
n := map[byte]int{1: 5, 2: 5, 7: 4, 8: 10}[b[2]]
if n == 0 {
n = 3
}
if len(b) < n {
return 0
}
p.dleDC4(s, b[:n])
return n
}
p.warnRaw(s, b[:1], "DLE suelto: ignorado")
return 1
}
var eotNames = map[byte]string{1: "estado de la impresora", 2: "causa de offline", 3: "causa del error", 4: "sensor de papel"}
func (p *Printer) dleEOT(s *Session, b []byte) {
n := b[2]
st := p.status
r := byte(0x12) // bits 1 y 4 fijos a 1
switch n {
case 1:
if st.DrawerOpen {
r |= 0x04
}
if st.Blocked() {
r |= 0x08
}
case 2:
if st.CoverOpen {
r |= 0x04
}
if st.PaperEnd {
r |= 0x20
}
if st.CoverOpen || st.PaperEnd || st.CutterError {
r |= 0x40
}
case 3:
if st.CutterError {
r |= 0x08
}
case 4:
if st.PaperNearEnd || st.PaperEnd {
r |= 0x0C
}
if st.PaperEnd {
r |= 0x60
}
default:
p.warnRaw(s, b, "DLE EOT %d — n no válido: sin respuesta", n)
return
}
p.cmd(s, b, "DLE EOT %d — tiempo real: %s", n, eotNames[n])
p.respond(s, r)
}
func (p *Printer) dleDC4(s *Session, b []byte) {
switch b[2] {
case 1:
p.status.DrawerOpen = true
p.sendASB()
p.cmd(s, b, "DLE DC4 1 — pulso al cajón en tiempo real → CAJÓN ABIERTO")
case 8:
for _, o := range p.sessions {
o.pending = nil
}
p.line = lineBuf{}
p.cmd(s, b, "DLE DC4 8 — borrar buffers")
default:
p.warnRaw(s, b, "DLE DC4 %d — no emulado", b[2])
}
}
// realtimeWhileBlocked ejecuta DLE EOT y DLE DC4 1 aunque la impresora esté
// detenida (como hace el firmware real) y devuelve el resto de bytes.
func (p *Printer) realtimeWhileBlocked(s *Session, data []byte) []byte {
rest := make([]byte, 0, len(data))
for i := 0; i < len(data); {
if data[i] == dle && i+2 < len(data) {
if data[i+1] == 0x04 && data[i+2] >= 1 && data[i+2] <= 4 {
p.dleEOT(s, data[i:i+3])
i += 3
continue
}
if data[i+1] == 0x14 && data[i+2] == 1 && i+4 < len(data) {
p.dleDC4(s, data[i:i+5])
i += 5
continue
}
}
rest = append(rest, data[i])
i++
}
return rest
}
func (p *Printer) fsCommand(s *Session, b []byte) int {
if len(b) < 2 {
return 0
}
n := map[byte]int{'.': 2, '&': 2, 'C': 3, '!': 3, '-': 3, 'W': 3, 'S': 4, 'p': 4}[b[1]]
if b[1] == '(' {
if len(b) < 5 {
return 0
}
n = 5 + (int(b[3]) | int(b[4])<<8)
}
if n == 0 {
n = 2
}
if len(b) < n {
return 0
}
switch b[1] {
case '.', '&':
p.cmd(s, b[:n], "FS %c — modo kanji %s: sin efecto", b[1], map[byte]string{'.': "desactivado", '&': "activado"}[b[1]])
default:
p.warnRaw(s, b[:n], "FS %c — no emulado", b[1])
}
return n
}
func (p *Printer) paperSensorByte() byte {
var r byte
if p.status.PaperNearEnd || p.status.PaperEnd {
r |= 0x03
}
if p.status.PaperEnd {
r |= 0x0C
}
return r
}
func (p *Printer) drawerByte() byte {
if p.status.DrawerOpen {
return 0x01
}
return 0x00
}
// sendASB envía los 4 bytes de estado automático si el host lo activó con GS a.
func (p *Printer) sendASB() {
if p.asbTo == nil || p.asbMask == 0 {
return
}
st := p.status
b1 := byte(0x10)
if st.DrawerOpen {
b1 |= 0x04
}
if st.Blocked() {
b1 |= 0x08
}
if st.CoverOpen {
b1 |= 0x20
}
var b2 byte
if st.CutterError {
b2 |= 0x08 | 0x40
}
var b3 byte
if st.PaperNearEnd || st.PaperEnd {
b3 |= 0x03
}
if st.PaperEnd {
b3 |= 0x0C
}
p.respond(p.asbTo, b1, b2, b3, 0x00)
}