13 Commits

Author SHA1 Message Date
Zhanghu
15cbcc206e 小改动 2026-03-26 11:12:13 +08:00
Zhanghu
e8eff1d512 * 美化启动画面
* 清除冗余代码
2026-03-16 14:38:20 +08:00
Zhanghu
5f1757fed4 #6 服务器不能访问后,长时间下来会闪退 2026-03-16 14:19:45 +08:00
Zhanghu
fcfc964fb6 changelog 2026-03-13 13:01:28 +08:00
Zhanghu
6c0204325f 修复闹钟没有循环的bug 2026-03-13 09:07:26 +08:00
Zhanghu
08144f5106 小改动 2026-03-12 15:33:49 +08:00
Zhanghu
40bfbd8ceb 定时开关机优化,支持三种模式 2026-03-12 15:28:44 +08:00
Zhanghu
98dba95232 去掉仪表盘风格
url 设置修改
串口命令发送3次,避免发送失败
2026-03-12 13:35:47 +08:00
Zhanghu
2a5a6e95b4 去掉仪表盘风格 2026-03-12 08:55:46 +08:00
Zhanghu
691d6c15ad * 设置界面增加电源开关测试
* App 启动时默认不发送开机指令
2026-03-10 10:45:46 +08:00
Zhanghu
cac9dd41aa 如果串口路径不为空时则开关一次设备 2026-03-05 14:47:07 +08:00
Zhanghu
d29aa0dfd1 * 仅串口路径为空时才自动检测设备
* 设置界面可清除串口路径
2026-03-05 14:04:38 +08:00
Zhanghu
5b0eb711a6 changelog 2026-01-05 16:57:27 +08:00
29 changed files with 914 additions and 499 deletions

View File

@@ -109,6 +109,7 @@ 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

View File

@@ -9,7 +9,40 @@ author:
2. $ {VERSION_CODE} (去掉空格),会自动替换实际修订号,比如 1.1.4.$ {VERSION_CODE} 2. $ {VERSION_CODE} (去掉空格),会自动替换实际修订号,比如 1.1.4.$ {VERSION_CODE}
--> -->
### [1.0.4.${VERSION_CODE}] - 2026.1.5 ### [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
#### 文件下载 #### 文件下载

View File

@@ -19,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"
@@ -29,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"
@@ -55,5 +50,4 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
</application> </application>
</manifest> </manifest>

View File

@@ -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);
}
}

View File

