Compare commits

...
4 Commits
40 changed files with 977 additions and 1166 deletions
+1
View File
@@ -5,3 +5,4 @@ app/build/
build/
.kotlin/
*.pdf
.gradle-home/
+2 -23
View File
@@ -1,7 +1,6 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0"
}
android {
@@ -10,8 +9,8 @@ android {
applicationId "com.example.iot_controlhost"
minSdkVersion 25
targetSdkVersion 30
versionCode 163
versionName "3.5.0.163"
versionCode 164
versionName "3.5.0.164"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
moduleName "mcu"
@@ -66,7 +65,6 @@ android {
buildFeatures {
viewBinding true
dataBinding true
compose true
}
kotlinOptions {
jvmTarget = '17'
@@ -76,30 +74,11 @@ android {
abortOnError false
checkReleaseBuilds false
}
composeOptions {
kotlinCompilerExtensionVersion rootProject.composeVersion
}
}
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao'
dependencies {
// Compose
implementation "androidx.activity:activity-compose:$rootProject.composeVersion"
implementation "androidx.compose.runtime:runtime:$rootProject.composeVersion"
implementation "androidx.compose.ui:ui:$rootProject.composeVersion"
implementation "androidx.compose.foundation:foundation:$rootProject.composeVersion"
implementation "androidx.compose.foundation:foundation-layout:$rootProject.composeVersion"
implementation "androidx.compose.material:material:$rootProject.composeVersion"
implementation "androidx.compose.runtime:runtime-livedata:$rootProject.composeVersion"
implementation "androidx.compose.ui:ui-tooling:$rootProject.composeVersion"
implementation("androidx.compose.material3:material3:1.4.0-alpha12")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
implementation("io.github.ltttttttttttt:ComposeViews:1.6.0.1")
implementation ("io.github.ehsannarmani:compose-charts:0.1.2")
// implementation "com.google.android.material:compose-theme-adapter:$rootProject.composeVersion"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
-4
View File
@@ -82,10 +82,6 @@
android:name=".ui.activity.DebugActivity"
android:exported="true" />
<activity
android:name=".ui.activity.ComposeActivity"
android:exported="true" />
<service
android:name=".service.MQTTService"
android:enabled="true"
@@ -27,6 +27,7 @@ import com.scwang.smart.refresh.footer.ClassicsFooter;
import com.scwang.smart.refresh.header.ClassicsHeader;
import com.scwang.smart.refresh.layout.SmartRefreshLayout;
import org.xutils.BuildConfig;
import org.xutils.x;
import java.io.File;
@@ -53,6 +54,11 @@ public class MyApp extends Application {
// 初始化日志库
Timber.plant(new MyLog());
Timber.plant(new UserLog());
UserLog.system("软件生命周期", "软件启动,版本=" + BuildConfig.VERSION_NAME);
MyLog.app("软件生命周期:进程启动,版本=" + BuildConfig.VERSION_NAME
+ "versionCode=" + BuildConfig.VERSION_CODE
+ "Android=" + android.os.Build.VERSION.RELEASE
+ "SDK=" + android.os.Build.VERSION.SDK_INT);
if (OldSet.isFirstTime()) {
OldSet.importOldSet(getApplicationContext());
}
@@ -6,7 +6,6 @@ import com.example.iot_controlhost.base.BaseRecyclerAdapter;
import com.example.iot_controlhost.databinding.ItemLogBinding;
import com.example.iot_controlhost.model.Log;
import com.example.iot_controlhost.utils.global.RoomSetting;
import com.example.iot_controlhost.utils.log.UserLog;
import java.util.List;
@@ -28,7 +27,7 @@ public class LogAdapter extends BaseRecyclerAdapter<Log, ItemLogBinding> {
data.getLevel() == android.util.Log.ERROR ? R.color.red : RoomSetting.isDarkTheme() ? R.color.white : R.color.black);
binding.tvMessage.setTextColor(targetColorId);
binding.tvDate.setTextColor(targetColorId);
binding.tvMessage.setText("[" + UserLog.getCategoryName(data.getTag()) + "] " + data.getMessage());
binding.tvMessage.setText("[" + data.getTag() + "] " + data.getMessage());
binding.tvDate.setText(TimeUtils.date2String(data.getDatetime()));
}
}
@@ -11,6 +11,7 @@ import androidx.annotation.NonNull;
import androidx.databinding.ViewDataBinding;
import com.example.iot_controlhost.R;
import com.example.iot_controlhost.ui.activity.MainActivity;
import com.example.iot_controlhost.utils.log.MyLog;
import java.lang.reflect.Method;
@@ -27,11 +28,31 @@ import es.dmoral.toasty.Toasty;
public abstract class BaseDialog<Binding extends ViewDataBinding> extends Dialog {
protected Binding binding;
protected Context mContext;
private boolean lockTimerNotified;
public BaseDialog(@NonNull Context context) {
this(context, false);
}
@Override
public void show() {
super.show();
if (!lockTimerNotified && mContext instanceof MainActivity) {
lockTimerNotified = true;
((MainActivity) mContext).onDialogShown();
}
}
@Override
public void dismiss() {
boolean needNotify = lockTimerNotified;
super.dismiss();
if (needNotify && mContext instanceof MainActivity) {
lockTimerNotified = false;
((MainActivity) mContext).onDialogDismissed();
}
}
/**
* 构造函数
*
@@ -1,116 +0,0 @@
package com.example.iot_controlhost.base
import androidx.compose.material3.SnackbarHostState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.ActivityUtils
import com.example.iot_controlhost.utils.DialogUtil
import com.example.iot_controlhost.utils.PollingTask
import es.dmoral.toasty.Toasty
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
open class BaseViewModel : ViewModel() {
protected var pollingTask: PollingTask =
PollingTask.getInstance(javaClass.simpleName) // 使用类名作为ID
protected fun doInUIThread(task: () -> Unit) {
viewModelScope.launch {
withContext(Dispatchers.Main) {
task()
}
}
}
protected fun <T> doInIoThreadThenUI(
loadingTips: String = "正在加载中",
showDialog: Boolean = true,
onError: (Throwable) -> Unit = { },
onIO: suspend () -> T,
onUI: (T) -> Unit,
) {
viewModelScope.launch {
val result = runCatching {
if (showDialog) {
DialogUtil.showLoadingDialog(ActivityUtils.getTopActivity(),loadingTips)
}
withContext(Dispatchers.IO) {
onIO()
}
}
withContext(Dispatchers.Main) {
if (showDialog) {
DialogUtil.hideLoadingDialog()
}
result.onSuccess { data ->
onUI(data)
}.onFailure { exception ->
exception.printStackTrace()
onError(exception)
exception.message?.let {
Toasty.error(ActivityUtils.getTopActivity(),it).show()
}
}
}
}
}
fun <T> doInIoThread(
loadingTips: String = "正在加载中",
showDialog: Boolean = true,
onError: (Throwable) -> Unit = { },
doInIO: suspend () -> T,
) {
doInIoThreadThenUI(loadingTips, showDialog, onError, doInIO) { }
}
fun <T> doInIoThreadNoDialog(
onError: (Throwable) -> Unit = { },
task: suspend () -> T,
) {
doInIoThread(showDialog = false, doInIO = task, onError = onError)
}
/**
* 在IO线程中执行任务,可选择是否显示加载对话框
*/
fun doInIoThreadWith(showLoading: Boolean,loadingTips: String, function: suspend () -> Unit) {
if (showLoading) {
doInIoThread(loadingTips) { function() }
} else {
doInIoThreadNoDialog { function() }
}
}
val scope = CoroutineScope(Dispatchers.IO)
/**
* 启动一个无限轮询任务
*
* @param pollingInterval 轮询间隔时间(单位:秒)
* @param pollingTask 轮询任务的挂起函数
*/
fun polling(intervalSeconds: Long, task: suspend () -> Unit) {
scope.launch {
while (true) {
task()
delay(intervalSeconds * 1000L) // 转换秒为毫秒
}
}
}
suspend fun delayPolling(delaySeconds: Long, intervalSeconds: Long, task: suspend () -> Unit) {
delay(delaySeconds * 1000L)
polling(intervalSeconds, task)
}
}
@@ -4,6 +4,7 @@ import com.example.iot_controlhost.utils.MyUtil;
import com.example.iot_controlhost.utils.global.AutoModelSet;
import com.example.iot_controlhost.utils.global.RoomController;
import com.example.iot_controlhost.utils.global.SpinnerList;
import com.example.iot_controlhost.utils.log.MyLog;
/**
* @Description 换气扇(总控所有换气扇)
@@ -27,23 +28,55 @@ public class Fan extends Relay {
@Override
public boolean setPowerSupply(boolean target) {
boolean result = false;
String mode = AutoModelSet.getVentilatorMode();
boolean intakeAvailable = fans[0].isAvailable();
boolean exhaustAvailable = fans[1].isAvailable();
MyLog.auto("换气扇分路解析:目标=" + (target ? "开启" : "关闭") + ",模式=" + mode
+ ",进气扇可用=" + intakeAvailable + ",端口=" + fans[0].getPorts()
+ ";排气扇可用=" + exhaustAvailable + ",端口=" + fans[1].getPorts());
if (target) {
if (SpinnerList.VENTILATION_MODES[0].equals(AutoModelSet.getVentilatorMode())
|| SpinnerList.VENTILATION_MODES[1].equals(AutoModelSet.getVentilatorMode())) {
result = MyUtil.autoControlOperateThird(fans[0], true,false);
if ((SpinnerList.VENTILATION_MODES[0].equals(mode)
|| SpinnerList.VENTILATION_MODES[1].equals(mode)) && intakeAvailable) {
boolean intakeResult = MyUtil.autoControlOperateThird(fans[0], true,false);
MyLog.auto("换气扇分路执行:设备=进气扇,目标=开启,端口=" + fans[0].getPorts()
+ ",结果=" + (intakeResult ? "成功" : "失败"));
result = intakeResult;
}
if (SpinnerList.VENTILATION_MODES[0].equals(AutoModelSet.getVentilatorMode())
|| SpinnerList.VENTILATION_MODES[2].equals(AutoModelSet.getVentilatorMode())) {
result = MyUtil.autoControlOperateThird(fans[1], true,false) || result;
if ((SpinnerList.VENTILATION_MODES[0].equals(mode)
|| SpinnerList.VENTILATION_MODES[2].equals(mode)) && exhaustAvailable) {
boolean exhaustResult = MyUtil.autoControlOperateThird(fans[1], true,false);
MyLog.auto("换气扇分路执行:设备=排气扇,目标=开启,端口=" + fans[1].getPorts()
+ ",结果=" + (exhaustResult ? "成功" : "失败"));
result = exhaustResult || result;
}
} else {
//防止出现“仅开进气扇-开启-设置为仅开排气扇-关闭”此时进气扇无法关闭的情况 所以每次都得全部关闭(各种换气扇)
result = MyUtil.autoControlOperateThird(fans[0], false,false);
result = MyUtil.autoControlOperateThird(fans[1], false,false) || result;
if (fans[0].isAvailable()) {
boolean intakeResult = MyUtil.autoControlOperateThird(fans[0], false,false);
MyLog.auto("换气扇分路执行:设备=进气扇,目标=关闭,端口=" + fans[0].getPorts()
+ ",结果=" + (intakeResult ? "成功" : "失败"));
result = intakeResult;
}
if (fans[1].isAvailable()) {
boolean exhaustResult = MyUtil.autoControlOperateThird(fans[1], false,false);
MyLog.auto("换气扇分路执行:设备=排气扇,目标=关闭,端口=" + fans[1].getPorts()
+ ",结果=" + (exhaustResult ? "成功" : "失败"));
result = exhaustResult || result;
}
}
return result;
}
/**
* 当前换气模式下是否至少存在一个可开启的分路。
*/
public boolean hasAvailableFanForCurrentMode() {
String mode = AutoModelSet.getVentilatorMode();
return (SpinnerList.VENTILATION_MODES[0].equals(mode) && isAvailable())
|| (SpinnerList.VENTILATION_MODES[1].equals(mode) && fans[0].isAvailable())
|| (SpinnerList.VENTILATION_MODES[2].equals(mode) && fans[1].isAvailable());
}
@Override
public boolean isPowerSupply() {
return fans[0].isPowerSupply() || fans[1].isPowerSupply();
@@ -56,7 +89,7 @@ public class Fan extends Relay {
@Override
public double getVoltage() {
int total = 0;
double total = 0;
int number = 0;
if (fans[0].isAvailable()) {
total += fans[0].getVoltage();
@@ -66,11 +99,11 @@ public class Fan extends Relay {
total += fans[1].getVoltage();
number++;
}
return total / number;
return number == 0 ? 0 : total / number;
}
public double getCurrent() {
int total = 0;
double total = 0;
int number = 0;
if (fans[0].isAvailable()) {
total += fans[0].getCurrent();
@@ -80,11 +113,11 @@ public class Fan extends Relay {
total += fans[1].getCurrent();
number++;
}
return total / number;
return number == 0 ? 0 : total / number;
}
public double getPower() {
int total = 0;
double total = 0;
if (fans[0].isAvailable()) {
total += fans[0].getPower();
}
@@ -95,7 +128,7 @@ public class Fan extends Relay {
}
public double getEnergyConsumption() {
int total = 0;
double total = 0;
if (fans[0].isAvailable()) {
total += fans[0].getEnergyConsumption();
}
@@ -321,7 +321,8 @@ public class Relay extends Controller implements SetAddress {
}
public String getPowerText() {
return new DecimalFormat("0.##").format(power);
// 使用访问器以支持 Fan 等聚合继电器重写功率计算逻辑。
return new DecimalFormat("0.##").format(getPower());
}
public void setPower(double power) {
@@ -1,6 +1,7 @@
package com.example.iot_controlhost.model.thread;
import com.example.iot_controlhost.base.BaseQueue;
import com.example.iot_controlhost.utils.log.MyLog;
/**
* 任务队列-IO任务
@@ -22,7 +23,12 @@ public class QueueIOTask extends BaseQueue {
@Override
public void run() {
try {
task.run();
} catch (Throwable throwable) {
MyLog.controllerError("控制队列任务异常:异常=" + throwable.getClass().getSimpleName()
+ ",原因=" + throwable.getMessage());
}
}
}
@@ -2,6 +2,7 @@ package com.example.iot_controlhost.model.thread;
import com.example.iot_controlhost.base.BaseQueue;
import com.example.iot_controlhost.base.IRxIOTask;
import com.example.iot_controlhost.utils.log.MyLog;
import com.xuexiang.rxutil2.rxjava.RxJavaUtils;
import com.xuexiang.rxutil2.rxjava.impl.IRxUITask;
import com.xuexiang.rxutil2.rxjava.task.RxUITask;
@@ -19,12 +20,31 @@ public abstract class QueueIOUITask<T> extends BaseQueue implements IRxIOTask<T>
RxJavaUtils.doInUIThread(new RxUITask<T>(outData) {
@Override
public void doInUIThread(T o) {
try {
QueueIOUITask.this.doInUIThread(o);
} catch (Throwable throwable) {
MyLog.appError("队列UI回调异常:异常=" + throwable.getClass().getSimpleName()
+ ",原因=" + throwable.getMessage());
QueueIOUITask.this.onError(throwable);
}
}
});
} catch (Throwable throwable) {
MyLog.controllerError("队列任务执行异常:异常=" + throwable.getClass().getSimpleName()
+ ",原因=" + throwable.getMessage());
RxJavaUtils.doInUIThread(new RxUITask<Object>(null) {
@Override
public void doInUIThread(Object o) {
QueueIOUITask.this.onError(throwable);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* IO任务或UI回调失败时执行,默认只记录日志;需要清理界面的任务可重写。
*/
public void onError(Throwable throwable) {
}
}
@@ -160,6 +160,7 @@ public class MQTTService extends Service {
MyLog.network("服务器连接成功");
HardwareSetting.setRegistered();
setMQTTConnect(true);
TopicClass.flushPendingLogs();
try {
// 订阅myTopic话题
client.subscribe(TopicClass.arealistGet(), 1);
@@ -1,21 +0,0 @@
package com.example.iot_controlhost.ui.activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import com.example.iot_controlhost.ui.compose.PIDScreen
/**
* @AuthorDuanKaiji
* @Date:2023年11月22日 17:19:35
* @Declaration:设备调试界面
*/
class ComposeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PIDScreen()
}
}
}
@@ -2,6 +2,8 @@ package com.example.iot_controlhost.ui.activity;
import android.text.Editable;
import android.text.TextWatcher;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import com.blankj.utilcode.util.StringUtils;
@@ -18,6 +20,7 @@ import com.example.iot_controlhost.utils.MyQueue;
import com.example.iot_controlhost.utils.global.RoomController;
import com.example.iot_controlhost.utils.global.RoomSensor;
import com.example.iot_controlhost.utils.global.SpinnerList;
import com.example.iot_controlhost.utils.log.MyLog;
import java.util.ArrayList;
import java.util.List;
@@ -32,6 +35,10 @@ import es.dmoral.toasty.Toasty;
*/
public class DebugActivity extends BaseActivity<ActivityDebugBinding, DebugActivityPresenter> {
private static final long TEST_TIMEOUT_MS = 10_000L;
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private int testRequestGeneration;
/**
* 当前设备
*/
@@ -176,22 +183,64 @@ public class DebugActivity extends BaseActivity<ActivityDebugBinding, DebugActiv
Toasty.error(mContext, "请先选择设备当前地址").show();
return;
}
final SetAddress testDevice = setAddress;
final int requestGeneration = ++testRequestGeneration;
final long startTime = System.currentTimeMillis();
binding.btnReadAndParse.setEnabled(false);
showLoadingDialog("正在执行测试指令");
Runnable timeoutTask = () -> {
if (requestGeneration != testRequestGeneration) {
return;
}
testRequestGeneration++;
binding.tvParse.setText("测试超时,请检查串口、设备地址或队列状态");
binding.btnReadAndParse.setEnabled(true);
hideLoadingDialog();
MyLog.test("设备调试超时:设备=" + sensorType + ",型号=" + sensorModel
+ ",地址=" + sensorAddress + ",等待=" + TEST_TIMEOUT_MS + "ms");
};
mainHandler.postDelayed(timeoutTask, TEST_TIMEOUT_MS);
MyLog.test("设备调试开始:设备=" + sensorType + ",型号=" + sensorModel + ",地址=" + sensorAddress);
MyQueue.start(MyQueue.TYPE_SENSOR, new QueueIOUITask<Boolean>() {
@Override
public Boolean doInQueueThread() throws Exception {
try{
return setAddress.test(sensorModel, sensorAddress);
}catch (Exception e){
try {
return testDevice.test(sensorModel, sensorAddress);
} catch (Throwable throwable) {
MyLog.test("设备调试执行异常:设备=" + sensorType + ",型号=" + sensorModel
+ ",地址=" + sensorAddress + ",异常="
+ throwable.getClass().getSimpleName() + ",原因=" + throwable.getMessage());
return false;
}
}
@Override
public void doInUIThread(Boolean aBoolean) {
if (requestGeneration != testRequestGeneration) {
return;
}
mainHandler.removeCallbacks(timeoutTask);
try {
binding.tvSend.setText(SerialPortUtil.T);
binding.tvReceive.setText(SerialPortUtil.R);
binding.tvParse.setText(aBoolean ? toSingleLine(setAddress.getTestState()) : "数据采集失败");
binding.tvParse.setText(aBoolean ? toSingleLine(testDevice.getTestState()) : "数据采集失败");
MyLog.test("设备调试完成:设备=" + sensorType + ",型号=" + sensorModel
+ ",地址=" + sensorAddress + ",结果=" + (aBoolean ? "成功" : "失败")
+ ",耗时=" + (System.currentTimeMillis() - startTime) + "ms");
} finally {
binding.btnReadAndParse.setEnabled(true);
hideLoadingDialog();
}
}
@Override
public void onError(Throwable throwable) {
if (requestGeneration != testRequestGeneration) {
return;
}
mainHandler.removeCallbacks(timeoutTask);
binding.tvParse.setText("测试执行异常:" + throwable.getClass().getSimpleName());
binding.btnReadAndParse.setEnabled(true);
hideLoadingDialog();
}
});
@@ -266,4 +315,11 @@ public class DebugActivity extends BaseActivity<ActivityDebugBinding, DebugActiv
.replaceAll("\\s+", " ");
}
@Override
protected void onDestroy() {
testRequestGeneration++;
mainHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
}
@@ -2,6 +2,8 @@ package com.example.iot_controlhost.ui.activity;
import android.content.Intent;
import android.graphics.Color;
import android.os.Handler;
import android.os.Looper;
import android.view.MotionEvent;
import android.view.View;
@@ -42,9 +44,6 @@ import com.xuexiang.rxutil2.rxjava.task.RxAsyncTask;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import es.dmoral.toasty.Toasty;
@@ -56,6 +55,10 @@ public class MainActivity extends BaseActivity<ActivityIncubationBinding, MainAc
AppBarViewModel appBarViewModel;
WorkDialog workDialog;
private PasswordDialog lockPasswordDialog;
private final Handler lockHandler = new Handler(Looper.getMainLooper());
private final Runnable lockRunnable = this::lock;
private int showingDialogCount;
private boolean mainPageResumed;
@Override
public void initView() {
@@ -232,9 +235,6 @@ public class MainActivity extends BaseActivity<ActivityIncubationBinding, MainAc
changeModelView(RoomSetting.getMode());
binding.cardKey.setOnClickListener(v -> lock());
binding.mainEnable.setOnTouchListener((v, event) -> true);
binding.textView8.setOnClickListener(v -> {
// startActivity(new Intent(this,ComposeActivity.class));
});
binding.textView0.setOnClickListener(v -> {
// todo 消息系统开发完成后打开
// new MessageDialog(mContext).show();
@@ -280,21 +280,21 @@ public class MainActivity extends BaseActivity<ActivityIncubationBinding, MainAc
* 刷新锁屏时间
*/
private void refreshLock() {
// MyLog.app("刷新主屏幕锁时间" + RoomSetting.getMainActivityOperateLockTimeOut());
if (mainLockExecutorService != null) {
mainLockExecutorService.shutdownNow();
stopLockTimer();
if (!mainPageResumed || showingDialogCount > 0 || binding.mainLock.getVisibility() == View.VISIBLE) {
return;
}
mainLockExecutorService = Executors.newSingleThreadScheduledExecutor();
mainLockExecutorService.schedule(() -> lock(), RoomSetting.getMainActivityOperateLockTimeOut(), TimeUnit.SECONDS);
lockHandler.postDelayed(lockRunnable, RoomSetting.getMainActivityOperateLockTimeOut() * 1000L);
}
private void lock() {
if (!mainPageResumed || showingDialogCount > 0 || binding.mainLock.getVisibility() == View.VISIBLE) {
return;
}
MyLog.app("屏幕已锁");
mainLockExecutorService.shutdownNow();
runOnUiThread(() -> {
stopLockTimer();
binding.mainLock.setVisibility(View.VISIBLE);
binding.mainKey.setImageDrawable(getDrawable(R.mipmap.lock));
});
}
private void showLockPasswordDialog() {
@@ -310,15 +310,50 @@ public class MainActivity extends BaseActivity<ActivityIncubationBinding, MainAc
lockPasswordDialog.show();
}
private ScheduledExecutorService mainLockExecutorService;
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN
&& binding.mainLock.getVisibility() != View.VISIBLE) {
refreshLock();
}
return super.onTouchEvent(event);
return super.dispatchTouchEvent(event);
}
public void onDialogShown() {
showingDialogCount++;
stopLockTimer();
}
public void onDialogDismissed() {
showingDialogCount = Math.max(0, showingDialogCount - 1);
if (showingDialogCount == 0) {
refreshLock();
}
}
private void stopLockTimer() {
lockHandler.removeCallbacks(lockRunnable);
}
@Override
protected void onResume() {
super.onResume();
mainPageResumed = true;
refreshLock();
}
@Override
protected void onPause() {
mainPageResumed = false;
stopLockTimer();
super.onPause();
}
@Override
protected void onDestroy() {
stopLockTimer();
lockHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
@Override
@@ -76,7 +76,8 @@ public class MainActivityPresenter extends BasePresenter {
@Override
public Void doInIOThread(Object x) {
// 刷新共育状态
pollingTask.startPollingTaskOnIOThread(RxTag.REFRESH_CUL_STATE, Variable.REFRESH_CULTURE_STATE_TIME, () -> noticeLiveData.setValue(11));
pollingTask.startPollingTaskOnUIThread(RxTag.REFRESH_CUL_STATE,
Variable.REFRESH_CULTURE_STATE_TIME, () -> noticeLiveData.setValue(11));
//更新主界面UI
pollingTask.startPollingTaskOnIOThread(RxTag.MAIN_UI, 5, () -> {
temperatureStr.postValue(String.valueOf(RoomSensor.getAverageIndoorTemperature()));
@@ -236,14 +236,17 @@ public class SetActivity extends BaseActivity<ActivitySetBinding, SetActivityPre
// //非集成控制器则需要设置传感器串口
setList.add(new Set(getString(R.string.serial_port_sensor), HardwareSetting.getSensorSerialCom() + "\t:\t" + HardwareSetting.getSensorSerialComBaud(), view ->
new SetDeviceBaseDialog(mContext, getString(R.string.serial_port_sensor), SpinnerList.SERIAL_ADDRESS, getString(R.string.serial_port_sensor), SpinnerList.BAUD, getString(R.string.baud), (serial, baud) -> {
new SetDeviceBaseDialog(mContext, getString(R.string.serial_port_sensor),
SpinnerList.SERIAL_ADDRESS, getString(R.string.serial_port_sensor), HardwareSetting.getSensorSerialCom(),
SpinnerList.BAUD, getString(R.string.baud), HardwareSetting.getSensorSerialComBaud(), (serial, baud) -> {
HardwareSetting.setSensorSerialCom(serial);
HardwareSetting.setSensorSerialComBaud(baud);
setController();
}).show()));
setList.add(new Set(getString(R.string.serial_port_controller), HardwareSetting.getControllerSerialCom() + "\t:\t" + HardwareSetting.getControllerSerialBaud(), view ->
new SetDeviceBaseDialog(mContext, getString(R.string.serial_port_controller), SpinnerList.SERIAL_ADDRESS, getString(R.string.serial_port_sensor),
SpinnerList.BAUD, getString(R.string.baud), (serial, baud) -> {
new SetDeviceBaseDialog(mContext, getString(R.string.serial_port_controller),
SpinnerList.SERIAL_ADDRESS, getString(R.string.serial_port_sensor), HardwareSetting.getControllerSerialCom(),
SpinnerList.BAUD, getString(R.string.baud), HardwareSetting.getControllerSerialBaud(), (serial, baud) -> {
HardwareSetting.setControllerSerialCom(serial);
HardwareSetting.setControllerSerialBaud(baud);
setController();
@@ -268,7 +271,7 @@ public class SetActivity extends BaseActivity<ActivitySetBinding, SetActivityPre
setRelay();
}));
setList.add(new Set(getString(R.string.set_base), RoomController.relay.toString(), view -> new SetDeviceBaseDialog(mContext, getString(R.string.set_relay), RoomController.relay.getModels(),
getString(R.string.mode), SpinnerList.ADDRESS, getString(R.string.address), (model, address) -> {
getString(R.string.mode), RoomController.relay.getModel(), SpinnerList.ADDRESS, getString(R.string.address), RoomController.relay.getAddress(), (model, address) -> {
RoomController.relay.setModel(model);
RoomController.relay.setAddress(address);
setRelay();
@@ -308,7 +311,8 @@ public class SetActivity extends BaseActivity<ActivitySetBinding, SetActivityPre
}));
setList.add(new Set("基础配置", dehumidifier485.toString(), view ->
new SetDeviceBaseDialog(mContext, "485除湿机基础配置", dehumidifier485.getModels(),
getString(R.string.select_model), Dehumidifier485.ADDRESS_LIST, getString(R.string.select_address), (model, address) -> {
getString(R.string.select_model), dehumidifier485.getModel(), Dehumidifier485.ADDRESS_LIST,
getString(R.string.select_address), dehumidifier485.getAddress(), (model, address) -> {
dehumidifier485.setModel(model);
dehumidifier485.setAddress(address);
setDehumidifier485();
@@ -330,7 +334,8 @@ public class SetActivity extends BaseActivity<ActivitySetBinding, SetActivityPre
}));
setList.add(new Set(getString(R.string.set_base_infrared), deHumidityInfrared.toString(), view ->
new SetDeviceBaseDialog(mContext, getString(R.string.set_base_infrared), deHumidityInfrared.getModels(),
getString(R.string.select_model), SpinnerList.ADDRESS, getString(R.string.select_address), (model, address) -> {
getString(R.string.select_model), deHumidityInfrared.getModel(), SpinnerList.ADDRESS,
getString(R.string.select_address), deHumidityInfrared.getAddress(), (model, address) -> {
deHumidityInfrared.setModel(model);
deHumidityInfrared.setAddress(address);
setDeHumidityInfrared();
@@ -394,7 +399,8 @@ public class SetActivity extends BaseActivity<ActivitySetBinding, SetActivityPre
}));
setList.add(new Set(getString(R.string.set_base_infrared), airConditionInfrared.toString(), view ->
new SetDeviceBaseDialog(mContext, getString(R.string.set_base_infrared), airConditionInfrared.getModels(),
getString(R.string.select_model), SpinnerList.ADDRESS, getString(R.string.select_address), (model, address) -> {
getString(R.string.select_model), airConditionInfrared.getModel(), SpinnerList.ADDRESS,
getString(R.string.select_address), airConditionInfrared.getAddress(), (model, address) -> {
airConditionInfrared.setModel(model);
airConditionInfrared.setAddress(address);
setAirConditionDetail(airId, airConditionInfrared);
@@ -465,7 +471,8 @@ public class SetActivity extends BaseActivity<ActivitySetBinding, SetActivityPre
//需要配置地暖型号与地址
setList.add(new Set(getString(R.string.set_base), RoomController.floor.toString(), view ->
new SetDeviceBaseDialog(mContext, "地暖" + RoomSetting.getFloorName(), RoomController.floor.getModels(),
getString(R.string.mode), SpinnerList.ADDRESS, getString(R.string.address), (model, address) -> {
getString(R.string.mode), RoomController.floor.getModel(), SpinnerList.ADDRESS,
getString(R.string.address), RoomController.floor.getAddress(), (model, address) -> {
RoomController.floor.setModel(model);
RoomController.floor.setAddress(address);
setFloor();
@@ -600,15 +607,11 @@ public class SetActivity extends BaseActivity<ActivitySetBinding, SetActivityPre
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() -> RoomController.dehumidifierInfrared.setPowerSupply(true)));
new DehumidityAddDialog(mContext, RoomController.dehumidifierInfrared).show();
}));
setList.add(new Set("PID测试及自整定", null, view -> {
startActivity(new Intent(this, ComposeActivity.class));
;
}));
setAdapter.setDataList(setList);
}
private void logSettingChanged(String name, Object oldValue, Object newValue) {
UserLog.operate("本机屏幕", "修改设备设置:" + name);
UserLog.operate("本机屏幕", "设备设置变化" + name + "=" + oldValue + "" + newValue);
MyLog.app("设备设置修改:" + name + "=" + oldValue + "" + newValue);
}
@@ -1,463 +0,0 @@
package com.example.iot_controlhost.ui.compose
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.animation.core.EaseInOutCubic
import androidx.compose.animation.core.tween
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.unit.dp
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.sp
import com.example.iot_controlhost.utils.MyUtil
import com.lt.compose_views.text_field.GoodTextField
import ir.ehsannarmani.compose_charts.LineChart
import ir.ehsannarmani.compose_charts.models.AnimationMode
import ir.ehsannarmani.compose_charts.models.DrawStyle
import ir.ehsannarmani.compose_charts.models.Line
import java.util.Date
@Preview(showBackground = true, widthDp = 1280, heightDp = 800)
@Composable
fun PIDScreenPV() {
PIDScreen()
}
@Composable
fun PIDScreen(
pidViewModel: PIDViewModel = viewModel()
) {
// 获取状态
var kp by remember { mutableStateOf(pidViewModel.Kp.toString()) }
var ki by remember { mutableStateOf(pidViewModel.Ki.toString()) }
var kd by remember { mutableStateOf(pidViewModel.Kd.toString()) }
var dtInput by remember { mutableStateOf(pidViewModel.dt.toString()) }
var tt by remember { mutableStateOf(pidViewModel.targetTemperature.toString()) }
var tp by remember { mutableStateOf(pidViewModel.tp.toString()) }
val currentTemp = pidViewModel.currentTemperature
val output = pidViewModel.output
val isTuning = pidViewModel.isTuning
val isControl = pidViewModel.isControl
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "BBIT-PID控制测试系统",
fontSize = 30.sp,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
Spacer(modifier = Modifier.height(10.dp))
Row(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Title("状态图表")
LineChart(
modifier = Modifier
.weight(5f)
.padding(horizontal = 22.dp),
data =
listOf(
Line(
label = "当前温度",
values = pidViewModel.tempList,
color = SolidColor(Color(0xFF23af92)),
firstGradientFillColor = Color(0xFF2BC0A1).copy(alpha = .5f),
secondGradientFillColor = Color.Transparent,
strokeAnimationSpec = tween(2000, easing = EaseInOutCubic),
gradientAnimationDelay = 1000,
drawStyle = DrawStyle.Stroke(width = 2.dp),
)
),
animationMode = AnimationMode.Together(delayBuilder = {
it * 500L
}),
)
Row(
modifier = Modifier
.weight(2f)
.padding(horizontal = 22.dp)
) {
LineChart(
labelHelperPadding = 0.dp,
modifier = Modifier.weight(1f), data = listOf(
Line(
label = "比例增益 Kp",
values = pidViewModel.kpList,
color = SolidColor(Color(0xFF23af92)),
)
)
)
LineChart(
labelHelperPadding = 0.dp,
modifier = Modifier.weight(1f), data = listOf(
Line(
label = "积分增益 Ki",
values = pidViewModel.kiList,
color = SolidColor(Color(0xFF23af92)),
)
)
)
LineChart(
labelHelperPadding = 0.dp,
modifier = Modifier.weight(1f), data = listOf(
Line(
label = "微分增益 Kd",
values = pidViewModel.kdList,
color = SolidColor(Color(0xFF23af92)),
)
)
)
}
Title("实时参数")
LazyVerticalGrid(modifier = Modifier, columns = GridCells.Fixed(3)) {
item {
InfoText("比例增益 Kp", pidViewModel.Kp)
}
item {
InfoText("积分增益 Ki", pidViewModel.Ki)
}
item {
InfoText("微分增益 Kd", pidViewModel.Kd)
}
item {
InfoText("目标温度", pidViewModel.targetTemperature)
}
item {
InfoText("当前温度", currentTemp)
}
item {
InfoText(
"温度类型",
if (pidViewModel.isVirtualTemp) "虚拟温度" else "真实温度"
)
}
item {
InfoText("PID输出", output)
}
item {
InfoText("采样时间间隔dt", pidViewModel.dt)
}
item {
InfoText("积分项累加值", pidViewModel.integral)
}
item {
InfoText("上个误差值", pidViewModel.prevError)
}
item {
InfoText("上次温度变化斜率", pidViewModel.prevSlope)
}
item {
InfoText("上次温度值", pidViewModel.lastTemp)
}
item {
InfoText("临界比例增益ku", pidViewModel.ku)
}
item {
InfoText("临界振荡周期tu", pidViewModel.tu)
}
item {
InfoText("自整定步骤计数", pidViewModel.stepCounter)
}
item {
InfoText("PID控制", if (isControl) "开启中" else "尚未控制")
}
item {
InfoText("自整定状态", if (isTuning) "正在自整定" else "尚未开始")
}
item {
InfoText("使用虚拟地暖", if (pidViewModel.isVirtualFloor) "" else "")
}
item {
InfoText("温度采集时间间隔(s)", pidViewModel.tp)
}
}
InfoText(
"最后峰值时间",
MyUtil.getFormatDateTime(Date(pidViewModel.lastPeakTime))
)
}
VerticalDivider(modifier = Modifier.padding(horizontal = 10.dp))
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(7.dp)
) {
Row {
Column(modifier = Modifier.weight(1f)) {
val logs by pidViewModel.logs.collectAsState()
// 显示当前温度和PID输出
Title("系统日志")
Box(
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.border(1.dp, Color.LightGray)
.verticalScroll(rememberScrollState())
.padding(8.dp)
) {
Column {
logs.forEach { logLine ->
Text(logLine, fontSize = 12.sp)
}
}
}
}
Column(modifier = Modifier.weight(1f)) {
Title("设备状态")
InfoText("空调1", pidViewModel.air1State)
InfoText("空调2", pidViewModel.air2State)
InfoText("地暖", pidViewModel.floorTState)
InfoText("虚拟地暖", pidViewModel.vFloorTState)
}
}
Title("参数设置")
// 输入框:Kp, Ki, Kd, 目标温度
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
ParamInputField(
isControl,
modifier = Modifier.weight(1f),
"比例增益 Kp",
kp
) { value ->
kp = value
pidViewModel.Kp = value.toDoubleOrNull() ?: 0.0
}
ParamInputField(
isControl,
modifier = Modifier.weight(1f),
"积分增益 Ki",
ki
) { value ->
ki = value
pidViewModel.Ki = value.toDoubleOrNull() ?: 0.0
}
ParamInputField(
isControl,
modifier = Modifier.weight(1f),
"微分增益 Kd",
kd
) { value ->
kd = value
pidViewModel.Kd = value.toDoubleOrNull() ?: 0.0
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
ParamInputField(
isControl,
modifier = Modifier.weight(1f),
"采样时间间隔 dt 单位s",
dtInput
) { value ->
dtInput = value
pidViewModel.dt = value.toDoubleOrNull() ?: 0.0
}
ParamInputField(
false,
modifier = Modifier.weight(1f),
"温度采集间隔 单位s",
tp
) { value ->
tp = value
pidViewModel.tp = value.toLongOrNull() ?: 0
}
ParamInputField(
false,
modifier = Modifier.weight(1f),
"目标温度",
tt
) { value ->
tt = value
pidViewModel.targetTemperature = value.toDoubleOrNull() ?: 0.0
}
}
Title("控制")
LazyColumn {
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
Button(
modifier = Modifier.weight(1f),
onClick = { pidViewModel.switchVirtualFloor() }) {
Text("${if (pidViewModel.isVirtualFloor) "停用" else "启动"}虚拟地暖")
}
Button(
modifier = Modifier.weight(1f),
onClick = { pidViewModel.targetTemperature += 0.5 }) {
Text("增加目标温度0.5℃")
}
Button(
modifier = Modifier.weight(1f),
onClick = { pidViewModel.targetTemperature -= 0.5 }) {
Text("降低目标温度0.5℃")
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
Button(
modifier = Modifier.weight(1f),
onClick = { pidViewModel.switchVirtualTemp() }) {
Text("${if (pidViewModel.isVirtualTemp) "停用" else "启动"}虚拟温度数据")
}
Button(
enabled = pidViewModel.isVirtualTemp,
modifier = Modifier.weight(1f),
onClick = { pidViewModel.addVirtualTemp() }) {
Text("增加虚拟当前温度0.5℃")
}
Button(
enabled = pidViewModel.isVirtualTemp,
modifier = Modifier.weight(1f),
onClick = { pidViewModel.reduceVirtualTemp() }) {
Text("降低虚拟当前温度0.5℃")
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
// 按钮:启动自整定,停止自整定
Button(
enabled = !isControl,
modifier = Modifier.weight(1f),
onClick = { pidViewModel.startControl() }) {
Text("开始控制")
}
Button(
enabled = isControl,
modifier = Modifier.weight(1f),
onClick = { pidViewModel.stopControl() }) {
Text("停止控制")
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
// 按钮:启动自整定,停止自整定
Button(
enabled = !isTuning && isControl,
modifier = Modifier.weight(1f),
onClick = { pidViewModel.startAutoTune() }) {
Text("启动自整定")
}
Button(
enabled = isTuning && isControl,
modifier = Modifier.weight(1f),
onClick = { pidViewModel.stopAutoTune() }) {
Text("停止自整定并重置参数")
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
Button(
modifier = Modifier.weight(1f),
onClick = { pidViewModel.saveParams() }) {
Text("保存当前PID参数")
}
Button(
modifier = Modifier.weight(1f),
onClick = { pidViewModel.loadParams() }) {
Text("读取PID参数")
}
}
}
}
}
}
}
}
@Composable
fun InfoText(title: String, content: Any) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = title,
modifier = Modifier
.padding(8.dp)
.weight(1f),
fontSize = 16.sp
)
Text(
text = if (content is Double) {
String.format("%.2f", content)
} else content.toString(),
modifier = Modifier.padding(8.dp),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
)
}
}
@Composable
fun Title(text: String) {
Text(
text = text,
fontSize = 20.sp,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
)
}
@Composable
fun ParamInputField(
isControl: Boolean = true,
modifier: Modifier,
label: String,
value: String,
onValueChange: (String) -> Unit
) {
Column(modifier = modifier.fillMaxWidth()) {
Text(
text = label,
color = if (isControl) Color.Red else Color.Black,
modifier = Modifier.padding(start = 8.dp, top = 4.dp)
)
GoodTextField(
enabled = !isControl,
value = value,
onValueChange = { newText -> onValueChange(newText) },
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
),
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.padding(8.dp)
)
}
}
@@ -1,332 +0,0 @@
package com.example.iot_controlhost.ui.compose
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.example.iot_controlhost.base.BaseViewModel
import com.example.iot_controlhost.utils.MMKVUtil
import com.example.iot_controlhost.utils.MyUtil
import com.example.iot_controlhost.utils.global.RoomController
import com.example.iot_controlhost.utils.global.RoomSensor
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.Date
class PIDViewModel : BaseViewModel() {
private val _logs = MutableStateFlow<List<String>>(emptyList())
val logs = _logs.asStateFlow()
var Kp by mutableStateOf(5.0)
var Ki by mutableStateOf(0.1)
var Kd by mutableStateOf(1.0)
var air1State by mutableStateOf("")
var air2State by mutableStateOf("")
var floorTState by mutableStateOf("")
var targetTemperature by mutableStateOf(22.0)
var isVirtualTemp by mutableStateOf(false)
var virtualCurTemp = 16.0
var isVirtualFloor by mutableStateOf(false)
var vFloorTState by mutableStateOf("关闭")
// 采样时间间隔 单位:秒
var dt by mutableStateOf(10.0)
// 温度采集时间间隔
var tp by mutableStateOf(10L)
var currentTemperature by mutableStateOf(-1.0)
var output by mutableStateOf(0.0)
var isTuning by mutableStateOf(false)
var isControl by mutableStateOf(false)
// 积分项累加值
var integral by mutableStateOf(0.0)
// 上一个误差值
var prevError by mutableStateOf(0.0)
// 前一次温度变化斜率
var prevSlope by mutableStateOf(0.0)
// 前一次温度值(用于斜率计算)
var lastTemp by mutableStateOf(0.0)
// 自整定状态 默认空闲状态
var tuneState = TuneState.Idle
// 临界比例增益(Ziegler-Nichols参数)
var ku by mutableStateOf(0.0)
// 临界振荡周期(秒)
var tu by mutableStateOf(0.0)
// 自整定步骤计数器
var stepCounter by mutableStateOf(0)
// 最后峰值时间戳
var lastPeakTime = System.currentTimeMillis()
// 温度历史数据(用于振荡检测)
val tempHistory: ArrayDeque<Double> = ArrayDeque()
val tempList = mutableStateListOf<Double>()
val kpList = mutableStateListOf<Double>()
val kiList = mutableStateListOf<Double>()
val kdList = mutableStateListOf<Double>()
// 振荡周期记录
val periods: MutableList<Double> = mutableListOf()
init {
doInIoThreadNoDialog {
// 每10秒获取一次当前温度
polling(tp) {
air1State = RoomController.airConditionInfrared.state
air2State = RoomController.airConditionInfrared2.state
floorTState = RoomController.floor.state
currentTemperature = if (isVirtualTemp) {
virtualCurTemp
} else {
RoomSensor.getAverageIndoorTemperature()
}
if (isControl || isTuning) {
tempList.add(currentTemperature)
// tempList.add(Random(System.currentTimeMillis()).nextDouble(17.0,30.0))
}
}
}
}
var tempLog = ""
private fun log(msg: String) {
if (msg == tempLog) return
_logs.value = listOf(MyUtil.getFormatDateTime(Date()) + msg) + _logs.value
tempLog = msg
}
/**
* 开始使用PID控制
*/
fun startControl() {
doInIoThreadNoDialog {
tempList.clear()
log("开始控制")
isControl = true
while (isControl) {
delay((dt * 1000).toLong()) // 等待 dt 秒
if (isTuning) {
autoTuneStep()
kpList.add(Kp)
kiList.add(Ki)
kdList.add(Kd)
output = (Kp * (targetTemperature - currentTemperature)).coerceIn(0.0, 100.0)
} else {
output = computePID()
}
// 控制逻辑
controlLogic(output)
}
}
}
/**
* 停止PID控制
*/
fun stopControl() {
doInIoThread("正在停止所有设备") {
log("停止控制")
isControl = false
isTuning = false
RoomController.closeAllControllers()
vFloorTState = "关闭"
}
}
fun computePID(): Double {
val error = targetTemperature - currentTemperature
val proportional = Kp * error
if (Math.abs(error) < 5.0) {
integral += error * dt
// integral = integral.coerceIn(-100.0, 100.0) // 防止积分风暴
integral = integral.coerceIn(0.0, 100.0 / (Ki.takeIf { it != 0.0 } ?: 1.0))
}
val derivative = (prevError - error) / dt
prevError = error
return (proportional + Ki * integral + Kd * derivative).coerceIn(0.0, 100.0)
}
/**
* 开始自整定
*/
fun startAutoTune() {
doInIoThreadNoDialog {
tempList.clear()
log("开始自整定 PID 控制器")
tuneState = TuneState.StepUp
Kp = 1.0
Ki = 0.0
Kd = 0.0
integral = 0.0
stepCounter = 0
periods.clear()
tempHistory.clear()
kpList.clear()
kiList.clear()
kdList.clear()
log("已重置所有参数")
isTuning = true
}
}
/**
* 停止自整定
*/
fun stopAutoTune() {
doInIoThread("正在停止自整定") {
log("停止自整定")
isTuning = false
tuneState = TuneState.Idle
stepCounter = 0
integral = 0.0
prevError = 0.0
log("重置部分参数")
}
}
private fun controlLogic(output: Double) {
if (output > 50) {
log("PID:开启地暖")
RoomController.floor.setGear(5)
} else {
log("PID:关闭地暖")
RoomController.floor.setGear(0)
}
if (isVirtualFloor) {
vFloorTState = if (output > 50) "开启" else "关闭"
}
}
/**
* 自整定步骤
*/
private fun autoTuneStep() {
when (tuneState) {
// 比例增益提升阶段
TuneState.StepUp -> {
stepCounter++
if (stepCounter % 5 == 0) {
Kp *= 1.2
log("尝试 Kp: ${Kp}")
}
tempHistory.addLast(currentTemperature)
if (tempHistory.size > 10)
tempHistory.removeFirst()
if (tempHistory.size == 10 && tempHistory.max() - tempHistory.min() > 0.5) {
log("检测到振荡,进入测量阶段")
ku = Kp
tuneState = TuneState.Measure
lastPeakTime = System.currentTimeMillis()
}
if (Kp > 50.0) {
log("自整定失败")
}
}
// 振荡周期测量阶段
TuneState.Measure -> {
val now = System.currentTimeMillis()
val temp = currentTemperature
val slope = temp - lastTemp
if ((temp > lastTemp && prevSlope < 0) || (temp < lastTemp && prevSlope > 0)) {
val period = (now - lastPeakTime) / 1000.0
lastPeakTime = now
periods.add(period)
log("振荡周期: $period")
if (periods.size >= 3) {
tu = periods.average()
tuneState = TuneState.Complete
}
}
prevSlope = slope
lastTemp = temp
}
// 自整定完成状态
TuneState.Complete -> {
Kp = 0.6 * ku
Ki = 1.2 * ku / tu
Kd = 0.075 * ku * tu
log("自整定完成!Kp=${Kp}, Ki=${Ki}, Kd=${Kd}")
}
else -> {}
}
}
fun saveParams() {
doInIoThread {
// 保存参数到数据库或文件
MMKVUtil.put("pid_kp", Kp)
MMKVUtil.put("pid_ki", Ki)
MMKVUtil.put("pid_kd", Kd)
log("保存 PID 参数: Kp=$Kp, Ki=$Ki, Kd=$Kd")
}
}
fun loadParams() {
doInIoThread {
// 从数据库或文件加载参数
Kp = MMKVUtil.get("pid_kp", 5.0)
Ki = MMKVUtil.get("pid_ki", 0.1)
Kd = MMKVUtil.get("pid_kd", 1.0)
log("加载 PID 参数: Kp=$Kp, Ki=$Ki, Kd=$Kd")
}
}
fun switchVirtualTemp() {
isVirtualTemp = !isVirtualTemp
log("虚拟温度开关:$isVirtualTemp")
}
fun switchVirtualFloor() {
isVirtualFloor = !isVirtualFloor
log("虚拟地暖开关:$isVirtualFloor")
}
fun addVirtualTemp() {
doInIoThreadNoDialog {
virtualCurTemp += 0.5
log("虚拟温度增加0.5°C,当前温度:$virtualCurTemp,目标温度:$targetTemperature")
}
}
fun reduceVirtualTemp() {
doInIoThreadNoDialog {
virtualCurTemp -= 0.5
log("虚拟温度减少0.5°C,当前温度:$virtualCurTemp,目标温度:$targetTemperature")
}
}
enum class TuneState {
Idle,// 空闲状态
StepUp,// 比例增益提升阶段
Measure,// 振荡周期测量阶段
Complete// 自整定完成状态
}
}
@@ -13,6 +13,7 @@ import com.example.iot_controlhost.model.AITHMode;
import com.example.iot_controlhost.utils.MMKVUtil;
import com.example.iot_controlhost.utils.MyUtil;
import com.example.iot_controlhost.utils.database.MyAIModeUtil;
import com.example.iot_controlhost.utils.control.ventilator.VentilatorController;
import com.example.iot_controlhost.utils.global.AIModelSet;
import com.example.iot_controlhost.utils.global.AutoModelSet;
import com.example.iot_controlhost.utils.global.RxTag;
@@ -158,7 +159,9 @@ public class SetAutoParamDialog extends BaseDialog<DialogSetAutoBinding> {
MMKVUtil.put(AutoModelSet.MAX_TEMPERATURE, max);
MMKVUtil.put(AutoModelSet.MIN_TEMPERATURE, min);
MMKVUtil.put(AutoModelSet.AUTO_TEMPERATURE, enabled);
UserLog.operate("本机屏幕", "修改自动温度参数");
UserLog.operate("本机屏幕", "自动温度参数变化:启用=" + oldEnabled + "" + enabled
+ ",目标=" + oldTarget + "" + target + "℃,上限=" + oldMax + "" + max
+ "℃,下限=" + oldMin + "" + min + "");
MyLog.app("自动温度参数修改:启用=" + oldEnabled + "" + enabled
+ ",目标=" + oldTarget + "" + target + "℃,上限=" + oldMax + "" + max
+ "℃,下限=" + oldMin + "" + min + "");
@@ -185,7 +188,9 @@ public class SetAutoParamDialog extends BaseDialog<DialogSetAutoBinding> {
MMKVUtil.put(AutoModelSet.MIN_HUMIDITY, min);
MMKVUtil.put(AutoModelSet.MAX_HUMIDITY, max);
MMKVUtil.put(AutoModelSet.AUTO_HUMIDITY, humidityEnabled);
UserLog.operate("本机屏幕", "修改自动湿度参数");
UserLog.operate("本机屏幕", "自动湿度参数变化:启用=" + humidityOldEnabled + "" + humidityEnabled
+ ",目标=" + humidityOldTarget + "" + target + "% ,上限=" + humidityOldMax
+ "" + max + "% ,下限=" + humidityOldMin + "" + min + "%");
MyLog.app("自动湿度参数修改:启用=" + humidityOldEnabled + "" + humidityEnabled
+ ",目标=" + humidityOldTarget + "" + target + "% ,上限=" + humidityOldMax + "" + max
+ "% ,下限=" + humidityOldMin + "" + min + "%");
@@ -213,10 +218,13 @@ public class SetAutoParamDialog extends BaseDialog<DialogSetAutoBinding> {
MMKVUtil.put(AutoModelSet.CYCLE_STOP, stop);
MMKVUtil.put(AutoModelSet.MAX_DENSITY_CO2, maxCo2);
MMKVUtil.put(AutoModelSet.AUTO_VENTILATOR, ventilatorEnabled);
UserLog.operate("本机屏幕", "修改自动换气参数");
UserLog.operate("本机屏幕", "自动换气参数变化:启用=" + ventilatorOldEnabled + ""
+ ventilatorEnabled + ",开启=" + oldStart + "" + start + "分钟,关闭="
+ oldStop + "" + stop + "分钟,CO₂上限=" + oldMaxCo2 + "" + maxCo2 + "ppm");
MyLog.app("自动换气参数修改:启用=" + ventilatorOldEnabled + "" + ventilatorEnabled
+ ",开启=" + oldStart + "" + start + "分钟,关闭=" + oldStop + ""
+ stop + "分钟,CO₂上限=" + oldMaxCo2 + "" + maxCo2 + "ppm");
VentilatorController.getInstance().reloadSchedule("本机屏幕-自动换气参数");
dismiss();
needRefresh = true;
} catch (NumberFormatException e) {
@@ -247,7 +255,8 @@ public class SetAutoParamDialog extends BaseDialog<DialogSetAutoBinding> {
mode.setTargetTemp(temperature);
mode.setTargetHumi(humidity);
MyAIModeUtil.update(mode);
UserLog.operate("本机屏幕", "修改智能模式当前龄期温湿度参数");
UserLog.operate("本机屏幕", "智能模式当前龄期参数变化:温度=" + oldTemperature + ""
+ temperature + "℃,湿度=" + oldHumidity + "" + humidity + "%");
MyLog.app("智能模式当前龄期参数修改:温度=" + oldTemperature + "" + temperature
+ "℃,湿度=" + oldHumidity + "" + humidity + "%");
RxBusUtils.get().post(RxTag.AI_INFO, 0);
@@ -277,9 +286,11 @@ public class SetAutoParamDialog extends BaseDialog<DialogSetAutoBinding> {
mode.setTargetVentOpen(start);
mode.setTargetVentClose(stop);
MyAIModeUtil.update(mode);
UserLog.operate("本机屏幕", "修改智能模式当前龄期换气参数");
UserLog.operate("本机屏幕", "智能模式当前龄期换气参数变化:开启=" + oldStart + ""
+ start + "分钟,关闭=" + oldStop + "" + stop + "分钟");
MyLog.app("智能模式当前龄期换气参数修改:开启=" + oldStart + "" + start
+ "分钟,关闭=" + oldStop + "" + stop + "分钟");
VentilatorController.getInstance().reloadSchedule("本机屏幕-智能换气参数");
RxBusUtils.get().post(RxTag.AI_INFO, 0);
dismiss();
needRefresh = true;
@@ -20,6 +20,7 @@ import com.example.iot_controlhost.utils.MMKVUtil;
import com.example.iot_controlhost.utils.MyQueue;
import com.example.iot_controlhost.utils.MyUtil;
import com.example.iot_controlhost.utils.control.temperature.TemperatureController;
import com.example.iot_controlhost.utils.control.ventilator.VentilatorController;
import com.example.iot_controlhost.utils.database.LogDBManager;
import com.example.iot_controlhost.utils.global.AutoModelSet;
import com.example.iot_controlhost.utils.global.RoomSetting;
@@ -186,7 +187,9 @@ public class SetBaseDialog extends BaseDialog<DialogSetBinding> {
MMKVUtil.put(AutoModelSet.AUTO_TEMPERATURE_HEAT, heatMode);
MMKVUtil.put(AutoModelSet.MIN_TEMPERATURE, target - bias);
MMKVUtil.put(AutoModelSet.MAX_TEMPERATURE, target + bias);
UserLog.operate("本机屏幕", "修改设置:温度控制模式");
UserLog.operate("本机屏幕", "设置变化:温度控制模式=" + oldMode + "" + selection
+ ",温度下限=" + min + "" + (target - bias) + "℃,温度上限="
+ max + "" + (target + bias) + "");
MyLog.app("温度控制模式修改:" + oldMode + "" + selection
+ ",自动调整目标上下限为" + (target - bias) + "~" + (target + bias) + "");
updateTemperatureControl();
@@ -226,6 +229,9 @@ public class SetBaseDialog extends BaseDialog<DialogSetBinding> {
String oldValue = AutoModelSet.getVentilatorMode();
MMKVUtil.put(AutoModelSet.VENTILATOR_MODE, selection);
logSettingChanged("换气扇控制方式", oldValue, selection);
if (!oldValue.equals(selection)) {
VentilatorController.getInstance().reloadSchedule("本机屏幕-换气扇控制方式");
}
setAutoParam();
}).show()));
setList.add(new Set("新风控制", AutoModelSet.getAirExchangeMode(), view ->
@@ -294,7 +300,7 @@ public class SetBaseDialog extends BaseDialog<DialogSetBinding> {
}
private void logSettingChanged(String name, Object oldValue, Object newValue) {
UserLog.operate("本机屏幕", "修改设置:" + name);
UserLog.operate("本机屏幕", "设置变化" + name + "=" + oldValue + "" + newValue);
MyLog.app("设置修改:" + name + "=" + oldValue + "" + newValue);
}
}
@@ -32,7 +32,10 @@ public class SetDeviceBaseDialog extends BaseDialog<DialogDeviceCustomSetBinding
* @param ac2Hint 下拉框2的提示
* @param listener 点击确定事件
*/
public SetDeviceBaseDialog(@NonNull Context context, String title, String[] ac1List, String ac1Hint, String[] ac2List, String ac2Hint, OnCommonSetListener listener) {
public SetDeviceBaseDialog(@NonNull Context context, String title,
String[] ac1List, String ac1Hint, String oldValue1,
String[] ac2List, String ac2Hint, String oldValue2,
OnCommonSetListener listener) {
super(context,true);
binding.tilSpinner1.setVisibility(View.VISIBLE);
binding.tilSpinner2.setVisibility(View.VISIBLE);
@@ -51,10 +54,13 @@ public class SetDeviceBaseDialog extends BaseDialog<DialogDeviceCustomSetBinding
Toasty.info(context, ac2Hint).show();
return;
}
listener.btnSet(binding.acInput1.getText().toString(), binding.acInput2.getText().toString());
UserLog.operate("本机屏幕", "修改设备配置:" + title);
MyLog.app(title + "修改:" + ac1Hint + "=" + binding.acInput1.getText()
+ "" + ac2Hint + "=" + binding.acInput2.getText());
String newValue1 = binding.acInput1.getText().toString();
String newValue2 = binding.acInput2.getText().toString();
listener.btnSet(newValue1, newValue2);
String change = title + "配置变化:" + ac1Hint + "=" + oldValue1 + "" + newValue1
+ "" + ac2Hint + "=" + oldValue2 + "" + newValue2;
UserLog.operate("本机屏幕", change);
MyLog.app(change);
dismiss();
});
}
@@ -48,7 +48,7 @@ public class SetPWMDialog extends BaseDialog<DialogDeviceCustomSetBinding> {
}
int oldPwm = RoomController.floor.getPwm(gear).getPwm();
RoomController.floor.getPwm(gear).setPwm(pwm);
UserLog.operate("本机屏幕", "修改地暖PWM" + gear + "");
UserLog.operate("本机屏幕", "地暖PWM变化" + gear + "=" + oldPwm + "" + pwm);
MyLog.app("地暖PWM修改:档位=" + gear + "PWM=" + oldPwm + "" + pwm);
} catch (Exception e) {
Toasty.error(mContext, "输入格式有误,请检查").show();
@@ -85,7 +85,8 @@ public class SetPortDialog extends BaseDialog<DialogDeviceCustomSetBinding> {
}
//确定后关闭弹窗
RoomController.relay.setPowerSupply(false, port);
UserLog.operate("本机屏幕", "修改继电器端口配置:端口" + port);
UserLog.operate("本机屏幕", "继电器端口配置变化:端口" + port + "=" + oldDeviceName
+ "" + (deviceName.isEmpty() ? "未配置" : deviceName));
MyLog.app("继电器端口配置修改:端口=" + port + ",设备=" + oldDeviceName
+ "" + (deviceName.isEmpty() ? "未配置" : deviceName));
dismiss();
@@ -70,6 +70,9 @@ public class SetSensorDialog extends BaseDialog<DialogSetSensorBinding> {
});
binding.btnYes.setOnClickListener(button -> {
//设置事件
boolean oldEnable = sensor.isEnable();
String oldModel = sensor.getModel();
String oldAddress = sensor.getAddress();
boolean enable = binding.switcher.isChecked();
if (enable) {
String address = binding.acAddress.getText().toString();
@@ -89,9 +92,11 @@ public class SetSensorDialog extends BaseDialog<DialogSetSensorBinding> {
sensor.setEnable(enable);
//刷新页面
listener.run(sensor);
UserLog.operate("本机屏幕", "修改传感器配置" + sensor.getName());
MyLog.app("传感器配置修改:名称=" + sensor.getName() + ",启用=" + sensor.isEnable()
+ "型号=" + sensor.getModel() + ",地址=" + sensor.getAddress());
String change = "传感器配置变化:名称=" + sensor.getName() + ",启用=" + oldEnable + ""
+ sensor.isEnable() + ",型号=" + oldModel + "" + sensor.getModel()
+ "地址=" + oldAddress + "" + sensor.getAddress();
UserLog.operate("本机屏幕", change);
MyLog.app(change);
dismiss();
});
}
@@ -43,7 +43,7 @@ public class THBiasDialog extends BaseDialog<DialogBiasThBinding> {
double newValue = Double.parseDouble(binding.tiSet1.getText().toString());
MMKVUtil.put(isTemp ? RoomSetting.BIAS_TEMP : RoomSetting.BIAS_HUMIDITY, newValue);
String typeName = isTemp ? "温度" : "湿度";
UserLog.operate("本机屏幕", "修改" + typeName + "校准偏差");
UserLog.operate("本机屏幕", typeName + "校准偏差变化:" + oldValue + "" + newValue);
MyLog.app(typeName + "校准偏差修改:" + oldValue + "" + newValue);
dismiss();
});
@@ -9,6 +9,7 @@ import android.view.WindowManager;
import android.widget.TextView;
import com.example.iot_controlhost.R;
import com.example.iot_controlhost.ui.activity.MainActivity;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.xuexiang.rxutil2.rxjava.RxJavaUtils;
import com.xuexiang.rxutil2.rxjava.task.RxUITask;
@@ -17,6 +18,7 @@ public class DialogUtil {
private static Dialog progressDialog;
private static TextView textViewInfo;
private static LinearProgressIndicator progressIndicator;
private static Context progressContext;
/**
* 显示加载中弹窗,支持中途改变提示文字
@@ -26,10 +28,10 @@ public class DialogUtil {
*/
public static void showLoadingDialog(Context context, String info) {
if (progressDialog != null) {
progressDialog.dismiss(); // 如果之前有Dialog显示,则先关闭
progressDialog = null; // 释放旧Dialog
hideLoadingDialog(); // 如果之前有Dialog显示,则先关闭并同步锁屏计数
}
progressDialog = new Dialog(context, R.style.MyDialogTheme);
progressContext = context;
progressDialog.setContentView(R.layout.dialog_loading);
progressDialog.setCancelable(false);
textViewInfo = progressDialog.findViewById(R.id.tv_loading);
@@ -44,6 +46,7 @@ public class DialogUtil {
layoutParams.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN;
progressDialog.getWindow().setAttributes(layoutParams);
progressDialog.show();
notifyDialogShown(context);
}
/**
@@ -69,6 +72,9 @@ public class DialogUtil {
public static void hideLoadingDialog() {
if (progressDialog != null) {
progressDialog.dismiss();
notifyDialogDismissed(progressContext);
progressDialog = null;
progressContext = null;
}
}
@@ -79,7 +85,11 @@ public class DialogUtil {
* @param info 提示信息
*/
public static void showProgressIndicatorDialog(Context context, String info) {
if (progressDialog != null) {
hideLoadingDialog();
}
progressDialog = new Dialog(context, R.style.MyDialogTheme);
progressContext = context;
progressDialog.setContentView(R.layout.dialog_process_indicator);
progressDialog.setCancelable(false);
textViewInfo = progressDialog.findViewById(R.id.tv_loading);
@@ -97,6 +107,7 @@ public class DialogUtil {
}
textViewInfo.setText(info);
progressDialog.show();
notifyDialogShown(context);
}
/**
@@ -118,6 +129,21 @@ public class DialogUtil {
public static void hideProgressIndicatorDialog() {
if (progressDialog != null) {
progressDialog.dismiss();
notifyDialogDismissed(progressContext);
progressDialog = null;
progressContext = null;
}
}
private static void notifyDialogShown(Context context) {
if (context instanceof MainActivity) {
((MainActivity) context).onDialogShown();
}
}
private static void notifyDialogDismissed(Context context) {
if (context instanceof MainActivity) {
((MainActivity) context).onDialogDismissed();
}
}
@@ -276,6 +276,7 @@ public class MyUtil {
*/
public static void exitApp() {
UserLog.system("本机屏幕", "关闭软件");
MyLog.app("软件生命周期:收到本机关闭软件指令,开始停止自动控制和设备输出");
MyUtil.stopAppControl("正在关闭软件中", () -> AppUtils.exitApp());
}
@@ -288,6 +289,7 @@ public class MyUtil {
public static void relaunchApp(String source) {
UserLog.system(source, "重启软件");
MyLog.app("软件生命周期:收到重启软件指令,来源=" + source + ",开始停止自动控制和设备输出");
MyUtil.stopAppControl("正在重启软件中", () -> {
// AppUtils.relaunchApp(); // 这个方法重启不干净
ProcessPhoenix.triggerRebirth(MyApp.getAppContext());
@@ -303,6 +305,7 @@ public class MyUtil {
public static void shutDown(String source) {
UserLog.system(source, "关闭设备");
MyLog.app("主机生命周期:收到关闭设备指令,来源=" + source + ",开始停止自动控制和设备输出");
//先关闭所有设备
MyUtil.stopAppControl("正在关闭设备中", () -> MyAPIContext.getInstance().shutDown());
}
@@ -316,6 +319,7 @@ public class MyUtil {
public static void reboot(String source) {
UserLog.system(source, "重启设备");
MyLog.app("主机生命周期:收到重启设备指令,来源=" + source + ",开始停止自动控制和设备输出");
MyUtil.stopAppControl("正在重启设备中", () -> MyAPIContext.getInstance().reBoot());
}
@@ -370,8 +374,14 @@ public class MyUtil {
*/
public static boolean autoControlOperateThird(Controller controller, boolean targetState, boolean needAutoLog) {
if (!controller.isAvailable()) {
// 聚合设备(例如换气扇总控)会自行跳过未安装的子设备,避免每轮自动控制重复报错。
if (needAutoLog) {
MyLog.autoError("自动控制跳过:设备=" + controller.getName() + ",目标="
+ (targetState ? "开启" : "关闭") + ",原因=设备不可用");
}
return false;
}
boolean previousState = controller.isPowerSupply();
int maxCount = 3;
int count = 0;
boolean success = false;
@@ -390,6 +400,16 @@ public class MyUtil {
if (!success) {
MyLog.autoError((needAutoLog ? "自动" : "系统联动") + "操作(连续" + maxCount + "次)失败:"
+ (targetState ? "开启" : "关闭") + controller.getName());
if (needAutoLog) {
UserLog.systemError("自动控制", "设备操作失败:" + controller.getName() + ",状态="
+ (previousState ? "开启" : "关闭") + "" + (targetState ? "开启" : "关闭"));
}
} else if (needAutoLog && previousState != targetState) {
UserLog.system("自动控制", "设备状态变化:" + controller.getName() + ""
+ (previousState ? "开启" : "关闭") + "" + (targetState ? "开启" : "关闭"));
MyLog.auto("自动控制状态确认:设备=" + controller.getName() + ",原状态="
+ (previousState ? "开启" : "关闭") + ",目标状态="
+ (targetState ? "开启" : "关闭") + ",尝试次数=" + count + ",结果=成功");
}
//自动操作完设备需要更新UI
RxBusUtils.get().post(RxTag.UPDATE_AUTO, 2);
@@ -5,36 +5,39 @@ import android.os.Looper;
import androidx.annotation.NonNull;
import com.example.iot_controlhost.utils.log.MyLog;
import com.example.iot_controlhost.utils.log.UserLog;
import com.xuexiang.rxutil2.rxjava.impl.IRxUITask;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class PollingTask {
private static Map<String, PollingTask> instances = new HashMap<>();
private static final Map<String, PollingTask> instances = new ConcurrentHashMap<>();
private final String instanceId;
private ScheduledExecutorService scheduler;
private Map<String, ScheduledFuture<?>> taskMap;
private final Map<String, ScheduledFuture<?>> taskMap = new ConcurrentHashMap<>();
private final Map<String, Long> lastUserErrorTime = new ConcurrentHashMap<>();
private volatile boolean stopped;
// 私有化构造函数,确保外部不能直接实例化
private PollingTask() {
private PollingTask(String instanceId) {
this.instanceId = instanceId;
createNewScheduler();
}
// 创建新的 ScheduledExecutorService 实例
private void createNewScheduler() {
private synchronized void createNewScheduler() {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
scheduler.shutdownNow();
}
scheduler = Executors.newScheduledThreadPool(3);
taskMap = new HashMap<>();
taskMap.clear();
stopped = false;
}
/**
@@ -48,7 +51,7 @@ public class PollingTask {
// 获取基于ID的单例实例
public static synchronized PollingTask getInstance(String id) {
if (!instances.containsKey(id)) {
instances.put(id, new PollingTask());
instances.put(id, new PollingTask(id));
}
return instances.get(id);
}
@@ -59,13 +62,22 @@ public class PollingTask {
* @param delaySeconds 延迟时间(以秒为单位)
* @param task 需要执行的任务
*/
public void startDelayedTask(String taskId, long delaySeconds, @NonNull final Runnable task) {
public synchronized void startDelayedTask(String taskId, long delaySeconds, @NonNull final Runnable task) {
stopPollingTask(taskId); // 如果已有相同ID的任务,先停止
ScheduledFuture<?> future = scheduler.schedule(() -> {
task.run(); // 执行任务
}, delaySeconds, TimeUnit.SECONDS);
if (stopped) {
MyLog.auto("调度器已停止,忽略延迟任务:控制器=" + instanceId + ",任务=" + taskId);
return;
}
ensureScheduler();
try {
ScheduledFuture<?> future = scheduler.schedule(
safeRunnable(taskId, task), Math.max(0, delaySeconds), TimeUnit.SECONDS);
taskMap.put(taskId, future); // 保存任务的ScheduledFuture
MyLog.auto("调度任务已创建:控制器=" + instanceId + ",任务=" + taskId
+ ",延迟=" + Math.max(0, delaySeconds) + "");
} catch (RejectedExecutionException e) {
recordTaskError(taskId, "调度延迟任务被拒绝", e);
}
}
/**
@@ -120,17 +132,72 @@ public class PollingTask {
* @param intervalSeconds 轮询间隔时间(以秒为单位)
* @param task 需要执行的任务
*/
private void startPollingTask(String taskId, long intervalSeconds, @NonNull final Runnable task) {
private synchronized void startPollingTask(String taskId, long intervalSeconds, @NonNull final Runnable task) {
stopPollingTask(taskId); // 如果已有相同ID的任务,先停止
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS);
if (stopped) {
MyLog.auto("调度器已停止,忽略轮询任务:控制器=" + instanceId + ",任务=" + taskId);
return;
}
ensureScheduler();
long safeInterval = Math.max(1, intervalSeconds);
try {
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(
safeRunnable(taskId, task), 0, safeInterval, TimeUnit.SECONDS);
taskMap.put(taskId, future); // 保存任务的ScheduledFuture
MyLog.auto("轮询任务已创建:控制器=" + instanceId + ",任务=" + taskId
+ ",间隔=" + safeInterval + "");
} catch (RejectedExecutionException e) {
recordTaskError(taskId, "调度轮询任务被拒绝", e);
}
}
private Runnable safeRunnable(String taskId, Runnable task) {
return () -> {
try {
task.run();
} catch (Throwable throwable) {
recordTaskError(taskId, "任务执行异常,调度器将继续运行", throwable);
}
};
}
private void recordTaskError(String taskId, String action, Throwable throwable) {
String detail = action + ":控制器=" + instanceId + ",任务=" + taskId
+ ",异常=" + throwable.getClass().getSimpleName() + ",原因=" + throwable.getMessage();
MyLog.autoError(detail);
long now = System.currentTimeMillis();
long lastTime = lastUserErrorTime.getOrDefault(taskId, 0L);
if (now - lastTime >= TimeUnit.MINUTES.toMillis(5)) {
lastUserErrorTime.put(taskId, now);
UserLog.systemError("任务调度", action + "" + instanceId + "/" + taskId);
}
}
private void ensureScheduler() {
if (scheduler == null || scheduler.isShutdown() || scheduler.isTerminated()) {
MyLog.auto("自动任务调度器已失效,正在重建:控制器=" + instanceId);
createNewScheduler();
}
}
public boolean isAvailable() {
return !stopped && scheduler != null && !scheduler.isShutdown() && !scheduler.isTerminated();
}
public boolean hasActiveTasks() {
if (!isAvailable()) {
return false;
}
taskMap.entrySet().removeIf(entry -> entry.getValue() == null
|| entry.getValue().isCancelled() || entry.getValue().isDone());
return !taskMap.isEmpty();
}
// 停止所有轮询任务
public void stopAllPollingTasks() {
public synchronized void stopAllPollingTasks() {
stopped = true;
for (String taskId : taskMap.keySet()) {
MyLog.test("关闭轮询任务" + taskId);
MyLog.auto("停止调度任务:控制器=" + instanceId + ",任务=" + taskId);
}
if (scheduler != null && !scheduler.isShutdown()) {
// scheduler.shutdown(); // 停止所有任务
@@ -144,14 +211,14 @@ public class PollingTask {
taskMap.clear(); // 清空任务映射
}
// 从实例映射中移除当前实例
instances.values().remove(this);
instances.remove(instanceId, this);
}
/**
* 停止轮询任务
*
* @param taskId 任务ID
*/
public void stopPollingTask(String taskId) {
public synchronized void stopPollingTask(String taskId) {
ScheduledFuture<?> future = taskMap.get(taskId);
if (future != null) {
future.cancel(true); // 取消任务,中断正在执行的任务
@@ -14,19 +14,33 @@ public abstract class Control {
/**
* 是否在开始中
*/
protected boolean start = false;
protected volatile boolean start = false;
protected PollingTask pollingTask ;
/**
* 开始控制
*/
public void start() {
if (start) {
public synchronized void start() {
if (start && pollingTask != null && pollingTask.hasActiveTasks()) {
return;
}
if (start) {
MyLog.autoError("检测到自动控制任务已无有效调度,尝试自恢复:" + getClass().getSimpleName());
}
pollingTask = PollingTask.getInstance(getClass().getSimpleName());
try {
start = startMethod();
if (start) {
MyLog.auto("自动控制启动成功:" + getClass().getSimpleName());
} else {
MyLog.autoError("自动控制未启动:" + getClass().getSimpleName() + ",设备不可用或配置不满足");
}
} catch (Throwable throwable) {
start = false;
MyLog.autoError("自动控制启动异常:" + getClass().getSimpleName() + ",异常="
+ throwable.getClass().getSimpleName() + ",原因=" + throwable.getMessage());
}
}
/**
@@ -39,16 +53,28 @@ public abstract class Control {
/**
* 停止控制
*/
public void stop() {
public synchronized void stop() {
if(pollingTask != null){
pollingTask.stopAllPollingTasks();
}
if (!start) {
return;
}
try {
stopMethod();
RxBusUtils.get().post(RxTag.UPDATE_AUTO, 2);
MyLog.auto("自动控制停止成功:" + getClass().getSimpleName());
} catch (Throwable throwable) {
MyLog.autoError("自动控制停止异常:" + getClass().getSimpleName() + ",异常="
+ throwable.getClass().getSimpleName() + ",原因=" + throwable.getMessage());
} finally {
start = false;
pollingTask = null;
}
}
public boolean isRunning() {
return start && pollingTask != null && pollingTask.hasActiveTasks();
}
/**
@@ -15,8 +15,7 @@ import com.example.iot_controlhost.utils.global.RxTag;
import com.example.iot_controlhost.utils.global.SpinnerList;
import com.example.iot_controlhost.utils.global.Variable;
import com.example.iot_controlhost.utils.log.MyLog;
import com.xuexiang.rxutil2.rxjava.DisposablePool;
import com.xuexiang.rxutil2.rxjava.RxJavaUtils;
import com.example.iot_controlhost.utils.log.UserLog;
/**
* @Description 自动模式-控制逻辑-湿度
@@ -29,6 +28,9 @@ public class HumidityController extends Control {
*/
private static boolean needOpenToTarget = false;
private static boolean needDehumidifyToTarget = false;
private Boolean lastHumidifierTarget;
private Boolean lastDehumidifierTarget;
private boolean invalidHumidityLogged;
/**
* 目标湿度
*/
@@ -53,7 +55,23 @@ public class HumidityController extends Control {
return false;
}
pollingTask.startPollingTaskOnIOThread(RxTag.AUTO_HUMIDITY, Variable.AUTO_CHECK_TIME, () -> {
double cur = RoomSensor.getAverageIndoorHumidity();
// 自动控制必须使用未叠加校准偏差的真实湿度;校准值仅用于界面显示和上报。
double cur = RoomSensor.getBeforeCalibratedIndoorHumidity();
double calibrated = RoomSensor.getAverageIndoorHumidity();
if (cur <= 0) {
MyLog.autoError("湿度自动控制跳过:没有有效真实湿度,校准湿度=" + calibrated + "%");
if (!invalidHumidityLogged) {
UserLog.systemError("自动控制", "湿度数据无效,已停止加湿以避免持续运行");
invalidHumidityLogged = true;
}
needOpenToTarget = false;
if (RoomController.humidifier.isAvailable() && RoomController.humidifier.isPowerSupply()) {
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() ->
MyUtil.autoControlOperateThird(RoomController.humidifier, false)));
}
return;
}
invalidHumidityLogged = false;
double min = 0;
double max = 0;
if (RoomSetting.getMode() == 1) {
@@ -85,6 +103,12 @@ public class HumidityController extends Control {
needDehumidifyToTarget = false;
needDehumidify = false;
}
// 真实湿度已接近100%时,即使负校准偏差导致显示值较低,也绝不继续加湿。
if (cur >= 99) {
needHumidify = false;
needOpenToTarget = false;
}
// === 互斥处理 ===
if (needHumidify && needDehumidify) {
// 理论不会发生,保险处理
@@ -93,6 +117,18 @@ public class HumidityController extends Control {
}
boolean humidifierState = needHumidify;
boolean dehumidifierState = needDehumidify;
if (lastHumidifierTarget == null || lastHumidifierTarget != humidifierState) {
MyLog.auto("湿度控制决策变化:加湿器=" + formatState(lastHumidifierTarget) + ""
+ formatState(humidifierState) + ",真实湿度=" + cur + "%"
+ ",校准湿度=" + calibrated + "% ,目标=" + target + "% ,区间=" + min + "~" + max + "%");
lastHumidifierTarget = humidifierState;
}
if (lastDehumidifierTarget == null || lastDehumidifierTarget != dehumidifierState) {
MyLog.auto("湿度控制决策变化:除湿机=" + formatState(lastDehumidifierTarget) + ""
+ formatState(dehumidifierState) + ",真实湿度=" + cur + "%"
+ ",校准湿度=" + calibrated + "% ,目标=" + target + "% ,区间=" + min + "~" + max + "%");
lastDehumidifierTarget = dehumidifierState;
}
Controller autoDehumidifier = RoomController.getAutoDehumidifier();
int dehumidifierTargetHumidity = (int) Math.round(target);
boolean needRefreshDehumidifierSetting = autoDehumidifier instanceof Dehumidifier485
@@ -103,16 +139,20 @@ public class HumidityController extends Control {
|| autoDehumidifier.isPowerSupply() == dehumidifierState && !needRefreshDehumidifierSetting;
if (humidifierKeep && dehumidifierKeep) {
MyLog.auto("湿度-控制维持当前状态");
// 状态维持时不重复输出,避免淹没真正的状态变化和异常日志。
} else {
// 加湿器控制
if (RoomController.humidifier.isAvailable()) {
MyLog.auto("自动:" + (humidifierState ? "开启" : "关闭") + "加湿器--");
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() -> {
MyUtil.autoControlOperateThird(RoomController.humidifier, humidifierState);
boolean humidifierResult = MyUtil.autoControlOperateThird(RoomController.humidifier, humidifierState);
MyLog.auto("湿度控制执行结果:设备=加湿器,目标=" + formatState(humidifierState)
+ ",结果=" + (humidifierResult ? "成功" : "失败") + ",真实湿度=" + cur
+ "% ,校准湿度=" + calibrated + "%");
String airExchangeMode = AutoModelSet.getAirExchangeMode();
if (RoomController.airExchange.isAvailable() && airExchangeMode.equals(SpinnerList.AIR_EXCHANGE_MODES[2])) {
MyUtil.autoControlOperateThird(RoomController.airExchange, humidifierState);
boolean airExchangeResult = MyUtil.autoControlOperateThird(RoomController.airExchange, humidifierState);
MyLog.auto("湿度联动执行结果:设备=新风,目标=" + formatState(humidifierState)
+ ",结果=" + (airExchangeResult ? "成功" : "失败"));
}
}));
}
@@ -137,6 +177,9 @@ public class HumidityController extends Control {
protected void stopMethod() {
needOpenToTarget = false;
needDehumidifyToTarget = false;
lastHumidifierTarget = null;
lastDehumidifierTarget = null;
invalidHumidityLogged = false;
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() -> {
MyUtil.autoControlOperateThird(RoomController.humidifier);
Controller autoDehumidifier = RoomController.getAutoDehumidifier();
@@ -151,4 +194,11 @@ public class HumidityController extends Control {
MyLog.auto("停止自动控制湿度————————————————————————————————————————————————————");
}
private String formatState(Boolean state) {
if (state == null) {
return "未初始化";
}
return state ? "开启" : "关闭";
}
}
@@ -9,6 +9,7 @@ import com.example.iot_controlhost.utils.global.RoomSetting;
import com.example.iot_controlhost.utils.global.RxTag;
import com.example.iot_controlhost.utils.global.Variable;
import com.example.iot_controlhost.utils.log.MyLog;
import com.example.iot_controlhost.utils.log.UserLog;
import com.xuexiang.rxutil2.rxbus.RxBusUtils;
/**
@@ -153,12 +154,14 @@ public abstract class ControlStrategy {
//防止重复发送相同的提示日志
tipsTempStr = tips.toString();
MyLog.auto("自动温控:" + tips);
UserLog.system("自动控制", "温度控制状态变化:" + tips.toString().trim());
}
} else {
if (mode != -1 && temp != -1 && RoomSetting.isAirConditionForceClose()) {
tips.append(",在消毒期间无法开启空调");
}
MyLog.autoError("自动温控操作失败:" + tips);
UserLog.systemError("自动控制", "温度控制操作失败:" + tips.toString().trim());
}
}
}
@@ -3,6 +3,7 @@ package com.example.iot_controlhost.utils.control.ventilator;
import com.example.iot_controlhost.model.thread.QueueIOTask;
import com.example.iot_controlhost.utils.MyQueue;
import com.example.iot_controlhost.utils.MyUtil;
import com.example.iot_controlhost.utils.PollingTask;
import com.example.iot_controlhost.utils.control.Control;
import com.example.iot_controlhost.utils.global.AIModelSet;
import com.example.iot_controlhost.utils.global.AutoModelSet;
@@ -26,6 +27,10 @@ public class VentilatorController extends Control {
* 关闭持续时间
*/
int targetClose;
/**
* 调度代次。参数重载或停止后,旧任务即使已经进入执行阶段也不能继续串接新任务。
*/
private volatile long scheduleGeneration;
/**
* 单例模式
@@ -47,8 +52,14 @@ public class VentilatorController extends Control {
protected boolean startMethod() {
//每次开启时获取最新参数
refreshRule();
MyLog.auto("自动:首先关闭换气扇");
stopFan(0);
if (!RoomController.fan.isAvailable()) {
MyLog.autoError("换气自动控制无法启动:换气扇不可用");
return false;
}
MyLog.auto("换气自动控制初始化:先关闭换气扇,开启时长=" + targetOpen
+ "分钟,关闭时长=" + targetClose + "分钟");
long generation = ++scheduleGeneration;
stopFan(0, generation);
return true;
}
@@ -62,26 +73,63 @@ public class VentilatorController extends Control {
}
}
private void startFan(int minutes) {
MyLog.auto("自动:" + minutes + "分钟后开启换气扇");
pollingTask.startDelayedTask(RxTag.VENTILATOR_AUTO_START_FUN, minutes * 60, () -> {
private void startFan(int minutes, long generation) {
PollingTask activePollingTask = pollingTask;
if (generation != scheduleGeneration || activePollingTask == null) {
MyLog.auto("换气旧任务不再创建开启调度:任务代次=" + generation
+ ",当前代次=" + scheduleGeneration);
return;
}
MyLog.auto("换气调度:计划" + minutes + "分钟后开启换气扇");
activePollingTask.startDelayedTask(RxTag.VENTILATOR_AUTO_START_FUN, minutes * 60, () -> {
try {
if (generation != scheduleGeneration) {
MyLog.auto("换气旧任务已失效,取消开启阶段:任务代次=" + generation
+ ",当前代次=" + scheduleGeneration);
return;
}
refreshRule();
if (targetOpen == 0 && targetClose == 0) {
//如果开始停止时间均为0,则3分钟后再次检测配置项
MyLog.auto("开始停止时间均为03分钟后再次检测配置");
startFan(3);
MyLog.auto("换气控制暂停:开启和关闭时长均为03分钟后重新读取配置");
startFan(3, generation);
return;
}
// 开启换气扇
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() -> {
if (generation != scheduleGeneration) {
MyLog.auto("换气旧任务已失效,取消已入队的开启操作:任务代次=" + generation);
return;
}
String airExchangeMode = AutoModelSet.getAirExchangeMode();
RoomController.fan.setPowerSupply(true);
boolean fanResult;
if (RoomController.fan.hasAvailableFanForCurrentMode()) {
fanResult = MyUtil.autoControlOperateThird(RoomController.fan, true);
} else {
fanResult = false;
MyLog.autoError("换气控制无法开启:模式=" + AutoModelSet.getVentilatorMode()
+ ",进气扇可用=" + RoomController.intakeFan.isAvailable()
+ ",端口=" + RoomController.intakeFan.getPorts()
+ ";排气扇可用=" + RoomController.exhaustFan.isAvailable()
+ ",端口=" + RoomController.exhaustFan.getPorts());
}
MyLog.auto("换气控制执行结果:设备=换气扇,目标=开启,结果="
+ (fanResult ? "成功" : "失败") + ",计划运行=" + targetOpen + "分钟");
if (RoomController.airExchange.isAvailable() && airExchangeMode.equals(SpinnerList.AIR_EXCHANGE_MODES[1])) {
MyUtil.autoControlOperateThird(RoomController.airExchange, true);
boolean airExchangeResult = MyUtil.autoControlOperateThird(RoomController.airExchange, true);
MyLog.auto("换气联动执行结果:设备=新风,目标=开启,结果="
+ (airExchangeResult ? "成功" : "失败"));
}
}));
MyLog.auto("自动:" + targetOpen + "分钟后关闭换气扇");
stopFan(targetOpen);
MyLog.auto("换气调度:计划" + targetOpen + "分钟后关闭换气扇");
stopFan(targetOpen, generation);
} catch (Throwable throwable) {
MyLog.autoError("换气扇开启阶段异常:异常=" + throwable.getClass().getSimpleName()
+ ",原因=" + throwable.getMessage() + "1分钟后尝试恢复调度");
if (generation == scheduleGeneration) {
startFan(1, generation);
}
}
});
}
@@ -90,22 +138,89 @@ public class VentilatorController extends Control {
*
* @param minutes 该时间后检测下一步动作
*/
private void stopFan(int minutes) {
private void stopFan(int minutes, long generation) {
PollingTask activePollingTask = pollingTask;
if (generation != scheduleGeneration || activePollingTask == null) {
MyLog.auto("换气旧任务不再创建关闭调度:任务代次=" + generation
+ ",当前代次=" + scheduleGeneration);
return;
}
// 开启换气扇,持续targetOpen分钟
pollingTask.startDelayedTask(RxTag.VENTILATOR_AUTO_STOP_FUN, minutes * 60, () -> {
activePollingTask.startDelayedTask(RxTag.VENTILATOR_AUTO_STOP_FUN, minutes * 60, () -> {
try {
if (generation != scheduleGeneration) {
MyLog.auto("换气旧任务已失效,取消关闭阶段:任务代次=" + generation
+ ",当前代次=" + scheduleGeneration);
return;
}
refreshRule();
// 关闭换气扇
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() -> {
if (RoomController.airExchange.isAvailable() && AutoModelSet.getAirExchangeMode().equals(SpinnerList.AIR_EXCHANGE_MODES[1])) {
MyUtil.autoControlOperateThird(RoomController.airExchange, false);
if (generation != scheduleGeneration) {
MyLog.auto("换气旧任务已失效,取消已入队的关闭操作:任务代次=" + generation);
return;
}
RoomController.fan.setPowerSupply(false);
if (RoomController.airExchange.isAvailable() && AutoModelSet.getAirExchangeMode().equals(SpinnerList.AIR_EXCHANGE_MODES[1])) {
boolean airExchangeResult = MyUtil.autoControlOperateThird(RoomController.airExchange, false);
MyLog.auto("换气联动执行结果:设备=新风,目标=关闭,结果="
+ (airExchangeResult ? "成功" : "失败"));
}
boolean fanResult = MyUtil.autoControlOperateThird(RoomController.fan, false);
MyLog.auto("换气控制执行结果:设备=换气扇,目标=关闭,结果="
+ (fanResult ? "成功" : "失败") + ",计划停运=" + targetClose + "分钟");
}));
startFan(targetClose);
startFan(targetClose, generation);
} catch (Throwable throwable) {
MyLog.autoError("换气扇关闭阶段异常:异常=" + throwable.getClass().getSimpleName()
+ ",原因=" + throwable.getMessage() + "1分钟后尝试恢复调度");
if (generation == scheduleGeneration) {
startFan(1, generation);
}
}
});
}
/**
* 启停时长或分路模式变化后,废弃旧计时并从关闭阶段按新规则重新建立周期。
*/
public synchronized void reloadSchedule(String source) {
int previousOpen = targetOpen;
int previousClose = targetClose;
boolean wasRunning = isRunning();
refreshRule();
if (RoomSetting.getMode() == 1 && !AutoModelSet.isAutoVentilator()) {
MyLog.auto("换气调度重载:来源=" + source + ",自动换气已关闭,停止现有任务");
stop();
return;
}
if (!wasRunning) {
if (RoomSetting.getMode() == 1 && AutoModelSet.isAutoVentilator()) {
MyLog.auto("换气调度重载:来源=" + source + ",控制器未运行,按最新参数启动;开启="
+ targetOpen + "分钟,关闭=" + targetClose + "分钟");
start();
} else {
MyLog.auto("换气参数已更新但控制器当前未运行:来源=" + source + ",开启="
+ targetOpen + "分钟,关闭=" + targetClose + "分钟");
}
return;
}
long invalidGeneration = ++scheduleGeneration;
if (pollingTask != null) {
pollingTask.stopAllPollingTasks();
}
pollingTask = null;
start = false;
MyLog.auto("换气调度重载:来源=" + source + ",开启=" + previousOpen + "" + targetOpen
+ "分钟,关闭=" + previousClose + "" + targetClose + "分钟,模式="
+ AutoModelSet.getVentilatorMode() + ",失效标记=" + invalidGeneration + ",旧任务已取消");
start();
}
@Override
protected void stopMethod() {
scheduleGeneration++;
MyLog.auto("停止自动控制换气————————————————————————————————————————————————————");
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() -> {
MyUtil.autoControlOperateThird(RoomController.fan);
@@ -22,8 +22,10 @@ import com.example.iot_controlhost.utils.log.MyLog;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import android_serialport_api.SerialPortUtil;
@@ -80,6 +82,16 @@ public class RoomController {
* 485除湿机物理电源
*/
public static Dehumidifier485Relay dehumidifier485Relay = new Dehumidifier485Relay(DEVICE_NAME[17]);
/**
* 红外除湿机运行功率阈值。
* 待机时约2W,风机启动后约60-70W,压缩机启动后约300W。
*/
private static final double DEHUMIDIFIER_RUNNING_POWER = 50.0;
private static final int DEHUMIDIFIER_MISMATCH_CONFIRM_TIMES = 2;
private static int dehumidifierMismatchTimes;
private static Boolean lastDehumidifierMismatchTarget;
private static final Set<String> dehumidifierPollingLogKeys = new HashSet<>();
/**
* 加湿器
*/
@@ -295,31 +307,29 @@ public class RoomController {
}));
}
});
// 检查除湿机功率是否与除湿机物理电源状态匹配,时刻监测当前除湿机功率状态,使之与软件开关匹配
// 防止出现软件界面中开启除湿器但实际上除湿器未开启(功率不足100W)的情况
if (RoomController.dehumidifier.isAvailable() || RoomController.dehumidifier485Relay.isAvailable()) {
// 485除湿机保持原有5分钟检测逻辑。
if (RoomController.dehumidifier485Relay.isAvailable()) {
PollingTask.getInstance().startPollingTaskOnIOThread(RxTag.TAG_DEHUMIDIFIER_STATUS, 60 * 5, () -> {
if (RoomController.dehumidifier.isAvailable() && RoomController.dehumidifier.isPowerSupply() && RoomController.dehumidifier.getPower() < 100) {
MyLog.controllerError("除湿机电源已开启但功率不足100W,现尝试再次开启除湿机红外电源");//红外开关是一样的指令 所以这里等于是重新发送
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() -> RoomController.dehumidifier.setPowerSupply(true)));
} else if (RoomController.dehumidifier.isAvailable() && !RoomController.dehumidifier.isPowerSupply() && RoomController.dehumidifier.getPower() > 100) {
MyLog.controllerError("除湿机电源已关闭但功率大于100W,现尝试再次关闭除湿机红外电源");
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() -> RoomController.dehumidifier.setPowerSupply(false)));
}
if (RoomController.dehumidifier485Relay.isAvailable()
&& RoomController.dehumidifier485.isPowerSupply()
if (RoomController.dehumidifier485.isPowerSupply()
&& RoomController.dehumidifier485Relay.getPower() < 100) {
MyLog.controllerError("485除湿机电源已开启但功率不足100W,现尝试再次开启485除湿机物理电源");
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() -> RoomController.dehumidifier485Relay.setPowerSupply(true)));
} else if (RoomController.dehumidifier485Relay.isAvailable()
&& !RoomController.dehumidifier485.isPowerSupply()
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(
() -> RoomController.dehumidifier485Relay.setPowerSupply(true)));
} else if (!RoomController.dehumidifier485.isPowerSupply()
&& RoomController.dehumidifier485Relay.getPower() > 100) {
MyLog.controllerError("485除湿机电源已关闭但功率大于100W,现尝试再次关闭485除湿机物理电源");
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() -> RoomController.dehumidifier485Relay.setPowerSupply(false)));
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(
() -> RoomController.dehumidifier485Relay.setPowerSupply(false)));
}
});
}
}
// 检查除湿机功率是否与软件状态匹配。红外开关为同一条翻转指令,必须确认状态后才能补发。
if (RoomController.dehumidifier.isAvailable()) {
PollingTask.getInstance().startPollingTaskOnIOThread(RxTag.TAG_INFRARED_DEHUMIDIFIER_STATUS, 60, () ->
MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(
new QueueIOTask(RoomController::checkInfraredDehumidifierStatus)));
}
//初始化新线程,时刻监测当前空调红外状态,以使得物理开关与之匹配
PollingTask.getInstance().startPollingTaskOnIOThread(RxTag.TAG_CONTROLLER_STATE_CHECK_AIR, RoomSetting.getTimeOfAirStopRelay(), () -> {
// MyLog.test("空调后台线程:现检查是否需要关闭空调物理电源");
@@ -380,6 +390,116 @@ public class RoomController {
public static boolean doNotNeedDelayCloseDehumidifier = true;
public static boolean doNotNeedDelayCloseDehumidifier485 = true;
/**
* 根据分路功率或总功率校验红外除湿机真实状态。
*/
private static void checkInfraredDehumidifierStatus() {
if (!RoomController.dehumidifier.isAvailable()) {
resetDehumidifierMismatch();
return;
}
boolean useBranchPower = RoomController.relay.getModel().equals(RoomController.relay.getModels()[2]);
double power;
String powerSource;
if (useBranchPower) {
if (!RoomController.dehumidifier.collectSensorData()) {
resetDehumidifierMismatchCount();
logDehumidifierPollingOnce("branch-collect-failed", "红外除湿机状态检测:BBIT继电器分路功率采集失败,本轮不判断状态", true);
return;
}
power = RoomController.dehumidifier.getPower();
powerSource = "BBIT继电器分路功率";
} else {
if (!RoomSensor.consumptionSensor.isAvailable()) {
resetDehumidifierMismatchCount();
logDehumidifierPollingOnce("total-unavailable", "红外除湿机状态检测:无BBIT继电器分路功率,且总能耗传感器不可用,本轮不判断状态", true);
return;
}
if (hasOtherPoweredDeviceForDehumidifierCheck()) {
resetDehumidifierMismatchCount();
logDehumidifierPollingOnce("other-device-running", "红外除湿机状态检测:其他设备正在运行,无法从总功率中分离除湿机功率,本轮不判断状态", false);
return;
}
if (!RoomSensor.consumptionSensor.collectSensorData(
RoomSensor.consumptionSensor.getModel(), RoomSensor.consumptionSensor.getAddress())) {
resetDehumidifierMismatchCount();
logDehumidifierPollingOnce("total-collect-failed", "红外除湿机状态检测:总能耗功率采集失败,本轮不判断状态", true);
return;
}
power = RoomSensor.consumptionSensor.getPower();
powerSource = "总能耗功率";
}
boolean targetState = RoomController.dehumidifier.isPowerSupply();
boolean actualState = power > DEHUMIDIFIER_RUNNING_POWER;
if (targetState == actualState) {
if (lastDehumidifierMismatchTarget != null) {
MyLog.controller("红外除湿机状态恢复一致:目标=" + formatPowerState(targetState)
+ "" + powerSource + "=" + power + "W");
}
resetDehumidifierMismatch();
return;
}
if (lastDehumidifierMismatchTarget == null || lastDehumidifierMismatchTarget != targetState) {
dehumidifierMismatchTimes = 0;
lastDehumidifierMismatchTarget = targetState;
dehumidifierPollingLogKeys.clear();
}
dehumidifierMismatchTimes++;
logDehumidifierPollingOnce("mismatch-" + targetState,
"红外除湿机状态不一致:目标=" + formatPowerState(targetState)
+ ",检测=" + formatPowerState(actualState) + "" + powerSource + "=" + power
+ "W,将连续确认" + DEHUMIDIFIER_MISMATCH_CONFIRM_TIMES + "次后纠正", true);
if (dehumidifierMismatchTimes < DEHUMIDIFIER_MISMATCH_CONFIRM_TIMES) {
return;
}
boolean result = RoomController.dehumidifier.setPowerSupply(targetState);
logDehumidifierPollingOnce("correct-" + targetState + "-" + result,
"红外除湿机状态纠正:重新发送" + formatPowerState(targetState) + "指令,结果="
+ (result ? "成功" : "失败") + ",纠正前" + powerSource + "=" + power + "W", !result);
dehumidifierMismatchTimes = 0;
}
/**
* 使用总能耗功率时,只有除湿机之外的设备均未开启,功率才可归属于除湿机。
*/
private static boolean hasOtherPoweredDeviceForDehumidifierCheck() {
for (Relay relayDevice : RoomController.getAllAvailableRelays()) {
if (relayDevice != RoomController.dehumidifier && relayDevice.isPowerSupply()) {
return true;
}
}
return RoomController.dehumidifier485.isAvailable() && RoomController.dehumidifier485.isPowerSupply();
}
private static void resetDehumidifierMismatchCount() {
dehumidifierMismatchTimes = 0;
}
private static void resetDehumidifierMismatch() {
dehumidifierMismatchTimes = 0;
lastDehumidifierMismatchTarget = null;
dehumidifierPollingLogKeys.clear();
}
private static void logDehumidifierPollingOnce(String key, String message, boolean error) {
if (!dehumidifierPollingLogKeys.add(key)) {
return;
}
if (error) {
MyLog.controllerError(message);
} else {
MyLog.controller(message);
}
}
private static String formatPowerState(boolean state) {
return state ? "开启" : "关闭";
}
public static void closeAllControllers() {
List<Relay> relays = RoomController.getAllAvailableRelays();
MyInfraredUtils.getInstances().setPowerSupply(false);
@@ -199,36 +199,55 @@ public class RoomSensor {
}
/**
* 获取室内平均湿度
* 获取未叠加校准偏差的室内平均真实湿度,供自动控制和安全保护使用。
*/
public static double getAverageIndoorHumidity() {
public static double getBeforeCalibratedIndoorHumidity() {
double sumOfHumidities = 0.0;
int numberOfEnabledSensors = 0;
for (THSensor sensor : dynamicTHSensorList) {
if (sensor.humidity == 0 || sensor.humidity == 100) {
if (sensor.humidity <= 0 || sensor.humidity > 100) {
continue;
}
sumOfHumidities += sensor.humidity;
numberOfEnabledSensors++;
}
for (GasSensor sensor : dynamicCO2SensorList) {
if (sensor.getHumidity() == 0 || sensor.getHumidity() == 100) {
if (sensor.getHumidity() <= 0 || sensor.getHumidity() > 100) {
continue;
}
sumOfHumidities += sensor.getHumidity();
numberOfEnabledSensors++;
}
for (GasSensor sensor : dynamicAmmoniaSensorList) {
if (sensor.getHumidity() == 0 || sensor.getHumidity() == 100) {
if (sensor.getHumidity() <= 0 || sensor.getHumidity() > 100) {
continue;
}
sumOfHumidities += sensor.getHumidity();
numberOfEnabledSensors++;
}
if (numberOfEnabledSensors == 0 || sumOfHumidities == 0) {
if (numberOfEnabledSensors == 0) {
return 0.0;
}
return Math.round((sumOfHumidities / numberOfEnabledSensors + RoomSetting.getBiasHumidity()) * 10.0) / 10.0;
return Math.round(sumOfHumidities / numberOfEnabledSensors * 10.0) / 10.0;
}
/**
* 获取叠加校准偏差后的室内平均湿度,供界面显示和上报使用。
*/
public static double getAverageIndoorHumidity() {
double averageHumidity = getBeforeCalibratedIndoorHumidity();
if (averageHumidity == 0.0) {
return 0.0;
}
return Math.min(
Math.round((averageHumidity + RoomSetting.getBiasHumidity()) * 10.0) / 10.0,
99.9
);
}
/**
@@ -60,6 +60,7 @@ public class RxTag {
* 持续检测除湿机工作状态
*/
public static final String TAG_DEHUMIDIFIER_STATUS = "TAG_DEHUMIDIFIER_STATUS";
public static final String TAG_INFRARED_DEHUMIDIFIER_STATUS = "TAG_INFRARED_DEHUMIDIFIER_STATUS";
/**
* 持续判断是否注册
*/
@@ -66,8 +66,21 @@ public class MyLog extends Timber.Tree {
public static final String TAG_FRP = "远程协助";
public static List<String> getUserQueryTag() {
return getSystemLogTags();
}
/**
* 系统诊断日志的全部落库标签包含普通和错误级别
*/
public static List<String> getSystemLogTags() {
List<String> tags = new ArrayList<>();
Collections.addAll(tags, TAG_APP, TAG_REMOTE, TAG_CONTROLLER, TAG_WARNING,TAG_SENSOR, TAG_NETWORK, TAG_AUTO);
List<String> baseTags = new ArrayList<>();
Collections.addAll(baseTags, TAG_TEST, TAG_WARNING, TAG_SENSOR, TAG_CONTROLLER, TAG_NETWORK,
TAG_APP, TAG_REMOTE, TAG_AUTO, TAG_FACE, TAG_FRP);
for (String tag : baseTags) {
tags.add(tag);
tags.add(tag + TAG_ERROR);
}
return tags;
}
@@ -17,11 +17,19 @@ import timber.log.Timber;
* 日志用户查看用
*/
public class UserLog extends Timber.Tree {
public static final String FILTER_ALL = "全部上报日志";
public static final String FILTER_ALL = "全部日志";
public static final String FILTER_USER_ALL = "全部用户日志";
public static final String FILTER_DIAGNOSTIC = "系统诊断日志";
public static final String FILTER_DIAGNOSTIC_AUTO = "自动控制诊断";
public static final String FILTER_DIAGNOSTIC_CONTROLLER = "控制器诊断";
public static final String FILTER_DIAGNOSTIC_SENSOR = "传感器诊断";
public static final String FILTER_DIAGNOSTIC_NETWORK = "网络诊断";
public static final String FILTER_OPERATE = "操作日志";
public static final String FILTER_USER_SENSOR = "用户传感器日志";
public static final String FILTER_SENSOR = "传感器日志";
public static final String FILTER_CONTROLLER = "控制器日志";
public static final String FILTER_TASK = "作业日志";
public static final String FILTER_SYSTEM = "系统日志";
public static final String FILTER_SYSTEM = "用户系统日志";
/**
* 用户-操作类
@@ -47,17 +55,34 @@ public class UserLog extends Timber.Tree {
}
public static List<String> getUserQueryFilters() {
return Arrays.asList(FILTER_ALL, FILTER_OPERATE, FILTER_SENSOR, FILTER_TASK, FILTER_SYSTEM);
return Arrays.asList(FILTER_ALL, FILTER_USER_ALL, FILTER_OPERATE, FILTER_USER_SENSOR,
FILTER_TASK, FILTER_SYSTEM, FILTER_DIAGNOSTIC, FILTER_DIAGNOSTIC_AUTO,
FILTER_CONTROLLER, FILTER_SENSOR, FILTER_DIAGNOSTIC_NETWORK);
}
/**
* 将界面筛选项转换为实际存储标签成功和错误级别都包含在同一分类中
*/
public static List<String> getStorageTagsForFilter(String filter) {
if (FILTER_DIAGNOSTIC.equals(filter)) {
return MyLog.getSystemLogTags();
}
if (FILTER_DIAGNOSTIC_AUTO.equals(filter)) {
return tagsWithError(MyLog.TAG_AUTO);
}
if (FILTER_DIAGNOSTIC_CONTROLLER.equals(filter) || FILTER_CONTROLLER.equals(filter)) {
return tagsWithError(MyLog.TAG_CONTROLLER);
}
if (FILTER_DIAGNOSTIC_SENSOR.equals(filter) || FILTER_SENSOR.equals(filter)) {
return tagsWithError(MyLog.TAG_SENSOR);
}
if (FILTER_DIAGNOSTIC_NETWORK.equals(filter)) {
return tagsWithError(MyLog.TAG_NETWORK);
}
if (FILTER_OPERATE.equals(filter) || TAG_USER_OPERATE.equals(filter)) {
return tagsWithError(TAG_USER_OPERATE);
}
if (FILTER_SENSOR.equals(filter) || TAG_USER_SENSOR.equals(filter)) {
if (FILTER_USER_SENSOR.equals(filter) || TAG_USER_SENSOR.equals(filter)) {
return tagsWithError(TAG_USER_SENSOR);
}
if (FILTER_TASK.equals(filter) || TAG_USER_TASK.equals(filter)) {
@@ -70,6 +95,9 @@ public class UserLog extends Timber.Tree {
for (String userTag : getUserLogTags()) {
tags.addAll(tagsWithError(userTag));
}
if (FILTER_ALL.equals(filter) || filter == null) {
tags.addAll(MyLog.getSystemLogTags());
}
return tags;
}
@@ -78,19 +106,7 @@ public class UserLog extends Timber.Tree {
}
public static String getCategoryName(String tag) {
if (tag == null) {
return "未知";
}
if (tag.contains(TAG_USER_OPERATE)) {
return "操作";
}
if (tag.contains(TAG_USER_SENSOR)) {
return "传感器";
}
if (tag.contains(TAG_USER_TASK)) {
return "作业";
}
return "系统";
return tag == null ? "未知" : tag;
}
public static void operate(String msg) {
@@ -137,6 +153,10 @@ public class UserLog extends Timber.Tree {
Timber.tag(TAG_USER_SYSTEM + MyLog.TAG_ERROR).e(msg);
}
public static void systemError(String source, String msg) {
systemError(formatSource(source, msg));
}
/**
* 崩溃场景使用非阻塞入口防止数据库繁忙时阻断进程重启
*/
@@ -16,6 +16,7 @@ import com.example.iot_controlhost.utils.MyQueue;
import com.example.iot_controlhost.utils.MyUtil;
import com.example.iot_controlhost.utils.control.RemoteControl;
import com.example.iot_controlhost.utils.control.infrared.MyInfraredUtils;
import com.example.iot_controlhost.utils.control.ventilator.VentilatorController;
import com.example.iot_controlhost.utils.global.AIModelSet;
import com.example.iot_controlhost.utils.global.AutoModelSet;
import com.example.iot_controlhost.utils.global.HardwareSetting;
@@ -35,6 +36,8 @@ import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* MQTT 主题
@@ -47,6 +50,8 @@ public class TopicClass {
private static String areaDataTemp = "";
private static String sensorDataTemp = "";
private static String sensorInfoDataTemp = "";
private static final int MAX_PENDING_LOGS = 200;
private static final Queue<PendingLog> PENDING_LOGS = new ConcurrentLinkedQueue<>();
//获取区域集合
@@ -309,16 +314,56 @@ public class TopicClass {
}
public static void uploadLog(int tag, int priority, String message, Date datetime) {
PendingLog pendingLog = new PendingLog(tag, priority, message, datetime);
if (!MQTTService.getMQTTConnect()) {
while (PENDING_LOGS.size() >= MAX_PENDING_LOGS) {
PENDING_LOGS.poll();
}
PENDING_LOGS.offer(pendingLog);
MyLog.network("用户日志等待MQTT连接后上传:类型=" + tag + ",级别=" + priority
+ ",待上传=" + PENDING_LOGS.size() + "");
return;
}
publishLog(pendingLog);
}
private static void publishLog(PendingLog pendingLog) {
JSONObject json = new JSONObject();
try {
json.put("areacode", RoomSetting.getAreaCode());
json.put("tage", tag);
json.put("msglevel", priority);
json.put("msg", message);
json.put("datetime", MyUtil.getDateTime(datetime));
json.put("tage", pendingLog.tag);
json.put("msglevel", pendingLog.priority);
json.put("msg", pendingLog.message);
json.put("datetime", MyUtil.getDateTime(pendingLog.datetime));
MQTTService.publish(DeviceRunLogsPost(), json.toString());
} catch (JSONException e) {
e.printStackTrace();
MyLog.networkError("用户日志序列化失败:类型=" + pendingLog.tag + ",原因=" + e.getMessage());
}
}
public static void flushPendingLogs() {
int count = 0;
PendingLog pendingLog;
while (MQTTService.getMQTTConnect() && (pendingLog = PENDING_LOGS.poll()) != null) {
publishLog(pendingLog);
count++;
}
if (count > 0) {
MyLog.network("MQTT连接恢复,已补传本次进程缓存的用户日志" + count + "");
}
}
private static final class PendingLog {
private final int tag;
private final int priority;
private final String message;
private final Date datetime;
private PendingLog(int tag, int priority, String message, Date datetime) {
this.tag = tag;
this.priority = priority;
this.message = message;
this.datetime = datetime;
}
}
@@ -564,6 +609,18 @@ public class TopicClass {
//唯一区域码 之前通过唯一区域码获得索引现在就一个房间所以不需要
String code = jsons.getString("code");
if (RoomSetting.getAreaCode().equals(code)) {
String oldProgram = AIModelSet.getAiSchemeName();
double oldTempMax = AutoModelSet.getMaxTemperature();
double oldTempMin = AutoModelSet.getMinTemperature();
double oldHumidityMax = AutoModelSet.getMaxHumidity();
double oldHumidityMin = AutoModelSet.getMinHumidity();
boolean oldAutoTemperature = AutoModelSet.isAutoTemperature();
double oldTargetTemperature = AutoModelSet.getTargetTemperature();
boolean oldAutoHumidity = AutoModelSet.isAutoHumidity();
double oldTargetHumidity = AutoModelSet.getTargetHumidity();
boolean oldAutoVentilator = AutoModelSet.isAutoVentilator();
int oldVentilatorOpen = AutoModelSet.getCycleStart();
int oldVentilatorClose = AutoModelSet.getCycleStop();
//预警值
MMKVUtil.put(AIModelSet.AI_SCHEME_NAME, jsons.getString("autocontrolprogram"));
MMKVUtil.put(AutoModelSet.MAX_TEMPERATURE, jsons.getDouble("tempmaxwarning"));
@@ -600,12 +657,32 @@ public class TopicClass {
MMKVUtil.put(AutoModelSet.AUTO_VENTILATOR, jsons.getInt("autovent") != 0);
MMKVUtil.put(AutoModelSet.CYCLE_START, jsons.getInt("autoventopenvalue"));
MMKVUtil.put(AutoModelSet.CYCLE_STOP, jsons.getInt("autoventclosevalue"));
boolean ventilatorRuleChanged = oldAutoVentilator != AutoModelSet.isAutoVentilator()
|| oldVentilatorOpen != AutoModelSet.getCycleStart()
|| oldVentilatorClose != AutoModelSet.getCycleStop();
//2025年5月7日取消以下逻辑
// //切换为自动模式
// RxBusUtils.get().post(RxTag.UPDATE_MAIN, 3);
UserLog.operate("远程控制", "自动模式参数更新成功");
String changeSummary = "自动模式参数更新:方案=" + oldProgram + "" + AIModelSet.getAiSchemeName()
+ ";温控启用=" + oldAutoTemperature + "" + AutoModelSet.isAutoTemperature()
+ ",目标=" + oldTargetTemperature + "" + AutoModelSet.getTargetTemperature() + ""
+ ",区间=" + oldTempMin + "~" + oldTempMax + ""
+ AutoModelSet.getMinTemperature() + "~" + AutoModelSet.getMaxTemperature() + ""
+ ";湿控启用=" + oldAutoHumidity + "" + AutoModelSet.isAutoHumidity()
+ ",目标=" + oldTargetHumidity + "" + AutoModelSet.getTargetHumidity() + "%"
+ ",区间=" + oldHumidityMin + "~" + oldHumidityMax + ""
+ AutoModelSet.getMinHumidity() + "~" + AutoModelSet.getMaxHumidity() + "%"
+ ";换气启用=" + oldAutoVentilator + "" + AutoModelSet.isAutoVentilator()
+ ",开启=" + oldVentilatorOpen + "" + AutoModelSet.getCycleStart() + "分钟"
+ ",关闭=" + oldVentilatorClose + "" + AutoModelSet.getCycleStop() + "分钟";
UserLog.operate("远程控制", changeSummary);
MyLog.remote(changeSummary + ",区域码=" + code);
if (RoomSetting.getMode() == 1) {
if (ventilatorRuleChanged) {
// 控制逻辑直接响应远程参数不依赖自动模式页面及其Presenter是否存活
VentilatorController.getInstance().reloadSchedule("远程控制");
}
// 如果是自动模式则刷新自动模式参数
RxBusUtils.get().post(RxTag.UPDATE_AUTO, 3);
}
-1
View File
@@ -1,4 +1,3 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableJetifier=true
composeVersion=1.8.0