fixed motor control unit

This commit is contained in:
00asdf 2025-04-27 01:55:10 +02:00
parent f0e5732cc7
commit eca3762cbb
4 changed files with 141 additions and 123 deletions

View File

@ -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"
}

View File

@ -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

View File

@ -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

View File

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