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) }