diff --git a/src/main/kotlin/dev/asdf00/visionfive/hc/GlobalDataStore.kt b/src/main/kotlin/dev/asdf00/visionfive/hc/GlobalDataStore.kt index 5a1bc02..8764513 100644 --- a/src/main/kotlin/dev/asdf00/visionfive/hc/GlobalDataStore.kt +++ b/src/main/kotlin/dev/asdf00/visionfive/hc/GlobalDataStore.kt @@ -3,18 +3,18 @@ package dev.asdf00.visionfive.hc import java.util.* object GlobalDataStore { - @Volatile var temperature = 0.0 - @Volatile var tempUpdated = Date() + @Volatile var targetTemp: Double = 22.5 + @Volatile var reduction: Double = 2.0 + @Volatile var pval: Double = 0.1 + @Volatile var ival: Double = 1.0 / 60 + @Volatile var dval: Double = 0.0 @Volatile var isSunny = false @Volatile var lowerTime = Date() @Volatile var resetTime = Date() - @Volatile var targetTemp: Double = 22.5 - @Volatile var reduction: Double = 2.0 - @Volatile var pval: Double = 0.1 - @Volatile var ival: Double = 1.0 / 120 - @Volatile var dval: Double = 0.0 + @Volatile var temperature = targetTemp + @Volatile var tempUpdated = Date() 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" } \ No newline at end of file diff --git a/src/main/kotlin/dev/asdf00/visionfive/hc/control/PidController.kt b/src/main/kotlin/dev/asdf00/visionfive/hc/control/PidController.kt index 15b9c1a..c506651 100644 --- a/src/main/kotlin/dev/asdf00/visionfive/hc/control/PidController.kt +++ b/src/main/kotlin/dev/asdf00/visionfive/hc/control/PidController.kt @@ -2,6 +2,7 @@ package dev.asdf00.visionfive.hc.control import dev.asdf00.visionfive.hc.GlobalDataStore import dev.asdf00.visionfive.hc.gpio.Controller +import java.lang.Math.clamp import java.util.* class PidController(vararg val controlUnits: Controller) { @@ -15,7 +16,7 @@ class PidController(vararg val controlUnits: Controller) { private fun step(actual: Double, desired: Double): Double { val error = desired - actual - errorInt = Math.clamp(errorInt + error, 0.0 / GlobalDataStore.ival, 1.0 / GlobalDataStore.ival) + errorInt = clamp(errorInt + error, 0.0 / GlobalDataStore.ival, 1.0 / GlobalDataStore.ival) val rv = GlobalDataStore.pval * error + GlobalDataStore.ival * errorInt + GlobalDataStore.dval * (error - errorLast) @@ -30,7 +31,7 @@ class PidController(vararg val controlUnits: Controller) { GlobalDataStore.targetTemp - GlobalDataStore.reduction else GlobalDataStore.targetTemp - val newState = Math.clamp(step(GlobalDataStore.temperature, myTarget), 0.0, 1.0) + val newState = clamp(step(GlobalDataStore.temperature, myTarget), 0.0, 1.0) controlUnits.forEach { it.state = newState diff --git a/src/main/kotlin/dev/asdf00/visionfive/hc/gpio/MotorControl.kt b/src/main/kotlin/dev/asdf00/visionfive/hc/gpio/MotorControl.kt index 727e2c9..7bf59cb 100644 --- a/src/main/kotlin/dev/asdf00/visionfive/hc/gpio/MotorControl.kt +++ b/src/main/kotlin/dev/asdf00/visionfive/hc/gpio/MotorControl.kt @@ -33,7 +33,7 @@ class Controller(private val senseBot: GpioPin, private val senseTop: GpioPin, p internalState = maxStep } else { // normal case - val dif = (maxStep.toDouble() * target).toInt() - internalState + var dif = (maxStep.toDouble() * target).toInt() - internalState if (dif > 0) { val realDif = stepAnti(dif) if (realDif != dif) { @@ -58,6 +58,7 @@ class Controller(private val senseBot: GpioPin, private val senseTop: GpioPin, p internalState += realDif } } else if (dif < 0) { + dif = -dif val realDif = stepClock(dif) if (realDif != dif) { // maybe log correction @@ -148,7 +149,7 @@ class Controller(private val senseBot: GpioPin, private val senseTop: GpioPin, p } companion object { - private const val STEP_DELAY = 10L + private const val STEP_DELAY = 2L private const val INITIAL_STEP = 0b1001L private const val RECALIB_DATA = 0.01 diff --git a/src/main/kotlin/dev/asdf00/visionfive/hc/server/Main.kt b/src/main/kotlin/dev/asdf00/visionfive/hc/server/Main.kt index aecd04d..4155ab8 100644 --- a/src/main/kotlin/dev/asdf00/visionfive/hc/server/Main.kt +++ b/src/main/kotlin/dev/asdf00/visionfive/hc/server/Main.kt @@ -20,9 +20,135 @@ import java.nio.charset.StandardCharsets import java.time.Duration import java.util.* +import kotlin.math.max + const val DEBUG = true fun main() { + val forecastQuery = Thread { + whileNotInterrupted { + var success = false + val request = HttpRequest.newBuilder(URI.create(GlobalDataStore.API)).GET().build() + HttpClient.newHttpClient() + .sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply { response -> + if (response.statusCode() == 200 && response.headers().firstValue("Content-Type") + .map { it.contains(ContentType.JSON.text) }.orElse(false) + ) { + try { + val data = JSONObject(response.body()) + var maxTemp = Double.NEGATIVE_INFINITY + var maxTempTime = -1 + ((data["hourly"] as? JSONObject)?.get("temperature_2m") as? JSONArray)?.forEachIndexed { h, t -> + (t as? BigDecimal)?.let { + val temp = it.toDouble() + if (maxTemp < temp) { + maxTemp = temp + maxTempTime = h + } + } + } + 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 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, 0) + it.set(Calendar.SECOND, 0) + it.add(Calendar.MINUTE, ((sunlight / 12) / 60).toInt()) + }.time + 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") + } + } else { + println("error querying weather API response") + } + } + .join() + + if (success) { + Thread.sleep(Duration.ofHours(12)) + } else { + Thread.sleep(Duration.ofMinutes(30)) + } + } + } + + 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) + + c1.runCalibrationSequence() + c2.runCalibrationSequence() + c3.runCalibrationSequence() + var cycles = 30 * 24 // recalibrate every day + whileNotInterrupted { + val timeBefore = System.currentTimeMillis() + pidController.tick() + if (cycles == 2) { + c1.runCalibrationSequence() + } else if (cycles == 1) { + c2.runCalibrationSequence() + } else if (cycles == 0) { + c3.runCalibrationSequence() + } else if (cycles < 0) { + cycles = 30 * 24 // recalibrate every day + } + cycles-- + val timeTaken = System.currentTimeMillis() - timeBefore + // tick every 2 minutes due to slow motor speeds + val twoMin = 1000L * 60 * 2 + if (timeTaken >= twoMin) { + println("WARNING: missed next step time by ${timeTaken - twoMin} millis") + } + Thread.sleep(max(1L, twoMin - timeTaken)) + } + } + } + // out-facing web page val server = buildWebServer(8030) { // main landing page @@ -42,6 +168,8 @@ fun main() {

