Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15cbcc206e | ||
|
|
e8eff1d512 | ||
|
|
5f1757fed4 | ||
|
|
fcfc964fb6 | ||
|
|
6c0204325f | ||
|
|
08144f5106 | ||
|
|
40bfbd8ceb | ||
|
|
98dba95232 | ||
|
|
2a5a6e95b4 | ||
|
|
691d6c15ad | ||
|
|
cac9dd41aa | ||
|
|
d29aa0dfd1 | ||
|
|
5b0eb711a6 | ||
|
|
dd03bbfcea |
@@ -96,6 +96,8 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation libs.serialport
|
||||||
|
implementation libs.acra.http
|
||||||
implementation libs.appcompat
|
implementation libs.appcompat
|
||||||
implementation libs.material
|
implementation libs.material
|
||||||
implementation libs.activity
|
implementation libs.activity
|
||||||
@@ -107,10 +109,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation libs.utilcodex
|
implementation libs.utilcodex
|
||||||
|
|
||||||
|
implementation libs.dialogx
|
||||||
implementation libs.okhttp
|
implementation libs.okhttp
|
||||||
implementation libs.banner
|
implementation libs.banner
|
||||||
implementation libs.glide
|
implementation libs.glide
|
||||||
annotationProcessor libs.glidecompiler
|
annotationProcessor libs.glidecompiler
|
||||||
|
|
||||||
implementation 'io.github.xmaihh:serialport:2.1.1'
|
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,50 @@ author:
|
|||||||
2. $ {VERSION_CODE} (去掉空格),会自动替换实际修订号,比如 1.1.4.$ {VERSION_CODE}
|
2. $ {VERSION_CODE} (去掉空格),会自动替换实际修订号,比如 1.1.4.$ {VERSION_CODE}
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### [1.0.3.${VERSION_CODE}] - 2025.11.1
|
### [1.1.1.35] - 2026.3.16
|
||||||
|
|
||||||
|
#### 文件下载
|
||||||
|
|
||||||
|
* [dashboardclient_1.1.1.apk](dashboardclient_1.1.1.apk)
|
||||||
|
|
||||||
|
#### 更新记录
|
||||||
|
* 修正达到重试加载链接上限之后闪退的问题
|
||||||
|
* 美化启动画面
|
||||||
|
* 清除冗余代码
|
||||||
|
|
||||||
|
### [1.1.0.32] - 2026.3.13
|
||||||
|
|
||||||
|
#### 文件下载
|
||||||
|
|
||||||
|
* [dashboardclient_1.1.0.apk](dashboardclient_1.1.0.apk)
|
||||||
|
|
||||||
|
#### 更新记录
|
||||||
|
* WebView 启用 LocalStorage,支持新版前端
|
||||||
|
* 优化定时开关机逻辑,增加不控制和本地控制
|
||||||
|
* 命令发送3次,避免命令丢失
|
||||||
|
* 支持老款网关检测
|
||||||
|
|
||||||
|
### [1.0.5.26] - 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
|
||||||
|
|
||||||
#### 文件下载
|
#### 文件下载
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".MyApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/title_activity_settings" />
|
android:label="@string/title_activity_settings" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.BuildingDashboardActivity"
|
android:name=".activity.DashboardActivity"
|
||||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/title_activity_building_dashboard"
|
android:label="@string/title_activity_building_dashboard"
|
||||||
@@ -28,14 +29,9 @@
|
|||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".activity.GalleryActivity"
|
|
||||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
|
||||||
android:exported="true" />
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".BootBroadcastReceiver"
|
android:name=".BootBroadcastReceiver"
|
||||||
@@ -54,5 +50,4 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -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()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package cn.ykbox.dashboard.activity;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
|
|
||||||
//import cn.slackz.signageapi.PlatformFactory;
|
|
||||||
|
|
||||||
public class BaseActivity extends AppCompatActivity {
|
|
||||||
final private String TAG = "BaseActivity";
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
hideSystemUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
Log.d(TAG, "onResume " + this);
|
|
||||||
// PlatformFactory.getInstance().hideNavBar(true);
|
|
||||||
// PlatformFactory.getInstance().setSlideShowNavBar(false);
|
|
||||||
// PlatformFactory.getInstance().setSlideShowNotificationBar(false);
|
|
||||||
hideSystemUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
|
|
||||||
showSystemUI();
|
|
||||||
Log.d(TAG, "onPause " + this);
|
|
||||||
// PlatformFactory.getInstance().hideNavBar(false);
|
|
||||||
// PlatformFactory.getInstance().setSlideShowNotificationBar(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onWindowFocusChanged(boolean hasFocus) {
|
|
||||||
super.onWindowFocusChanged(hasFocus);
|
|
||||||
// if (hasFocus) {
|
|
||||||
hideSystemUI();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void hideSystemUI() {
|
|
||||||
ActionBar actionBar = getSupportActionBar();
|
|
||||||
if (actionBar != null) {
|
|
||||||
actionBar.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enables regular immersive mode.
|
|
||||||
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
|
|
||||||
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
||||||
View decorView = getWindow().getDecorView();
|
|
||||||
|
|
||||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT > 11 && Build.VERSION.SDK_INT < 19) {
|
|
||||||
decorView.setSystemUiVisibility(View.GONE);
|
|
||||||
} else if (Build.VERSION.SDK_INT >= 19) {
|
|
||||||
decorView.setSystemUiVisibility(
|
|
||||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
||||||
// Set the content to appear under the system bars so that the
|
|
||||||
// content doesn't resize when the system bars hide and show.
|
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
// Hide the nav bar and status bar
|
|
||||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Shows the system bars by removing all the flags
|
|
||||||
// except for the ones that make the content appear under the system bars.
|
|
||||||
private void showSystemUI() {
|
|
||||||
View decorView = getWindow().getDecorView();
|
|
||||||
decorView.setSystemUiVisibility(
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
package cn.ykbox.dashboard.activity;
|
|
||||||
|
|
||||||
import android.app.AlarmManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
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;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.webkit.WebResourceError;
|
|
||||||
import android.webkit.WebResourceRequest;
|
|
||||||
import android.webkit.WebResourceResponse;
|
|
||||||
import android.webkit.WebSettings;
|
|
||||||
import android.webkit.WebView;
|
|
||||||
import android.webkit.WebViewClient;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.util.Calendar;
|
|
||||||
|
|
||||||
import cn.ykbox.dashboard.ConfigReader;
|
|
||||||
import cn.ykbox.dashboard.databinding.ActivityBuildingDashboardBinding;
|
|
||||||
import cn.ykbox.dashboard.receiver.CommandBroadcastReceiver;
|
|
||||||
import cn.ykbox.dashboard.serial.SerialControlDevices;
|
|
||||||
|
|
||||||
public class BuildingDashboardActivity extends FullscreenActivity {
|
|
||||||
private final static String TAG = "DashboardActivity";
|
|
||||||
|
|
||||||
private static final boolean AUTO_HIDE = true;
|
|
||||||
private static final int AUTO_HIDE_DELAY_MILLIS = 3000;
|
|
||||||
private static final long CONFIG_LOAD_INTERVAL = 10 * 60 * 1000; // 10 minutes
|
|
||||||
private static final int MAX_COMMAND_ALARMS = 20; // 支持的最大闹钟指令数量
|
|
||||||
|
|
||||||
private Context mContext;
|
|
||||||
private String mainUrl;
|
|
||||||
private int retryCount = 0;
|
|
||||||
private static final int MAX_RETRY = 99; // 最大重试次数
|
|
||||||
private static final int RETRY_DELAY = 60000; // 1分钟 = 60000毫秒
|
|
||||||
private String configUrl;
|
|
||||||
|
|
||||||
private ConfigReader configReader;
|
|
||||||
private String lastAppliedSerialConfig = null;
|
|
||||||
|
|
||||||
// --- Periodic Config Loading ---
|
|
||||||
private final Handler configLoadHandler = new Handler(Looper.getMainLooper());
|
|
||||||
private Runnable configLoadRunnable;
|
|
||||||
|
|
||||||
private ActivityBuildingDashboardBinding binding;
|
|
||||||
private ActivityResultLauncher<Intent> settingsLauncher;
|
|
||||||
|
|
||||||
private final View.OnTouchListener mDelayHideTouchListener = (view, motionEvent) -> {
|
|
||||||
switch (motionEvent.getAction()) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
if (AUTO_HIDE) {
|
|
||||||
delayedHide(AUTO_HIDE_DELAY_MILLIS);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
view.performClick();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
binding = ActivityBuildingDashboardBinding.inflate(getLayoutInflater());
|
|
||||||
setContentView(binding.getRoot());
|
|
||||||
|
|
||||||
super.setViews(binding.webview, binding.fullscreenContentControls);
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
initSettingsLauncher();
|
|
||||||
initListener();
|
|
||||||
initWebView();
|
|
||||||
initConfigLoader();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
// 当用户选择了自动跳转时,直接访问 urlPrefix,这样 url 可以自由设置
|
|
||||||
SharedPreferences pre = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
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;
|
|
||||||
|
|
||||||
configLoadHandler.post(configLoadRunnable);
|
|
||||||
loadUrlWithRetry();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadUrlWithRetry() {
|
|
||||||
Log.i("WebView", "Loading " + mainUrl);
|
|
||||||
binding.webview.loadUrl(mainUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleLoadError() {
|
|
||||||
if (retryCount < MAX_RETRY) {
|
|
||||||
retryCount++;
|
|
||||||
Log.d("WebView", "加载失败,将在1分钟后重试 (第" + retryCount + "次重试)");
|
|
||||||
|
|
||||||
// 使用Handler延迟执行重试
|
|
||||||
configLoadHandler.postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Log.d("WebView", "开始重试加载...");
|
|
||||||
loadUrlWithRetry();
|
|
||||||
}
|
|
||||||
}, RETRY_DELAY);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Log.e("WebView", "重试次数已达上限,加载失败");
|
|
||||||
// 这里可以显示错误页面或提示用户
|
|
||||||
showToast("网页加载失败,请检查网络连接");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
configLoadHandler.removeCallbacks(configLoadRunnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
if (configReader != null) {
|
|
||||||
configReader.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听按键,因为 WebView 可能会拦截功能键,这里不使用 onKeyDown
|
|
||||||
* @param event
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
|
||||||
Log.d(TAG, "dispatchKeyEvent:" + event.getKeyCode());
|
|
||||||
|
|
||||||
if ( event.getKeyCode() == KeyEvent.KEYCODE_F8
|
|
||||||
&& event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
||||||
openSettingsActivity();
|
|
||||||
return true; // 不再向下分发
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.dispatchKeyEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initListener() {
|
|
||||||
binding.settingsButton.setOnClickListener(view -> openSettingsActivity());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initSettingsLauncher() {
|
|
||||||
settingsLauncher = registerForActivityResult(
|
|
||||||
new ActivityResultContracts.StartActivityForResult(),
|
|
||||||
result -> {}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initWebView() {
|
|
||||||
WebSettings webSettings = binding.webview.getSettings();
|
|
||||||
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
|
|
||||||
webSettings.setJavaScriptEnabled(true);
|
|
||||||
|
|
||||||
// 禁用缩放相关设置
|
|
||||||
webSettings.setTextZoom(100);
|
|
||||||
webSettings.setSupportZoom(false); // 禁止缩放
|
|
||||||
webSettings.setBuiltInZoomControls(false);
|
|
||||||
|
|
||||||
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.isForMainFrame()) {
|
|
||||||
hasError = true;
|
|
||||||
handleLoadError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceivedHttpError(WebView view, WebResourceRequest request,
|
|
||||||
WebResourceResponse errorResponse) {
|
|
||||||
super.onReceivedHttpError(view, request, errorResponse);
|
|
||||||
if (request.isForMainFrame() && errorResponse.getStatusCode() >= 400) {
|
|
||||||
hasError = true;
|
|
||||||
handleLoadError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPageFinished(WebView view, String url) {
|
|
||||||
super.onPageFinished(view, url);
|
|
||||||
if (!hasError) {
|
|
||||||
retryCount = 0;
|
|
||||||
Log.d("WebView", "加载OK");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initConfigLoader() {
|
|
||||||
configLoadRunnable = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (!TextUtils.isEmpty(configUrl)) {
|
|
||||||
Log.d(TAG, "Periodically loading config...");
|
|
||||||
if (configReader != null) {
|
|
||||||
configReader.loadConfig(configUrl);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "configUrl is empty, skipping config load.");
|
|
||||||
}
|
|
||||||
configLoadHandler.postDelayed(this, CONFIG_LOAD_INTERVAL);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (configReader == null) {
|
|
||||||
configReader = new ConfigReader(this);
|
|
||||||
configReader.setOnConfigLoadListener(new ConfigReader.OnConfigLoadListener() {
|
|
||||||
@Override
|
|
||||||
public void onConfigLoaded() {
|
|
||||||
Log.i(TAG, "Config loaded successfully. Refreshing alarms.");
|
|
||||||
String serialConfig = configReader.getSavedSerialConfig();
|
|
||||||
refreshAlarms(serialConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConfigLoadFailed(String error) {
|
|
||||||
Log.e(TAG, "Config load failed: " + error + ". Refreshing alarms with local config.");
|
|
||||||
String serialConfig = configReader.getSavedSerialConfig();
|
|
||||||
refreshAlarms(serialConfig);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshAlarms(String serialConfigJson) {
|
|
||||||
if (serialConfigJson != null && serialConfigJson.equals(lastAppliedSerialConfig)) {
|
|
||||||
Log.d(TAG, "Serial config has not changed. Skipping alarm refresh.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
|
|
||||||
|
|
||||||
for (int i = 0; i < MAX_COMMAND_ALARMS; i++) {
|
|
||||||
Intent intent = new Intent(this, CommandBroadcastReceiver.class);
|
|
||||||
intent.setAction(CommandBroadcastReceiver.ACTION_SEND_COMMAND);
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, i, intent, PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE);
|
|
||||||
if (pendingIntent != null) {
|
|
||||||
alarmManager.cancel(pendingIntent);
|
|
||||||
pendingIntent.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
for (int i = 0; i < commands.length() && i < MAX_COMMAND_ALARMS; i++) {
|
|
||||||
JSONObject command = commands.getJSONObject(i);
|
|
||||||
String time = command.getString("Time");
|
|
||||||
String hex = command.getString("Hex");
|
|
||||||
|
|
||||||
String[] timeParts = time.split(":");
|
|
||||||
int hour = Integer.parseInt(timeParts[0]);
|
|
||||||
int minute = Integer.parseInt(timeParts[1]);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
Calendar calendar = Calendar.getInstance();
|
|
||||||
calendar.setTimeInMillis(System.currentTimeMillis());
|
|
||||||
calendar.set(Calendar.HOUR_OF_DAY, hour);
|
|
||||||
calendar.set(Calendar.MINUTE, minute);
|
|
||||||
calendar.set(Calendar.SECOND, 0);
|
|
||||||
|
|
||||||
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
|
|
||||||
calendar.add(Calendar.DAY_OF_YEAR, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
} 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");
|
|
||||||
} 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
// alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pendingIntent);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastAppliedSerialConfig = serialConfigJson;
|
|
||||||
Log.d(TAG, "Alarms refreshed and config backed up.");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Failed to parse or schedule commands", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openSettingsActivity() {
|
|
||||||
Intent intent = new Intent(this, SettingsActivity.class);
|
|
||||||
settingsLauncher.launch(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showToast(String message) {
|
|
||||||
runOnUiThread(() -> Toast.makeText(mContext,
|
|
||||||
message,
|
|
||||||
Toast.LENGTH_SHORT).show());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
package cn.ykbox.dashboard.activity;
|
||||||
|
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.webkit.WebResourceError;
|
||||||
|
import android.webkit.WebResourceRequest;
|
||||||
|
import android.webkit.WebResourceResponse;
|
||||||
|
import android.webkit.WebSettings;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import android.webkit.WebViewClient;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.kongzue.dialogx.dialogs.TipDialog;
|
||||||
|
import com.kongzue.dialogx.dialogs.WaitDialog;
|
||||||
|
|
||||||
|
import cn.ykbox.dashboard.alarm.PowerAlarmManager;
|
||||||
|
import cn.ykbox.dashboard.databinding.ActivityDashboardBinding;
|
||||||
|
import cn.ykbox.dashboard.perferences.PreferenceConfiguration;
|
||||||
|
import cn.ykbox.dashboard.serial.SerialPortDetector;
|
||||||
|
|
||||||
|
public class DashboardActivity extends FullscreenActivity {
|
||||||
|
private final static String TAG = "DashboardActivity";
|
||||||
|
private static final boolean AUTO_HIDE = true;
|
||||||
|
private static final int AUTO_HIDE_DELAY_MILLIS = 3000;
|
||||||
|
private String mainUrl;
|
||||||
|
private long retryCount = 0;
|
||||||
|
private static final long MAX_RETRY = Long.MAX_VALUE; // 最大重试次数
|
||||||
|
private static final int RETRY_DELAY = 60000; // 1分钟 = 60000毫秒
|
||||||
|
|
||||||
|
private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener;
|
||||||
|
|
||||||
|
private PowerAlarmManager powerAlarmManager;
|
||||||
|
|
||||||
|
private ActivityDashboardBinding 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:
|
||||||
|
if (AUTO_HIDE) {
|
||||||
|
delayedHide(AUTO_HIDE_DELAY_MILLIS);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
view.performClick();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
binding = ActivityDashboardBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
|
super.setViews(binding.webview, binding.fullscreenContentControls);
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
initSerialPort();
|
||||||
|
initSettingsLauncher();
|
||||||
|
initListener();
|
||||||
|
initWebView();
|
||||||
|
|
||||||
|
// 获取配置 URL
|
||||||
|
SharedPreferences pre = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
String urlPrefix = pre.getString("k_url_prefix", "http://172.18.22.211:8002/Dashboard");
|
||||||
|
String configUrl = urlPrefix + "/data/config.json";
|
||||||
|
|
||||||
|
// 初始化 PowerAlarmManager
|
||||||
|
powerAlarmManager = new PowerAlarmManager(this, configUrl);
|
||||||
|
powerAlarmManager.start();
|
||||||
|
|
||||||
|
initPreferenceChangeListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
SharedPreferences pre = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
String urlPrefix = pre.getString("k_url_prefix", "http://172.18.22.211:8002/Dashboard");
|
||||||
|
String urlPath = pre.getString("k_url_path", "/index.html");
|
||||||
|
mainUrl = urlPrefix + urlPath;
|
||||||
|
|
||||||
|
Log.i(TAG, "Main: " + mainUrl);
|
||||||
|
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(5000); // 等待 5 秒
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.e(TAG, "Sleep interrupted", e);
|
||||||
|
}
|
||||||
|
int loop = PreferenceConfiguration.getSerialCmdLoop(this);
|
||||||
|
detector.sendPowerOnCommand(loop);
|
||||||
|
}).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("正在测试串口路径: " + portPath + "\n" + current + "/" + total);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetectionSuccess(String portPath) {
|
||||||
|
Log.i(TAG, "Detection success: " + portPath);
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
if (progressDialog != null) {
|
||||||
|
progressDialog.dismiss();
|
||||||
|
}
|
||||||
|
Toast.makeText(DashboardActivity.this,
|
||||||
|
"串口检测成功!\n路径: " + portPath,
|
||||||
|
Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
|
PreferenceConfiguration.setSerialPortPath(DashboardActivity.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(DashboardActivity.this,
|
||||||
|
"未找到有效的串口设备,请检查设备连接后重试。",
|
||||||
|
Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadUrlWithRetry() {
|
||||||
|
Log.i("WebView", "Loading " + mainUrl);
|
||||||
|
binding.webview.loadUrl(mainUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleLoadError() {
|
||||||
|
if (retryCount < MAX_RETRY) {
|
||||||
|
retryCount++;
|
||||||
|
Log.d("WebView", "加载失败,将在1分钟后重试 (第" + retryCount + "次重试)");
|
||||||
|
|
||||||
|
WaitDialog.dismiss();
|
||||||
|
// 使用Handler延迟执行重试
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Log.d("WebView", "开始重试加载...");
|
||||||
|
loadUrlWithRetry();
|
||||||
|
}
|
||||||
|
}, RETRY_DELAY);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Log.e("WebView", "重试次数已达上限,加载失败");
|
||||||
|
// 这里可以显示错误页面或提示用户
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
TipDialog.show("重新加载失败次数已达上限,请联系管理员。", WaitDialog.TYPE.ERROR, -1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
// PowerAlarmManager 内部已处理配置加载的暂停
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (preferenceChangeListener != null) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener);
|
||||||
|
}
|
||||||
|
if (powerAlarmManager != null) {
|
||||||
|
powerAlarmManager.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听按键,因为 WebView 可能会拦截功能键,这里不使用 onKeyDown
|
||||||
|
* @param event
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||||
|
Log.d(TAG, "dispatchKeyEvent:" + event.getKeyCode());
|
||||||
|
|
||||||
|
if ( event.getKeyCode() == KeyEvent.KEYCODE_F8
|
||||||
|
&& event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||||
|
openSettingsActivity();
|
||||||
|
return true; // 不再向下分发
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.dispatchKeyEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initListener() {
|
||||||
|
binding.settingsButton.setOnClickListener(view -> openSettingsActivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initSettingsLauncher() {
|
||||||
|
settingsLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initWebView() {
|
||||||
|
WebSettings webSettings = binding.webview.getSettings();
|
||||||
|
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
|
||||||
|
webSettings.setJavaScriptEnabled(true);
|
||||||
|
webSettings.setDomStorageEnabled(true);
|
||||||
|
// 允许媒体自动播放 (Android 8+ 必须)
|
||||||
|
webSettings.setMediaPlaybackRequiresUserGesture(false);
|
||||||
|
|
||||||
|
// 禁用缩放相关设置
|
||||||
|
webSettings.setTextZoom(100);
|
||||||
|
webSettings.setSupportZoom(false); // 禁止缩放
|
||||||
|
webSettings.setBuiltInZoomControls(false);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if(retryCount <= 0)
|
||||||
|
WaitDialog.show("正在加载...");
|
||||||
|
else
|
||||||
|
WaitDialog.show( "第 " + retryCount + " 次重试... ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceivedError(WebView view, WebResourceRequest request,
|
||||||
|
WebResourceError error) {
|
||||||
|
super.onReceivedError(view, request, error);
|
||||||
|
|
||||||
|
if (request.isForMainFrame()) {
|
||||||
|
hasError = true;
|
||||||
|
handleLoadError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceivedHttpError(WebView view, WebResourceRequest request,
|
||||||
|
WebResourceResponse errorResponse) {
|
||||||
|
super.onReceivedHttpError(view, request, errorResponse);
|
||||||
|
if (request.isForMainFrame() && errorResponse.getStatusCode() >= 400) {
|
||||||
|
hasError = true;
|
||||||
|
handleLoadError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(WebView view, String url) {
|
||||||
|
super.onPageFinished(view, url);
|
||||||
|
if (!hasError) {
|
||||||
|
retryCount = 0;
|
||||||
|
WaitDialog.dismiss();
|
||||||
|
Log.d("WebView", "加载OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initPreferenceChangeListener() {
|
||||||
|
preferenceChangeListener = (sharedPreferences, key) -> {
|
||||||
|
if (key.equals("k_power_control_mode") ||
|
||||||
|
key.equals("k_power_on_time") ||
|
||||||
|
key.equals("k_power_off_time")) {
|
||||||
|
Log.d(TAG, "Power control preference changed: " + key);
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
if (key.equals("k_power_control_mode")) {
|
||||||
|
int mode = PreferenceConfiguration.getPowerControlMode(DashboardActivity.this);
|
||||||
|
powerAlarmManager.onModeChanged(mode);
|
||||||
|
} else {
|
||||||
|
// 本地时间变更
|
||||||
|
powerAlarmManager.onLocalTimeChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.registerOnSharedPreferenceChangeListener(preferenceChangeListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openSettingsActivity() {
|
||||||
|
Intent intent = new Intent(this, SettingsActivity.class);
|
||||||
|
settingsLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,10 +22,14 @@ public class FullscreenActivity extends AppCompatActivity {
|
|||||||
private static final int UI_ANIMATION_DELAY = 300;
|
private static final int UI_ANIMATION_DELAY = 300;
|
||||||
private final Handler mHideHandler = new Handler(Looper.myLooper());
|
private final Handler mHideHandler = new Handler(Looper.myLooper());
|
||||||
private View mContentView;
|
private View mContentView;
|
||||||
|
private View mControlsView;
|
||||||
private final Runnable mHidePart2Runnable = new Runnable() {
|
private final Runnable mHidePart2Runnable = new Runnable() {
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
if(mContentView == null)
|
||||||
|
return;
|
||||||
|
|
||||||
// Delayed removal of status and navigation bar
|
// Delayed removal of status and navigation bar
|
||||||
if (Build.VERSION.SDK_INT >= 30) {
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
mContentView.getWindowInsetsController().hide(
|
mContentView.getWindowInsetsController().hide(
|
||||||
@@ -43,7 +47,6 @@ public class FullscreenActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private View mControlsView;
|
|
||||||
private final Runnable mShowPart2Runnable = new Runnable() {
|
private final Runnable mShowPart2Runnable = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -52,6 +55,8 @@ public class FullscreenActivity extends AppCompatActivity {
|
|||||||
if (actionBar != null) {
|
if (actionBar != null) {
|
||||||
actionBar.show();
|
actionBar.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(mControlsView != null)
|
||||||
mControlsView.setVisibility(View.VISIBLE);
|
mControlsView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -76,9 +81,8 @@ public class FullscreenActivity extends AppCompatActivity {
|
|||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
|
|
||||||
mContentView.setOnTouchListener(new View.OnTouchListener() {
|
if(mContentView != null) {
|
||||||
@Override
|
mContentView.setOnTouchListener((view, motionEvent) -> {
|
||||||
public boolean onTouch(View view, MotionEvent motionEvent) {
|
|
||||||
switch (motionEvent.getAction()) {
|
switch (motionEvent.getAction()) {
|
||||||
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_DOWN:
|
||||||
toggle();
|
toggle();
|
||||||
@@ -90,9 +94,9 @@ public class FullscreenActivity extends AppCompatActivity {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostCreate(Bundle savedInstanceState) {
|
protected void onPostCreate(Bundle savedInstanceState) {
|
||||||
@@ -120,6 +124,7 @@ public class FullscreenActivity extends AppCompatActivity {
|
|||||||
if (actionBar != null) {
|
if (actionBar != null) {
|
||||||
actionBar.hide();
|
actionBar.hide();
|
||||||
}
|
}
|
||||||
|
if(mControlsView != null)
|
||||||
mControlsView.setVisibility(View.GONE);
|
mControlsView.setVisibility(View.GONE);
|
||||||
mVisible = false;
|
mVisible = false;
|
||||||
|
|
||||||
@@ -130,6 +135,7 @@ public class FullscreenActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
private void show() {
|
private void show() {
|
||||||
// Show the system bar
|
// Show the system bar
|
||||||
|
if(mContentView != null) {
|
||||||
if (Build.VERSION.SDK_INT >= 30) {
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
mContentView.getWindowInsetsController().show(
|
mContentView.getWindowInsetsController().show(
|
||||||
WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
|
WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
|
||||||
@@ -137,6 +143,7 @@ public class FullscreenActivity extends AppCompatActivity {
|
|||||||
mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
mVisible = true;
|
mVisible = true;
|
||||||
|
|
||||||
// Schedule a runnable to display UI elements after a delay
|
// Schedule a runnable to display UI elements after a delay
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
package cn.ykbox.dashboard.activity;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import com.blankj.utilcode.util.ScreenUtils;
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import com.youth.banner.adapter.BannerImageAdapter;
|
|
||||||
import com.youth.banner.holder.BannerImageHolder;
|
|
||||||
import com.youth.banner.indicator.CircleIndicator;
|
|
||||||
|
|
||||||
import cn.ykbox.dashboard.data.BannerBean;
|
|
||||||
import cn.ykbox.dashboard.databinding.ActivityGalleryBinding;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An example full-screen activity that shows and hides the system UI (i.e.
|
|
||||||
* status bar and navigation/system bar) with user interaction.
|
|
||||||
*/
|
|
||||||
public class GalleryActivity extends BaseActivity {
|
|
||||||
|
|
||||||
private ActivityGalleryBinding binding;
|
|
||||||
private Context mContext;
|
|
||||||
|
|
||||||
private Handler mHandler = new Handler();
|
|
||||||
|
|
||||||
private final static int CON_COUNTS = 6;// 连续点击次数
|
|
||||||
private final static long CON_DURATION = 1500;// 连续点击最小间隔时间
|
|
||||||
private static long[] CON_Hits = new long[CON_COUNTS];
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
mContext = this;
|
|
||||||
|
|
||||||
binding = ActivityGalleryBinding.inflate(getLayoutInflater());
|
|
||||||
setContentView(binding.getRoot());
|
|
||||||
binding.banner.setAdapter(new BannerImageAdapter<BannerBean>(BannerBean.getTestData()) {
|
|
||||||
@Override
|
|
||||||
public void onBindView(BannerImageHolder holder, BannerBean data, int position, int size) {
|
|
||||||
Log.d("fullshow", "load" + data.imageRes);
|
|
||||||
Glide.with(holder.itemView)
|
|
||||||
.load(data.imageRes)
|
|
||||||
.into(holder.imageView);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.addBannerLifecycleObserver(this)//添加生命周期观察者
|
|
||||||
.setIndicator(new CircleIndicator(this))
|
|
||||||
.setLoopTime(5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
|
||||||
int h = ScreenUtils.getScreenHeight();
|
|
||||||
|
|
||||||
if(ev.getActionMasked() == MotionEvent.ACTION_DOWN && ev.getX() < 100 && ev.getY() > h-100)
|
|
||||||
finishByHits();
|
|
||||||
return super.dispatchTouchEvent(ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void finishByHits() {
|
|
||||||
//每次点击时,数组向前移动一位
|
|
||||||
System.arraycopy(CON_Hits, 1, CON_Hits, 0, CON_Hits.length - 1);
|
|
||||||
//为数组最后一位赋值
|
|
||||||
CON_Hits[CON_Hits.length - 1] = SystemClock.uptimeMillis();
|
|
||||||
if (CON_Hits[0] >= (SystemClock.uptimeMillis() - CON_DURATION)) {
|
|
||||||
CON_Hits = new long[CON_COUNTS];//重新初始化数组
|
|
||||||
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
package cn.ykbox.dashboard.activity;
|
package cn.ykbox.dashboard.activity;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.preference.EditTextPreference;
|
||||||
|
import androidx.preference.ListPreference;
|
||||||
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
|
import com.blankj.utilcode.util.ToastUtils;
|
||||||
|
|
||||||
import cn.ykbox.dashboard.R;
|
import cn.ykbox.dashboard.R;
|
||||||
|
import cn.ykbox.dashboard.perferences.PreferenceConfiguration;
|
||||||
|
import cn.ykbox.dashboard.serial.SerialPortDetector;
|
||||||
|
|
||||||
public class SettingsActivity extends AppCompatActivity {
|
public class SettingsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@@ -26,10 +36,117 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragmentCompat {
|
public static class SettingsFragment extends PreferenceFragmentCompat
|
||||||
|
implements Preference.OnPreferenceClickListener {
|
||||||
@Override
|
@Override
|
||||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||||
|
|
||||||
|
ListPreference powerControlModePref = findPreference("k_power_control_mode");
|
||||||
|
EditTextPreference powerOnTimePref = findPreference("k_power_on_time");
|
||||||
|
EditTextPreference powerOffTimePref = findPreference("k_power_off_time");
|
||||||
|
|
||||||
|
Preference clearDevicePref = findPreference("k_clear_device");
|
||||||
|
if (clearDevicePref != null) {
|
||||||
|
clearDevicePref.setOnPreferenceClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Preference testPowerOnPref = findPreference("k_test_power_on");
|
||||||
|
if (testPowerOnPref != null) {
|
||||||
|
testPowerOnPref.setOnPreferenceClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Preference testPowerOffPref = findPreference("k_test_power_off");
|
||||||
|
if (testPowerOffPref != null) {
|
||||||
|
testPowerOffPref.setOnPreferenceClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (powerControlModePref != null) {
|
||||||
|
powerControlModePref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
int mode = Integer.parseInt((String) newValue);
|
||||||
|
boolean isLocalMode = (mode == 2);
|
||||||
|
if (powerOnTimePref != null) {
|
||||||
|
powerOnTimePref.setEnabled(isLocalMode);
|
||||||
|
}
|
||||||
|
if (powerOffTimePref != null) {
|
||||||
|
powerOffTimePref.setEnabled(isLocalMode);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int mode = PreferenceConfiguration.getPowerControlMode(getContext());
|
||||||
|
boolean isLocalMode = (mode == 2);
|
||||||
|
if (powerOnTimePref != null) {
|
||||||
|
powerOnTimePref.setEnabled(isLocalMode);
|
||||||
|
}
|
||||||
|
if (powerOffTimePref != null) {
|
||||||
|
powerOffTimePref.setEnabled(isLocalMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPreferenceClick(@NonNull Preference preference) {
|
||||||
|
String key = preference.getKey();
|
||||||
|
if(key.equals("k_clear_device")) {
|
||||||
|
clearSerialPath();
|
||||||
|
return true;
|
||||||
|
} else if(key.equals("k_test_power_on")) {
|
||||||
|
testPowerOn();
|
||||||
|
return true;
|
||||||
|
} else if(key.equals("k_test_power_off")) {
|
||||||
|
testPowerOff();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearSerialPath() {
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle("确认清除")
|
||||||
|
.setMessage("确定要清除当前串口设备路径吗?")
|
||||||
|
.setPositiveButton("确定", (dialog, which) -> {
|
||||||
|
// 清空
|
||||||
|
PreferenceConfiguration.setSerialPortPath(requireContext(), "");
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
Preference serialPortPathPref = findPreference("k_serial_port_path");
|
||||||
|
if (serialPortPathPref != null) {
|
||||||
|
String newValue = PreferenceConfiguration.getSerialPortPath(requireContext());
|
||||||
|
((EditTextPreference) serialPortPathPref).setText(newValue);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testPowerOn() {
|
||||||
|
String portPath = PreferenceConfiguration.getSerialPortPath(getContext());
|
||||||
|
int baudRate = PreferenceConfiguration.getSerialPortBaudRate(getContext());
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(portPath)) {
|
||||||
|
ToastUtils.showShort("串口路径未设置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SerialPortDetector detector = new SerialPortDetector(portPath, baudRate);
|
||||||
|
detector.sendPowerOnCommand(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void testPowerOff() {
|
||||||
|
String portPath = PreferenceConfiguration.getSerialPortPath(getContext());
|
||||||
|
int baudRate = PreferenceConfiguration.getSerialPortBaudRate(getContext());
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(portPath)) {
|
||||||
|
ToastUtils.showShort("串口路径未设置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SerialPortDetector detector = new SerialPortDetector(portPath, baudRate);
|
||||||
|
detector.sendPowerOffCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,38 +5,29 @@ import android.content.Intent;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
||||||
import androidx.activity.EdgeToEdge;
|
import cn.ykbox.dashboard.databinding.ActivityDashboardBinding;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import cn.ykbox.dashboard.databinding.ActivityStartBinding;
|
||||||
import androidx.core.graphics.Insets;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
|
||||||
|
|
||||||
import cn.ykbox.dashboard.R;
|
import cn.ykbox.dashboard.R;
|
||||||
import cn.ykbox.dashboard.serial.SerialControlDevices;
|
|
||||||
|
|
||||||
public class StartActivity extends AppCompatActivity {
|
public class StartActivity extends FullscreenActivity {
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
|
private ActivityStartBinding binding;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
binding = ActivityStartBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
|
super.setViews(binding.flContent, null);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
// 开机后手动发送一次开启电源指令
|
|
||||||
SerialControlDevices.sendCommand("/dev/ttyS2", 9600, "ACEAB400ED");
|
|
||||||
|
|
||||||
mContext = this;
|
mContext = this;
|
||||||
EdgeToEdge.enable(this);
|
|
||||||
setContentView(R.layout.activity_start);
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
|
|
||||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
|
||||||
return insets;
|
|
||||||
});
|
|
||||||
|
|
||||||
new Handler().postDelayed(new Runnable() {
|
new Handler().postDelayed(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Intent myIntent = new Intent(mContext, BuildingDashboardActivity.class);
|
Intent myIntent = new Intent(mContext, DashboardActivity.class);
|
||||||
myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
mContext.startActivity(myIntent);
|
mContext.startActivity(myIntent);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
package cn.ykbox.dashboard.alarm;
|
||||||
|
|
||||||
|
import android.app.AlarmManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
|
||||||
|
import cn.ykbox.dashboard.ConfigReader;
|
||||||
|
import cn.ykbox.dashboard.perferences.PreferenceConfiguration;
|
||||||
|
import cn.ykbox.dashboard.receiver.CommandBroadcastReceiver;
|
||||||
|
import cn.ykbox.dashboard.serial.SerialPortDetector;
|
||||||
|
|
||||||
|
public class PowerAlarmManager {
|
||||||
|
private static final String TAG = "PowerAlarmManager";
|
||||||
|
private static final int MAX_COMMAND_ALARMS = 20;
|
||||||
|
private static final int ALARM_ID_POWER_ON = 0;
|
||||||
|
private static final int ALARM_ID_POWER_OFF = 1;
|
||||||
|
private static final long CONFIG_LOAD_INTERVAL = 10 * 60 * 1000; // 10 minutes
|
||||||
|
|
||||||
|
private static final int POWER_MODE_NONE = 0;
|
||||||
|
private static final int POWER_MODE_REMOTE = 1;
|
||||||
|
private static final int POWER_MODE_LOCAL = 2;
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private String lastAppliedSerialConfig = null;
|
||||||
|
private ConfigReader configReader;
|
||||||
|
private String configUrl;
|
||||||
|
private Handler configLoadHandler;
|
||||||
|
private Runnable configLoadRunnable;
|
||||||
|
|
||||||
|
public PowerAlarmManager(Context context, String configUrl) {
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
|
this.configUrl = configUrl;
|
||||||
|
this.configLoadHandler = new Handler(Looper.getMainLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动管理器,根据当前模式初始化
|
||||||
|
*/
|
||||||
|
public void start() {
|
||||||
|
int mode = PreferenceConfiguration.getPowerControlMode(context);
|
||||||
|
Log.d(TAG, "PowerAlarmManager starting, mode: " + mode);
|
||||||
|
|
||||||
|
refreshAlarms();
|
||||||
|
|
||||||
|
if (mode == POWER_MODE_REMOTE) {
|
||||||
|
// 服务器配置模式,初始化 ConfigLoader
|
||||||
|
initConfigLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止管理器,释放资源
|
||||||
|
*/
|
||||||
|
public void stop() {
|
||||||
|
if (configLoadRunnable != null) {
|
||||||
|
configLoadHandler.removeCallbacks(configLoadRunnable);
|
||||||
|
}
|
||||||
|
if (configReader != null) {
|
||||||
|
configReader.release();
|
||||||
|
configReader = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模式变更时调用
|
||||||
|
*/
|
||||||
|
public void onModeChanged(int newMode) {
|
||||||
|
Log.d(TAG, "Mode changed to: " + newMode);
|
||||||
|
|
||||||
|
if (newMode == POWER_MODE_REMOTE) {
|
||||||
|
// 切换到服务器配置模式
|
||||||
|
if (configReader == null) {
|
||||||
|
initConfigLoader();
|
||||||
|
}
|
||||||
|
clearServerConfigCache();
|
||||||
|
if (configLoadRunnable != null) {
|
||||||
|
configLoadHandler.post(configLoadRunnable);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 切换到其他模式,停止配置加载
|
||||||
|
if (configLoadRunnable != null) {
|
||||||
|
configLoadHandler.removeCallbacks(configLoadRunnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshAlarms();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地时间变更时调用
|
||||||
|
*/
|
||||||
|
public void onLocalTimeChanged() {
|
||||||
|
int mode = PreferenceConfiguration.getPowerControlMode(context);
|
||||||
|
if (mode == POWER_MODE_LOCAL) {
|
||||||
|
// 本地控制模式,刷新闹钟
|
||||||
|
refreshAlarms();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前模式刷新闹钟
|
||||||
|
*/
|
||||||
|
public void refreshAlarms() {
|
||||||
|
int mode = PreferenceConfiguration.getPowerControlMode(context);
|
||||||
|
Log.d(TAG, "Refreshing alarms, mode: " + mode);
|
||||||
|
|
||||||
|
cancelAllAlarms();
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case POWER_MODE_NONE: // 不控制
|
||||||
|
Log.d(TAG, "Power control: No control mode. No alarms set.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case POWER_MODE_REMOTE: // 根据服务器配置
|
||||||
|
Log.d(TAG, "Power control: Server config mode.");
|
||||||
|
// 使用已加载的配置或请求立即加载
|
||||||
|
if (configReader != null) {
|
||||||
|
String serialConfig = configReader.getSavedSerialConfig();
|
||||||
|
refreshWithServerConfig(serialConfig, false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case POWER_MODE_LOCAL: // 本地控制
|
||||||
|
Log.d(TAG, "Power control: Local control mode.");
|
||||||
|
setupLocalControlAlarms();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Log.w(TAG, "Unknown power control mode: " + mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用服务器配置刷新闹钟
|
||||||
|
* @param serialConfigJson 服务器配置 JSON
|
||||||
|
* @param forceRefresh 是否强制刷新(模式切换时为 true)
|
||||||
|
*/
|
||||||
|
private void refreshWithServerConfig(String serialConfigJson, boolean forceRefresh) {
|
||||||
|
cancelAllAlarms();
|
||||||
|
|
||||||
|
if (serialConfigJson == null) {
|
||||||
|
Log.w(TAG, "Server config is null.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forceRefresh && serialConfigJson.equals(lastAppliedSerialConfig)) {
|
||||||
|
Log.d(TAG, "Server config has not changed. Skipping alarm refresh.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupServerConfigAlarms(serialConfigJson);
|
||||||
|
this.lastAppliedSerialConfig = serialConfigJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消所有闹钟
|
||||||
|
*/
|
||||||
|
private void cancelAllAlarms() {
|
||||||
|
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
for (int i = 0; i < MAX_COMMAND_ALARMS; i++) {
|
||||||
|
Intent intent = new Intent(context, CommandBroadcastReceiver.class);
|
||||||
|
intent.setAction(CommandBroadcastReceiver.ACTION_SEND_COMMAND);
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context, i, intent,
|
||||||
|
PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE
|
||||||
|
);
|
||||||
|
if (pendingIntent != null) {
|
||||||
|
alarmManager.cancel(pendingIntent);
|
||||||
|
pendingIntent.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置服务器配置的闹钟
|
||||||
|
*/
|
||||||
|
private void setupServerConfigAlarms(String serialConfigJson) {
|
||||||
|
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject serialConfig = new JSONObject(serialConfigJson);
|
||||||
|
|
||||||
|
if (serialConfig.has("Commands")) {
|
||||||
|
JSONArray commands = serialConfig.getJSONArray("Commands");
|
||||||
|
for (int i = 0; i < commands.length() && i < MAX_COMMAND_ALARMS; i++) {
|
||||||
|
JSONObject command = commands.getJSONObject(i);
|
||||||
|
String time = command.getString("Time");
|
||||||
|
String hex = command.getString("Hex");
|
||||||
|
|
||||||
|
String[] timeParts = time.split(":");
|
||||||
|
int hour = Integer.parseInt(timeParts[0]);
|
||||||
|
int minute = Integer.parseInt(timeParts[1]);
|
||||||
|
|
||||||
|
setupAlarm(alarmManager, i, time, hex, hour, minute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Server config alarms set successfully.");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to parse server config", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置本地控制的闹钟
|
||||||
|
*/
|
||||||
|
private void setupLocalControlAlarms() {
|
||||||
|
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
String onTime = PreferenceConfiguration.getPowerOnTime(context);
|
||||||
|
String offTime = PreferenceConfiguration.getPowerOffTime(context);
|
||||||
|
|
||||||
|
int[] onParts = parseTimeParts(onTime);
|
||||||
|
int[] offParts = parseTimeParts(offTime);
|
||||||
|
|
||||||
|
if (onParts != null) {
|
||||||
|
setupAlarm(alarmManager, ALARM_ID_POWER_ON, onTime,
|
||||||
|
SerialPortDetector.TEST_CMD_ON,
|
||||||
|
onParts[0], onParts[1]);
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Invalid power-on time, alarm skipped: " + onTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offParts != null) {
|
||||||
|
setupAlarm(alarmManager, ALARM_ID_POWER_OFF, offTime,
|
||||||
|
SerialPortDetector.TEST_CMD_OFF,
|
||||||
|
offParts[0], offParts[1]);
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Invalid power-off time, alarm skipped: " + offTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Local control alarms set: ON=" + onTime + ", OFF=" + offTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 "HH:mm" 格式时间字符串
|
||||||
|
* @return int[]{hour, minute},解析失败返回 null
|
||||||
|
*/
|
||||||
|
private int[] parseTimeParts(String time) {
|
||||||
|
if (time == null || time.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String[] parts = time.split(":");
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int hour = Integer.parseInt(parts[0].trim());
|
||||||
|
int minute = Integer.parseInt(parts[1].trim());
|
||||||
|
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new int[]{hour, minute};
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
Log.e(TAG, "Failed to parse time: " + time, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置单个闹钟
|
||||||
|
*/
|
||||||
|
private void setupAlarm(AlarmManager alarmManager, int alarmId, String timeStr,
|
||||||
|
String cmdHex, int hour, int minute) {
|
||||||
|
Intent intent = new Intent(context, CommandBroadcastReceiver.class);
|
||||||
|
intent.setAction(CommandBroadcastReceiver.ACTION_SEND_COMMAND);
|
||||||
|
intent.putExtra(CommandBroadcastReceiver.EXTRA_COMMAND_HEX, cmdHex);
|
||||||
|
// 传递时间信息和 alarmId,用于重新设置下一个闹钟
|
||||||
|
intent.putExtra(CommandBroadcastReceiver.EXTRA_HOUR, hour);
|
||||||
|
intent.putExtra(CommandBroadcastReceiver.EXTRA_MINUTE, minute);
|
||||||
|
intent.putExtra(CommandBroadcastReceiver.EXTRA_ALARM_ID, alarmId);
|
||||||
|
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context, alarmId, intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||||
|
);
|
||||||
|
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.setTimeInMillis(System.currentTimeMillis());
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, hour);
|
||||||
|
calendar.set(Calendar.MINUTE, minute);
|
||||||
|
calendar.set(Calendar.SECOND, 0);
|
||||||
|
|
||||||
|
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
|
||||||
|
calendar.add(Calendar.DAY_OF_YEAR, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
alarmManager.setExactAndAllowWhileIdle(
|
||||||
|
AlarmManager.RTC_WAKEUP,
|
||||||
|
calendar.getTimeInMillis(), pendingIntent);
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
alarmManager.setExact(
|
||||||
|
AlarmManager.RTC_WAKEUP,
|
||||||
|
calendar.getTimeInMillis(), pendingIntent);
|
||||||
|
} else {
|
||||||
|
alarmManager.set(
|
||||||
|
AlarmManager.RTC_WAKEUP,
|
||||||
|
calendar.getTimeInMillis(), pendingIntent);
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Alarm set: " + timeStr + " with command " + cmdHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 ConfigLoader
|
||||||
|
*/
|
||||||
|
private void initConfigLoader() {
|
||||||
|
if (configReader != null) {
|
||||||
|
return; // 已初始化
|
||||||
|
}
|
||||||
|
|
||||||
|
configLoadRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (!TextUtils.isEmpty(configUrl)) {
|
||||||
|
Log.d(TAG, "Periodically loading config...");
|
||||||
|
configReader.loadConfig(configUrl);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "configUrl is empty, skipping config load.");
|
||||||
|
}
|
||||||
|
configLoadHandler.postDelayed(this, CONFIG_LOAD_INTERVAL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
configReader = new ConfigReader(context);
|
||||||
|
configReader.setOnConfigLoadListener(new ConfigReader.OnConfigLoadListener() {
|
||||||
|
@Override
|
||||||
|
public void onConfigLoaded() {
|
||||||
|
Log.i(TAG, "Config loaded successfully. Refreshing alarms.");
|
||||||
|
String serialConfig = configReader.getSavedSerialConfig();
|
||||||
|
refreshWithServerConfig(serialConfig, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigLoadFailed(String error) {
|
||||||
|
Log.e(TAG, "Config load failed: " + error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 立即执行一次
|
||||||
|
configLoadRunnable.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存的服务器配置
|
||||||
|
*/
|
||||||
|
private void clearServerConfigCache() {
|
||||||
|
this.lastAppliedSerialConfig = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package cn.ykbox.dashboard.data;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import cn.ykbox.dashboard.R;
|
|
||||||
|
|
||||||
public class BannerBean {
|
|
||||||
public Integer imageRes;
|
|
||||||
public String imageUrl;
|
|
||||||
public String title;
|
|
||||||
public int viewType;
|
|
||||||
|
|
||||||
public BannerBean(Integer imageRes, String title, int viewType) {
|
|
||||||
this.imageRes = imageRes;
|
|
||||||
this.title = title;
|
|
||||||
this.viewType = viewType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BannerBean(String imageUrl, String title, int viewType) {
|
|
||||||
this.imageUrl = imageUrl;
|
|
||||||
this.title = title;
|
|
||||||
this.viewType = viewType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<BannerBean> getTestData() {
|
|
||||||
List<BannerBean> list = new ArrayList<>();
|
|
||||||
list.add(new BannerBean(R.drawable.image1, "第3代无线智能中控", 1));
|
|
||||||
list.add(new BannerBean(R.drawable.image2, "智慧班牌", 3));
|
|
||||||
list.add(new BannerBean(R.drawable.image3, "无线话筒", 3));
|
|
||||||
list.add(new BannerBean(R.drawable.image4, "可移动式智能讲桌", 1));
|
|
||||||
list.add(new BannerBean(R.drawable.image5, "TD2+智慧屏集成讲桌", 1));
|
|
||||||
list.add(new BannerBean(R.drawable.image6, "TD5+智慧屏集成讲台", 3));
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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";
|
||||||
|
private static final String KEY_POWER_CONTROL_MODE = "k_power_control_mode";
|
||||||
|
private static final String KEY_POWER_ON_TIME = "k_power_on_time";
|
||||||
|
private static final String KEY_POWER_OFF_TIME = "k_power_off_time";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getSerialCmdLoop(Context context) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getPowerControlMode(Context context) {
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
String mode = prefs.getString(KEY_POWER_CONTROL_MODE, "1");
|
||||||
|
return Integer.parseInt(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getPowerOnTime(Context context) {
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
return prefs.getString(KEY_POWER_ON_TIME, "07:30");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getPowerOffTime(Context context) {
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
return prefs.getString(KEY_POWER_OFF_TIME, "20:30");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,100 @@
|
|||||||
package cn.ykbox.dashboard.receiver;
|
package cn.ykbox.dashboard.receiver;
|
||||||
|
|
||||||
|
import android.app.AlarmManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import cn.ykbox.dashboard.serial.SerialControlDevices;
|
import java.util.Calendar;
|
||||||
|
|
||||||
|
import cn.ykbox.dashboard.perferences.PreferenceConfiguration;
|
||||||
|
import cn.ykbox.dashboard.serial.SerialPortDetector;
|
||||||
|
|
||||||
public class CommandBroadcastReceiver extends BroadcastReceiver {
|
public class CommandBroadcastReceiver extends BroadcastReceiver {
|
||||||
private static final String TAG = "CommandReceiver";
|
private static final String TAG = "CommandReceiver";
|
||||||
public static final String ACTION_SEND_COMMAND = "cn.ykbox.dashboard.ACTION_SEND_COMMAND";
|
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_COMMAND_HEX = "EXTRA_COMMAND_HEX";
|
||||||
public static final String EXTRA_PORT_PATH = "EXTRA_PORT_PATH";
|
public static final String EXTRA_HOUR = "EXTRA_HOUR";
|
||||||
public static final String EXTRA_BAUD_RATE = "EXTRA_BAUD_RATE"; // 新增:波特率的 Extra Key
|
public static final String EXTRA_MINUTE = "EXTRA_MINUTE";
|
||||||
|
public static final String EXTRA_ALARM_ID = "EXTRA_ALARM_ID";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
if (intent != null && ACTION_SEND_COMMAND.equals(intent.getAction())) {
|
if (intent != null && ACTION_SEND_COMMAND.equals(intent.getAction())) {
|
||||||
String hexCommand = intent.getStringExtra(EXTRA_COMMAND_HEX);
|
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);
|
||||||
|
int loop = PreferenceConfiguration.getSerialCmdLoop(context);
|
||||||
|
|
||||||
|
if (hexCommand != null && !hexCommand.isEmpty() && !TextUtils.isEmpty(portPath)) {
|
||||||
Log.d(TAG, "Received alarm to send command '" + hexCommand + "' to port '" + portPath + "' at " + baudRate + " baud");
|
Log.d(TAG, "Received alarm to send command '" + hexCommand + "' to port '" + portPath + "' at " + baudRate + " baud");
|
||||||
// 使用新的静态方法发送指令
|
// 使用新的静态方法发送指令
|
||||||
boolean success = SerialControlDevices.sendCommand(portPath, baudRate, hexCommand);
|
boolean success = SerialPortDetector.sendCommand(portPath, baudRate, hexCommand, loop);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Log.e(TAG, "Failed to send command via broadcast receiver.");
|
Log.e(TAG, "Failed to send command via broadcast receiver.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重新设置明天的闹钟(实现每天循环)
|
||||||
|
Integer hour = intent.getIntExtra(EXTRA_HOUR, -1);
|
||||||
|
Integer minute = intent.getIntExtra(EXTRA_MINUTE, -1);
|
||||||
|
int alarmId = intent.getIntExtra(EXTRA_ALARM_ID, 0);
|
||||||
|
|
||||||
|
if (hour >= 0 && minute >= 0) {
|
||||||
|
scheduleNextAlarm(context, alarmId, hexCommand, hour, minute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度下一个闹钟(明天同一时间)
|
||||||
|
*/
|
||||||
|
private void scheduleNextAlarm(Context context, int alarmId, String cmdHex, int hour, int minute) {
|
||||||
|
new Handler(Looper.getMainLooper()).post(() -> {
|
||||||
|
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
|
||||||
|
Intent intent = new Intent(context, CommandBroadcastReceiver.class);
|
||||||
|
intent.setAction(ACTION_SEND_COMMAND);
|
||||||
|
intent.putExtra(EXTRA_COMMAND_HEX, cmdHex);
|
||||||
|
intent.putExtra(EXTRA_HOUR, hour);
|
||||||
|
intent.putExtra(EXTRA_MINUTE, minute);
|
||||||
|
intent.putExtra(EXTRA_ALARM_ID, alarmId);
|
||||||
|
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context, alarmId, intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||||
|
);
|
||||||
|
|
||||||
|
// 计算明天的触发时间
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.add(Calendar.DAY_OF_YEAR, 1);
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, hour);
|
||||||
|
calendar.set(Calendar.MINUTE, minute);
|
||||||
|
calendar.set(Calendar.SECOND, 0);
|
||||||
|
|
||||||
|
// 使用精确的一次性闹钟
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
alarmManager.setExactAndAllowWhileIdle(
|
||||||
|
AlarmManager.RTC_WAKEUP,
|
||||||
|
calendar.getTimeInMillis(), pendingIntent);
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
alarmManager.setExact(
|
||||||
|
AlarmManager.RTC_WAKEUP,
|
||||||
|
calendar.getTimeInMillis(), pendingIntent);
|
||||||
|
} else {
|
||||||
|
alarmManager.set(
|
||||||
|
AlarmManager.RTC_WAKEUP,
|
||||||
|
calendar.getTimeInMillis(), pendingIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Next alarm scheduled for " + hour + ":" + minute + " tomorrow (ID: " + alarmId + ")");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package cn.ykbox.dashboard.serial;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
import tp.xmaihh.serialport.SerialHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description: 通过串口设置班牌的功能。本类提供一个静态方法用于发送单次命令。
|
|
||||||
* @author: Hu Zhang <hu.zhang@live.com>
|
|
||||||
* @date: 2024/1/5
|
|
||||||
**/
|
|
||||||
public class SerialControlDevices {
|
|
||||||
private static final String TAG = "SerialControlDevices";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 私有构造函数,防止外部实例化此类。
|
|
||||||
*/
|
|
||||||
private SerialControlDevices() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开指定串口,发送十六进制命令,然后立即关闭串口。
|
|
||||||
*
|
|
||||||
* @param portPath 串口的设备路径 (例如, "/dev/ttyS2").
|
|
||||||
* @param hexCommand 要发送的十六进制格式的命令字符串.
|
|
||||||
* @return 如果命令发送成功则返回 true, 否则返回 false.
|
|
||||||
*/
|
|
||||||
public static boolean sendCommand(String portPath, int baud, String hexCommand) {
|
|
||||||
if (portPath == null || portPath.isEmpty() || hexCommand == null || hexCommand.isEmpty()) {
|
|
||||||
Log.e(TAG, "Port path or command is empty.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每次调用都创建一个新的 SerialHelper 实例
|
|
||||||
SerialHelper serialHelper = new SerialHelper(portPath, baud) {
|
|
||||||
@Override
|
|
||||||
protected void onDataReceived(tp.xmaihh.serialport.bean.ComBean ComRecData) {
|
|
||||||
// 可以在这里处理返回的数据,但对于单次发送任务,通常不需要
|
|
||||||
// Log.d(TAG, "Received data from " + portPath + ": " + new String(ComRecData.bRec));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
serialHelper.open();
|
|
||||||
serialHelper.sendHex(hexCommand);
|
|
||||||
Log.d(TAG, "Successfully sent command '" + hexCommand + "' to port '" + portPath + "'");
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error sending command to port " + portPath, e);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
serialHelper.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,407 @@
|
|||||||
|
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";
|
||||||
|
// 测试命令
|
||||||
|
public static final String TEST_CMD_OFF = "ACEAB500ED"; // 关闭设备
|
||||||
|
public 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开指定串口,发送十六进制命令,然后立即关闭串口。
|
||||||
|
*
|
||||||
|
* @param portPath 串口的设备路径 (例如, "/dev/ttyS2").
|
||||||
|
* @param hexCommand 要发送的十六进制格式的命令字符串.
|
||||||
|
* @return 如果命令发送成功则返回 true, 否则返回 false.
|
||||||
|
*/
|
||||||
|
public static boolean sendCommand(String portPath, int baud, String hexCommand, int loop) {
|
||||||
|
if (portPath == null || portPath.isEmpty() || hexCommand == null || hexCommand.isEmpty()) {
|
||||||
|
Log.e(TAG, "Port path or command is empty.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每次调用都创建一个新的 SerialHelper 实例
|
||||||
|
SerialHelper serialHelper = new SerialHelper(portPath, baud) {
|
||||||
|
@Override
|
||||||
|
protected void onDataReceived(tp.xmaihh.serialport.bean.ComBean ComRecData) {
|
||||||
|
// 可以在这里处理返回的数据,但对于单次发送任务,通常不需要
|
||||||
|
// Log.d(TAG, "Received data from " + portPath + ": " + new String(ComRecData.bRec));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
serialHelper.open();
|
||||||
|
for(int i = 0; i < loop; i++) {
|
||||||
|
serialHelper.sendHex(hexCommand);
|
||||||
|
Log.d(TAG, "Successfully sent command '" + hexCommand + "' to port '" + portPath + "'");
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error sending command to port " + portPath, e);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
serialHelper.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(int loop) {
|
||||||
|
sendCommand(TEST_CMD_ON, loop);
|
||||||
|
}
|
||||||
|
private void sendCommand(String commandHex) {
|
||||||
|
sendCommand(commandHex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送指令到已保存的串口(不等待响应)
|
||||||
|
* @param commandHex 十六进制命令字符串
|
||||||
|
*/
|
||||||
|
private void sendCommand(String commandHex, int loop) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
for(int i = 0; i < loop; i ++) {
|
||||||
|
serialHelper.sendHex(commandHex);
|
||||||
|
Log.d(TAG, "Sent command: " + commandHex);
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
Thread.sleep(1000);
|
||||||
|
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 或者 AC XX XX XX XX ED
|
||||||
|
*/
|
||||||
|
private boolean isValidResponse(byte[] data) {
|
||||||
|
if (data == null || data.length < 6) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查起始字节是否为 0xDA 或 0xAC
|
||||||
|
int head = data[0] & 0xFF;
|
||||||
|
if (head != 0xDA && head != 0xAC) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查结束字节是否为 0xED
|
||||||
|
int end = data[data.length - 1] & 0xFF;
|
||||||
|
return end == 0xED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字节数组转十六进制字符串(用于日志)
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 741 KiB |
|
Before Width: | Height: | Size: 530 KiB |
|
Before Width: | Height: | Size: 385 KiB |
|
Before Width: | Height: | Size: 574 KiB |
|
Before Width: | Height: | Size: 631 KiB |
|
Before Width: | Height: | Size: 671 KiB |
BIN
app_dashboard/src/main/res/drawable/splash.png
Normal file
|
After Width: | Height: | Size: 733 KiB |
@@ -5,7 +5,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/fullscreenBackgroundColor"
|
android:background="?attr/fullscreenBackgroundColor"
|
||||||
android:theme="@style/ThemeOverlay.DashBoardClient.FullscreenContainer"
|
android:theme="@style/ThemeOverlay.DashBoardClient.FullscreenContainer"
|
||||||
tools:context=".BuildingDashboardActivity">
|
tools:context=".activity.DashboardActivity">
|
||||||
|
|
||||||
<!-- The primary full-screen view. This can be replaced with whatever view
|
<!-- The primary full-screen view. This can be replaced with whatever view
|
||||||
is needed to present your content, e.g. VideoView, SurfaceView,
|
is needed to present your content, e.g. VideoView, SurfaceView,
|
||||||
@@ -5,14 +5,12 @@
|
|||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".StartActivity">
|
android:background="@drawable/splash"
|
||||||
|
tools:context=".activity.StartActivity">
|
||||||
|
|
||||||
<TextView
|
<FrameLayout
|
||||||
|
android:id="@+id/fl_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"/>
|
||||||
android:gravity="center"
|
|
||||||
android:text="启动中,请稍后..."
|
|
||||||
android:textSize="30sp">
|
|
||||||
</TextView>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string-array name="url_end_point_entries">
|
|
||||||
<item>自动跳转</item>
|
|
||||||
<item>按楼层</item>
|
|
||||||
<item>网格+统计</item>
|
|
||||||
</string-array>
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -9,16 +9,4 @@
|
|||||||
<item>reply</item>
|
<item>reply</item>
|
||||||
<item>reply_all</item>
|
<item>reply_all</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="url_end_point_entries">
|
|
||||||
<item>Auto Redirect</item>
|
|
||||||
<item>Floor</item>
|
|
||||||
<item>Grid+Statics</item>
|
|
||||||
</string-array>
|
|
||||||
|
|
||||||
<string-array name="url_end_point_values">
|
|
||||||
<item>/</item>
|
|
||||||
<item>/dashboards/1</item>
|
|
||||||
<item>/dashboards/2</item>
|
|
||||||
</string-array>
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -3,4 +3,15 @@
|
|||||||
<string name="title_activity_building_dashboard">Building Dashboard</string>
|
<string name="title_activity_building_dashboard">Building Dashboard</string>
|
||||||
<string name="title_activity_settings">Settings</string>
|
<string name="title_activity_settings">Settings</string>
|
||||||
<string name="url_end_point_default_value" translatable="false">/</string>
|
<string name="url_end_point_default_value" translatable="false">/</string>
|
||||||
|
|
||||||
|
<string-array name="power_control_mode_entries">
|
||||||
|
<item>不控制</item>
|
||||||
|
<item>根据服务器配置</item>
|
||||||
|
<item>本地控制</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="power_control_mode_values">
|
||||||
|
<item>0</item>
|
||||||
|
<item>1</item>
|
||||||
|
<item>2</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,16 +1,66 @@
|
|||||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
<PreferenceCategory app:title="网页地址">
|
<PreferenceCategory app:title="服务器端地址">
|
||||||
<EditTextPreference
|
<EditTextPreference
|
||||||
app:key="k_url_prefix"
|
app:key="k_url_prefix"
|
||||||
app:title="URL 前缀"
|
app:title="URL 前缀"
|
||||||
app:defaultValue="http://172.18.22.211:8002/Dashboard"
|
app:defaultValue="http://172.18.22.211:8002/Dashboard"
|
||||||
|
app:summary="用于拼接网页和配置文件链接,网页:{URL前缀}/{URL路径},配置文件:{URL前缀}/data/config.json" />
|
||||||
|
<EditTextPreference
|
||||||
|
app:key="k_url_path"
|
||||||
|
app:title="URL 路径"
|
||||||
|
app:defaultValue="/index.html"
|
||||||
app:useSimpleSummaryProvider="true"/>
|
app:useSimpleSummaryProvider="true"/>
|
||||||
<ListPreference
|
</PreferenceCategory>
|
||||||
app:title="仪表盘风格"
|
|
||||||
app:key="k_url_end_point"
|
<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"
|
app:useSimpleSummaryProvider="true"
|
||||||
app:entries="@array/url_end_point_entries"
|
app:isPreferenceVisible="false"/>
|
||||||
app:entryValues="@array/url_end_point_values"
|
<ListPreference
|
||||||
app:defaultValue="@string/url_end_point_default_value" />
|
app:key="k_power_control_mode"
|
||||||
|
app:title="电源插座定时开关"
|
||||||
|
app:entries="@array/power_control_mode_entries"
|
||||||
|
app:entryValues="@array/power_control_mode_values"
|
||||||
|
app:defaultValue="1"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
<EditTextPreference
|
||||||
|
app:key="k_power_on_time"
|
||||||
|
app:title="开电源时间"
|
||||||
|
app:defaultValue="07:30"
|
||||||
|
app:summary="设置自动开电源的时间,格式:HH:mm"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
<EditTextPreference
|
||||||
|
app:key="k_power_off_time"
|
||||||
|
app:title="关电源时间"
|
||||||
|
app:defaultValue="20:30"
|
||||||
|
app:summary="设置自动关电源的时间,格式:HH:mm"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
<CheckBoxPreference
|
||||||
|
app:key="k_send_power_on_cmd"
|
||||||
|
app:title="APP 启动时重启电源插座"
|
||||||
|
app:summary="APP 启动时先关闭电源,5秒后再打开电源"
|
||||||
|
app:defaultValue="true" />
|
||||||
|
<Preference
|
||||||
|
app:key="k_clear_device"
|
||||||
|
app:title="串口设备路径"
|
||||||
|
app:summary="清除当前串口设备路径, 下次启动时自动检测设备。" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
<PreferenceCategory app:title="测试物联网关">
|
||||||
|
<Preference
|
||||||
|
app:key="k_test_power_on"
|
||||||
|
app:title="打开电源"
|
||||||
|
app:summary="发送命令给物联网关,开启供电" />
|
||||||
|
<Preference
|
||||||
|
app:key="k_test_power_off"
|
||||||
|
app:title="关闭电源"
|
||||||
|
app:summary="发送命令给物联网关,停止供电" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
@@ -15,10 +15,11 @@ buildscript {
|
|||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
maven { url 'https://jitpack.io' }
|
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://maven.aliyun.com/repository/jcenter' } // 代替 jc
|
||||||
|
maven { url 'https://repo1.maven.org/maven2/' }
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url 'https://repo1.maven.org/maven2/' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提示过时的API
|
// 提示过时的API
|
||||||
|
|||||||
20
clear.bat
@@ -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
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
serialport = "2.1.1"
|
||||||
|
acraHttp = "5.13.1"
|
||||||
agp = "8.8.2"
|
agp = "8.8.2"
|
||||||
exoplayer = "2.19.1"
|
exoplayer = "2.19.1"
|
||||||
banner = "2.2.2"
|
banner = "2.2.2"
|
||||||
@@ -28,6 +30,8 @@ utilcodex = "1.31.1"
|
|||||||
xlog = "1.11.1"
|
xlog = "1.11.1"
|
||||||
|
|
||||||
[libraries]
|
[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" }
|
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
|
||||||
banner = { module = "io.github.youth5201314:banner", version.ref = "banner" }
|
banner = { module = "io.github.youth5201314:banner", version.ref = "banner" }
|
||||||
exoplayer = { module = "com.google.android.exoplayer:exoplayer", version.ref = "exoplayer" }
|
exoplayer = { module = "com.google.android.exoplayer:exoplayer", version.ref = "exoplayer" }
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
**注意:本项目已停止更新,可完全由 [CastBox](http://code.slackz.cn/ykbox/CastBox) 替代**
|
||||||
|
|
||||||
# Dashboard Client
|
# Dashboard Client
|
||||||
|
|
||||||
## 1. 简介
|
## 1. 简介
|
||||||
@@ -34,8 +36,6 @@
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"DevicePath": "/dev/ttyS2",
|
|
||||||
"Baud": 9600,
|
|
||||||
"Commands": [
|
"Commands": [
|
||||||
{
|
{
|
||||||
"Time": "08:00",
|
"Time": "08:00",
|
||||||
|
|||||||
@@ -1,8 +1 @@
|
|||||||
include ':app_dashboard'
|
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'
|
|
||||||
|
|||||||