fixed motor control unit
This commit is contained in:
parent
f0e5732cc7
commit
eca3762cbb
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<h1>Welcome to HeatControl</h1>
|
||||
<p>You are logged in as ${uname}. To log out, click <a href="/login/out">here</a>.</p>
|
||||
<p>
|
||||
${if (!DEBUG && !controller.isAlive) "CONTROLLER THREAD HAS DIED!!!<br />" else ""}
|
||||
${if (!forecastQuery.isAlive) "WEATHER QUERY THREAD HAS DIED!!!<br />" else ""}
|
||||
The current recorded temperature is currently ${String.format("%.1f", GlobalDataStore.temperature)} C<br />
|
||||
Last updated ${GlobalDataStore.tempUpdated}<br />
|
||||
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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user