778 lines
19 KiB
Go
778 lines
19 KiB
Go
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)
|
||
}
|