Welcome to HeatControl

You are logged in as ${uname}. To log out, click here.

+ ${if (!DEBUG && !controller.isAlive) "CONTROLLER THREAD HAS DIED!!!
" else ""} + ${if (!forecastQuery.isAlive) "WEATHER QUERY THREAD HAS DIED!!!
" else ""} The current recorded temperature is currently ${String.format("%.1f", GlobalDataStore.temperature)} C
Last updated ${GlobalDataStore.tempUpdated}
Current forecast for today: ${if (GlobalDataStore.isSunny) "sunny" else "cloudy"} @@ -223,118 +351,6 @@ fun main() { } } - val forecastQuery = Thread { - whileNotInterrupted { - var success = false - val request = HttpRequest.newBuilder(URI.create(GlobalDataStore.API)).GET().build() - HttpClient.newHttpClient() - .sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .thenApply { response -> - if (response.statusCode() == 200 && response.headers().firstValue("Content-Type") - .map { it.contains(ContentType.JSON.text) }.orElse(false) - ) { - try { - val data = JSONObject(response.body()) - var maxTemp = Double.NEGATIVE_INFINITY - var maxTempTime = -1 - ((data["hourly"] as? JSONObject)?.get("temperature_2m") as? JSONArray)?.forEachIndexed { h, t -> - (t as? BigDecimal)?.let { - val temp = it.toDouble() - if (maxTemp < temp) { - maxTemp = temp - maxTempTime = h - } - } - } - 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 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, 0) - it.set(Calendar.SECOND, 0) - it.add(Calendar.MINUTE, ((sunlight / 12) / 60).toInt()) - }.time - 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") - } - } else { - println("error querying weather API response") - } - } - .join() - - if (success) { - Thread.sleep(Duration.ofHours(12)) - } else { - Thread.sleep(Duration.ofMinutes(30)) - } - } - } - - 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 - whileNotInterrupted { - 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()