weather sensitive pid controller
This commit is contained in:
parent
8096c57952
commit
968c47b404
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -32,3 +32,4 @@ bin/
|
|||
.DS_Store
|
||||
/.idea/misc.xml
|
||||
/userlist.txt
|
||||
/VFHeatControl.jar
|
||||
|
|
|
|||
11
.idea/artifacts/VFHeatControl_jar.xml
Normal file
11
.idea/artifacts/VFHeatControl_jar.xml
Normal 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>
|
||||
|
|
@ -5,13 +5,16 @@ import java.util.*
|
|||
object GlobalDataStore {
|
||||
@Volatile var temperature = 0.0
|
||||
@Volatile var tempUpdated = Date()
|
||||
@Volatile var isSunny = true
|
||||
|
||||
@Volatile var isSunny = false
|
||||
@Volatile var lowerTime = Date()
|
||||
@Volatile var resetTime = Date()
|
||||
|
||||
@Volatile var targetTemp = 22.5
|
||||
@Volatile var reduction = 2.0
|
||||
@Volatile var pval = 0.0
|
||||
@Volatile var ival = 1.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"
|
||||
}
|
||||
|
|
@ -1,4 +1,39 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@ package dev.asdf00.visionfive.hc.server
|
|||
import com.sun.net.httpserver.HttpExchange
|
||||
|
||||
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.JSONException
|
||||
|
|
@ -17,6 +20,8 @@ import java.nio.charset.StandardCharsets
|
|||
import java.time.Duration
|
||||
import java.util.*
|
||||
|
||||
const val DEBUG = true
|
||||
|
||||
fun main() {
|
||||
// out-facing web page
|
||||
val server = buildWebServer(8030) {
|
||||
|
|
@ -167,7 +172,10 @@ fun main() {
|
|||
// new values
|
||||
try {
|
||||
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)
|
||||
} else {
|
||||
val temp = data["temp"].toString().toDouble()
|
||||
|
|
@ -239,29 +247,50 @@ fun main() {
|
|||
}
|
||||
}
|
||||
if (maxTempTime > 0) {
|
||||
val daylight = ((data["daily"] as? JSONObject)?.get("daylight_duration") as? JSONArray)?.let {
|
||||
if (it.length() == 1)
|
||||
(it[0] as? BigDecimal)?.let { it.toDouble() }
|
||||
else
|
||||
null
|
||||
} ?: 0.0
|
||||
val sun = ((data["daily"] as? JSONObject)?.get("sunshine_duration") as? JSONArray)?.let {
|
||||
val daylight =
|
||||
((data["daily"] as? JSONObject)?.get("daylight_duration") as? JSONArray)?.let {
|
||||
if (it.length() == 1)
|
||||
(it[0] as? BigDecimal)?.let { it.toDouble() }
|
||||
else
|
||||
null
|
||||
} ?: 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 {
|
||||
it.set(Calendar.HOUR_OF_DAY, maxTempTime)
|
||||
it.set(Calendar.MINUTE, 30)
|
||||
it.set(Calendar.MINUTE, 0)
|
||||
it.set(Calendar.SECOND, 0)
|
||||
it.add(Calendar.MINUTE, ((sunlight / 12) / 60).toInt())
|
||||
}.time
|
||||
GlobalDataStore.isSunny = sun > daylight / 2
|
||||
GlobalDataStore.isSunny = sunlight > daylight / 2
|
||||
success = true
|
||||
} else {
|
||||
println("error reading weather API response")
|
||||
}
|
||||
} else {
|
||||
println("error reading weather API response")
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
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()
|
||||
server.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()) {
|
||||
println()
|
||||
print("Username to add: ")
|
||||
val userName = readln()
|
||||
if (userName == "")
|
||||
break
|
||||
print("Password for '$userName': ")
|
||||
val pwd = readln()
|
||||
LoginBehavior.Users[userName] = pwd
|
||||
}
|
||||
println("shutdown requested ...")
|
||||
if (!DEBUG) {
|
||||
controller.interrupt()
|
||||
}
|
||||
forecastQuery.interrupt()
|
||||
server.stop(1)
|
||||
tempServer.stop(1)
|
||||
}
|
||||
|
||||
fun String.uft8() = toByteArray(StandardCharsets.UTF_8)
|
||||
|
|
|
|||
3
src/main/resources/META-INF/MANIFEST.MF
Normal file
3
src/main/resources/META-INF/MANIFEST.MF
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Manifest-Version: 1.0
|
||||
Main-Class: dev.asdf00.visionfive.hc.server.MainKt
|
||||
|
||||
Loading…
Reference in New Issue
Block a user