Voice Broadcast (Virtual Device): Deploy & Use
A spare Android phone with the voice broadcast APP installed and activated becomes a "virtual device": when a payment succeeds, the phone announces the received amount by voice, and with a Bluetooth receipt printer connected it prints the receipt at the same time. No cloud speaker or cloud printer hardware to buy — the cheapest way to add "hear every payment, keep every receipt" to a store.
What It Does
| Capability | Description |
|---|---|
| Voice broadcast | On payment success, the received amount and channel are announced; the voice clips are built into the APP (no TTS or mobile data needed) |
| Receipt printing | With a Bluetooth receipt printer connected (classic Bluetooth SPP and BLE supported), receipts print automatically on payment success; 58 / 80mm paper widths supported |
| Online status | The APP reports heartbeats to the platform; both the admin panel and the merchant panel show whether a device is online |
| Per-app switches | Each payment app can independently enable / disable voice broadcast and receipt printing, and choose the receipt layout |
| Test broadcast / test print | Device lists on both panels provide test entries — verify the broadcast and printing chain without a real payment |
Two design boundaries to avoid misreading as faults:
- Orders from the online API and EasyPay sources are muted by default (no broadcast, no print): the feature targets manual cashier scenarios; API orders have no in-store announcement need.
- Broadcast payloads are encrypted with a device-level key, and the access account can only subscribe (never publish) on the message broker — forged payment announcements are blocked (built in, nothing to configure).
Prerequisite: Set Up the Message Pipeline
Broadcast messages are pushed from the platform to the phone over a persistent MQTT over WebSocket (wss) connection. This pipeline must work first — otherwise an activated device receives nothing. Nine out of ten "activated but no broadcast" issues sit in this step. Pick one path per your deployment style:
| Deployment style | Path |
|---|---|
| Ops panel / one-click install | Path 1 (recommended) — a few clicks in the panel |
| Source deployment, self-hosted broker | Path 2 — manual Artemis configuration |
Path 1: Ops Panel Deployment (Recommended)
Panel users do this under "Gateway Management → MQTT Access" (full page guide: Gateway Management - MQTT Access):
- One-click apply: on the "Artemis MQTT Access" status card, click "One-Click Apply" — the panel configures the message broker for access and creates the access account (
app-device); - Bind the access domain: enter a bare domain (e.g.
mqtt.example.com); the phone's actual access address is thenwss://<that-domain>— no port or path to append. The HTTPS certificate is configured on the outer layer (outer gateway / load balancer / CDN); - Note down the access credentials: the "Access Credentials" card shows the password of the
app-deviceaccount (reveal / copy) — needed in the next step; - Fill it in on the platform side: on the admin panel "System Management → System Configuration → Platform Configuration → MQTT Access" tab, enter the access URL (
wss://mqtt.example.com), the access account and password, then save.
The access domain should not sit behind most CDNs
The phone keeps a persistent long connection to the message broker; most CDNs handle long connections poorly (short timeouts or outright disconnects), causing frequent device drops and missed broadcasts. Point the access domain directly at the origin IP, or terminate HTTPS on an outer gateway / load balancer that supports WebSocket long connections.
When platform configuration takes effect
The access URL and account are delivered to the phone at device activation; changes made afterwards do not propagate to already-activated devices — re-activate once in the APP to pick up the new configuration.
Path 2: Manual Deployment (Self-Hosted Artemis)
Source deployments already depend on the Artemis message broker; add the following configuration to it manually. The snippets below match what the ops panel injects automatically — paste them as-is.
Topic contract (Artemis 2.33+ / ARTEMIS-4532): devices subscribe to a slash topic, while the platform publishes to a dot-separated multi-segment core address; the broker maps both to the same address:
| Direction | Value | Notes |
|---|---|---|
| Device subscription (delivered at activation) | daxpay/device/audio/{deviceNo} | The APP subscribes as-is; / is the MQTT hierarchy separator |
| Broker core address | daxpay.device.audio.{deviceNo} | The slash topic maps to this multi-segment address; ACL / address-setting match against it |
| Platform publish (JMS Topic) | daxpay.device.audio.{deviceNo} | Same address as the subscription queue; routed directly by the broker |
Do not subscribe to the dot topic or publish to the escaped literal
Artemis 2.33+ follows the official MQTT semantics: / is the hierarchy separator, while . is a literal character escaped as \.. If a device subscribes to the dot topic daxpay.device.audio.{deviceNo}, it lands on the single-segment address daxpay\.device\.audio\.{deviceNo}, which does not match security-setting match="daxpay.device.#"; the subscription is rejected (SUBACK 0x80, usually with no clear reason in the broker log). If the platform publishes to the escaped literal daxpay\.device\.audio\.{deviceNo}, it does not share the address of the queue created by the slash subscription, so messages are silently dropped. The topic in the device credential is fixed at activation time; existing devices must re-activate once after any topic-contract change.
① Open the MQTT over WebSocket port: add port 61614 inside the <acceptors> section of broker.xml:
<acceptor name="mqtt-ws">tcp://0.0.0.0:61614?protocols=MQTT</acceptor>② Device message address policy: add inside <address-settings> — device topics are cleaned up automatically and messages expire quickly (no pile-up for long-offline devices):
<address-setting match="daxpay.device.#">
<auto-delete-addresses>true</auto-delete-addresses>
<auto-delete-queues>true</auto-delete-queues>
<expiry-delay>600000</expiry-delay>
</address-setting>③ Device address permission matrix: add inside <security-settings> — the platform (amq role) may publish, while the access account (app-device role) can only subscribe, never publish, preventing forged pushes:
<security-setting match="daxpay.device.#">
<permission type="send" roles="amq"/>
<permission type="createAddress" roles="amq"/>
<permission type="consume" roles="app-device"/>
<permission type="createNonDurableQueue" roles="app-device"/>
<permission type="createDurableQueue" roles="app-device"/>
<permission type="createAddress" roles="app-device"/>
</security-setting>Permissions must be exhaustive
security-setting matches the most specific address and does not inherit from the default block — this block must list every permission both amq and app-device need. A missing send / createAddress for amq stops the platform from publishing; a missing createAddress for app-device rejects a phone that subscribes before the platform does — neither failure shows a clear reason in the logs.
Artemis 2.33+ uniformly uses slash topics
The panel pins apache/artemis:2.57.0-alpine; other 2.3x/2.5x versions such as 2.44 follow the same slash-topic contract. The old advice "the subscription queue does not land on the daxpay.device.# match, so widen the consume/create permissions of the catch-all # block to app-device" is no longer recommended: it only bypasses the ACL, does not fix the dot-topic address mapping, and opens subscription access to every address for the device account, violating least privilege. If a subscription keeps failing, first verify that the device uses the slash topic and that the backend has been upgraded and the device re-activated — do not widen #.
Multiple roles
When a roles attribute lists multiple roles they must be comma-separated (amq,app-device); space-separated lists silently fail on older versions. The precise matrix above needs no multiple roles; this note is only for checking existing configuration.
④ Narrow the core port: remove MQTT from the protocols parameter of the port-61616 acceptor (if present) so 61614 is the only MQTT entry; acceptors without a protocols parameter need no change.
⑤ Create the access account (run inside the Artemis instance directory; <admin-user> is the broker administrator):
cd /var/lib/artemis-instance
./bin/artemis user add \
--user '<admin-user>' --password '<admin-password>' \
--user-command-user 'app-device' --user-command-password '<custom-access-password>' \
--role 'app-device'⑥ Restart Artemis for the configuration to take effect.
⑦ Expose the wss entry: in production, terminate HTTPS with nginx (or similar) and proxy to 61614 — mind the WebSocket upgrade headers and generous timeouts:
server {
listen 443 ssl;
server_name mqtt.example.com;
# ... certificate ...
location / {
proxy_pass http://127.0.0.1:61614;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Idle timeout must exceed twice the client heartbeat interval; too short drops the connection and loses messages
proxy_read_timeout 180s;
proxy_send_timeout 180s;
proxy_buffering off;
}
}Finally make sure the main application can reach this broker (the main application already talks to Artemis for messaging; same-host deployments need nothing extra), then fill in the wss:// access URL and the app-device account credentials on the admin panel "Platform Configuration → MQTT Access". For internal-network debugging a temporary ws:// (unencrypted) address also works; use wss:// in production.
Setup and Usage Flow
With the pipeline ready, four steps take you from generating a device to hearing the broadcast:
Step 1: Generate and bind the device on the admin panel
Admin panel "Virtual Devices → Device Management" (under the payment system menu):
- "Generate Device" creates a virtual device; the device number is generated by the system (starts with
VD); - Use "Bind Merchant" on the device row to assign the device to a merchant and app — a device without a bound merchant cannot be activated;
- To verify the chain, use the "Test Broadcast" / "Test Print" entries on the device row (test print lets you pick the paper width).
TIP
Unbind or delete a merchant's devices before deleting the merchant — deleting a merchant does not clean up devices automatically.
Step 2: Merchant views the device and activation QR code
The merchant panel "Merchant Center → Basic Management → Payment Device" page lists the bound devices; each row offers the activation QR code entry. Merchants can also adjust the owning app, bind a store, or enable / disable the device here.
Step 3: Activate by scanning with the phone APP
- Install the voice broadcast APP (Android; the server address is built into the package at compile time — see APP usage notes);
- Open the APP and scan the activation QR code shown on the merchant panel — the code content is
daxpay:activate:{device number}, carrying only the device number, so scanning completes the activation. When scanning is not possible, manually enter the device number on the activation page; - On success the APP connects to the message pipeline and listens in the background; errors during activation are shown on screen.
Step 4: Verify broadcast and printing
Make a small payment (or use the row's "Test Broadcast") — the phone should announce the amount immediately on success; with a printer connected, the receipt prints too. The online status in the device lists updates accordingly.
Per-App Broadcast and Print Control
Broadcast and receipt printing are controlled per payment app, in the "App Management" areas of both panels (the device section of admin "Merchants & Apps → Application Configuration" and merchant "Merchants & Apps → App Management"):
| Setting | Description |
|---|---|
| Voice broadcast | Switch — off means payments of this app are not announced |
| Receipt printing | Switch — off means payments of this app do not print |
| Receipt layout | Layout template choice; one built-in "standard receipt" is currently available |
A device follows the switches of its owning app at payment time: broadcast and printing are independent — announce-only or print-only both work.
APP Usage Notes
- Get & install: the APP is an Android package (APK). The open-source edition can be downloaded directly from daxpay-virtual-device-release.apk (currently v0.1.0, Android 8.0+), or from the download section of the website product page and the system demo page; the commercial-edition package is distributed through the commercial channel. The two packages can be installed side by side;
- Keep-alive: to avoid being killed by the phone, follow the in-app guide to disable battery optimization for the APP and allow self-start / background pop-ups (paths differ across vendors). If killed, reopening the APP restores listening;
- Connect a printer: pair the Bluetooth receipt printer in the APP (SPP and BLE supported) and keep the APP's paper width setting (58 / 80mm) consistent with the actual paper — a mismatch breaks the receipt layout;
- Installing a package for a different server environment forces re-activation: the server address is built into the APP package at compile time; after switching to a package pointing at another environment, the APP asks for re-activation to prevent mixing credentials across environments — just scan again when prompted, this is expected;
- More capabilities: a desktop amount widget, on-device payment history, and do-not-disturb hours are available in the APP.
FAQ
| Symptom | Explanation & fix |
|---|---|
| Activated but no broadcast received | Nine times out of ten the MQTT pipeline is broken (wrong access URL / certificate / 61614 not open on the broker / platform MQTT config empty) — follow Voice Broadcast Not Received step by step |
| Payment received but no print | Check in order: the app's "Receipt Printing" switch, whether the printer is connected in the APP, whether the APP paper width matches the actual paper, and paper status of the printer |
| APP asks to re-activate after installing a package for a different server environment | Expected — prevents mixing credentials across environments; scan again to re-activate |
| Orders from the online API / EasyPay are not announced | Muted by design; the feature targets manual cashier scenarios |
| Receipt time is Beijing time | Outward-facing receipts always use UTC+8, independent of the timezone setting shown in the platform back office |
For the full pipeline troubleshooting guide see Voice Broadcast Not Received.
Related Links
- Gateway Management - MQTT Access: panel-side message pipeline configuration
- Payment Platform Configuration - MQTT Access: platform-side access URL and account configuration
- Voice Broadcast Not Received: full pipeline troubleshooting steps
- Mobile Overview: how the mobile apps differ