完善智能称重终端界面与电子秤接入
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<queries>
|
||||
<package android:name="com.android.hct.android_hid_scale_service" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* modbus_poller.c — RS-485 Modbus RTU 串口轮询器
|
||||
* 针对 Firefly AIO-3399C 工业终端优化
|
||||
* RS485 端口: /dev/ttysWK0 (wk2xxx SPI-UART 桥接芯片)
|
||||
*
|
||||
* 编译: aarch64-linux-android26-clang -O2 -o modbus_poller modbus_poller.c
|
||||
*
|
||||
* 用法:
|
||||
* modbus_poller /dev/ttysWK0 [baudRate] [slaveId] [funcCode] [regAddr] [regCount] [dataType] [byteOrder] [pollMs]
|
||||
*
|
||||
* 默认: /dev/ttysWK0 9600 1 4 0 2 float32 ABCD 125
|
||||
*
|
||||
* 输出 (stdout, 每行一个 JSON):
|
||||
* {"status":"connecting","message":"..."}
|
||||
* {"status":"connected","message":"..."}
|
||||
* {"status":"sample","weightKg":12.34}
|
||||
* {"status":"error","message":"..."}
|
||||
* {"status":"disconnected"}
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <termios.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <time.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static volatile int g_running = 1;
|
||||
|
||||
static void on_signal(int sig) {
|
||||
(void)sig;
|
||||
g_running = 0;
|
||||
}
|
||||
|
||||
/* ---- Modbus CRC-16 (polynomial 0xA001, init 0xFFFF) ---- */
|
||||
static uint16_t crc16(const uint8_t *data, int len) {
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (int i = 0; i < len; i++) {
|
||||
crc ^= (uint16_t)data[i];
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if (crc & 1)
|
||||
crc = (crc >> 1) ^ 0xA001;
|
||||
else
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
/* ---- 设置串口参数 ---- */
|
||||
static int setup_serial(int fd, int baud_rate) {
|
||||
struct termios t;
|
||||
memset(&t, 0, sizeof(t)); /* 先清零,防止 tcgetattr 失败时使用垃圾值 */
|
||||
|
||||
if (tcgetattr(fd, &t) < 0) {
|
||||
fprintf(stderr, "[modbus] tcgetattr warning: %s (using defaults)\n", strerror(errno));
|
||||
/* tcgetattr 失败时 t 已经是零,继续配置 */
|
||||
}
|
||||
|
||||
cfmakeraw(&t);
|
||||
t.c_cflag &= ~(CSIZE | CSTOPB | PARENB | PARODD);
|
||||
t.c_cflag |= CS8 | CLOCAL | CREAD;
|
||||
t.c_iflag = 0;
|
||||
t.c_oflag = 0;
|
||||
t.c_lflag = 0;
|
||||
t.c_cc[VMIN] = 0;
|
||||
t.c_cc[VTIME] = 0;
|
||||
|
||||
/* 设置波特率 */
|
||||
speed_t speed;
|
||||
switch (baud_rate) {
|
||||
case 1200: speed = B1200; break;
|
||||
case 2400: speed = B2400; break;
|
||||
case 4800: speed = B4800; break;
|
||||
case 9600: speed = B9600; break;
|
||||
case 19200: speed = B19200; break;
|
||||
case 38400: speed = B38400; break;
|
||||
case 57600: speed = B57600; break;
|
||||
case 115200: speed = B115200; break;
|
||||
default: speed = B9600; break;
|
||||
}
|
||||
cfsetispeed(&t, speed);
|
||||
cfsetospeed(&t, speed);
|
||||
|
||||
if (tcsetattr(fd, TCSANOW, &t) < 0) {
|
||||
fprintf(stderr, "[modbus] tcsetattr warning: %s\n", strerror(errno));
|
||||
}
|
||||
|
||||
/* 确保非阻塞模式 */
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- 发送 Modbus RTU 请求并读取响应 ---- */
|
||||
static int modbus_transaction(int fd, int slave_id, int func_code,
|
||||
int reg_addr, int reg_count,
|
||||
uint8_t *response, int resp_max) {
|
||||
uint8_t request[8];
|
||||
request[0] = (uint8_t)slave_id;
|
||||
request[1] = (uint8_t)func_code;
|
||||
request[2] = (uint8_t)(reg_addr >> 8);
|
||||
request[3] = (uint8_t)(reg_addr & 0xFF);
|
||||
request[4] = (uint8_t)(reg_count >> 8);
|
||||
request[5] = (uint8_t)(reg_count & 0xFF);
|
||||
uint16_t crc = crc16(request, 6);
|
||||
request[6] = (uint8_t)(crc & 0xFF);
|
||||
request[7] = (uint8_t)(crc >> 8);
|
||||
|
||||
/* 清空接收缓冲区 */
|
||||
tcflush(fd, TCIOFLUSH);
|
||||
|
||||
/* 发送请求(Firefly RS485 硬件自动方向控制,无需手动 RTS)*/
|
||||
int written = write(fd, request, 8);
|
||||
if (written != 8) {
|
||||
return -1;
|
||||
}
|
||||
/* 等待发送完成 */
|
||||
tcdrain(fd);
|
||||
|
||||
/* 短暂延时等待 RS485 硬件方向切换 */
|
||||
usleep(5000);
|
||||
|
||||
/* 读取响应(非阻塞 + 手动超时)*/
|
||||
int expected = 3 + 2 * reg_count + 2;
|
||||
int total_read = 0;
|
||||
struct timespec t_start, t_now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t_start);
|
||||
|
||||
while (total_read < expected && g_running) {
|
||||
int n = read(fd, response + total_read, resp_max - total_read);
|
||||
if (n > 0) {
|
||||
total_read += n;
|
||||
if (total_read >= 3) {
|
||||
int byte_count = response[2];
|
||||
int actual_len = 3 + byte_count + 2;
|
||||
if (total_read >= actual_len) break;
|
||||
}
|
||||
} else {
|
||||
usleep(2000);
|
||||
}
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &t_now);
|
||||
double elapsed = (t_now.tv_sec - t_start.tv_sec) * 1000.0 +
|
||||
(t_now.tv_nsec - t_start.tv_nsec) / 1000000.0;
|
||||
if (elapsed > 500.0) break;
|
||||
}
|
||||
|
||||
if (total_read < 5) return -2;
|
||||
if (total_read < 3) return -2;
|
||||
|
||||
/* 验证 CRC */
|
||||
uint16_t resp_crc = crc16(response, total_read - 2);
|
||||
uint16_t recv_crc = (uint16_t)response[total_read - 2] |
|
||||
((uint16_t)response[total_read - 1] << 8);
|
||||
if (resp_crc != recv_crc) return -3;
|
||||
|
||||
if (response[0] != slave_id) return -4;
|
||||
if (response[1] != func_code) return -5;
|
||||
|
||||
return total_read;
|
||||
}
|
||||
|
||||
/* ---- 从寄存器数据解析重量值 ---- */
|
||||
static double parse_weight(const uint8_t *data, int data_len,
|
||||
const char *data_type, const char *byte_order) {
|
||||
if (data_len < 2) return -1.0;
|
||||
|
||||
if (strcmp(data_type, "uint16") == 0) {
|
||||
return (double)((data[0] << 8) | data[1]);
|
||||
}
|
||||
if (strcmp(data_type, "int16") == 0) {
|
||||
int16_t v = (int16_t)((data[0] << 8) | data[1]);
|
||||
return (double)v;
|
||||
}
|
||||
if (data_len < 4) return -1.0;
|
||||
|
||||
uint8_t b[4];
|
||||
if (strcmp(byte_order, "ABCD") == 0) {
|
||||
b[0]=data[0]; b[1]=data[1]; b[2]=data[2]; b[3]=data[3];
|
||||
} else if (strcmp(byte_order, "DCBA") == 0) {
|
||||
b[0]=data[3]; b[1]=data[2]; b[2]=data[1]; b[3]=data[0];
|
||||
} else if (strcmp(byte_order, "BADC") == 0) {
|
||||
b[0]=data[1]; b[1]=data[0]; b[2]=data[3]; b[3]=data[2];
|
||||
} else if (strcmp(byte_order, "CDAB") == 0) {
|
||||
b[0]=data[2]; b[1]=data[3]; b[2]=data[0]; b[3]=data[1];
|
||||
} else {
|
||||
b[0]=data[0]; b[1]=data[1]; b[2]=data[2]; b[3]=data[3];
|
||||
}
|
||||
|
||||
if (strcmp(data_type, "float32") == 0) {
|
||||
uint32_t raw = ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) |
|
||||
((uint32_t)b[2] << 8) | (uint32_t)b[3];
|
||||
float f;
|
||||
memcpy(&f, &raw, 4);
|
||||
return (double)f;
|
||||
}
|
||||
if (strcmp(data_type, "uint32") == 0) {
|
||||
uint32_t v = ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) |
|
||||
((uint32_t)b[2] << 8) | (uint32_t)b[3];
|
||||
return (double)v;
|
||||
}
|
||||
if (strcmp(data_type, "int32") == 0) {
|
||||
int32_t v = (int32_t)(((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) |
|
||||
((uint32_t)b[2] << 8) | (uint32_t)b[3]);
|
||||
return (double)v;
|
||||
}
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
const char *port = argc > 1 ? argv[1] : "/dev/ttysWK0";
|
||||
int baud_rate = argc > 2 ? atoi(argv[2]) : 9600;
|
||||
int slave_id = argc > 3 ? atoi(argv[3]) : 1;
|
||||
int func_code = argc > 4 ? atoi(argv[4]) : 4;
|
||||
int reg_addr = argc > 5 ? atoi(argv[5]) : 0;
|
||||
int reg_count = argc > 6 ? atoi(argv[6]) : 2;
|
||||
const char *data_type = argc > 7 ? argv[7] : "float32";
|
||||
const char *byte_order = argc > 8 ? argv[8] : "ABCD";
|
||||
int poll_ms = argc > 9 ? atoi(argv[9]) : 125;
|
||||
|
||||
signal(SIGTERM, on_signal);
|
||||
signal(SIGINT, on_signal);
|
||||
|
||||
printf("{\"status\":\"connecting\",\"message\":\"Opening %s at %d baud\"}\n", port, baud_rate);
|
||||
fflush(stdout);
|
||||
|
||||
int fd = open(port, O_RDWR | O_NOCTTY | O_NONBLOCK);
|
||||
if (fd < 0) {
|
||||
printf("{\"status\":\"error\",\"message\":\"Cannot open %s: %s\"}\n", port, strerror(errno));
|
||||
fflush(stdout);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (setup_serial(fd, baud_rate) < 0) {
|
||||
printf("{\"status\":\"error\",\"message\":\"Cannot configure serial port\"}\n");
|
||||
fflush(stdout);
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
tcflush(fd, TCIOFLUSH);
|
||||
|
||||
printf("{\"status\":\"connected\",\"message\":\"%s ready, %d baud, slave %d, func %d, addr 0x%X\"}\n",
|
||||
port, baud_rate, slave_id, func_code, reg_addr);
|
||||
fflush(stdout);
|
||||
|
||||
usleep(100000); /* 100ms 稳定等待 */
|
||||
|
||||
uint8_t response[256];
|
||||
int error_count = 0;
|
||||
int poll_interval = poll_ms < 50 ? 50 : poll_ms;
|
||||
|
||||
while (g_running) {
|
||||
int resp_len = modbus_transaction(fd, slave_id, func_code,
|
||||
reg_addr, reg_count,
|
||||
response, sizeof(response));
|
||||
|
||||
if (resp_len > 0) {
|
||||
error_count = 0;
|
||||
int byte_count = response[2];
|
||||
if (byte_count >= 2 && resp_len >= byte_count + 5) {
|
||||
double weight = parse_weight(response + 3, byte_count,
|
||||
data_type, byte_order);
|
||||
if (weight >= 0) {
|
||||
printf("{\"status\":\"sample\",\"weightKg\":%.4f}\n", weight);
|
||||
} else {
|
||||
printf("{\"status\":\"error\",\"message\":\"Invalid weight data\"}\n");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error_count++;
|
||||
if (error_count <= 3 || error_count % 20 == 0) {
|
||||
const char *err;
|
||||
switch (resp_len) {
|
||||
case -1: err = "Write failed"; break;
|
||||
case -2: err = "No response (timeout)"; break;
|
||||
case -3: err = "CRC error"; break;
|
||||
case -4: err = "Wrong slave ID"; break;
|
||||
case -5: err = "Modbus exception"; break;
|
||||
default: err = "Unknown error"; break;
|
||||
}
|
||||
printf("{\"status\":\"error\",\"message\":\"%s\"}\n", err);
|
||||
}
|
||||
}
|
||||
fflush(stdout);
|
||||
usleep(poll_interval * 1000);
|
||||
}
|
||||
|
||||
tcflush(fd, TCIOFLUSH);
|
||||
close(fd);
|
||||
|
||||
printf("{\"status\":\"disconnected\"}\n");
|
||||
fflush(stdout);
|
||||
return 0;
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.Settings;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.JavascriptInterface;
|
||||
@@ -29,7 +28,10 @@ import java.nio.charset.StandardCharsets;
|
||||
public class MainActivity extends Activity {
|
||||
// Android 6 到 Android 9 写入公共目录时需要动态申请存储权限。
|
||||
private static final int STORAGE_PERMISSION_REQUEST = 1001;
|
||||
private static final int DESIGN_VIEWPORT_WIDTH = 1024;
|
||||
private static final int DESIGN_VIEWPORT_HEIGHT = 600;
|
||||
private WebView webView;
|
||||
private ScaleBroadcastBridge scaleBroadcastBridge;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -63,6 +65,7 @@ public class MainActivity extends Activity {
|
||||
settings.setUseWideViewPort(true);
|
||||
settings.setLoadWithOverviewMode(false);
|
||||
// 向前端暴露受控的文件保存和系统页面跳转能力。
|
||||
scaleBroadcastBridge = new ScaleBroadcastBridge(this, webView);
|
||||
webView.addJavascriptInterface(new AndroidBridge(), "WoerNative");
|
||||
|
||||
setContentView(webView);
|
||||
@@ -115,6 +118,21 @@ public class MainActivity extends Activity {
|
||||
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
return launchSystemActivity(intent);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public boolean startScale() {
|
||||
return scaleBroadcastBridge != null && scaleBroadcastBridge.start();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public boolean stopScale() {
|
||||
return scaleBroadcastBridge != null && scaleBroadcastBridge.stop();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public boolean scaleCommand(String command) {
|
||||
return scaleBroadcastBridge != null && scaleBroadcastBridge.sendCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean launchSystemActivity(final Intent intent) {
|
||||
@@ -167,17 +185,12 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
|
||||
private void loadApp() {
|
||||
/*
|
||||
* 终端的 WebView 会根据 density 缩小 CSS 视口。
|
||||
* 这里使用真实横屏宽度改写 viewport,使 1920×1080 等分辨率都能自适应。
|
||||
*/
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
|
||||
int landscapeWidth = Math.round(
|
||||
Math.max(metrics.widthPixels, metrics.heightPixels) / metrics.density);
|
||||
// 操作界面按 1024×600 横屏终端标定,保持控件在目标设备上的一致尺寸。
|
||||
try {
|
||||
String html = readAsset("public/index.html")
|
||||
.replace("width=device-width", "width=" + landscapeWidth)
|
||||
.replace(
|
||||
"width=device-width",
|
||||
"width=" + DESIGN_VIEWPORT_WIDTH + ", height=" + DESIGN_VIEWPORT_HEIGHT)
|
||||
.replace(", viewport-fit=cover", "");
|
||||
webView.loadDataWithBaseURL(
|
||||
"file:///android_asset/public/",
|
||||
@@ -205,6 +218,10 @@ public class MainActivity extends Activity {
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (scaleBroadcastBridge != null) {
|
||||
scaleBroadcastBridge.destroy();
|
||||
scaleBroadcastBridge = null;
|
||||
}
|
||||
if (webView != null) {
|
||||
// 主动销毁 WebView,释放页面定时器和 JavaScript 上下文。
|
||||
webView.destroy();
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
package com.woer.smartscale;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* RS-485 Modbus RTU 桥接器。
|
||||
*
|
||||
* 通过启动原生 modbus_poller 子进程读取电子秤重量,
|
||||
* 将结果以 JavaScript CustomEvent 形式派发到 WebView。
|
||||
*
|
||||
* 配置 JSON 格式(留空使用默认值):
|
||||
* {
|
||||
* "portPath": "/dev/ttysWK0",
|
||||
* "baudRate": 9600,
|
||||
* "slaveId": 1,
|
||||
* "functionCode": 4,
|
||||
* "registerAddress": 0,
|
||||
* "registerCount": 2,
|
||||
* "dataType": "float32",
|
||||
* "byteOrder": "ABCD",
|
||||
* "pollIntervalMs": 125
|
||||
* }
|
||||
*/
|
||||
public class ModbusRtuBridge {
|
||||
private static final String TAG = "ModbusRtuBridge";
|
||||
private static final String POLLER_PATH = "/data/local/tmp/modbus_poller";
|
||||
|
||||
private final Activity activity;
|
||||
private final WebView webView;
|
||||
private final AtomicReference<Process> processRef = new AtomicReference<>();
|
||||
private volatile boolean running = false;
|
||||
|
||||
public ModbusRtuBridge(Activity activity, WebView webView) {
|
||||
this.activity = activity;
|
||||
this.webView = webView;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 Modbus 轮询。
|
||||
* @param configJson 前端传入的配置 JSON 字符串
|
||||
* @return true 如果成功启动
|
||||
*/
|
||||
public boolean start(String configJson) {
|
||||
if (running) {
|
||||
Log.w(TAG, "Already running");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 解析配置
|
||||
String portPath = "/dev/ttysWK0";
|
||||
int baudRate = 9600;
|
||||
int slaveId = 1;
|
||||
int functionCode = 4;
|
||||
int registerAddress = 0;
|
||||
int registerCount = 2;
|
||||
String dataType = "float32";
|
||||
String byteOrder = "ABCD";
|
||||
int pollIntervalMs = 125;
|
||||
|
||||
if (configJson != null && !configJson.trim().isEmpty()) {
|
||||
try {
|
||||
JSONObject cfg = new JSONObject(configJson);
|
||||
portPath = cfg.optString("portPath", portPath);
|
||||
baudRate = cfg.optInt("baudRate", baudRate);
|
||||
slaveId = cfg.optInt("slaveId", slaveId);
|
||||
functionCode = cfg.optInt("functionCode", functionCode);
|
||||
registerAddress = cfg.optInt("registerAddress", registerAddress);
|
||||
registerCount = cfg.optInt("registerCount", registerCount);
|
||||
dataType = cfg.optString("dataType", dataType);
|
||||
byteOrder = cfg.optString("byteOrder", byteOrder);
|
||||
pollIntervalMs = cfg.optInt("pollIntervalMs", pollIntervalMs);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Config parse error", e);
|
||||
dispatchEvent("error", "Modbus 配置 JSON 解析失败: " + e.getMessage(), 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final String[] cmd = {
|
||||
"su", "0", POLLER_PATH,
|
||||
portPath,
|
||||
String.valueOf(baudRate),
|
||||
String.valueOf(slaveId),
|
||||
String.valueOf(functionCode),
|
||||
String.valueOf(registerAddress),
|
||||
String.valueOf(registerCount),
|
||||
dataType,
|
||||
byteOrder,
|
||||
String.valueOf(pollIntervalMs)
|
||||
};
|
||||
|
||||
Log.i(TAG, "Starting modbus_poller: " + portPath + " " + baudRate + " " +
|
||||
slaveId + " " + functionCode + " " + registerAddress + " " +
|
||||
registerCount + " " + dataType + " " + byteOrder + " " + pollIntervalMs);
|
||||
|
||||
try {
|
||||
ProcessBuilder pb = new ProcessBuilder(cmd);
|
||||
pb.redirectErrorStream(true);
|
||||
Process process = pb.start();
|
||||
processRef.set(process);
|
||||
running = true;
|
||||
|
||||
// 读取子进程输出的线程
|
||||
Thread readerThread = new Thread(() -> {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream()))) {
|
||||
String line;
|
||||
while (running && (line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) continue;
|
||||
handlePollerOutput(line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (running) {
|
||||
Log.e(TAG, "Read error", e);
|
||||
dispatchEvent("error", "串口读取异常: " + e.getMessage(), 0);
|
||||
}
|
||||
} finally {
|
||||
running = false;
|
||||
processRef.set(null);
|
||||
}
|
||||
}, "ModbusReader");
|
||||
readerThread.setDaemon(true);
|
||||
readerThread.start();
|
||||
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to start modbus_poller", e);
|
||||
dispatchEvent("error", "无法启动串口程序: " + e.getMessage(), 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 Modbus 轮询。
|
||||
*/
|
||||
public boolean stop() {
|
||||
running = false;
|
||||
Process process = processRef.getAndSet(null);
|
||||
if (process != null) {
|
||||
process.destroy();
|
||||
Log.i(TAG, "modbus_poller stopped");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁桥接器,释放资源。
|
||||
*/
|
||||
public void destroy() {
|
||||
stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 modbus_poller 输出的一行 JSON。
|
||||
*/
|
||||
private void handlePollerOutput(String json) {
|
||||
try {
|
||||
JSONObject obj = new JSONObject(json);
|
||||
String status = obj.optString("status", "");
|
||||
String message = obj.optString("message", "");
|
||||
double weightKg = obj.optDouble("weightKg", -1);
|
||||
|
||||
switch (status) {
|
||||
case "connecting":
|
||||
dispatchEvent("connecting", message, 0);
|
||||
break;
|
||||
case "connected":
|
||||
dispatchEvent("connected", message, 0);
|
||||
break;
|
||||
case "sample":
|
||||
if (weightKg >= 0) {
|
||||
dispatchEvent("sample", message, weightKg);
|
||||
}
|
||||
break;
|
||||
case "error":
|
||||
dispatchEvent("error", message, 0);
|
||||
break;
|
||||
case "disconnected":
|
||||
dispatchEvent("disconnected", "串口已断开", 0);
|
||||
break;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.w(TAG, "Cannot parse: " + json);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 WebView 派发 woer-modbus CustomEvent。
|
||||
*/
|
||||
private void dispatchEvent(String status, String message, double weightKg) {
|
||||
if (webView == null) return;
|
||||
|
||||
// 转义消息中的特殊字符
|
||||
String safeMessage = message == null ? "" : message
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("'", "\\'")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "");
|
||||
|
||||
StringBuilder js = new StringBuilder();
|
||||
js.append("(function(){");
|
||||
js.append(" var detail = {");
|
||||
js.append(" status: '").append(status).append("'");
|
||||
if (!safeMessage.isEmpty()) {
|
||||
js.append(" , message: '").append(safeMessage).append("'");
|
||||
}
|
||||
if (weightKg > 0 || (status.equals("sample") && weightKg == 0)) {
|
||||
js.append(" , weightKg: ").append(weightKg);
|
||||
}
|
||||
js.append(" };");
|
||||
js.append(" window.dispatchEvent(new CustomEvent('woer-modbus', { detail: detail }));");
|
||||
js.append("})();");
|
||||
|
||||
final String jsCode = js.toString();
|
||||
activity.runOnUiThread(() -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
webView.evaluateJavascript(jsCode, null);
|
||||
} else {
|
||||
webView.loadUrl("javascript:" + jsCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.woer.smartscale;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.webkit.WebView;
|
||||
|
||||
/**
|
||||
* HCT 智能秤后台服务广播桥接器。
|
||||
*
|
||||
* 后台服务 com.android.hct.android_hid_scale_service 会发送
|
||||
* com.android.scale.weight 广播,weight 字段格式为:状态,净重,皮重,单位。
|
||||
*/
|
||||
public class ScaleBroadcastBridge {
|
||||
private static final String SCALE_SERVICE_PACKAGE = "com.android.hct.android_hid_scale_service";
|
||||
private static final String ACTION_WEIGHT = "com.android.scale.weight";
|
||||
private static final String ACTION_SET = "com.android.scale.set";
|
||||
|
||||
private final Activity activity;
|
||||
private final WebView webView;
|
||||
private boolean receiverRegistered;
|
||||
|
||||
private final BroadcastReceiver weightReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String reading = intent.getStringExtra("weight");
|
||||
if (reading == null || reading.trim().isEmpty()) {
|
||||
dispatchEvent("error", "电子秤广播缺少重量数据", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
String[] fields = reading.replace("\r", "").replace("\n", "").trim().split(",");
|
||||
if (fields.length < 4) {
|
||||
dispatchEvent("error", "电子秤重量数据格式无效", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String stability = fields[0].trim();
|
||||
double weightKg = toKilograms(Double.parseDouble(fields[1].trim()), fields[3].trim());
|
||||
String message = "ST".equals(stability) ? "重量稳定" : "US".equals(stability) ? "重量波动中" : stability;
|
||||
dispatchEvent("sample", message, weightKg);
|
||||
} catch (NumberFormatException ignored) {
|
||||
dispatchEvent("error", "电子秤重量值无效", 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public ScaleBroadcastBridge(Activity activity, WebView webView) {
|
||||
this.activity = activity;
|
||||
this.webView = webView;
|
||||
}
|
||||
|
||||
public boolean start() {
|
||||
if (!isScaleServiceInstalled()) {
|
||||
dispatchEvent("error", "未安装电子秤后台服务 android_hid_scale_service", 0);
|
||||
return false;
|
||||
}
|
||||
if (!receiverRegistered) {
|
||||
IntentFilter filter = new IntentFilter(ACTION_WEIGHT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
activity.registerReceiver(weightReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
} else {
|
||||
activity.registerReceiver(weightReceiver, filter);
|
||||
}
|
||||
receiverRegistered = true;
|
||||
}
|
||||
launchScaleService();
|
||||
dispatchEvent("connecting", "正在连接电子秤后台服务", 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean stop() {
|
||||
if (receiverRegistered) {
|
||||
try {
|
||||
activity.unregisterReceiver(weightReceiver);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Activity 已销毁时接收器可能已被系统注销。
|
||||
}
|
||||
receiverRegistered = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean sendCommand(String command) {
|
||||
if (!"zero".equals(command) && !"tare".equals(command)) return false;
|
||||
if (!isScaleServiceInstalled()) {
|
||||
dispatchEvent("error", "未安装电子秤后台服务 android_hid_scale_service", 0);
|
||||
return false;
|
||||
}
|
||||
Intent commandIntent = new Intent(ACTION_SET);
|
||||
commandIntent.putExtra("set", command);
|
||||
activity.sendBroadcast(commandIntent);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
stop();
|
||||
}
|
||||
|
||||
private boolean isScaleServiceInstalled() {
|
||||
try {
|
||||
activity.getPackageManager().getPackageInfo(SCALE_SERVICE_PACKAGE, 0);
|
||||
return true;
|
||||
} catch (PackageManager.NameNotFoundException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void launchScaleService() {
|
||||
Intent launchIntent = activity.getPackageManager().getLaunchIntentForPackage(SCALE_SERVICE_PACKAGE);
|
||||
if (launchIntent != null) {
|
||||
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
activity.startActivity(launchIntent);
|
||||
}
|
||||
}
|
||||
|
||||
private static double toKilograms(double value, String unit) {
|
||||
String normalized = unit.trim().toLowerCase();
|
||||
if ("g".equals(normalized) || "克".equals(unit.trim())) return value / 1000d;
|
||||
if ("lb".equals(normalized) || "lbs".equals(normalized)) return value * 0.45359237d;
|
||||
return value;
|
||||
}
|
||||
|
||||
private void dispatchEvent(String status, String message, double weightKg) {
|
||||
String safeMessage = message == null ? "" : message
|
||||
.replace("\\", "\\\\")
|
||||
.replace("'", "\\'")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "");
|
||||
StringBuilder javascript = new StringBuilder("(function(){window.dispatchEvent(new CustomEvent('woer-modbus',{detail:{status:'")
|
||||
.append(status).append("',message:'").append(safeMessage).append("'");
|
||||
if ("sample".equals(status)) javascript.append(",weightKg:").append(weightKg);
|
||||
javascript.append("}}));})();");
|
||||
activity.runOnUiThread(() -> webView.evaluateJavascript(javascript.toString(), null));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user