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() {
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()