weather sensitive pid controller

This commit is contained in:
00asdf 2025-04-26 01:42:59 +02:00
parent 8096c57952
commit 968c47b404
6 changed files with 147 additions and 24 deletions

1
.gitignore vendored
View File

@ -32,3 +32,4 @@ bin/
.DS_Store .DS_Store
/.idea/misc.xml /.idea/misc.xml
/userlist.txt /userlist.txt
/VFHeatControl.jar

View File

@ -0,0 +1,11 @@
<component name="ArtifactManager">
<artifact type="jar" name="VFHeatControl:jar">
<output-path>$PROJECT_DIR$</output-path>
<root id="archive" name="VFHeatControl.jar">
<element id="module-output" name="VFHeatControl" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/json/json/20250107/json-20250107.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.1.0/kotlin-stdlib-2.1.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" path-in-jar="/" />
</root>
</artifact>
</component>

View File

@ -5,13 +5,16 @@ import java.util.*
object GlobalDataStore { object GlobalDataStore {
@Volatile var temperature = 0.0 @Volatile var temperature = 0.0
@Volatile var tempUpdated = Date() @Volatile var tempUpdated = Date()
@Volatile var isSunny = true
@Volatile var isSunny = false
@Volatile var lowerTime = Date()
@Volatile var resetTime = Date() @Volatile var resetTime = Date()
@Volatile var targetTemp = 22.5 @Volatile var targetTemp = 22.5
@Volatile var reduction = 2.0
@Volatile var pval = 0.0 @Volatile var pval = 0.0
@Volatile var ival = 1.0 @Volatile var ival = 1.0
@Volatile var dval = 2.0 @Volatile var dval = 2.0
const val API = "https://api.open-meteo.com/v1/forecast?latitude=47.6649&longitude=14.3401&daily=sunshine_duration,daylight_duration&hourly=temperature_2m&timezone=Europe%2FBerlin&forecast_days=1&format=json&timeformat=unixtime" const val API = "https://api.open-meteo.com/v1/forecast?latitude=47.6649&longitude=14.3401&daily=sunshine_duration,daylight_duration,sunrise&hourly=temperature_2m&timezone=Europe/Berlin&forecast_days=1&format=json&timeformat=unixtime"
} }

View File

@ -1,4 +1,39 @@
package dev.asdf00.visionfive.hc.control package dev.asdf00.visionfive.hc.control
class PidController { import dev.asdf00.visionfive.hc.GlobalDataStore
import dev.asdf00.visionfive.hc.gpio.Controller
import java.util.*
class PidController(vararg val controlUnits: Controller) {
init {
if (controlUnits.size < 1)
throw IllegalArgumentException("PID-Controller needs motor control units to act")
}
private var errorInt = 0.0
private var errorLast = 0.0
private fun step(actual: Double, desired: Double): Double {
val error = desired - actual
errorInt = Math.clamp(errorInt + error, 0.0, 1.0)
val rv = GlobalDataStore.pval * error +
GlobalDataStore.ival * errorInt +
GlobalDataStore.dval * (error - errorLast)
errorLast = error
return rv
}
fun tick() {
val now = Date()
val myTarget =
if (GlobalDataStore.isSunny && now.after(GlobalDataStore.lowerTime) && now.before(GlobalDataStore.resetTime))
GlobalDataStore.targetTemp - GlobalDataStore.reduction
else
GlobalDataStore.targetTemp
val newState = Math.clamp(step(GlobalDataStore.temperature, myTarget), 0.0, 1.0)
controlUnits.forEach {
it.state = newState
}
}
} }

View File

