diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 7062b20..0000000 --- a/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "mod_utils"] - path = mod_utils - url = git@192.168.196.173:ykbox/mod_utils.git -[submodule "mod_signageapi"] - path = mod_signageapi - url = git@192.168.196.173:ykbox/mod_signageapi.git -[submodule "mod_serialport"] - path = mod_serialport - url = git@192.168.196.173:ykbox/mod_serialport.git diff --git a/app_dashboard/build.gradle b/app_dashboard/build.gradle index 542961e..29ce32b 100644 --- a/app_dashboard/build.gradle +++ b/app_dashboard/build.gradle @@ -48,6 +48,7 @@ dependencies { implementation libs.utilcodex + implementation libs.okhttp implementation libs.banner implementation libs.glide annotationProcessor libs.glidecompiler diff --git a/app_dashboard/src/main/AndroidManifest.xml b/app_dashboard/src/main/AndroidManifest.xml index fcaa8b9..c9a9d17 100644 --- a/app_dashboard/src/main/AndroidManifest.xml +++ b/app_dashboard/src/main/AndroidManifest.xml @@ -12,17 +12,17 @@ android:theme="@style/Theme.Dashboard" android:usesCleartextTraffic="true"> @@ -31,7 +31,7 @@ @@ -42,6 +42,11 @@ + + \ No newline at end of file diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/BootBroadcastReceiver.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/BootBroadcastReceiver.java index bb71d3b..7530753 100644 --- a/app_dashboard/src/main/java/cn/ykbox/dashboard/BootBroadcastReceiver.java +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/BootBroadcastReceiver.java @@ -4,6 +4,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import cn.ykbox.dashboard.activity.StartActivity; + public class BootBroadcastReceiver extends BroadcastReceiver { diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/ConfigReader.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/ConfigReader.java index 8abbed4..c8dfad6 100644 --- a/app_dashboard/src/main/java/cn/ykbox/dashboard/ConfigReader.java +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/ConfigReader.java @@ -1,3 +1,5 @@ +package cn.ykbox.dashboard; + import android.content.Context; import android.content.SharedPreferences; import android.os.AsyncTask; @@ -13,22 +15,19 @@ import java.util.concurrent.TimeUnit; public class ConfigReader { private static final String TAG = "ConfigReader"; private static final String PREFS_NAME = "AppConfig"; - private static final String POWER_OFF_TIME_KEY = "PowerOffTVTime"; + private static final String SERIAL_CONFIG_KEY = "SerialConfig"; private Context context; - private String configUrl; private OkHttpClient httpClient; private OnConfigLoadListener listener; - // 回调接口 public interface OnConfigLoadListener { - void onConfigLoaded(String powerOffTime); + void onConfigLoaded(); void onConfigLoadFailed(String error); } - public ConfigReader(Context context, String configUrl) { + public ConfigReader(Context context) { this.context = context; - this.configUrl = configUrl; this.httpClient = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) @@ -40,56 +39,40 @@ public class ConfigReader { this.listener = listener; } - // 异步读取配置文件 - public void loadConfig() { - new ConfigLoadTask().execute(); + public void loadConfig(String configUrl) { + new ConfigLoadTask().execute(configUrl); } - // 从本地获取保存的PowerOffTVTime - public String getSavedPowerOffTime() { + public String getSavedSerialConfig() { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - return prefs.getString(POWER_OFF_TIME_KEY, "23:00"); // 默认值 + return prefs.getString(SERIAL_CONFIG_KEY, "{}"); } - // 保存PowerOffTVTime到本地 - private void savePowerOffTime(String powerOffTime) { + private void saveSerialConfig(String serialJsonString) { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); - editor.putString(POWER_OFF_TIME_KEY, powerOffTime); + editor.putString(SERIAL_CONFIG_KEY, serialJsonString); editor.apply(); - Log.d(TAG, "PowerOffTVTime saved: " + powerOffTime); + Log.d(TAG, "Serial config saved: " + serialJsonString); } - // 异步任务类 - private class ConfigLoadTask extends AsyncTask { + private class ConfigLoadTask extends AsyncTask { private String errorMessage = null; @Override - protected String doInBackground(Void... voids) { + protected JSONObject doInBackground(String... params) { + String configUrl = params[0]; try { - // 创建HTTP请求 - Request request = new Request.Builder() - .url(configUrl) - .addHeader("Accept", "application/json") - .build(); - - // 执行请求 + Request request = new Request.Builder().url(configUrl).addHeader("Accept", "application/json").build(); Response response = httpClient.newCall(request).execute(); - if (response.isSuccessful() && response.body() != null) { String jsonString = response.body().string(); Log.d(TAG, "Received JSON: " + jsonString); - - // 解析JSON - JSONObject jsonObject = new JSONObject(jsonString); - String powerOffTime = jsonObject.getString("PowerOffTVTime"); - - return powerOffTime; + return new JSONObject(jsonString); } else { errorMessage = "HTTP请求失败,状态码: " + response.code(); return null; } - } catch (IOException e) { errorMessage = "网络连接失败: " + e.getMessage(); Log.e(TAG, errorMessage, e); @@ -102,17 +85,27 @@ public class ConfigReader { } @Override - protected void onPostExecute(String powerOffTime) { - if (powerOffTime != null) { - // 保存到本地 - savePowerOffTime(powerOffTime); + protected void onPostExecute(JSONObject jsonObject) { + if (jsonObject != null) { + try { + if (jsonObject.has("Serial")) { + JSONObject serialObject = jsonObject.getJSONObject("Serial"); + saveSerialConfig(serialObject.toString()); - // 通知回调 - if (listener != null) { - listener.onConfigLoaded(powerOffTime); + if (listener != null) { + listener.onConfigLoaded(); + } + } else { + throw new JSONException("JSON中缺少 'Serial' 字段"); + } + } catch (JSONException e) { + String error = "解析JSON字段时出错: " + e.getMessage(); + Log.e(TAG, error, e); + if (listener != null) { + listener.onConfigLoadFailed(error); + } } } else { - // 通知错误 if (listener != null) { listener.onConfigLoadFailed(errorMessage != null ? errorMessage : "未知错误"); } @@ -120,7 +113,6 @@ public class ConfigReader { } } - // 释放资源 public void release() { if (httpClient != null) { httpClient.dispatcher().executorService().shutdown(); diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/ScheduledCommandTimer .java b/app_dashboard/src/main/java/cn/ykbox/dashboard/ScheduledCommandTimer .java deleted file mode 100644 index 0dfab48..0000000 --- a/app_dashboard/src/main/java/cn/ykbox/dashboard/ScheduledCommandTimer .java +++ /dev/null @@ -1,249 +0,0 @@ -public class ScheduledCommandTimer { - private static final String TAG = "ScheduledCommandTimer"; - - private Handler handler; - private Runnable timerRunnable; - private boolean isTimerRunning = false; - - // 配置参数 - private long checkInterval = 10 * 1000; // 10秒检查间隔 - private int targetHour = 14; // 目标时间:小时 - private int targetMinute = 30; // 目标时间:分钟 - private long commandInterval = 10 * 1000; // 命令发送间隔10秒 - private int maxCommandCount = 3; // 最多发送次数 - - // 状态变量 - private int commandSentCount = 0; - private boolean hasReachedTargetTime = false; - private long firstCommandTime = 0; - - // 回调接口 - private CommandCallback commandCallback; - private TimerStatusCallback statusCallback; - - // 回调接口定义 - public interface CommandCallback { - void onCommandExecute(int currentCount, int totalCount); - } - - public interface TimerStatusCallback { - void onTimerStarted(); - void onTimerStopped(); - void onTargetTimeReached(); - void onCommandSequenceCompleted(); - void onTimeCheck(int currentHour, int currentMinute); - } - - // 构造函数 - public ScheduledCommandTimer() { - initTimer(); - } - - public ScheduledCommandTimer(int targetHour, int targetMinute) { - this.targetHour = targetHour; - this.targetMinute = targetMinute; - initTimer(); - } - - // 建造者模式配置类 - public static class Builder { - private ScheduledCommandTimer timer = new ScheduledCommandTimer(); - - public Builder setTargetTime(int hour, int minute) { - timer.targetHour = hour; - timer.targetMinute = minute; - return this; - } - - public Builder setCheckInterval(long intervalMs) { - timer.checkInterval = intervalMs; - return this; - } - - public Builder setCommandInterval(long intervalMs) { - timer.commandInterval = intervalMs; - return this; - } - - public Builder setMaxCommandCount(int count) { - timer.maxCommandCount = count; - return this; - } - - public Builder setCommandCallback(CommandCallback callback) { - timer.commandCallback = callback; - return this; - } - - public Builder setStatusCallback(TimerStatusCallback callback) { - timer.statusCallback = callback; - return this; - } - - public ScheduledCommandTimer build() { - timer.initTimer(); - return timer; - } - } - - private void initTimer() { - handler = new Handler(Looper.getMainLooper()); - timerRunnable = new Runnable() { - @Override - public void run() { - checkTimeAndSendCommand(); - - if (isTimerRunning) { - handler.postDelayed(this, checkInterval); - } - } - }; - } - - // 启动定时器 - public void start() { - if (!isTimerRunning) { - isTimerRunning = true; - handler.post(timerRunnable); - Log.d(TAG, "定时器已启动"); - - if (statusCallback != null) { - statusCallback.onTimerStarted(); - } - } - } - - // 停止定时器 - public void stop() { - if (isTimerRunning) { - isTimerRunning = false; - handler.removeCallbacks(timerRunnable); - Log.d(TAG, "定时器已停止"); - - if (statusCallback != null) { - statusCallback.onTimerStopped(); - } - } - } - - // 重启定时器 - public void restart() { - stop(); - resetCommandState(); - start(); - } - - private void checkTimeAndSendCommand() { - Calendar now = Calendar.getInstance(); - int currentHour = now.get(Calendar.HOUR_OF_DAY); - int currentMinute = now.get(Calendar.MINUTE); - - Log.d(TAG, String.format("时间检查: %02d:%02d, 目标: %02d:%02d", - currentHour, currentMinute, targetHour, targetMinute)); - - if (statusCallback != null) { - statusCallback.onTimeCheck(currentHour, currentMinute); - } - - // 检查是否到达指定时间 - if (currentHour == targetHour && currentMinute >= targetMinute) { - if (!hasReachedTargetTime) { - hasReachedTargetTime = true; - firstCommandTime = System.currentTimeMillis(); - commandSentCount = 0; - Log.d(TAG, "到达目标时间,开始发送命令序列"); - - if (statusCallback != null) { - statusCallback.onTargetTimeReached(); - } - } - - processCommandSequence(); - } else { - if (hasReachedTargetTime) { - resetCommandState(); - } - } - } - - private void processCommandSequence() { - long currentTime = System.currentTimeMillis(); - long elapsedTime = currentTime - firstCommandTime; - - // 检查是否在30秒窗口内且未超过最大发送次数 - if (elapsedTime <= 30 * 1000 && commandSentCount < maxCommandCount) { - long expectedCommandTime = firstCommandTime + (commandSentCount * commandInterval); - - if (currentTime >= expectedCommandTime) { - commandSentCount++; - Log.d(TAG, String.format("执行命令 %d/%d, 已用时: %d毫秒", - commandSentCount, maxCommandCount, elapsedTime)); - - if (commandCallback != null) { - commandCallback.onCommandExecute(commandSentCount, maxCommandCount); - } - - // 检查是否完成所有命令 - if (commandSentCount >= maxCommandCount) { - Log.d(TAG, "命令序列执行完成"); - if (statusCallback != null) { - statusCallback.onCommandSequenceCompleted(); - } - resetCommandState(); - } - } - } else if (elapsedTime > 30 * 1000) { - Log.d(TAG, "30秒窗口超时,重置状态"); - resetCommandState(); - } - } - - private void resetCommandState() { - hasReachedTargetTime = false; - commandSentCount = 0; - firstCommandTime = 0; - Log.d(TAG, "命令发送状态已重置"); - } - - // 获取当前状态 - public boolean isRunning() { - return isTimerRunning; - } - - public boolean hasReachedTarget() { - return hasReachedTargetTime; - } - - public int getCommandSentCount() { - return commandSentCount; - } - - public long getRemainingTime() { - if (!hasReachedTargetTime) return -1; - long elapsedTime = System.currentTimeMillis() - firstCommandTime; - return Math.max(0, 30 * 1000 - elapsedTime); - } - - // 配置方法 - public void setTargetTime(int hour, int minute) { - this.targetHour = hour; - this.targetMinute = minute; - resetCommandState(); - } - - public void setCommandCallback(CommandCallback callback) { - this.commandCallback = callback; - } - - public void setStatusCallback(TimerStatusCallback callback) { - this.statusCallback = callback; - } - - // 释放资源 - public void destroy() { - stop(); - commandCallback = null; - statusCallback = null; - handler = null; - } -} \ No newline at end of file diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/BaseActivity.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/BaseActivity.java index 0255a4f..2213be8 100644 --- a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/BaseActivity.java +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/BaseActivity.java @@ -1,4 +1,4 @@ -package cn.ykbox.dashboard; +package cn.ykbox.dashboard.activity; import android.os.Build; import android.os.Bundle; diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/BuildingDashboardActivity.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/BuildingDashboardActivity.java index 68f5d6f..a87646e 100644 --- a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/BuildingDashboardActivity.java +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/BuildingDashboardActivity.java @@ -1,66 +1,69 @@ -package cn.ykbox.dashboard; +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.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.webkit.WebViewClient; +import android.widget.Toast; -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultCallback; 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; public class BuildingDashboardActivity extends FullscreenActivity { private final static String TAG = "DashboardActivity"; - /** - * Whether or not the system UI should be auto-hidden after - * {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds. - */ private static final boolean AUTO_HIDE = true; - - /** - * If {@link #AUTO_HIDE} is set, the number of milliseconds to wait after - * user interaction before hiding the system UI. - */ 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 ScheduledCommandTimer timer; + private String mainUrl; + private String configUrl; private ConfigReader configReader; + private String lastAppliedSerialConfig = null; + + // --- Periodic Config Loading --- + private final Handler configLoadHandler = new Handler(Looper.getMainLooper()); + private Runnable configLoadRunnable; - /** - * Touch listener to use for in-layout UI controls to delay hiding the - * system UI. This is to prevent the jarring behavior of controls going away - * while interacting with activity UI. - */ - private final View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent 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; - } - }; private ActivityBuildingDashboardBinding binding; - /// ActivityResultLauncher 用于处理设置页面的返回结果 private ActivityResultLauncher 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()); @@ -69,184 +72,162 @@ public class BuildingDashboardActivity extends FullscreenActivity { super.setViews(binding.webview, binding.fullscreenContentControls); super.onCreate(savedInstanceState); - // 初始化 ActivityResultLauncher initSettingsLauncher(); - setupConfig(); + initListener(); initWebView(); - initTimer(); - - // Upon interacting with UI controls, delay any scheduled hide() - // operations to prevent the jarring behavior of controls going away - // while interacting with the UI. - binding.settingsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - // 打开设置界面 - openSettingsActivity(); - } - }); + initConfigLoader(); } @Override protected void onResume() { super.onResume(); - if (timer != null) { - timer.start(); - } - SharedPreferences pre = PreferenceManager.getDefaultSharedPreferences(this); String url = pre.getString("k_url", "http://10.1.58.176:8002"); - binding.webview.loadUrl(url); // 加载网页 + mainUrl = url + "/index.html"; + configUrl = url + "/data/config.json"; + + configLoadHandler.post(configLoadRunnable); + binding.webview.loadUrl(mainUrl); } @Override protected void onPause() { super.onPause(); - if (timer != null) { - timer.stop(); - } + configLoadHandler.removeCallbacks(configLoadRunnable); } @Override protected void onDestroy() { super.onDestroy(); - if (timer != null) { - timer.destroy(); - } if (configReader != null) { configReader.release(); } } - /** - * 初始化设置页面的 ActivityResultLauncher - */ + private void initListener() { + binding.settingsButton.setOnClickListener(view -> openSettingsActivity()); + } + private void initSettingsLauncher() { settingsLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), - result -> { - - } + result -> {} ); } - /** - * 打开设置页面 - */ + private void initWebView() { + binding.webview.getSettings().setJavaScriptEnabled(true); + binding.webview.setWebViewClient(new WebViewClient()); + } + + 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); + } + + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pendingIntent); + Log.d(TAG, "Set repeating alarm for " + time + " with command " + hex + " on port " + portPath + " at " + baudRate + " baud"); + } + } + + 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 setupConfig(String configUrl) { - // 初始化配置读取器 - configReader = new ConfigReader(this, configUrl); - configReader.setOnConfigLoadListener(new ConfigReader.OnConfigLoadListener() { - @Override - public void onConfigLoaded(String powerOffTime) { - // 配置加载成功 - Log.i("MainActivity", "PowerOffTVTime加载成功: " + powerOffTime); - // 可以在这里更新UI或执行其他操作 - } - - @Override - public void onConfigLoadFailed(String error) { - // 配置加载失败 - Log.e("MainActivity", "配置加载失败: " + error); - - // 使用本地保存的配置 - String savedTime = configReader.getSavedPowerOffTime(); - Log.i("MainActivity", "使用本地保存的PowerOffTVTime: " + savedTime); - } - }); - - // 加载配置 - configReader.loadConfig(); - - // 或者直接获取本地保存的值 - String localPowerOffTime = configReader.getSavedPowerOffTime(); - Log.i("MainActivity", "本地PowerOffTVTime: " + localPowerOffTime); - } - - private void initWebView() { - binding.webview.getSettings().setJavaScriptEnabled(true); // 启用 JavaScript - binding.webview.setWebViewClient(new WebViewClient()); // 防止跳转到外部浏览器 - } - - private void initTimer() { - // 方式1:使用建造者模式 - timer = new ScheduledCommandTimer.Builder() - .setTargetTime(14, 30) // 设置目标时间为14:30 - .setCheckInterval(10 * 1000) // 10秒检查一次 - .setCommandInterval(10 * 1000) // 命令间隔10秒 - .setMaxCommandCount(3) // 最多发送3次 - .setCommandCallback(new ScheduledCommandTimer.CommandCallback() { - @Override - public void onCommandExecute(int currentCount, int totalCount) { - // 执行你的命令逻辑 - executeCommand(currentCount, totalCount); - } - }) - .setStatusCallback(new ScheduledCommandTimer.TimerStatusCallback() { - @Override - public void onTimerStarted() { - Log.d("MainActivity", "定时器启动"); - showToast("定时器已启动"); - } - - @Override - public void onTimerStopped() { - Log.d("MainActivity", "定时器停止"); - showToast("定时器已停止"); - } - - @Override - public void onTargetTimeReached() { - Log.d("MainActivity", "到达目标时间"); - showToast("到达目标时间,开始执行命令"); - } - - @Override - public void onCommandSequenceCompleted() { - Log.d("MainActivity", "命令序列完成"); - showToast("所有命令执行完成"); - } - - @Override - public void onTimeCheck(int currentHour, int currentMinute) { - // 可选:更新UI显示当前时间 - updateTimeDisplay(currentHour, currentMinute); - } - }) - .build(); - - // 方式2:直接构造 - // timer = new ScheduledCommandTimer(14, 30); - // timer.setCommandCallback(commandCallback); - // timer.setStatusCallback(statusCallback); - } - - private void executeCommand(int currentCount, int totalCount) { - // 在这里实现你的具体命令逻辑 - Log.i("MainActivity", String.format("执行命令 %d/%d", currentCount, totalCount)); - - // 示例:发送网络请求 - // sendNetworkRequest(); - - // 示例:调用系统服务 - // callSystemService(); - - // 示例:发送广播 - // sendBroadcast(); - - runOnUiThread(() -> { - showToast(String.format("执行命令 %d/%d", currentCount, totalCount)); - }); - } - private void showToast(String message) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } - -} \ No newline at end of file +} diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/FullscreenActivity.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/FullscreenActivity.java index 44f3160..0ed32c8 100644 --- a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/FullscreenActivity.java +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/FullscreenActivity.java @@ -1,4 +1,4 @@ -package cn.ykbox.dashboard; +package cn.ykbox.dashboard.activity; import android.annotation.SuppressLint; import android.os.Build; diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/GalleryActivity.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/GalleryActivity.java index 7b3b65b..59756b0 100644 --- a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/GalleryActivity.java +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/GalleryActivity.java @@ -1,4 +1,4 @@ -package cn.ykbox.dashboard; +package cn.ykbox.dashboard.activity; import android.content.Context; import android.os.Bundle; diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/SettingsActivity.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/SettingsActivity.java index 4e17d66..f5d3726 100644 --- a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/SettingsActivity.java +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/SettingsActivity.java @@ -1,4 +1,4 @@ -package cn.ykbox.dashboard; +package cn.ykbox.dashboard.activity; import android.os.Bundle; @@ -6,6 +6,8 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.PreferenceFragmentCompat; +import cn.ykbox.dashboard.R; + public class SettingsActivity extends AppCompatActivity { @Override diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/StartActivity.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/StartActivity.java index 8e2b0ba..87934dc 100644 --- a/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/StartActivity.java +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/activity/StartActivity.java @@ -1,4 +1,4 @@ -package cn.ykbox.dashboard; +package cn.ykbox.dashboard.activity; import android.content.Context; import android.content.Intent; @@ -11,6 +11,8 @@ import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import cn.ykbox.dashboard.R; + public class StartActivity extends AppCompatActivity { private Context mContext; diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/receiver/CommandBroadcastReceiver.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/receiver/CommandBroadcastReceiver.java new file mode 100644 index 0000000..40ea0a8 --- /dev/null +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/receiver/CommandBroadcastReceiver.java @@ -0,0 +1,35 @@ +package cn.ykbox.dashboard.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import cn.ykbox.dashboard.serial.SerialControlDevices; + +public class CommandBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = "CommandReceiver"; + public static final String ACTION_SEND_COMMAND = "cn.ykbox.dashboard.ACTION_SEND_COMMAND"; + public static final String EXTRA_COMMAND_HEX = "EXTRA_COMMAND_HEX"; + public static final String EXTRA_PORT_PATH = "EXTRA_PORT_PATH"; + public static final String EXTRA_BAUD_RATE = "EXTRA_BAUD_RATE"; // 新增:波特率的 Extra Key + + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null && ACTION_SEND_COMMAND.equals(intent.getAction())) { + String hexCommand = intent.getStringExtra(EXTRA_COMMAND_HEX); + String portPath = intent.getStringExtra(EXTRA_PORT_PATH); + // 新增:从 Intent 中获取波特率,如果不存在则默认为 9600 + int baudRate = intent.getIntExtra(EXTRA_BAUD_RATE, 9600); + + if (hexCommand != null && !hexCommand.isEmpty() && portPath != null && !portPath.isEmpty()) { + Log.d(TAG, "Received alarm to send command '" + hexCommand + "' to port '" + portPath + "' at " + baudRate + " baud"); + // 使用新的静态方法发送指令 + boolean success = SerialControlDevices.sendCommand(portPath, baudRate, hexCommand); + if (!success) { + Log.e(TAG, "Failed to send command via broadcast receiver."); + } + } + } + } +} diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/serial/SerialControlDevices.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/serial/SerialControlDevices.java index be346c6..ee41528 100644 --- a/app_dashboard/src/main/java/cn/ykbox/dashboard/serial/SerialControlDevices.java +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/serial/SerialControlDevices.java @@ -1,72 +1,53 @@ -/************************************************************************* - * 版权所有 (C) 2024 宁波升维信息技术有限公司. 保留所有权利. - * - * 本软件是由 宁波升维信息技术有限公司 开发的。 - * 未经版权所有者明确授权,任何人不得使用、复制、修改、分发本软件及其相关文档。 - * 本软件包含机密和专有信息,未经授权不得向任何第三方披露。 - *************************************************************************/ +package cn.ykbox.dashboard.serial; + +import android.util.Log; +import tp.xmaihh.serialport.SerialHelper; /** - * @description: 通过串口设置班牌的功能 + * @description: 通过串口设置班牌的功能。本类提供一个静态方法用于发送单次命令。 * @author: Hu Zhang * @date: 2024/1/5 **/ - -package com.nb6868.classtv.serial; - -import android.os.Handler; -import android.os.Message; -import cn.ykbox.utils.Log; - -import cn.ykbox.signageapi.utils.ByteArrayUtil; -import tp.xmaihh.serialport.SerialHelper; -import tp.xmaihh.serialport.bean.ComBean; - - -public class SerialControlDevices extends SerialHelper { +public class SerialControlDevices { private static final String TAG = "SerialControlDevices"; + /** + * 私有构造函数,防止外部实例化此类。 + */ + private SerialControlDevices() {} - private OnErrorListener mErrorListener = null; - - public static synchronized SerialControlDevices getInstance() { - if (control == null) { - control = new 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; } - return control; - } - private SerialControlDevices() { - super("/dev/ttyS3", 9600); - try - { - super.open(); - Log.i(TAG,"串口打开成功"); + // 每次调用都创建一个新的 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, "串口打开失败:" + e.getMessage()); + Log.e(TAG, "Error sending command to port " + portPath, e); + return false; + } finally { + serialHelper.close(); } } - - public void setErrorListener(OnErrorListener listener) { - this.mErrorListener = listener; - } - - @Override - protected void onDataReceived(ComBean ComRecData) { - Log.d(TAG,"onDataReceived: "+ ByteArrayUtil.toHexString(ComRecData.bRec)); - - String serialData = ByteArrayUtil.toHexString(ComRecData.bRec); - Message msg = workHandler.obtainMessage(); - msg.what = SERIAL_MESSAGE; - msg.obj = serialData; - workHandler.sendMessage(msg); - } - - public void powerOffTV() { - sendHex("AABB0600000001060304"); - } - - public interface OnErrorListener { - void onError(int error); - } -} +} \ No newline at end of file diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/utils/ByteArrayUtil.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/utils/ByteArrayUtil.java new file mode 100644 index 0000000..8c86da0 --- /dev/null +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/utils/ByteArrayUtil.java @@ -0,0 +1,84 @@ +package cn.ykbox.dashboard.utils; + +import android.text.TextUtils; + +import java.nio.charset.StandardCharsets; + +public class ByteArrayUtil { + + public static String toHexString(final byte[] bytes) { + return toHexString(bytes, ""); + } + + public static String toHexString(final byte[] bytes, String sep) { + return toHexString(bytes, sep, true); + } + + /** + * byte 数组转化为16进制字符串 + * @param bytes 待转换的数据 + * @param sep 字符间插入的的符号 + * @param upper 是否转成大写 + * @return 格式为 0102AABBCC 的字符串 + */ + public static String toHexString(final byte[] bytes, String sep, boolean upper) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + sb.append(String.format("%02x", bytes[i] & 0xff)); + if (!TextUtils.isEmpty(sep) && i != bytes.length - 1) + sb.append(sep); + } + if(upper) + return sb.toString().toUpperCase(); + else + return sb.toString().toLowerCase(); + } + + public static String toHexString(final String input) { + return toHexString(input); + } + + public static String toHexString(final String input, String sep) { + return toHexString(input, sep, true); + } + + public static String toHexString(final String input, String sep, boolean upper) { + byte[] bytes = input.getBytes(StandardCharsets.UTF_8); + return toHexString(bytes, sep, upper); + } + + // 字符串转换为 16 进制 + public static String stringToHex(String input) { + StringBuilder hexString = new StringBuilder(); + for (char character : input.toCharArray()) { + hexString.append(Integer.toHexString((int) character)); + } + return hexString.toString(); + } + + /** + * 16进制字符串转化为 byte 数组 + * @param s 待转换的16进制字符串 + * @return byte 数据 + */ + public static byte[] toByteArray(String s) { + // 去掉所有空白字符 + s = s.replaceAll("\\s", ""); + int len = s.length(); + + // 字符串长度必须为偶数 + if(len % 2 != 0) + return null; + + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + // 必须为16进制字符 + int a = Character.digit(s.charAt(i), 16); + int b = Character.digit(s.charAt(i+1), 16); + if(a == -1 || b == -1 ) + return null; + data[i / 2] = (byte) ((byte) (a << 4) + (byte)b); + } + return data; + } +} \ No newline at end of file diff --git a/app_dashboard/src/main/java/cn/ykbox/dashboard/viewmodel/AppViewModel.java b/app_dashboard/src/main/java/cn/ykbox/dashboard/viewmodel/AppViewModel.java new file mode 100644 index 0000000..a0ac53a --- /dev/null +++ b/app_dashboard/src/main/java/cn/ykbox/dashboard/viewmodel/AppViewModel.java @@ -0,0 +1,15 @@ +package cn.ykbox.dashboard.viewmodel; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +public class AppViewModel extends ViewModel { + private static final String TAG = "AppViewModel"; + + public MutableLiveData livePowerOffTvHexString = new MutableLiveData<>(); + public MutableLiveData liveSerialConfig = new MutableLiveData<>(); + + public void updateSerialConfig(String config) { + liveSerialConfig.postValue(config); + } +} diff --git a/app_dashboard/src/main/res/layout/activity_building_dashboard.xml b/app_dashboard/src/main/res/layout/activity_building_dashboard.xml index 015b309..452a866 100644 --- a/app_dashboard/src/main/res/layout/activity_building_dashboard.xml +++ b/app_dashboard/src/main/res/layout/activity_building_dashboard.xml @@ -35,10 +35,15 @@