@@ -1,13 +1,9 @@
package cn.ykbox.dashboard.activity; package cn.ykbox.dashboard.activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@@ -28,41 +24,28 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.json.JSONArray; import com.kongzue.dialogx.dialogs.TipDialog;
import org.json.JSONObject; import com.kongzue.dialogx.dialogs.WaitDialog;
import java.util.Calendar; import cn.ykbox.dashboard.alarm.PowerAlarmManager;
import cn.ykbox.dashboard.databinding.ActivityDashboardBinding;
import cn.ykbox.dashboard.ConfigReader;
import cn.ykbox.dashboard.databinding.ActivityBuildingDashboardBinding;
import cn.ykbox.dashboard.perferences.PreferenceConfiguration; import cn.ykbox.dashboard.perferences.PreferenceConfiguration;
import cn.ykbox.dashboard.receiver.CommandBroadcastReceiver;
import cn.ykbox.dashboard.serial.SerialControlDevices;
import cn.ykbox.dashboard.serial.SerialPortDetector; import cn.ykbox.dashboard.serial.SerialPortDetector;
public class BuildingDashboardActivity extends FullscreenActivity { public class DashboardActivity extends FullscreenActivity {
private final static String TAG = "DashboardActivity"; private final static String TAG = "DashboardActivity";
private static final boolean AUTO_HIDE = true; private static final boolean AUTO_HIDE = true;
private static final int AUTO_HIDE_DELAY_MILLIS = 3000; 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 String mainUrl;
private int retryCount = 0; private long retryCount = 0;
private static final int MAX_RETRY = 99; // 最大重试次数 private static final long MAX_RETRY = Long.MAX_VALUE; // 最大重试次数
private static final int RETRY_DELAY = 60000; // 1分钟 = 60000毫秒 private static final int RETRY_DELAY = 60000; // 1分钟 = 60000毫秒
private String configUrl;
private ConfigReader configReader; private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener;
private String lastAppliedSerialConfig = null;
// --- Periodic Config Loading --- private PowerAlarmManager powerAlarmManager;
private final Handler configLoadHandler = new Handler(Looper.getMainLooper());
private Runnable configLoadRunnable;
private ActivityBuildingDashboardBinding binding; private ActivityDashboardBinding binding;
private ActivityResultLauncher<Intent> settingsLauncher; private ActivityResultLauncher<Intent> settingsLauncher;
private SerialPortDetector detector; private SerialPortDetector detector;
@@ -86,7 +69,7 @@ public class BuildingDashboardActivity extends FullscreenActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
binding = ActivityBuildingDashboardBinding.inflate(getLayoutInflater()); binding = ActivityDashboardBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot()); setContentView(binding.getRoot());
super.setViews(binding.webview, binding.fullscreenContentControls); super.setViews(binding.webview, binding.fullscreenContentControls);
@@ -96,36 +79,64 @@ public class BuildingDashboardActivity extends FullscreenActivity {
initSettingsLauncher(); initSettingsLauncher();
initListener(); initListener();
initWebView(); initWebView();
initConfigLoader();
// 获取配置 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 @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
// 当用户选择了自动跳转时直接访问 urlPrefix这样 url 可以自由设置
SharedPreferences pre = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences pre = PreferenceManager.getDefaultSharedPreferences(this);
String urlPrefix = pre.getString("k_url_prefix", "http://172.18.22.211:8002/Dashboard"); String urlPrefix = pre.getString("k_url_prefix", "http://172.18.22.211:8002/Dashboard");
String urlEndPoint = pre.getString("k_url_end_point", "/").replaceFirst("^/", ""); String urlPath = pre.getString("k_url_path", "/index.html");
configUrl = urlPrefix + "/data/config.json"; mainUrl = urlPrefix + urlPath;
mainUrl = urlEndPoint.isEmpty() ? urlPrefix : urlPrefix + "/" + urlEndPoint;
configLoadHandler.post(configLoadRunnable); Log.i(TAG, "Main: " + mainUrl);
loadUrlWithRetry(); loadUrlWithRetry();
} }
private void initSerialPort() { private void initSerialPort() {
String portPath = PreferenceConfiguration.getSerialPortPath(this); String portPath = PreferenceConfiguration.getSerialPortPath(this);
int baudRate = PreferenceConfiguration.getSerialPortBaudRate(this); int baudRate = PreferenceConfiguration.getSerialPortBaudRate(this);
boolean sendPowerOnCmd = PreferenceConfiguration.getSendPowerOnCmd(this);
// 初始化串口检测器 // 初始化串口检测器
detector = new SerialPortDetector(portPath, baudRate); detector = new SerialPortDetector(portPath, baudRate);
// 每次启动都进行串口检测/验证 // 只有 portPath 为空时才进行串口检测
if (TextUtils.isEmpty(portPath)) {
Log.i(TAG, "Starting serial port detection/verification...");
// 参数 true 表示确保设备开机发送两个命令 // 参数 true 表示确保设备开机发送两个命令
// 参数 false 表示仅检测串口任一命令有响应即可 // 参数 false 表示仅检测串口任一命令有响应即可
Log.i(TAG, "Starting serial port detection/verification...");
startSerialPortDetection(true); 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();
}
}
} }
/** /**
@@ -157,7 +168,7 @@ public class BuildingDashboardActivity extends FullscreenActivity {
Log.d(TAG, "Testing port " + current + "/" + total + ": " + portPath); Log.d(TAG, "Testing port " + current + "/" + total + ": " + portPath);
runOnUiThread(() -> { runOnUiThread(() -> {
if (progressDialog != null) { if (progressDialog != null) {
progressDialog.setMessage("正在测试串口 " + current + "/" + total + "\n" + portPath); progressDialog.setMessage("正在测试串口路径: " + portPath + "\n" + current + "/" + total);
} }
}); });
} }
@@ -169,11 +180,11 @@ public class BuildingDashboardActivity extends FullscreenActivity {
if (progressDialog != null) { if (progressDialog != null) {
progressDialog.dismiss(); progressDialog.dismiss();
} }
Toast.makeText(BuildingDashboardActivity.this, Toast.makeText(DashboardActivity.this,
"串口检测成功!\n路径: " + portPath, "串口检测成功!\n路径: " + portPath,
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
PreferenceConfiguration.setSerialPortPath(BuildingDashboardActivity.this, portPath); PreferenceConfiguration.setSerialPortPath(DashboardActivity.this, portPath);
}); });
} }
@@ -198,7 +209,7 @@ public class BuildingDashboardActivity extends FullscreenActivity {
* 显示检测失败的对话框 * 显示检测失败的对话框
*/ */
private void showDetectionFailedDialog() { private void showDetectionFailedDialog() {
Toast.makeText(BuildingDashboardActivity.this, Toast.makeText(DashboardActivity.this,
"未找到有效的串口设备,请检查设备连接后重试。", "未找到有效的串口设备,请检查设备连接后重试。",
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
} }
@@ -213,8 +224,9 @@ public class BuildingDashboardActivity extends FullscreenActivity {
retryCount++; retryCount++;
Log.d("WebView", "加载失败将在1分钟后重试 (第" + retryCount + "次重试)"); Log.d("WebView", "加载失败将在1分钟后重试 (第" + retryCount + "次重试)");
WaitDialog.dismiss();
// 使用Handler延迟执行重试 // 使用Handler延迟执行重试
configLoadHandler.postDelayed(new Runnable() { new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override @Override
public void run() { public void run() {
Log.d("WebView", "开始重试加载..."); Log.d("WebView", "开始重试加载...");
@@ -225,21 +237,27 @@ public class BuildingDashboardActivity extends FullscreenActivity {
} else { } else {
Log.e("WebView", "重试次数已达上限,加载失败"); Log.e("WebView", "重试次数已达上限,加载失败");
// 这里可以显示错误页面或提示用户 // 这里可以显示错误页面或提示用户
showToast("网页加载失败,请检查网络连接"); runOnUiThread(() -> {
TipDialog.show("重新加载失败次数已达上限,请联系管理员。", WaitDialog.TYPE.ERROR, -1);
});
} }
} }
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
configLoadHandler.removeCallbacks(configLoadRunnable); // PowerAlarmManager 内部已处理配置加载的暂停
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
if (configReader != null) { if (preferenceChangeListener != null) {
configReader.release(); PreferenceManager.getDefaultSharedPreferences(this)
.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener);
}
if (powerAlarmManager != null) {
powerAlarmManager.stop();
} }
} }
@@ -276,6 +294,9 @@ public class BuildingDashboardActivity extends FullscreenActivity {
WebSettings webSettings = binding.webview.getSettings(); WebSettings webSettings = binding.webview.getSettings();
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
webSettings.setJavaScriptEnabled(true); webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);
// 允许媒体自动播放 (Android 8+ 必须)
webSettings.setMediaPlaybackRequiresUserGesture(false);
// 禁用缩放相关设置 // 禁用缩放相关设置
webSettings.setTextZoom(100); webSettings.setTextZoom(100);
@@ -283,13 +304,17 @@ public class BuildingDashboardActivity extends FullscreenActivity {
webSettings.setBuiltInZoomControls(false); webSettings.setBuiltInZoomControls(false);
binding.webview.setInitialScale(100); binding.webview.setInitialScale(100);
binding.webview.setWebViewClient(new WebViewClient() { binding.webview.setWebViewClient(new WebViewClient() {
private boolean hasError = false; private boolean hasError = false;
@Override @Override
public void onPageStarted(WebView view, String url, Bitmap favicon) { public void onPageStarted(WebView view, String url, Bitmap favicon) {
hasError = false; hasError = false;
if(retryCount <= 0)
WaitDialog.show("正在加载...");
else
WaitDialog.show( "" + retryCount + " 次重试... ");
} }
@Override @Override
@@ -318,125 +343,36 @@ public class BuildingDashboardActivity extends FullscreenActivity {
super.onPageFinished(view, url); super.onPageFinished(view, url);
if (!hasError) { if (!hasError) {
retryCount = 0; retryCount = 0;
WaitDialog.dismiss();
Log.d("WebView", "加载OK"); Log.d("WebView", "加载OK");
} }
} }
}); });
} }
private void initConfigLoader() { private void initPreferenceChangeListener() {
configLoadRunnable = new Runnable() { preferenceChangeListener = (sharedPreferences, key) -> {
@Override if (key.equals("k_power_control_mode") ||
public void run() { key.equals("k_power_on_time") ||
if (!TextUtils.isEmpty(configUrl)) { key.equals("k_power_off_time")) {
Log.d(TAG, "Periodically loading config..."); Log.d(TAG, "Power control preference changed: " + key);
if (configReader != null) { runOnUiThread(() -> {
configReader.loadConfig(configUrl); if (key.equals("k_power_control_mode")) {
} int mode = PreferenceConfiguration.getPowerControlMode(DashboardActivity.this);
powerAlarmManager.onModeChanged(mode);
} else { } else {
Log.e(TAG, "configUrl is empty, skipping config load."); // 本地时间变更
} powerAlarmManager.onLocalTimeChanged();
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);
} }
}); });
} }
} };
PreferenceManager.getDefaultSharedPreferences(this)
private void refreshAlarms(String serialConfigJson) { .registerOnSharedPreferenceChangeListener(preferenceChangeListener);
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);
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);
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);
} 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);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
Log.d(TAG, "Set repeating alarm for " + time + " with command " + hex);
}
}
}
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() { private void openSettingsActivity() {
Intent intent = new Intent(this, SettingsActivity.class); Intent intent = new Intent(this, SettingsActivity.class);
settingsLauncher.launch(intent); settingsLauncher.launch(intent);
} }
private void showToast(String message) {
runOnUiThread(() -> Toast.makeText(mContext,
message,
Toast.LENGTH_SHORT).show());
}
} }