@ -3,6 +3,9 @@ package dev.asdf00.visionfive.hc.server
import com.sun.net.httpserver.HttpExchange import com.sun.net.httpserver.HttpExchange
import dev.asdf00.visionfive.hc.GlobalDataStore import dev.asdf00.visionfive.hc.GlobalDataStore
import dev.asdf00.visionfive.hc.control.PidController
import dev.asdf00.visionfive.hc.gpio.Controller
import dev.asdf00.visionfive.hc.gpio.usingPins
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
@ -17,6 +20,8 @@ import java.nio.charset.StandardCharsets
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*
const val DEBUG = true
fun main() { fun main() {
// out-facing web page // out-facing web page
val server = buildWebServer(8030) { val server = buildWebServer(8030) {
@ -167,7 +172,10 @@ fun main() {
// new values // new values
try { try {
val data = JSONObject(exchange.requestBody.readAllBytes().utf8()).toMap() val data = JSONObject(exchange.requestBody.readAllBytes().utf8()).toMap()
if (!data.containsKey("temp") || !data.containsKey("p") || !data.containsKey("i") || !data.containsKey("d")) { if (!data.containsKey("temp") || !data.containsKey("p") || !data.containsKey("i") || !data.containsKey(
"d"
)
) {
exchange.replyCat(400) exchange.replyCat(400)
} else { } else {
val temp = data["temp"].toString().toDouble() val temp = data["temp"].toString().toDouble()
@ -239,29 +247,50 @@ fun main() {
} }
} }
if (maxTempTime > 0) { if (maxTempTime > 0) {
val daylight = ((data["daily"] as? JSONObject)?.get("daylight_duration") as? JSONArray)?.let { val daylight =
if (it.length() == 1) ((data["daily"] as? JSONObject)?.get("daylight_duration") as? JSONArray)?.let {
(it[0] as? BigDecimal)?.let { it.toDouble() }
else
null
} ?: 0.0
val sun = ((data["daily"] as? JSONObject)?.get("sunshine_duration") as? JSONArray)?.let {
if (it.length() == 1) if (it.length() == 1)
(it[0] as? BigDecimal)?.let { it.toDouble() } (it[0] as? BigDecimal)?.let { it.toDouble() }
else else
null null
} ?: 0.0 } ?: 0.0
val sunlight =
((data["daily"] as? JSONObject)?.get("sunshine_duration") as? JSONArray)?.let {
if (it.length() == 1)
(it[0] as? BigDecimal)?.let { it.toDouble() }
else
null
} ?: 0.0
val sunrise = ((data["daily"] as? JSONObject)?.get("sunrise") as? JSONArray)?.let {
if (it.length() == 1)
(it[0] as? Number)?.let { unixTime ->
Calendar.getInstance().also {
it.time = Date(unixTime.toLong() * 1000)
it.add(Calendar.MINUTE, ((daylight / 12) / 60).toInt())
}.time
}
else
null
}
if (sunrise != null) {
GlobalDataStore.lowerTime = sunrise
GlobalDataStore.resetTime = Calendar.getInstance().also { GlobalDataStore.resetTime = Calendar.getInstance().also {
it.set(Calendar.HOUR_OF_DAY, maxTempTime) it.set(Calendar.HOUR_OF_DAY, maxTempTime)
it.set(Calendar.MINUTE, 30) it.set(Calendar.MINUTE, 0)
it.set(Calendar.SECOND, 0) it.set(Calendar.SECOND, 0)
it.add(Calendar.MINUTE, ((sunlight / 12) / 60).toInt())
}.time }.time
GlobalDataStore.isSunny = sun > daylight / 2 GlobalDataStore.isSunny = sunlight > daylight / 2
success = true success = true
} else { } else {
println("error reading weather API response") println("error reading weather API response")
} }
} else {
println("error reading weather API response")
}
} catch (e: JSONException) { } catch (e: JSONException) {
println("error parsing weather API response") println("error parsing weather API response")
} }
@ -279,19 +308,60 @@ fun main() {
} }
} }
val controller = Thread {
usingPins(
55 to false, 42 to false, 43 to false, 47 to false, 38 to true, 54 to true, // motor 1
51 to false, 50 to false, 56 to false, 40 to false, 36 to true, 61 to true, // motor 2
45 to false, 37 to false, 39 to false, 63 to false, 60 to true, 44 to true, // motor 3
) { pins ->
val c1 = Controller(pins[4], pins[5], pins[0], pins[1], pins[2], pins[3])
val c2 = Controller(pins[10], pins[11], pins[6], pins[7], pins[8], pins[9])
val c3 = Controller(pins[16], pins[17], pins[12], pins[13], pins[14], pins[15])
val pidController = PidController(c1, c2, c3)
var cycles = -1
while (!Thread.interrupted()) {
if (cycles < 0) {
c1.runCalibrationSequence()
c2.runCalibrationSequence()
c3.runCalibrationSequence()
cycles = 24 * 60 // run once per day
}
cycles--
pidController.tick()
// tick every minute
Thread.sleep(Duration.ofMinutes(1))
}
}
}
forecastQuery.start() forecastQuery.start()
server.start() server.start()
tempServer.start() tempServer.start()
println("HttpServer is running, press CTRL+C to exit") if (!DEBUG) {
// only start the controller on the real thing since GPIO pins are only available there
controller.start()
} else {
println("running in DEBUG MODE!")
}
println("HttpServer is running, press ENTER to exit")
while (!Thread.interrupted()) { while (!Thread.interrupted()) {
println() println()
print("Username to add: ") print("Username to add: ")
val userName = readln() val userName = readln()
if (userName == "")
break
print("Password for '$userName': ") print("Password for '$userName': ")
val pwd = readln() val pwd = readln()
LoginBehavior.Users[userName] = pwd LoginBehavior.Users[userName] = pwd
} }
println("shutdown requested ...")
if (!DEBUG) {
controller.interrupt()
}
forecastQuery.interrupt() forecastQuery.interrupt()
server.stop(1)
tempServer.stop(1)
} }
fun String.uft8() = toByteArray(StandardCharsets.UTF_8) fun String.uft8() = toByteArray(StandardCharsets.UTF_8)

View File

@ -0,0 +1,3 @@
Manifest-Version: 1.0
Main-Class: dev.asdf00.visionfive.hc.server.MainKt