完善智能称重终端界面与电子秤接入

This commit is contained in:
FinleyHsu
2026-08-06 10:48:47 +08:00
parent 839edd1e4a
commit 3b856f0c11
8 changed files with 967 additions and 181 deletions
@@ -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));
}
}