Manual Completo para Programar Drivers de Impresora en Linux

Este manual cubre desde los conceptos básicos hasta la implementación avanzada de drivers de impresora para Linux, compatible con cualquier modelo de impresora.

1. Arquitectura de Impresión en Linux

1.1 Componentes clave

1.2 Flujo de trabajo de impresión

  1. Aplicación genera documento (PostScript, PDF, etc.)
  2. CUPS procesa el documento usando filtros
  3. El driver convierte a lenguaje de impresora (PCL, ESC/P, etc.)
  4. Datos se envían a la impresora vía USB, red o puerto paralelo

2. Tipos de Drivers de Impresora

2.1 Drivers basados en filtros

# Ejemplo de filtro básico (script shell)
#!/bin/sh
# Filtro para impresora genérica

# Leer cabecera CUPS
read -r job user title copies options

# Procesar cada copia
for ((i=1; i<=$copies; i++))
do
    # Convertir PostScript a lenguaje de impresora
    gs -q -dNOPAUSE -dBATCH -sDEVICE="pxlmono" \
       -sOutputFile=- - | sed 's/^/PRINT:/'
done

# Enviar código de finalización
echo -e "\x1B\x45"  # ESC E = Reset printer

exit 0

2.2 Drivers en C para impresoras específicas

/* Ejemplo básico de driver en C para impresora térmica */
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[]) {
    int fd = open("/dev/usb/lp0", O_WRONLY);
    if (fd < 0) {
        perror("Error al abrir dispositivo");
        return 1;
    }
    
    // Comandos de inicialización (ESC/POS)
    char init_cmds[] = {0x1B, 0x40}; // Reset
    write(fd, init_cmds, sizeof(init_cmds));
    
    // Escribir texto
    dprintf(fd, "Factura #1234\nProducto\tCantidad\n");
    
    // Cortar papel (si la impresora lo soporta)
    char cut_cmd[] = {0x1D, 0x56, 0x00}; // Comando de corte
    write(fd, cut_cmd, sizeof(cut_cmd));
    
    close(fd);
    return 0;
}

3. Implementación de un Driver Básico

3.1 Estructura del driver

// Estructura básica de un driver de impresora en C
#include <linux/module.h>
#include <linux/usb.h>
#include <linux/usb/ch9.h>

// Estructura para nuestra impresora
struct usb_printer {
    struct usb_device *udev;
    struct usb_interface *interface;
    unsigned char *bulk_out_buffer;
    size_t bulk_out_size;
    int bulk_out_endpointAddr;
};

// Tabla de dispositivos soportados
static struct usb_device_id id_table[] = {
    { USB_DEVICE(0x04F9, 0x0042) }, // Brother HL-2140
    { } /* Terminador */
};
MODULE_DEVICE_TABLE(usb, id_table);

// Funciones del driver
static int printer_probe(struct usb_interface *interface,
              const struct usb_device_id *id);
static void printer_disconnect(struct usb_interface *interface);

// Estructura principal del driver USB
static struct usb_driver printer_driver = {
    .name = "usb_printer",
    .probe = printer_probe,
    .disconnect = printer_disconnect,
    .id_table = id_table,
};

// Registro del módulo
module_usb_driver(printer_driver);

3.2 Funciones principales

// Función probe - llamada cuando se conecta una impresora compatible
static int printer_probe(struct usb_interface *interface,
              const struct usb_device_id *id) {
    struct usb_device *udev = interface_to_usbdev(interface);
    struct usb_printer *dev;
    int retval = -ENOMEM;
    
    // Asignar memoria para nuestra estructura
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;
    
    dev->udev = udev;
    dev->interface = interface;
    
    // Configurar endpoints USB
    struct usb_host_interface *iface_desc = interface->cur_altsetting;
    struct usb_endpoint_descriptor *endpoint;
    
    for (int i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
        endpoint = &iface_desc->endpoint[i].desc;
        
        if (usb_endpoint_is_bulk_out(endpoint)) {
            dev->bulk_out_size = usb_endpoint_maxp(endpoint);
            dev->bulk_out_endpointAddr = endpoint->bEndpointAddress;
            
            // Asignar buffer para transferencias
            dev->bulk_out_buffer = kmalloc(dev->bulk_out_size, GFP_KERNEL);
            if (!dev->bulk_out_buffer) {
                kfree(dev);
                return -ENOMEM;
            }
        }
    }
    
    // Guardar nuestra estructura en la interfaz
    usb_set_intfdata(interface, dev);
    
    printk(KERN_INFO "Impresora USB conectada\n");
    return 0;
}

