by Intelliverse X

Charge bank end-to-end — wiring, factory attach, and sign-off

This is the page a manufacturer technician, an installer, and an on-call engineer should be able to follow without opening the code. It covers the Qbee / ZHZN sharing power-bank cabinet from the crate to the first paid rental, and the factory initial-setup questions that associate every accessory with the kiosk it is bolted to.

Companion pages:

Page What it is for
powerbank-rental.md Pricing, states, operator API, 2am procedures
Android manufacturer sign-off Camera, factory tests, accessory evidence and signatures
payments-nayax.md Nayax cashless on the vending side
ContentX docs/ops/NAYAX_ZHZN_CARD_ENABLEMENT.md Card tile + MDB on a ZHZN vending cabinet (different hardware)

Two hardware classes must not be mixed up:

Sharing charge-bank cabinet ZHZN vending kiosk
What it is Qbee-branded locker of rental power banks Snack / photo / card-pay cabinet
How it talks Raw TCP or MQTT, {CMD,…,CRC} frames Android APK → /zhzn/* HTTPS
Identity Door sticker, e.g. TPQBEEPA0100000311 equipmentNo / kiosk id, e.g. 1A8B08520A50
Customer pay Stripe Checkout on a phone (no app) Nayax card and/or Scan & Pay
Onboarded pair TPQBEEPA0100000311PB-311, …312PB-312 whatever the APK registers

A charge bank can live alone (its own machine record) or be bolted onto a vending kiosk. Factory attach is how the second case is recorded.


1. What “wired end to end” means

The path is finished only when all of these are true at once:

  1. The cabinet has power, a working uplink, and firmware pointed at our gateway (not the vendor’s).
  2. Its device id is on the onboarded list (or factory-attached to a kiosk) so the listener will accept the socket.
  3. A Kiosk-X machine record owns it (machineNo), so revenue and Operator X have someone to attribute.
  4. The door sticker is our QR (/rent/{deviceId}), not the vendor’s.
  5. Stripe is live, the hold is the taxed non-return fee, and a test rental has gone out and back on that exact cabinet.
  6. If this unit is a kiosk build: the factory tablet has answered the three attach questions and scanned every “yes” onto this kiosk id.

Until (4), every customer who scans is paying the vendor. Until (2), the cabinet is online from its own point of view and invisible from ours.


2. Physical wiring (manufacturer / bench)

2.1 Charge-bank cabinet (standalone or side-car)

AC mains ──► cabinet PSU ──► lock board + slot rails + banks
                         └──► 4G / Ethernet modem
                                    │
                                    ▼
                         TCP  : KIOSKX_POWERBANK_TCP_PORT (default off)
                         or MQTT to the configured broker

2.2 When it is attached to a vending kiosk

The kiosk is a separate computer. Typical factory build:

                    ┌─────────────────────────────────────┐
                    │           Vending kiosk             │
  Touch + camera ──►│  Android APK  ──HTTPS──► /zhzn/*    │
  Vend board UART ─►│                                     │
  Nayax VPOS ──MDB─►│  (card path; see Nayax docs)        │
  Photo printer ───►│  USB / vendor cable                 │
                    └──────────────┬──────────────────────┘
                                   │ factory tablet
                                   │ scans stickers
                    ┌──────────────▼──────────────────────┐
                    │  Qbee charge-bank sidecar           │
                    │  own modem, own TCP/MQTT            │
                    └─────────────────────────────────────┘

The sidecar does not ride the kiosk’s agent process. Association is a cloud bind: device id → kiosk machineNo. That bind is what factory attach writes.

2.3 Nayax on the kiosk (not on the charge bank)

Card payment for snacks is the VPOS Touch on the kiosk, MDB to the APK. Charge-bank rentals are Stripe on the shopper’s phone. Do not expect a Nayax tap to eject a power bank.

Two Nayax numbers exist. Only one is bindable:

What Example Bind this?
Device Number (terminal id) 4434331225106629 Yes — on the reader sticker / Core
Machine ID (MoMa record) 111146311 No — webhooks never match it

The factory tablet refuses a 9-digit scan as “looks like a Machine ID”.

2.4 Photo print

The photobooth is the kiosk camera + ContentX compose pipeline. A physical printer (DNP / similar) is an optional accessory. Factory records fitted / not fitted (and a serial if one exists). It does not turn the photobooth feature flag on — that stays an operator decision after the camera has passed.


3. Cloud wiring

3.1 Environment (gateway)

KIOSKX_POWERBANK_PW=<cabinet PW>
KIOSKX_POWERBANK_TCP_PORT=9500          # 0 = listener off
# or
KIOSKX_POWERBANK_MQTT=1

KIOSKX_POWERBANK_CABINETS=TPQBEEPA0100000313:PB-313:Hotel lobby

# Never in production:
# KIOSKX_POWERBANK_ALLOW_UNKNOWN_DEVICES=1

TCP must sit behind an internal NLB restricted to the cabinets’ source addresses. The protocol authenticates nothing. See powerbank-rental.md § “What the TCP port actually exposes”.

Stripe: same keys as Scan & Pay. Checkout uses capture_method=manual. The authorised amount is NONRETURN_USD × (1 + TAX_RATE), not the bare fee.

3.2 Firmware on the cabinet

Service menu → server host / port (or MQTT product key) → our gateway. Reboot. Within a minute:

GET /api/v1/powerbank/cabinets          # machines:read
# online: true, slots populated

If it never appears: wrong host, listener still 0, device id not onboarded, or CRC/PW mismatch. GET /api/v1/powerbank/healthtransport.

3.3 Door QR (step 4 — cannot be done remotely)

The cabinets arrive wearing Qbee's own sticker. Until it is covered, every customer who scans pays the vendor.

What URL
Customer page (what the sticker must encode) GET /rent/{deviceId}
Bare QR PNG GET /rent/{deviceId}/qr.png
Print-ready 4×6 sticker (QR + rate + device id) GET /rent/{deviceId}/sticker.png
Operator pack (JSON of every owned cabinet) GET /api/v1/powerbank/stickers
Operator print sheet GET /api/v1/powerbank/stickers/print
Operator download GET /api/v1/powerbank/cabinets/{deviceId}/sticker.png

Onboarded pair (production KIOSKX_BASE_URL, default https://api.kiosk-x.ai):

Cabinet Page Sticker
TPQBEEPA0100000311 /rent/TPQBEEPA0100000311 /rent/TPQBEEPA0100000311/sticker.png
TPQBEEPA0100000312 /rent/TPQBEEPA0100000312 /rent/TPQBEEPA0100000312/sticker.png

Print, cover the vendor code, scan with a phone. The page must be ours. Then run one Stripe out-and-back (and a Nayax tap if this unit has a reader).


4. Factory initial setup (the three questions)

Record these facts using the authorized bench workflow before sign-off and before operator claim. The current ZHZN APK does not have an accessory scan/write screen. Do not search for a planned tablet control. Match the actual cabinet and equipmentNo before each write, and read back the saved binding.

4.1 Questions

# Question If no If yes
1 Power bank attached? {attached: false} — done Scan the door sticker (vendor QR or Kiosk-X /rent/… URL). We store deviceId and bind it to this kiosk.
2 Photo print attached? {attached: false} Mark fitted and record a serial when present. Physical print acceptance is still required before shipping.
3 Nayax device attached? {attached: false} Scan the Device Number on the reader (16-digit typical). Bind POST-equivalent to this kiosk.

attached: false is a complete answer. Shipping without accessories is normal. attached: true with no scan blocks sign-off.

Saying not fitted after a bad scan unbinds that accessory from this kiosk (power-bank pointer cleared, Nayax returned to a provisional placeholder, photo-print flag cleared). Do not assume a later attached: false is a no-op.

A later POST /zhzn/factory-test that repeats only some kinds merges with scans already on file. Unmentioned kinds stay. Send attached: false for a kind you want to take back.

4.2 QR scan API (authorized bench tooling)

Use the approved bootstrap authentication before enrolment, or the established device credential/witness after enrolment. A Partner manufacturer key does not replace cabinet authentication on this route. Follow the detailed factory sign-off procedure and verify the same serial.

One accessory at a time (scan → bind → next question):

POST /zhzn/factory-attach
{
  "kind": "powerbank" | "photoPrint" | "nayax",
  "attached": true,
  "scanned": "https://kiosk.example/rent/TPQBEEPA0100000311"
}

scanned may be a raw id, a rent URL, or a query with deviceId / terminalId. The gateway keeps only the id.

Response (shape that matters on the screen):

{
  "code": 200,
  "data": {
    "machineNo": "1A8B08520A50",
    "kind": "powerbank",
    "deviceId": "TPQBEEPA0100000311",
    "attached": true,
    "bound": true,
    "attachments": { "...": "full kit so far" },
    "canSignOff": false,
    "blockers": []
  }
}

bound: false + bindError is a hard stop: already on another kiosk, or a Nayax Machine ID. Do not sign.

Whole kit with the hardware report:

POST /zhzn/factory-test
{
  "results": { "camera": "pass", "speaker": "pass", "mic": "pass",
               "touch": "pass", "screen": "pass", "board": "pass" },
  "ports": {
    "board": { "acked": "/dev/ttyS3", "protocol": "csm", "module": "SH" },
    "camera": { "id": "0", "facing": "front" }
  },
  "attachments": {
    "powerbank":  { "attached": true,  "scanned": "TPQBEEPA0100000311", "source": "qr" },
    "photoPrint": { "attached": true,  "model": "DNP DS620" },
    "nayax":      { "attached": true,  "scanned": "4434331225106629", "source": "qr" }
  }
}

Older APKs that send no attachments still sign off — a cabinet with no kit is not defective. Once a kind is present and attached: true, the scan is required (photo print excepted).

4.3 What the bind actually writes

Accessory Durable record Runtime effect
Power bank machine.powerbankDeviceId + cabinet machine_no Rentals and revenue attribute to this kiosk; ejects target this cabinet
Nayax machine.nayax.terminalId, provisional: false Card webhooks settle to this kiosk
Photo print machine.photoPrintAttached (+ optional photoPrintDeviceId) Provenance only; photobooth flag stays operator-controlled

On gateway restart, powerbankDeviceId on the machine is rebound so a sidecar does not fall back to the standalone PB-311 placeholder.

4.4 Sign-off gate (extra blockers)

Existing rules still apply (required tests pass, board port + module, camera id). New ones:

A technician cannot finger-sign past these. /zhzn/factory-sign re-checks the same list.

4.5 After the ocean — field installer

Follow first-time installation: authorized claim, reviewed address match, separate on-site installation declaration, configured payment, actual stock and physical acceptance. Read the saved accessories; their presence does not establish receiving-operator acceptance. Also:

  1. Confirm GET /api/v1/machines/{no}/factory-test shows the kit and stage: signed_off.
  2. Confirm GET /api/v1/powerbank/cabinets/{no} is online (sidecar pointed at production).
  3. Confirm GET /api/v1/machines/{no}/nayax has the Device Number and provisional: false if a reader was fitted.
  4. Replace any remaining vendor QR on the charge-bank door.

5. Customer path (charge bank)

No app.

  1. Scan door → /rent/{deviceId} (or the kiosk’s bound cabinet).
  2. Read rate, 24-hour cap, hold, 72-hour return deadline, terms (the numbers Operator X last saved for that station or fleet).
  3. Tap Rent now → Stripe hold.
  4. Take the bank from the named slot.
  5. Return to any Kiosk-X charge-bank station. Meter stops; we capture only the metered amount.

Losing the tab: scan the same door again. The cookie + server guard send them back to the running rental; a second hold is not opened.


6. Operator path

Need Call
See the sidecar GET /api/v1/powerbank/cabinets
Re-bind after a move POST /api/v1/powerbank/cabinets/{id}/bind {"machineNo":"…"}
Test eject (no money) POST /api/v1/powerbank/cabinets/{id}/eject {"slot":1}
Bind a reader later POST /api/v1/machines/{no}/nayax {"terminalId":"…"}
Factory kit + cert GET /api/v1/machines/{no}/factory-test
Revenue GET /api/v1/revenue/summarystreams.powerbankRentals

Ownership: an operator can only bind a cabinet or a reader they own, onto a machine they own. Admin can do both.


7. End-to-end test at the factory (before the crate closes)

Run on the exact unit that will ship, not a lab twin.

Step Pass looks like
Factory test all-green + ports identified canSignOff true except kit
Q1–Q3 answered; every yes scanned attachments.*.bound == true
Charge-bank firmware → staging/prod gateway cabinet online, slots > 0
Free eject slot 1 Bank physically leaves; no rental row. This is POST /api/v1/powerbank/cabinets/{id}/eject with an operator key — the factory tablet cannot do it over /zhzn/*.
Seat it back Slot returns to rentable (charge ≥ min)
Phone: scan our QR, authorise, take, return in grace Status “Returned”; hold released; $0 captured. Needs a phone + live Stripe, not the APK.
If Nayax fitted: one $0.00 / $0.01 Core test tap Webhook lands on this machineNo. Physical tap; there is no /zhzn/selfcheck in this build.
If photo print fitted: camera test frame + (optional) test print Factory camera pass; printer feeds
Android handwritten certificate Actual certificate read back for this serial/run; a queued upload is unfinished.
Separate Partner factory signature Final physical FAT and build evidence reviewed and signed by the authorized manufacturer; retain both factory records before shipping.

The Android certificate alone is not shipping acceptance. Follow manufacturer acceptance, including all physical tests and both factory signatures. Operator delivery acceptance is a separate receiving milestone.

Do not sign a unit whose charge-bank sidecar is still pointed at the vendor cloud. You cannot fix that sticker or that host from Operator X.


8. Real-world scene matrix (sign-off)

Exercised in code by tests/test_powerbank_*.py, tests/test_factory_attachments.py, and the review-fix suite. Each row is a scene a reviewer ran, not a slogan.

Scene Expected Status
Happy rent → return inside grace $0 capture, hold released Pass
Return after hours / at daily cap Capture ≤ hold Pass
Non-return at 72h Closed as non-return, not “Returned — thank you” Pass
Accrual hits hardware price (~24h+1s) Converts to sale; meter stops Pass
Tax on (e.g. 8.875%) Hold grossed up; capture clamped to hold Pass
Double-tap / retry / Back from Stripe One rental, one hold Pass
Close tab, re-scan door Resume running rental Pass
Cabinet empty / jam after pay Hold released; page says nothing came out Pass
Late eject report after we timed out Adopted as ACTIVE, not discarded Pass
Pod dies mid-dispense Sweep voids / resolves; hold not frozen forever Pass
Two replicas, stale “authorised” write Cannot demote ACTIVE Pass
Cabinet reboot (returnState 0) Slots not offered until re-acked Pass
Stale socket teardown after redial Live connection kept Pass
Offline return hours later Billed to last-seen, not reconnect Pass
Unknown device on TCP Refused Pass
Empty CRC key Not a universal pass Pass
Factory: no accessories Sign-off allowed Pass
Factory: “yes” and no scan Sign-off blocked Pass
Factory: scan rent URL Id parsed, bound to this kiosk Pass
Factory: same bank on two kiosks Second bind refused Pass
Factory: Nayax Machine ID Refused; named as Machine ID Pass
Factory: Nayax Device Number Bound, provisional: false Pass
Factory: photo print yes, no serial Bound; sign-off allowed Pass

Open, accepted risk: two shoppers can still pass the last-bank availability check in the same instant. Duplicate-rental guard covers the common double-tap; a true slot reservation is not in this build. Do not put two cabinets on one busy doorway without knowing that.

Operator X may still list PB-311 / PB-312 after a sidecar is factory-attached to a real kiosk. Rental traffic follows the kiosk bind; the leftover standalone row is a placeholder, not a second cabinet.

Swarm sign-off (19 Aug 2026): software wiring YES WITH CAVEATS. Crate-close on a shipping unit NO until §7 is run on that exact cabinet (our gateway, our QR, one real Stripe out-and-back). Factory three-question + QR→kiosk association is signed off in tests/test_factory_attachments.py.

Not signed off on hardware we do not have in this repo: a live Qbee cabinet against production Stripe, and a live VPOS MDB session on the same kiosk. Those need a bench or site run using §7. Software paths for both are covered.


9. When it is wrong

Symptom First check
Cabinet online locally, missing in API Onboarded list / factory powerbankDeviceId / TCP port 0
Customer paid, no bank GET /api/v1/powerbank/health needsAttention; eject queue; online
Card tap on kiosk does nothing for a rental Expected — rentals are Stripe. Card is Nayax on vending.
Webhook on the wrong machine Bound Machine ID instead of Device Number; re-scan
Two holds on one phone Cookie blocked (in-app browser) + they started twice on two cabinets; support from /rent/r/{id} footer
$39.99 and the page says Returned Should not happen after the review fixes; if it does, charge.basis / unreturnedReason on /rent/api/{id}

Full 2am table: powerbank-rental.md § “When something is wrong”.


10. Implementation map

Piece Where
Protocol / CRC app/powerbank/protocol.py
Live cabinet app/powerbank/cabinet.py
Money + meter app/powerbank/rentals.py
TCP / MQTT app/powerbank/transport.py
Seed + factory rehydrate app/powerbank/onboarding.py
Customer + operator HTTP app/routes/powerbank.py
Factory questions + QR bind app/factory_test.py, POST /zhzn/factory-attach
Nayax bind POST /api/v1/machines/{no}/nayax