486 lines
13 KiB
Go
486 lines
13 KiB
Go
// 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
|
|
}
|