// Función disconnect - llamada al desconectar la impresora
static void printer_disconnect(struct usb_interface *interface) {
    struct usb_printer *dev = usb_get_intfdata(interface);
    
    // Liberar recursos
    kfree(dev->bulk_out_buffer);
    kfree(dev);
    
    printk(KERN_INFO "Impresora USB desconectada\n");
}

4. Comunicación con la Impresora

4.1 Lenguajes de impresora comunes

Lenguaje Descripción Ejemplo de Comando
ESC/P Epson Standard Code for Printers \x1B\x40 (Reset)
PCL Printer Command Language (HP) \x1B\x45 (Bold on)
PostScript Lenguaje de descripción de página %!PS\nshowpage
ESC/POS Para impresoras térmicas \x1D\x56\x00 (Cortar papel)

4.2 Envío de datos a la impresora

// Función para enviar datos a la impresora
static ssize_t printer_write(struct file *file, const char __user *user_buf,
                        size_t count, loff_t *ppos) {
    struct usb_printer *dev = file->private_data;
    int retval;
    int bytes_written = 0;
    
    // Verificar que la impresora esté conectada
    if (!dev->udev)
        return -ENODEV;
    
    // Copiar datos del espacio de usuario al kernel
    if (copy_from_user(dev->bulk_out_buffer, user_buf, count))
        return -EFAULT;
    
    // Enviar datos a la impresora
    retval = usb_bulk_msg(dev->udev,
                usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),
                dev->bulk_out_buffer, count, &bytes_written, 5000);
                
    if (retval)
        printk(KERN_ERR "Error al enviar datos: %d\n", retval);
    
    return bytes_written;
}

5. Integración con CUPS

5.1 Crear un PPD personalizado

*% Generic PostScript Printer Driver
*% Manufacturer: MyPrinter
*% ModelName: Generic PS
*% NickName: My Printer
*% FileVersion: 1.0

*PCFileName: "myprinter.ppd"
*LanguageVersion: English
*LanguageEncoding: ISOLatin1
*ColorDevice: False

*OpenUI *PageSize/Page Size: PickOne
*OrderDependency: 10 AnySetup *PageSize
*DefaultPageSize: Letter
*PageSize Letter/US Letter: "
    *<</PageSize[612 792]>>setpagedevice
*End
*PageSize A4/A4: "
    *<</PageSize[595 842]>>setpagedevice
*End

*DefaultFont: Courier
*Font AvantGarde-Book: Standard "(001.006)" Standard ROM
*Font Courier: Standard "(001.004)" Standard ROM

*% Filtro para nuestro driver
*cupsFilter: "application/vnd.cups-postscript 0 myprinter_filter"

5.2 Configurar el filtro CUPS

Instalación del driver en CUPS

1. Crear archivo de filtro en /usr/lib/cups/filter/myprinter_filter:

#!/bin/bash

# Leer cabecera CUPS
read -r job user title copies options

# Convertir PostScript a lenguaje de impresora
if [ "$1" = "0" ]; then
    # Usar Ghostscript para conversión
    exec gs -q -dNOPAUSE -dBATCH -sDEVICE="pxlmono" \
           -sOutputFile=- -
else
    # Opción directa para impresoras PostScript
    cat
fi

2. Hacer ejecutable el filtro:

sudo chmod +x /usr/lib/cups/filter/myprinter_filter

3. Instalar el PPD:

sudo cp myprinter.ppd /usr/share/cups/model/

4. Agregar la impresora:

sudo lpadmin -p MyPrinter -m myprinter.ppd -v usb://MyPrinter/ -E

6. Manejo de Errores y Depuración

6.1 Comandos útiles para depuración

lsusb

Listar dispositivos USB conectados (ver ID de impresora)

usb_printer_info /dev/usb/lp0

Obtener información técnica de la impresora