View File

@@ -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

View File

@@ -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();
}
}
}

View File

@@ -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();
}
} }
} }

View File

@@ -5,35 +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);
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);

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -13,6 +13,10 @@ public class PreferenceConfiguration {
private static final String KEY_SERIAL_PORT_PATH = "k_serial_port_path"; 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_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) { public static String getSerialPortPath(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@@ -29,4 +33,29 @@ public class PreferenceConfiguration {
String baud = prefs.getString(KEY_SERIAL_PORT_BAUD_RATE, "9600"); String baud = prefs.getString(KEY_SERIAL_PORT_BAUD_RATE, "9600");
return Integer.parseInt(baud); 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");
}
} }

View File

@@ -1,21 +1,28 @@
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.content.SharedPreferences; import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.preference.PreferenceManager; import java.util.Calendar;
import cn.ykbox.dashboard.perferences.PreferenceConfiguration; import cn.ykbox.dashboard.perferences.PreferenceConfiguration;
import cn.ykbox.dashboard.serial.SerialControlDevices; 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_HOUR = "EXTRA_HOUR";
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) {
@@ -24,15 +31,70 @@ public class CommandBroadcastReceiver extends BroadcastReceiver {
String portPath = PreferenceConfiguration.getSerialPortPath(context); String portPath = PreferenceConfiguration.getSerialPortPath(context);
int baudRate = PreferenceConfiguration.getSerialPortBaudRate(context); int baudRate = PreferenceConfiguration.getSerialPortBaudRate(context);
int loop = PreferenceConfiguration.getSerialCmdLoop(context);
if (hexCommand != null && !hexCommand.isEmpty() && !TextUtils.isEmpty(portPath)) { 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 + ")");
});
}
} }

