Appearance
SUNMI device provisioning
Need to do something in the console? Start with the task-oriented SUNMI DMP runbook — console traps, group/profile/device recipes, licensing, and current fleet state. This document is the narrative of what we learned and why; the runbook is the how.
Overview
SUNMI hardware (D3 PRO countertop tills, FLEX 3 kiosk screens) is managed through SUNMI DMP on the Partner Platform, not through a third-party EMM and not through Google zero-touch. This document records how that actually behaves, measured on real hardware — including the parts that do not work, because several of them look like they do.
Everything below was verified on a D3 PRO (Android 13, SN DP02261212845) in September 2026 unless explicitly marked OPEN.
Entities: devices belong to a tenant, and only that tenant can see them
Upvendo has two SUNMI enterprise entities. The hardware lives in Upvendo Benelux BV; Upvendo, Inc. is a separate, empty entity.
This matters more than it sounds. The device-binding check (POST public/v1/devicemanage/checkMsnList, behind Device Ownership → Device Bound → Batch binding) returns four buckets — noExistCount / noBindCount / bindCount / frozenNum — and a serial belonging to another tenant comes back in noExistCount with an empty machineModel, indistinguishable from a serial that does not exist at all. That is ordinary anti-enumeration behaviour, not a fault, but it means:
"Device not found" in the SUNMI console does not mean the device is unknown to SUNMI. It means it is not visible to the account you are logged into.
Before concluding a device is missing, check whether a colleague registered it under a different entity.
A SUNMI user account belongs to exactly one entity. Inviting an address that already belongs elsewhere fails with "The user has already joined another partner". A second entity therefore needs a second email address, or the first must be released via Transfer Ownership.
How devices arrive
There is no manual "add device by serial". The dashboard's Add Device button opens documentation, not a form. Devices reach an entity in exactly two ways:
- Purchased through official SUNMI channels — the SN auto-binds at purchase
- A supplier/distributor initiates a device transfer on their backend
For a transfer, the supplier needs the receiving Entity (Account) ID (Account Center → Partner → Copy ID). Inbound transfers land in Device Ownership → Transfer Record → Ongoing and require manual acceptance unless "Automatically receive transfers from partners" is enabled — it defaults off, so a transfer can sit unnoticed.
Groups
Two parallel trees, and they are not the same thing:
- Organisational groups — System Management → User&Org. Creating one with "Create a device group with the same name and associate it" also creates the device group.
- Device groups — Device → Device Group. What profiles and app policy bind to.
The device-binding picker lists only child groups; the root enterprise group is excluded (you cannot bind devices to the entity itself). With only a root group present the picker is empty and the API returns {"code":1,"data":{},"msg":"success"} — HTTP 200, msg: "success", empty payload. Nothing in the UI indicates why.
⚠ Group names are globally unique across SUNMI, not per-entity. A collision fails with {"code":13010129,"msg":"channel name repeat"} — visible only in the network response; the dialog just stays open with no error. Prefix group names distinctively.
Profiles
Device → System Customization → Profile. A profile is a config bundle: Create → Publish → bind to a device or device group. Configuration items include Wallpaper, Boot Animation, Launcher, Language, WLAN/SSID, Time Zone, Kiosk Mode, App Lock, Default Startup, Printer, Subscreen, Display, Sound, Security.
Three things that cost real time:
1. Every item defaults to the weak control mode. Each config item offers Recommended Settings ("applied before terminal changes or after factory reset") or Mandatory Control ("always enforced by the system"). Recommended is the default and will not touch a device already in service. Use Mandatory when retrofitting a live fleet.
⚠ Superseded 2026-09-22: group binding (group → Profile → Bind Profile → Save → Confirm Binding) now verifiably works and is the preferred path — see the runbook. Kept for history.
2. Bind from the profile, not from the group. The device group's Profile → Bind Profile wizard walks through select → Next → Priority Sort → Save and then silently does nothing — no API call, no error, and the count stays at 0. The working path is the profile's own detail page → Bound tab → Add Device / Add Group → Confirm Binding.
3. The console's own binding state is unreliable. The profile list column, the Bound tab and the publish confirmation dialog have been observed disagreeing about how many devices a profile is bound to. The device is the only trustworthy oracle.
Precedence, per SUNMI: Device > Group > Smart Group, with non-conflicting settings from multiple profiles merged — so a group baseline plus per-device overrides is the intended pattern.
Required DMP settings for every kiosk (2026-09-29)
These are required, not optional. The kiosk app (upvendo-kiosk #720) only recovers by itself when the profile gives it these. Each one is backed by a measurement on the FLEX 3 (F30625AVJ1031) in the 2026-09-28/29 kiosk audit device rounds (docs/superpowers/2026-09-28-kiosk-audit/device-round{1,2,3}.md in the monorepo).
| # | Profile item | Setting | Status | Why (measured) |
|---|---|---|---|---|
| 1 | Default Startup | the kiosk app, Auto Recovery ON | REQUIRED | The only protection against a force-stop of the kiosk package itself (Settings → Force stop, a crash loop, an operator). A force-stop cancels the app's own alarms by Android design, so the in-app watchdog cannot bring it back. Round 2 and round 3 test T6: force-stop left the device on Settings; sunmi_watchdog_package was empty (Auto Recovery OFF). |
| 2 | App Control → Super Permissions | add the kiosk app, grant Alert Window | REQUIRED | Android 13 blocks an app from starting an activity from the background unless it holds the overlay permission (SYSTEM_ALERT_WINDOW) or is the default HOME app (AOSP 13 ActivityStarter.shouldAbortBackgroundActivityStart). The kiosk's watchdog, boot and package-replaced relaunches all start from the background. Rounds 2 and 3 recovered from a WebView update in 17–19.5 s because the overlay permission was allowed on that unit ("BAL allowed via SYSTEM_ALERT_WINDOW" in logcat). The value name DMP writes for Alert Window was not recorded; verify with adb shell appops get <pkg> SYSTEM_ALERT_WINDOW → allow. The app's own Slack fix text (#720 ProvisioningCheck) words this as "App permission strategy: grant 'Display over other apps'". That is the same permission under Android's name; in DMP it is Super Permissions → Alert Window. |
| 3 | Kiosk Mode | the kiosk app in the lock table | REQUIRED (already part of the kiosk profile) | With lock task active the kiosk's watchdog brings the kiosk back after 3 min behind another app (round 3 T5: re-fronted at 180–181 s). Without it the watchdog deliberately does not re-front (it would fight staff), so a guest who reaches Settings stays there. |
| 4 | Launcher = the kiosk app, or disable Launcher3 | Recommended | After a crash with nothing else in the task stack the system shows HOME. With stock Launcher3 as HOME the guest sees, and can tap into, Launcher3 for ~18 s until the watchdog relaunches the kiosk (round 3, N4). With Launcher3 disabled the kiosk claims HOME itself and is back in ~1 s (round 1, phase B2). Not verified: whether the DMP Launcher picker lists the kiosk. With Launcher3 enabled the app offers and then withdraws its HOME entry, because HOME becomes ambiguous (round 3 logcat: HOME alias enabled → state=AMBIGUOUS → disabled), so the kiosk may not appear in the picker. If it does not, disable com.android.launcher3 (Component Control; also not yet verified). |
The kiosk reports its own gaps — once upvendo-kiosk #720 ships (not in current builds; #720 is on hold). With #720, the app checks items 1–4 on boot and on every resume. It logs one Provisioning: provisioning: … line to logcat each time, and posts at most one Slack message per device per day, naming the device and the exact fix for each gap. It only reports a gap it sees twice, at least 2 minutes apart, because Kiosk Mode and the HOME default settle asynchronously at boot. A reading without Kiosk Mode is ignored for the first 3 minutes after launch (LOCK_TASK_SETTLE_MS). Item 1 is read from Settings.Global sunmi_watchdog_package, the only trace of Auto Recovery we have observed: {"package":"<pkg>","times":N} with it ON, empty with it OFF. That is not a documented SUNMI API. If the app cannot read it, the state is logged as UNKNOWN and never reported as a gap. Whether an app can read it under targetSdk 35 is still to be confirmed on a device (the logcat line shows the raw value). Open question: times may count recoveries that have actually happened. If so, a device with Auto Recovery ON that has never needed it might also read empty and be reported as OFF. Only a device with Auto Recovery just switched ON (before any recovery) can settle this.
Check a provisioned kiosk over adb:
adb shell settings get global sunmi_watchdog_package # item 1: {"package":"<kiosk pkg>",...}
adb shell appops get <kiosk pkg> SYSTEM_ALERT_WINDOW # item 2: allow
adb shell dumpsys activity activities | grep mLockTaskModeState # item 3: not NONE (LOCKED_SUNMI seen)
adb shell cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.HOME | tail -1 # item 4
adb logcat -d -s Provisioning:I | tail -1 # the app's own verdictApp distribution
An app must be published in the SUNMI App Store before a profile can reference it. Until then it does not appear in the Default Startup or Kiosk Mode app picker at all.
- Private App Store — visible only within your channel; automatic review only (no manual review), so publishing is fast. A private app cannot reach a second entity's devices.
- Public App Store — reachable by other partners' customers; requires manual review.
Listing gotchas: Notes is capped at 200 characters; screenshots are required per orientation (3–5 each, portrait 480×854 / landscape 854×480, each under 100KB); a device ticked in the Portrait list forces portrait screenshots, and FLEX3 appears in both lists but is one device — unticking it in Portrait removes it from the listing entirely.
Publishing to the private pool is a trap. The Upload APK button uploads to whichever pool tab is active, and the page opens on Public. Clicking the Private tab and immediately clicking Upload still produced pond_type=0 once — the tab must actually be active first (verify: the listing shows your existing private apps). Check the form you land on:
Public pool (pond_type=0) | Private pool (pond_type=1) | |
|---|---|---|
| Visible To | "All Channels" / "Designated Channels Only" | no options |
| note shown | "Sunmi Public App Store provides applications used by all Sunmi partners" | "will not be visible to other partners" |
SUNMI's manual is the authority here: "Private pool: this pool is visible to the original channel by default. No other available options." (Public and Private Pools Manual). If the form offers you "All Channels", you are about to publish to the public store — back out.
Two more form defaults that block submission or mislead:
- Country/Region defaults to "Chinese Mainland", which makes an ICP number mandatory ("京ICP备..."). Untick it and tick Other (overseas) — the ICP field then disappears.
- A Privacy Policy URL is required. Ours is
https://upvendo.com/legal/privacy. - The submit dialog says "Review typically takes one business day", but a private-pool app was Published immediately in practice (2026-09-05).
An app is only selectable in a profile once it is Published, and the picker searches by app NAME, not package name. Measured: with com.upvendo.pos.testing uploaded but unpublished, searching "POS TEST" returned No Data; searching com.upvendo.pos also returns No Data (package names are not matched) while "Upvendo" returns the app. After publishing, "Upvendo POS TEST" found it.
⚠ Adding an app to Default Startup silently switches Disable Uninstallation back ON. It is on by default for a newly added app even if the profile you are editing had it off. Given the block is awkward to clear (see below), check that switch every time you change the app.
Default Startup also installs: "If the app is not present on the device, it will be installed automatically", with optional Disable Uninstallation and Auto Recovery (restarts the app, then reboots the device if it keeps failing).
Per-environment applicationIds
upvendo-pos builds a distinct applicationId per environment so all three can coexist and each gets its own store listing:
| env | gradle flavor | applicationId |
|---|---|---|
| testing | dev | com.upvendo.pos.testing |
| staging | staging | com.upvendo.pos.staging |
| production | production | com.upvendo.pos |
The flavor is dev, not testing, because AGP rejects flavor names starting with test.
⚠ CapacitorUpdater.appId stays pinned to the bare com.upvendo.pos for every environment. Capgo keys on it; a suffixed build without the pin gets HTTP 400 and silently stops updating. The environment is carried by the channel, not the app id.
⚠ A changed applicationId is a different app, not an update. Device state does not migrate — the activation credential and any undrained order/punch outboxes live in package-scoped storage. Drain → install → reactivate → verify → remove the old install.
Fullscreen is done by the app, not by the MDM
SUNMI DMP's Kiosk Mode has Hide Status Bar / Hide Navigation Bar toggles. Configured, published and bound to a D3 PRO, the bars stayed visible.
The same profile's Disable Uninstallation did take effect (adb uninstall returns DELETE_FAILED_OWNER_BLOCKED), so the profile reached the device and the Device Owner enforces it — the app-bound kiosk rules did not bite, most likely because the APK was sideloaded (installer=null) rather than installed by SUNMI.
upvendo-pos therefore hides both bars itself in MainActivity, and so does upvendo-kiosk. Do not make fullscreen contingent on an MDM agent polling.
Note:
upvendo-kiosk'scom.upvendo.devicesdkwrapper frontscom.ys.rkapi.MyManagerfromysapi.jar— a non-SUNMI vendor SDK for the kiosk board. It is a no-op on SUNMI hardware. SUNMI's own SDKs (customer display, cash drawer, printer, scanner) are a separate, unstarted integration.
Kiosk exit PIN defaults to 1234
The Kiosk Mode Temporary Exit Password — what lets someone leave kiosk mode and reach the device — ships as 1234 on a new profile. On a merchant till that is a hole: anyone who knows the vendor default can unlock the terminal. Change it, and prefer the Dynamic pw option for a fleet so one leaked code cannot unlock every customer's device.
Removing or replacing an app on a managed device
Two independent SUNMI rules block uninstall. This is the single most expensive thing to get wrong, because turning off the obvious one does nothing while the other is still set.
Disable Uninstallation — the explicit Profile toggle.
Default Startup / Auto Install — SUNMI's Help Desk states it plainly:
"If you are using the channel customization machine, and the channel partners set some applications in their partner account as the default startup / automatic installation, end user cant delete these applications from device." — Why some apps can't be deleted? (retrieved 2026-09-05)
Selecting a Default Startup app also switches that app to automatic installation, so setting Default Startup alone is enough to make the app undeletable — no Disable Uninstallation required.
What we measured
On the D3 PRO, adb uninstall com.upvendo.pos returned DELETE_FAILED_OWNER_BLOCKED after all of the following:
- unbinding the profile (retried over ~4 min)
- republishing the profile as V2.0 with Disable Uninstallation OFF, still bound (~3 min)
Neither is evidence of a SUNMI defect: com.upvendo.pos remained the Default Startup app throughout, so rule 2 was active the whole time. Neither test isolated the variable. Treat the earlier reading of this as "unbind does not roll back" — which is what it looked like — as unproven.
The documented removal procedure
Do these in order, with the profile still bound — once unbound, DMP may no longer have the association it needs to deliver a clearing instruction:
- Cancel Default Startup for the app.
- Device → App List → find the app → unselect "App auto install" (SUNMI: How to Turn Auto Install off, retrieved 2026-09-05).
- Set Disable Uninstallation OFF and publish.
- Keep the device online; verify a normal uninstall succeeds.
- Only then unbind the profile.
DMP also has a remote removal command — Remote Assistance → Other Remote Management → Uninstall app — which supports batch uninstall and needs the device online.
The escape hatch: pm uninstall -k bypasses the block
Measured 2026-09-05 on the D3 PRO, reproduced 4/4. With the profile bound, Default Startup set to com.upvendo.pos, and the app freshly reinstalled each time:
adb install -r app-testing-release.apk -> Success
adb shell pm uninstall com.upvendo.pos -> Failure [DELETE_FAILED_OWNER_BLOCKED]
adb shell pm uninstall -k com.upvendo.pos -> Success # app goneSame package, same shell, seconds apart — the only difference is -k (DELETE_KEEP_DATA). Negative control: pm uninstall -k com.upvendo.nonexistent.control returns Failure [DELETE_FAILED_INTERNAL_ERROR], so the Success is a real deletion, not a no-op that reports success for anything.
Two controls rule out the obvious alternative — that the second attempt succeeds regardless of the flag, e.g. because the all-users block check runs in a posted handler against a fresh snapshot:
| Control | Result |
|---|---|
-k as the first command after a fresh install, no plain attempt before it | Success |
plain pm uninstall three times in a row | blocked all three, app still installed |
So it is not ordering, not repetition, and not an async-timing window: the block is stably on, and -k succeeds immediately without any preceding attempt.
Afterwards pm list packages is empty and /data/app/*upvendo* is gone, but pm list packages -u still lists the package — -k leaves a data-retained tombstone.
Two consequences:
- This is the working way to unstick a device when an app is blocked and you have adb.
- "Disable Uninstallation" is not a hard guarantee. It stops the Settings UI and a normal
pm uninstall; it does not stop anyone with adb shell access. On a merchant till that is academic (USB debugging is off), but do not treat the toggle as tamper-proofing.
⚠ Reinstalling does not clear the block. In all four runs the freshly installed app was blocked again immediately — the policy is keyed on the package name and survives the package being removed, so it applies the moment the name reappears.
Mechanism: stock AOSP cannot do this — SUNMI's ROM diverges. Read from frameworks/base at tag android-13.0.0_r24:
PackageManagerShellCommand.runUninstall()—-ksets onlyDELETE_KEEP_DATA; omitting--userleavesuserId = USER_ALL, which setsDELETE_ALL_USERS. So plainpm uninstallandpm uninstall -kdiffer byDELETE_KEEP_DATAand nothing else.DeletePackageHelper.deletePackageVersionedInternal()returnsDELETE_FAILED_OWNER_BLOCKEDfrom exactly two places, and neither consultsDELETE_KEEP_DATA:if (!deleteAllUsers && snapshot.getBlockUninstallForUser(pkg, userId))— catches--user 0.- the
deleteAllUsersbranch: if any user blocks it, the loop deletes only for non-blocked users and then setsreturnCode = DELETE_FAILED_OWNER_BLOCKED. With user 0 the only user and blocked, nothing is deleted.
DELETE_KEEP_DATAappears in that file only in the deletion mechanics (whether to wipe data) and in the system-app-update path — never in the guard.
On stock Android 13 both commands therefore hit the same guard and both should fail. On the D3 PRO one fails and the other succeeds, 4/4. That difference is not explicable by AOSP, so the SUNMI ROM (or its BaseService/agent hook) is diverging from upstream in the delete path.
Worth reporting to SUNMI: it means their own "non-removable" guarantee has a hole, and it is their code path, not Android's.
After repointing Default Startup to a new package
Measured 2026-09-05 when the D3 PRO was moved from com.upvendo.pos to com.upvendo.pos.testing (profile V3.0). Two things do not clean themselves up:
sunmi_watchdog_packagekeeps pointing at the OLD package. On the device it still read{"package":"com.upvendo.pos","times":2}after the repoint, naming a package that is no longer installed. Harmless while Auto Recovery is off; repoint it before enabling that.- The new app is uninstall-blocked too, even with Disable Uninstallation off. Verified: the toggle was explicitly turned off in V3.0, and
pm uninstall com.upvendo.pos.testingstill returnedDELETE_FAILED_OWNER_BLOCKED. That is the documented behaviour — Default Startup blocks uninstall on its own — not a failed publish.pm uninstall -kremains the way out.
Useful settings to read the applied state directly (namespaces matter):
adb shell settings list system | grep auto_start_app # Default Startup, and whether enabled
adb shell settings list global | grep watchdog # Auto Recovery targetStill unverified
- Whether remote uninstall bypasses an active restriction or requires clearing it first.
- Whether any DMP action explicitly calls
setUninstallBlocked(..., false). Android has no framework auto-revert — something must call it withfalse— and SUNMI does not document which console action does.dumpsysdoes not expose the flag (neitherdevice_policynorpackagedumps it), so the only definitive on-device query is an app callingisUninstallBlocked(). - No SUNMI source gives a convergence time for a republished profile, or says a reboot is required.
Kiosk profile on the FLEX 3 (2026-09-14) — applied and verified
Profile "Upvendo BNL Kiosk" created and bound to the FLEX 3 (F30625AVJ1031) only:
- Default Startup =
com.upvendo.kiosk.dev, Disable Uninstallation OFF (testing device — the block is awkward to clear —pm uninstall -kis the way out, see above — so keep it off here), Auto Recovery OFF. - Kiosk Mode = app
com.upvendo.kiosk.dev, Active Recovery ON, Hide Status/Navigation Bar ON, Restrict Jump OFF (locking to the single app can break payment/external activities; the app hides its own bars anyway). - Exit password changed off the
1234default to a static 4-digit (rotate for a merchant fleet, or switch to Dynamic pw so one code can't unlock every kiosk).
Verified on the device (adb shell settings): kiosk_mode_enabled=1, app_manager_auto_start_app=com.upvendo.kiosk.dev (enabled), kiosk_mode_limit_single_app=0, kiosk_mode_dynamic_pwd_enabled=0. ⚠ The OS hide-bar flags (system_ui_kiosk_hide_statusbar, system_ui_kiosk_navigation_bar_hidden) read 0 despite the profile toggles being ON — undiagnosed (may need a reboot, or the FLEX 3 handles it differently), but immaterial because the kiosk hides both bars itself in MainActivity. The watchdog reset to empty when this profile took over.
Epson TM-m30III USB printing on the FLEX 3 — verdict (analysis; no printer attached to test)
The kiosk's EpsonUsbPrinterPlugin (packages/capacitor-printer-transport-usb) is standard Android USB-host, device-agnostic — nothing SUNMI-specific: it matches Epson 04b8:0e32, enumerates already-attached devices in load() and listens for USB_DEVICE_ATTACHED, and res/xml/usb_device_filter.xml (same VID/PID) carries the implicit USB-permission grant on attach-launch. So the same code that prints on the Elo should print on the FLEX 3 — no code change expected. Two things to verify when a printer is on hand (the FLEX 3 had none — its USB bus is ASIX Ethernet + ILITEK touch + a hub):
- PID of the exact model. The plugin hardcodes
0x0e32(TM-m30III). RM's unit is the TM-m30III-H variant — confirm it enumerates the same PID (dumpsys usb/lsusbwith it plugged in). A different PID means neither the filter nor the plugin matches and it silently won't print. - Cold-boot USB permission under kiosk lockdown. The implicit grant arrives via the attach intent (hot-plug) or attach-launch. If the printer is already attached at cold boot and the app is started by Default Startup (not by the USB-attach intent), it may lack permission and fall to a
requestPermission()dialog — bad in locked kiosk mode. Mitigations: power-sequence so the printer attaches after boot (fires the intent), or pre-grant via the profile's Super Permissions. This is the one plausible FLEX-3/kiosk adjustment; verify it on hardware. LAN printing (proven on Elo, kiosk#587) sidesteps USB permission entirely and stays the safer default where a network printer is available.
Hiding the system bars on SUNMI — the hard-won recipe (2026-09-14)
App-level immersive mode does NOT hide SUNMI's system bars. Both upvendo-pos and upvendo-kiosk hide bars in their own MainActivity, and both still showed the status bar and SUNMI's navigation bar (the bottom bar carries SUNMI launcher icons — it is a custom system bar, not the stock one). Only DMP Kiosk Mode removes them. Do not waste time on app-side immersive for fullscreen on SUNMI.
The DMP Kiosk Mode editor is buggy/finicky — this cost hours. The recipe that works:
- Profile → Kiosk Mode → Add app → in the picker, type the name so the list filters to your app, then click Add. ⚠ Verify the app actually appears in the Kiosk Mode lock TABLE (the "App / Device User Lock / Operation" table), not just that the picker closed. On several tries the app silently failed to land in the table.
- If no app is in the lock table, the whole Kiosk Mode item cannot be saved — and it is then dropped from the published profile entirely. Symptoms this produces:
- deploy fails with "Unsaved configuration items detected. Please click Save and try again" (the real cause is the empty Kiosk Mode item, not a literal unsaved toggle);
- or it "publishes" but the version does not bump and the DETAIL (read-only) view shows only your other items (e.g. Default Startup) with no Kiosk Mode section.
- Only once the app is in the lock table do the rule switches (Hide Status Bar, Hide Navigation Bar, Active Recovery) become saveable. Toggle Hide Status/Nav Bar ON.
- ⚠ "Save Configuation" does NOT visibly disable after saving — do not treat a disabled/enabled Save as proof of anything. Verify by re-opening the DETAIL view.
- Deploy Profile → Operation Confirmation → Continue → Final Confirmation (CAPTCHA) → enter code. Then confirm the profile version incremented (ours went V1.0 → V2.0, Item count 1 → 2). If it is still V1.0, it did NOT publish.
- On the device, the bars hide once Kiosk Mode engages (app foreground / after boot). SUNMI's own flags (
system_ui_kiosk_hide_statusbar,..._navigation_bar_hidden) are not the live enforcement — setting them by adb does nothing; kiosk-mode engagement is what hides the bars, and SUNMI's doc says these are "Forced Configuration — restored after restart."
Verified 2026-09-14: profile "Upvendo BNL Kiosk" V2.0 (Default Startup + Kiosk Mode with com.upvendo.kiosk.dev locked, Hide Status/Nav Bar ON, Restrict Jump OFF, exit PIN off the 1234 default) → FLEX 3 shows the kiosk menu full-screen, no bars.
Blank white kiosk after reboot — was transient, not a broken bundle
After the reboots + profile deploy the kiosk WebView came up blank white. It was not a Capgo-erased build: logcat showed CapgoUpdater … notifyAppReady was called and getLatest … up_to_date (defaultChannel testing, bundle 1.0.2609132037) — the JS had loaded fine — and network was up. A simple am force-stop + relaunch rendered the menu correctly. If a kiosk is blank after boot, force-stop/restart it before assuming anything worse. (The testing build still ships with VITE_ENABLE_LIVE_UPDATES at its default; for a sideloaded testing kiosk, consider building it false to avoid OTA churn — see the Capgo trap note in the kiosk repo.)
Epson TM-m30III over USB — CONFIRMED matching (correcting an earlier note)
RM connected the Epson to the FLEX 3 by USB (there is also a LAN Epson at 192.168.0.231:9100). It enumerates on the FLEX 3 USB bus as VID 0x04b8 / PID 0x0e32 (product_id=3634) — exactly the EPSON_VID/TM_M30III_PID the kiosk's EpsonUsbPrinterPlugin hardcodes. So the -H variant matches, and USB printing needs no code change. (Its USB descriptor enumerated null manufacturer/product strings, which is why an earlier scan missed it — match on PID, not name.)
Confirmed working at the OS level (2026-09-14, printer powered): powered on, it enumerates as manufacturer=EPSON product=TM-m30III on /dev/bus/usb/001/007, and dumpsys usb shows a USB permission grant for uid 10180 = com.upvendo.kiosk.dev — the attach-launch flow granted it automatically on hot-plug, no dialog. So USB printing needs no code change and no manual permission; the only remaining step to print is app-level (point the kiosk at the USB printer in the back office
- run an order).
The one kiosk-lockdown caveat still stands: a printer attached at cold boot when the app is auto-started by Default Startup has no attach-intent, so the implicit grant may not happen — verify that case (power-sequencing, or the profile's Super Permissions) before relying on it in production.
POS/D3 bars — FIXED the same way (2026-09-15)
Applied the identical Kiosk Mode recipe to the Upvendo BNL POS profile: added com.upvendo.pos.testing to the lock table, Hide Status Bar + Hide Navigation Bar ON, Active Recovery ON (main app = the POS), redeployed → V4.0 (CAPTCHA), Device Count 1. After a reboot of the D3 PRO, both bars are gone (confirmed visually by RM — top status bar and bottom taskbar).
Two D3-specific notes worth keeping:
kiosk_mode_enabledreadsnullon the D3 even with bars hidden. Do not trust it. The D3 (ROM 4.0.79) enforces the lock throughsunmi_super_app_mode=1(+sunmi_super_app_list/_selfstart/_cloud_listall =com.upvendo.pos.testing), not the legacykiosk_mode_enabledflag the FLEX 3 sets. Same "settings don't map 1:1 — verify behaviourally" trap as the nav-bar flag. The config is pushed live (super-app settings populate immediately) but only engages after a reboot — a fresh app relaunch did not engage it; the reboot did.- Physical Keys is NOT available on the D3 PRO (SUNMI's Physical Keys doc lists only L3 / FLEX 3), so the power-button dialog (Power off / Restart / Exit Kiosk) can't be disabled on the D3 at all — even less than on the FLEX 3. Accepted: the D3 is the staff-facing till, so the restart/shutdown risk is lower than on a customer kiosk.
The D3's POS was still on the "Activate this device" screen during this work (running but not bound to a merchant) — unrelated to the bars; Kiosk Mode locks the app regardless of activation.
Reboots drop Wireless debugging
Every FLEX 3 reboot turns Wireless debugging OFF (not persistent) — adb is lost until it is re-enabled on the device, after which the mDNS connect port changes (re-discover with adb mdns services; the earlier pairing persists, so no re-pair needed).
Disabling the power button (customer restart/shutdown) — Physical Keys is a NO-OP on this FLEX 3 ROM (2026-09-15)
Goal. On the customer-facing kiosk, a long-press of the physical power button opens SUNMI's kiosk global-actions dialog with three items — Power off · Restart · Exit Kiosk — and only Exit Kiosk is code-gated (the Temporary Exit Password). Power off / Restart fire immediately, so a customer can shut the kiosk down. Requirement: customers must not be able to restart/shut down; staff keep a code-gated path.
What SUNMI offers, and what it does NOT. Checked every relevant Profile config (Kiosk Mode, Physical Keys, Security, Access Control):
- There is no setting to remove just Power off/Restart from that dialog, and no way to put the exit code on them. The dialog is all-or-nothing.
- The only lever is Profile → System → Physical Keys (docs say "Supported Models: L3, FLEX3"): Long Press Power Key = No Interference / Disable Long Press / Show Power Menu / Voice Assistant; Short Press = No Interference / Disable Short Press / Sleep&Lock; rule Recommended vs Mandatory (Enforced). The editor itself warns: "This setting affects long-press exit for Kiosk Mode" — i.e. Disable Long Press also kills the Exit Kiosk item, so the on-device admin exit must move into the app (it already has one — see below).
What we did. Added a Physical Key item to "Upvendo BNL Kiosk" (Long Press = Disable, Short Press = Disable, Mandatory Control), deployed → V3.0, 3 items (CAPTCHA passed), Device Count 1.
Result: the FLEX 3 silently ignores it. Verified on hardware (SN F30625AVJ1031, ROM 4.0.23_336, Android 13): after deploy and a full reboot (Active Recovery re-applies the profile on boot), a long-press — physical and injected input keyevent --longpress KEYCODE_POWER — still opens the Power off/Restart/Exit dialog. logcat shows SUNMI's ROM handler doing it: SunmiCustomKey: interceptKeyBeforeQueueing::26 → GlobalActionsDialogLite: getCurrentKioskMode true → shouldShowAction true. The handler runs but never applies the disable policy. The device reaches the SUNMI cloud fine (ping OK, MDM agent com.sunmi.remotecontrol.pro running), so this is not a delivery/connectivity failure.
Root cause = ROM support gap, not a config error. SUNMI's own Supported Profiles List shows the FLEX 3 column blank for most newer System configs (Display Size, Font Size, Screen brightness/timeout/rotation, Wi-Fi/Ethernet toggle…) — all ✅ only on T2s / T3 PRO. Physical Keys is newer still and not in that matrix at all. SUNMI's caveat is explicit: "a ✅ does not guarantee that your individual device meets the necessary conditions for support under its current system version." Net: the DMP accepts and stores Physical Keys, but ROM 4.0.23 doesn't enforce it.
Consequences / options.
- The Physical Key config was left in the profile (V3.0) — harmless, and it will activate automatically if a supporting ROM is ever installed. Removing it needs another CAPTCHA for no gain.
- Real residual harm is small: Restart → device reboots → Default Startup + Active Recovery relaunch the kiosk (~1 min, self-healing); Exit Kiosk → needs the code; only Power off (device stays off) is a genuine nuisance, and only after a deliberate long-press.
- Proper fix is a SUNMI support ticket (does FLEX 3 support Physical Keys, and on which ROM?) → ROM update → re-test. Not attempted unattended (ROM update = downtime + re-verification).
The app already has the staff path, independent of the button. upvendo-kiosksrc/pages/setting.vue is a PIN-gated admin screen (5 taps on the landing screen → PIN 111111, hardcoded in src/components/KeyboardPasswordNumber.vue) with a Power section: Reboot / Restart App / Shutdown (each behind a confirm) plus printer setup, live-update, admin-switch, sign-out. So staff can reboot/shut down with a code regardless of the hardware button — the button lockdown was only ever about removing the un-coded customer path.
⚠ Do not read a setting to confirm a SUNMI Kiosk/Physical-Keys config applied. These configs do not map 1:1 to readable
settings: e.g. Kiosk Mode "Hide Navigation Bar" is ON in the profile yetsystem_ui_kiosk_navigation_bar_hiddenreads0on the device while the bar is visibly hidden. Verify config application behaviourally, never bysettings get.
USB printer permission: Super Permissions is the durable fix — but it does NOT un-break a cold call (2026-09-15)
Symptom. After a reboot, the FLEX 3 lost the Epson's USB grant and the kiosk crashed outright:
java.lang.SecurityException: User has not given 10180/com.upvendo.kiosk.dev
permission to access device /dev/bus/usb/001/007
at com.upvendo.plugins.printerusb.EpsonUsbPrinterPlugin.isAttached(EpsonUsbPrinterPlugin.java:92)isAttached() called UsbDevice.getSerialNumber(), which requires USB permission. Uncaught → the process died. Because isAttached() runs on the hidden settings screen and on the checkout print path, the kiosk dropped out of kiosk mode to the SUNMI launcher — which surfaced SUNMI Remote Assistance to the customer — and fired Firebase Crashlytics + the Slack printer alert. One bug, three symptoms: the crash, the alerts, and the Remote Assistance window.
The config half — Profile → App Control → Super Permissions. SUNMI can auto-grant USB so the attach-launch grant never has to be re-earned. Add the app, then Edit Related Permissions:
| Permission | Value | Why |
|---|---|---|
| USB Permission Bypass | APP_USB | "use any USB peripheral as the default device without requiring user confirmation" — the fix; survives reboots |
| App Protection (LMK) | APP_LMK | raises OOM priority so Android is less likely to kill an unattended kiosk |
| Dynamic Permission Bypass | DYNAMIC | auto-grants runtime permission prompts, so a customer never hits a dialog in locked kiosk mode |
The rest (Power Manager, Network Management, Write Setting, Do not disturb) have no use for our kiosk — don't grant powers you don't use. Correction 2026-09-29: Alert Window IS required — it is what lets the kiosk relaunch itself from the background on Android 13; see Required DMP settings for every kiosk. Deployed on "Upvendo BNL Kiosk" → V4.0. Verify on device: settings get global super_power_app must list the package with APP_USB; DYNAMIC shows up separately as auto-granted runtime permissions in settings get system super_permission_list.
⚠ Granting
APP_USBdid NOT stop the crash on its own — measured, not assumed. With the permission live insuper_power_app, a coldgetSerialNumber()still threw: the bypass auto-approves arequestPermission()flow, it does not retroactively satisfy the framework'sUsbUserPermissionManager.checkPermission()for a call that never asked. Config and code are both required. The code half isupvendo-kioskPR #666 — the serial is optional (the printer is already matched on VID/PID), so a missing grant reportsattachedinstead of killing the app.
End state verified on hardware (FLEX 3 F30625AVJ1031): settings opens without crashing and reports "TM-m30III — USB verbonden", and a real test order printed end-to-end.
Rebuilding the kiosk APK — four traps that cost a build each (2026-09-15)
Delivering the fix above meant a native rebuild (a Capgo OTA cannot carry native code — it is JS-only). In order:
android/keystore.propertiespointed at the legacyupvendo_snackbar.keystorewith a password that does not open it (keystore password was incorrect). The real key isandroid/upvendo_kiosk.keystore, aliasupvendo_kiosk(SHA-256B9:26:A4:D4…51:8A:6B:42) — the same key the store build was signed with, soadb install -rupdates in place. Password belongs in 1Password, not a scratchpad.android/app/gradle.propertiesis generated, not committed. A fresh clone/worktree has no copy, and the release guard refuses the build: "native env values are missing or blank: BASE_URL, WEBHOOK_SLACK_URL, VITE_ENV, CAPACITOR_API_KEY".scripts/build-apk-<env>.shwrites it — and noteBASE_URLmaps fromVITE_CLOUDFLARE_D1_API_URL, not the backend endpoint. Guessing that mapping bakes a wrong URL, which is exactly what the guard exists to catch.- Build from a CURRENT branch or you ship stale web. The APK's builtin bundle is whatever the branch builds. Building from a branch 62 commits behind
origin/testingput ~4-week-old JS on the kiosk. Worse, sideloaded builds run Capgo withautoUpdate = onlyDownload(deliberate, so OTA can't erase a sideload), so the device logs "downloaded update will not be set as next bundle" and will not self-recover — the newer bundle sitspendingforever. Cut the branch offorigin/testing. - A reinstall drops the device session. The kiosk returns to "Login using your device code" and must be re-activated with the merchant's device code. The in-app code entry uses the app's own on-screen keyboard, so
adb shell input textdoes nothing — tap the keys.
⚠ Version-train debt: forcing installs over a hand-bumped APK left the internal FLEX 3 on versionCode 233 while origin/testing is 230. Any official build below 233 will be refused with INSTALL_FAILED_VERSION_DOWNGRADE until the train passes it (or the app is uninstalled, which costs another re-activation). Don't hand-bump a field device unless you must.
Group membership does NOT provision a device — bind the profile to the device (2026-09-15)
⚠ Superseded 2026-09-22:
Internalis now split intoInternal Kiosks/Internal POS, each with its type's profile group-bound, so dropping a device in the right group does provision it. Current tree and procedure: runbook.
A second D3 PRO (DP02261811229, ROM 4.0.79) was activated for internal testing at a colleague's site. It appeared in DMP immediately (Activation Time set, Last Active: - — registered but never checked in), and it was already inside the Upvendo BNL Internal group with the other units. That group membership provisioned almost nothing, and the reason generalises:
Upvendo BNL Internalhas exactly one bound profile —Upvendo BNL POS BaselineV1.0 — whose single config item is Time zone (Europe/Brussels+ Forbid User Modification). Verified by opening the item, not by trusting the profile's description.- Everything that actually makes a till a till — Default Startup, Kiosk Mode (app lock, hidden bars) — lives on
Upvendo BNL POS, which was bound to one device (DP02261212845), with Group (0) and Smart Group (0).
So a new device dropped into the right group still boots to the bare SUNMI launcher: no app auto-start, system bars visible, no lockdown. Fix: bind the profile to the device, from the profile's own Bound tab → Add Device (the device-group wizard silently no-ops — see above). Binding needs no CAPTCHA; unlike publishing a profile it takes effect without the high-risk gate. Result here: Upvendo BNL POS → Device (2).
⚠ Do not "just bind the POS profile to the group" as a shortcut.
Upvendo BNL Internalis mixed-type — it holds the FLEX 3 kiosk and both D3 tills. A group binding would push POS Default Startup onto the kiosk. Either bind per device, or split the group by device type (Internal → Kiosks / POS) so future units inherit correctly. DMP merges profiles by priority Device > Group > Smart Group, so a per-device binding also wins over the group baseline.
A device bound while offline is fine — the configuration applies when it first checks in.
Fleet provisioning: what DMP offers that we are not yet using
Documented on SUNMI's DMP product page and in the Partners Platform Manual. None of this has been exercised by us yet — it is a survey of what exists, not a report of what we have done:
- Staged / grayscale app rollout — release a new app version to selected devices or SNs first, then widen. This is the closest thing SUNMI has to our
testing → staging → productionsplit, and it is the natural way to let Keanu test a build before it reaches merchants. - Staged OTA rollout — same idea for firmware, with SN batches and scheduling.
- Targeting by group, serial number, or device model, over hierarchical device groups.
- Batch workflows — move devices between groups, and bind/transfer ownership by pasting SNs or uploading an Excel template.
- Offline QR-code setup and factory pre-installation — provisioning paths that do not need the device to be configured by hand at all.
- Copying a master account's app-store configuration to a controlled subaccount. Relevant to resellers: it propagates the store config, not a whole device image.
⚠ SUNMI's batch SN workflows cover transfers, subaccount binding and recovery. That is not necessarily permission to self-import arbitrary new devices — some official onboarding instructions tell partners to send the SN list to SUNMI support instead.
Not found in any public SUNMI documentation: a reusable provisioning template separate from Profiles, a "clone this device's setup" action, or any way to capture one device's current state and reproduce it on others. Profiles are the reuse mechanism.
First boot: five layers, and which one the OpenAPI solves
Written after powering on the FLEX 3 (SN F30625AVJ1031) on 2026-09-14: out of the box it runs the same Android setup wizard as the D3 PRO, and its screen did not rotate to match its mount. The Parameter Management work further down solves only the last of these layers.
| Layer | Today | What could automate it |
|---|---|---|
| 1. Android setup wizard (language, network, Google) | manual on every device | Nothing documented that we can do ourselves on already-shipped units — see below |
| 2. Network | Ethernet, or WiFi typed on the device | Ethernet — see below |
| 3. SUNMI Profile | automatic once online | already works (timezone proven on hardware 2026-09-04) |
| 4. App install | Default Startup | the app must be published in the private store for that model |
| 5. Merchant binding | activation code typed on the device | Parameter Management push — see Automating this |
Layer 1. DMP can suppress only the SUNMI ID step (System Customization → Common → "Display the SUNMI ID process when the machine is activated"). SUNMI advertises "offline QR code setup" and "factory pre-installation" on its DMP page but documents neither: no QR schema, no statement that either skips the Android wizard, and nothing on whether it applies to units already shipped. These are questions for SUNMI, not settings we have. Android Enterprise QR provisioning (six taps on the welcome screen) does skip most of the wizard and can carry WiFi, but it provisions its own Device Owner, and Android refuses a second one ("device owner is already set", AOSP DevicePolicyManagerService). With SUNMI's agent already owner, that route either fails or ends SUNMI DMP management — not compatible with the decision to stay on DMP.
FLEX 3 read over adb 2026-09-14 (post-activation)
Paired + connected over its Ethernet NIC (192.168.0.109, adb guid confirms SN F30625AVJ1031). Android 13/SDK 33, Device Owner com.sunmi.remotecontrol.pro (same as the D3).
Profile pipeline works on FLEX 3.
persist.sys.timezone = Europe/Brussels— the Baseline profile reached it automatically after activation, zero touch, exactly as on the D3.localestayeden-GB/en-US(Baseline only enforces time zone).Rotation: RESOLVED — no action needed. RM confirms the FLEX 3 auto-rotated correctly once past the first-boot wizard; the wizard just hadn't rotated yet. Consistent with the device state:
accelerometer_rotation = 1(auto-rotate on), sensor present. So the earlier "didn't rotate" was a wizard-only artefact, not a provisioning gap. Drop Duranmatic Q5. (upvendo-kiosklocks orientation only on phones —smallestScreenWidthDp < 600inMainActivity— and following the sensor is fine here; an in-app lock remains available if a fixed mount ever needs determinism, but it is not required.)Our app is NOT installed (
pm list packages com.upvendoempty, no Default Startup). The FLEX 3 is a kiosk screen, so it runsupvendo-kiosk— a separate Capacitor app fromupvendo-pos, productionapplicationId com.upvendo.kiosk, versionCode 229 / versionName 1.0.205. To reach it the scalable way (Default Startup, as POS did on the D3) the kiosk must be published to the SUNMI private store as a signed APK.Signing — three keys, do not conflate them (checked 2026-09-14):
upvendo-kiosk/android/upvendo_snackbar.keystore— legacy APK-signing key.keytool -listwith the stored password fails "keystore password was incorrect". This path is abandoned; the lost password does not block the FLEX 3.android-bundle-signing-key— the real production key:build:productionbuilds an AAB (bundleRelease+jarsigner … upvendo_snackbar) for Google Play, which is how existing Elo/merchant kiosks are distributed (native via Play, JS via Capgo OTA). The file is not on this Mac and there is no in-repo Play automation — prod releases are built/uploaded by hand (Faizal owns prod). Do not touch this key or channel.upvendo-pos/android/upvendo_pos.keystore— opens with its stored password; a working signing path we control.
So publishing the kiosk to the SUNMI store is NOT blocked on the lost password. The SUNMI private store is a new channel with zero installed base, so sign the FLEX 3 build with a key we control and publish — existing merchants are on Google Play + Capgo and are untouched. Recommendation: mint a dedicated
upvendo_kiosk.keystore(keep it forever for the SUNMI channel so FLEX 3 units get in-place updates; password to 1Password) rather than reuse the POS key. Open sub-decision: whether the SUNMI/kiosk build should also get a per-env applicationId (com.upvendo.kiosk.testing, the way POS did in #104) — not required to unblock, but cleaner. The stalecom.upvendo.kiosk.taufikseen in a January APK is a dead contractor artefact, in no tracked file — ignore it. Interim eyes-on:assembleDevDebug(debug key, no password) sideloads via adb, testing only.
Kiosk on the FLEX 3 — built, signed, and running (2026-09-14)
The FLEX 3 now runs upvendo-kiosk, signed with a dedicated key we control — no contractor, no merchant impact. Proven end to end: keystore minted → signed APK → installed on the real FLEX 3 → kiosk activation screen live, in portrait.
- Key:
upvendo_kiosk.keystore, RSA 4096, aliasupvendo_kiosk, DNCN=Upvendo Kiosk, OU=Kiosk, O=Upvendo Benelux BV, L=Gent, C=BE, SHA-256b926a4d4…518a6b42. In 1Password; gitignored (follows the POS convention, not the legacy commit-the-keystore one). SUNMI channel only — keep it forever for in-place FLEX 3 updates. - Artifact:
com.upvendo.kiosk.dev(dev flavor = testing env), versionName 1.0.206 / code 230,apksigner verifyclean, cert = the key above. Testing build deliberately (internal FLEX 3 → testing backend); production is the identical process with theproductionflavor for the merchant wave. Both can live in the private pool side by side, like POS. - On device: installs and launches; first run requests Display over other apps (grant via the profile's Super Permissions in production; granted by adb for this test). Screen is portrait (1080×1920) — auto-rotate handled it, no app change needed.
Activated and running on the FLEX 3 (2026-09-14). Entered a back-office device code on the unit; it activated to the "Foodconcept NUUS" test merchant and rendered the real kiosk — language picker, dine-in/takeaway landing, live menu with products/prices/out-of-stock — in portrait. The whole merchant flow works on SUNMI hardware, signed with our key.
⚠ Kiosk is portrait-only; landscape content does not render (it degrades to a loading logo). This matters for the store listing: FLEX 3 is one device that appears in BOTH the portrait and landscape lists (shared checkbox value=291; ticking or unticking one toggles both), so SUNMI demands 3–5 portrait AND 3–5 landscape screenshots. We have 3 good portrait shots (activation, landing, menu) and only one good landscape frame (the full-bleed storefront attract screen); the content screens have no usable landscape rendering. So publishing the kiosk to the store is blocked on landscape screenshots, a real product constraint, not a mechanical one. Either the kiosk gains a landscape layout, or the FLEX 3 listing uses attract-style landscape imagery. The app is meanwhile sideloaded and working on the FLEX 3, so testing is not blocked.
Build recipe + the gotchas that cost time (do it in a worktree off origin/testing):
git worktree add … testingthengit merge --ff-only origin/testing— the localtestingwas 209 behind; the preflight requires exact sync.git submodule update --init --recursive— shared-ui is a submodule and a fresh worktree does not check it out; without itbuild:cssdies onsrc/shared-ui/styles/global.scss.- Copy
.env.testing.localfrom the main checkout (worktree lacks untracked files) and addVITE_SENTRY_DSN= the existingVITE_SECURE_SENTRY_DSNvalue — the env-contract validator wants the canonical name; the repo's env files still use the oldVITE_SECURE_name. (DSN is a client-embeddable id, not a secret.) - Put
upvendo_kiosk.keystore+ akeystore.propertiespointing at it in the worktree; add both (and the env files) to the common.git/info/excludeso the preflight'sgit status --porcelainclean-gate passes (per-worktreeinfo/excludeis not read). - Build with JDK 21, not 17 —
:sentry-capacitornow targets Java 21 (invalid source release: 21under 17). JDK 21 still compiles the Java-17 printer modules. Memory note saying "JDK 17" is stale. gradlew cleandeletes committedpackages/*/android/build/**artifacts (a repo wart — build output was checked in), dirtying the tree for the next run's preflight;git restore packages/before re-running (preflight runs before clean, so this is enough).npm run build-apk:testing→assembleDevRelease→builds/app-dev-release.apk.
PUBLISHED to the private store 2026-09-14 — [DEV] Upvendo Kiosk / com.upvendo.kiosk.dev / v1.0.206, status Under review (POS went straight to Published; the kiosk shows Under review, likely because it declares a USB service intent — is_service_action=1). The FLEX 3 listing needs both orientations (see the portrait-only note above): uploaded 3 real portrait shots (activation, landing, menu) to the vertical uploader and 3 landscape to the horizontal one — the storefront attract frame plus two letterboxed content shots, acceptable for a private, channel-only listing. Two uploader gotchas: the portrait target is vertical-img-uploader, the landscape is horizontal-img-uploader (uploading portrait to the horizontal slot is silently rejected on dimensions); and Notes is capped at 200 chars. Once approved, bind it via a Profile's Default Startup so the FLEX 3 auto-installs/persists it.
The fleet answer: factory pre-provisioning via the distributor (Duranmatic)
For units already in hand the first-boot wizard is an unavoidable per-device human step (Ethernet removes the network typing; language + a couple of confirmations remain, then profile/app/kiosk flow automatically). That does not scale to a merchant fleet, and the scalable path is not anything we toggle in the console — it is factory channel customization, arranged on the order through the distributor we buy from (Duranmatic), who places the SUNMI factory order.
Two things happen at the factory, both the distributor's to arrange:
- Channel customization — our APK preinstalled, our DMP profile applied, wizard suppressed, so the unit boots straight into the POS.
- SN → account binding — each serial bound to the Upvendo Benelux partner account at source. This rides on the supplier transfer step by which devices already reach our entity today.
⚠ Not confirmed that Duranmatic/SUNMI offer this for Benelux orders — it is the correct party to ask, not a known capability. Questions to put to Duranmatic: (a) factory channel customization (app + profile + wizard suppression)? (b) SN binding to our partner account at the factory? (c) MOQ, lead time, cost? (d) covers both D3 PRO and FLEX 3? (e) can FLEX 3 default orientation be set at the factory — the clean fix if the DMP profile cannot rotate it.
SUNMI's advertised "offline QR code setup" could shorten the in-hand path to one scan, but a docs search (2026-09-14) found no developer documentation for it — only DMP self-hosting deployment guides. Treat it as another question for SUNMI, not a usable feature yet.
FLEX 3 activation completed 2026-09-14 16:08
After a human finished the on-screen wizard (Ethernet already connected), the FLEX 3 (F30625AVJ1031) flipped from online-not-activated to activated: activate_time 2026-09-14 16:08:58, is_online: 1, rom_name SUB_FLEX_3-WF_S22, version_name still empty (populates on a later heartbeat). It is in Upvendo BNL Internal, which has the Baseline profile bound, so locale/timezone should now apply. Rotation and applied-profile state remain unread — needs adb (enable Wireless debugging on the unit) to confirm whether it auto-rotated once activated and whether the Baseline landed. mdm_device_info.can_control: false / support_control: true — remote control is supported but not currently available. The "needs a licence" guess recorded here was wrong: can_control follows the device's own Remote Assistance → Unattend toggle (unattended_switch) — see the solved Remote Control section below. This FLEX 3 still has it OFF.
Measured on hardware 2026-09-14
Both units read live from the console API (.../device-manage/device/getGroupDevice) and, for the D3 PRO, over adb:
FLEX 3 (F30625AVJ1031), connected by Ethernet: is_online: 1 — so Ethernet worked and the unit reached SUNMI cloud with no WiFi typed. But activate_time: 0, version_name: "", rom_name: SUB_FLEX_3-WF_S22: it is online-but-not-activated — registered to the entity and in the group, yet the Android setup/activation is not finished, which is why it shows no ROM and why its screen was still whatever the wizard displays. Online ≠ provisioned. mdm_device_info: support_control: true but can_control: false, support_pci: false.
D3 PRO (DP02261212845), on WiFi: the repoint held across days and reboots — on-device settings get system app_manager_auto_start_app = com.upvendo.pos.testing, and persist.sys.timezone = Europe/Brussels (Baseline profile, still applied). Ethernet hardware is confirmed present even though it runs on WiFi: dumpsys ethernet → "Ethernet State: enabled", interface filter eth\d, eth0 in /sys/class/net. So the D3 PRO's spec-sheet RJ45 is real, not just a datasheet claim.
⚠ Leftover to clean: com.upvendo.pos (the old package) is still installed on the D3 PRO (installer=woyou.market — the SUNMI store client — lastUpdateTime 2026-09-05 09:46, before the V3.0 repoint). It is idle (not launcher, not Default Startup) but occupying the device. Remove with adb shell pm uninstall -k com.upvendo.pos when convenient.
Ethernet vs WiFi at first boot
Both models have built-in Gigabit Ethernet, per SUNMI's own spec sheets: D3 PRO "1RJ45 LAN port", "LAN 1000M" (spec); FLEX 3 "2RJ45 (Gigabit Ethernet)", PoE module optional (spec PDF p.2; an earlier pre-launch sheet listed one port).
- Ethernet — plug in, expected to take an address by DHCP with no typing. Not yet measured on our units, and nothing documents whether the SUNMI agent pulls its Profile before the wizard finishes.
- WiFi — someone types the network name and password during the wizard. The Profile WLAN item cannot supply the first connection, because the Profile arrives over it. It can move a device onto the merchant's WiFi afterwards.
Rotation on the FLEX 3
- SUNMI's manual documents both portrait and landscape mounting; panel 1920x1080. The panel's native orientation is undocumented.
- SUNMI's per-model Profile table lists Screen Rotation for D3 PRO; the FLEX 3 cell is blank (read 2026-09-04), so rotation via Profile is not established for it.
- A reseller lists "Auto-rotate (GSensor)" for the FLEX 3; no SUNMI source confirms it.
- Nothing in our stack sets FLEX 3 orientation today.
upvendo-kiosk's portrait lock is phone-only —MainActivity.javalocks only whensmallestScreenWidthDp < 600, for dogfooding on phones — so on the FLEX 3 the app follows the device. Measure the unit before choosing a fix.
POS and kiosk are different on the device, identical at the backend
Both call the same POST /device-auth/activate (routes/api/guest.php). Everything device-side differs, so any provisioning change to "the app" is two changes:
upvendo-pos | upvendo-kiosk | |
|---|---|---|
| Activation client | src/api/services/device-services.ts → src/stores/device.ts | src/api/services/auth-services.ts → src/stores/auth.ts |
| Token storage | Keychain / storage seam | plain localStorage |
| Native layer | webview + permissions bridges | Masung printer SDK, ysapi.jar board SDK, Slack notifier |
| Auto-start | none of its own | own BOOT_COMPLETED receiver (StartUpBroadcastReceiver) |
| Orientation | JS lock: landscape station, portrait handheld (src/native/appShell.ts) | portrait lock on phones only |
| Package ids | .testing / .staging / bare | .dev / .staging / bare |
On SUNMI hardware two kiosk behaviours are worth knowing. Its boot receiver overlaps Default Startup, so a FLEX 3 would have two auto-start mechanisms. And its scheduled power on/off runs through the ysapi board SDK, whose wrapper describes itself as a no-op off its own kiosk hardware, so those schedules are expected to do nothing on SUNMI (not tested); SUNMI's equivalent is the OS Configuration scheduled power on/off strategy API.
⚠ Stale: later on 2026-09-14 a dedicated upvendo_kiosk.keystore was minted and the dev kiosk published to the private store — see "Kiosk on the FLEX 3" above. Kept for history.The FLEX 3 cannot receive the kiosk through Default Startup yet. com.upvendo.kiosk is not in the SUNMI store, and it cannot be release-signed: the local keystore.properties password has been wrong since 2026-06. The POS listing covers D3_PRO only.
Automating this: the SUNMI OpenAPI
Everything above is manual console work. SUNMI exposes an OpenAPI that could move most of it into upvendo-backend. Nothing below has been built or called yet — this is a survey of what the account actually has, read from Development → My Capabilities on 2026-09-05, not a design that has been tested.
Endpoints: openapi.sunmi.com, openapi.us.sunmi.com, openapi.eu.sunmi.com (use the EU one). Requests are signed; see Signature & Verification and Generating App Public Key in the Capability Integration Guide.
To use any of it: SUNMI Partners → become a Developer → Development → Apps Integration → create a Cloud app (Cloud-to-Cloud) or Android app (End-to-End) → link the capabilities → only then can the endpoints be called.
Capabilities present on the Upvendo Benelux entity that matter here:
| Capability | What SUNMI says it does | Integration |
|---|---|---|
| Parameter Management | "OpenAPI for parameter creation, editing and other operations… create parameter templates, fill in parameters and push parameters" | Cloud + Android |
| Parameter Management SDK | "enabling apps that need to receive parameters to successfully obtain pushed parameters from the cloud" | Android |
| SUNMI Appstore | "interfaces for creating and updating apps… publish apps on their own cloud and submit them to SUNMI App Store synchronously" | Cloud |
| SUNMI Remote | "Easily manage all series of SUNMI POS terminals" | — |
| OTA Open Service | "OTA upgrade capability for device firmware or applications" | — |
Parameter Management (capability id 27) is the interesting one, and its detail page says: Prerequisite No, Integration Type Cloud, Android, Authentication required Yes, Available to "mainland China, HK, Macao, Taiwan, other countries & regions", Supported SUNMI devices All. So it is available to this account with no precondition.
Why that pair matters
Today a till needs an activation code typed on the device, taken from the back office (POST /backoffice/devices/new-activation-code; the Device record carries activation_code and location_id). The backend currently knows nothing about SUNMI — no serial numbers, no DMP.
Parameter Management is the missing link: the cloud half pushes a parameter to a device, and the SDK half lets the app read it. That is the shape of a zero-touch activation — the device could learn which merchant and location it belongs to without anyone typing anything.
The endpoints, read from SUNMI's API reference (2026-09-05)
Auth on every call: headers Sunmi-Appid, Sunmi-Nonce (6-digit), Sunmi-Timestamp (10-digit Unix), Sunmi-Sign (HMAC-SHA256). Docs show openapi.sunmi.com; use openapi.eu.sunmi.com.
Parameter push targets ONE device by serial.POST /v2/fin/params/open/device/push/save
| field | req | meaning |
|---|---|---|
msn | yes | "Specified device MSN" — a single device |
app_id, version_id | yes | app + version |
params | yes | JSON string, max 200,000 characters |
Returns push_id. Companion endpoints: Device Application Parameter List / Details / Deletion / Push Cancellation, plus nine template endpoints (create general vs official, clone, update, validate). ⚠ Template content is authored per app version, and the console lists templates under Development → Params Management → Parameter Template, keyed by published application — our app does not appear there until it declares parameter support.
The App store API reaches the private pool — and defaults to public.POST /v2/appstore/appstore/app/createApp
pond_type— "App store type: 0: Public App Store (default), 1: Private App Store"
Pass pond_type: 1 explicitly. This is the same default-to-public trap as the console UI. Other required fields mirror the manual flow exactly: apk_uuid (from Upload Apk API), icon_url_uuid (Upload Image API), pic_horizontal_screen_uuid[] / pic_vertical_screen_uuid[] (max 5 each), app_introduction (10–1000 chars), cf_id (category), terminals[], area[] (1 = Mainland China, 2 = HK/Macau/Taiwan, 3 = …). Also: Upload APK, Upgrade App version, Get App review, Audit Result Callback (requires a notify_url), Delete App.
"SUNMI Remote" is far more than remote assist. Under Remote management:
- Device Group APIs — Create Group, Move devices to group, Get Group info by SN list, Get Group list, Rename, Delete, Get device information
- Device Transfer APIs — Bound to sub account, Unbound from sub account, Transfer to friend, Get Friend list, Device Force Reclaim
- Command APIs — Issue Reboot / Power Off / Lock Device / Unlock / Clear Lock Screen Password, Send Notifications
- Device APIs — real-time and batch status, app list on device, coordinates, traffic
OS Configuration adds APN, scheduled power on/off, APP permission and setting items strategies (create/update/get).
Spike, 2026-09-05: the OpenAPI round trip works
Verified by real signed calls, not from docs.
A Cloud app "Upvendo Provisioning" exists on the Upvendo Benelux entity (AppID edcd82ad28de43f9adc3584b41213906) with Parameter Management linked. Creating it was self-serve and free — the form asks only for a name, type (Android/Cloud) and an IP Whitelist; there is no payment step. ⚠ The whitelist is real: the calling host's egress IP must be listed, so upvendo-backend's DigitalOcean egress addresses have to be added before this works from production.
Signing, confirmed working:
Sunmi-Sign = HMAC-SHA256(RequestBody + Sunmi-Appid + Sunmi-Timestamp + Sunmi-Nonce, AppKey)lowercase hex; Sunmi-Nonce is 6 random digits, Sunmi-Timestamp a 10-digit Unix time, Content-Type: application/json, always POST.
⚠ Use openapi.sunmi.com — the regional endpoints do NOT work for this capability. Measured with the identical signed request:
| endpoint | response |
|---|---|
openapi.eu.sunmi.com | {"msg":"can not get ability","code":30001} |
openapi.us.sunmi.com | {"msg":"can not get ability","code":30001} |
openapi.sunmi.com | {"code":1,"msg":"success"} |
This contradicts the natural assumption that an EU business uses the EU host, and it costs an hour if you assume otherwise — the signature is fine, so the error looks like a permissions problem.
POST /v2/fin/params/open/template/summaryList with body {} returned 8 apps — the same list the console shows under Development → Params Management, none of them ours.
Our app has no parameter template, and that is the real blocker. With a control to prove the call works:
POST /v2/fin/params/open/template/list | result |
|---|---|
{"app_id":51767} (Upvendo POS TEST) | {"total":0,"list":[]} |
{"app_id":47270} (Adumo, control) | {"total":11,...} |
So push/save cannot be exercised yet: it requires an app_id and version_id that carry a template. A template has to be created for com.upvendo.pos.testing first (app_id 51767), and the register must integrate the Parameter Management SDK before it can read anything back.
Pricing signal: in the capability picker, SUNMI Remote is tagged "Trial"; Parameter Management and SUNMI Appstore are not. Consistent with the 2022 support page putting Remote Control on metered minutes.
What the POS app needs, and what must NOT be pushed
The register side is small, because the existing activation path can be reused unchanged. upvendo-pos already has the exact bridge pattern needed — @CapacitorPlugin(name="…") in android/app/src/main/java/com/upvendo/pos/ registered from MainActivity, with a matching registerPlugin<T>('…') wrapper in src/native/ (see BackofficeWebViewPlugin.java + src/native/backofficeWebView.ts, or permissionsBridge.ts). Adding a SUNMI parameter bridge is the same shape:
- Native — add the SUNMI Parameter Management SDK and expose a
getParams()call. - JS —
src/native/sunmiParams.ts, mirroring the existing wrappers. - Wire — before the activation screen renders, read the pushed value and hand it to the store's existing
activate(code)insrc/stores/device.ts. No new backend endpoint: that method already doesPOST /device-auth/activateand stores the returned token.
⚠ Do not push the activation code itself. In AuthDeviceService::activate() the code is only used to look up the device — it is not consumed, cleared or expired on success (the unset() there strips it from the save payload, nothing more). An activation code is therefore a durable, reusable credential that keeps working until someone rotates it via new-activation-code. Putting one into a channel whose device-side read permissions SUNMI does not document is the wrong trade.
Push a short-lived, single-use bootstrap token instead, which the register exchanges for a real activation. That needs a small backend addition, and it is the piece to design before writing any SDK code.
Still genuinely open
- Secret-safety. SUNMI documents no statement on whether pushed parameters are suitable for credentials, nor which apps on the device can read them. Treat a pushed value as not a secret until proven: prefer pushing a short-lived, single-use bootstrap token that the register exchanges with our backend for real credentials, rather than the credential itself.
- Pricing, as read from the console (2026-09-22). We are on Premium Edition (id 94), valid
2026-09-04 → 2026-10-05 15:59 UTC,has_renew: 0, total spend USD $0.01 — i.e. a trial. Under it, licences and remote-control minutes are both unlimited (-1) and unused (0). The Free Edition's "30 minutes each month" (2022 SUNMI NA support page) is the fallback we drop to on 2026-10-05 if nobody renews. Any commercial enquiry should still get a written quote covering capability activation, per-SN licences, Remote minutes, OTA operations and API-call fees (Thecan_controlquestion is answered: the on-device Unattend toggle — see Remote support.) - No public rate limits, and no sandbox for these capabilities. (An old cloud-printer API documented
uat.openapi.sunmi.com, but that is scoped to the printer protocol — do not assume it works for these.) - Device ownership (first bind of an SN to the account) still looks manual — the portal's Device Bound / batch-SN import, or emailing SUNMI support. The Group and Transfer APIs above operate on devices the account already owns.
Remote support: what actually works on our GMS devices (measured 2026-09-15)
Measured on both D3_PRO_E_GL units (ROM 4.0.79, com.sunmi.remotecontrol.pro 4.3.5, sunmi.remotemanager 5.1.2, GMS present — ro.com.google.gmsversion=13_202401, Play Store installed). Each device's ⋯ menu in DMP offers: Device Detail, Profile, Remote Control, Clear Password, Lock Device, Remote ADB, Unlock Device, Send Message, Get Log, Send Files, Power Off, Reboot, Device parameter. Offered is not the same as available:
| Action | Result on our fleet | Why |
|---|---|---|
| Remote Control (screen mirroring) | SOLVED 2026-09-22 — works on DP02261212845 | The gate was the on-device Remote Assistance → Unattend toggle (unattended_switch), which sets can_control. Not GMS, not a licence — two wrong causes were recorded first; see below. |
| Remote ADB | Task runs, then fails: "System version is too low" — on both devices | ROM 4.0.79 is below the feature's floor. Presumably fixable by an OS upgrade wave (Device → Device Upgrade), not yet attempted |
| Reboot | Works | Gated behind a "Risk Operation Confirmation" ("will disrupt merchants' regular business operations") |
| Send Message | Sent; delivery unconfirmed | No task row appeared under Task Mgmt afterwards. Treat as unproven |
✅ SOLVED 2026-09-22: Remote Control needs the on-device "Unattend" toggle
Remote Control is live — RM drove DP02261212845 on 2026-09-22. No purchase, no licence binding, no OS upgrade. Two server fields are involved and they are not the same thing:
can_control— whether the console's Remote Control action is clickable. Latches true.unattended_switch— whether the merchant must tap Accept per session. Follows the on-deviceRemote Assistance → Unattendtoggle, both ways.
Measured on DP02261212845, with DP02261811229 as an untouched control:
start unattended 0 can_control false (greyed) | control: 0 / false
toggle ON unattended 1 can_control true (clickable) | control: 0 / false
toggle OFF unattended 0 can_control TRUE <-- latched | control: 0 / falseTurning unattended back off does not take Remote Control away. An earlier draft of this section said unattended_switch gates can_control — it does not. That was the third wrong causal story about this one greyed button in a week; it lasted about an hour, because this time the control device was already in place to contradict it.
What latches can_control: throwing the Unattend toggle once. The "app has been launched at least once" hypothesis is DISPROVEN — on DP02261811229, Get Log opened a live file browser of the device's storage, which only com.sunmi.remotecontrol.pro can serve, so the app is running and answering MDM requests; can_control was still false immediately afterwards. The only action ever performed on the working device and never on the control device is turning Unattend ON.
So there is no remote or central way to do the first enable. One physical (or adb) touch per device, before it leaves your hands. Turning it back off afterwards is fine — can_control has already latched.
Not yet tested: whether a session works in attended mode (unattended OFF, can_control true, merchant taps Accept). Two minutes on DP02261212845; if it does, staffed tills can run with the prompt on.
Guard: tools/sunmi-unattended.sh {status,on,off} [-s <serial>] in the monorepo root. It opens the app over adb (which works through SUNMI Kiosk Mode — am start is not blocked by mLockTaskModeState=LOCKED_SUNMI), taps the toggle and re-reads it, exiting 1 if it did not land in the requested state.
No central switch — checked, not assumed. (a) The full DMP Profile catalogue is Wallpaper / Boot Animation / Launcher · Kiosk Mode / Default Startup · WLAN / Access Control / mDNS · Notification / App Lock / Settings / Super Permissions / Component Control · Printer / Scanner / Subscreen · Time Zone / Languages / Keyboard / Security / Display / Sound and Vibration / NFC / Battery / Physical Key — nothing remote-assistance related. (b) The commands enabled on this account (.../mdm/partner/account/enableCommand) are download, upload, clearKeyGuard, lockDevice, openRemoteADB, timingSwitch, launcher, apn, appAuthControl, remoteControl, remoteUnlockDevice, shutdown, reboot, uninstall, sendMessage, settingManage, timingSleep, securityConfig — no unattended command and no remoteScript. (c) A full settings list global/secure/system diff across an on→off toggle showed zero differences, so there is no settings put shortcut. (d) SUNMI advertises ~82 DMP open APIs and the family is real (/v2/mdm/open/open/device/applyControl), but the public Sunmi Remote docs 404 — ask SUNMI whether an unattended endpoint exists, alongside the Remote ADB question.
The remaining gap is field units. Anything we hold before it ships costs seconds (tools/sunmi-unattended.sh on) — put it in the bench checklist, it is the one step that cannot be done later from a desk. A unit already at a merchant has no remote path today unless Remote ADB starts working, which would let the script run remotely. That raises the value of the OS upgrade wave: it is the difference between a site visit and a script.
Bonus capability found while testing: Get Log is not a log dump — it opens a live, browsable file tree of the device (sdcard + data/public), and Send Files pushes files back. Both work on an online device with no unattended and no Remote Control. That is a real support tool we were not using: pull a log off a merchant till without a visit. (It also suggests, untested and not recommended on a live till, that the privileged agent might reach /data/data/com.sunmi.remotecontrol.pro/shared_prefs/ — a possible remote-enable hack to try on a bench unit, never on a merchant's.)
Two traps. (1) mainScreenUnattendedSwitch is a custom android.view.View, not a Switch, so uiautomator reports checked="false" even when the toggle is visibly orange — read the pixel (ON ≈ rgb(255,96,0), OFF ≈ rgb(164,166,170)). (2) The toggle reverts on a weak connection.
Security. Unattended means any operator on our DMP account can open that screen at any time with no prompt on the device. Right for an unmanned kiosk; a real exposure on a staffed till.
Two wrong causes were recorded before this — kept, because the pattern is the finding
For one unchanged observation — the button is grey — two explanations were adopted and written up under headings that say "measured".
Wrong cause #1: "GMS certification removes screen mirroring." From a SUNMI marketing blog. Disproved on our own GMS D3 (DP02261212845, ROM 4.0.79, gmsversion=13_202401):
codePath = /system_ext/priv-app/SunmiRemoteControl_v4.3.5_gms_release
privateFlags = [ ... PRIVILEGED SYSTEM_EXT ... ]
android.permission.CAPTURE_VIDEO_OUTPUT
android.permission.CAPTURE_SECURE_VIDEO_OUTPUT: granted=true
android.permission.BIND_DEVICE_ADMIN: granted=trueWrong cause #2: "we own zero licences — License Management reads Unbind: 0/0." From a field label, made plausible by a remembered $10/unit/yr price. POST /public/v1/mdm/partner/account/dashboard: package_name: "Premium Edition" (id 94) · 2026-09-04 → 2026-10-05 15:59 UTC · has_renew: 0 · total_license: -1 (unlimited) / used_license: 0 · total_time_len: -1 / used_rmtctrl: 0 · commands.single includes remoteControl · spend USD $0.01. Unbind is total_unbind/used_unbind — the unbind quota, not a licence count.
Never record a vendor's story, or a field name, as a measurement. Both stories would have been killed in five minutes by the thing that finally worked: open the app on the device and look.
Licence status, separately: the Premium trial lapses 2026-10-05 and drops us to Free Edition (30 remote-control minutes/month). Now that Remote Control works, exercise it before then and decide whether 30 min/month is enough.
Still true from the original measurement: Remote ADB fails "System version is too low" on ROM 4.0.79 (unattended does not fix it), and the reboot caveat below stands.
⚠ Superseded: the account was switched to Self-OTA on 2026-09-22 and
DP02261811229is on 4.0.102 (2026-09-28); Remote ADB there is untested. Current state: runbook.
The OS upgrade that might fix Remote ADB is not available to us (attempted 2026-09-22). The DMP OS List is a catalogue, not an availability statement: it lists 4.0.102 (code 1318, built 2026-09-22) with Supported Models: D3_PRO_E_GL — our D3s, on 4.0.79_1011 — and its notes include "Supported button configuration" (likely the Physical Keys support this ROM ignores) while 4.0.87 carries "Fixed USB external device issues". But: (a) the console cannot push it — Upgrade Mode is Public Follow Mode ("Manufacturer-led… End users choose whether to upgrade"), and Upgrade Management lists only the 10 un-activated FLEX 3s, neither D3; and (b) the device is not offered it — com.sunmi.ota after an explicit Check for updates reports "SUNMI OS 4.0.79 / 1011 — Already the latest version." Options: wait for the rollout, ask SUNMI to release it to our SNs, or switch the account to Self-OTA Mode — an account-wide change affecting every present and future device, which is a decision, not a console click. See the runbook.
⚠ Reboot is not a free action. Used once to release a stuck print-bridge lease: it worked — the lease moved to the on-site register within the TTL and printing recovered — but the rebooted device did not come back on its own. Forty-five minutes later it was still offline both in Upvendo (status: Offline) and in DMP (grey dot), so it needed someone physically present. Remote Control (on units whose can_control has latched) shows the screen, but there is no remote recovery path for a device that fails to rejoin the network after a reboot. Prefer asking the person at the site; reboot only when nobody is there and you can accept the device staying down.
The SUNMI ADB tool (Mac/Linux/Win, downloadable from the Remote ADB intro modal) is a fork of adb carrying an extra sunmi_connect <sn> verb that tunnels through SUNMI's server node; the Mac build is adb 1.0.36 (2019, x86_64 — runs under Rosetta). Stock platform-tools adb has no such verb. It is useless until the Remote ADB task itself succeeds.
Not attempted
- Google zero-touch. SUNMI is a certified Google Reseller and can bind order SNs to a zero-touch account, but it is unnecessary here: SUNMI's agent is already Device Owner out of the box, which is why the console has no zero-touch page. Using a third-party EMM instead would require a factory reset and would forfeit the SUNMI agent, since Android permits only one Device Owner.
- Reseller-facing provisioning runbook. (The internal console runbook now exists: runbook.) Deliberately not written yet — kiosk mode and app removal are unresolved above, and a runbook for a flow that does not work end-to-end would be worse than none.