tail -f /var/log/cups/error_log

Monitorear errores de CUPS en tiempo real

sudo cupsfilter -p myprinter.ppd -m application/pdf file.pdf > output.prn

Probar el filtro sin imprimir

6.2 Manejo de errores en el driver

// Función mejorada con manejo de errores
static ssize_t printer_write(struct file *file, const char __user *user_buf,
                        size_t count, loff_t *ppos) {
    struct usb_printer *dev = file->private_data;
    int retval;
    int bytes_written = 0;
    
    // Verificar parámetros
    if (!dev || !user_buf || count == 0)
        return -EINVAL;
    
    if (!dev->udev)
        return -ENODEV;
    
    if (count > dev->bulk_out_size)
        count = dev->bulk_out_size;
    
    // Copiar datos de manera segura
    if (copy_from_user(dev->bulk_out_buffer, user_buf, count))
        return -EFAULT;
    
    // Enviar en bloques para impresoras lentas
    while (bytes_written < count) {
        int chunk_size = min(count - bytes_written, dev->bulk_out_size);
        
        retval = usb_bulk_msg(dev->udev,
                    usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),
                    dev->bulk_out_buffer + bytes_written, 
                    chunk_size, &chunk_size, 5000);
                    
        if (retval) {
            printk(KERN_ERR "Error al enviar datos: %d\n", retval);
            break;
        }
        
        bytes_written += chunk_size;
    }
    
    return bytes_written ? bytes_written : retval;
}

7. Optimización del Driver

7.1 Bufferización de datos

// Estructura extendida con buffer circular
struct usb_printer {
    struct usb_device *udev;
    struct usb_interface *interface;
    
    // Buffers para transferencia
    unsigned char *bulk_out_buffer;
    size_t bulk_out_size;
    int bulk_out_endpointAddr;
    
    // Buffer circular
    struct circular_buffer {
        unsigned char *data;
        size_t size;
        size_t head;
        size_t tail;
        spinlock_t lock;
    } print_buffer;
    
    // Workqueue para envío asíncrono
    struct work_struct printer_work;
};

// Función para agregar datos al buffer
static int buffer_write(struct usb_printer *dev, 
                      const char *data, size_t count) {
    unsigned long flags;
    size_t space_avail;
    
    spin_lock_irqsave(&dev->print_buffer.lock, flags);
    
    space_avail = dev->print_buffer.size - 
                 ((dev->print_buffer.head - dev->print_buffer.tail) % 
                  dev->print_buffer.size) - 1;
                  
    if (space_avail < count) {
        spin_unlock_irqrestore(&dev->print_buffer.lock, flags);
        return -ENOSPC; // Buffer lleno
    }
    
    for (size_t i = 0; i < count; i++) {
        dev->print_buffer.data[dev->print_buffer.head] = data[i];
        dev->print_buffer.head = 
            (dev->print_buffer.head + 1) % dev->print_buffer.size;
    }
    
    spin_unlock_irqrestore(&dev->print_buffer.lock, flags);
    
    // Programar trabajo de envío
    schedule_work(&dev->printer_work);
    
    return count;
}

7.2 Envío asíncrono

// Workqueue handler para envío asíncrono
static void printer_work_handler(struct work_struct *work) {
    struct usb_printer *dev = container_of(work, struct usb_printer, printer_work);
    unsigned long flags;
    size_t bytes_to_send;
    int retval;
    
    spin_lock_irqsave(&dev->print_buffer.lock, flags);
    
    // Calcular cantidad de datos a enviar
    bytes_to_send = (dev->print_buffer.head - dev->print_buffer.tail) % 
                   dev->print_buffer.size;
                   
    if (bytes_to_send == 0) {
        spin_unlock_irqrestore(&dev->print_buffer.lock, flags);
        return;
    }
    
    // Copiar datos al buffer de transferencia USB
    if (dev->print_buffer.tail + bytes_to_send <= dev->print_buffer.size) {
        memcpy(dev->bulk_out_buffer, 
               dev->print_buffer.data + dev->print_buffer.tail,
               bytes_to_send);
    } else {
        size_t first_chunk = dev->print_buffer.size - dev->print_buffer.tail;
        memcpy(dev->bulk_out_buffer,
               dev->print_buffer.data + dev->print_buffer.tail,
               first_chunk);
        memcpy(dev->bulk_out_buffer + first_chunk,
               dev->print_buffer.data,
               bytes_to_send - first_chunk);
    }
    
    dev->print_buffer.tail = 
        (dev->print_buffer.tail + bytes_to_send) % dev->print_buffer.size;
    
    spin_unlock_irqrestore(&dev->print_buffer.lock, flags);
    
    // Enviar datos
    retval = usb_bulk_msg(dev->udev,
                usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),
                dev->bulk_out_buffer, bytes_to_send, NULL, 5000);
                
    if (retval)
        printk(KERN_ERR "Error en envío asíncrono: %d\n", retval);
}

