Printers

Raw printing and LPR

Any print job sent as it is to a network printer, on port 9100 or through an LPR/LPD queue, with timeouts, retries and the printer's reply read back, and a check that a printer answers.

Version 1.0.0 · 7 functions · 1 crate

#printing#network#retries#devices

Functions

What use raw_print.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use raw_print_file, raw_print_text from raw_print (Quillo 0.6).

raw_print_file(path: str, host: str, options: any) → anyor an error

Sends a file to a printer's raw port as it is: host or host:port. options: { port: 9100, timeout_ms: 5000, retries: 2, retry_delay_ms: 1000, read_reply_ms: 0 (wait that long for an answer, for status questions) }. Gives { bytes, attempts, ms, printer, reply }. A retry sends the whole job again, so a job that failed half-way may print twice.

raw_print_text(text: str, host: str, options: any) → anyor an error

Sends text (ZPL, EPL, TSPL, PJL...) to a printer's raw port, as raw_print_file does.

raw_print_base64(data: str, host: str, options: any) → anyor an error

Sends bytes given in base64 (a job another plugin made) to a printer's raw port, as raw_print_file does.

raw_print_reachable(host: str, port: int, timeout_ms: int) → any

Whether a printer takes connections on port (9100 raw, 515 LPD, 631 IPP), and how long it took: { open, ms, error }. Nothing is printed.

raw_print_lpr_control(options: any) → stror an error

The LPR control file a job would send, for checking it: options as lpr_print.

raw_print_lpr(path: str, host: str, queue: str, options: any) → anyor an error

Prints a file through an LPD print server or printer (port 515) on queue queue (often "raw", "lp", "PASSTHRU" or the printer's name). options: { user, name (the job's), copies, format: "raw" | "text" | "postscript", banner, job (a number 0-999), timeout_ms, retries }. Gives { bytes, attempts, queue, job }.

raw_print_lpq(host: str, queue: str, long: bool, timeout_ms: int) → stror an error

The state of an LPD queue as the server writes it (long: the long form): the jobs waiting, or that it is empty.

Get it

$ quillo install raw_print               # raw_print.qwer into this folder$ quillo install raw_print --example     # and its example, raw_print.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/raw_print.qwer$ curl -O https://quillolang.org/plugins/files/raw_print.qwe$ curl -O https://quillolang.org/plugins/files/raw_print_sample.zpl

Then, in your program, all of it or only what you call:

main.qwe
use raw_print.qweruse raw_print_file, raw_print_text from raw_print      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
base64 = "0.22"

Example

raw_print.qwe
// Sending jobs to network printers as they are: a ZPL file to a label printer's port 9100 with// retries, a status question whose answer is read back, a check that the printer answers at all,// and the LPR control file a job to a print server's queue would carry.////   quillo run raw_print.qwe      then open http://localhost:8080/lpr//   PRINTER_HOST=192.168.1.60 quillo run raw_print.qwe      and http://localhost:8080/printuse "raw_print.qwer"route GET /print {    let host = env("PRINTER_HOST", null) ?? reply 503 "set PRINTER_HOST to the printer's address (host or host:port)"    let sent = raw_print_file("raw_print_sample.zpl", host, { timeout_ms: 3000, retries: 2, retry_delay_ms: 500 })?    // Zebra's host status question: three lines come back.    let status = raw_print_text("~HS", host, { read_reply_ms: 500 })?    { bytes: sent.bytes, attempts: sent.attempts, status_lines: len(split(trim(status.reply), "\n")) }}route GET /check {    let host = env("PRINTER_HOST", null) ?? "127.0.0.1"    { printer: raw_print_reachable(host, 9100, 1000).open, nothing_there: raw_print_reachable("127.0.0.1:1", 0, 500).open }}route GET /lpr = respond(200, raw_print_lpr_control({ user: "warehouse", name: "Pick list 7", client: "wms01", copies: 2, job: 42 })?, { "content-type": "text/plain" })route GET /fails = { error: raw_print_text("x", "127.0.0.1:1", { timeout_ms: 300, retries: 1, retry_delay_ms: 100 }).error }
$ quillo run raw_print.qwe$ curl 'localhost:8080/print'$ curl 'localhost:8080/lpr'$ curl 'localhost:8080/fails'$ curl 'localhost:8080/check'

It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.

Source

