diff --git a/app/build.gradle b/app/build.gradle index ba5e25e..54a64f4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' @@ -196,4 +175,4 @@ dependencies { // // // optional - Test helpers for Lifecycle runtime // testImplementation "androidx.lifecycle:lifecycle-runtime-testing:$lifecycle_version" -} \ No newline at end of file +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 802783f..d8f7559 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -82,10 +82,6 @@ android:name=".ui.activity.DebugActivity" android:exported="true" /> - - - \ No newline at end of file + diff --git a/app/src/main/java/com/example/iot_controlhost/base/BaseViewModel.kt b/app/src/main/java/com/example/iot_controlhost/base/BaseViewModel.kt deleted file mode 100644 index d6aa005..0000000 --- a/app/src/main/java/com/example/iot_controlhost/base/BaseViewModel.kt +++ /dev/null @@ -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 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 doInIoThread( - loadingTips: String = "正在加载中", - showDialog: Boolean = true, - onError: (Throwable) -> Unit = { }, - doInIO: suspend () -> T, - ) { - doInIoThreadThenUI(loadingTips, showDialog, onError, doInIO) { } - } - - fun 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) - } - -} diff --git a/app/src/main/java/com/example/iot_controlhost/model/controller/relay/Fan.java b/app/src/main/java/com/example/iot_controlhost/model/controller/relay/Fan.java index 5e2d396..3fa7660 100644 --- a/app/src/main/java/com/example/iot_controlhost/model/controller/relay/Fan.java +++ b/app/src/main/java/com/example/iot_controlhost/model/controller/relay/Fan.java @@ -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,29 +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())) - && fans[0].isAvailable()) { - 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())) - && fans[1].isAvailable()) { - 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 { //防止出现“仅开进气扇-开启-设置为仅开排气扇-关闭”此时进气扇无法关闭的情况 所以每次都得全部关闭(各种换气扇) if (fans[0].isAvailable()) { - result = MyUtil.autoControlOperateThird(fans[0], false,false); + boolean intakeResult = MyUtil.autoControlOperateThird(fans[0], false,false); + MyLog.auto("换气扇分路执行:设备=进气扇,目标=关闭,端口=" + fans[0].getPorts() + + ",结果=" + (intakeResult ? "成功" : "失败")); + result = intakeResult; } if (fans[1].isAvailable()) { - result = MyUtil.autoControlOperateThird(fans[1], false,false) || result; + 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(); @@ -62,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(); @@ -72,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(); @@ -86,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(); } @@ -101,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(); } diff --git a/app/src/main/java/com/example/iot_controlhost/model/controller/relay/Relay.java b/app/src/main/java/com/example/iot_controlhost/model/controller/relay/Relay.java index 3ac14b7..21d9ce0 100644 --- a/app/src/main/java/com/example/iot_controlhost/model/controller/relay/Relay.java +++ b/app/src/main/java/com/example/iot_controlhost/model/controller/relay/Relay.java @@ -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) { diff --git a/app/src/main/java/com/example/iot_controlhost/ui/activity/ComposeActivity.kt b/app/src/main/java/com/example/iot_controlhost/ui/activity/ComposeActivity.kt deleted file mode 100644 index c131836..0000000 --- a/app/src/main/java/com/example/iot_controlhost/ui/activity/ComposeActivity.kt +++ /dev/null @@ -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 - -/** - * @Author:DuanKaiji - * @Date:2023年11月22日 17:19:35 - * @Declaration:设备调试界面 - */ -class ComposeActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - PIDScreen() - } - } -} diff --git a/app/src/main/java/com/example/iot_controlhost/ui/activity/MainActivity.java b/app/src/main/java/com/example/iot_controlhost/ui/activity/MainActivity.java index 308c122..308dc89 100644 --- a/app/src/main/java/com/example/iot_controlhost/ui/activity/MainActivity.java +++ b/app/src/main/java/com/example/iot_controlhost/ui/activity/MainActivity.java @@ -235,9 +235,6 @@ public class MainActivity extends BaseActivity 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(); diff --git a/app/src/main/java/com/example/iot_controlhost/ui/activity/MainActivityPresenter.java b/app/src/main/java/com/example/iot_controlhost/ui/activity/MainActivityPresenter.java index 66855a3..c2b7e57 100644 --- a/app/src/main/java/com/example/iot_controlhost/ui/activity/MainActivityPresenter.java +++ b/app/src/main/java/com/example/iot_controlhost/ui/activity/MainActivityPresenter.java @@ -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())); diff --git a/app/src/main/java/com/example/iot_controlhost/ui/activity/SetActivity.java b/app/src/main/java/com/example/iot_controlhost/ui/activity/SetActivity.java index 04d6cfe..b4958a1 100644 --- a/app/src/main/java/com/example/iot_controlhost/ui/activity/SetActivity.java +++ b/app/src/main/java/com/example/iot_controlhost/ui/activity/SetActivity.java @@ -607,10 +607,6 @@ public class SetActivity extends BaseActivity 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); } diff --git a/app/src/main/java/com/example/iot_controlhost/ui/compose/PIDScreen.kt b/app/src/main/java/com/example/iot_controlhost/ui/compose/PIDScreen.kt deleted file mode 100644 index 26b19af..0000000 --- a/app/src/main/java/com/example/iot_controlhost/ui/compose/PIDScreen.kt +++ /dev/null @@ -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) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/iot_controlhost/ui/compose/PIDViewModel.kt b/app/src/main/java/com/example/iot_controlhost/ui/compose/PIDViewModel.kt deleted file mode 100644 index 6406cc2..0000000 --- a/app/src/main/java/com/example/iot_controlhost/ui/compose/PIDViewModel.kt +++ /dev/null @@ -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>(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 = ArrayDeque() - val tempList = mutableStateListOf() - - val kpList = mutableStateListOf() - val kiList = mutableStateListOf() - val kdList = mutableStateListOf() - - // 振荡周期记录 - val periods: MutableList = 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// 自整定完成状态 - } - -} - diff --git a/app/src/main/java/com/example/iot_controlhost/ui/dialog/SetAutoParamDialog.java b/app/src/main/java/com/example/iot_controlhost/ui/dialog/SetAutoParamDialog.java index 4e99abf..e02b814 100644 --- a/app/src/main/java/com/example/iot_controlhost/ui/dialog/SetAutoParamDialog.java +++ b/app/src/main/java/com/example/iot_controlhost/ui/dialog/SetAutoParamDialog.java @@ -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; @@ -223,6 +224,7 @@ public class SetAutoParamDialog extends BaseDialog { MyLog.app("自动换气参数修改:启用=" + ventilatorOldEnabled + "→" + ventilatorEnabled + ",开启=" + oldStart + "→" + start + "分钟,关闭=" + oldStop + "→" + stop + "分钟,CO₂上限=" + oldMaxCo2 + "→" + maxCo2 + "ppm"); + VentilatorController.getInstance().reloadSchedule("本机屏幕-自动换气参数"); dismiss(); needRefresh = true; } catch (NumberFormatException e) { @@ -288,6 +290,7 @@ public class SetAutoParamDialog extends BaseDialog { + start + "分钟,关闭=" + oldStop + " → " + stop + "分钟"); MyLog.app("智能模式当前龄期换气参数修改:开启=" + oldStart + "→" + start + "分钟,关闭=" + oldStop + "→" + stop + "分钟"); + VentilatorController.getInstance().reloadSchedule("本机屏幕-智能换气参数"); RxBusUtils.get().post(RxTag.AI_INFO, 0); dismiss(); needRefresh = true; diff --git a/app/src/main/java/com/example/iot_controlhost/ui/dialog/SetBaseDialog.java b/app/src/main/java/com/example/iot_controlhost/ui/dialog/SetBaseDialog.java index fa7d02e..b498d7f 100644 --- a/app/src/main/java/com/example/iot_controlhost/ui/dialog/SetBaseDialog.java +++ b/app/src/main/java/com/example/iot_controlhost/ui/dialog/SetBaseDialog.java @@ -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; @@ -228,6 +229,9 @@ public class SetBaseDialog extends BaseDialog { 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 -> diff --git a/app/src/main/java/com/example/iot_controlhost/utils/control/ventilator/VentilatorController.java b/app/src/main/java/com/example/iot_controlhost/utils/control/ventilator/VentilatorController.java index 2b9a20e..d681984 100644 --- a/app/src/main/java/com/example/iot_controlhost/utils/control/ventilator/VentilatorController.java +++ b/app/src/main/java/com/example/iot_controlhost/utils/control/ventilator/VentilatorController.java @@ -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; /** * 单例模式 @@ -53,7 +58,8 @@ public class VentilatorController extends Control { } MyLog.auto("换气自动控制初始化:先关闭换气扇,开启时长=" + targetOpen + "分钟,关闭时长=" + targetClose + "分钟"); - stopFan(0); + long generation = ++scheduleGeneration; + stopFan(0, generation); return true; } @@ -67,21 +73,46 @@ public class VentilatorController extends Control { } } - private void startFan(int minutes) { + private void startFan(int minutes, long generation) { + PollingTask activePollingTask = pollingTask; + if (generation != scheduleGeneration || activePollingTask == null) { + MyLog.auto("换气旧任务不再创建开启调度:任务代次=" + generation + + ",当前代次=" + scheduleGeneration); + return; + } MyLog.auto("换气调度:计划" + minutes + "分钟后开启换气扇"); - pollingTask.startDelayedTask(RxTag.VENTILATOR_AUTO_START_FUN, minutes * 60, () -> { + 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("换气控制暂停:开启和关闭时长均为0,3分钟后重新读取配置"); - startFan(3); + startFan(3, generation); return; } // 开启换气扇 MyQueue.getInstance(MyQueue.TYPE_CONTROLLER).addTask(new QueueIOTask(() -> { + if (generation != scheduleGeneration) { + MyLog.auto("换气旧任务已失效,取消已入队的开启操作:任务代次=" + generation); + return; + } String airExchangeMode = AutoModelSet.getAirExchangeMode(); - boolean fanResult = MyUtil.autoControlOperateThird(RoomController.fan, 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])) { @@ -91,11 +122,13 @@ public class VentilatorController extends Control { } })); MyLog.auto("换气调度:计划" + targetOpen + "分钟后关闭换气扇"); - stopFan(targetOpen); + stopFan(targetOpen, generation); } catch (Throwable throwable) { MyLog.autoError("换气扇开启阶段异常:异常=" + throwable.getClass().getSimpleName() + ",原因=" + throwable.getMessage() + ",1分钟后尝试恢复调度"); - startFan(1); + if (generation == scheduleGeneration) { + startFan(1, generation); + } } }); } @@ -105,13 +138,28 @@ 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 (generation != scheduleGeneration) { + MyLog.auto("换气旧任务已失效,取消已入队的关闭操作:任务代次=" + generation); + return; + } if (RoomController.airExchange.isAvailable() && AutoModelSet.getAirExchangeMode().equals(SpinnerList.AIR_EXCHANGE_MODES[1])) { boolean airExchangeResult = MyUtil.autoControlOperateThird(RoomController.airExchange, false); MyLog.auto("换气联动执行结果:设备=新风,目标=关闭,结果=" @@ -121,17 +169,58 @@ public class VentilatorController extends Control { MyLog.auto("换气控制执行结果:设备=换气扇,目标=关闭,结果=" + (fanResult ? "成功" : "失败") + ",计划停运=" + targetClose + "分钟"); })); - startFan(targetClose); + startFan(targetClose, generation); } catch (Throwable throwable) { MyLog.autoError("换气扇关闭阶段异常:异常=" + throwable.getClass().getSimpleName() + ",原因=" + throwable.getMessage() + ",1分钟后尝试恢复调度"); - startFan(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); diff --git a/app/src/main/java/com/example/iot_controlhost/utils/network/TopicClass.java b/app/src/main/java/com/example/iot_controlhost/utils/network/TopicClass.java index 0fc46a1..c5f708f 100644 --- a/app/src/main/java/com/example/iot_controlhost/utils/network/TopicClass.java +++ b/app/src/main/java/com/example/iot_controlhost/utils/network/TopicClass.java @@ -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; @@ -656,6 +657,9 @@ 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日:取消以下逻辑 // //切换为自动模式 @@ -675,6 +679,10 @@ public class TopicClass { UserLog.operate("远程控制", changeSummary); MyLog.remote(changeSummary + ",区域码=" + code); if (RoomSetting.getMode() == 1) { + if (ventilatorRuleChanged) { + // 控制逻辑直接响应远程参数,不依赖自动模式页面及其Presenter是否存活。 + VentilatorController.getInstance().reloadSchedule("远程控制"); + } // 如果是自动模式,则刷新自动模式参数 RxBusUtils.get().post(RxTag.UPDATE_AUTO, 3); } diff --git a/gradle.properties b/gradle.properties index bedde7b..cc18a0c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 android.useAndroidX=true android.enableJetifier=true -composeVersion=1.8.0