initial commit

This commit is contained in:
2026-09-30 10:22:42 +02:00
commit 3b0f5e365f
32 changed files with 4498 additions and 0 deletions
+777
View File
@@ -0,0 +1,777 @@
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)
}