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
+485
View File
@@ -0,0 +1,485 @@
// Package usbip exporta la impresora como un dispositivo USB real mediante
// el protocolo USB/IP. Un cliente USB/IP (usbip-win2 en Windows, usbip en
// Linux) conecta el dispositivo al sistema operativo, que lo ve como una
// impresora USB más: clase 07 (impresora), subclase 01, protocolo 02
// (bidireccional), con un endpoint bulk OUT (EP 1) y uno bulk IN (EP 2).
//
// Protocolo: https://docs.kernel.org/usb/usbip_protocol.html
package usbip
import (
"bytes"
"encoding/binary"
"errors"
"io"
"net"
"sync"
"unicode/utf16"
"escpos-emulator/internal/escpos"
)
const (
protocolVersion = 0x0111
opReqDevlist = 0x8005
opRepDevlist = 0x0005
opReqImport = 0x8003
opRepImport = 0x0003
cmdSubmit = 1
cmdUnlink = 2
retSubmit = 3
retUnlink = 4
dirOut = 0
dirIn = 1
epBulkOut = 1
epBulkIn = 2
speedFull = 2
errPipe = -32 // -EPIPE: STALL
errConnReset = -104 // -ECONNRESET: URB cancelada
maxInBuffer = 4096
maxTransfer = 16 << 20
)
// BusID es el identificador con el que se conecta el dispositivo
// (usbip attach -r <host> -b 1-1).
const BusID = "1-1"
// Device describe la identidad USB del dispositivo.
type Device struct {
VendorID uint16
ProductID uint16
Manufacturer string
Product string
Serial string
IEEE1284ID string // respuesta a GET_DEVICE_ID de la clase impresora
}
// DeviceFromProfile construye la identidad USB a partir del perfil.
func DeviceFromProfile(p escpos.Profile) Device {
return Device{
VendorID: p.USBVendorID,
ProductID: p.USBProductID,
Manufacturer: p.Maker,
Product: p.Model,
Serial: p.Serial,
IEEE1284ID: "MFG:" + p.Maker + ";MDL:" + p.Model + ";CMD:ESC/POS;CLS:PRINTER;DES:" + p.Name + ";",
}
}
// Server atiende clientes USB/IP.
type Server struct {
ln net.Listener
dev Device
printer *escpos.Printer
mu sync.Mutex
attached bool
}
// Listen abre el puerto (normalmente 3240) y atiende en segundo plano.
func Listen(addr string, dev Device, p *escpos.Printer) (*Server, error) {
ln, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
s := &Server{ln: ln, dev: dev, printer: p}
go s.serve()
return s, nil
}
// Addr devuelve la dirección en la que escucha.
func (s *Server) Addr() net.Addr { return s.ln.Addr() }
// Close deja de aceptar conexiones.
func (s *Server) Close() error { return s.ln.Close() }
func (s *Server) serve() {
for {
conn, err := s.ln.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
continue
}
go s.handle(conn)
}
}
func (s *Server) handle(conn net.Conn) {
defer conn.Close()
for {
var h [8]byte
if _, err := io.ReadFull(conn, h[:]); err != nil {
return
}
switch binary.BigEndian.Uint16(h[2:]) {
case opReqDevlist:
reply := opHeader(opRepDevlist, 0)
reply = binary.BigEndian.AppendUint32(reply, 1)
reply = append(reply, s.deviceRecord()...)
reply = append(reply, 0x07, 0x01, 0x02, 0x00) // interfaz 0: clase, subclase, protocolo, relleno
if _, err := conn.Write(reply); err != nil {
return
}
case opReqImport:
var bus [32]byte
if _, err := io.ReadFull(conn, bus[:]); err != nil {
return
}
id := string(bytes.TrimRight(bus[:], "\x00"))
if id != BusID || !s.acquire() {
s.printer.Note("USB/IP: rechazada la conexión a %q desde %s (no existe o ya está conectado)", id, conn.RemoteAddr())
_, _ = conn.Write(opHeader(opRepImport, 1))
return
}
_, err := conn.Write(append(opHeader(opRepImport, 0), s.deviceRecord()...))
if err == nil {
s.runDevice(conn)
}
s.release()
return
default:
return
}
}
}
func (s *Server) acquire() bool {
s.mu.Lock()
defer s.mu.Unlock()
if s.attached {
return false
}
s.attached = true
return true
}
func (s *Server) release() {
s.mu.Lock()
s.attached = false
s.mu.Unlock()
}
func opHeader(code uint16, status uint32) []byte {
b := binary.BigEndian.AppendUint16(nil, protocolVersion)
b = binary.BigEndian.AppendUint16(b, code)
return binary.BigEndian.AppendUint32(b, status)
}
// deviceRecord es la estructura usbip_usb_device (312 bytes, big endian).
func (s *Server) deviceRecord() []byte {
b := make([]byte, 312)
copy(b[0:256], "/sys/devices/platform/escpos-emulator/usb1/"+BusID)
copy(b[256:288], BusID)
binary.BigEndian.PutUint32(b[288:], 1) // busnum
binary.BigEndian.PutUint32(b[292:], 2) // devnum
binary.BigEndian.PutUint32(b[296:], speedFull)
binary.BigEndian.PutUint16(b[300:], s.dev.VendorID)
binary.BigEndian.PutUint16(b[302:], s.dev.ProductID)
binary.BigEndian.PutUint16(b[304:], 0x0100) // bcdDevice
b[306], b[307], b[308] = 0, 0, 0 // la clase se declara en la interfaz
b[309] = 1 // bConfigurationValue
b[310] = 1 // bNumConfigurations
b[311] = 1 // bNumInterfaces
return b
}
// ---------------------------------------------------------------------------
// Dispositivo conectado: intercambio de URBs.
type inURB struct {
seq uint32
length int
numPackets uint32
}
type devConn struct {
srv *Server
conn net.Conn
session *escpos.Session
wmu sync.Mutex // serializa las escrituras en conn
mu sync.Mutex
pendingIn []inURB
inBuf []byte
config byte
}
func (s *Server) runDevice(conn net.Conn) {
d := &devConn{srv: s, conn: conn}
d.session = s.printer.NewSession("USB "+conn.RemoteAddr().String(), d.pushIn)
defer d.session.Close()
var h [48]byte
for {
if _, err := io.ReadFull(conn, h[:]); err != nil {
return
}
var err error
switch binary.BigEndian.Uint32(h[0:]) {
case cmdSubmit:
err = d.submit(h)
case cmdUnlink:
err = d.unlink(h)
default:
return
}
if err != nil {
return
}
}
}
func (d *devConn) submit(h [48]byte) error {
seq := binary.BigEndian.Uint32(h[4:])
dir := binary.BigEndian.Uint32(h[12:])
ep := binary.BigEndian.Uint32(h[16:])
length := binary.BigEndian.Uint32(h[24:])
numPackets := binary.BigEndian.Uint32(h[32:])
var setup [8]byte
copy(setup[:], h[40:48])
if length > maxTransfer {
return errors.New("usbip: transferencia demasiado grande")
}
var data []byte
if dir == dirOut && length > 0 {
data = make([]byte, length)
if _, err := io.ReadFull(d.conn, data); err != nil {
return err
}
}
switch {
case ep == 0:
return d.control(seq, numPackets, setup, data)
case ep == epBulkOut && dir == dirOut:
d.session.Write(data)
return d.reply(seq, numPackets, 0, len(data), nil)
case ep == epBulkIn && dir == dirIn:
// El host deja una lectura pendiente; se completa cuando la
// impresora tenga algo que enviar (mientras, el USB responde NAK).
d.mu.Lock()
d.pendingIn = append(d.pendingIn, inURB{seq: seq, length: int(length), numPackets: numPackets})
d.mu.Unlock()
return d.drainIn()
default:
return d.reply(seq, numPackets, errPipe, 0, nil)
}
}
func (d *devConn) unlink(h [48]byte) error {
seq := binary.BigEndian.Uint32(h[4:])
target := binary.BigEndian.Uint32(h[20:])
d.wmu.Lock()
defer d.wmu.Unlock()
d.mu.Lock()
var status int32
for i, u := range d.pendingIn {
if u.seq == target {
d.pendingIn = append(d.pendingIn[:i], d.pendingIn[i+1:]...)
status = errConnReset
break
}
}
d.mu.Unlock()
b := make([]byte, 48)
binary.BigEndian.PutUint32(b[0:], retUnlink)
binary.BigEndian.PutUint32(b[4:], seq)
binary.BigEndian.PutUint32(b[20:], uint32(status))
_, err := d.conn.Write(b)
return err
}
// pushIn recibe las respuestas de la impresora (DLE EOT, GS I...).
func (d *devConn) pushIn(b []byte) {
d.mu.Lock()
d.inBuf = append(d.inBuf, b...)
if over := len(d.inBuf) - maxInBuffer; over > 0 {
d.inBuf = d.inBuf[over:]
}
d.mu.Unlock()
_ = d.drainIn()
}
// drainIn completa lecturas bulk IN pendientes con los datos disponibles.
func (d *devConn) drainIn() error {
d.wmu.Lock()
defer d.wmu.Unlock()
for {
d.mu.Lock()
if len(d.pendingIn) == 0 || len(d.inBuf) == 0 {
d.mu.Unlock()
return nil
}
u := d.pendingIn[0]
d.pendingIn = d.pendingIn[1:]
n := min(u.length, len(d.inBuf))
chunk := append([]byte(nil), d.inBuf[:n]...)
d.inBuf = d.inBuf[n:]
d.mu.Unlock()
if _, err := d.conn.Write(retSubmitPacket(u.seq, u.numPackets, 0, n, chunk)); err != nil {
return err
}
}
}
func (d *devConn) reply(seq, numPackets uint32, status int32, actual int, data []byte) error {
d.wmu.Lock()
defer d.wmu.Unlock()
_, err := d.conn.Write(retSubmitPacket(seq, numPackets, status, actual, data))
return err
}
func retSubmitPacket(seq, numPackets uint32, status int32, actual int, data []byte) []byte {
b := make([]byte, 48, 48+len(data))
binary.BigEndian.PutUint32(b[0:], retSubmit)
binary.BigEndian.PutUint32(b[4:], seq)
binary.BigEndian.PutUint32(b[20:], uint32(status))
binary.BigEndian.PutUint32(b[24:], uint32(actual))
// Igual que el servidor de Linux: se devuelve el mismo number_of_packets
// que envió el cliente (0 o 0xFFFFFFFF en transferencias no isócronas).
binary.BigEndian.PutUint32(b[32:], numPackets)
return append(b, data...)
}
// ---------------------------------------------------------------------------
// Endpoint 0: peticiones de control.
func (d *devConn) control(seq, numPackets uint32, setup [8]byte, data []byte) error {
reqType, req := setup[0], setup[1]
value := binary.LittleEndian.Uint16(setup[2:])
wLength := int(binary.LittleEndian.Uint16(setup[6:]))
var resp []byte
ok := false
switch reqType & 0x60 {
case 0x00:
resp, ok = d.standardRequest(reqType, req, value)
case 0x20:
resp, ok = d.classRequest(reqType, req)
}
if !ok {
return d.reply(seq, numPackets, errPipe, 0, nil)
}
if reqType&0x80 == 0 {
return d.reply(seq, numPackets, 0, len(data), nil)
}
if len(resp) > wLength {
resp = resp[:wLength]
}
return d.reply(seq, numPackets, 0, len(resp), resp)
}
func (d *devConn) standardRequest(reqType, req byte, value uint16) ([]byte, bool) {
switch req {
case 0x00: // GET_STATUS
if reqType&0x1F == 0 {
return []byte{0x01, 0x00}, true // autoalimentado
}
return []byte{0x00, 0x00}, true
case 0x01, 0x03, 0x05: // CLEAR_FEATURE, SET_FEATURE, SET_ADDRESS
return nil, true
case 0x06: // GET_DESCRIPTOR
return d.srv.dev.descriptor(byte(value>>8), byte(value))
case 0x08: // GET_CONFIGURATION
d.mu.Lock()
defer d.mu.Unlock()
return []byte{d.config}, true
case 0x09: // SET_CONFIGURATION
d.mu.Lock()
d.config = byte(value)
d.mu.Unlock()
return nil, true
case 0x0A: // GET_INTERFACE
return []byte{0x00}, true
case 0x0B: // SET_INTERFACE
return nil, value == 0
}
return nil, false
}
// classRequest atiende las peticiones de la clase impresora USB 1.1.
func (d *devConn) classRequest(reqType, req byte) ([]byte, bool) {
switch {
case reqType == 0xA1 && req == 0: // GET_DEVICE_ID
id := d.srv.dev.IEEE1284ID
n := len(id) + 2
return append([]byte{byte(n >> 8), byte(n)}, id...), true
case reqType == 0xA1 && req == 1: // GET_PORT_STATUS
st := d.srv.printer.Status()
v := byte(0x18) // bit 3: sin error, bit 4: seleccionada
if st.Blocked() {
v &^= 0x08
}
if st.Offline {
v &^= 0x10
}
if st.PaperEnd {
v |= 0x20
}
return []byte{v}, true
case reqType == 0x21 && req == 2: // SOFT_RESET
return nil, true
}
return nil, false
}
// descriptor devuelve los descriptores USB estándar (little endian).
func (dev Device) descriptor(typ, index byte) ([]byte, bool) {
switch typ {
case 1: // dispositivo
return []byte{
18, 1, 0x00, 0x02, // bLength, DEVICE, bcdUSB 2.00
0, 0, 0, 64, // clase/subclase/protocolo en la interfaz, bMaxPacketSize0
byte(dev.VendorID), byte(dev.VendorID >> 8),
byte(dev.ProductID), byte(dev.ProductID >> 8),
0x00, 0x01, // bcdDevice 1.00
1, 2, 3, // iManufacturer, iProduct, iSerialNumber
1, // bNumConfigurations
}, true
case 2: // configuración + interfaz + endpoints
if index != 0 {
return nil, false
}
return []byte{
9, 2, 32, 0, 1, 1, 0, 0xC0, 1, // CONFIGURATION: wTotalLength 32, 1 interfaz, autoalimentado
9, 4, 0, 0, 2, 0x07, 0x01, 0x02, 0, // INTERFACE: 2 endpoints, impresora bidireccional
7, 5, 0x01, 0x02, 64, 0, 0, // ENDPOINT 1 OUT, bulk, 64 bytes
7, 5, 0x82, 0x02, 64, 0, 0, // ENDPOINT 2 IN, bulk, 64 bytes
}, true
case 3: // cadenas
switch index {
case 0:
return []byte{4, 3, 0x09, 0x04}, true // idioma: inglés EE.UU.
case 1:
return stringDescriptor(dev.Manufacturer), true
case 2:
return stringDescriptor(dev.Product), true
case 3:
return stringDescriptor(dev.Serial), true
}
}
// DEVICE_QUALIFIER, BOS...: un dispositivo full speed USB 2.00 los rechaza con STALL.
return nil, false
}
func stringDescriptor(s string) []byte {
u := utf16.Encode([]rune(s))
b := []byte{byte(2 + 2*len(u)), 3}
for _, c := range u {
b = append(b, byte(c), byte(c>>8))
}
return b
}
+185
View File
@@ -0,0 +1,185 @@
package usbip
import (
"bytes"
"encoding/binary"
"io"
"net"
"testing"
"time"
"escpos-emulator/internal/escpos"
)
func startServer(t *testing.T) (*Server, *escpos.Printer) {
t.Helper()
prof, _ := escpos.FindProfile("80mm")
p, err := escpos.NewPrinter(prof)
if err != nil {
t.Fatal(err)
}
s, err := Listen("127.0.0.1:0", DeviceFromProfile(prof), p)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { s.Close() })
return s, p
}
func dial(t *testing.T, s *Server) net.Conn {
t.Helper()
c, err := net.Dial("tcp", s.Addr().String())
if err != nil {
t.Fatal(err)
}
_ = c.SetDeadline(time.Now().Add(5 * time.Second))
t.Cleanup(func() { c.Close() })
return c
}
func readN(t *testing.T, c net.Conn, n int) []byte {
t.Helper()
b := make([]byte, n)
if _, err := io.ReadFull(c, b); err != nil {
t.Fatalf("leyendo %d bytes: %v", n, err)
}
return b
}
func TestDevlist(t *testing.T) {
s, _ := startServer(t)
c := dial(t, s)
c.Write([]byte{0x01, 0x11, 0x80, 0x05, 0, 0, 0, 0})
h := readN(t, c, 12)
if binary.BigEndian.Uint16(h[2:]) != opRepDevlist || binary.BigEndian.Uint32(h[8:]) != 1 {
t.Fatalf("cabecera DEVLIST inesperada: % X", h)
}
dev := readN(t, c, 312+4)
if got := string(bytes.TrimRight(dev[256:288], "\x00")); got != BusID {
t.Fatalf("busid = %q", got)
}
if !bytes.Equal(dev[312:], []byte{7, 1, 2, 0}) {
t.Fatalf("interfaz = % X, want clase impresora 07 01 02", dev[312:])
}
}
type client struct {
t *testing.T
c net.Conn
seq uint32
isIn map[uint32]bool
}
func attach(t *testing.T, s *Server) *client {
c := dial(t, s)
req := []byte{0x01, 0x11, 0x80, 0x03, 0, 0, 0, 0}
bus := make([]byte, 32)
copy(bus, BusID)
c.Write(append(req, bus...))
h := readN(t, c, 8)
if binary.BigEndian.Uint32(h[4:]) != 0 {
t.Fatalf("IMPORT rechazado: % X", h)
}
readN(t, c, 312)
return &client{t: t, c: c, isIn: map[uint32]bool{}}
}
func (cl *client) submit(dir, ep uint32, length int, setup []byte, data []byte) uint32 {
cl.seq++
cl.isIn[cl.seq] = dir == dirIn
h := make([]byte, 48)
binary.BigEndian.PutUint32(h[0:], cmdSubmit)
binary.BigEndian.PutUint32(h[4:], cl.seq)
binary.BigEndian.PutUint32(h[8:], 0x00010002)
binary.BigEndian.PutUint32(h[12:], dir)
binary.BigEndian.PutUint32(h[16:], ep)
binary.BigEndian.PutUint32(h[24:], uint32(length))
binary.BigEndian.PutUint32(h[32:], 0xFFFFFFFF)
copy(h[40:], setup)
cl.c.Write(append(h, data...))
return cl.seq
}
func (cl *client) ret() (seq uint32, status int32, data []byte) {
h := readN(cl.t, cl.c, 48)
seq = binary.BigEndian.Uint32(h[4:])
status = int32(binary.BigEndian.Uint32(h[20:]))
if n := int(binary.BigEndian.Uint32(h[24:])); cl.isIn[seq] && n > 0 {
data = readN(cl.t, cl.c, n)
}
return
}
func TestControlAndBulk(t *testing.T) {
s, p := startServer(t)
cl := attach(t, s)
// GET_DESCRIPTOR(DEVICE)
cl.submit(dirIn, 0, 18, []byte{0x80, 6, 0, 1, 0, 0, 18, 0}, nil)
_, st, d := cl.ret()
if st != 0 || len(d) != 18 || d[1] != 1 {
t.Fatalf("descriptor de dispositivo: status %d, % X", st, d)
}
if vid := binary.LittleEndian.Uint16(d[8:]); vid != 0x1209 {
t.Fatalf("VID = %04X", vid)
}
// GET_DESCRIPTOR(CONFIGURATION) con wLength 9: solo la cabecera
cl.submit(dirIn, 0, 9, []byte{0x80, 6, 0, 2, 0, 0, 9, 0}, nil)
_, _, d = cl.ret()
if len(d) != 9 || d[2] != 32 {
t.Fatalf("configuración: % X", d)
}
// GET_DEVICE_ID de la clase impresora
cl.submit(dirIn, 0, 1024, []byte{0xA1, 0, 0, 0, 0, 0, 0, 4}, nil)
_, _, d = cl.ret()
if !bytes.Contains(d, []byte("CMD:ESC/POS")) {
t.Fatalf("GET_DEVICE_ID = %q", d)
}
// DEVICE_QUALIFIER debe dar STALL
cl.submit(dirIn, 0, 10, []byte{0x80, 6, 0, 6, 0, 0, 10, 0}, nil)
if _, st, _ = cl.ret(); st != errPipe {
t.Fatalf("DEVICE_QUALIFIER status = %d, want %d", st, errPipe)
}
// Lectura bulk IN pendiente antes de que haya datos
inSeq := cl.submit(dirIn, epBulkIn, 64, nil, nil)
// Bulk OUT: texto + DLE EOT 1
payload := []byte("\x1b@Hola USB\n\x10\x04\x01")
outSeq := cl.submit(dirOut, epBulkOut, len(payload), nil, payload)
got := map[uint32][]byte{}
for i := 0; i < 2; i++ {
seq, st, data := cl.ret()
if st != 0 {
t.Fatalf("seq %d status %d", seq, st)
}
got[seq] = data
}
if !bytes.Equal(got[inSeq], []byte{0x12}) {
t.Fatalf("bulk IN = % X, want 12", got[inSeq])
}
if _, ok := got[outSeq]; !ok {
t.Fatal("falta la respuesta del bulk OUT")
}
// UNLINK de una lectura pendiente
pending := cl.submit(dirIn, epBulkIn, 64, nil, nil)
cl.seq++
u := make([]byte, 48)
binary.BigEndian.PutUint32(u[0:], cmdUnlink)
binary.BigEndian.PutUint32(u[4:], cl.seq)
binary.BigEndian.PutUint32(u[20:], pending)
cl.c.Write(u)
h := readN(t, cl.c, 48)
if binary.BigEndian.Uint32(h[0:]) != retUnlink || int32(binary.BigEndian.Uint32(h[20:])) != errConnReset {
t.Fatalf("RET_UNLINK inesperado: % X", h)
}
if len(p.Snapshot(0).Connections) != 1 {
t.Fatal("debería haber una conexión USB activa")
}
}