删除compose相关代码;优化换气扇控制逻辑

This commit is contained in:
BBIT-Kai
2026-08-10 11:33:38 +08:00
parent 090b120365
commit 381605cf70
16 changed files with 166 additions and 998 deletions
+3 -24
View File
@@ -1,7 +1,6 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0"
}
android {
@@ -10,8 +9,8 @@ android {
applicationId "com.example.iot_controlhost"
minSdkVersion 25
targetSdkVersion 30
versionCode 163
versionName "3.5.0.163"
versionCode 164
versionName "3.5.0.164"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
moduleName "mcu"
@@ -66,7 +65,6 @@ android {
buildFeatures {
viewBinding true
dataBinding true
compose true
}
kotlinOptions {
jvmTarget = '17'
@@ -76,30 +74,11 @@ android {
abortOnError false
checkReleaseBuilds false
}
composeOptions {
kotlinCompilerExtensionVersion rootProject.composeVersion
}
}
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao'
dependencies {
// Compose
implementation "androidx.activity:activity-compose:$rootProject.composeVersion"
implementation "androidx.compose.runtime:runtime:$rootProject.composeVersion"
implementation "androidx.compose.ui:ui:$rootProject.composeVersion"
implementation "androidx.compose.foundation:foundation:$rootProject.composeVersion"
implementation "androidx.compose.foundation:foundation-layout:$rootProject.composeVersion"
implementation "androidx.compose.material:material:$rootProject.composeVersion"
implementation "androidx.compose.runtime:runtime-livedata:$rootProject.composeVersion"
implementation "androidx.compose.ui:ui-tooling:$rootProject.composeVersion"
implementation("androidx.compose.material3:material3:1.4.0-alpha12")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
implementation("io.github.ltttttttttttt:ComposeViews:1.6.0.1")
implementation ("io.github.ehsannarmani:compose-charts:0.1.2")
// implementation "com.google.android.material:compose-theme-adapter:$rootProject.composeVersion"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
@@ -196,4 +175,4 @@ dependencies {
//
// // optional - Test helpers for Lifecycle runtime
// testImplementation "androidx.lifecycle:lifecycle-runtime-testing:$lifecycle_version"
}
}
+1 -5
View File
@@ -82,10 +82,6 @@
android:name=".ui.activity.DebugActivity"
android:exported="true" />
<activity
android:name=".ui.activity.ComposeActivity"
android:exported="true" />
<service
android:name=".service.MQTTService"
android:enabled="true"
@@ -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
/**
* @AuthorDuanKaiji
* @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();
@@ -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 ->
@@ -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);
}