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.*
|
import java.util.*
|
||||||
|
|
||||||
object GlobalDataStore {
|
object GlobalDataStore {
|
||||||
@Volatile var temperature = 0.0
|
@Volatile var targetTemp: Double = 22.5
|
||||||
@Volatile var tempUpdated = Date()
|
@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 isSunny = false
|
||||||
@Volatile var lowerTime = Date()
|
@Volatile var lowerTime = Date()
|
||||||
@Volatile var resetTime = Date()
|
@Volatile var resetTime = Date()
|
||||||
|
|
||||||
@Volatile var targetTemp: Double = 22.5
|
@Volatile var temperature = targetTemp
|
||||||
@Volatile var reduction: Double = 2.0
|
@Volatile var tempUpdated = Date()
|
||||||
@Volatile var pval: Double = 0.1
|
|
||||||
@Volatile var ival: Double = 1.0 / 120
|
|
||||||
@Volatile var dval: Double = 0.0
|
|
||||||
|
|
||||||
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"
|
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.GlobalDataStore
|
||||||
import dev.asdf00.visionfive.hc.gpio.Controller
|
import dev.asdf00.visionfive.hc.gpio.Controller
|
||||||
|
import java.lang.Math.clamp
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class PidController(vararg val controlUnits: Controller) {
|
class PidController(vararg val controlUnits: Controller) {
|
||||||
|
|
@ -15,7 +16,7 @@ class PidController(vararg val controlUnits: Controller) {
|
||||||
|
|
||||||
private fun step(actual: Double, desired: Double): Double {
|
private fun step(actual: Double, desired: Double): Double {
|
||||||
val error = desired - actual
|
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 +
|
val rv = GlobalDataStore.pval * error +
|
||||||
GlobalDataStore.ival * errorInt +
|
GlobalDataStore.ival * errorInt +
|
||||||
GlobalDataStore.dval * (error - errorLast)
|
GlobalDataStore.dval * (error - errorLast)
|
||||||
|
|
@ -30,7 +31,7 @@ class PidController(vararg val controlUnits: Controller) {
|
||||||
GlobalDataStore.targetTemp - GlobalDataStore.reduction
|
GlobalDataStore.targetTemp - GlobalDataStore.reduction
|
||||||
else
|
else
|
||||||
GlobalDataStore.targetTemp
|
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 {
|
controlUnits.forEach {
|
||||||
it.state = newState
|
it.state = newState
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class Controller(private val senseBot: GpioPin, private val senseTop: GpioPin, p
|
||||||
internalState = maxStep
|
internalState = maxStep
|
||||||
} else {
|
} else {
|
||||||
// normal case
|
// normal case
|
||||||
val dif = (maxStep.toDouble() * target).toInt() - internalState
|
var dif = (maxStep.toDouble() * target).toInt() - internalState
|
||||||
if (dif > 0) {
|
if (dif > 0) {
|
||||||
val realDif = stepAnti(dif)
|
val realDif = stepAnti(dif)
|
||||||
if (realDif != dif) {
|
if (realDif != dif) {
|
||||||
|
|
@ -58,6 +58,7 @@ class Controller(private val senseBot: GpioPin, private val senseTop: GpioPin, p
|
||||||
internalState += realDif
|
internalState += realDif
|
||||||
}
|
}
|
||||||
} else if (dif < 0) {
|
} else if (dif < 0) {
|
||||||
|
dif = -dif
|
||||||
val realDif = stepClock(dif)
|
val realDif = stepClock(dif)
|
||||||
if (realDif != dif) {
|
if (realDif != dif) {
|
||||||
// maybe log correction
|
// maybe log correction
|
||||||
|
|
@ -148,7 +149,7 @@ class Controller(private val senseBot: GpioPin, private val senseTop: GpioPin, p
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val STEP_DELAY = 10L
|
private const val STEP_DELAY = 2L
|
||||||
private const val INITIAL_STEP = 0b1001L
|
private const val INITIAL_STEP = 0b1001L
|
||||||
private const val RECALIB_DATA = 0.01
|
private const val RECALIB_DATA = 0.01
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,135 @@ import java.nio.charset.StandardCharsets
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
const val DEBUG = true
|
const val DEBUG = true
|
||||||
|
|
||||||
fun main() {
|
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
|
// out-facing web page
|
||||||
val server = buildWebServer(8030) {
|
val server = buildWebServer(8030) {
|
||||||
// main landing page
|
// main landing page
|
||||||
|
|
@ -42,6 +168,8 @@ fun main() {
|
||||||
<h1>Welcome to HeatControl</h1>
|
<h1>Welcome to HeatControl</h1>
|
||||||
<p>You are logged in as ${uname}. To log out, click <a href="/login/out">here</a>.</p>
|
<p>You are logged in as ${uname}. To log out, click <a href="/login/out">here</a>.</p>
|
||||||
<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 />
|
The current recorded temperature is currently ${String.format("%.1f", GlobalDataStore.temperature)} C<br />
|
||||||
Last updated ${GlobalDataStore.tempUpdated}<br />
|
Last updated ${GlobalDataStore.tempUpdated}<br />
|
||||||
Current forecast for today: ${if (GlobalDataStore.isSunny) "sunny" else "cloudy"}
|
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()
|
forecastQuery.start()
|
||||||
server.start()
|
server.start()
|
||||||
tempServer.start()
|
tempServer.start()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user