software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,420 @@
/*
* 自然写互动课堂应用开发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资源已释放");
}
}