Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Displayformatet

Displayerna är speglar. All logik - vad en tangent betyder, vad som står på kvittot, vilken dialog som väntar - finns i kassan (Till, driven av web_till). En display får ett färdigt tillstånd och ritar det; den skickar tillbaka händelser (en tangent trycktes ned, en rad klickades) och väntar på nästa tillstånd. Den räknar aldrig själv, och den behöver inte veta varför ett tillstånd ser ut som det gör.

Samma tillstånd bär båda displayerna: operatörsdisplayens kvittovy och kunddisplayens två rader ligger i samma meddelande. De byggs ur samma Till i samma ögonblick, så de kan inte komma ur takt.

Transport

web_till har två portar:

port (standard)flaggaprotokollanvänds av
127.0.0.1:8090--httpHTTPstatiska filer, GET /state, POST /key_press m.fl.
127.0.0.1:8091--wsWebSocketoperatörsklienten web_client/app.js

GET /server_info.json svarar med den faktiska WebSocket-porten och kassans decimalinställning, så att en klient inte behöver gissa porten:

{"ws_port": 8091, "decimal_places": 0}

Kassan annonseras också på mDNS som _rgo-kassa._tcp.local. med ws_port och version i TXT-posten.

WebSocket är huvudvägen. Varje ansluten flik registreras, och efter varje tillståndsändrande kommando sänds det nya tillståndet till alla anslutna flikar, inte bara den som skickade kommandot - en streckkod som läses in via HTTP syns därmed i alla öppna fönster. Vid anslutning skickas aktuellt tillstånd direkt till den nya fliken.

HTTP-pollning finns för klienter som inte kan hålla en WebSocket: ES5-klienten web_client/app.es5.js (gamla Android-WebViews på V-R100 och CHD6800) pollar GET /state var 2:a sekund och skickar tangenter med POST /key_press, och kundfönstret web_client_customer/customer.js pollar GET /state en gång i sekunden och skickar aldrig något.

Kuvertet

Varje WebSocket-meddelande, i båda riktningar, är ett kuvert:

{"destination": "operator_display", "type": "state", "payload": { ... }}
fältbetydelse
destinationAlltid "operator_display" i dag. Finns för att en ny mottagare ska kunna bli ett nytt värde i stället för ett nytt protokoll. Servern läser inte fältet i inkommande meddelanden; klienten app.js kastar meddelanden med annan destination.
typeMeddelandets slag, se nedan.
payloadInnehållet.

Från kassan kommer två typer: state och terminal_display. Från klienten tas tre emot: key_press, select_tab och select_line. Ett meddelande av annan typ, eller som inte är giltig JSON, ignoreras.

GET /state och POST /key_press svarar med exakt samma state-kuvert som WebSocket-vägen skickar.

Tillståndet (type: "state")

Byggs av snapshot_to_json (web_till/json.rs) ur Till::display_snapshot och Till::customer_display_snapshot.

fälttypbetydelse
buffersträngDet som är inslaget på sifferbordet. "0" när inget är inslaget. Med decimaler påslagna kan den innehålla ett , ("35,5"). Det är inslagna tecken, inte ett tolkat belopp.
modesträngVad kassan väntar på: "price", "awaiting_confirmation", "registering_product" eller "awaiting_credit_checksum".
promptsträng eller nullFärdig svensk text att visa. Satt för awaiting_confirmation och awaiting_credit_checksum, annars null.
barcodesträng eller nullDen lästa streckkoden när mode är registering_product, annars null.
terminal_displaysträng eller nullKortterminalens aktuella text (Nets), eller null. Se nedan.
cart_lineslistaDet pågående kvittots rader: {"group_name": sträng, "amount_minor": heltal}. group_name är varugruppens namn, eller artikelns namn vid streckkodsförsäljning. Belopp i öre.
subtotal_minorheltalKvittots summa i öre.
vat_minorheltalKvittots moms i öre, summerad över momssatserna.
receipt_display_lineslista av DisplayLineKvittot som rader och kolumner, se nedan. Tom lista när inget är inslaget.
customer_displayobjektKunddisplayens två rader: {"row1": [DisplayColumn], "row2": [DisplayColumn]}.
selected_line_event_idheltal eller nullRaden som är markerad (dess row_id), så att alla flikar markerar samma rad.
active_tab_indexheltalVilken flik i varupanelen som är vald.
last_errorsträng eller nullEtt engångsmeddelande som hör till just den händelse som gav svaret. Se nedan.

