Vending manufacturers manual — Reyeah & ZHZN, end to end
The operator-facing manual for the two vending hardware families Kiosk-X
supports: Reyeah (a.k.a. reyeah-jd / Ruiye "JD", com.ruiye.jd) and
ZHZN (a.k.a. CSM, on the Huiwan T3568 board). It explains what actually
differs between them, and walks an operator/installer from a boxed machine to
selling on either one — registration, planogram, pricing, the customer
purchase, the physical product fall, receipt/telemetry, revenue, and Nayax.
Everything here is grounded in the real codebase; each claim cites the file it
rests on. Where the code cannot prove a hardware-only detail, the manual says
so and marks it confirm with hardware.
The one-sentence version. The two manufacturers converge on a single operator experience and a single cloud, and diverge only at the device layer: Reyeah is an Android kiosk APK that drops product over a
FF0055…serial protocol; ZHZN uses its own Android APK (ai.intelliverse.zhzn.kioskx) or a Python agent on Linux that drops product over the CSMAA…BBserial protocol. Neither is "pure install" for product fall — see Product-fall verdict.Android first install: Installation and operator handover. Linux env-file, SoftAP and systemd procedures elsewhere in this manual do not apply to the Android APK.
Factory line (ZHZN, bilingual): /manual/factory-zhzn — photo print is optional; power-bank door QR and Nayax Device Number (not Machine ID) must be scanned if fitted; unsigned crates are refused at the USA warehouse. Chinese: 照片打印选配;充电宝与读卡器若安装必须扫码绑定。
1. The two manufacturers at a glance
| Dimension | Reyeah (reyeah-jd) | ZHZN (CSM / T3568) |
|---|---|---|
| On-machine software | Android APK com.ruiye.jd (the universal "Kiosk X" build) |
ZHZN Android APK with WebView storefront; Python agent for Linux hosts |
| Android host board | Rockchip RK3288, Android 7.1.2 (API 25), 32-bit armeabi-v7a |
Rockchip RK3568 (Huiwan HW-T3568), Android/Linux, 6× COM |
| Dispense mechanism | Spring/aisle motors driven by the VMC | Y-lift (elevator/gantry) or spring board (SH_YAxis / SH) |
| Controller serial protocol | FF 00 55 … <chk> (reply FF 00 AA …), sum-mod-256 checksum |
AA <len> … <xor> BB (Y-lift) / no-checksum spring frames |
| Serial port(s) / baud | /dev/ttyS1 main (115200/9600), /dev/ttyS0 dispense (9600) |
/dev/ttyS4 dispenser (9600 8N1); a second COM for payment |
| Dispense command | FF00554102 <aisle> <qty> <chk> |
AA 14 01 01 … <aisle> … <xor> BB (type 0x01 deliver) |
| Dispense OK / fault | FF00AAE10101 OK · FF00AAE101xx fault |
flag 0x02 success · 0x03 + board error code |
| Cloud device protocol | /apk/* on api.kiosk-x.ai (getEquipment/getGoods/getStyle/getAd/createOrder…) |
/zhzn/* gateway (register/config/goods/pending/report/heartbeat/upgrade) |
| Device auth | serial-derived equipmentNo + persisted cookie; auto-provision on first contact |
header secret: <fleet secret> + equipmentNo (ZHZN_DEVICE_SECRET) |
| Remote dispense | MQTT push type:"1" / "21" to a topic named after the serial |
leased queue GET /zhzn/pending polled by the agent |
| Real-time presence | MQTT {equipmentNo,state} + Last-Will |
POST /zhzn/heartbeat (and /machine/heartbeat) |
| OTA | /apk/getUpgradeVersion → signed APK on S3, HTTPS + sha256 verified before install |
Android: versionCode + SHA-256 checked APK, same signing key; Linux: signed agent archive |
| Payment (card) | Nayax VPOS Touch over MDB to the VMC | Nayax/MDB via an MDB-RS232 adapter on a dedicated COM |
| Lead time (by fulfillment, not brand) | in-stock ~4 wk when shipped from US warehouse stock | made-to-order ~11 wk for factory builds (both brands' factory runs use MTO timing) |
| Product-fall on install | APK contains the drop logic, but needs serial wiring + provisioning | ZHZN APK contains the CSM driver; requires wiring, enrolment and acceptance |
Sources: reyeah-jd-vending-kiosk/README.md,
reyeah-jd-vending-kiosk/emulator/WIRING_AND_DEPLOY.md;
zhzn-vending-kiosk/docs/t3568-board.md,
zhzn-vending-kiosk/docs/protocol-ylift.md,
zhzn-vending-kiosk/agent/config.py;
kiosk-x/app/routes/device.py, kiosk-x/app/routes/zhzn.py,
kiosk-x/app/mqtt_bridge.py, kiosk-x/app/dfy_manufacturing.py.
2. Where they converge, where they diverge
machines · inventory · pricing · orders · payments · revenue"] end subgraph CLOUD["Kiosk-X cloud"] D1["/apk/* device protocol"] D2["/zhzn/* device gateway"] MQ["MQTT broker mq.kiosk-x.ai"] end subgraph REY["Reyeah machine"] R1["Kiosk X APK (com.ruiye.jd)"] R2["VMC control board
FF0055 over /dev/ttyS0,S1"] end subgraph ZH["ZHZN machine (T3568)"] Z1["ZHZN Android APK (or Linux agent)"] Z2["CSM control / Y-lift board
AA..BB over /dev/ttyS4"] end C1 --> C2 C2 --- D1 C2 --- D2 D1 <--> R1 MQ <--> R1 R1 --> R2 D2 <--> Z1 Z1 --> Z2
- Converge (identical for the operator): account, machine registration/claim,
planogram/aisle model, pricing, inventory, orders, revenue, Nayax settlement,
and the same cloud (
api.kiosk-x.ai). The ZHZN gateway is explicitly built as "the Reyeah template for our own agent" so both dialects resolve to one planogram, one order/ledger model with near-zero per-vendor code (kiosk-x/app/routes/zhzn.py:1-40,:200-247). - Diverge (device layer only): the on-machine software (APK vs agent), the
serial protocol (
FF0055…vsAA…BB), the dispense mechanics (spring vs Y-lift), the remote-dispense transport (MQTT push vs leased pull-queue), and the OTA packaging (APK vs tarball).
3. Differentiators in depth
3.1 Dispensing / product-fall mechanism
Reyeah drives spring/aisle motors through the VMC over UART. The APK builds
a dispense frame FF00554102 <aisle> <qty> <chk> (checksum = sum of every byte
after FF 00, mod 256), e.g. aisle 5 qty 1 → FF0055410205019E. The board
answers FF00AAE10101 (OK) or FF00AAE101xx (fault). Ports:
/dev/ttyS1 main control and /dev/ttyS0 dispensing, both 9600 for
commands (port discovery also probes at 9600; 115200 is used only temporarily
for VMC firmware updates). An optical "drop" sensor frame
(FF0055050104) provides anti-cheat drop detection
(reyeah-jd-vending-kiosk/emulator/WIRING_AND_DEPLOY.md:30-60,
reyeah-jd-vending-kiosk/README.md:22-35).
ZHZN uses a Y-lift (elevator/gantry that travels to a cargo lane, opens
a pickup door, and returns to the port) or a spring board, selected by
ZHZN_MODULE (SH_YAxis | SH). The deliver frame is
AA 14 01 01 <push_delay> 00 <door_hi> <door_lo> <aisle> …<xor> BB
(type 0x01, XOR check over header→last data byte). Success is response flag
0x02; an error is flag 0x03 carrying a board code
(0x07 = "no shipment detected", etc.). A spring board's "correct response
with drop-eye = 0" is a soft success logged as not_detected
(zhzn-vending-kiosk/docs/protocol-ylift.md:44-70,162-186,
zhzn-vending-kiosk/agent/dispenser.py:36-79). Y-lift lanes must be
step-calibrated (0x07 set-steps, saved with the 0xFF lane) before aisles
land correctly (protocol-ylift.md:107-120).
3.2 How the machine talks to the controller
Reyeah: the APK opens raw Linux serial nodes via a JNI libserialport.so, so
the APK itself is the controller driver. ZHZN: the ZHZN APK or Linux agent owns the
serial port (SerialDispenser over pyserial, one lock per write-then-read
transaction) and drives the documented AA…BB protocol directly — no vendor
Android SDK is required for the agent path (zhzn-vending-kiosk/agent/dispenser.py:1-9,131-203).
The Reyeah APR's FF0055… protocol and the ZHZN AA…BB protocol are mutually
incompatible, which is why "the Reyeah app cannot drive ZHZN machines"
(zhzn-vending-kiosk/docs/INTEGRATION.md:10-12).
3.3 MQTT / broker (Reyeah only)
The Reyeah firmware keeps a persistent MQTT session on the Intelliverse broker
mq.kiosk-x.ai (no vendor infra). Device→cloud publishes
{equipmentNo, state} for presence with a Last-Will marking it offline;
cloud→device publishes commands to a topic named after the machine's
equipmentNo: type:"1" remote dispense {orderNumber, aisleNo, buyNum},
type:"21" multi-line cart dispense, type:"2" push APK install, type:"3"
push VMC firmware (the firmware's MqttUtils also handles log-upload, ad-expire,
cache-clear, lock and reboot types)
(kiosk-x/app/mqtt_bridge.py:1-18,125-159;
reyeah-jd-vending-kiosk/decompiled … MqttUtils.java). The stock build points
at the vendor broker; the Kiosk-X build is repointed to tcp://mq.kiosk-x.ai
at repack time (scripts/build_vending_variants.sh). MQTT is best-effort and optional
— vending runs over HTTP /apk/* and never depends on the broker being reachable
(mqtt_bridge.py:14-18).
ZHZN uses no MQTT. Presence is POST /zhzn/heartbeat; remote dispatch is a
leased pull-queue the agent polls (see next).
3.4 Queue mechanics — ZHZN's leased queue vs Reyeah's push
Reyeah remote dispense is a fire-and-push MQTT message (with a
GET /commands/pending HTTP fallback so a drop is never lost if the broker is
down — kiosk-x/app/routes/fulfill.py:74-80,150-156).
ZHZN remote dispense is a leased, exactly-once queue
(kiosk-x/app/routes/zhzn.py:23-39,250-398):
GET /zhzn/pendingleases items forLEASE_SECONDS(60s) rather than popping them — an agent that crashes between poll and vend gets the item re-delivered after the lease expires, so a paid dispense is never silently dropped (zhzn.py:250-265).POST /zhzn/reportis exactly-once: thedispenseIdis reserved under a lock before the forward, concurrent duplicates get a retryable503, upstream5xx/auth failures return502so the device spool retries, and a reservation whose owner died mid-forward is taken over after 60s (zhzn.py:285-398). Queue and dedupe state is fleet-mirrored so a dispense queued via one pod is delivered when the device polls another (zhzn.py:66-98).
The agent additionally supports kiosk-led dispatch (on-device UI POSTs the
paid order to a loopback 127.0.0.1:8770/dispense, Bearer-token protected) and
remote-led (poll the queue), or both (ZHZN_DISPATCH_MODE;
zhzn-vending-kiosk/agent/config.py:105-133,
zhzn-vending-kiosk/docs/INTEGRATION.md:34-49).
3.5 Provisioning / machine identity
Reyeah: on first boot the APK reads its board ID off the VMC
(FF00553101AD → e.g. ASCII VM20240001) and that becomes the equipmentNo.
Kiosk-X auto-provisions an unseen serial into the unclaimed pool (fully
stocked, ten empty aisles) so the kiosk boots to a working grid instead of the
vendor cloud's "Device non-existed"; an operator claims it later via
POST /api/v1/machines/register (kiosk-x/app/routes/device.py:9-17,436-450;
reyeah-jd-vending-kiosk/emulator/WIRING_AND_DEPLOY.md:126-141).
ZHZN: identity is the hardware serial → machine code, resolved by
ZHZN_MACHINE_MAP at the gateway, with three onboarding paths
(zhzn-vending-kiosk/docs/GO-LIVE.md:54-110):
- Path A — kiosk-x gateway (preferred):
ZHZN_CLOUD=kioskx, the agent authenticates with the fleet secret +equipmentNoexactly like Reyeah, self-registers viaPOST /zhzn/register, and kiosk-x auto-provisions unknown serials. No Cognito, no per-machine accounts (zhzn.py:181-191;config.py:24-34). - Path B — direct zero-touch:
ZHZN_CLOUD=direct+ZHZN_ENROLLMENT_KEY; the agent callsPOST /machine/bootstrapand the user service mints the machine +kiosk_useraccount (config.py:56-66). - Path C — direct manual: pre-create the machine +
kiosk_user(fromDevice:"machine",macAddress:<machine code>) and put the credentials on the device (GO-LIVE.md:98-110,INTEGRATION.md:127-148).
ZHZN catalog seam (read this or the machine will not sell). kiosk-x registration gives the device identity + hardware config, but the product catalog, slots, and dispense reports all key on the Intelliverse machine record, which kiosk-x does not create. Map the hardware serial to the Intelliverse machine code in
KIOSKX_ZHZN_MACHINE_MAP, then assign products to(row,column)slots — otherwisekiosk-products/by-machinereturns nothing (GO-LIVE.md:77-89).
3.6 Planogram / aisle model (shared)
Both resolve to one planogram. Reyeah "goods" are 1:1 with aisles; a
purchase of aisle N decrements aisle N (device.py:133-195). The ZHZN gateway
serves the same store in the device's row/column dialect, deriving the aisle
with aisle = (row-1)·columnsPerRow + column (columnsPerRow default 10;
zhzn.py:200-247, config.py:155-181, INTEGRATION.md:88-98). So a Reyeah APK
(/apk/getGoods) and a ZHZN device (/zhzn/goods) pointed at the same machine
read an identical planogram. (Honesty note: the shipped ZHZN agent currently
fetches its catalog from the Intelliverse Products API
(kiosk-products/by-machine) rather than /zhzn/goods — the gateway surface
exists so both dialects can resolve to the one store;
zhzn-vending-kiosk/agent/backend.py, zhzn.py:229-247.)
3.7 Firmware / OTA
Reyeah OTA is an APK rollout: /apk/getUpgradeVersion returns a
vendor-shaped descriptor with an HTTPS url (S3) and a sha256/checksum;
the firmware verifies the artifact before install, then reports the new build
via /apk/apkUp, which flips the machine to "upgraded" and auto-completes the
rollout (device.py:1330-1372,1969-1982). Client-side integrity was hardened in
the vending repo (release 1.2.1 / versionCode 121): a self-contained
com.kioskx.OtaIntegrity class rejects non-HTTPS offers and streams the
download through SHA-256, deleting the file and aborting on mismatch
before install — offers with no digest (legacy backend) keep working with a
logged warning so a backend rollback can't brick fleet OTA
(reyeah-jd-vending-kiosk commit 8d5d0f3,
scripts/build_vending_variants.sh:112-138, scripts/patch_smali.py).
ZHZN Android OTA is a signed APK offered through /zhzn/upgrade; it requires
a higher versionCode, matching SHA-256 and the existing Android signing key.
The following archive description applies only to the Linux Python agent: /zhzn/upgrade (or ZHZN_OTA_MANIFEST_URL) returns
{version, url, sha256}; the agent verifies the sha256 over TLS, stages the new
tree, swaps the install root (previous stays at <root>.old for rollback), and
restarts under systemd (zhzn.py:409-418, config.py:71-78,
GO-LIVE.md:191-219). Known gaps: manifests are sha256-verified but not
signed, and the install-root swap is two renames (tiny non-atomic window) —
both flagged for hardening before large fleets (GO-LIVE.md:210-219).
3.8 Lead times (provisioning economics)
The DFY manufacturing lead-time model keys on fulfillment mode, not brand:
in-stock (US warehouse pick/config + LTL freight, ~27 days ≈ 4 weeks end to
end) vs made-to-order (factory build + ocean freight from Shenzhen/Ningbo,
~80 days ≈ 11 weeks: qa 25d and delivered 35d dominate). The manufacturers
enum is hotbox | reyeah | zhzn, and the code's comment attributes MTO timing
to "Reyeah/ZHZN factory builds" — i.e. either brand's factory run takes the
~11-week path, and either can be in-stock if warehoused
(kiosk-x/app/dfy_manufacturing.py:31-82, docs/autopilot-investments.md,
docs/superadmin-money-ops-manual.md).
4. The universal "Kiosk X" APK
4.1 What makes it universal
The universal build is the rebuilt Reyeah APK (com.ruiye.jd), pointed at
the Kiosk-X cloud. Its universality is cloud-driven, not hardcoded:
getStyleserves the whole skin (grid chrome, pay-sheet chrome, colors) as URLs the firmware Glide-caches, so a redesign ships from the cloud without a reinstall (device.py:567-578; skin assets baked byreyeah-jd-vending-kiosk/scripts/skin_shop_grid.py).getEquipmentserves config/theming and pins the real payment surface in code —isPaypal:1(Scan & Pay QR),isBill:0/isCoin:0(this hardware has no bill/coin mech) — plus currency$, tax line, and the age gate, independent of any APK build (device.py:318-420).isCard(Nayax) is the one pay flag that is per cabinet rather than fleet-wide: it is withdrawn when the cabinet's own MDB sweep reports no card reader (or one present and disabled), and when no Nayax terminal is bound to the machine at all, because a tile over a reader that is not there is a tap that dies of a timeout. It stays on for a cabinet that has never reported a sweep and for a still-provisionalbinding — seecard_tile_visibilityfor why each verdict rests on the evidence it does.getGoodsbuilds the grid from the machine's own aisles;getAdassembles a never-empty attract loop (direct-sold → operator ads → programmatic fill → house/attract/showcase posters), all safety-gated so an image never lands in the video player (device.py:436-504,1130-1327).- The two cloud variants differ by exactly one value —
UrlConfigString.baseUrl:reyeah-vending-oldcloud.apk→sapi.vapevendingsoftware.com(dead-ends at "Device non-existed" for an unprovisioned board) vsreyeah-vending-kioskx.apk→kiosk-x.intelli-verse-x.ai(auto-provisions → boots to a live grid) (reyeah-jd-vending-kiosk/README.md:54-84).
4.2 What the APK does on first boot (verified in the decompiled source)
SplashActivity→ permissions + cached identity restore; with no cachedequipmentNoit scans serial ports (SerialPortFinder, probing at 9600 with the clear frame and acceptingFF00AAA201014Eas "this is a VMC").- Reads the board ID (
FF00553101AD…→ ASCII, e.g.VM20240001) — that becomes theequipmentNosent on every cloud call. - Bootstraps over HTTP in order:
getUpgradeVersion→getAd→getStyle→getEquipment→apkUp, then connects MQTT and queries VMC firmware. - After
getEquipmentit configures the optical drop-eye (config.info.isEye) and enters the product grid, which loads/apk/getGoods.
(decompiled … SplashFragment.java, MyIntentService.java;
emulator/WIRING_AND_DEPLOY.md:119-141.)
Device-auth honesty note: the APK sends a baked-in secret header
(UrlConfigString.secret) plus apkVersion/equipmentNo on every call. The
vendor cloud requires that secret; the Kiosk-X /apk/* surface identifies the
device by equipmentNo/cookie and does not validate the Reyeah secret
header (kiosk-x/app/routes/device.py:58-96) — in contrast to the ZHZN gateway,
which enforces its fleet secret (zhzn.py:135-141).
4.3 Product-fall verdict: the honest answer
"Does product fall work just by installing the APK?" — No, on neither manufacturer is it pure-install. Be precise about why:
| Reyeah | ZHZN | |
|---|---|---|
| Does the universal APK contain the drop logic? | Yes — the FF0055… dispense frames ship in the APK |
The Reyeah APK does not; the separate ZHZN APK implements AA…BB |
| What actually performs the fall | the APK over /dev/ttyS0/S1 |
the ZHZN APK over its configured UART/USB port, or the Linux agent |
| Extra manual steps required | serial wiring; raw-serial access (platform-sign or chmod); uninstall factory app first (different signing cert); armeabi-v7a build; a provisioned/claimed serial |
install ZHZN APK; device owner + HOME; wire/calibrate; enrol; assign slots; complete factory acceptance |
| Verdict | install + serial wiring + provisioning | ZHZN APK + wiring + provisioning + acceptance |
Reyeah specifics: the emulator proved the full chain
app → serial handshake → device id → cloud config → grid → purchase → dispense
except the vendor-side device record; a raw /dev/ttySx needs root/platform
signing, and Android refuses to update the factory-signed package with our
release cert, so you uninstall the factory app first
(reyeah-jd-vending-kiosk/README.md:86-102,
emulator/WIRING_AND_DEPLOY.md:64-104).
ZHZN Android specifics: install the dedicated ZHZN APK with its embedded
WebView storefront. Linux hosts instead run the headless Python agent. Follow
Android install and handover for Android cabinets. Product fall is the agent driving AA…BB; "runs today, proven by
simulation" (zhzn-vending-kiosk/docs/GO-LIVE.md:1-14,
docs/INTEGRATION.md:100-111).
confirm with hardware: the RK3288/RK3568/dev/ttySN↔ physical COM mapping and the exactchmod/SELinux/platform-signing needed for raw serial access are board-specific and cannot be proven from this code (zhzn-vending-kiosk/docs/t3568-board.md:26-41,reyeah-jd-vending-kiosk/emulator/WIRING_AND_DEPLOY.md:64-77).
5. Cloud API surface for the kiosk
All device traffic is the envelope {code, data, msg} with code == 0 = success
(distinct from the Partner API's 200). Base host api.kiosk-x.ai
(device builds use kiosk-x.intelli-verse-x.ai).
5.1 Reyeah /apk/* (implemented in kiosk-x/app/routes/device.py)
| Endpoint | Purpose |
|---|---|
/apk/getEquipment |
Config/theming; auto-provisions an unseen serial; pins currency/pay tiles/age gate (:436-450) |
/apk/getGoods |
Planogram → grid (aisle 1:1 good), cutout product art (:488-504) |
/apk/getStyle |
Full skin (grid + pay-sheet chrome URLs) (:567-578) |
/apk/getAd |
Never-empty attract loop, player-safety gated (:1130-1327) |
/apk/createOrder · /apk/createOrderCart (+ offline variants) |
Book a sale; payType 3=card(Nayax), 4=Scan&Pay QR; $0 promo path (:1947-1964) |
/apk/orderStatus · /apk/payQr |
Scan & Pay poll + QR (Stripe webhook settles, then MQTT dispense) (:2028-2086) |
/apk/ordersUpdate |
Post-payment dispense outcome; a failed vend marks needsRefund (:1999-2015) |
/apk/getUpgradeVersion · /apk/apkUp |
OTA offer (HTTPS + sha256) + install ack (:1330-1372,1969-1982) |
/apk/saveAisleGoods |
On-machine manager-menu planogram edit persists (:2121-2155) |
/apk/submitFault · /apk/uploadLog · /apk/vmcUp |
Fault/telemetry (fault blocks the aisle) (:2164-2201) |
/apk/saveAdRecord |
Ad proof-of-play → sponsor/SSP reporting (:2218-2254) |
/apk/authPassword · /apk/validDiscountCode · /apk/ageVerify |
Manager gate · free-product/QuestX codes · age gate (:1382-1597) |
/device/qr/machine/{no}.png |
Machine-number registration QR → operator-console deep link (:717-731) |
5.2 ZHZN /zhzn/* (implemented in kiosk-x/app/routes/zhzn.py)
| Endpoint | Purpose |
|---|---|
POST /zhzn/register |
First contact: serial → machine code + config; auto-provision (:181-191) |
GET /zhzn/config |
getStyle-equivalent (module/grid/intervals) (:194-197) |
GET /zhzn/goods |
Planogram as row/col slots — same store as /apk/getGoods (:229-247) |
GET /zhzn/pending |
Leased remote-dispense queue (:250-265) |
POST /zhzn/report |
Dispense outcome; exactly-once forward to trigger-fall / order-complete (:285-398) |
POST /zhzn/heartbeat |
Presence (:401-406) |
GET /zhzn/upgrade |
OTA manifest {version,url,sha256} (:409-418) |
POST /zhzn/dispense |
Operator-facing: enqueue a remote-led dispense (Partner-API auth, ownership enforced) (:423-450) |
5.3 Shared vs manufacturer-specific
- Shared (operator/Partner API), manufacturer-agnostic:
POST /api/v1/machines/register(claim any serial —machines.py:181-219),.../{machineNo}/nayax(bind reader —machines.py:235-275),GET/PUT /api/v1/inventory/…,GET /api/v1/orders,GET /api/v1/revenue/summary,GET /api/v1/payments*, andPOST /api/v1/fulfill(walkup|api_drop). - Reyeah-specific: the
/apk/*surface + the MQTT push channel. - ZHZN-specific: the
/zhzn/*gateway + the Intelliverse product/order endpoints it forwards to (kiosk-products/by-machine,orders/kiosk,kiosk-products/trigger-fall,orders/kiosk/dispense/complete—INTEGRATION.md:51-77).
OTA hardening (both): offers are HTTPS-only and carry a sha256 the client
verifies before installing (Reyeah com.kioskx.OtaIntegrity; ZHZN agent
tarball verify). Machine-number QR registration lets an operator scan a
serial at the machine instead of typing it (device.py:703-731).
6. End-to-end vending for one operator
The same operator runs both kinds from one Operator X app / web console.
Operator setup (identical for both):
1. Register/claim — POST /api/v1/machines/register with the serial
(or scan the machine-number QR). If the board already auto-provisioned, this
claims it (and any vends it already took) into your fleet
(machines.py:181-219).
2. Planogram + pricing — assign products to aisles/slots with stock and
price via PUT /api/v1/inventory/… (Reyeah) or slot assignment (ZHZN); both
land in the one shared store.
3. Bind Nayax — POST /api/v1/machines/{no}/nayax with the reader's Device
Number (see §7).
Customer purchase (converges in the cloud): a walk-up card sale is
machine-led (the device commands its own motors and the cloud records the sale);
a remote sale is api_drop (Reyeah MQTT push) or a queued /zhzn/dispense
(ZHZN pull). Either way the order, dispense outcome, Nayax settlement, and the
revenue-ledger entry flow through the same operator endpoints
(fulfill.py:83-123, device.py:1999-2015, zhzn.py:285-398,
docs/hybrid-fulfillment.md).
7. Nayax per manufacturer
Nayax acquiring is independent of the vending cloud and settles to the
operator's Nayax merchant account, decided by the terminal→account binding
inside Nayax Core — not by anything the kiosk sends
(docs/payments-nayax.md:24-52,189-201).
Binding (identical for both): POST /api/v1/machines/{no}/nayax with the
reader's Device Number (terminalId), mirrored on the machine record; the
webhook resolves the machine by exact terminal match and force-routes
settlement from that binding (payments-nayax.md:54-85,
kiosk-x/app/routes/machines.py:235-275). The webhook
POST /api/v1/payments/nayax/webhook fails closed: it requires
X-Nayax-Signature (hex HMAC-SHA256 of the raw body with
KIOSKX_NAYAX_WEBHOOK_SECRET); unsigned/mis-signed → 401, unknown terminal →
404 (payments-nayax.md:109-149).
| Reyeah | ZHZN | |
|---|---|---|
| Reader | Nayax VPOS Touch | Nayax reader via MDB-RS232 adapter |
| Wiring to controller | reader → MDB → VMC | MDB peripheral → MDB-RS232 adapter → a dedicated COM on the T3568 (separate from the dispenser COM) |
| Card tile flow | createOrder payType=3 → app arms reader over serial → VMC HavePaid → app drives motor → ordersUpdate (payments-nayax.md:87-107) |
adapter is the MDB master, pushes accept events (30…/08…); app authorizes then calls the dispenser; payment path is independent of the CSM SDK (docs/mdb-rs232.md:15-71) |
| Settlement record | webhook → orders + payments APIs | same webhook → same APIs (manufacturer-agnostic) |
Adapter serial config: 9600 8N1, HEX; events are prefixed with the MDB
device ID and have no checksum; resend a host command if no reply after 2s
(mdb-rs232.md:8-28). On vend failure, refund via escrow-return (35 00) or
coin payout (mdb-rs232.md:63-71).
confirm with hardware: the exact MDB-RS232 ↔ Nayax reader model compatibility and which T3568 COM carries it are per-build wiring choices, not determinable from code (mdb-rs232.md:63-66,t3568-board.md:26-32).
8. Manual setup procedures
Steps marked [same] are identical across both; [Reyeah] / [ZHZN] are manufacturer-specific.
8.1 Reyeah — boxed machine to selling
- [same] Power + network — 12V/mains per the machine; Ethernet or WiFi with
outbound HTTPS to
kiosk-x.intelli-verse-x.ai(and MQTT tomq.kiosk-x.ai, optional). - [Reyeah] Wire the controller — UART TX/RX/GND from the Android board to
the VMC; match voltage (TTL 3.3V or RS232 via level shifter);
/dev/ttyS1main (115200/9600),/dev/ttyS0dispense (9600) (emulator/WIRING_AND_DEPLOY.md:64-77). - [Reyeah] Install the APK — build the
armeabi-v7aKiosk-X variant, then on a board still running the factory app:adb uninstall com.ruiye.jd→adb install -r dist/reyeah-vending-kioskx.apk→ launch. Grant raw serial access (platform-sign orchmod 666 /dev/ttyS*); set it as the boot launcher (BOOT_COMPLETEDautostart) (README.md:86-102,emulator/WIRING_AND_DEPLOY.md:87-104). - [same] Register/claim —
POST /api/v1/machines/registerwith the serial, or tap the on-screen machine number → scan the QR (device.py:717-731,machines.py:181-219). - [same] Planogram + pricing — set aisle products/stock/price
(
PUT /api/v1/inventory/…). - [same] Bind Nayax —
POST /api/v1/machines/{no}/nayaxwith the Device Number. - [Reyeah] Test vend — use the manager menu (long-press the invisible
top-right hotspot → cloud password →
ManagementActivity) test-dispense, oram start -n com.ruiye.jd/.ui.activity.TestActivity; confirmFF00AAE10101(OK) (emulator/WIRING_AND_DEPLOY.md:87-104,reyeah-jd-vending-kiosk/docs/operator-view-feasibility.md:13-27). - [same] OTA thereafter — create a rollout; devices pull the signed APK
from
/apk/getUpgradeVersion(HTTPS + sha256) and ack via/apk/apkUp.
8.2 ZHZN — boxed machine to selling
- [same] Power + network — 12V input; set the VDC-SEL jumper for
auto-power-on after outages; outbound HTTPS (
t3568-board.md:18-41). - [ZHZN] Wire the board — RS232
GND/TXD/RXD, 9600 8N1, to the CSM dispenser COM (vendor demo uses/dev/ttyS4); reserve a second COM for the MDB-RS232 payment adapter (t3568-board.md:26-32,INTEGRATION.md:150-154). - [ZHZN] Calibrate (Y-lift) — run step calibration (
0x03/0x07, save with the0xFFlane) so aisles land; setZHZN_COLUMNS_PER_ROWto match the grid (protocol-ylift.md:107-120,INTEGRATION.md:88-98). - [ZHZN] Install the agent —
cp -r zhzn-vending-kiosk /opt/…,pip install -r agent/requirements.txt, fill/etc/zhzn-agent.env(ZHZN_CLOUD=kioskx,ZHZN_DEVICE_SECRET=<fleet secret>,ZHZN_SERIAL_PORT, and for kiosk-led aZHZN_LOCAL_TOKEN), enable the systemd unit (Restart=always) (GO-LIVE.md:112-153). - [same] Register/claim + catalog seam — Path A auto-provisions the serial,
then create/claim the Intelliverse machine record, map serial→machine
code in
KIOSKX_ZHZN_MACHINE_MAP, and assign products to(row,col)slots (GO-LIVE.md:54-110). - [same] Bind Nayax —
POST /api/v1/machines/{no}/nayax. - [ZHZN] Smoke-test one aisle —
python3 agent.py --once, orSerialDispenser("/dev/ttyS4",9600).dispense(aisle=1)→ expectstate="success";no_ack⇒ check baud/TX-RX/board type (INTEGRATION.md:156-169). - [same] Full transaction test — buy the cheapest item; confirm the
physical drop and a
fall_logsrow withmetadata.success=true(GO-LIVE.md:154-179). - [same] OTA thereafter — publish
{version,url,sha256}; the agent verifies and swaps the install root, keeping<root>.oldfor rollback.
8.3 Factory line (China) — the acceptance gate before a unit ships
Run the APK's factory test with the cabinet open, on the line, before the unit is crated. The signature at the end is not a formality: it is the last moment anyone has physical access, and two of the facts it captures are impossible to recover remotely.
The gate refuses a signature (POST /zhzn/factory-sign → 409, and the kiosk
greys its own button) until all of the following hold:
| Requirement | Why it blocks the ship |
|---|---|
camera, speaker, mic, touch, screen, board all pass |
A skip is an unverified subsystem. "Nothing failed" is not the bar — a report with five skips and one pass used to read as a perfect machine. |
Vend-board port identified — some /dev/tty* actually ACKed |
The SBC's physical COM headers map to Linux device nodes per unit. If the line does not record which node answered, the field agent guesses. |
Tray mechanism identified — SH (spring/spiral) or SH_YAxis (Y-lift) |
The two send incompatible init frames. Configure the wrong one and a healthy board answers nothing on every port — indistinguishable from a severed cable. This cost days on a live cabinet. |
| Camera device identified | The photobooth cannot be enabled for a machine whose camera was never proven. |
keypadLock is the one deliberate exception: it is fitted after the line runs,
is recorded as pending, appears on the certificate as an open item, and never
blocks the ship.
Once green, the discovered port and tray variant are pinned as that machine's agent config, so it boots on proven wiring on its first power-on in the field rather than on a default that may be wrong. An operator who later corrects the wiring by hand outranks the factory record and is never overwritten.
Operator X shows the certificate (technician, employee ID, lot, date), the recorded port map, and flags any cabinet that shipped unsigned.
9. Troubleshooting (common failures)
| Symptom | Reyeah | ZHZN |
|---|---|---|
| Device not registered | Old cloud shows "Device non-existed" — point the build at Kiosk-X (auto-provision) and claim via /api/v1/machines/register (README.md:54-62, device.py:440-450) |
Serial not in KIOSKX_ZHZN_MACHINE_MAP, or no Intelliverse machine record / slots — fix the catalog seam (GO-LIVE.md:77-89) |
| Black screen / ad loop crash | An image served as type:1 crashes the video player; getAd player-safety gating prevents it and guarantees a never-empty loop (device.py:331-399,1290-1327) |
N/A (web storefront); check the browser kiosk app |
| Dispense failure | Watch faults FF00AAE101xx; a failed vend ordersUpdate marks needsRefund for reconciliation (WIRING_AND_DEPLOY.md:59-60, device.py:1999-2015) |
Board error flag 0x03 + code (e.g. 0x07 no shipment); spring not_detected soft-success; check calibration/wiring (protocol-ylift.md:162-186, dispenser.py:36-60) |
no_ack from board |
serial not opened / wrong port / no raw access | wrong baud (must be 9600), TX/RX swap, wrong ZHZN_MODULE (INTEGRATION.md:166-169) |
| MQTT not connecting | Best-effort only — vending still runs over /apk/*; the api_drop HTTP fallback delivers commands (mqtt_bridge.py:14-18, fulfill.py:74-80) |
ZHZN uses no MQTT — check heartbeat + /zhzn/pending polling instead |
| Nayax webhook rejected | 401 = missing/wrong signature (set the real KIOSKX_NAYAX_WEBHOOK_SECRET); 404 = terminal not bound (payments-nayax.md:109-149) |
same webhook, same fixes |
| OTA won't install | Non-HTTPS offer or sha256 mismatch is refused by com.kioskx.OtaIntegrity (by design) — ensure the rollout URL is HTTPS and the digest matches (commit 8d5d0f3) |
sha256 mismatch aborts the swap; a requirements.txt change needs pip install -r on the box (GO-LIVE.md:215-219) |
10. Claims to confirm with hardware
These are not determinable from code and are marked for hardware verification:
- The RK3288/RK3568
/dev/ttySN↔ physical COM mapping, and the exactchmod/SELinux/platform-signing needed for raw serial access (t3568-board.md:26-41,WIRING_AND_DEPLOY.md:64-77). - Per-machine Y-lift step calibration values and
columnsPerRowvs the real grid (protocol-ylift.md:107-120,INTEGRATION.md:96-98). - The MDB-RS232 ↔ Nayax reader model compatibility and which T3568 COM
carries payment (
mdb-rs232.md:63-66). - Reyeah drop-sensor / anti-cheat behavior on a live board beyond the emulator
round-trip (
WIRING_AND_DEPLOY.md:143-148). - ZHZN device secret: the gateway fails closed — it is disabled until
KIOSKX_ZHZN_DEVICE_SECRETis set (zhzn.py:135-138). On the live deployment the secret is CI-bootstrapped and wired since 2026-08-12 (kube-infrab534dd65; live existing-zhzn lifecycle PASS, run 8657773248 — seedocs/lifecycle-matrix.md).KIOSKX_ZHZN_MACHINE_MAP/KIOSKX_ZHZN_FORWARD_AUTHremain per-fleet ops config (GO-LIVE.md:56-76). - ZHZN walk-up ledger parity: a ZHZN walk-up vend is reported to the
Intelliverse trigger-fall / order-complete endpoints; whether it always books
a Kiosk-X
ordersrow with the same fidelity as/apk/createOrderis not fully spelled out in code — verify on a live unit (zhzn.py:327-357,docs/hybrid-fulfillment.md). - A physical Nayax tap end-to-end on either manufacturer is still marked
BLOCKED-HW/BLOCKED-BIZ in the live lifecycle matrix
(
docs/lifecycle-matrix.md).
11. Source index
| Area | Files |
|---|---|
| Reyeah device protocol (cloud) | kiosk-x/app/routes/device.py |
| Animated product art / motion on screen (which surfaces can, and what each costs) | kiosk-x/docs/animated-product-art.md |
| Reyeah APK + serial + build | reyeah-jd-vending-kiosk/README.md, emulator/WIRING_AND_DEPLOY.md, scripts/build_vending_variants.sh, scripts/patch_smali.py, scripts/skin_shop_grid.py, docs/operator-view-feasibility.md |
| MQTT push channel | kiosk-x/app/mqtt_bridge.py |
| ZHZN gateway (cloud) | kiosk-x/app/routes/zhzn.py, kiosk-x/app/config.py (ZHZN_* keys) |
| ZHZN agent + protocol | zhzn-vending-kiosk/agent/{config,dispenser}.py, docs/{INTEGRATION,GO-LIVE,protocol-ylift,mdb-rs232,t3568-board}.md |
| Operator API | kiosk-x/app/routes/{machines,inventory,orders,fulfill}.py |
| Nayax | kiosk-x/docs/payments-nayax.md, kiosk-x/app/routes/machines.py |
| Lifecycle / lead times | kiosk-x/app/{lifecycle,dfy_manufacturing}.py, docs/{machine-lifecycle,lifecycle-matrix,autopilot-investments}.md |
Cross-references: Machine client · APK rollouts · Payments & Nayax · Hybrid fulfillment · Lifecycle feature matrix.