2 Commits
12 changed files with 297 additions and 50 deletions

No files matched your search

+5 -2
View File
@@ -13,8 +13,11 @@ android {
applicationId = "com.racingapp.pitlog"
minSdk = 26
targetSdk = 36
versionCode = 3
versionName = "0.003"
// Three digits. versionCode is that number with the dot removed.
// A fix adds 0.001. One feature adds 0.010. A batch of features adds 0.100.
// The release tag is v plus versionName, such as v0.013.
versionCode = 23
versionName = "0.023"
}
buildTypes {
@@ -1,6 +1,8 @@
package com.racingapp.pitlog.model
import java.util.Locale
import kotlin.math.abs
import kotlin.math.roundToInt
fun sanitizeSignedDecimal(raw: String, wholeDigits: Int, fractionDigits: Int): String {
val negative = raw.trimStart().startsWith('-')
@@ -31,6 +33,72 @@ fun parseDecimal(raw: String): Double? {
return trimmed.toDoubleOrNull()
}
fun sanitizeInches(raw: String, signed: Boolean = false): String {
val negative = signed && raw.trimStart().startsWith('-')
val source = raw.trim().removePrefix("-").removePrefix("+").replace(',', '.')
val out = StringBuilder()
var slash = false
var dot = false
var space = false
var digits = 0
for (ch in source) {
when {
ch.isDigit() -> {
val limit = when {
slash || dot -> if (dot) 4 else 2
else -> 2
}
if (digits < limit) {
out.append(ch)
digits++
}
}
ch == '/' && !slash && !dot && out.isNotEmpty() && !out.endsWith(' ') -> {
slash = true
digits = 0
out.append('/')
}
ch == '.' && !dot && !slash && !space -> {
dot = true
digits = 0
out.append('.')
}
ch == ' ' && !space && !slash && !dot && digits > 0 -> {
space = true
digits = 0
out.append(' ')
}
}
}
val text = out.toString()
if (text.isEmpty()) return if (negative) "-" else ""
return if (negative) "-$text" else text
}
fun parseInches(raw: String): Double? {
val trimmed = raw.trim().removeSuffix(" ")
if (trimmed.isEmpty() || trimmed == "-" || trimmed.endsWith("/") || trimmed.endsWith(".")) return null
val negative = trimmed.startsWith("-")
val body = trimmed.removePrefix("-").trim()
val value = if (body.contains("/")) parseFraction(body) else parseDecimal(body)
value ?: return null
return if (negative) -value else value
}
fun formatInches(value: Double?): String {
if (value == null) return ""
val negative = value < 0
val magnitude = abs(value)
val scaled = magnitude * 64.0
val nearest = scaled.roundToInt()
val body = if (abs(scaled - nearest) <= 0.04) {
fractionInches(nearest)
} else {
String.format(Locale.US, "%.3f", magnitude).trimEnd('0').trimEnd('.')
}
return if (negative && body != "0") "-$body" else body
}
fun parseIntField(raw: String): Int? = raw.trim().toIntOrNull()
fun formatPsi(value: Double?): String =
@@ -52,15 +120,48 @@ fun formatDegrees(value: Double?): String =
fun formatStagger(value: Double?): String {
if (value == null) return "—"
return String.format(Locale.US, "%.2f", value).trimEnd('0').trimEnd('.')
return formatInches(value)
}
fun psiField(value: Double?): String =
if (value == null) "" else String.format(Locale.US, "%.1f", value)
fun staggerField(value: Double?): String {
if (value == null) return ""
return String.format(Locale.US, "%.2f", value).trimEnd('0').trimEnd('.')
fun staggerField(value: Double?): String = formatInches(value)
private fun parseFraction(body: String): Double? {
val parts = body.split(' ').filter { it.isNotEmpty() }
val whole: Double
val token: String
when (parts.size) {
1 -> {
whole = 0.0
token = parts[0]
}
2 -> {
whole = parts[0].toDoubleOrNull() ?: return null
token = parts[1]
}
else -> return null
}
val bits = token.split('/')
if (bits.size != 2 || bits[1].isEmpty()) return null
val numerator = bits[0].toDoubleOrNull() ?: return null
val denominator = bits[1].toDoubleOrNull() ?: return null
if (denominator == 0.0) return null
return whole + numerator / denominator
}
private fun fractionInches(sixtyFourths: Int): String {
val whole = sixtyFourths / 64
var numerator = sixtyFourths % 64
var denominator = 64
if (numerator == 0) return whole.toString()
while (numerator % 2 == 0) {
numerator /= 2
denominator /= 2
}
val fraction = "$numerator/$denominator"
return if (whole == 0) fraction else "$whole $fraction"
}
fun intField(value: Int?): String = value?.toString().orEmpty()
@@ -123,7 +123,7 @@ data class RunForm(
if (sheet.cold.any() || sheet.hot.any()) return true
if (sheet.heat.values.any { it.any() }) return true
if (parseIntField(laps) != null || parseIntField(finish) != null) return true
if (parseDecimal(stagger) != null) return true
if (parseInches(stagger) != null) return true
if (tireSet.isNotBlank() || notes.isNotBlank()) return true
return handling != null || handlingWhere != null
}
@@ -137,7 +137,7 @@ data class RunForm(
tireSet = tireSet.trim(),
laps = parseIntField(laps),
finish = parseIntField(finish),
staggerInches = parseDecimal(stagger),
staggerInches = parseInches(stagger),
coldLf = sheet.cold.lf,
coldRf = sheet.cold.rf,
coldLr = sheet.cold.lr,
@@ -28,22 +28,61 @@ fun setupLines(sheet: SetupSheet): List<String> = buildList {
"RR" to sheet.weightRr,
).mapNotNull { (corner, pounds) -> pounds?.let { "$corner $it" } }
if (weights.isNotEmpty()) add("Weights ${weights.joinToString(" ")} lb")
weightSplit(sheet.weightLf, sheet.weightRf, sheet.weightLr, sheet.weightRr)?.let { add("From the weights $it") }
addAll(scaleLines(sheet.weightLf, sheet.weightRf, sheet.weightLr, sheet.weightRr))
sheet.rideHeight?.let { add("Ride height ${inches(it)} in") }
sheet.pinion?.let { add("Pinion ${formatDegrees(it)}°") }
sheet.brakeFront?.let { add("Front brake ${onePlace(it)}%") }
}
fun weightSplit(lf: Int?, rf: Int?, lr: Int?, rr: Int?): String? {
fun scaleLines(lf: Int?, rf: Int?, lr: Int?, rr: Int?): List<String> {
val reading = scaleReading(lf, rf, lr, rr) ?: return emptyList()
return listOf(
"Total ${reading.total} lb",
"Left ${pair(reading.left, reading.leftPercent)}",
"Front ${pair(reading.front, reading.frontPercent)}",
"Rear ${pair(reading.rear, reading.rearPercent)}",
"Cross ${pair(reading.cross, reading.crossPercent)}",
"Rear bite ${reading.rearBite}",
)
}
private data class ScaleReading(
val total: Int,
val left: Int,
val leftPercent: Double,
val front: Int,
val frontPercent: Double,
val rear: Int,
val rearPercent: Double,
val cross: Int,
val crossPercent: Double,
val rearBite: Int,
)
private fun scaleReading(lf: Int?, rf: Int?, lr: Int?, rr: Int?): ScaleReading? {
if (lf == null || rf == null || lr == null || rr == null) return null
val total = lf + rf + lr + rr
if (total <= 0) return null
val left = (lf + lr) * 100.0 / total
val rear = (lr + rr) * 100.0 / total
val cross = (rf + lr) * 100.0 / total
return "Left ${onePlace(left)}% Rear ${onePlace(rear)}% Cross ${onePlace(cross)}%"
val left = lf + lr
val front = lf + rf
val rear = lr + rr
val cross = rf + lr
return ScaleReading(
total = total,
left = left,
leftPercent = left * 100.0 / total,
front = front,
frontPercent = front * 100.0 / total,
rear = rear,
rearPercent = rear * 100.0 / total,
cross = cross,
crossPercent = cross * 100.0 / total,
rearBite = lr - rr,
)
}
private fun pair(pounds: Int, percent: Double) = "$pounds lb, ${onePlace(percent)}%"
fun setupDiffs(before: SetupSheet, after: SetupSheet): List<String> = buildList {
changed(before.crossPercent, after.crossPercent, ::percent)?.let { add("Cross $it") }
changedInt(before.springLf, after.springLf, " lb")?.let { add("LF spring $it") }
@@ -63,18 +102,40 @@ fun setupDiffs(before: SetupSheet, after: SetupSheet): List<String> = buildList
changedInt(before.weightRf, after.weightRf, " lb")?.let { add("RF weight $it") }
changedInt(before.weightLr, after.weightLr, " lb")?.let { add("LR weight $it") }
changedInt(before.weightRr, after.weightRr, " lb")?.let { add("RR weight $it") }
addAll(scaleChanges(before, after))
changed(before.rideHeight, after.rideHeight) { "${inches(it)} in" }?.let { add("Ride height $it") }
changed(before.pinion, after.pinion) { "${formatDegrees(it)}°" }?.let { add("Pinion $it") }
changed(before.brakeFront, after.brakeFront) { "${onePlace(it)}%" }?.let { add("Front brake $it") }
}
private fun scaleChanges(before: SetupSheet, after: SetupSheet): List<String> {
val old = scaleReading(before.weightLf, before.weightRf, before.weightLr, before.weightRr)
val next = scaleReading(after.weightLf, after.weightRf, after.weightLr, after.weightRr)
if (old == null || next == null || old == next) return emptyList()
return buildList {
if (old.total != next.total) add("Total ${old.total} lb → ${next.total} lb")
if (old.left != next.left || onePlace(old.leftPercent) != onePlace(next.leftPercent)) {
add("Left ${pair(old.left, old.leftPercent)} → ${pair(next.left, next.leftPercent)}")
}
if (old.front != next.front || onePlace(old.frontPercent) != onePlace(next.frontPercent)) {
add("Front ${pair(old.front, old.frontPercent)} → ${pair(next.front, next.frontPercent)}")
}
if (old.rear != next.rear || onePlace(old.rearPercent) != onePlace(next.rearPercent)) {
add("Rear ${pair(old.rear, old.rearPercent)} → ${pair(next.rear, next.rearPercent)}")
}
if (old.cross != next.cross || onePlace(old.crossPercent) != onePlace(next.crossPercent)) {
add("Cross ${pair(old.cross, old.crossPercent)} → ${pair(next.cross, next.crossPercent)}")
}
if (old.rearBite != next.rearBite) add("Rear bite ${old.rearBite} → ${next.rearBite}")
}
}
private fun percent(value: Double): String = String.format(Locale.US, "%.1f%%", value)
private fun onePlace(value: Double): String =
String.format(Locale.US, "%.1f", value).trimEnd('0').trimEnd('.')
private fun inches(value: Double): String =
String.format(Locale.US, "%.3f", value).trimEnd('0').trimEnd('.')
private fun inches(value: Double): String = formatInches(value)
private fun changed(
before: Double?,
@@ -94,14 +94,22 @@ fun NumberField(
onValue: (String) -> Unit,
modifier: Modifier = Modifier,
decimal: Boolean = true,
inches: Boolean = false,
placeholder: String = "",
) {
OutlinedTextField(
value = value,
onValueChange = onValue,
label = { Text(label) },
placeholder = if (placeholder.isEmpty()) null else ({ Text(placeholder) }),
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = if (decimal) KeyboardType.Decimal else KeyboardType.Number,
capitalization = KeyboardCapitalization.None,
keyboardType = when {
inches -> KeyboardType.Uri
decimal -> KeyboardType.Decimal
else -> KeyboardType.Number
},
),
textStyle = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
modifier = modifier,
@@ -49,6 +49,7 @@ import com.racingapp.pitlog.model.exportFileName
import com.racingapp.pitlog.model.chassisSetup
import com.racingapp.pitlog.model.chassisTips
import com.racingapp.pitlog.model.formatDegrees
import com.racingapp.pitlog.model.formatInches
import com.racingapp.pitlog.model.formatPsi
import com.racingapp.pitlog.model.formatStagger
import com.racingapp.pitlog.model.handlingText
@@ -237,8 +238,8 @@ private fun SetupBlock(night: RaceNight, latest: RunLog?, onSetup: () -> Unit) {
SetupLine(
"Blocks",
listOf(
night.blockLr?.let { "LR ${String.format(java.util.Locale.US, "%.1f", it)} in" },
night.blockRr?.let { "RR ${String.format(java.util.Locale.US, "%.1f", it)} in" },
night.blockLr?.let { "LR ${formatInches(it)} in" },
night.blockRr?.let { "RR ${formatInches(it)} in" },
).filterNotNull().joinToString(" ").ifBlank { null },
)
SetupLine(
@@ -31,9 +31,12 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.racingapp.pitlog.data.RaceNight
import com.racingapp.pitlog.data.Repository
import com.racingapp.pitlog.model.formatInches
import com.racingapp.pitlog.model.parseDecimal
import com.racingapp.pitlog.model.parseInches
import com.racingapp.pitlog.model.parseIntField
import com.racingapp.pitlog.model.sanitizeDecimal
import com.racingapp.pitlog.model.sanitizeInches
import com.racingapp.pitlog.model.sanitizeInt
import com.racingapp.pitlog.model.sanitizeSignedDecimal
import com.racingapp.pitlog.ui.FieldRow
@@ -71,8 +74,8 @@ private data class SetupForm(
casterLf = parseDecimal(casterLf),
casterRf = parseDecimal(casterRf),
swayBarHole = parseIntField(swayBarHole),
blockLr = parseDecimal(blockLr),
blockRr = parseDecimal(blockRr),
blockLr = parseInches(blockLr),
blockRr = parseInches(blockRr),
)
companion object {
@@ -87,8 +90,8 @@ private data class SetupForm(
casterLf = signedField(night.casterLf),
casterRf = signedField(night.casterRf),
swayBarHole = night.swayBarHole?.toString().orEmpty(),
blockLr = night.blockLr?.let { String.format(Locale.US, "%.1f", it) }.orEmpty(),
blockRr = night.blockRr?.let { String.format(Locale.US, "%.1f", it) }.orEmpty(),
blockLr = formatInches(night.blockLr),
blockRr = formatInches(night.blockRr),
)
}
}
@@ -254,16 +257,20 @@ fun SetupEditorScreen(nightId: Long, onBack: () -> Unit) {
label = "LR",
value = form.blockLr,
onValue = { value ->
viewModel.update { it.copy(blockLr = sanitizeDecimal(value, 1, 1)) }
viewModel.update { it.copy(blockLr = sanitizeInches(value)) }
},
inches = true,
placeholder = "1/2",
modifier = Modifier.weight(1f),
)
NumberField(
label = "RR",
value = form.blockRr,
onValue = { value ->
viewModel.update { it.copy(blockRr = sanitizeDecimal(value, 1, 1)) }
viewModel.update { it.copy(blockRr = sanitizeInches(value)) }
},
inches = true,
placeholder = "1/2",
modifier = Modifier.weight(1f),
)
}
@@ -52,6 +52,7 @@ import com.racingapp.pitlog.model.Tread
import com.racingapp.pitlog.model.formatPsi
import com.racingapp.pitlog.model.hasColdPressure
import com.racingapp.pitlog.model.sanitizeDecimal
import com.racingapp.pitlog.model.sanitizeInches
import com.racingapp.pitlog.model.sanitizeInt
import com.racingapp.pitlog.model.suspiciousReadings
import com.racingapp.pitlog.model.toSheet
@@ -261,8 +262,10 @@ fun RunEditorScreen(
label = "Stagger",
value = form.stagger,
onValue = { value ->
viewModel.update { it.copy(stagger = sanitizeDecimal(value, 2, 2)) }
viewModel.update { it.copy(stagger = sanitizeInches(value)) }
},
inches = true,
placeholder = "1/8",
modifier = Modifier.weight(1f),
)
}
@@ -32,13 +32,16 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.racingapp.pitlog.data.Repository
import com.racingapp.pitlog.data.SetupSheet
import com.racingapp.pitlog.model.formatInches
import com.racingapp.pitlog.model.parseDecimal
import com.racingapp.pitlog.model.parseInches
import com.racingapp.pitlog.model.parseIntField
import com.racingapp.pitlog.model.sanitizeDecimal
import com.racingapp.pitlog.model.sanitizeInches
import com.racingapp.pitlog.model.sanitizeInt
import com.racingapp.pitlog.model.sanitizeSignedDecimal
import com.racingapp.pitlog.model.scaleLines
import com.racingapp.pitlog.model.setupDiffs
import com.racingapp.pitlog.model.weightSplit
import com.racingapp.pitlog.ui.FieldRow
import com.racingapp.pitlog.ui.NumberField
import com.racingapp.pitlog.ui.PitActionButton
@@ -88,15 +91,15 @@ private data class CarSetupForm(
casterLf = parseDecimal(casterLf),
casterRf = parseDecimal(casterRf),
swayBarHole = parseIntField(swayBarHole),
blockLr = parseDecimal(blockLr),
blockRr = parseDecimal(blockRr),
toeFront = parseDecimal(toeFront),
toeRear = parseDecimal(toeRear),
blockLr = parseInches(blockLr),
blockRr = parseInches(blockRr),
toeFront = parseInches(toeFront),
toeRear = parseInches(toeRear),
weightLf = parseIntField(weightLf),
weightRf = parseIntField(weightRf),
weightLr = parseIntField(weightLr),
weightRr = parseIntField(weightRr),
rideHeight = parseDecimal(rideHeight),
rideHeight = parseInches(rideHeight),
pinion = parseDecimal(pinion),
brakeFront = parseDecimal(brakeFront),
)
@@ -113,15 +116,15 @@ private data class CarSetupForm(
casterLf = decimal(sheet.casterLf, 1),
casterRf = decimal(sheet.casterRf, 1),
swayBarHole = sheet.swayBarHole?.toString().orEmpty(),
blockLr = decimal(sheet.blockLr, 1),
blockRr = decimal(sheet.blockRr, 1),
toeFront = decimal(sheet.toeFront, 3),
toeRear = decimal(sheet.toeRear, 3),
blockLr = formatInches(sheet.blockLr),
blockRr = formatInches(sheet.blockRr),
toeFront = formatInches(sheet.toeFront),
toeRear = formatInches(sheet.toeRear),
weightLf = sheet.weightLf?.toString().orEmpty(),
weightRf = sheet.weightRf?.toString().orEmpty(),
weightLr = sheet.weightLr?.toString().orEmpty(),
weightRr = sheet.weightRr?.toString().orEmpty(),
rideHeight = decimal(sheet.rideHeight, 1),
rideHeight = formatInches(sheet.rideHeight),
pinion = decimal(sheet.pinion, 1),
brakeFront = decimal(sheet.brakeFront, 1),
note = if (keepNote) sheet.note else "",
@@ -320,13 +323,17 @@ fun CarSetupEditorScreen(
NumberField(
label = "LR",
value = form.blockLr,
onValue = { value -> viewModel.update { it.copy(blockLr = sanitizeDecimal(value, 1, 1)) } },
onValue = { value -> viewModel.update { it.copy(blockLr = sanitizeInches(value)) } },
inches = true,
placeholder = "1/2",
modifier = Modifier.weight(1f),
)
NumberField(
label = "RR",
value = form.blockRr,
onValue = { value -> viewModel.update { it.copy(blockRr = sanitizeDecimal(value, 1, 1)) } },
onValue = { value -> viewModel.update { it.copy(blockRr = sanitizeInches(value)) } },
inches = true,
placeholder = "1/2",
modifier = Modifier.weight(1f),
)
}
@@ -354,12 +361,12 @@ fun CarSetupEditorScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
FieldRow {
SignedField("Front", form.toeFront, { value -> viewModel.update { it.copy(toeFront = value) } }, Modifier.weight(1f), 1, 3)
SignedField("Rear", form.toeRear, { value -> viewModel.update { it.copy(toeRear = value) } }, Modifier.weight(1f), 1, 3)
SignedField("Front", form.toeFront, { value -> viewModel.update { it.copy(toeFront = value) } }, Modifier.weight(1f), inches = true)
SignedField("Rear", form.toeRear, { value -> viewModel.update { it.copy(toeRear = value) } }, Modifier.weight(1f), inches = true)
}
SectionLabel("Corner weights, lb")
Text(
"Pounds on the pads. Left, rear, and cross are figured from these four.",
"Pounds on the pads. Left, front, rear, and cross come out in pounds and percent. Rear bite is left rear minus right rear.",
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
FieldRow {
@@ -394,13 +401,13 @@ fun CarSetupEditorScreen(
modifier = Modifier.weight(1f),
)
}
weightSplit(
scaleLines(
form.weightLf.toIntOrNull(),
form.weightRf.toIntOrNull(),
form.weightLr.toIntOrNull(),
form.weightRr.toIntOrNull(),
)?.let { split ->
Text(split, modifier = Modifier.padding(top = 8.dp))
).forEach { line ->
Text(line, modifier = Modifier.padding(top = 8.dp))
}
SectionLabel("Ride height")
Text(
@@ -410,7 +417,9 @@ fun CarSetupEditorScreen(
NumberField(
label = "Inches",
value = form.rideHeight,
onValue = { value -> viewModel.update { it.copy(rideHeight = sanitizeDecimal(value, 2, 1)) } },
onValue = { value -> viewModel.update { it.copy(rideHeight = sanitizeInches(value)) } },
inches = true,
placeholder = "5",
modifier = Modifier.fillMaxWidth(),
)
SectionLabel("Pinion")
@@ -457,6 +466,7 @@ private fun SignedField(
value: String,
onValue: (String) -> Unit,
modifier: Modifier = Modifier,
inches: Boolean = false,
wholeDigits: Int = 2,
fractionDigits: Int = 1,
) {
@@ -464,7 +474,14 @@ private fun SignedField(
NumberField(
label = label,
value = value,
onValue = { onValue(sanitizeSignedDecimal(it, wholeDigits, fractionDigits)) },
onValue = {
onValue(
if (inches) sanitizeInches(it, signed = true)
else sanitizeSignedDecimal(it, wholeDigits, fractionDigits),
)
},
inches = inches,
placeholder = if (inches) "1/16" else "",
modifier = Modifier.fillMaxWidth(),
)
TextButton(
@@ -0,0 +1,31 @@
package com.racingapp.pitlog.model
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class InchesTest {
@Test
fun aFractionParsesAndComesBackAsAFraction() {
assertEquals(0.0625, parseInches("1/16")!!, 0.00001)
assertEquals(0.125, parseInches("1/8")!!, 0.00001)
assertEquals(2.5, parseInches("2 1/2")!!, 0.00001)
assertEquals(-0.0625, parseInches("-1/16")!!, 0.00001)
assertEquals(1.125, parseInches("1.125")!!, 0.00001)
assertEquals("1/16", formatInches(parseInches("1/16")))
assertEquals("1/8", formatInches(parseInches("1/8")))
assertEquals("2 1/2", formatInches(parseInches("2 1/2")))
assertEquals("-1/16", formatInches(parseInches("-1/16")))
assertEquals("1 1/8", formatInches(1.125))
}
@Test
fun typingKeepsTheSlashUntilTheFractionIsFinished() {
assertEquals("1/16", sanitizeInches("1/16"))
assertEquals("1/", sanitizeInches("1/"))
assertEquals("2 1/2", sanitizeInches("2 1/2"))
assertEquals("-1/8", sanitizeInches("-1/8", signed = true))
assertNull(parseInches("1/"))
assertNull(parseInches("1/0"))
}
}
@@ -23,7 +23,7 @@ class SetupLogTest {
listOf(
"RF spring 1000 lb → 1050 lb",
"Sway bar Setting 4 → Setting 3",
"Front toe 0.125 in → 0.063 in",
"Front toe 1/8 in → 1/16 in",
),
setupDiffs(original, later),
)
@@ -56,7 +56,12 @@ class SetupLogTest {
assertEquals(
listOf(
"Weights LF 800 RF 760 LR 740 RR 700 lb",
"From the weights Left 51.3% Rear 48% Cross 50%",
"Total 3000 lb",
"Left 1540 lb, 51.3%",
"Front 1560 lb, 52%",
"Rear 1440 lb, 48%",
"Cross 1500 lb, 50%",
"Rear bite 40",
"Ride height 5 in",
"Pinion -2.0°",
"Front brake 62%",
@@ -66,6 +71,11 @@ class SetupLogTest {
assertEquals(
listOf(
"LF weight 800 lb → 820 lb",
"Total 3000 lb → 3020 lb",
"Left 1540 lb, 51.3% → 1560 lb, 51.7%",
"Front 1560 lb, 52% → 1580 lb, 52.3%",
"Rear 1440 lb, 48% → 1440 lb, 47.7%",
"Cross 1500 lb, 50% → 1500 lb, 49.7%",
"Ride height 5 in → 5.2 in",
"Pinion -2.0° → -1.5°",
"Front brake 62% → 65%",
+6 -1
View File
@@ -451,7 +451,12 @@ def serve():
print(f"Pit Log sync listening on {port}", flush=True)
print("Each car in the app has its own token.", flush=True)
server = ThreadingHTTPServer(("0.0.0.0", port), Handler)
server.serve_forever()
try:
server.serve_forever()
except KeyboardInterrupt:
print("Pit Log sync stopped.", flush=True)
finally:
server.server_close()
def selftest():