8. Ejemplo Completo: Driver para Impresora Térmica

8.1 Implementación completa

/* Driver para impresora térmica ESC/POS */
#include <linux/module.h>
#include <linux/usb.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

// Comandos ESC/POS
#define ESC 0x1B
#define GS  0x1D
#define RESET_PRINTER    {ESC, 0x40}
#define CUT_PAPER        {GS, 0x56, 0x00}
#define SET_LINE_SPACING {ESC, 0x33, 24} // 24/180 pulgadas

struct thermal_printer {
    struct usb_device *udev;
    struct usb_interface *interface;
    unsigned char *bulk_out_buffer;
    size_t bulk_out_size;
    int bulk_out_endpointAddr;
    struct mutex lock; // Para acceso concurrente
};

// Dispositivos soportados
static struct usb_device_id id_table[] = {
    { USB_DEVICE(0x0416, 0x5011) }, // WinPOS thermal printer
    { USB_DEVICE(0x04b8, 0x0e03) }, // Epson TM-T88V
    { } /* Terminador */
};
MODULE_DEVICE_TABLE(usb, id_table);

// Funciones del driver
static int thermal_probe(struct usb_interface *interface,
              const struct usb_device_id *id);
static void thermal_disconnect(struct usb_interface *interface);
static int thermal_open(struct inode *inode, struct file *file);
static ssize_t thermal_write(struct file *file, const char __user *user_buf,
                        size_t count, loff_t *ppos);
static long thermal_ioctl(struct file *file, unsigned int cmd,
                       unsigned long arg);

// Operaciones de archivo
static const struct file_operations thermal_fops = {
    .owner = THIS_MODULE,
    .open = thermal_open,
    .write = thermal_write,
    .unlocked_ioctl = thermal_ioctl,
};

// Estructura principal del driver USB
static struct usb_driver thermal_driver = {
    .name = "thermal_printer",
    .probe = thermal_probe,
    .disconnect = thermal_disconnect,
    .id_table = id_table,
};

// Inicialización del módulo
module_usb_driver(thermal_driver);

// Implementación de funciones
static int thermal_probe(struct usb_interface *interface,
              const struct usb_device_id *id) {
    struct usb_device *udev = interface_to_usbdev(interface);
    struct thermal_printer *dev;
    struct usb_host_interface *iface_desc;
    struct usb_endpoint_descriptor *endpoint;
    int retval = -ENOMEM;
    
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;
    
    dev->udev = udev;
    dev->interface = interface;
    mutex_init(&dev->lock);
    
    // Configurar endpoint bulk out
    iface_desc = interface->cur_altsetting;
    for (int i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
        endpoint = &iface_desc->endpoint[i].desc;
        
        if (usb_endpoint_is_bulk_out(endpoint)) {
            dev->bulk_out_size = usb_endpoint_maxp(endpoint);
            dev->bulk_out_endpointAddr = endpoint->bEndpointAddress;
            dev->bulk_out_buffer = kmalloc(dev->bulk_out_size, GFP_KERNEL);
            if (!dev->bulk_out_buffer)
                goto error;
        }
    }
    
    // Inicializar impresora
    unsigned char init_cmds[] = RESET_PRINTER;
    int bytes_sent;
    
    retval = usb_bulk_msg(udev,
                usb_sndbulkpipe(udev, dev->bulk_out_endpointAddr),
                init_cmds, sizeof(init_cmds), &bytes_sent, 1000);
    if (retval)
        printk(KERN_WARNING "No se pudo inicializar impresora\n");
    
    usb_set_intfdata(interface, dev);
    printk(KERN_INFO "Impresora térmica conectada\n");
    return 0;
    
error:
    if (dev->bulk_out_buffer)
        kfree(dev->bulk_out_buffer);
    kfree(dev);
    return retval;
}

