删除compose相关代码;优化换气扇控制逻辑
This commit is contained in:
+3
-24
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -160,4 +156,4 @@
|
||||
android:value="true" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -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,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();
|
||||
}
|
||||
|
||||
@@ -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,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,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();
|
||||
|
||||
+2
-1
@@ -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()));
|
||||
|
||||
@@ -607,10 +607,6 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -223,6 +224,7 @@ public class SetAutoParamDialog extends BaseDialog<DialogSetAutoBinding> {
|
||||
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<DialogSetAutoBinding> {
|
||||
+ 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;
|
||||
@@ -228,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 ->
|
||||
|
||||
+100
-11
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user