From 381605cf7091ff92cba3007bd5ef097498b56ad6 Mon Sep 17 00:00:00 2001
From: BBIT-Kai <2911862937@qq.com>
Date: Mon, 10 Aug 2026 11:33:38 +0800
Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4compose=E7=9B=B8=E5=85=B3?=
=?UTF-8?q?=E4=BB=A3=E7=A0=81=EF=BC=9B=E4=BC=98=E5=8C=96=E6=8D=A2=E6=B0=94?=
=?UTF-8?q?=E6=89=87=E6=8E=A7=E5=88=B6=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/build.gradle | 27 +-
app/src/main/AndroidManifest.xml | 6 +-
.../iot_controlhost/base/BaseViewModel.kt | 116 -----
.../model/controller/relay/Fan.java | 59 ++-
.../model/controller/relay/Relay.java | 3 +-
.../ui/activity/ComposeActivity.kt | 21 -
.../ui/activity/MainActivity.java | 3 -
.../ui/activity/MainActivityPresenter.java | 3 +-
.../ui/activity/SetActivity.java | 4 -
.../iot_controlhost/ui/compose/PIDScreen.kt | 463 ------------------
.../ui/compose/PIDViewModel.kt | 332 -------------
.../ui/dialog/SetAutoParamDialog.java | 3 +
.../ui/dialog/SetBaseDialog.java | 4 +
.../ventilator/VentilatorController.java | 111 ++++-
.../utils/network/TopicClass.java | 8 +
gradle.properties | 1 -
16 files changed, 166 insertions(+), 998 deletions(-)
delete mode 100644 app/src/main/java/com/example/iot_controlhost/base/BaseViewModel.kt
delete mode 100644 app/src/main/java/com/example/iot_controlhost/ui/activity/ComposeActivity.kt
delete mode 100644 app/src/main/java/com/example/iot_controlhost/ui/compose/PIDScreen.kt
delete mode 100644 app/src/main/java/com/example/iot_controlhost/ui/compose/PIDViewModel.kt
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