Ordningen på nycklarna i JSON är inte garanterad.

mode och prompt

mode slår ihop flera interna väntelägen till ett enda awaiting_confirmation, eftersom en display bara behöver visa en Ja/Nej-fråga, inte veta vilken. Kassan väljer i samma ordning som den själv tolkar nästa Ja/Nej, så frågan som visas är alltid den som besvaras:

lägeprompt
Z-rapportens utskrift misslyckades"Z-rapport: försök skriva ut igen? (Y/N)"
kontrollenheten svarade inte"Kontrollenheten svarar inte - försök igen? (Ja/Nej)"
journalen kunde inte skrivas"Försök igen? (Y/N)"
kassans interna utskrift väntar på svar"Skriv kvitto? (Y/N)"

registering_product betyder att en streckkod lästes som inte finns i varuregistret: operatören slår ett pris och en varugrupp, och varan läggs upp och säljs i samma tryck. awaiting_credit_checksum betyder att ett tillgodobelopp har slagits in och kassan väntar på kontrollsumman från tillgodokvittot; prompt innehåller då texten "Tillgodo <kr>,<öre> - ange checksumman, tryck TILLGODO".

receipt_display_lines och DisplayLine

Raderna är samma ReceiptLayout som ett kvitto skrivs ut från (se Utskriftsformatet), omräknade till kolumner med position. Under ett pågående köp används den förenklade layouten build_receipt_layout_in_progress: en rad per inslagen rad plus en SUBTOTAL-rad. Efter betalningen, så länge kvittot ligger kvar i “slussen” i väntan på att skrivas ut eller avslutas, visas i stället hela det betalda kvittot (build_receipt_layout, utan logga) - med betalsätt, växel och kontrollkod, exakt som på papperet.

fälttypbetydelse
columnslista av DisplayColumnRadens kolumner.
row_idheltal eller nullevent_id för radens LINE_ADDED i journalen. Sätts bara på varurader i ett pågående köp; null för SUBTOTAL och för alla rader i ett betalt kvitto. Det är värdet klienten skickar i select_line.
stylesträngRadens stil med samma namn som på kvittot: normal, header, bold_total, dbl_height, dbl_width, dbl_size. Kolumnstilar (column_styles) följer inte med.

En bildrad blir en kolumn med texten "[BILD: <source>]" och en QR-rad blir "[QR]"; inga sådana rader förekommer i displaylayouterna i dag.

DisplayColumn

{"text": "35:00", "percent": 85.0, "align": "right"}
fälttypbetydelse
textsträngDet som ska stå. Belopp är formaterade som på kvittot ("35:00").
percenttalKolumnens position i procent av radens bredd.
align"left", "right", "center"Justering.

percent betyder olika saker beroende på align, och det är den vanligaste källan till fel i en egen klient:

alignpercent ärkolumnens högerkant
leftvänsterkanten-
rightkolumnens början; texten slutar där nästa kolumn börjar, eller vid 100 % för sista kolumnennästa kolumns percent, annars 100
centermittpunkten - en centrerad rad över hela bredden har percent 50-

För en flerkolumnsrad kommer percent direkt från kvittoradens tabs_percent, och align från alignments. För en enkolumnsrad är percent 0 för vänster och höger och 50 för centrerat. Kolumnerna ska placeras mot displayens faktiska bredd (operatörsklienten gör det med absolut positionerade element i procent), inte paddas till ett antal tecken - skärmens ruta är bredare än kvittots 35 tecken.

Kunddisplayen: row1 och row2

Kunddisplayen är tänkt som en fysisk display med 2 rader om 20 tecken, utan grafik. Den visar bara den senaste händelsen av tre slag, inte hela kvittot:

senaste händelserow1row2
ingenting ännu (sedan kassan startade)[][]
en rad slogs invarans namn, vänster, percent 0antal (vänster, percent 0) och radens belopp (höger, percent 50)
SUBTOTAL trycktes"Att betala", vänsteråterstående belopp, höger
betalningen klar"Växel", vänsterväxeln, höger ("0:00" för allt utom kontant med angivet belopp)

