7 Commits
1.0.2 ... 1.0.5

Author SHA1 Message Date
Zhanghu
cac9dd41aa 如果串口路径不为空时则开关一次设备 2026-03-05 14:47:07 +08:00
Zhanghu
d29aa0dfd1 * 仅串口路径为空时才自动检测设备
* 设置界面可清除串口路径
2026-03-05 14:04:38 +08:00
Zhanghu
5b0eb711a6 changelog 2026-01-05 16:57:27 +08:00
Zhanghu
dd03bbfcea 自动检测串口,检测时保证发送一次开机指令; 增加 acra 2026-01-05 15:39:16 +08:00
Zhanghu
80341f540f 当选择自动跳转时,直接访问 URL 前缀,这样网址自由设置 2025-11-01 10:03:36 +08:00
Zhanghu
cb151b989e fix retry count 2025-10-31 17:32:58 +08:00
Zhanghu
d450cf4933 changelog 2025-10-11 17:12:39 +08:00
18 changed files with 697 additions and 85 deletions

View File

@@ -96,6 +96,8 @@ repositories {
}
dependencies {
implementation libs.serialport
implementation libs.acra.http
implementation libs.appcompat
implementation libs.material
implementation libs.activity
@@ -111,6 +113,4 @@ dependencies {
implementation libs.banner
implementation libs.glide
annotationProcessor libs.glidecompiler
implementation 'io.github.xmaihh:serialport:2.1.1'
}

View File

@@ -9,7 +9,36 @@ author:
2. $ {VERSION_CODE} (去掉空格),会自动替换实际修订号,比如 1.1.4.$ {VERSION_CODE}
-->
### [1.0.2.${VERSION_CODE}] - 2025.10.11
### [1.0.5.${VERSION_CODE}] - 2026.3.5
#### 文件下载
* [dashboardclient_1.0.5.apk](dashboardclient_1.0.5.apk)
#### 更新记录
* App 启动时如果串口路径为空时自动检测设备, 否则开关一次设备
* 设置界面可清除串口路径
### [1.0.4.23] - 2026.1.5
#### 文件下载
* [dashboardclient_1.0.4.apk](dashboardclient_1.0.4.apk)
#### 更新记录
* 自动检测串口,检测时保证发送一次开机指令
* 增加 acra
### [1.0.3.22] - 2025.11.1
#### 文件下载
* [dashboardclient_1.0.3.apk](dashboardclient_1.0.3.apk)
#### 更新记录
* 当选择自动跳转时,直接访问 URL 前缀,这样网址自由设置
### [1.0.2.19] - 2025.10.11
#### 文件下载

View File

@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"

View File

@@ -0,0 +1,46 @@
/*************************************************************************
* 版权所有 (C) 2026宁波升维信息技术有限公司. 保留所有权利.
*
* 本软件是由 宁波升维信息技术有限公司 开发的。
* 未经版权所有者明确授权,任何人不得使用、复制、修改、分发本软件及其相关文档。
* 本软件包含机密和专有信息,未经授权不得向任何第三方披露。
*************************************************************************/
package cn.ykbox.dashboard;
import android.app.Application;
import org.acra.ACRA;
import org.acra.config.CoreConfigurationBuilder;
import org.acra.config.HttpSenderConfigurationBuilder;
import org.acra.data.StringFormat;
public class MyApplication extends Application {
public static final String TAG = "MyApplication";
@Override
public void onCreate() {
super.onCreate();
initLog();
initAcra();
}
private void initLog() {
}
private void initAcra() {
ACRA.init(this, new CoreConfigurationBuilder()
.withBuildConfigClass(BuildConfig.class)
.withReportFormat(StringFormat.JSON)
.withPluginConfigurations(
new HttpSenderConfigurationBuilder()
.withBasicAuthLogin("u9UEf5W0giZOBrtR")
.withBasicAuthPassword("B39uMIhh15cCoOK7")
.withUri("http://acra.slackz.cn/report")
.build()
)
);
}
}

View File

@@ -2,9 +2,11 @@ package cn.ykbox.dashboard.activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -33,8 +35,10 @@ import java.util.Calendar;
import cn.ykbox.dashboard.ConfigReader;
import cn.ykbox.dashboard.databinding.ActivityBuildingDashboardBinding;
import cn.ykbox.dashboard.perferences.PreferenceConfiguration;
import cn.ykbox.dashboard.receiver.CommandBroadcastReceiver;
import cn.ykbox.dashboard.serial.SerialControlDevices;
import cn.ykbox.dashboard.serial.SerialPortDetector;
public class BuildingDashboardActivity extends FullscreenActivity {
private final static String TAG = "DashboardActivity";
@@ -61,6 +65,9 @@ public class BuildingDashboardActivity extends FullscreenActivity {
private ActivityBuildingDashboardBinding binding;
private ActivityResultLauncher<Intent> settingsLauncher;
private SerialPortDetector detector;
private ProgressDialog progressDialog;
private final View.OnTouchListener mDelayHideTouchListener = (view, motionEvent) -> {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
@@ -85,6 +92,7 @@ public class BuildingDashboardActivity extends FullscreenActivity {
super.setViews(binding.webview, binding.fullscreenContentControls);
super.onCreate(savedInstanceState);
initSerialPort();
initSettingsLauncher();
initListener();
initWebView();
@@ -95,21 +103,130 @@ public class BuildingDashboardActivity extends FullscreenActivity {
protected void onResume() {
super.onResume();
// 当用户选择了自动跳转时,直接访问 urlPrefix这样 url 可以自由设置
SharedPreferences pre = PreferenceManager.getDefaultSharedPreferences(this);
String url = pre.getString("k_url", "http://172.18.22.211:8002/Dashboard");
String type = pre.getString("k_dashboard_type", "0");
configUrl = url + "/data/config.json";
String urlPrefix = pre.getString("k_url_prefix", "http://172.18.22.211:8002/Dashboard");
String urlEndPoint = pre.getString("k_url_end_point", "/").replaceFirst("^/", "");
configUrl = urlPrefix + "/data/config.json";
mainUrl = urlEndPoint.isEmpty() ? urlPrefix : urlPrefix + "/" + urlEndPoint;
if(!type.equals("0"))
mainUrl = url + "/dashboards/" + type + "/index.html";
else
mainUrl = url + "/index.html";
Log.i(TAG, "Main: " + mainUrl);
Log.i(TAG, "Config: " + configUrl);
configLoadHandler.post(configLoadRunnable);
loadUrlWithRetry();
}
private void initSerialPort() {
String portPath = PreferenceConfiguration.getSerialPortPath(this);
int baudRate = PreferenceConfiguration.getSerialPortBaudRate(this);
boolean sendPowerOnCmd = PreferenceConfiguration.getSendPowerOnCmd(this);
// 初始化串口检测器
detector = new SerialPortDetector(portPath, baudRate);
// 只有 portPath 为空时才进行串口检测
if (TextUtils.isEmpty(portPath)) {
Log.i(TAG, "Starting serial port detection/verification...");
// 参数 true 表示确保设备开机(发送两个命令)
// 参数 false 表示仅检测串口(任一命令有响应即可)
startSerialPortDetection(true);
} else {
Log.i(TAG, "Using saved serial port path: " + portPath);
// 如果勾选了"发送打开电源指令",则先发送关闭再发送打开
if (sendPowerOnCmd) {
Log.i(TAG, "Sending power on sequence...");
new Thread(() -> {
detector.sendPowerOffCommand();
try {
Thread.sleep(3000); // 等待 3 秒
} catch (InterruptedException e) {
Log.e(TAG, "Sleep interrupted", e);
}
detector.sendPowerOnCommand();
}).start();
}
}
}
/**
* 启动串口检测
* @param ensurePowerOn 是否确保设备开机
*/
private void startSerialPortDetection(boolean ensurePowerOn) {
// 显示进度对话框
progressDialog = new ProgressDialog(this);
progressDialog.setTitle("串口检测");
progressDialog.setMessage("正在检测串口...");
progressDialog.setCancelable(false);
progressDialog.show();
// 设置检测回调
detector.setCallback(new SerialPortDetector.DetectionCallback() {
@Override
public void onDetectionStart() {
Log.d(TAG, "Detection started");
runOnUiThread(() -> {
if (progressDialog != null) {
progressDialog.setMessage("开始检测串口...");
}
});
}
@Override
public void onDetectionProgress(String portPath, int current, int total) {
Log.d(TAG, "Testing port " + current + "/" + total + ": " + portPath);
runOnUiThread(() -> {
if (progressDialog != null) {
progressDialog.setMessage("正在测试串口 " + current + "/" + total + "\n" + portPath);
}
});
}
@Override
public void onDetectionSuccess(String portPath) {
Log.i(TAG, "Detection success: " + portPath);
runOnUiThread(() -> {
if (progressDialog != null) {
progressDialog.dismiss();
}
Toast.makeText(BuildingDashboardActivity.this,
"串口检测成功!\n路径: " + portPath,
Toast.LENGTH_LONG).show();
PreferenceConfiguration.setSerialPortPath(BuildingDashboardActivity.this, portPath);
});
}
@Override
public void onDetectionFailed() {
Log.e(TAG, "Detection failed");
runOnUiThread(() -> {
if (progressDialog != null) {
progressDialog.dismiss();
}
// 可以显示错误界面或重试选项
showDetectionFailedDialog();
});
}
});
// 开始检测
detector.startDetection(ensurePowerOn);
}
/**
* 显示检测失败的对话框
*/
private void showDetectionFailedDialog() {
Toast.makeText(BuildingDashboardActivity.this,
"未找到有效的串口设备,请检查设备连接后重试。",
Toast.LENGTH_LONG).show();
}
private void loadUrlWithRetry() {
Log.i("WebView", "Loading " + mainUrl);
binding.webview.loadUrl(mainUrl);
}
@@ -181,6 +298,8 @@ public class BuildingDashboardActivity extends FullscreenActivity {
WebSettings webSettings = binding.webview.getSettings();
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
webSettings.setJavaScriptEnabled(true);
// 允许媒体自动播放 (Android 8+ 必须)
webSettings.setMediaPlaybackRequiresUserGesture(false);
// 禁用缩放相关设置
webSettings.setTextZoom(100);
@@ -190,13 +309,20 @@ public class BuildingDashboardActivity extends FullscreenActivity {
binding.webview.setInitialScale(100);
binding.webview.setWebViewClient(new WebViewClient() {
private boolean hasError = false;
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
hasError = false;
}
@Override
public void onReceivedError(WebView view, WebResourceRequest request,
WebResourceError error) {
super.onReceivedError(view, request, error);
// 只处理主页面的错误,不处理资源文件错误
if (request.getUrl().toString().equals(mainUrl)) {
if (request.isForMainFrame()) {
hasError = true;
handleLoadError();
}
}
@@ -205,9 +331,8 @@ public class BuildingDashboardActivity extends FullscreenActivity {
public void onReceivedHttpError(WebView view, WebResourceRequest request,
WebResourceResponse errorResponse) {
super.onReceivedHttpError(view, request, errorResponse);
// 处理HTTP错误如404, 500等
if (request.getUrl().toString().equals(mainUrl)) {
if (request.isForMainFrame() && errorResponse.getStatusCode() >= 400) {
hasError = true;
handleLoadError();
}
}
@@ -215,15 +340,12 @@ public class BuildingDashboardActivity extends FullscreenActivity {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// 页面加载成功,重置重试计数
if (url.equals(mainUrl)) {
if (!hasError) {
retryCount = 0;
Log.d("WebView", "页面加载成功: " + url);
Log.d("WebView", "加载OK");
}
}
});
}
private void initConfigLoader() {
@@ -282,8 +404,6 @@ public class BuildingDashboardActivity extends FullscreenActivity {
try {
JSONObject serialConfig = new JSONObject(serialConfigJson);
String portPath = serialConfig.optString("DevicePath", "/dev/ttyS2");
int baudRate = serialConfig.optInt("Baud", 9600);
if (serialConfig.has("Commands")) {
JSONArray commands = serialConfig.getJSONArray("Commands");
@@ -299,8 +419,6 @@ public class BuildingDashboardActivity extends FullscreenActivity {
Intent intent = new Intent(this, CommandBroadcastReceiver.class);
intent.setAction(CommandBroadcastReceiver.ACTION_SEND_COMMAND);
intent.putExtra(CommandBroadcastReceiver.EXTRA_COMMAND_HEX, hex);
intent.putExtra(CommandBroadcastReceiver.EXTRA_PORT_PATH, portPath);
intent.putExtra(CommandBroadcastReceiver.EXTRA_BAUD_RATE, baudRate);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, i, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
@@ -316,17 +434,14 @@ public class BuildingDashboardActivity extends FullscreenActivity {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
Log.d(TAG, "setExactAndAllowWhileIdle, Set repeating alarm for " + time + " with command " + hex + " on port " + portPath + " at " + baudRate + " baud");
Log.d(TAG, "setExactAndAllowWhileIdle, Set repeating alarm for " + time + " with command " + hex);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
Log.d(TAG, "setExact, Set repeating alarm for " + time + " with command " + hex + " on port " + portPath + " at " + baudRate + " baud");
Log.d(TAG, "setExact, Set repeating alarm for " + time + " with command " + hex);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
Log.d(TAG, "Set repeating alarm for " + time + " with command " + hex + " on port " + portPath + " at " + baudRate + " baud");
Log.d(TAG, "Set repeating alarm for " + time + " with command " + hex);
}
// alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pendingIntent);
}
}

View File

@@ -1,12 +1,16 @@
package cn.ykbox.dashboard.activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import cn.ykbox.dashboard.R;
import cn.ykbox.dashboard.perferences.PreferenceConfiguration;
public class SettingsActivity extends AppCompatActivity {
@@ -30,6 +34,21 @@ public class SettingsActivity extends AppCompatActivity {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
Preference clearDevicePref = findPreference("k_clear_device");
if (clearDevicePref != null) {
clearDevicePref.setOnPreferenceClickListener(preference -> {
new AlertDialog.Builder(requireContext())
.setTitle("确认清除")
.setMessage("确定要清除当前串口设备路径吗?")
.setPositiveButton("确定", (dialog, which) -> {
PreferenceConfiguration.setSerialPortPath(requireContext(), "");
})
.setNegativeButton("取消", null)
.show();
return true;
});
}
}
}
}

View File

@@ -21,9 +21,6 @@ public class StartActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 开机后手动发送一次开启电源指令
SerialControlDevices.sendCommand("/dev/ttyS2", 9600, "ACEAB400ED");
mContext = this;
EdgeToEdge.enable(this);
setContentView(R.layout.activity_start);

View File

@@ -0,0 +1,38 @@
package cn.ykbox.dashboard.perferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
/**
* 配置管理
* TODO 代码中零散的读取配置选项代码都集中到这里
*/
public class PreferenceConfiguration {
private final static String TAG = "PreferenceConfiguration";
private static final String KEY_SERIAL_PORT_PATH = "k_serial_port_path";
private static final String KEY_SERIAL_PORT_BAUD_RATE = "k_serial_baud";
private static final String KEY_SEND_POWER_ON_CMD = "k_send_power_on_cmd";
public static String getSerialPortPath(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getString(KEY_SERIAL_PORT_PATH, "");
}
public static void setSerialPortPath(Context context, String portPath) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putString(KEY_SERIAL_PORT_PATH, portPath).apply();
}
public static int getSerialPortBaudRate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String baud = prefs.getString(KEY_SERIAL_PORT_BAUD_RATE, "9600");
return Integer.parseInt(baud);
}
public static boolean getSendPowerOnCmd(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(KEY_SEND_POWER_ON_CMD, true);
}
}

View File

@@ -3,25 +3,29 @@ package cn.ykbox.dashboard.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import androidx.preference.PreferenceManager;
import cn.ykbox.dashboard.perferences.PreferenceConfiguration;
import cn.ykbox.dashboard.serial.SerialControlDevices;
public class CommandBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "CommandReceiver";
public static final String ACTION_SEND_COMMAND = "cn.ykbox.dashboard.ACTION_SEND_COMMAND";
public static final String EXTRA_COMMAND_HEX = "EXTRA_COMMAND_HEX";
public static final String EXTRA_PORT_PATH = "EXTRA_PORT_PATH";
public static final String EXTRA_BAUD_RATE = "EXTRA_BAUD_RATE"; // 新增:波特率的 Extra Key
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && ACTION_SEND_COMMAND.equals(intent.getAction())) {
String hexCommand = intent.getStringExtra(EXTRA_COMMAND_HEX);
String portPath = intent.getStringExtra(EXTRA_PORT_PATH);
int baudRate = intent.getIntExtra(EXTRA_BAUD_RATE, 9600);
if (hexCommand != null && !hexCommand.isEmpty() && portPath != null && !portPath.isEmpty()) {
String portPath = PreferenceConfiguration.getSerialPortPath(context);
int baudRate = PreferenceConfiguration.getSerialPortBaudRate(context);
if (hexCommand != null && !hexCommand.isEmpty() && !TextUtils.isEmpty(portPath)) {
Log.d(TAG, "Received alarm to send command '" + hexCommand + "' to port '" + portPath + "' at " + baudRate + " baud");
// 使用新的静态方法发送指令
boolean success = SerialControlDevices.sendCommand(portPath, baudRate, hexCommand);

View File

@@ -0,0 +1,363 @@
package cn.ykbox.dashboard.serial;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import android_serialport_api.SerialPortFinder;
import tp.xmaihh.serialport.SerialHelper;
import tp.xmaihh.serialport.bean.ComBean;
public class SerialPortDetector {
private static final String TAG = "SerialPortDetector";
// 测试命令
private static final String TEST_CMD_OFF = "ACEAB500ED"; // 关闭设备
private static final String TEST_CMD_ON = "ACEAB400ED"; // 打开设备
// 响应超时时间(毫秒)
private static final long RESPONSE_TIMEOUT = 6000;
private DetectionCallback callback;
private Handler mainHandler;
private String savedPath;
private int baudRate;
public interface DetectionCallback {
void onDetectionStart();
void onDetectionProgress(String portPath, int current, int total);
void onDetectionSuccess(String portPath);
void onDetectionFailed();
}
public SerialPortDetector(String savedPath, int baudRate) {
this.savedPath = savedPath;
this.baudRate = baudRate;
this.mainHandler = new Handler(Looper.getMainLooper());
}
public void setCallback(DetectionCallback callback) {
this.callback = callback;
}
/**
* 发送关闭设备指令到已保存的串口(不等待响应)
*/
public void sendPowerOffCommand() {
sendCommand(TEST_CMD_OFF);
}
/**
* 发送打开设备指令到已保存的串口(不等待响应)
*/
public void sendPowerOnCommand() {
sendCommand(TEST_CMD_ON);
}
/**
* 发送指令到已保存的串口(不等待响应)
* @param commandHex 十六进制命令字符串
*/
private void sendCommand(String commandHex) {
if (TextUtils.isEmpty(savedPath)) {
Log.w(TAG, "No serial port path configured");
return;
}
new Thread(() -> {
SerialHelper serialHelper = null;
try {
serialHelper = new SimpleSerialHelper(savedPath, baudRate);
serialHelper.open();
serialHelper.sendHex(commandHex);
Log.d(TAG, "Sent command: " + commandHex);
} catch (Exception e) {
Log.e(TAG, "Error sending command: " + e.getMessage());
} finally {
if (serialHelper != null) {
try {
serialHelper.close();
} catch (Exception e) {
Log.e(TAG, "Error closing port: " + e.getMessage());
}
}
}
}).start();
}
/**
* 开始串口检测(智能检测)
* 先验证已保存的串口,如果不可用再进行全局检测
*/
public void startDetection() {
startDetection(false);
}
/**
* 开始串口检测(智能检测)
* @param ensurePowerOn 是否确保设备开机(发送两个命令)
*/
public void startDetection(boolean ensurePowerOn) {
new Thread(() -> {
notifyDetectionStart();
if (!TextUtils.isEmpty(savedPath)) {
Log.d(TAG, "Found saved serial port path: " + savedPath);
Log.d(TAG, "Verifying saved port: " + savedPath);
// 验证已保存的串口是否可用
if (testSerialPort(savedPath, baudRate, ensurePowerOn)) {
Log.i(TAG, "Saved serial port is valid: " + savedPath);
notifyDetectionSuccess(savedPath);
return;
} else {
Log.w(TAG, "Saved serial port is invalid, starting full detection");
}
} else {
Log.d(TAG, "No saved serial port path, starting full detection");
}
// 已保存的串口不可用或不存在,进行全局检测
performFullDetection(ensurePowerOn);
}).start();
}
private void performFullDetection(boolean ensurePowerOn) {
SerialPortFinder finder = new SerialPortFinder();
String[] devicePaths = finder.getAllDevicesPath();
if (devicePaths == null || devicePaths.length == 0) {
Log.e(TAG, "No serial ports found");
notifyDetectionFailed();
return;
}
Log.d(TAG, "Found " + devicePaths.length + " serial ports");
// 遍历所有串口
for (int i = 0; i < devicePaths.length; i++) {
String portPath = devicePaths[i];
notifyDetectionProgress(portPath, i + 1, devicePaths.length);
Log.d(TAG, "Testing port: " + portPath + " at " + baudRate + " baud");
if (testSerialPort(portPath, baudRate, ensurePowerOn)) {
Log.i(TAG, "Serial port detected successfully: " + portPath);
notifyDetectionSuccess(portPath);
return;
}
}
Log.e(TAG, "No valid serial port found");
notifyDetectionFailed();
}
/**
* 测试指定的串口
* @param portPath 串口路径
* @param baudRate 波特率
* @param ensurePowerOn 是否确保设备开机(发送两个命令)
*/
private boolean testSerialPort(String portPath, int baudRate, boolean ensurePowerOn) {
TestSerialHelper serialHelper = null;
try {
serialHelper = new TestSerialHelper(portPath, baudRate);
serialHelper.open();
if (ensurePowerOn) {
// 需要确保设备开机,两个命令都要发送
Log.d(TAG, "Ensuring device power on by sending both commands");
boolean cmd1Success = sendCommandAndWaitResponse(serialHelper, TEST_CMD_OFF);
boolean cmd2Success = sendCommandAndWaitResponse(serialHelper, TEST_CMD_ON);
// 只要有一个命令收到有效响应就认为串口可用
if (cmd1Success || cmd2Success) {
Log.d(TAG, "Device responded (CMD1: " + cmd1Success + ", CMD2: " + cmd2Success + ")");
return true;
}
return false;
} else {
// 只需要检测串口,任意一个命令有响应即可
if (sendCommandAndWaitResponse(serialHelper, TEST_CMD_OFF)) {
return true;
}
if (sendCommandAndWaitResponse(serialHelper, TEST_CMD_ON)) {
return true;
}
return false;
}
} catch (Exception e) {
Log.e(TAG, "Error testing port " + portPath + ": " + e.getMessage());
return false;
} finally {
if (serialHelper != null) {
try {
serialHelper.close();
} catch (Exception e) {
Log.e(TAG, "Error closing port: " + e.getMessage());
}
}
}
}
/**
* 发送命令并等待响应
*/
private boolean sendCommandAndWaitResponse(TestSerialHelper serialHelper, String command) {
try {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean responseReceived = new AtomicBoolean(false);
serialHelper.setResponseListener((isValid) -> {
if (isValid) {
responseReceived.set(true);
}
latch.countDown();
});
// 清空之前的响应标志
serialHelper.resetResponse();
// 发送命令
serialHelper.sendHex(command);
Log.d(TAG, "Sent command: " + command);
// 等待响应
boolean timeout = !latch.await(RESPONSE_TIMEOUT, TimeUnit.MILLISECONDS);
if (timeout) {
Log.d(TAG, "Response timeout for command: " + command);
return false;
}
return responseReceived.get();
} catch (InterruptedException e) {
Log.e(TAG, "Wait interrupted: " + e.getMessage());
return false;
}
}
// 回调通知方法
private void notifyDetectionStart() {
if (callback != null) {
mainHandler.post(() -> callback.onDetectionStart());
}
}
private void notifyDetectionProgress(String portPath, int current, int total) {
if (callback != null) {
mainHandler.post(() -> callback.onDetectionProgress(portPath, current, total));
}
}
private void notifyDetectionSuccess(String portPath) {
if (callback != null) {
mainHandler.post(() -> callback.onDetectionSuccess(portPath));
}
}
private void notifyDetectionFailed() {
if (callback != null) {
mainHandler.post(() -> callback.onDetectionFailed());
}
}
/**
* 简单的串口 Helper用于只发送命令不等待响应
*/
private static class SimpleSerialHelper extends SerialHelper {
public SimpleSerialHelper(String sPort, int iBaudRate) {
super(sPort, iBaudRate);
}
@Override
protected void onDataReceived(ComBean comBean) {
// 不需要处理响应
}
}
/**
* 内部测试用的 SerialHelper
*/
private static class TestSerialHelper extends SerialHelper {
private ResponseListener responseListener;
public interface ResponseListener {
void onResponse(boolean isValid);
}
public TestSerialHelper(String sPort, int iBaudRate) {
super(sPort, iBaudRate);
}
public void setResponseListener(ResponseListener listener) {
this.responseListener = listener;
}
public void resetResponse() {
// 重置响应状态,为下一次测试做准备
}
@Override
protected void onDataReceived(ComBean comBean) {
byte[] data = comBean.bRec;
// 检查是否为有效响应格式: DA XX XX XX XX ED
if (isValidResponse(data)) {
Log.d(TAG, "Valid response received: " + bytesToHex(data));
if (responseListener != null) {
responseListener.onResponse(true);
}
} else {
Log.d(TAG, "Invalid response: " + bytesToHex(data));
if (responseListener != null) {
responseListener.onResponse(false);
}
}
}
/**
* 检查响应是否符合格式: DA XX XX XX XX ED
*/
private boolean isValidResponse(byte[] data) {
if (data == null || data.length < 6) {
return false;
}
// 检查起始字节是否为 0xDA
if ((data[0] & 0xFF) != 0xDA) {
return false;
}
// 检查结束字节是否为 0xED
if ((data[data.length - 1] & 0xFF) != 0xED) {
return false;
}
return true;
}
/**
* 字节数组转十六进制字符串(用于日志)
*/
private String bytesToHex(byte[] bytes) {
if (bytes == null) {
return "null";
}
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b & 0xFF));
}
return sb.toString().trim();
}
}
}

View File

@@ -1,7 +1,7 @@
<resources>
<string-array name="dashboard_type_entries">
<item>0 (根据服务器配置跳转)</item>
<item>1 (按楼层)</item>
<item>2 (网格+统计)</item>
<string-array name="url_end_point_entries">
<item>自动跳转</item>
<item>按楼层</item>
<item>网格+统计</item>
</string-array>
</resources>

View File

@@ -10,15 +10,15 @@
<item>reply_all</item>
</string-array>
<string-array name="dashboard_type_entries">
<item>0 (Auto Redirect)</item>
<item>1 (Floor)</item>
<item>2 (Grid+Statics)</item>
<string-array name="url_end_point_entries">
<item>Auto Redirect</item>
<item>Floor</item>
<item>Grid+Statics</item>
</string-array>
<string-array name="dashboard_type_values">
<item>0</item>
<item>1</item>
<item>2</item>
<string-array name="url_end_point_values">
<item>/</item>
<item>/dashboards/1</item>
<item>/dashboards/2</item>
</string-array>
</resources>

View File

@@ -2,5 +2,5 @@
<string name="app_name">Dashboard</string>
<string name="title_activity_building_dashboard">Building Dashboard</string>
<string name="title_activity_settings">Settings</string>
<string name="dashboard_type_default_value" translatable="false">0</string>
<string name="url_end_point_default_value" translatable="false">/</string>
</resources>

View File

@@ -1,16 +1,38 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="网页地址">
<EditTextPreference
app:key="k_url"
app:key="k_url_prefix"
app:title="URL 前缀"
app:defaultValue="http://172.18.22.211:8002/Dashboard"
app:useSimpleSummaryProvider="true" />
<ListPreference
app:title="仪表盘类型"
app:key="k_dashboard_type"
app:title="仪表盘风格"
app:key="k_url_end_point"
app:useSimpleSummaryProvider="true"
app:entries="@array/dashboard_type_entries"
app:entryValues="@array/dashboard_type_values"
app:defaultValue="@string/dashboard_type_default_value" />
app:entries="@array/url_end_point_entries"
app:entryValues="@array/url_end_point_values"
app:defaultValue="@string/url_end_point_default_value" />
</PreferenceCategory>
<PreferenceCategory app:title="外部设备">
<EditTextPreference
app:key="k_serial_port_path"
app:title="串口设备"
app:defaultValue=""
app:useSimpleSummaryProvider="true" />
<EditTextPreference
app:key="k_serial_baud"
app:title="串口波特率"
app:defaultValue="9600"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
app:key="k_send_power_on_cmd"
app:title="App 启动时发送打开电源指令"
app:summary="每次启动 App 时先发送关闭再发送打开电源指令"
app:defaultValue="true" />
<Preference
app:key="k_clear_device"
app:title="清除设备"
app:summary="清除当前串口设备路径, 下次启动时自动检测设备。" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -15,10 +15,11 @@ buildscript {
allprojects {
repositories {
maven { url 'https://jitpack.io' }
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'https://maven.aliyun.com/repository/jcenter' } // 代替 jc
maven { url 'https://repo1.maven.org/maven2/' }
google()
mavenCentral()
maven { url 'https://repo1.maven.org/maven2/' }
}
// 提示过时的API

View File

@@ -1,20 +0,0 @@
@echo off
echo clear builds
rd /s /q %~dp0\app_classtv\build
rd /s /q %~dp0\app_sinclass\build
rd /s /q %~dp0\app_sinclasspad\build
rd /s /q %~dp0\app_sinclasspad2\build
rd /s /q %~dp0\app_startap\build
rd /s /q %~dp0\bjcast\build
rd /s /q %~dp0\cccl\build
rd /s /q %~dp0\demo_coaface\build
rd /s /q %~dp0\demo_serial\build
rd /s /q %~dp0\demo_serviceecd\build
rd /s /q %~dp0\demo_twoscreen\build
rd /s /q %~dp0\serialport\build
rd /s /q %~dp0\serviceecd\build
rd /s /q %~dp0\signageapi\build
rd /s /q %~dp0\signageui\build
rd /s /q %~dp0\signageutil\build

View File

@@ -1,4 +1,6 @@
[versions]
serialport = "2.1.1"
acraHttp = "5.13.1"
agp = "8.8.2"
exoplayer = "2.19.1"
banner = "2.2.2"
@@ -28,6 +30,8 @@ utilcodex = "1.31.1"
xlog = "1.11.1"
[libraries]
serialport = { module = "io.github.xmaihh:serialport", version.ref = "serialport" }
acra-http = { module = "ch.acra:acra-http", version.ref = "acraHttp" }
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
banner = { module = "io.github.youth5201314:banner", version.ref = "banner" }
exoplayer = { module = "com.google.android.exoplayer:exoplayer", version.ref = "exoplayer" }

View File

@@ -1,8 +1 @@
include ':app_dashboard'
//include ':utils'
//project(':utils').projectDir = new File("mod_utils/utils")
//include ':signageapi'
//project(':signageapi').projectDir = new File("mod_signageapi/signageapi")
//include ':serialport'
//project(':serialport').projectDir = new File("mod_serialport/serialport")
//include ':dashboard'