raw_print.qwer
//! Print jobs sent as they are to network printers: raw TCP on port 9100 (JetDirect, AppSocket) and LPR/LPD (RFC 1179, port 515) with queues, job names and the queue's state, each with timeouts and retries, the printer's reply read back when it gives one, and a check that a printer answers at all. For ZPL, EPL, PCL, PDF, ESC/POS or any file a printer takes.//! version: 1.0.0//!//! [dependencies]//! base64 = "0.22"use serde_json::{json, Value};use std::io::{Read, Write};use std::net::{SocketAddr, TcpStream, ToSocketAddrs};use std::time::{Duration, Instant};fn addr(host: &str, port: u16) -> Result<SocketAddr, String> {    let h = host.trim();    let with_port = if h.starts_with('[') {        if h.contains("]:") { h.to_string() } else { format!("{}:{}", h, port) }    } else if h.matches(':').count() == 1 {        h.to_string()    } else if h.contains(':') {        format!("[{}]:{}", h, port)    } else {        format!("{}:{}", h, port)    };    with_port.to_socket_addrs().map_err(|e| format!("{}: {}", host, e))?.next().ok_or(format!("{} has no address", host))}struct Opts {    timeout: Duration,    retries: u32,    delay: Duration,    read_ms: u64,}fn opts(o: &Value) -> Opts {    Opts {        timeout: Duration::from_millis(o["timeout_ms"].as_u64().unwrap_or(5000).clamp(100, 300_000)),        retries: o["retries"].as_u64().unwrap_or(2).min(20) as u32,        delay: Duration::from_millis(o["retry_delay_ms"].as_u64().unwrap_or(1000).min(60_000)),        read_ms: o["read_reply_ms"].as_u64().unwrap_or(0).min(60_000),    }}fn connect(a: SocketAddr, t: Duration) -> Result<TcpStream, String> {    let s = TcpStream::connect_timeout(&a, t).map_err(|e| format!("cannot reach {}: {}", a, e))?;    s.set_write_timeout(Some(t)).map_err(|e| e.to_string())?;    s.set_read_timeout(Some(t)).map_err(|e| e.to_string())?;    let _ = s.set_nodelay(true);    Ok(s)}/// Sends once: connects, writes everything, half-closes, and reads what the printer says within `read_ms`.fn send_once(a: SocketAddr, data: &[u8], o: &Opts) -> Result<Vec<u8>, String> {    let mut s = connect(a, o.timeout)?;    for chunk in data.chunks(64 * 1024) {        s.write_all(chunk).map_err(|e| format!("sending to {}: {}", a, e))?;    }    s.flush().map_err(|e| e.to_string())?;    let mut reply = vec![];    if o.read_ms > 0 {        let until = Instant::now() + Duration::from_millis(o.read_ms);        let _ = s.set_read_timeout(Some(Duration::from_millis(50)));        let mut buf = [0u8; 4096];        while Instant::now() < until && reply.len() < 1_000_000 {            match s.read(&mut buf) {                Ok(0) => break,                Ok(n) => reply.extend_from_slice(&buf[..n]),                Err(_) => {}            }        }    }    let _ = s.shutdown(std::net::Shutdown::Write);    Ok(reply)}fn raw(data: &[u8], host: &str, options: &Value) -> Result<Value, String> {    let port = options["port"].as_u64().unwrap_or(9100).clamp(1, 65535) as u16;    let a = addr(host, port)?;    let o = opts(options);    let started = Instant::now();    let mut last = String::new();    for attempt in 1..=o.retries + 1 {        match send_once(a, data, &o) {            Ok(reply) => {                return Ok(json!({                    "bytes": data.len(), "attempts": attempt, "ms": started.elapsed().as_millis() as u64, "printer": a.to_string(),                    "reply": String::from_utf8_lossy(&reply),                }))            }            Err(e) => {                last = e;                if attempt <= o.retries {                    std::thread::sleep(o.delay);                }            }        }    }    Err(format!("{} (after {} attempts)", last, o.retries + 1))}/// Sends a file to a printer's raw port as it is: `host` or `host:port`. `options`: `{ port: 9100, timeout_ms: 5000, retries: 2, retry_delay_ms: 1000, read_reply_ms: 0 (wait that long for an answer, for status questions) }`./// Gives `{ bytes, attempts, ms, printer, reply }`. A retry sends the whole job again, so a job that failed half-way may print twice.pub fn raw_print_file(path: &str, host: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    let data = std::fs::read(path).map_err(|e| format!("{}: {}", path, e))?;    raw(&data, host, &options)}/// Sends text (ZPL, EPL, TSPL, PJL...) to a printer's raw port, as `raw_print_file` does.pub fn raw_print_text(text: &str, host: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    raw(text.as_bytes(), host, &options)}/// Sends bytes given in base64 (a job another plugin made) to a printer's raw port, as `raw_print_file` does.pub fn raw_print_base64(data: &str, host: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    use base64::Engine;    let bytes = base64::engine::general_purpose::STANDARD.decode(data.trim()).map_err(|e| format!("not base64: {}", e))?;    raw(&bytes, host, &options)}/// Whether a printer takes connections on `port` (9100 raw, 515 LPD, 631 IPP), and how long it took: `{ open, ms, error }`. Nothing is printed.pub fn raw_print_reachable(host: &str, port: i64, timeout_ms: i64) -> serde_json::Value {    let started = Instant::now();    let a = match addr(host, port.clamp(1, 65535) as u16) {        Ok(a) => a,        Err(e) => return json!({ "open": false, "ms": 0, "error": e }),    };    match TcpStream::connect_timeout(&a, Duration::from_millis(timeout_ms.clamp(50, 60_000) as u64)) {        Ok(_) => json!({ "open": true, "ms": started.elapsed().as_millis() as u64, "error": null }),        Err(e) => json!({ "open": false, "ms": started.elapsed().as_millis() as u64, "error": e.to_string() }),    }}fn clean(s: &str, max: usize) -> String {    s.chars().filter(|c| (' '..='~').contains(c)).take(max).collect()}/// The LPR control file for a job (RFC 1179): host, user, job and file names, and how to print it.fn control(job: u32, options: &Value) -> Result<(String, String), String> {    let host = clean(options["client"].as_str().unwrap_or("quillo"), 31).replace(' ', "");    let host = if host.is_empty() { "quillo".to_string() } else { host };    let user = clean(options["user"].as_str().unwrap_or("quillo"), 31);    let name = clean(options["name"].as_str().unwrap_or("job"), 99);    let df = format!("dfA{:03}{}", job % 1000, host);    // `l` prints the file as it is (control characters kept): right for printer languages; `f` formats plain text.    let kind = match options["format"].as_str().unwrap_or("raw") {        "raw" => 'l',        "text" => 'f',        "postscript" => 'o',        other => return Err(format!("format {:?}: raw, text or postscript", other)),    };    let copies = options["copies"].as_u64().unwrap_or(1).clamp(1, 100);    let mut c = format!("H{}\nP{}\nJ{}\nN{}\n", host, user, name, name);    if options["banner"] == true {        c.push_str(&format!("C{}\nL{}\n", host, user));    }    for _ in 0..copies {        c.push_str(&format!("{}{}\n", kind, df));    }    c.push_str(&format!("U{}\n", df));    Ok((c, df))}/// The LPR control file a job would send, for checking it: `options` as `lpr_print`.pub fn raw_print_lpr_control(options: serde_json::Value) -> Result<String, String> {    let job = options["job"].as_u64().unwrap_or(1) as u32;    Ok(control(job, &options)?.0)}fn ack(s: &mut TcpStream, what: &str) -> Result<(), String> {    let mut b = [0u8; 1];    s.read_exact(&mut b).map_err(|e| format!("no answer to {}: {}", what, e))?;    if b[0] != 0 {        return Err(format!("the print server refused {} (code {})", what, b[0]));    }    Ok(())}/// Prints a file through an LPD print server or printer (port 515) on queue `queue` (often "raw", "lp", "PASSTHRU" or the printer's name)./// `options`: `{ user, name (the job's), copies, format: "raw" | "text" | "postscript", banner, job (a number 0-999), timeout_ms, retries }`. Gives `{ bytes, attempts, queue, job }`.pub fn raw_print_lpr(path: &str, host: &str, queue: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    let data = std::fs::read(path).map_err(|e| format!("{}: {}", path, e))?;    let q = clean(queue, 64);    if q.is_empty() || q.contains(' ') {        return Err("a queue name without spaces".into());    }    let a = addr(host, options["port"].as_u64().unwrap_or(515).clamp(1, 65535) as u16)?;    let o = opts(&options);    let job = options["job"].as_u64().unwrap_or((std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0) % 1000) as u64) as u32;    let (cf, df) = control(job, &options)?;    let cf_name = df.replacen("dfA", "cfA", 1);    let mut last = String::new();    for attempt in 1..=o.retries + 1 {        let run = || -> Result<(), String> {            let mut s = connect(a, o.timeout)?;            s.write_all(format!("\x02{}\n", q).as_bytes()).map_err(|e| e.to_string())?;            ack(&mut s, "the queue")?;            s.write_all(format!("\x02{} {}\n", cf.len(), cf_name).as_bytes()).map_err(|e| e.to_string())?;            ack(&mut s, "the control file")?;            s.write_all(cf.as_bytes()).map_err(|e| e.to_string())?;            s.write_all(&[0]).map_err(|e| e.to_string())?;            ack(&mut s, "the control file's content")?;            s.write_all(format!("\x03{} {}\n", data.len(), df).as_bytes()).map_err(|e| e.to_string())?;            ack(&mut s, "the data file")?;            s.write_all(&data).map_err(|e| e.to_string())?;            s.write_all(&[0]).map_err(|e| e.to_string())?;            ack(&mut s, "the data")?;            Ok(())        };        match run() {            Ok(()) => return Ok(json!({ "bytes": data.len(), "attempts": attempt, "queue": q, "job": job })),            Err(e) => {                last = e;                if attempt <= o.retries {                    std::thread::sleep(o.delay);                }            }        }    }    Err(format!("{} (after {} attempts)", last, o.retries + 1))}/// The state of an LPD queue as the server writes it (`long`: the long form): the jobs waiting, or that it is empty.pub fn raw_print_lpq(host: &str, queue: &str, long: bool, timeout_ms: i64) -> Result<String, String> {    let q = clean(queue, 64);    let a = addr(host, 515)?;    let t = Duration::from_millis(timeout_ms.clamp(100, 60_000) as u64);    let mut s = connect(a, t)?;    s.write_all(format!("{}{}\n", if long { '\x04' } else { '\x03' }, q).as_bytes()).map_err(|e| e.to_string())?;    let mut out = vec![];    let mut buf = [0u8; 4096];    loop {        match s.read(&mut buf) {            Ok(0) => break,            Ok(n) => {                out.extend_from_slice(&buf[..n]);                if out.len() > 1_000_000 {                    break;                }            }            Err(e) if out.is_empty() => return Err(format!("no answer from {}: {}", a, e)),            Err(_) => break,        }    }    Ok(String::from_utf8_lossy(&out).into_owned())}
Brother QL and P-touch ptouch.qwer · 1.0.0Brother QL label printers and P-touch tape printers in raster mode: text made into a label picture, any picture made into the raster job for a DK roll or TZe tape, cut, compressed, sent over the network, and the printer's status read.printinglabelsimagesdevicesaddresses7 views · 7 downloadsDatamax DPL labels dpl.qwer · 1.0.0Honeywell and Datamax-O'Neil DPL labels from fields placed in millimetres from the top left: resident fonts, Code 128, EAN-13, QR and Data Matrix, lines and boxes, heat, speed and quantity, sent to port 9100.printinglabelsbarcodesdevices7 views · 9 downloadsEPL2 labels epl.qwer · 1.0.0Zebra and Eltron EPL2 labels from fields in millimetres: text in the resident fonts, Code 128, EAN-13, Code 39 and QR barcodes, lines, boxes and pictures, with label size, gap, speed and copies, sent to port 9100.printinglabelsbarcodesdevices6 views · 5 downloadsEpson ESC/P dot-matrix escp.qwer · 1.0.0Epson ESC/P and ESC/P2 for dot-matrix printers: pre-printed and multipart forms filled by line and column, reports with bold, double-width and condensed text, pitch, line spacing and page length, in code page 437 with box-drawing lines.printingformsinvoicesreportsdevices7 views · 11 downloadsGodex EZPL labels ezpl.qwer · 1.0.0Godex EZPL labels for G500, RT200 and other Godex printers: text in the resident fonts, Code 128, EAN-13, Code 39 and QR codes, lines and boxes, label size, darkness, speed and copies, sent to port 9100.printinglabelsbarcodesdevices6 views · 8 downloadsHP PCL print jobs pcl.qwer · 1.0.0HP PCL 5 jobs for laser printers: pages of text placed in millimetres in the printer's fonts, boxes, lines and shading, pictures as raster graphics, plain listings, paper, tray, duplex and copies, wrapped in PJL, sent to port 9100.printingdocumentsreportsdevices8 views · 8 downloads
Thread
66Useful? Like it, or add to its thread below.

Thread 0 posts

On Raw printing and LPR: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.