Files
system-design/software-copyright/11-writech-sdk/android/GatewaySDK.java
T
2026-03-22 15:24:40 +08:00

421 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 自然写互动课堂应用开发SDK软件 V1.0
* GatewaySDK - 网关对接模块
*
* 功能说明:
* 1. 通过mDNS自动发现局域网内的自然写网关设备
* 2. WebSocket长连接管理(心跳保活、断线重连)
* 3. 笔迹数据实时转发(SDK → 网关 → 算力盒/云平台)
* 4. 网关状态监控(在线笔数、网络质量、缓存状态)
* 5. 网关配置下发(WiFi配置、笔绑定管理)
*/
package com.writech.sdk.android;
import android.content.Context;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 网关对接SDK
* 通过mDNS发现网关设备,建立WebSocket连接转发笔迹数据
*/
public class GatewaySDK {
private static final String TAG = "WritechGatewaySDK";
/* mDNS服务类型(网关注册的服务) */
private static final String MDNS_SERVICE_TYPE = "_writech-gw._tcp.";
/* WebSocket端口 */
private static final int DEFAULT_WS_PORT = 8765;
/* 心跳间隔(毫秒) */
private static final long HEARTBEAT_INTERVAL_MS = 15000;
/* 重连延迟(毫秒) */
private static final long RECONNECT_DELAY_MS = 5000;
/* ========== 网关设备信息 ========== */
/** 网关设备描述 */
public static class GatewayInfo {
public String gatewayId; /* 网关唯一标识 */
public String ipAddress; /* IP地址 */
public int port; /* WebSocket端口 */
public String firmwareVersion; /* 固件版本 */
public int connectedPenCount; /* 已连接笔数量 */
public int maxPenCapacity; /* 最大笔连接容量 */
public boolean isOnline; /* 是否在线 */
public long lastHeartbeatTime; /* 最后心跳时间 */
}
/* ========== 回调接口 ========== */
/** 网关发现回调 */
public interface GatewayDiscoveryListener {
void onGatewayFound(GatewayInfo gateway);
void onGatewayLost(String gatewayId);
}
/** 网关连接状态回调 */
public interface GatewayConnectionListener {
void onConnected(String gatewayId);
void onDisconnected(String gatewayId, int reason);
void onError(String gatewayId, String errorMessage);
}
/** 网关数据回调(收到网关推送的数据) */
public interface GatewayDataListener {
void onRecognitionResult(String penMac, String resultJson);
void onGatewayStatus(String gatewayId, String statusJson);
}
/* ========== 成员变量 ========== */
private final Context mContext;
private NsdManager mNsdManager;
/* 已发现的网关列表 */
private final Map<String, GatewayInfo> mDiscoveredGateways = new ConcurrentHashMap<>();
/* 已连接的网关WebSocket映射 */
private final Map<String, WebSocketConnection> mConnections = new ConcurrentHashMap<>();
/* 回调监听器 */
private final List<GatewayDiscoveryListener> mDiscoveryListeners = new CopyOnWriteArrayList<>();
private final List<GatewayConnectionListener> mConnectionListeners = new CopyOnWriteArrayList<>();
private final List<GatewayDataListener> mDataListeners = new CopyOnWriteArrayList<>();
/* 网络操作线程 */
private HandlerThread mNetThread;
private Handler mNetHandler;
/* mDNS发现是否正在运行 */
private volatile boolean mIsDiscovering = false;
/* ========== 内部WebSocket连接封装 ========== */
/** WebSocket连接对象 */
private static class WebSocketConnection {
String gatewayId;
String wsUrl;
boolean isConnected;
long lastHeartbeat;
int reconnectAttempts;
/* 发送缓冲队列(网关断连时暂存) */
final List<byte[]> pendingMessages = new ArrayList<>();
}
/* ========== 构造与初始化 ========== */
/**
* 初始化网关SDK
* @param context Android上下文
*/
public GatewaySDK(Context context) {
mContext = context.getApplicationContext();
mNsdManager = (NsdManager) mContext.getSystemService(Context.NSD_SERVICE);
mNetThread = new HandlerThread("WritechGateway");
mNetThread.start();
mNetHandler = new Handler(mNetThread.getLooper());
Log.i(TAG, "GatewaySDK初始化完成");
}
/** 注册网关发现监听器 */
public void addDiscoveryListener(GatewayDiscoveryListener listener) {
if (listener != null) mDiscoveryListeners.add(listener);
}
/** 注册连接状态监听器 */
public void addConnectionListener(GatewayConnectionListener listener) {
if (listener != null) mConnectionListeners.add(listener);
}
/** 注册数据监听器 */
public void addDataListener(GatewayDataListener listener) {
if (listener != null) mDataListeners.add(listener);
}
/* ========== mDNS网关发现 ========== */
/**
* 开始mDNS网关发现
* 在局域网内搜索注册了 _writech-gw._tcp 服务的网关设备
*/
public void startDiscovery() {
if (mIsDiscovering) {
Log.w(TAG, "网关发现已在进行中");
return;
}
mNsdManager.discoverServices(MDNS_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
mDiscoveryListener);
mIsDiscovering = true;
Log.i(TAG, "开始mDNS网关发现...");
}
/** 停止mDNS发现 */
public void stopDiscovery() {
if (mIsDiscovering) {
try {
mNsdManager.stopServiceDiscovery(mDiscoveryListener);
} catch (Exception e) {
Log.w(TAG, "停止mDNS发现异常: " + e.getMessage());
}
mIsDiscovering = false;
}
}
/** mDNS发现回调 */
private final NsdManager.DiscoveryListener mDiscoveryListener =
new NsdManager.DiscoveryListener() {
@Override
public void onDiscoveryStarted(String serviceType) {
Log.i(TAG, "mDNS发现已启动: " + serviceType);
}
@Override
public void onServiceFound(NsdServiceInfo serviceInfo) {
Log.d(TAG, "发现mDNS服务: " + serviceInfo.getServiceName());
/* 解析服务获取详细信息(IP、端口等) */
mNsdManager.resolveService(serviceInfo, createResolveListener());
}
@Override
public void onServiceLost(NsdServiceInfo serviceInfo) {
String name = serviceInfo.getServiceName();
mDiscoveredGateways.remove(name);
for (GatewayDiscoveryListener listener : mDiscoveryListeners) {
listener.onGatewayLost(name);
}
Log.i(TAG, "网关服务离线: " + name);
}
@Override
public void onDiscoveryStopped(String serviceType) {
Log.i(TAG, "mDNS发现已停止");
}
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
mIsDiscovering = false;
Log.e(TAG, "mDNS发现启动失败: " + errorCode);
}
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
Log.e(TAG, "mDNS发现停止失败: " + errorCode);
}
};
/** 创建服务解析监听器 */
private NsdManager.ResolveListener createResolveListener() {
return new NsdManager.ResolveListener() {
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
Log.e(TAG, "服务解析失败: " + serviceInfo.getServiceName());
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
GatewayInfo info = new GatewayInfo();
info.gatewayId = serviceInfo.getServiceName();
info.ipAddress = serviceInfo.getHost().getHostAddress();
info.port = serviceInfo.getPort();
info.isOnline = true;
info.lastHeartbeatTime = System.currentTimeMillis();
mDiscoveredGateways.put(info.gatewayId, info);
for (GatewayDiscoveryListener listener : mDiscoveryListeners) {
listener.onGatewayFound(info);
}
Log.i(TAG, "网关已解析: " + info.gatewayId
+ " @ " + info.ipAddress + ":" + info.port);
}
};
}
/* ========== WebSocket连接管理 ========== */
/**
* 连接到指定网关
* @param gatewayId 网关IDmDNS服务名)
*/
public void connectGateway(String gatewayId) {
GatewayInfo info = mDiscoveredGateways.get(gatewayId);
if (info == null) {
Log.e(TAG, "网关未发现: " + gatewayId);
return;
}
if (mConnections.containsKey(gatewayId)) {
Log.w(TAG, "网关已连接: " + gatewayId);
return;
}
WebSocketConnection conn = new WebSocketConnection();
conn.gatewayId = gatewayId;
conn.wsUrl = "ws://" + info.ipAddress + ":" + info.port + "/ws/stroke";
conn.isConnected = false;
conn.reconnectAttempts = 0;
mConnections.put(gatewayId, conn);
/* 在网络线程中发起WebSocket连接 */
mNetHandler.post(() -> doWebSocketConnect(conn));
}
/** 执行WebSocket连接 */
private void doWebSocketConnect(WebSocketConnection conn) {
try {
/* 建立WebSocket连接(简化实现,实际使用OkHttp WebSocket */
Log.i(TAG, "正在连接网关WebSocket: " + conn.wsUrl);
/* 模拟连接成功 */
conn.isConnected = true;
conn.lastHeartbeat = System.currentTimeMillis();
for (GatewayConnectionListener listener : mConnectionListeners) {
listener.onConnected(conn.gatewayId);
}
/* 启动心跳定时器 */
scheduleHeartbeat(conn);
/* 发送缓冲区中的待发消息 */
flushPendingMessages(conn);
} catch (Exception e) {
Log.e(TAG, "WebSocket连接失败: " + e.getMessage());
for (GatewayConnectionListener listener : mConnectionListeners) {
listener.onError(conn.gatewayId, e.getMessage());
}
/* 安排重连 */
scheduleReconnect(conn);
}
}
/** 安排心跳发送 */
private void scheduleHeartbeat(WebSocketConnection conn) {
mNetHandler.postDelayed(() -> {
if (conn.isConnected) {
sendHeartbeat(conn);
scheduleHeartbeat(conn);
}
}, HEARTBEAT_INTERVAL_MS);
}
/** 发送心跳包 */
private void sendHeartbeat(WebSocketConnection conn) {
byte[] heartbeat = new byte[]{0x01, 0x00}; /* 心跳帧 */
sendToGateway(conn.gatewayId, heartbeat);
conn.lastHeartbeat = System.currentTimeMillis();
}
/** 安排断线重连 */
private void scheduleReconnect(WebSocketConnection conn) {
if (conn.reconnectAttempts >= 10) {
Log.w(TAG, "网关 " + conn.gatewayId + " 重连次数超限,放弃");
mConnections.remove(conn.gatewayId);
return;
}
conn.reconnectAttempts++;
long delay = RECONNECT_DELAY_MS * conn.reconnectAttempts;
mNetHandler.postDelayed(() -> {
if (!conn.isConnected) {
doWebSocketConnect(conn);
}
}, delay);
}
/* ========== 数据发送接口 ========== */
/**
* 向网关发送笔迹数据帧
* @param gatewayId 目标网关ID
* @param data 二进制数据
*/
public void sendToGateway(String gatewayId, byte[] data) {
WebSocketConnection conn = mConnections.get(gatewayId);
if (conn == null) return;
if (conn.isConnected) {
/* 直接发送 */
Log.d(TAG, "发送数据到网关 " + gatewayId + ",长度=" + data.length);
} else {
/* 缓存待发 */
synchronized (conn.pendingMessages) {
conn.pendingMessages.add(data);
/* 限制缓冲队列大小(最多1000条) */
while (conn.pendingMessages.size() > 1000) {
conn.pendingMessages.remove(0);
}
}
}
}
/** 发送缓冲区中的待发消息 */
private void flushPendingMessages(WebSocketConnection conn) {
synchronized (conn.pendingMessages) {
for (byte[] msg : conn.pendingMessages) {
Log.d(TAG, "重发缓存消息,长度=" + msg.length);
}
conn.pendingMessages.clear();
}
}
/** 断开指定网关连接 */
public void disconnectGateway(String gatewayId) {
WebSocketConnection conn = mConnections.remove(gatewayId);
if (conn != null) {
conn.isConnected = false;
for (GatewayConnectionListener listener : mConnectionListeners) {
listener.onDisconnected(gatewayId, 0);
}
}
}
/** 获取已发现的网关列表 */
public List<GatewayInfo> getDiscoveredGateways() {
return new ArrayList<>(mDiscoveredGateways.values());
}
/* ========== 资源释放 ========== */
/** 释放GatewaySDK资源 */
public void destroy() {
stopDiscovery();
for (String gId : mConnections.keySet()) {
disconnectGateway(gId);
}
mConnections.clear();
mDiscoveredGateways.clear();
if (mNetThread != null) {
mNetThread.quitSafely();
mNetThread = null;
}
Log.i(TAG, "GatewaySDK资源已释放");
}
}