Skip to content

Commit 0962124

Browse files
committed
Thermo sensor with group alarm.
1 parent 7e03ebe commit 0962124

File tree

15 files changed

+303
-70
lines changed

15 files changed

+303
-70
lines changed

controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,20 @@ public fun <T1, T2, R> StateContainer.combineState(
157157
transformation: (T1, T2) -> R,
158158
): DeviceState<R> = registerState(DeviceState.combine(first, second, transformation))
159159

160+
/**
161+
* Combines multiple device states into a single state by applying a transformation function.
162+
*
163+
* @param T the type of the individual state values.
164+
* @param R the type of the combined state value.
165+
* @param states a collection of [DeviceState] instances to be combined.
166+
* @param transformation a function that takes an array of individual state values and maps it to a combined value.
167+
* @return a new [DeviceState] representing the combined state, with the value computed by the transformation function.
168+
*/
169+
public inline fun <reified T, R> StateContainer.combineState(
170+
states: Collection<DeviceState<T>>,
171+
crossinline transformation: (Array<T>) -> R,
172+
): DeviceState<R> = registerState(DeviceState.combine(states, transformation))
173+
160174
/**
161175
* Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically
162176
* transferred onto [targetState], but not vise versa.
@@ -210,7 +224,7 @@ public fun <T1, T2, R> StateContainer.combineTo(
210224
): Job {
211225
val descriptor = ConnectionConstructorElement(setOf(sourceState1, sourceState2), setOf(targetState))
212226
registerElement(descriptor)
213-
return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
227+
return combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
214228
targetState.value = it
215229
}.launchIn(this).apply {
216230
invokeOnCompletion {
@@ -231,7 +245,7 @@ public inline fun <reified T, R> StateContainer.combineTo(
231245
): Job {
232246
val descriptor = ConnectionConstructorElement(sourceStates, setOf(targetState))
233247
registerElement(descriptor)
234-
return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach {
248+
return combine(sourceStates.map { it.valueFlow }, transformation).onEach {
235249
targetState.value = it
236250
}.launchIn(this).apply {
237251
invokeOnCompletion {

controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ package space.kscience.controls.constructor
22

33
import kotlinx.coroutines.CoroutineScope
44
import kotlinx.coroutines.Job
5-
import kotlinx.coroutines.flow.Flow
6-
import kotlinx.coroutines.flow.launchIn
7-
import kotlinx.coroutines.flow.map
8-
import kotlinx.coroutines.flow.onEach
5+
import kotlinx.coroutines.flow.*
96
import space.kscience.controls.constructor.units.NumericalValue
107
import space.kscience.controls.constructor.units.UnitsOfMeasurement
118
import kotlin.reflect.KProperty
@@ -97,7 +94,85 @@ public fun <T1, T2, R> DeviceState.Companion.combine(
9794

9895
override val value: R get() = mapper(state1.value, state2.value)
9996

100-
override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)
97+
override val valueFlow: Flow<R> = combine(state1.valueFlow, state2.valueFlow, mapper)
10198

10299
override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2)"
100+
}
101+
102+
/**
103+
* Combines three device states into a single device state by applying a mapping function.
104+
*
105+
* @param state1 The first device state to combine.
106+
* @param state2 The second device state to combine.
107+
* @param state3 The third device state to combine.
108+
* @param mapper The mapping function that combines the values of the three states into a resulting value.
109+
* @return A new device state whose value depends on the combined values of the provided states.
110+
*/
111+
public fun <T1, T2, T3, R> DeviceState.Companion.combine(
112+
state1: DeviceState<T1>,
113+
state2: DeviceState<T2>,
114+
state3: DeviceState<T3>,
115+
mapper: (T1, T2, T3) -> R,
116+
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
117+
override val dependencies = listOf(state1, state2, state3)
118+
119+
override val value: R get() = mapper(state1.value, state2.value, state3.value)
120+
121+
override val valueFlow: Flow<R> = combine(state1.valueFlow, state2.valueFlow, state3.valueFlow, mapper)
122+
123+
override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2, state3=$state3)"
124+
}
125+
126+
/**
127+
* Combines multiple [DeviceState] instances into a single [DeviceStateWithDependencies].
128+
* The combined state value is derived by applying the provided [mapper] function to the collection of individual state values.
129+
*
130+
* @param T the type of the individual state values.
131+
* @param R the type of the combined state value.
132+
* @param states a collection of [DeviceState] instances whose values are to be combined.
133+
* @param mapper a function that takes a collection of state values and maps it to a combined value.
134+
* @return a [DeviceStateWithDependencies] representing the combined state, which has dependencies on the input [states].
135+
*/
136+
public inline fun <reified T, R> DeviceState.Companion.combine(
137+
states: Collection<DeviceState<T>>,
138+
crossinline mapper: (Array<T>) -> R,
139+
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
140+
override val dependencies = states
141+
142+
override val value: R get() = mapper(states.map { it.value }.toTypedArray())
143+
144+
override val valueFlow: Flow<R> = combine<T, R>(states.map { it.valueFlow }, mapper)
145+
146+
override fun toString(): String = "DeviceState.combine(states=${states.joinToString()})"
147+
}
148+
149+
/**
150+
* Combines multiple `DeviceState` instances into a single `DeviceStateWithDependencies`.
151+
* The combined state is derived by applying the provided `mapper` function to the values of the input states.
152+
*
153+
* @param T the type of individual states' values.
154+
* @param K the type of the keys in the input state map.
155+
* @param R the type of the resulting combined state value.
156+
* @param states a map of keys to `DeviceState` instances representing the individual states to be combined.
157+
* @param mapper a function that takes a map of key-value pairs (where keys are from `states` and values are the current
158+
* values of the corresponding `DeviceState` instances) and produces the value for the combined state.
159+
* @return a `DeviceStateWithDependencies` instance representing the combined state, with its value computed
160+
* dynamically based on the input states and the `mapper` function.
161+
*/
162+
public inline fun <reified T, K, R> DeviceState.Companion.combine(
163+
states: Map<K, DeviceState<T>>,
164+
crossinline mapper: (Map<K, T>) -> R,
165+
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
166+
override val dependencies = states.values
167+
168+
override val value: R get() = mapper(states.mapValues { it.value.value })
169+
170+
private val entries = states.entries.toList()
171+
172+
override val valueFlow: Flow<R> = combine<T, R>(entries.map { it.value.valueFlow }) { array: Array<T> ->
173+
// restore mapping
174+
mapper(entries.indices.associate { entries[it].key to array[it] })
175+
}
176+
177+
override fun toString(): String = "DeviceState.associate(states=${states})"
103178
}

controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package space.kscience.controls.api
22

33
import kotlinx.serialization.Serializable
4+
import space.kscience.dataforge.meta.ValueType
45
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
56
import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder
67

@@ -25,6 +26,18 @@ public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() ->
2526
}
2627
}
2728

29+
/**
30+
* Sets the value type and additional types for a property descriptor.
31+
*
32+
* @param valueType The main value type to be assigned to the property descriptor.
33+
* @param otherTypes Additional value types to be assigned to the property descriptor.
34+
*/
35+
public fun PropertyDescriptor.valueType(valueType: ValueType, vararg otherTypes: ValueType) {
36+
metaDescriptor {
37+
valueType(valueType, *otherTypes)
38+
}
39+
}
40+
2841
/**
2942
* A descriptor for property
3043
*/

controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import space.kscience.dataforge.meta.MutableMeta
1010
import space.kscience.dataforge.names.Name
1111
import space.kscience.dataforge.names.get
1212
import space.kscience.dataforge.names.parseAsName
13-
import kotlin.collections.set
1413
import kotlin.properties.ReadOnlyProperty
1514

1615
/**
@@ -38,14 +37,16 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
3837
}
3938
}
4039

41-
public fun <D : Device> DeviceManager.install(name: String, device: D): D {
42-
registerDevice(name.parseAsName(), device)
40+
public fun <D : Device> DeviceManager.install(name: Name, device: D): D {
41+
registerDevice(name, device)
4342
device.launch {
4443
device.start()
4544
}
4645
return device
4746
}
4847

48+
public fun <D : Device> DeviceManager.install(name: String, device: D): D = install(name.parseAsName(), device)
49+
4950
public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device)
5051

5152

demo/thermo/build.gradle.kts

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ kscience {
2525
}
2626
},
2727
jsConfig = {
28+
//otherwise compose-bootstrap does not work
2829
useCommonJs()
2930
}
3031
// development = true
@@ -38,8 +39,8 @@ kscience {
3839
implementation(projects.controlsCore)
3940
implementation(projects.controlsConstructor)
4041
implementation(projects.controlsVision)
42+
implementation(compose.runtime)
4143

42-
//web UI dependencies
4344
implementation(libs.plotlykt.core)
4445
}
4546

@@ -49,12 +50,6 @@ kscience {
4950
implementation(projects.controlsModbus)
5051
implementation(projects.controlsOpcua)
5152

52-
//compose desktop dependencies
53-
implementation(projects.controlsVisualisationCompose)
54-
implementation(compose.runtime)
55-
implementation(compose.desktop.currentOs)
56-
implementation(compose.material3)
57-
5853
implementation(libs.visionforge.server)
5954
implementation("org.jetbrains.kotlin-wrappers:kotlin-css")
6055
implementation(spclibs.ktor.server.cio)
@@ -65,20 +60,4 @@ kscience {
6560
jsMain {
6661
implementation(libs.visionforge.compose.html)
6762
}
68-
}
69-
70-
compose {
71-
desktop {
72-
application {
73-
mainClass = "center.sciprog.controls.demo.thermo.ComposePanelKt"
74-
75-
nativeDistributions {
76-
packageName = "ControlsThermoSensor"
77-
packageVersion = "1.0.0"
78-
windows {
79-
includeAllModules = true
80-
}
81-
}
82-
}
83-
}
84-
}
63+
}

demo/thermo/src/commonMain/kotlin/ThermoSensorAnalyzer.kt

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ package center.sciprog.controls.demo.thermo
33
import kotlinx.coroutines.sync.Mutex
44
import kotlinx.coroutines.sync.withLock
55
import kotlinx.serialization.Serializable
6+
import space.kscience.controls.api.valueType
67
import space.kscience.controls.constructor.*
78
import space.kscience.controls.time.ValueWithTime
9+
import space.kscience.dataforge.context.Context
810
import space.kscience.dataforge.meta.MetaConverter
11+
import space.kscience.dataforge.meta.ValueType
912
import space.kscience.dataforge.names.asName
13+
import kotlin.math.abs
1014
import kotlin.time.Duration.Companion.milliseconds
1115

1216
@Serializable
@@ -21,14 +25,17 @@ enum class ThermoSensorStatus {
2125
class ThermoSensorAnalyzer(
2226
val sensor: ThermoSensor,
2327
val analyzerConfig: ThermoSensorAnalyzerConfig
24-
) : DeviceConstructor(sensor.context) {
28+
) : DeviceConstructor(sensor.context, analyzerConfig.meta) {
2529
init {
2630
install("sensor".asName(), sensor)
2731
}
2832

2933
val temperature by property(
30-
MetaConverter.Companion.double,
31-
sensor.propertyAsState(ThermoSensor.temperature, Double.NaN)
34+
converter = MetaConverter.Companion.double,
35+
state = sensor.propertyAsState(ThermoSensor.temperature, Double.NaN),
36+
descriptorBuilder = {
37+
valueType(ValueType.NUMBER)
38+
}
3239
)
3340

3441
private val statusState = MutableDeviceState(ThermoSensorStatus.NotConnected)
@@ -37,13 +44,19 @@ class ThermoSensorAnalyzer(
3744

3845
private val averagedTemperatureState = MutableDeviceState(Double.NaN)
3946

40-
val averageTemperature: DeviceState<Double> by property(MetaConverter.double, averagedTemperatureState)
47+
val averageTemperature: DeviceState<Double> by property(
48+
converter = MetaConverter.double,
49+
state = averagedTemperatureState,
50+
descriptorBuilder = {
51+
valueType(ValueType.NUMBER)
52+
}
53+
)
4154

4255
private val history = ArrayList<ValueWithTime<Double>>()
4356

4457
private val mutex = Mutex()
4558

46-
val statusUpdateJob = temperature.onNext(
59+
private val statusUpdateJob = temperature.onNext(
4760
writes = listOf(status)
4861
) { next ->
4962
if (next.isNaN()) {
@@ -71,17 +84,54 @@ class ThermoSensorAnalyzer(
7184
statusState.value = newStatus
7285
}
7386
}
87+
}
7488

75-
// val status by property(
76-
// converter = MetaConverter.Companion.enum<ThermoSensorStatus>(),
77-
// state = temperature.map {
78-
// //TODO add analysis for history data
79-
// when {
80-
// it < -100.0 || it == Double.NaN -> ThermoSensorStatus.NotConnected
81-
// it > analyzerConfig.alarmThreshold -> ThermoSensorStatus.Alarm
82-
// it > analyzerConfig.warningThreshold -> ThermoSensorStatus.Warning
83-
// else -> ThermoSensorStatus.Normal
84-
// }
85-
// }
86-
// )
89+
/**
90+
* A group-level analyzer for multiple thermal sensors. This class aggregates and monitors
91+
* the states of a collection of `ThermoSensorAnalyzer` instances, providing insights and
92+
* meta-state calculations based on the group-wide behavior.
93+
*
94+
* @constructor Creates a new instance with the provided context, list of analyzers, and
95+
* configuration settings.
96+
* @param context The system context under which the analyzer operates.
97+
* @param sensors A collection of `ThermoSensorAnalyzer` instances, each representing a
98+
* sensor to be monitored as part of the group.
99+
* @param config A configuration object defining the parameters for the group-level
100+
* analysis, including thresholds and metadata.
101+
*
102+
* Properties:
103+
* - `discrepancy`: A computed property representing the absolute maximum deviation of any
104+
* sensor's temperature from the group's average temperature. Helps in identifying significant
105+
* sensor outliers in the group.
106+
* - `status`: A computed property reflecting the overall status of the group, based on the
107+
* discrepancy between individual sensor readings. The status is determined using the
108+
* discrepancy threshold defined in the `ThermoSensorGroupConfig`. Possible statuses include
109+
* `Normal` and `Alarm`.
110+
*/
111+
class ThermoSensorGroupAnalyzer(
112+
context: Context,
113+
val sensors: List<ThermoSensorAnalyzer>,
114+
val config: ThermoSensorGroupConfig
115+
) : DeviceConstructor(context, config.meta) {
116+
117+
val discrepancy by property(
118+
converter = MetaConverter.double,
119+
state = combineState(sensors.map { it.temperature }) { values ->
120+
val average = values.average()
121+
values.maxOf { abs(average - it) }
122+
},
123+
descriptorBuilder = {
124+
valueType(ValueType.NUMBER)
125+
}
126+
)
127+
128+
val status by property(
129+
converter = MetaConverter.enum<ThermoSensorStatus>(),
130+
state = discrepancy.map { discrepancy ->
131+
when {
132+
discrepancy >= config.discrepancyThreshold -> ThermoSensorStatus.Alarm
133+
else -> ThermoSensorStatus.Normal
134+
}
135+
}
136+
)
87137
}

demo/thermo/src/commonMain/kotlin/ThermoSensorHub.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import space.kscience.controls.api.Device
44
import space.kscience.controls.api.DeviceHub
55
import space.kscience.dataforge.context.ContextAware
66
import space.kscience.dataforge.names.Name
7+
import space.kscience.dataforge.names.NameToken
8+
import space.kscience.dataforge.names.asName
79
import space.kscience.dataforge.names.parseAsName
810

911

1012
interface ThermoSensorHub : DeviceHub, ContextAware {
1113
val sensors: Map<String, ThermoSensorAnalyzer>
14+
val groups: Map<String, ThermoSensorGroupAnalyzer>
1215

13-
override val devices: Map<Name, Device> get() = sensors.mapKeys { it.key.parseAsName() }
16+
override val devices: Map<Name, Device>
17+
get() = sensors.mapKeys { it.key.parseAsName() } + groups.mapKeys { NameToken("group",it.key).asName() }
1418
}

0 commit comments

Comments
 (0)