// Implementación completa de thermal_write, thermal_ioctl, etc...
// (similar a ejemplos anteriores pero con comandos ESC/POS)

8.2 Configuración e instalación

Compilación y carga del driver

1. Crear Makefile:

obj-m := thermal_printer.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

2. Compilar:

make

3. Cargar módulo:

sudo insmod thermal_printer.ko

4. Verificar carga:

dmesg | tail
lsmod | grep thermal

5. Probar impresión:

echo "Test print" | sudo tee /dev/thermal0

9. Integración con Aplicaciones de Usuario

9.1 Comunicación desde espacio de usuario

/* Ejemplo de aplicación para imprimir factura */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

// Comandos ESC/POS
void print_header(int fd) {
    char cmd[] = {0x1B, 0x21, 0x30}; // Texto grande
    write(fd, cmd, sizeof(cmd));
    dprintf(fd, "MI EMPRESA\n");
    
    cmd[2] = 0x00; // Reset tamaño
    write(fd, cmd, sizeof(cmd));
    dprintf(fd, "Factura #1234\n\n");
}

int main() {
    int fd = open("/dev/thermal0", O_WRONLY);
    if (fd < 0) {
        perror("Error al abrir impresora");
        return 1;
    }
    
    print_header(fd);
    dprintf(fd, "Producto\tCant\tPrecio\n");
    dprintf(fd, "----------------------------\n");
    dprintf(fd, "Laptop\t1\t$1200.00\n");
    dprintf(fd, "Mouse\t2\t$20.00\n");
    dprintf(fd, "----------------------------\n");
    dprintf(fd, "Total:\t\t$1240.00\n\n");
    
    // Cortar papel
    char cut_cmd[] = {0x1D, 0x56, 0x00};
    write(fd, cut_cmd, sizeof(cut_cmd));
    
    close(fd);
    return 0;
}

9.2 Uso con CUPS

Configurar filtro CUPS para impresora térmica

1. Crear archivo de filtro /usr/lib/cups/filter/thermal_filter:

#!/bin/bash

# Leer cabecera CUPS
read -r job user title copies options

# Convertir a texto simple y añadir comandos ESC/POS
{
    # Reset printer
    echo -ne "\x1B\x40"
    
    # Procesar cada copia
    for ((i=1; i<=$copies; i++)); do
        # Convertir PostScript/PDF a texto
        if [[ "$1" = "application/pdf" ]]; then
            pdftotext -layout - -
        elif [[ "$1" = "application/postscript" ]]; then
            pstotext -
        else
            cat -
        fi
    done
    
    # Corte de papel al final
    echo -ne "\x1D\x56\x00"
} > /dev/thermal0

exit 0

2. Crear archivo PPD básico:

*% Basic thermal printer
*PPD-Adobe: "4.3"
*FormatVersion: "4.3"
*FileVersion: "1.0"
*LanguageVersion: English
*Manufacturer: "Generic"
*Product: "(Thermal Printer)"
*ModelName: "Generic Thermal"
*ShortNickName: "Thermal Printer"
*NickName: "Generic Thermal Printer"
*PCFileName: "thermal.ppd"
*LanguageEncoding: ISOLatin1
*ColorDevice: False

*DefaultPageSize: 80mm
*PageSize 80mm/80mm: "
    *<</PageSize[280 10000]>>setpagedevice
*End

*cupsFilter: "application/pdf 0 thermal_filter"
*cupsFilter: "application/postscript 0 thermal_filter"
*cupsFilter: "text/plain 0 thermal_filter"

10. Recursos y Referencias

10.1 Documentación oficial

10.2 Herramientas útiles

Este manual cubre los aspectos esenciales para desarrollar drivers de impresora en Linux, desde control básico hasta integración avanzada con CUPS. Para implementaciones específicas, consulta la documentación del fabricante de la impresora.