How We Halved Mobile Hardware Integration Time with a Modular Device SDK
The article details how the Youzan Retail mobile team re‑architected their hardware SDK—splitting devices into modular AAR packages, introducing layered OEM/Core/Base/Library components, unifying printer protocols with JsCore, and adding IoT monitoring—reducing new device onboarding from days to hours while cutting online issues by up to 55%.
Background
The retail app integrates more than 20 categories of smart hardware (printers, scales, POS terminals, facial‑recognition cameras, NFC/RFID readers, etc.). The original monolithic SDK caused high development cost, frequent releases, and unstable device interactions due to unclear device classification, diverse communication protocols, and duplicated platform adaptations.
Hardware Matrix
All‑in‑One Terminal : Cash register used for order entry.
Printers : Receipt printing (e.g., 365, Yimei Cloud, Jiabo, Supreet, Yilianyun).
Secondary Screens : Real‑time cart, member and payment display (e.g., Sunmi T2, T1, D2).
Customer‑Facing Displays : Low‑cost external screens for data projection.
Facial Recognition : Fast member identification (e.g., Frog Pro, Dragonfly).
NFC & RFID : Card payment and RFID‑tagged item support.
Electronic Scales : Fresh produce weighing (e.g., Kaishi, Dahua, Oulu, S2).
POS Terminals : Card‑payment terminals with swipe capability (e.g., WANGPOS, SUNMI P series).
SDK Architecture Refactoring
The SDK was split into four independent layers to improve modularity and scalability.
OEM Layer
Provides manager APIs such as PrinterManager, PosManager, and WeightManager. Each device type is packaged as an independent AAR, allowing business modules to depend only on the required AAR.
Core Layer
Implements common capabilities: device model abstraction, connection handling, caching, heartbeat monitoring, exception handling, and thread management. Core contracts include:
interface IPrinter : IDevice {
fun getPagerType(): PagerType
fun getProtocol(): Protocol
fun print(content: ByteArray): PrinterResponse
fun jsDeviceName(): String
fun isSupportJSPrinter(): Boolean
}
interface IPos : IDevice {
fun payByPos(entity: PhoinexPosPayResult): Observable<PhoinexPosPayResult>
fun revoke(orderNo: String, voucherNo: String): Observable<Any>
fun refund(orderNo: String): Observable<Any>
}
interface IWeight : IDevice {
fun doTare(): Pair<Boolean, String>
fun doZero(): Pair<Boolean, String>
}Base Layer
Provides foundational utilities such as network request handling and logging.
Library Layer
Encapsulates third‑party vendor SDKs (e.g., woyou.aidlservice for Sunmi printers, sprtprintersdk for Supreet, paymentService for Sunmi P1, cloudpossdk / wangpossdk for WANGPOS) to avoid duplicate dependencies across device modules.
Implementation Details
Printer Integration
Each printer implementation extends AbsPrinter (which implements IPrinter) and may also implement IMoneyBox for cash‑drawer control. The manager delegates shared services such as caching and threading to DeviceCoreManager.
POS Integration
Early POS code directly handled the 8583 protocol, causing instability. The refactor switched to vendor‑provided SDKs while preserving a compatibility layer for legacy code. PosManager exposes payment APIs defined by IPos.
Scale Integration
Scales implement IWeight, providing tare and zero functions. WeightManager offers CRUD operations and broadcasts weight updates via a CallableData (LiveData‑like) mechanism.
Heartbeat & State Monitoring
A background thread polls device states every two seconds and notifies registered listeners only on state change.
fun registerCheckState(listener: IDeviceStateListener) {
if (!deviceStateListeners.contains(listener)) {
deviceStateListeners.add(listener)
}
checkHeart()
}Cache Management
Device information is persisted to local files so that reconnections (e.g., after app upgrade) can restore previous state quickly.
class DeviceCacheManager {
fun addDevice(deviceInfo: DeviceInfo?) { /* add to memory and flush to file */ }
fun removeDevice(deviceInfo: DeviceInfo?) { /* remove from memory and flush */ }
fun getCacheDevices(tag: String): List<IDevice>? { /* load from file if needed */ }
}Unified Printer Protocol via JsCore
Printing content is described with an HTML‑like template. JsCore parses the template, replaces {{key}} placeholders with runtime data, and generates the appropriate ESC/POS or vendor‑specific byte stream. The template can be updated remotely from the backend, allowing instant bug fixes without redeploying the app. Example template:
<html><head></head>
<body>
<p style="font-size:24px;text-align:center;">{{shopName}}</p>
<p>订单号:{{orderNo}}</p>
<qrcode>{{invoiceUrl}}</qrcode>
</body>
</html>Secondary‑Screen Plugin Architecture
The secondary screen displays cart, member and payment information in real time. Plugins are defined by a class name; the framework reflects the class, inflates its layout, and injects data via SubPlugin callbacks. Core data structures:
@Keep
public class SubEntity {
@SerializedName("action") public int action;
@SerializedName("title") public String title;
@SerializedName("jsonData") public String jsonData;
@SerializedName("leftPlugin") public String leftPlugin;
@SerializedName("rightPlugin") public String rightPlugin;
} public abstract class SubPlugin {
private Context context;
public SubPlugin(Context ctx) { context = ctx; }
public abstract int getLayout();
public abstract void createView(View view, Bundle bundle);
public abstract void updateView(Bundle bundle);
public abstract View getView();
@Nullable
public abstract View getView(LayoutInflater inflater, @Nullable ViewGroup container);
}Plugin instantiation via reflection:
public static SubPlugin getPlugin(Context ctx, String className) {
if (TextUtils.isEmpty(className)) return null;
try {
return (SubPlugin) Class.forName(className)
.getConstructor(Context.class).newInstance(ctx);
} catch (Exception e) { e.printStackTrace(); }
return null;
}Layout inflation and view injection:
if (leftPlugin != null) {
View left = LayoutInflater.from(mContext).inflate(leftPlugin.getLayout(), leftView, false);
leftPlugin.createView(left, bundle);
leftView.removeAllViews();
leftView.addView(leftPlugin.getView());
}IoT Monitoring
All external devices report status to a cloud backend, enabling real‑time health checks, remote unbinding, and centralized diagnostics. The SDK pushes state changes to the IoT service; the backend can issue commands to reset or remove devices, dramatically reducing troubleshooting time.
Impact Metrics
New hardware onboarding time reduced by ~50% (from 1‑2 days to a few hours).
Online hardware issue tickets dropped 55% for developers and 33% for support staff.
Fewer SDK releases and more stable device interactions.
Future Directions
Expand the IoT platform for company‑wide hardware solutions.
Open hardware integration APIs for third‑party devices.
Develop a self‑diagnosis assistant to let merchants troubleshoot locally.
Introduce finer‑grained device classification for selective dependency injection.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Youzan Coder
Official Youzan tech channel, delivering technical insights and occasional daily updates from the Youzan tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