View File

@@ -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();
}
}
}

View File

@@ -16,8 +16,8 @@ import tp.xmaihh.serialport.bean.ComBean;
public class SerialPortDetector { public class SerialPortDetector {
private static final String TAG = "SerialPortDetector"; private static final String TAG = "SerialPortDetector";
// 测试命令 // 测试命令
private static final String TEST_CMD_OFF = "ACEAB500ED"; // 关闭设备 public static final String TEST_CMD_OFF = "ACEAB500ED"; // 关闭设备
private static final String TEST_CMD_ON = "ACEAB400ED"; // 打开设备 public static final String TEST_CMD_ON = "ACEAB400ED"; // 打开设备
// 响应超时时间(毫秒) // 响应超时时间(毫秒)
private static final long RESPONSE_TIMEOUT = 6000; private static final long RESPONSE_TIMEOUT = 6000;
@@ -35,6 +35,44 @@ public class SerialPortDetector {
void onDetectionFailed(); 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) { public SerialPortDetector(String savedPath, int baudRate) {
this.savedPath = savedPath; this.savedPath = savedPath;
this.baudRate = baudRate; this.baudRate = baudRate;
@@ -45,6 +83,58 @@ public class SerialPortDetector {
this.callback = 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();
}
/** /**
* 开始串口检测(智能检测) * 开始串口检测(智能检测)
* 先验证已保存的串口,如果不可用再进行全局检测 * 先验证已保存的串口,如果不可用再进行全局检测
@@ -100,7 +190,6 @@ public class SerialPortDetector {
notifyDetectionProgress(portPath, i + 1, devicePaths.length); notifyDetectionProgress(portPath, i + 1, devicePaths.length);
Log.d(TAG, "Testing port: " + portPath + " at " + baudRate + " baud"); Log.d(TAG, "Testing port: " + portPath + " at " + baudRate + " baud");
if (testSerialPort(portPath, baudRate, ensurePowerOn)) { if (testSerialPort(portPath, baudRate, ensurePowerOn)) {
Log.i(TAG, "Serial port detected successfully: " + portPath); Log.i(TAG, "Serial port detected successfully: " + portPath);
notifyDetectionSuccess(portPath); notifyDetectionSuccess(portPath);
@@ -127,9 +216,11 @@ public class SerialPortDetector {
if (ensurePowerOn) { if (ensurePowerOn) {
// 需要确保设备开机,两个命令都要发送 // 需要确保设备开机,两个命令都要发送
//
Log.d(TAG, "Ensuring device power on by sending both commands"); Log.d(TAG, "Ensuring device power on by sending both commands");
boolean cmd1Success = sendCommandAndWaitResponse(serialHelper, TEST_CMD_OFF); boolean cmd1Success = sendCommandAndWaitResponse(serialHelper, TEST_CMD_OFF);
Thread.sleep(1000);
boolean cmd2Success = sendCommandAndWaitResponse(serialHelper, TEST_CMD_ON); boolean cmd2Success = sendCommandAndWaitResponse(serialHelper, TEST_CMD_ON);
// 只要有一个命令收到有效响应就认为串口可用 // 只要有一个命令收到有效响应就认为串口可用
@@ -226,6 +317,20 @@ public class SerialPortDetector {
} }
} }
/**
* 简单的串口 Helper用于只发送命令不等待响应
*/
private static class SimpleSerialHelper extends SerialHelper {
public SimpleSerialHelper(String sPort, int iBaudRate) {
super(sPort, iBaudRate);
}
@Override
protected void onDataReceived(ComBean comBean) {
// 不需要处理响应
}
}
/** /**
* 内部测试用的 SerialHelper * 内部测试用的 SerialHelper
*/ */
@@ -267,24 +372,22 @@ public class SerialPortDetector {
} }
/** /**
* 检查响应是否符合格式: DA XX XX XX XX ED * 检查响应是否符合格式: DA XX XX XX XX ED 或者 AC XX XX XX XX ED
*/ */
private boolean isValidResponse(byte[] data) { private boolean isValidResponse(byte[] data) {
if (data == null || data.length < 6) { if (data == null || data.length < 6) {
return false; return false;
} }
// 检查起始字节是否为 0xDA // 检查起始字节是否为 0xDA 或 0xAC
if ((data[0] & 0xFF) != 0xDA) { int head = data[0] & 0xFF;
if (head != 0xDA && head != 0xAC) {
return false; return false;
} }
// 检查结束字节是否为 0xED // 检查结束字节是否为 0xED
if ((data[data.length - 1] & 0xFF) != 0xED) { int end = data[data.length - 1] & 0xFF;
return false; return end == 0xED;
}
return true;
} }
/** /**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 741 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

View File

@@ -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,

View File

@@ -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>

View File

@@ -1,7 +1,3 @@
<resources> <resources>
<string-array name="url_end_point_entries">
<item>自动跳转</item>
<item>按楼层</item>
<item>网格+统计</item>
</string-array>
</resources> </resources>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,29 +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:useSimpleSummaryProvider="true" /> app:summary="用于拼接网页和配置文件链接,网页:{URL前缀}/{URL路径},配置文件:{URL前缀}/data/config.json" />
<ListPreference <EditTextPreference
app:title="仪表盘风格" app:key="k_url_path"
app:key="k_url_end_point" app:title="URL 路径"
app:useSimpleSummaryProvider="true" app:defaultValue="/index.html"
app:entries="@array/url_end_point_entries" app:useSimpleSummaryProvider="true"/>
app:entryValues="@array/url_end_point_values"
app:defaultValue="@string/url_end_point_default_value" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="外部设备"> <PreferenceCategory app:title="配置物联网关">
<EditTextPreference <EditTextPreference
app:key="k_serial_port_path" app:key="k_serial_port_path"
app:title="串口设备" app:title="串口设备路径"
app:defaultValue="" app:defaultValue=""
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
<EditTextPreference <EditTextPreference
app:key="k_serial_baud" app:key="k_serial_baud"
app:title="串口波特率" app:title="串口波特率"
app:defaultValue="9600" app:defaultValue="9600"
app:useSimpleSummaryProvider="true"
app:isPreferenceVisible="false"/>
<ListPreference
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" /> 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>

View File

@@ -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",