Kunddisplayens tillstånd journalförs inte och återskapas inte efter en omstart; det är en markör för “vad visas nu”, inget annat.

Begränsningarna:

  • 20 tecken per rad. Kassan kortar inte texten. Både webbfönstret (customer.js, layoutRow) och den fysiska displayen i Casio-appen (CaiosCustomerDisplay.layoutRow) lägger ut kolumnerna på 20 teckenpositioner och klipper varje kolumn vid sin plats - ett långt varunamn blir alltså avklippt.
  • Ingen grafik och inga stilar. DisplayColumn har ingen stil.
  • I row2 för en inslagen rad börjar antalet vid percent 0 och beloppet vid percent 50. Klienterna räknar en kolumns slut som nästa kolumns percent, så antalet får radens vänstra halva (10 tecken) och beloppet den högra, högerställt mot kanten. Både customer.js och Casio-appens layoutRow lägger alltså ut raden likadant. Ett belopp längre än 10 tecken klipps.
  • Antalet i row2 är det verkliga antalet (multiplikatorn som slogs in), trots att en kommentar i koden fortfarande säger att det alltid är 1.

Casio-appens skrivartjänst (android-casio-receipt-print) tar emot exakt den här formen på POST /display ({"row1": [...], "row2": [...]}, eller två strängar), men web_till skickar inte dit i dag.

terminal_display och meddelandet terminal_display

Under ett kortköp mot Nets visar kortterminalen uppmaningar (“Sätt i kort”, “Ange PIN”). De skickas inte genom Till, som står blockerad i kortbetalningen hela tiden, utan direkt från terminalens läsartråd som ett eget meddelande till alla WebSocket-klienter:

{"destination": "operator_display", "type": "terminal_display", "payload": {"text": "Ange PIN"}}

"text": null betyder att köpet är slut och texten ska bort. Samma text läggs också i fältet terminal_display i state, så att en pollande klient ser den. Eftersom state byggs av den blockerade kassatråden kan det fältet vara inaktuellt null under köpet; app.js låter därför state bara fylla i en terminaltext, aldrig ta bort den - bara terminal_display-meddelandets null tar bort den. Worldline-terminalen skickar inga sådana texter.

last_error

last_error hör till den enskilda händelsen, inte till kassans tillstånd. Det sätts bara i svaret på händelsen som orsakade det och är null i nästa svar. GET /state har alltid null.

För key_press bär det tre slags meddelanden, i den här prioritetsordningen:

  1. Ett fel från kassan själv (Till::handle), när tangenten inte kunde utföras - till exempel "Inget att betala - registrera minst en rad först", ett nekat kortköp eller en okänd varugrupp. Felet loggas också.
  2. Kontrollenhetens fel vid en betalning.
  3. Utfallet av SKVFS-exporten ("SKVFS-export skapad: <sökväg>" eller "SKVFS-export misslyckades: ...").

Texterna är skrivna för kassören och är på svenska. Kortterminalens och bokföringens fel vid en rapport når inte last_error; de loggas bara. För select_tab, select_line och device_event blir varje fel last_error.

Meddelanden från klienten

key_press

{"destination": "operator_display", "type": "key_press",
 "payload": {"scancode": 101, "event_type": "make", "caps_lock": false}}
{"destination": "operator_display", "type": "key_press",
 "payload": {"scancode": 101, "event_type": "break", "caps_lock": false}}
fälttypbetydelse
scancodeheltalTangentens kod. Klienten har redan översatt en knapp eller en fysisk tangent till en kod via config/panel_layout.json och config/web_keymap.json; kassan gör ingen tangentmappning, bara tolkning av koden.
event_type"make" eller "break"Nedtryckning respektive uppsläppning. Obligatoriskt. Saknas det eller har ett annat värde kastas meddelandet - kassan gissar inte, eftersom en felgissning tyst skulle förstöra spårningen av Shift/Ctrl/Cmd.
caps_lockboolOm Caps Lock var på. Valfritt, standard false.

Varje tangent är två meddelanden. Kassan gör ingenting på make utom att minnas vilka modifierare som var nedtryckta just då; funktionen utförs på break, med modifierarna från tangentens egen make. En knapp på skärmen skickar make och break direkt efter varandra.

