Print node
Pairing a device with Superroute, its events and its diagnostics.
A print node receives print jobs from Superroute and prints them on the printers of its registry. It reports its printers and their state, prints and acknowledges each job as soon as it is sent, and answers the diagnostics of the Print Nodes page.
Pairing
Create the node over a printer registry and its saved state, pair it with the code shown on the Print Nodes page, then start it.
// Dart / Flutter
final node = await PrintNode.create(printers, state: savedState, deviceName: 'Packing station 2');
node.events.listen((e) {
if (e is NodeStateEvent) save(e.state.json); // holds the node token
});
await node.pair('K2M9XY'); // the code from the Print Nodes page
await node.start();
# Python
node = PrintNode(printers, state=saved_state, on_event=handle)
node.pair("K2M9XY")
node.start()
// C# / .NET
using var node = new PrintNode(printers, savedState);
node.StateChanged += (_, s) => Save(s);
node.JobFinished += (_, e) => Console.WriteLine($"{e.JobId}: {e.Result.Ok}");
node.Pair("K2M9XY");
node.Start();
// Swift
let node = try PrintNode(registry: printers, state: savedState)
try node.pair("K2M9XY")
try node.start()
for await event in node.events {
if case .state(let state) = event { save(state) }
}
// Kotlin
val node = PrintNode(printers, savedState, deviceName = "Packing station 2") { event ->
if (event is NodeEvent.State) save(event.state)
}
node.pair("K2M9XY")
node.start()
A device without a screen can register without a code: it appears on the Print Nodes page as unbound, is claimed there and receives its node token at the next heartbeat.
Unpairing removes the node token. The node then needs a new code.
Events
The node reports what happens as events. Save the state of every state event: it holds the node token.
| Event | Meaning |
|---|---|
state |
The state of the node changed. Save it. |
job |
A job was printed or failed, with its result. |
log |
A log line. |
sweep |
A protocol check of a printer changed. |
settings |
The printer settings from the server changed. They are already applied. |
printing |
A job started or ended printing. |
Heartbeat
The node sends a heartbeat with its printers, their transports and their languages, and fetches its jobs. When a job is sent, a push message makes the node fetch it at once.
Diagnostics
The node answers the diagnostics, the probe report and the protocol check of the Print Nodes page. The status shows the last heartbeat and the push link.
// Dart / Flutter
final status = await node.status(); // last heartbeat, push link
final sweeps = await node.sweeps(); // protocol checks running on this node
await node.sweepPress(sweeps.first.sweep, 'next');
final report = await printers.probeReport('Labels');
A protocol check prints one test per language. The person at the printer confirms the one that printed by scanning it, or chooses the next language or stops the check.
Node hooks
An app that runs a print node can answer remote commands itself, follow the printer settings from the server, send the languages chosen on the device, add its own printer fields to the heartbeat, see the jobs in flight and read the label size of ZPL printers.
Node hooks are available from sPrint 0.1.5 in every package except the browser build.
Commands answered by the app
The node passes every remote command from the Print Nodes page to the app's command handler first. The handler returns an answer, or nothing to let the node answer the command itself. An error raised by the handler fails the command with its message. A command without an answer after 30 seconds counts as not handled and the node answers it.
The app answers the commands only it can carry out, such as the connectivity test or restarting the app, and may replace the node's own answers, such as collecting logs or checking for updates.
Commands poll interval
The interval sets how often the node checks for remote commands. The default is 30 seconds. Push messages deliver commands at once; the poll is the fallback.
// Dart / Flutter
final node = await PrintNode.create(printers, state: savedState,
commandsEvery: const Duration(seconds: 15), // commands poll (default 30 s)
onCommand: (cmd) async => switch (cmd.action) { // null: the node answers
'connectivity_test' => CommandAnswer(output: await checkServer()),
_ => null,
});
# Python
from superroute_sprint import CommandAnswer
def on_command(cmd): # on a worker thread; None leaves it to the node
if cmd.action == "connectivity_test":
return CommandAnswer(output=check_server())
return None
node = PrintNode(printers, state=saved_state, on_event=on_event,
on_command=on_command, commands_every=15)
// C# / .NET
using var node = new PrintNode(printers, savedState,
onCommand: async c => c.Action == "connectivity_test"
? new CommandAnswer { Output = await CheckNetwork() }
: null, // null: the node answers it
commandsEvery: TimeSpan.FromSeconds(15));
// Swift
let node = try PrintNode(registry: printers, state: savedState, commandsEvery: 15) { cmd in
switch cmd.action {
case "connectivity_test": return CommandAnswer(output: try await myConnectivityTest())
default: return nil // the node answers it itself
}
}
// Kotlin
val node = PrintNode(printers, savedState, "Front desk",
commandsEvery = 15.seconds,
onCommand = { cmd -> // its own thread; null = the node answers
when (cmd.action) {
"connectivity_test" -> CommandAnswer(checkServer())
else -> null
}
},
) { ev -> handle(ev) }
Printer settings from the server
The node applies the printer settings in every heartbeat answer:
- Locked languages: a printer whose language is locked on the Print Nodes page keeps that language. Saving another language for it is refused and saves nothing. Identifying the printer reports what it found and changes nothing.
- Operator languages: a language an administrator chooses for a printer becomes its language, also on a locked printer. Auto leaves the language to the printer's identification.
- Default paper: a printer's default paper is used for every job that names no paper, whether it comes from Superroute, from a LAN peer or from the app.
Locks and default papers are saved with the printer registry and apply after a restart, before the first heartbeat. The node returns the languages, locks and papers of the last answer on request and reports each change as a settings event.
Languages chosen on the device
Pushing languages applies the languages chosen on the device to the registry and sends them to the server. A change on a locked printer is refused on the device and not sent, except a correction: the printer's own answer to the check after a job. The call returns the printers that the device or the server refused and applies the settings the server returns.
// Dart / Flutter
node.events.listen((e) {
if (e is NodeSettingsEvent) showLocks(e.settings); // languages, locks, paper: already applied
});
final settings = await node.printerSettings();
final refused = await node.pushLanguages({'Labels': 'zpl'}); // a pick made here
await node.pushLanguages({'Labels': 'tspl'}, corrections: ['Labels']); // the printer's own answer
# Python
# on_event gets kind "settings" (e.settings: languages, locks, default papers, already applied)
node.printer_settings().language_locked("Labels")
refused = node.push_languages({"Labels": "zpl"}) # a pick made here
node.push_languages({"Labels": "cpcl"}, corrections=["Labels"]) # the printer's own answer
// C# / .NET
node.SettingsChanged += (_, s) => ShowLocks(s.Locks, s.Paper); // already applied
var settings = node.PrinterSettings;
var refused = node.PushLanguages(new Dictionary<string, string> { ["Labels"] = "zpl" });
node.PushLanguages(new Dictionary<string, string> { ["Labels"] = "tspl" }, corrections: new[] { "Labels" });
// Swift
for await e in node.events {
if case .settings(let s) = e { lockedPrinters = s.locks.filter { $0.value.language }.map(\.key) }
}
let settings = try node.printerSettings()
let refused = try node.pushLanguages(["Labels": "zpl"]) // a pick made here
try node.pushLanguages(["Labels": "cpcl"], corrections: ["Labels"]) // the printer's own answer
// Kotlin
// handle(ev): is NodeEvent.Settings -> showLocks(ev.settings) // languages, locks, paper
val settings = node.printerSettings
val refused = node.pushLanguages(mapOf("Labels" to "zpl")) // a pick made here
node.pushLanguages(mapOf("Labels" to "tspl"), corrections = listOf("Labels")) // the printer's own answer
Printer fields in the heartbeat
The app can add its own fields to a printer's entry in the heartbeat, for example the result of the check after a job. They are merged over the node's own fields and may replace any of them except the printer name. An empty value removes them.
Jobs in flight
The printing event reports when a job starts and ends printing: the state, the source (Superroute or a LAN peer), the job ID or the peer name, the printer, the time and, at the end, the result. The node also returns the list of jobs printing now.
Label size of ZPL printers
The ZPL media query asks a printer set to ZPL for its resolution and the loaded label, without printing. It returns the resolution in dpi and the width and length in millimetres and in dots. It returns nothing when the printer does not answer, and a width and length of 0 when the printer reports only its resolution.
// Dart / Flutter
await node.setPrinterFields('Labels', {'language_kept': {'language': 'zpl', 'since': since, 'jobs': 12}});
node.events.listen((e) {
if (e is NodePrintingEvent) showPrinting(e.printing); // started / ended, cloud or peer
});
final inFlight = await node.printing();
final media = await SPrint.queryZplMedia(zebra); // dpi, widthMm, lengthMm, widthDots, lengthDots or null
# Python
node.set_printer_fields("Labels", {"language_kept": {"language": "zpl", "since": since, "jobs": 12}})
# on_event gets kind "printing" (e.printing: a cloud or peer job started / ended)
in_flight = node.printing()
media = SPrint.query_zpl_media(zebra) # ZplMedia(dpi, width_mm, length_mm, ...) or None
// C# / .NET
node.SetPrinterFields("Labels", new JsonObject { ["language_kept"] = kept });
node.Printing += (_, e) => ShowPrinting(e.Printer, e.Started); // cloud and peer jobs
var inFlight = node.PrintingNow;
var media = SPrint.QueryZplMedia(zebra); // dpi, width × length mm; null: no answer
// Swift
try node.setPrinterFields("Labels", fields: ["language_check": ["result": "confirmed", "language": "zpl"]])
for await e in node.events {
if case .printing(let p) = e { showPrinting(p.printer, p.state == "started") }
}
let inFlight = try node.printing()
let media = try SPrint.queryZplMedia(zebra) // dpi, width_mm, length_mm
// Kotlin
node.setPrinterFields("Labels", JSONObject().put("language_kept", kept))
// handle(ev): is NodeEvent.Printing -> showPrinting(ev.printing) // started / ended, cloud or peer
val inFlight = node.printing
val media = SPrint.queryZplMedia(zebra) // dpi, widthMm, lengthMm or null