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
|
.DS_Store
|
||||||
/.idea/misc.xml
|
/.idea/misc.xml
|
||||||
/userlist.txt
|
/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 {
|
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"
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
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