Svaret är ett state-kuvert till alla anslutna flikar. Undantag: när ett kortköp mot Nets pågår och make för C (CLEAR) kommer, avbryts köpet i terminalen i stället, och meddelandet går aldrig till kassan. Över WebSocket skickas då inget tillstånd; över HTTP blir svaret {"cancelled": true}.

select_tab

{"destination": "operator_display", "type": "select_tab", "payload": {"tab_index": 5}}

Ett direkt klick på en flik i varupanelen. Till skillnad från att slå flikens nummer och trycka på fliktangenten rör det inte det inslagna beloppet.

select_line

{"destination": "operator_display", "type": "select_line", "payload": {"row_id": 7}}

Markerar en rad i kvittovyn (row_id från receipt_display_lines). Markeringen tar inte bort raden - det gör BACKA, som då tar bort den markerade raden. Ett felaktigt tryck på en pekskärm ska aldrig kunna makulera en rad, och en makulering ska alltid journalföras.

HTTP-motsvarigheterna

HTTP-vägarna tar inte kuvertet, bara innehållet i payload, och svarar med ett state-kuvert (som också sänds till alla WebSocket-flikar):

vägkropp
POST /key_press{"scancode": 101, "event_type": "break", "caps_lock": false}
POST /select_tab{"tab_index": 5}
POST /select_line{"row_id": 7}
POST /device_event{"device_type": "scanner", "value": "7300000000000"}
GET /state-

Saknade fält ger 400 med {"error": "..."}. Om kassatråden inte svarar blir det 500 med {"error": "till thread unavailable"}.

POST /device_event är vägen för seriella inmatningsenheter - streckkodsläsare, magnetkortsläsare, RFID - som receiptprinter vidarebefordrar. device_type används bara i felmeddelanden; alla enheter behandlas lika. Värdet tolkas först som en tillgodo-QR-kod (om kassan har en signeringsnyckel), annars som en streckkod: finns varan säljs den, annars går kassan till registering_product.

Statiska filer: / serverar operatörsklienten, /customer/ kundfönstret, /gl/ ett experimentellt WebGL-skal, och /config/... filerna i kassans konfigurationskatalog (till exempel panel_layout.json).

Exempel

Två rader är inslagna och operatören har tryckt SUBTOTAL:

{
  "destination": "operator_display",
  "type": "state",
  "payload": {
    "buffer": "0",
    "mode": "price",
    "prompt": null,
    "barcode": null,
    "terminal_display": null,
    "cart_lines": [
      {"group_name": "Kaffe", "amount_minor": 3500},
      {"group_name": "Bullar", "amount_minor": 1250}
    ],
    "subtotal_minor": 4750,
    "vat_minor": 950,
    "receipt_display_lines": [
      {"columns": [{"text": "Kaffe", "percent": 0.0, "align": "left"},
                   {"text": "35:00", "percent": 85.0, "align": "right"}],
       "row_id": 7, "style": "normal"},
      {"columns": [{"text": "Bullar", "percent": 0.0, "align": "left"},
                   {"text": "12:50", "percent": 85.0, "align": "right"}],
       "row_id": 8, "style": "normal"},
      {"columns": [{"text": "SUBTOTAL", "percent": 0.0, "align": "left"},
                   {"text": "47:50", "percent": 85.0, "align": "right"}],
       "row_id": null, "style": "bold_total"}
    ],
    "customer_display": {
      "row1": [{"text": "Att betala", "percent": 0.0, "align": "left"}],
      "row2": [{"text": "47:50", "percent": 0.0, "align": "right"}]
    },
    "selected_line_event_id": null,
    "active_tab_index": 0,
    "last_error": null
  }
}

(Momsen förutsätter 25 % på båda raderna: 700 + 250 öre.)

Direkt efter att raden “Kaffe” slogs in med multiplikatorn 2 såg kunddisplayen ut så här:

"customer_display": {
  "row1": [{"text": "Kaffe", "percent": 0.0, "align": "left"}],
  "row2": [{"text": "2", "percent": 0.0, "align": "left"},
           {"text": "70:00", "percent": 50.0, "align": "right"}]
}

vilket på 2x20 blir:

Kaffe
2              70:00