Testar impressão sem ter a impressora
Quem desenvolve PDV, delivery, comanda de cozinha ou emissor de cupom conhece o ciclo: muda o layout, manda imprimir, confere, rasga a bobina, repete. Com a impressora na mesa, isso já é lento. Sem ela, o teste simplesmente não acontece.
- Home office sem impressora. A térmica fica na loja ou no escritório, e o código de impressão vira o único trecho que você não consegue testar de casa.
- Bobina gasta em teste. Cada ajuste de alinhamento ou de quebra de linha custa papel e tempo.
- Loja com várias impressoras. Caixa, cozinha e bar recebem cupons diferentes, e simular isso com uma impressora só, ou nenhuma, não dá.
- Erro descoberto pelo cliente. Cupom cortado, item fora de ordem ou pedido saindo na impressora errada só aparecem na implantação.
A solução: uma impressora de mentira na sua rede
O printer.sh transforma o seu computador numa impressora térmica de rede de mentira. Ele cria um IP próprio na LAN, escuta a porta 9100 como uma impressora ESC/POS (Epson, Elgin, Bematech e similares) e mostra no terminal, em tempo real, o texto que iria para o papel.
Para a aplicação, a conexão é indistinguível da real: TCP puro na porta 9100, num IP que existe de verdade na rede, atendendo uma conexão por vez. O que muda é só a ponta final: em vez da cabeça térmica, os bytes chegam a um filtro que limpa os comandos e escreve o texto na tela.
Na prática, isso resolve cada ponto da lista acima:
- Sem papel e sem hardware. Cada ajuste de layout é conferido na tela, sem gastar bobina.
- Sem mudar o código. A aplicação continua enviando bytes para
IP:9100. Só o endereço configurado aponta para o emulador, do mesmo jeito que seria configurado na loja. - Várias impressoras ao mesmo tempo. Caixa, cozinha e bar podem ter cada um o seu IP (
192.168.5.111,.112,.113), cada um na sua janela. - Acessível de qualquer aparelho da rede. Como o IP existe na interface de rede, um tablet Android, outro PC ou um container Docker também conseguem imprimir nele.
Como uma impressora térmica de rede recebe dados
Uma impressora térmica de rede é, na prática, um socket TCP aberto na porta 9100 que entrega tudo o que recebe direto ao interpretador de comandos. Esse modo se chama RAW (ou JetDirect, nome herdado das placas de rede da HP). Não há protocolo por cima: nada de HTTP, IPP, LPD ou handshake de aplicação.
O ciclo de uma impressão é sempre o mesmo:
- A aplicação abre uma conexão TCP com
IP:9100. - Escreve um fluxo de bytes: texto misturado com comandos ESC/POS.
- Fecha a conexão. A impressora já vinha imprimindo enquanto os bytes chegavam.
ESC/POS é a linguagem criada pela Epson e adotada por quase todos os fabricantes de impressora de cupom. Cada comando começa com um byte de controle, principalmente ESC (0x1B) e GS (0x1D), seguido de uma letra e, às vezes, de parâmetros. Todo o resto é texto, enviado nos bytes da code page ativa.
Comandos comuns e como aparecem no emulador
O filtro do emulador descarta o byte ESC ou GS e o byte seguinte, depois remove os caracteres não imprimíveis. Parâmetros que caem na faixa imprimível do ASCII sobram como letras soltas. A coluna da direita é a saída real do printer-output.py para cada comando:
| Comando | Bytes (hex) | Função | Saída no emulador |
|---|---|---|---|
ESC @ | 1B 40 | Inicializa a impressora | Nada |
ESC a n | 1B 61 n | Alinhamento (0 esquerda, 1 centro, 2 direita) | Nada com n = 00–02; sobra 0, 1 ou 2 com n = 30–32 |
ESC E n | 1B 45 n | Negrito liga/desliga | Nada com n = 00/01 |
ESC ! n | 1B 21 n | Modo de impressão (tamanho, negrito) | Sobra um caractere quando n é imprimível (30 vira 0) |
GS ! n | 1D 21 n | Altura e largura do caractere | Nada com valores comuns como 11 |
ESC d n | 1B 64 n | Avança n linhas | Nada; as linhas em branco somem |
LF | 0A | Imprime a linha e avança | Quebra de linha |
GS V m | 1D 56 m | Corte do papel | Nada com m = 00/01; sobra A ou B com m = 41/42 |
GS ( k … | 1D 28 6B … | QR Code | O conteúdo do QR aparece, cercado de restos como k1A2k1Ck1P0 |
GS v 0 … | 1D 76 30 … | Imagem raster (logo) | Lixo binário, como 0��AB�801… |
DLE EOT n | 10 04 n | Pede status em tempo real | Nada, e nenhuma resposta volta |
Onde o emulador é idêntico ao hardware e onde não é
Tudo o que é rede e conexão é reproduzido; o que é renderização vira texto.
| Aspecto | Impressora real | Emulador (printer.sh) |
|---|---|---|
| Endereço | IP próprio na LAN | IP próprio na LAN (alias na interface) |
| Transporte | TCP, porta 9100, RAW | TCP, porta 9100, RAW |
| Conexões | Em geral uma por vez | Uma por vez com o nc do macOS; as demais esperam na fila |
| Comandos ESC/POS | Executados | Descartados; parâmetros imprimíveis sobram |
| Respostas de status | Responde a DLE EOT e GS a | Não responde nada |
| Velocidade | Limitada pela mecânica; com o buffer cheio, o TCP segura o envio | Lê tudo na hora, sem limite |
| Code page | CP850, CP860, CP1252 etc., escolhida por ESC t | Sempre decodifica como UTF-8 |
| Saída | Papel de 58 ou 80 mm | Texto no terminal, sem largura fixa |
Para validar conteúdo, ordem dos itens, quebras de linha e a conexão em si, o emulador basta. Para validar negrito, tamanho de fonte, logo e QR Code, ainda vale um teste final no papel.
Arquitetura do emulador
O emulador troca só a ponta final do caminho: a aplicação fala com o mesmo IP e a mesma porta, mas quem atende é um nc ligado a um IP extra do computador.

Lido na horizontal, cada peça do emulador ocupa o lugar de uma parte da impressora: alias e nc fazem o papel da placa de rede, o filtro Python faz o papel do interpretador ESC/POS e a janela do Terminal substitui a bobina.
O ciclo de vida tem dois comandos:
./printer.sh IPcria o alias (se ainda não existir), garante que o IP seja roteado localmente, abre a janela e sobe o pipelinenc | python3../printer.sh stop IPmata oncdaquele IP e remove o alias e a rota local. As outras impressoras virtuais continuam rodando.
Código completo para macOS
O emulador são dois arquivos na mesma pasta: printer.sh cuida de rede, janela e listener; printer-output.py filtra os bytes e escreve o texto. O macOS já traz tudo o que eles usam: ifconfig, nc, osascript e, com as Command Line Tools, python3.
Instalação
mkdir -p ~/printer-emulador && cd ~/printer-emulador
# salve printer.sh e printer-output.py nesta pasta
chmod +x printer.sh
xcode-select --install # só se o python3 ainda não existir
./printer.sh 192.168.5.111printer.sh
#!/bin/bash
set -e
# Interface da rota padrão (sobrescreva com INTERFACE=en1 ./printer.sh ...)
INTERFACE="${INTERFACE:-$(route -n get default 2>/dev/null | awk '/interface:/ { print $2; exit }')}"
PORT=9100
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -z "$1" ]; then
echo "Uso:"
echo " $0 <IP>"
echo " $0 stop <IP>"
echo
echo "Exemplos:"
echo " $0 192.168.5.111"
echo " $0 192.168.5.112"
echo " $0 stop 192.168.5.111"
echo " INTERFACE=en0 $0 stop 192.168.5.111 # remove um alias criado na interface antiga"
exit 1
fi
if [ -z "$INTERFACE" ]; then
echo "Interface de rede não encontrada. Use: INTERFACE=en1 $0 $*"
exit 1
fi
if ! ifconfig "$INTERFACE" >/dev/null 2>&1; then
echo "Interface de rede inválida: $INTERFACE"
exit 1
fi
valid_ip() {
local octet
local -a octets
[[ "$1" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || return 1
IFS=. read -r -a octets <<< "$1"
for octet in "${octets[@]}"; do
(( 10#$octet <= 255 )) || return 1
done
}
# -------------------------
# STOP
# -------------------------
if [ "$1" = "stop" ]; then
IP="$2"
if ! valid_ip "$IP"; then
echo "Informe o IP:"
echo "$0 stop 192.168.5.111"
exit 1
fi
echo "🛑 Parando impressora virtual $IP:$PORT"
# Mata somente o nc desse IP
pkill -f "nc -lk ${IP} ${PORT}" 2>/dev/null || true
# Remove alias
sudo ifconfig "$INTERFACE" -alias "$IP" 2>/dev/null || true
if route -n get "$IP" 2>/dev/null | grep -qE 'interface: lo0$'; then
sudo route -n delete -host "$IP"
fi
echo "✓ Impressora $IP removida"
exit 0
fi
# -------------------------
# START
# -------------------------
IP="$1"
if ! valid_ip "$IP"; then
echo "IP inválido: $IP"
exit 1
fi
echo
echo "🖨️ ESC/POS Virtual Printer"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "IP: $IP"
echo "Porta: $PORT"
echo "Interface: $INTERFACE"
echo
if ifconfig "$INTERFACE" | grep -q 'status: inactive'; then
echo "Interface $INTERFACE inativa. Conecte-a à rede ou escolha outra com INTERFACE."
exit 1
fi
# Verifica alias
if ! ifconfig "$INTERFACE" | grep -q "inet $IP "; then
echo "Adicionando IP $IP..."
sudo ifconfig "$INTERFACE" alias "$IP" netmask 255.255.255.0
else
echo "✓ IP já configurado"
fi
# Um alias pode manter uma entrada ARP antiga em vez de uma rota local.
if ! ROUTE_INFO="$(route -n get "$IP" 2>&1)"; then
echo "Não foi possível consultar a rota de $IP: $ROUTE_INFO"
exit 1
fi
if ! printf '%s\n' "$ROUTE_INFO" | grep -qE 'interface: lo0$'; then
if printf '%s\n' "$ROUTE_INFO" | grep -qE "destination: ${IP//./\\.}$"; then
sudo route -n delete -host "$IP"
fi
sudo route -n add -host "$IP" -interface lo0
fi
if ! route -n get "$IP" | grep -qE 'interface: lo0$'; then
echo "A rota de $IP não aponta para lo0. Impressora não iniciada."
exit 1
fi
# Abre terminal pequeno
osascript - "$IP" "$PORT" "$SCRIPT_DIR" <<'EOF'
on run argv
set printerIP to item 1 of argv
set printerPort to item 2 of argv
set scriptDirectory to item 3 of argv
set quotedIP to quoted form of printerIP
set quotedPort to quoted form of printerPort
set displayScript to quoted form of (scriptDirectory & "/printer-output.py")
set commandText to "clear; printf '\\033[1;32m🖨 ESC/POS VIRTUAL PRINTER\\033[0m\\n'; printf '\\033[1mIP: %s | Porta: %s\\033[0m\\n' " & quotedIP & " " & quotedPort & "; echo '────────────────────────────────────────'; echo ''; nc -lk " & quotedIP & " " & quotedPort & " | python3 -u " & displayScript
tell application "Terminal"
activate
set w to do script commandText
delay 0.5
end tell
end run
EOF
echo
echo "✓ Impressora virtual iniciada"
echo
echo "Destino:"
echo "tcp://$IP:$PORT"
echoprinter-output.py
#!/usr/bin/env python3
import codecs
import sys
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
skip_command_byte = False
sys.stdout.write("\033[1;97m")
sys.stdout.flush()
try:
while True:
chunk = sys.stdin.buffer.read1(4096)
if not chunk:
break
text_bytes = bytearray()
for byte in chunk:
if skip_command_byte:
skip_command_byte = False
elif byte in (0x1B, 0x1D):
skip_command_byte = True
else:
text_bytes.append(byte)
text = decoder.decode(text_bytes)
readable = "".join(
char for char in text if char in "\r\n\t" or char.isprintable()
)
if readable:
sys.stdout.write(readable)
sys.stdout.flush()
finally:
sys.stdout.write("\033[0m")
sys.stdout.flush()O que cada parte faz
- Configuração (linhas 3–8).
set -eaborta no primeiro erro.INTERFACEvem da rota padrão:route -n get defaultdiz qual placa leva à rede, e oawkpega o nome depois deinterface:. Se a variável já vier preenchida (INTERFACE=en1 ./printer.sh ...), ela prevalece.PORTé a porta RAW padrão eSCRIPT_DIRguarda o caminho absoluto da pasta, para achar oprinter-output.pyde qualquer diretório. - Ajuda (linhas 10–21). Sem argumentos, mostra o uso e sai com código 1. O último exemplo lembra que o
stopprecisa da interface onde o alias foi criado: se você trocou do Wi-Fi para o cabo, passe a antiga emINTERFACE. - Validações (linhas 23–41). O script para se não achou interface ou se o
ifconfignão a reconhece.valid_ipconfere o formaton.n.n.ne se cada octeto vai até 255; o10#força base 10, para que08e09não sejam lidos como octal. A função vale tanto nostopquanto no início. - Parada (linhas 47–72).
pkill -fcompara a linha de comando inteira dos processos, então mata só oncdaquele IP e deixa as outras impressoras de pé. Em seguida,ifconfig "$INTERFACE" -aliasremove o endereço, e a rota de host emlo0, se ainda existir, é apagada. Opkille oifconfigterminam em|| truepara ostopnão falhar se a impressora já estiver parada. - Interface ativa (linhas 94–97). Se o
ifconfigmostrastatus: inactive(cabo solto, Wi-Fi desligado), o script para antes de mexer na rede. - Alias de IP (linhas 100–111). O
grep -q "inet $IP "verifica se o IP já existe; o espaço no fim evita confundir.11com.111. Se não existir,ifconfig "$INTERFACE" aliasadiciona um segundo endereço na mesma placa. A partir daí o Mac responde ARP por esse IP, e qualquer aparelho da rede chega nele. - Rota local (linhas 114–130). O macOS pode guardar uma entrada ARP antiga para o IP, de quando ele era de outro aparelho ou de um teste anterior. Com ela, conexões feitas do próprio Mac saem pela placa em vez de chegar ao
nc. O script consultaroute -n get IP: se a rota não usalo0, apaga a entrada de host antiga e criaroute add -host IP -interface lo0. Se ainda assim não apontar paralo0, para sem abrir a janela. - Janela do Terminal (linhas 133–149). O
osascriptroda um AppleScript embutido via heredoc. IP, porta e pasta entram comoargv, equoted form ofcoloca cada um entre aspas simples, o que protege contra espaços no caminho. Odo scriptabre uma janela nova, limpa a tela, imprime o cabeçalho com cores ANSI e sobe o pipeline. - O pipeline.
nc -lescuta e-ko faz voltar a escutar quando o cliente fecha a conexão, em vez de sair. Com o IP antes da porta, oncse liga só ao alias, e não a todas as interfaces.python3 -udesliga o buffer de saída do Python. read1(4096). Diferente deread(4096), devolve o que já chegou sem esperar completar 4096 bytes. É isso que deixa a saída em tempo real.- Máquina de estados.
skip_command_bytemarca que o próximo byte é a letra de um comando ESC/GS e deve ser descartado. Como a variável vive fora do laço, funciona mesmo quando o ESC chega no fim de um pacote TCP e a letra no começo do próximo. - Decodificador incremental. Um
çem UTF-8 ocupa dois bytes (C3 A7) e pode chegar dividido entre dois pacotes. Ogetincrementaldecoderguarda o byte incompleto até o próximo pedaço, eerrors="replace"troca bytes inválidos por�em vez de derrubar o script. - Filtro final e cores. Ficam só os caracteres imprimíveis, mais
\r,\ne\t. O texto sai em branco brilhante e negrito (\033[1;97m), e ofinallyrestaura a cor do terminal quando oncfecha o pipe.
Permissões que o macOS pode pedir
- sudo: senha de administrador para criar e remover o alias.
- Automação: na primeira execução, o macOS pergunta se o app de onde você roda o script pode controlar o Terminal. Aceite.
- Firewall: o
ncé software do sistema e, por padrão, pode receber conexões. Se o firewall estiver bloqueando todas as conexões de entrada, outros aparelhos da rede não vão conseguir imprimir.
Ajustes úteis
- Interface certa. O script usa a interface da rota padrão.
en0costuma ser o Wi-Fi nos MacBooks e a Ethernet no Mac mini e no iMac. Para escolher outra, rodeINTERFACE=en1 ./printer.sh 192.168.5.111, e passe a mesma variável nostop. - Máscara. Se o
ifconfigrecusar o alias por conflito de rota, troque paranetmask 255.255.255.255: o alias fica só com o próprio endereço e não disputa a rota da sub-rede.
Código para Linux e o que muda
O printer-output.py é o mesmo nos dois sistemas; só o script de controle precisa de uma versão própria, o printer-linux.sh. Três peças do macOS não existem no Linux: o ifconfig alias (vira ip addr add), o osascript com o app Terminal (vira o emulador de terminal instalado) e a garantia de um nc com -k.
| Aspecto | macOS (printer.sh) | Linux (printer-linux.sh) |
|---|---|---|
| Criar o IP extra | ifconfig en0 alias IP netmask 255.255.255.0 | ip addr add IP/24 dev eth0 |
| Remover o IP | ifconfig en0 -alias IP | ip addr del IP/24 dev eth0 |
| Garantir a rota local | route -n add -host IP -interface lo0 | ip route replace local IP/32 ... table local |
| Interface | Detectada pela rota padrão, ou INTERFACE=... | Detectada pela rota padrão, ou INTERFACE=... |
| Abrir a janela | osascript + Terminal | gnome-terminal, konsole, xfce4-terminal ou xterm |
| Sem interface gráfica | Não se aplica | Roda no terminal atual (SSH, servidor) ou com --here |
| netcat | nc BSD nativo, uma conexão por vez | netcat-openbsd (uma por vez) ou ncat (aceita conexões simultâneas) |
| Firewall | Firewall de aplicativos do macOS | ufw ou firewalld, liberando 9100/tcp |
| Ao parar | A janela continua aberta | A janela fecha junto com o listener |
Pré-requisitos e uso
# Debian / Ubuntu
sudo apt install netcat-openbsd python3 iproute2
# Fedora / RHEL
sudo dnf install nmap-ncat python3 iproute
chmod +x printer-linux.sh
./printer-linux.sh 192.168.5.111 # abre uma janela nova
./printer-linux.sh 192.168.5.111 --here # roda no terminal atual
./printer-linux.sh stop 192.168.5.111printer-linux.sh
#!/usr/bin/env bash
set -e
PORT=9100
PREFIX=24
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Interface da rota padrão (sobrescreva com INTERFACE=eth0 ./printer-linux.sh ...)
INTERFACE="${INTERFACE:-$(ip route show default 2>/dev/null | awk '{for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); exit }}')}"
if [ -z "$1" ]; then
echo "Uso:"
echo " $0 <IP> [--here]"
echo " $0 stop <IP>"
echo
echo "Exemplos:"
echo " $0 192.168.5.111"
echo " $0 192.168.5.112 --here # roda no terminal atual"
echo " $0 stop 192.168.5.111"
echo " INTERFACE=eth0 $0 stop 192.168.5.111 # remove um IP criado na interface antiga"
exit 1
fi
if [ -z "$INTERFACE" ]; then
echo "Interface de rede não encontrada. Use: INTERFACE=eth0 $0 $*"
exit 1
fi
if ! LINK_INFO="$(ip -o link show dev "$INTERFACE" 2>&1)"; then
echo "Não foi possível consultar a interface $INTERFACE: $LINK_INFO"
exit 1
fi
valid_ip() {
local octet
local -a octets
[[ "$1" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || return 1
IFS=. read -r -a octets <<< "$1"
for octet in "${octets[@]}"; do
(( 10#$octet <= 255 )) || return 1
done
}
# -------------------------
# STOP
# -------------------------
if [ "$1" = "stop" ]; then
IP="$2"
if ! valid_ip "$IP"; then
echo "Informe o IP:"
echo "$0 stop 192.168.5.111"
exit 1
fi
echo "🛑 Parando impressora virtual $IP:$PORT"
# Mata somente o listener desse IP (nc ou ncat)
pkill -f "nc(at)? -lk ${IP} ${PORT}" 2>/dev/null || true
# Remove o IP extra
sudo ip addr del "${IP}/${PREFIX}" dev "$INTERFACE" 2>/dev/null || true
echo "✓ Impressora $IP removida"
exit 0
fi
# -------------------------
# START
# -------------------------
IP="$1"
MODE="$2"
if ! valid_ip "$IP"; then
echo "IP inválido: $IP"
exit 1
fi
LINK_FLAGS="${LINK_INFO#*<}"
LINK_FLAGS="${LINK_FLAGS%%>*}"
if [[ ",$LINK_FLAGS," != *,UP,* || ",$LINK_FLAGS," != *,LOWER_UP,* ]]; then
echo "Interface $INTERFACE inativa. Conecte-a à rede ou escolha outra com INTERFACE."
exit 1
fi
# Precisa de um netcat com -k (OpenBSD ou ncat); o netcat-traditional não serve
if command -v nc >/dev/null 2>&1 && nc -h 2>&1 | grep -qiE 'keep inbound|keep-open'; then
LISTENER="nc"
elif command -v ncat >/dev/null 2>&1; then
LISTENER="ncat"
else
echo "Nenhum netcat com suporte a -k encontrado."
echo " Debian/Ubuntu: sudo apt install netcat-openbsd"
echo " Fedora/RHEL: sudo dnf install nmap-ncat"
exit 1
fi
echo
echo "🖨️ ESC/POS Virtual Printer"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "IP: $IP"
echo "Porta: $PORT"
echo "Interface: $INTERFACE"
echo
# Verifica o IP extra
if ! ip -4 addr show dev "$INTERFACE" | grep -qF "inet ${IP}/"; then
echo "Adicionando IP $IP em $INTERFACE..."
sudo ip addr add "${IP}/${PREFIX}" dev "$INTERFACE"
else
echo "✓ IP já configurado"
fi
if ! LOCAL_ROUTE="$(ip -4 route show table local type local exact "${IP}/32" 2>&1)"; then
echo "Não foi possível consultar a rota local de $IP: $LOCAL_ROUTE"
exit 1
fi
# O kernel normalmente cria essa rota ao adicionar o endereço.
if [ -z "$LOCAL_ROUTE" ]; then
echo "Restaurando rota local de $IP..."
sudo ip -4 route replace local "${IP}/32" dev "$INTERFACE" proto kernel scope host src "$IP" table local
fi
if ! ROUTE_INFO="$(ip -4 route get "$IP" 2>&1)"; then
echo "Não foi possível consultar a rota de $IP: $ROUTE_INFO"
exit 1
fi
if ! printf '%s\n' "$ROUTE_INFO" | grep -qE "^local ${IP//./\\.}[[:space:]]"; then
echo "O IP $IP não está sendo roteado localmente. Impressora não iniciada."
echo "$ROUTE_INFO"
echo "Verifique as regras de roteamento com: ip -4 rule show"
exit 1
fi
DISPLAY_SCRIPT="$(printf %q "$SCRIPT_DIR/printer-output.py")"
CMD="clear; printf '\033[1;32m🖨 ESC/POS VIRTUAL PRINTER\033[0m\n'; printf '\033[1mIP: %s | Porta: %s\033[0m\n' $IP $PORT; echo '────────────────────────────────────────'; echo; $LISTENER -lk $IP $PORT | python3 -u $DISPLAY_SCRIPT; echo; read -rp 'Listener encerrado. Enter para fechar. '"
# Abre uma janela nova no primeiro terminal gráfico encontrado
open_window() {
[ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] || return 1
if command -v gnome-terminal >/dev/null 2>&1; then
gnome-terminal -- bash -c "$CMD"
elif command -v konsole >/dev/null 2>&1; then
nohup konsole -e bash -c "$CMD" >/dev/null 2>&1 &
elif command -v xfce4-terminal >/dev/null 2>&1; then
nohup xfce4-terminal --title="Impressora $IP" -x bash -c "$CMD" >/dev/null 2>&1 &
elif command -v xterm >/dev/null 2>&1; then
nohup xterm -T "Impressora $IP" -e bash -c "$CMD" >/dev/null 2>&1 &
else
return 1
fi
}
echo
echo "Destino:"
echo "tcp://$IP:$PORT"
echo
if [ "$MODE" != "--here" ] && open_window; then
echo "✓ Impressora virtual iniciada em nova janela"
echo
else
echo "✓ Impressora virtual rodando neste terminal (Ctrl+C para parar)"
exec bash -c "$CMD"
fiO que muda em relação ao macOS
- Interface automática.
ip route show defaultdevolve algo comodefault via 192.168.5.1 dev eth0 ..., e oawkpega a palavra depois dedev. Em seguida,ip -o link showconfirma que a placa existe. Para forçar outra:INTERFACE=wlp2s0 ./printer-linux.sh 192.168.5.111. - Validação do IP. O IP vai para o
sudoe para dentro de umbash -c, então o script recusa qualquer coisa fora do formaton.n.n.nou com octeto acima de 255. A funçãovalid_ipé a mesma do macOS. - Interface ativa. A saída de
ip -o linktraz as flags entre<e>, como<BROADCAST,MULTICAST,UP,LOWER_UP>. O script exigeUP(placa ligada) eLOWER_UP(cabo ou Wi-Fi conectado); sem as duas, para antes de mexer na rede. - Detecção do netcat. O Linux tem três netcats diferentes. O script procura no
nc -ha descrição do-kdo OpenBSD (Keep inbound) ou do ncat (keep-open). Onetcat-traditional, que não tem-k, cai na mensagem de instalação. - IP extra com
ip addr.ip addr add IP/24cria um endereço secundário na placa. Ele some no reboot e pode sumir quando o NetworkManager reconecta; nesse caso, basta rodar o script de novo. - Rota local. Ao receber o endereço, o kernel cria na tabela
locala rota que entrega esse IP à própria máquina. Se ela não estiver lá, o script a recria comip route replace local IP/32 ... table local. Depois,ip route get IPprecisa começar comlocal; se uma regra de roteamento (VPN, policy routing) desviar o IP, o script mostra a rota e para, sugerindoip -4 rule show. - Janela. Sem
DISPLAYnemWAYLAND_DISPLAY, não há interface gráfica e o listener roda no próprio terminal. Com interface gráfica, o script usa o primeiro terminal que encontrar, e onohup … &desacopla a janela do script. - Parada. O padrão
nc(at)? -lk IP PORTdopkillpega tantoncquantoncat. A linha de comando dobash -cda janela também contém esse texto, então ele é encerrado junto e a janela fecha. Se o listener cair sozinho (IP já em uso, por exemplo), oreadno fim mantém a janela aberta para mostrar o erro.
Firewall e WSL
sudo ufw allow 9100/tcp # Ubuntu com ufw
sudo firewall-cmd --add-port=9100/tcp # Fedora/RHEL, vale até o rebootNo WSL2, a rede passa por NAT: o IP extra fica dentro da VM e não aparece na LAN. Ali, o emulador serve para aplicações rodando dentro do próprio WSL.
Integrando com a sua aplicação
O código de impressão não muda: basta trocar o IP da impressora pelo IP do emulador. Os exemplos abaixo usam as bibliotecas ESC/POS mais comuns, todas enviando para 192.168.5.111:9100.
Python, com python-escpos
from escpos.printer import Network
p = Network("192.168.5.111", port=9100)
p.set(align="center", bold=True)
p.text("PEDIDO #1234\n")
p.set(align="left", bold=False)
p.text("1x X-Burguer R$ 25,90\n")
p.cut()
p.close()Node.js, com node-thermal-printer
const { ThermalPrinter, PrinterTypes } = require("node-thermal-printer");
async function imprimir() {
const printer = new ThermalPrinter({
type: PrinterTypes.EPSON,
interface: "tcp://192.168.5.111:9100",
});
printer.alignCenter();
printer.println("PEDIDO #1234");
printer.alignLeft();
printer.println("1x X-Burguer R$ 25,90");
printer.cut();
await printer.execute();
}
imprimir();PHP, com escpos-php
use Mike42\Escpos\PrintConnectors\NetworkPrintConnector;
use Mike42\Escpos\Printer;
$connector = new NetworkPrintConnector("192.168.5.111", 9100);
$printer = new Printer($connector);
$printer->setJustification(Printer::JUSTIFY_CENTER);
$printer->text("PEDIDO #1234\n");
$printer->cut();
$printer->close();C#, com socket puro
using System.Net.Sockets;
using System.Text;
using var client = new TcpClient("192.168.5.111", 9100);
using var stream = client.GetStream();
byte[] init = { 0x1B, 0x40 }; // ESC @
byte[] cut = { 0x1D, 0x56, 0x00 }; // GS V 0
byte[] text = Encoding.UTF8.GetBytes("PEDIDO #1234\nAção: entregar\n\n");
stream.Write(init);
stream.Write(text);
stream.Write(cut);Bibliotecas como python-escpos convertem o texto para a code page da impressora antes de enviar. Nesse caso, os acentos chegam fora de UTF-8 e aparecem como � no emulador; a seção seguinte mostra como ajustar.
Outros aparelhos da rede
O IP extra está na placa de rede, então qualquer aparelho da mesma sub-rede chega nele: tablet de comanda, outro PC, maquininha com app de PDV. Configure nele o IP do emulador e a porta 9100. Para conferir a partir de outro computador:
printf 'teste de outro PC\n' | nc -w 1 192.168.5.111 9100Docker
Containers em geral alcançam o IP extra sem configuração, porque ele é um endereço local do host. Para confirmar no seu ambiente:
docker run --rm python:3-alpine python -c "import socket; s = socket.create_connection(('192.168.5.111', 9100)); s.sendall(b'teste via docker\n'); s.close()"Várias impressoras
Cada chamada cria um IP e abre uma janela própria, então dá para simular o ambiente inteiro de uma loja:
./printer.sh 192.168.5.111 # caixa
./printer.sh 192.168.5.112 # cozinha
./printer.sh 192.168.5.113 # barSó na própria máquina, sem sudo
Se a aplicação roda no mesmo computador e não precisa de um IP da LAN, dá para pular o alias e escutar no loopback. Portas acima de 1024 não exigem root:
nc -lk 127.0.0.1 9100 | python3 -u printer-output.pyLimitações e solução de problemas
O emulador valida conexão e conteúdo, mas quatro diferenças em relação ao hardware afetam o que você vê na tela:
- Code page. O filtro sempre decodifica UTF-8. Se a aplicação envia CP850 ou CP860, comuns em impressoras vendidas no Brasil,
çãovira��o. A correção está logo abaixo. - Parâmetros que sobram. Só o byte logo após ESC ou GS é descartado. Letras soltas como
B,0ouk1P0no meio do texto são parâmetros de comandos (veja a tabela de comandos acima), não erro da aplicação. - Sem respostas de status. Aplicações que consultam o status antes de imprimir (
DLE EOT,GS a) esperam uma resposta que nunca vem e podem travar até o timeout. No ambiente de teste, desligue essa verificação. - Sem limite de velocidade. O emulador lê tudo na hora. Uma impressora real é mais lenta e, com o buffer cheio, segura o envio pelo TCP; timeouts de escrita calibrados no emulador podem estourar no hardware.
Para aplicações que enviam CP850, troque a linha do decodificador no printer-output.py (use cp860 se for o caso):
decoder = codecs.getincrementaldecoder("cp850")(errors="replace")
Problemas comuns
| Sintoma | Causa provável | Solução |
|---|---|---|
nc sai na hora com Address already in use | Outro processo já escuta na 9100 desse IP | lsof -nP -iTCP:9100 -sTCP:LISTEN, depois ./printer.sh stop IP |
127.0.0.1:9100 ocupada numa máquina com Flutter | O Dart DevTools usa a porta 9100 no loopback | Usar o alias de IP, que não conflita, ou outra porta |
nc reclama de assign requested address | O alias não existe na interface | Rodar o script de novo e conferir a interface |
| Outro aparelho não conecta | IP fora da sub-rede, interface errada ou firewall | Conferir máscara e INTERFACE; liberar 9100/tcp |
| A impressão sai numa impressora de verdade | O IP já tinha dono na rede | Antes de escolher, confirmar que ping -c 1 IP não responde |
Acentos viram � | Aplicação envia CP850 ou CP860 | Trocar o decodificador, como acima |
| O IP sumiu | Reboot, troca de Wi-Fi ou NetworkManager | Rodar o script de novo |
| Interface … inativa | A interface da rota padrão está sem cabo ou sem Wi-Fi | Conectar a placa ou escolher outra com INTERFACE= |
| A rota não aponta para lo0 (macOS) ou o IP não está sendo roteado localmente (Linux) | Entrada de rota antiga, VPN ou regra de roteamento desviando o IP | No macOS, conferir route -n get IP; no Linux, ip -4 rule show; ou escolher outro IP |
Boas práticas
- Use IPs fora da faixa do DHCP do roteador, para não colidir com aparelhos que entram na rede depois.
- Nunca use o IP da impressora real enquanto ela estiver ligada na mesma rede: os dois responderiam ARP pelo mesmo endereço.
- Mantenha um IP por impressora lógica (caixa, cozinha, bar), igual à configuração da loja.
- Ao terminar, rode
stoppara remover o alias e liberar o IP.
Testando a impressora
Com o emulador rodando em 192.168.5.111, este comando envia um cupom de teste com inicialização, centralização, negrito, acentos e corte de papel, como uma aplicação real faria. Funciona igual no macOS e no Linux:
printf '\033@\033a\001*** TESTE DE IMPRESSAO ***\n\033a\000\033E\001Pedido #1234\033E\000\n1x X-Burguer ........ R$ 25,90\n1x Refrigerante ...... R$ 6,00\n--------------------------------\nTOTAL ................ R$ 31,90\nAcentos: ação, pão, café\n\n\n\035V\000' | nc -w 1 192.168.5.111 9100Na janela do emulador, os comandos somem e fica só o texto do cupom:
*** TESTE DE IMPRESSAO ***
Pedido #1234
1x X-Burguer ........ R$ 25,90
1x Refrigerante ...... R$ 6,00
--------------------------------
TOTAL ................ R$ 31,90
Acentos: ação, pão, caféNo comando, \033@ inicializa a impressora, \033a\001 centraliza, \033E\001 liga o negrito e \035V\000 corta o papel. O -w 1 faz o nc encerrar a conexão depois de enviar, como uma aplicação faz ao terminar a impressão.
Duas variações:
- Com
ncat(padrão no Fedora e RHEL): troquenc -w 1porncat --send-only, que encerra quando a entrada acaba. - Sem netcat, só com bash (o
/dev/tcpnão existe no zsh, por isso obash -c):
bash -c "printf 'Teste via /dev/tcp\n' > /dev/tcp/192.168.5.111/9100"




