Ein Inventarprogramm anbinden¶
Eine Anleitung für die Entwickler eines Programms, das über den Druckserver Etiketten druckt, hier ein Inventarprogramm, das Inventaretiketten druckt: ein Etikett, eine Serie von Etiketten in einer Anfrage, den Auftrag verfolgen, nach einem Papierstau weiterdrucken und Fehler behandeln. Jede Anfrage ist in der API-Referenz beschrieben, die Einrichtung des Servers in der Serverinstallation.
Die Ausgangslage¶
Die Beispiele verwenden diese Namen; Ihre werden anders lauten:
- Der Server:
https://labels.example.com/api/v1, erreichbar über den Reverse Proxy der Firma. - Das Etikett
asset-tag.tavlettaim Etikettenordner, gestaltet in der Tavletta-App, mit einem QR-Code und einem Text, die beide den Platzhalter{AssetNumber}enthalten. - Der Drucker
it-cabausprinters.toml: ein cab-Etikettendrucker mit Thermotransfer (method = "transfer"), der um 180 Grad gedreht druckt (upside_down = true). - Ein Zugangstoken mit der Rolle
print, beschränkt aufit-cab, erstellt vom Administrator des Servers:
tavletta token new inventory-tool --role print --printer it-cab \
--tokens tokens.toml --printers printers.toml
Bewahren Sie das Token im Geheimnisspeicher des Programms auf (verschlüsselte Konfiguration, ein Vault), nicht im Code. In den Shell-Beispielen:
TAVLETTA=https://labels.example.com/api/v1
TOKEN=tav_3f9c0a1b2c4d_Xq2…
1. Was das Etikett braucht¶
curl -H "Authorization: Bearer $TOKEN" "$TAVLETTA/labels/asset-tag"
Die Antwort nennt die Spalten des Etiketts, columns (hier
["AssetNumber"]): Jede Datenzeile braucht diese Schlüssel. Ein Platzhalter
ohne Wert in der Zeile lässt den Druck mit 422 label_unprintable ("No such
column") abweisen, so druckt ein Etikett nie einen halb leeren Text.
2. Vorschau¶
Eine Vorschau druckt nichts und liefert das Etikett als PNG-Bild, zum Beispiel um es vor dem Drucken im Programm zu zeigen:
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"label": "asset-tag", "printer": "it-cab", "data_rows": [{"AssetNumber": "100001"}]}' \
-o preview.png "$TAVLETTA/preview"
- Mit
"format": "json"ist die Antwort JSON mit dem Bild in Base64 und den Warnungen,warnings(zum Beispielfont_missing,barcode_narrow). "view": "printer"zeigt das Bild so, wie der Drucker es bekommt (gedreht).- Eine Vorschau braucht nur die Rolle
preview.
3. Ein Etikett drucken¶
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"label": "asset-tag", "printer": "it-cab", "data_rows": [{"AssetNumber": "100001"}], "wait": 30}' \
"$TAVLETTA/print"
- Der Server prüft die Anfrage, reiht einen Auftrag für den Drucker ein und
antwortet mit dem Auftrag. Mit
"wait": 30wartet er bis zu 30 Sekunden auf den Auftrag: 200 mit"state": "done", sobald gedruckt, sonst 202 mit dem Auftrag nochqueuedoderprinting. - Der Header
Locationder Antwort und dieiddes Auftrags führen zuGET /api/v1/jobs/{id}. - Jeder Drucker druckt einen Auftrag nach dem anderen; Aufträge verschiedener Drucker laufen nebeneinander.
4. Eine Serie in einer Anfrage¶
Zwanzig Inventaretiketten sind eine Anfrage mit zwanzig Datenzeilen, nicht zwanzig Anfragen: Sie werden als ein Auftrag der Reihe nach gedruckt, und ein Problem hält den Auftrag bei einer bekannten Zeile an.
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"label": "asset-tag", "printer": "it-cab", "wait": 60,
"data_rows": [{"AssetNumber": "100001"}, {"AssetNumber": "100002"},
{"AssetNumber": "100003"}, {"AssetNumber": "100020"}]}' \
"$TAVLETTA/print"
Das empfohlene Vorgehen: Das Programm verwaltet die Nummern. Das Programm nimmt die nächsten freien Inventarnummern aus seiner eigenen Datenbank, speichert sie bei den Geräten und schickt sie als Datenzeilen. Das Etikett zählt nie selbst, so können ein zweiter Server, eine zurückgespielte Sicherung oder ein Nachdruck nie eine Nummer doppelt vergeben.
Die Alternative: der Zähler des Etiketts¶
Ein Etikett mit der Variablen {=counter} (zum Beispiel {=counter:000000}
für sechs Stellen) zählt selbst. Das Programm kann trotzdem sagen, wo es
beginnt:
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"label": "asset-tag-counter", "printer": "it-cab",
"counter_start": 100001, "copies": 20, "no_save_counter": true}' \
"$TAVLETTA/print"
counter_startist die erste Nummer; der Zähler steigt mit jedem Etikett und jeder Kopie: 100001 bis 100020.no_save_counter: Der Server speichert die nächste Nummer nicht in der Etikettendatei. Ohne diese Angabe behält die Etikettendatei die nächste Nummer (100021), und der nächste Druck ohnecounter_startmacht dort weiter.- Verwenden Sie dies nur, wenn die Nummern nicht schon vor dem Drucken in der Datenbank des Programms stehen müssen.
5. Den Auftrag verfolgen¶
curl -H "Authorization: Bearer $TOKEN" "$TAVLETTA/jobs/$JOB"
state |
Bedeutung |
|---|---|
queued |
wartet auf den Drucker |
waiting |
wartet auf die Etikettendatei (ein anderer Auftrag speichert seinen Zähler) |
printing |
druckt; printed von total Etiketten sind draussen |
done |
alle Etiketten gedruckt |
failed |
angehalten: error sagt warum, rows_printed wie weit er kam |
canceled |
abgebrochen |
- Fragen Sie alle ein bis zwei Sekunden nach, solange der Zustand nicht endgültig ist; der Server behält abgeschlossene Aufträge 24 Stunden lang.
- Ein
print-Token sieht nur seine eigenen Aufträge; der Auftrag eines anderen Tokens ist "nicht gefunden".
6. Nach einem Stau: den Rest drucken¶
Gehen dem Drucker die Etiketten aus oder staut er, schlägt der Auftrag fehl, zum Beispiel:
{"state": "failed", "printed": 13, "total": 20, "rows_printed": 13,
"error": {"code": "printer_out_of_labels",
"message": "… Printing stopped at row 14. To continue, print again from row 14."}}
Sobald der Drucker bereit ist, schicken Sie dieselbe Anfrage noch einmal mit
rows, den noch fehlenden Zeilen:
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"label": "asset-tag", "printer": "it-cab", "rows": "14-20",
"data_rows": [{"AssetNumber": "100001"}, {"AssetNumber": "100020"}]}' \
"$TAVLETTA/print"
rows zählt die Datenzeilen ab 1: "14-20", "14-" (bis zur letzten),
"5". Schicken Sie dieselben Datenzeilen wie zuvor, damit Zeile 14 dasselbe
Gerät ist. Ein Etikett, das halb gedruckt war, als der Drucker anhielt,
gehört zur fehlgeschlagenen Zeile und wird noch einmal gedruckt.
7. Abbrechen¶
curl -X DELETE -H "Authorization: Bearer $TOKEN" "$TAVLETTA/jobs/$JOB"
Ein wartender Auftrag wird sofort abgebrochen; ein druckender hält nach dem
Etikett an, das gerade gedruckt wird. 409 job_finished, wenn er schon
fertig war.
8. Wiederholen und Fehler¶
POST /print ist nicht idempotent: Eine zweimal geschickte Anfrage druckt
zweimal. Deshalb:
- Automatisch wiederholen nur Antworten, die abgewiesen wurden, bevor
ein Auftrag entstand: 503
busyund 429queue_full, nach einer wachsenden Pause (1, 2, 4, 8 s). - Nicht automatisch wiederholen, wenn die Verbindung abbrach oder in ein
Timeout lief, nachdem die Anfrage geschickt war: Der Auftrag kann
existieren und drucken. Zeigen Sie der Bedienperson, was geschehen ist,
lassen Sie sie den Drucker prüfen und drucken Sie dann die fehlenden
Etiketten mit
rows. - Die Anfrage korrigieren bei anderen 4xx-Antworten; der Code sagt, was falsch ist:
| Code | Was das Programm tut |
|---|---|
unauthorized (401) |
Token falsch, abgelaufen oder widerrufen: beim Administrator ein neues verlangen. |
forbidden (403) |
Die Rolle des Tokens reicht nicht. |
printer_not_found, label_not_found (404) |
Name falsch geschrieben, oder das Token darf diesen Drucker nicht verwenden. |
invalid_field, unknown_field (400) |
Ein Fehler im Programm: Die Meldung nennt das Feld. |
too_many_copies (400), too_many_labels (413) |
Die Serie auf mehrere Aufträge verteilen. |
label_unprintable (422) |
Die Daten passen nicht zum Etikett (eine fehlende Spalte, ein Code, der den Wert nicht fassen kann): die Meldung zeigen. |
busy (503), queue_full (429) |
Nach einer Pause wiederholen. |
printer_<code> in einem fehlgeschlagenen Auftrag |
Der Bedienperson die Meldung zeigen; nach der Behebung mit rows weitermachen. |
Die Meldung (error.message) ist in der Sprache von Accept-Language und
für Menschen gedacht: Zeigen Sie sie, aber entscheiden Sie nach dem Code.
Beispiel: Python¶
Mit dem Paket requests:
import os
import time
import requests
BASE = os.environ.get("TAVLETTA", "https://labels.example.com/api/v1")
TOKEN = os.environ["TAVLETTA_TOKEN"] # from the secret store, never in the code
FINAL = {"done", "failed", "canceled"}
class LabelError(Exception):
def __init__(self, status, code, message):
super().__init__("%s %s: %s" % (status, code, message))
self.status, self.code = status, code
session = requests.Session()
session.headers.update({"Authorization": "Bearer " + TOKEN, "Accept-Language": "de-CH"})
def call(method, path, **kwargs):
response = session.request(method, BASE + path, timeout=330, **kwargs)
if response.status_code >= 400:
try:
error = response.json()["error"]
except (ValueError, KeyError): # not from Tavletta, e.g. the proxy
error = {"code": "http_%d" % response.status_code, "message": response.text}
raise LabelError(response.status_code, error["code"], error["message"])
return response
def print_assets(numbers, printer="it-cab", rows=None):
"""One label per asset number, in one job; returns the finished job."""
body = {
"label": "asset-tag",
"printer": printer,
"data_rows": [{"AssetNumber": str(n)} for n in numbers],
"wait": 30,
}
if rows:
body["rows"] = rows # e.g. "14-20": only these rows
for attempt in range(5):
try:
job = call("POST", "/print", json=body).json()
break
except LabelError as e:
# refused before it was queued: nothing printed, safe to retry
if e.code not in ("busy", "queue_full") or attempt == 4:
raise
time.sleep(2**attempt)
while job["state"] not in FINAL: # still printing after "wait"
time.sleep(2)
job = call("GET", "/jobs/" + job["id"]).json()
return job
def preview(number, path="preview.png"):
body = {"label": "asset-tag", "data_rows": [{"AssetNumber": str(number)}]}
with open(path, "wb") as file:
file.write(call("POST", "/preview", json=body).content)
if __name__ == "__main__":
preview(100001)
numbers = range(100001, 100021) # 20 labels
job = print_assets(numbers)
if job["state"] == "failed":
print("Stopped:", job["error"]["message"])
# rows_printed rows are done: print the rest once the printer is fixed
rest = "%d-%d" % (job["rows_printed"] + 1, len(numbers))
print("To continue:", rest)
else:
print(job["state"], job["printed"], "of", job["total"])
Beispiel: C# und .NET¶
Mit HttpClient und System.Net.Http.Json (.NET 8 oder neuer), als
Konsolenprogramm; in einer Anwendung registrieren Sie einen HttpClient
(zum Beispiel mit IHttpClientFactory) und verwenden ihn wieder.
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
var baseUrl = Environment.GetEnvironmentVariable("TAVLETTA") ?? "https://labels.example.com/api/v1";
var token = Environment.GetEnvironmentVariable("TAVLETTA_TOKEN")!; // from the secret store
using var http = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"), Timeout = TimeSpan.FromSeconds(330) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
http.DefaultRequestHeaders.AcceptLanguage.ParseAdd("de-CH");
var labels = new LabelClient(http);
var numbers = Enumerable.Range(100001, 20).Select(n => n.ToString()).ToList();
var job = await labels.PrintAssetsAsync(numbers, "it-cab");
Console.WriteLine(job.State == "failed"
? $"Stopped: {job.Error?.Message} To continue: rows {job.RowsPrinted + 1}-{numbers.Count}"
: $"{job.State}: {job.Printed} of {job.Total}");
public record ApiError(string Code, string Message);
public record ErrorBody(ApiError Error);
public record Job(
string Id, string State, int Printed, int Total,
[property: JsonPropertyName("rows_printed")] int RowsPrinted, ApiError? Error);
public class LabelException(HttpStatusCode status, string code, string message) : Exception(message)
{
public HttpStatusCode Status { get; } = status;
public string Code { get; } = code;
}
public class LabelClient(HttpClient http)
{
static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
public async Task<Job> PrintAssetsAsync(IList<string> assetNumbers, string printer, string? rows = null)
{
var body = new Dictionary<string, object>
{
["label"] = "asset-tag",
["printer"] = printer,
["data_rows"] = assetNumbers.Select(n => new Dictionary<string, string> { ["AssetNumber"] = n }),
["wait"] = 30,
};
if (rows != null) body["rows"] = rows; // e.g. "14-20"
Job job;
for (var attempt = 0; ; attempt++)
{
try { job = await SendAsync<Job>(HttpMethod.Post, "print", body); break; }
catch (LabelException e) when (e.Code is "busy" or "queue_full" && attempt < 4)
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); // nothing was printed
}
}
while (job.State is "queued" or "waiting" or "printing")
{
await Task.Delay(2000);
job = await SendAsync<Job>(HttpMethod.Get, $"jobs/{job.Id}");
}
return job;
}
public Task<Job> CancelAsync(string jobId) => SendAsync<Job>(HttpMethod.Delete, $"jobs/{jobId}");
async Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null)
{
using var request = new HttpRequestMessage(method, path);
// as a string: the server needs a Content-Length (JsonContent would send it chunked)
if (body != null)
request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
ErrorBody? error = null;
try { error = await response.Content.ReadFromJsonAsync<ErrorBody>(Json); } catch (JsonException) { }
throw new LabelException(response.StatusCode,
error?.Error.Code ?? $"http_{(int)response.StatusCode}",
error?.Error.Message ?? response.ReasonPhrase ?? "");
}
return (await response.Content.ReadFromJsonAsync<T>(Json))!;
}
}
- Die Basisadresse endet mit
/, und die Pfade beginnen ohne ("print","jobs/…"): So behältHttpClientdas/api/v1. - Der Inhalt geht als
StringContent: Der Server braucht eineContent-Length, undJsonContent.Createwürde den Inhalt in Stücken ohne eine solche schicken (411length_required).