Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecd5c286dd |
No files matched your search
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId = "com.racingapp.pitlog"
|
applicationId = "com.racingapp.pitlog"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 2
|
||||||
versionName = "0.001"
|
versionName = "0.002"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -234,9 +234,21 @@ internal val MIGRATION_8_9 = object : Migration(8, 9) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal val MIGRATION_9_10 = object : Migration(9, 10) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL("ALTER TABLE setup_sheets ADD COLUMN weightLf INTEGER")
|
||||||
|
db.execSQL("ALTER TABLE setup_sheets ADD COLUMN weightRf INTEGER")
|
||||||
|
db.execSQL("ALTER TABLE setup_sheets ADD COLUMN weightLr INTEGER")
|
||||||
|
db.execSQL("ALTER TABLE setup_sheets ADD COLUMN weightRr INTEGER")
|
||||||
|
db.execSQL("ALTER TABLE setup_sheets ADD COLUMN rideHeight REAL")
|
||||||
|
db.execSQL("ALTER TABLE setup_sheets ADD COLUMN pinion REAL")
|
||||||
|
db.execSQL("ALTER TABLE setup_sheets ADD COLUMN brakeFront REAL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [RaceNight::class, RunLog::class, Car::class, SetupSheet::class],
|
entities = [RaceNight::class, RunLog::class, Car::class, SetupSheet::class],
|
||||||
version = 9,
|
version = 10,
|
||||||
exportSchema = false,
|
exportSchema = false,
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
@@ -245,7 +257,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
companion object {
|
companion object {
|
||||||
fun create(context: Context): AppDatabase {
|
fun create(context: Context): AppDatabase {
|
||||||
return Room.databaseBuilder(context, AppDatabase::class.java, "pitlog.db")
|
return Room.databaseBuilder(context, AppDatabase::class.java, "pitlog.db")
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ data class SetupSheet(
|
|||||||
val blockRr: Double? = null,
|
val blockRr: Double? = null,
|
||||||
val toeFront: Double? = null,
|
val toeFront: Double? = null,
|
||||||
val toeRear: Double? = null,
|
val toeRear: Double? = null,
|
||||||
|
val weightLf: Int? = null,
|
||||||
|
val weightRf: Int? = null,
|
||||||
|
val weightLr: Int? = null,
|
||||||
|
val weightRr: Int? = null,
|
||||||
|
val rideHeight: Double? = null,
|
||||||
|
val pinion: Double? = null,
|
||||||
|
val brakeFront: Double? = null,
|
||||||
@ColumnInfo(defaultValue = "''") val syncId: String = "",
|
@ColumnInfo(defaultValue = "''") val syncId: String = "",
|
||||||
@ColumnInfo(defaultValue = "0") val updatedAt: Long = 0,
|
@ColumnInfo(defaultValue = "0") val updatedAt: Long = 0,
|
||||||
@ColumnInfo(defaultValue = "0") val deleted: Boolean = false,
|
@ColumnInfo(defaultValue = "0") val deleted: Boolean = false,
|
||||||
|
|||||||
@@ -200,6 +200,13 @@ private fun sheetJson(sheet: SetupSheet) = JSONObject()
|
|||||||
.putNullable("blockRr", sheet.blockRr)
|
.putNullable("blockRr", sheet.blockRr)
|
||||||
.putNullable("toeFront", sheet.toeFront)
|
.putNullable("toeFront", sheet.toeFront)
|
||||||
.putNullable("toeRear", sheet.toeRear)
|
.putNullable("toeRear", sheet.toeRear)
|
||||||
|
.putNullable("weightLf", sheet.weightLf)
|
||||||
|
.putNullable("weightRf", sheet.weightRf)
|
||||||
|
.putNullable("weightLr", sheet.weightLr)
|
||||||
|
.putNullable("weightRr", sheet.weightRr)
|
||||||
|
.putNullable("rideHeight", sheet.rideHeight)
|
||||||
|
.putNullable("pinion", sheet.pinion)
|
||||||
|
.putNullable("brakeFront", sheet.brakeFront)
|
||||||
|
|
||||||
private fun sheetFrom(json: JSONObject) = SetupSheet(
|
private fun sheetFrom(json: JSONObject) = SetupSheet(
|
||||||
recordedAt = json.optLong("recordedAt"),
|
recordedAt = json.optLong("recordedAt"),
|
||||||
@@ -218,6 +225,13 @@ private fun sheetFrom(json: JSONObject) = SetupSheet(
|
|||||||
blockRr = json.optionalDouble("blockRr"),
|
blockRr = json.optionalDouble("blockRr"),
|
||||||
toeFront = json.optionalDouble("toeFront"),
|
toeFront = json.optionalDouble("toeFront"),
|
||||||
toeRear = json.optionalDouble("toeRear"),
|
toeRear = json.optionalDouble("toeRear"),
|
||||||
|
weightLf = json.optionalInt("weightLf"),
|
||||||
|
weightRf = json.optionalInt("weightRf"),
|
||||||
|
weightLr = json.optionalInt("weightLr"),
|
||||||
|
weightRr = json.optionalInt("weightRr"),
|
||||||
|
rideHeight = json.optionalDouble("rideHeight"),
|
||||||
|
pinion = json.optionalDouble("pinion"),
|
||||||
|
brakeFront = json.optionalDouble("brakeFront"),
|
||||||
syncId = json.optString("syncId"),
|
syncId = json.optString("syncId"),
|
||||||
updatedAt = json.optLong("updatedAt"),
|
updatedAt = json.optLong("updatedAt"),
|
||||||
deleted = json.optBoolean("deleted"),
|
deleted = json.optBoolean("deleted"),
|
||||||
|
|||||||
@@ -21,6 +21,27 @@ fun setupLines(sheet: SetupSheet): List<String> = buildList {
|
|||||||
sheet.casterRf?.let { add("RF caster ${formatDegrees(it)}") }
|
sheet.casterRf?.let { add("RF caster ${formatDegrees(it)}") }
|
||||||
sheet.toeFront?.let { add("Front toe ${inches(it)} in out") }
|
sheet.toeFront?.let { add("Front toe ${inches(it)} in out") }
|
||||||
sheet.toeRear?.let { add("Rear toe ${inches(it)} in") }
|
sheet.toeRear?.let { add("Rear toe ${inches(it)} in") }
|
||||||
|
val weights = listOf(
|
||||||
|
"LF" to sheet.weightLf,
|
||||||
|
"RF" to sheet.weightRf,
|
||||||
|
"LR" to sheet.weightLr,
|
||||||
|
"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") }
|
||||||
|
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? {
|
||||||
|
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)}%"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setupDiffs(before: SetupSheet, after: SetupSheet): List<String> = buildList {
|
fun setupDiffs(before: SetupSheet, after: SetupSheet): List<String> = buildList {
|
||||||
@@ -38,10 +59,20 @@ fun setupDiffs(before: SetupSheet, after: SetupSheet): List<String> = buildList
|
|||||||
changed(before.casterRf, after.casterRf, ::formatDegrees)?.let { add("RF caster $it") }
|
changed(before.casterRf, after.casterRf, ::formatDegrees)?.let { add("RF caster $it") }
|
||||||
changed(before.toeFront, after.toeFront) { "${inches(it)} in" }?.let { add("Front toe $it") }
|
changed(before.toeFront, after.toeFront) { "${inches(it)} in" }?.let { add("Front toe $it") }
|
||||||
changed(before.toeRear, after.toeRear) { "${inches(it)} in" }?.let { add("Rear toe $it") }
|
changed(before.toeRear, after.toeRear) { "${inches(it)} in" }?.let { add("Rear toe $it") }
|
||||||
|
changedInt(before.weightLf, after.weightLf, " lb")?.let { add("LF weight $it") }
|
||||||
|
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") }
|
||||||
|
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 percent(value: Double): String = String.format(Locale.US, "%.1f%%", value)
|
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 =
|
private fun inches(value: Double): String =
|
||||||
String.format(Locale.US, "%.3f", value).trimEnd('0').trimEnd('.')
|
String.format(Locale.US, "%.3f", value).trimEnd('0').trimEnd('.')
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,38 @@ package com.racingapp.pitlog.ui.guide
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.racingapp.pitlog.ui.PitScroll
|
|
||||||
import com.racingapp.pitlog.ui.SectionLabel
|
import com.racingapp.pitlog.ui.SectionLabel
|
||||||
import com.racingapp.pitlog.ui.theme.pitTopBarColors
|
import com.racingapp.pitlog.ui.theme.pitTopBarColors
|
||||||
|
|
||||||
@@ -208,9 +225,131 @@ private val handling = listOf(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val starts = listOf(
|
||||||
|
Tip("Crossweight", "52–55% on a leaf-spring car. Less than a coil car usually wants."),
|
||||||
|
Tip("Front springs", "Rate each spring at the height it sits in the car. Replace a bent or tired one. Keep the split under about 100 lb. Softening the front adds front traction, so the cross has to come up or the car gets loose. If the front out-rolls the rear, stiffen the front springs and use less bar."),
|
||||||
|
Tip("Caster", "Top of the ball-joint line tilted back is positive. Tilted forward is negative. Positive caster holds a line. Too much makes the wheel heavy. Negative caster lets the car wander on the straight. A short track often wants about 2–4° of split, left front around 1–2° and right front around 3–5°. More banking wants less caster. A tighter radius wants more split. At mid-turn the wheel should pull slightly. Check the toe after you change caster."),
|
||||||
|
Tip("Blocks", "Under the leaves. More block tightens the entry and the middle. Over 3 inches cuts forward bite. South Sound allows steel leaves, at least four, with blocks, shackles, and sliders. The tri-state book allows steel or poly leaves and shackles."),
|
||||||
|
Tip("Sway bar", "Less bar if the car is tight. More bar if it is loose. The tri-state book allows a 1-1/4 inch bar in the stock saddles, adjusted with all-thread, no heim joints. South Sound's book does not list a bar."),
|
||||||
|
Tip("Stagger", "Right rear minus left rear, around the center of the tread, with a flexible 1/4-inch tape, at the cold pressure you will race. On a bias-ply tire, more air on the right, or less on the left, grows the stagger before you change tires. That rear-stagger gain takes cross out. More air that grows the right front adds cross. A tighter radius wants more. More banking wants a little less. Too much makes the car turn left on the straight, so the driver steers right and scrubs speed. On a bias-ply tire a sliding right rear heats, grows, and the loose gets worse as the run goes on. Too little heats the right front and the push gets worse the same way."),
|
||||||
|
Tip("Toe", "1/16 to 1/8 inch out in front on asphalt. A tighter track wants the larger end. Tracks under a half mile are published out to 1/4 inch. Rear wheels straight. Toe out takes the slack out of the tie-rod ends. Check it after a camber change, a caster change, a ride-height change, or any wheel contact."),
|
||||||
|
Tip("Added toe", "Toe that grows as the wheel turns is Ackermann. Toe that shrinks is reverse Ackermann. One degree is about 1/2 inch of toe on an 85-inch tire. A tight quarter-mile only needs about 1/16 inch more on the left front. A half-mile needs about 1/64 inch. Stock boxes already add toe in a left turn, so measure it before you add any. Another note calls about 3° of extra left-front steer, in 10° of wheel turn, a common amount, and more of it on a 3/8-mile and under. Past what the corner needs, the left front grains, runs hot, and drags, and the car gets loose off. A little can free a tight middle. You feel it at the apex."),
|
||||||
|
Tip("String check", "Fronts straight. Mark the floor 10 feet ahead of each spindle. Turn the wheel about as far as the driver does in the corner and mark again. If the left mark moved farther than the right, the car gained toe. If the right mark moved farther, the car lost toe. Tie rods that angle forward from the inner pivot add Ackermann. If the spindle arm is slotted and the book allows it, moving the left tie-rod end closer to the ball joint adds more."),
|
||||||
|
Tip("Brakes", "About 60–65% of the braking force on the front. A gauge in the bleeder tells you the pressure at that wheel. More rear brake loosens a hard entry. If the adjuster ends up slammed to one side, the master-cylinder size or the pads are doing the work the knob cannot do."),
|
||||||
|
Tip("Lead", "Look for quarter pounds when you build the car, and leave the safety parts alone. Stay at the book's minimum total. Running heavy to buy left-side percent makes the tires work harder. One low clump, left of center, near the middle of the car, as close to the left-side maximum as that weight allows. Lead split front to rear, or left rail to right rail, reads the same on the scales and transfers more once the car is moving. Battery, tanks, and other heavy parts go on the left. Paint the lead white with the car number."),
|
||||||
|
Tip("Pinion", "A leaf car needs more angle than a coil car because the spring wraps and the pinion rises. Measure pinion versus driveshaft at ride height, car weighted. The U-joint wants at least 1/2° so the needles turn, and most shops want it under 3°. About 2° nose-down from the transmission centerline is a circle-track leaf note so the joints oppose. On asphalt, make a rear-steer change small, then leave it and tune the car with the other pieces. The 5–7° nose-down figure is a drag-racing launch number."),
|
||||||
|
Tip("Tires", "Hot pressure has to land within about 0.5 psi of the tire maker's target. The tread, inside middle outside, says whether that tire was high or low once it was hot. Air is also rate, footprint, and cross. Two tires with the same size stamp can still differ by an inch or more around. Buy that rollout. Air only fine-tunes it."),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val tracks = listOf(
|
||||||
|
Tip("Camber", "2 inches on the right and 1 inch on the left, measured with a square at the top of the wheel."),
|
||||||
|
Tip("Tri-state book", "Stateline hobby stock and Wenatchee thunder cars. A second-gen Camaro: 3,200 lb, 55% left maximum, 48% rear maximum, on a stock-dimension engine under 360 cubic inches. Hoosier 970, and the tire you qualify on starts the main. Sway bar 1-1/4 inch maximum, stock saddles, all-thread adjuster. No weight jacks. Pocket spring adjusters, steel or poly leaves, and shackles are allowed. Wenatchee allows an open rear. Stateline is spool or welded. No limited slip and no Detroit Locker. Ride height 5 inches. Lead on 1/2-inch grade 5 bolts."),
|
||||||
|
Tip("South Sound book", "3,250 lb, 54% left maximum, 49% rear maximum. American Racer take-off slicks. Steel leaves, at least four, with blocks, shackles, and sliders. Open, welded, or a spool. Same 2-inch and 1-inch camber, and the same 5-inch ride height. The book does not list a sway bar."),
|
||||||
|
Tip("Ride height", "5 inches from the lowest frame at the driver's door."),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val leaveAlone = listOf(
|
||||||
|
Tip("Big-bar soft-spring", "A late-model package. Soft front springs and a big bar are there to hold a sleek nose on the ground so the aero can work. The bar adds rate, so the car is stiffer, and the tires have to live with that load. A stock-body leaf-spring car does not get that aero, and a bigger bar on a leaf-spring Camaro faded after a few laps. Banking already holds the nose down, so the bar does less of that job."),
|
||||||
|
Tip("Dirt sheets", "Clay numbers, including a half inch of front toe-out, are for a slick rutted track."),
|
||||||
|
Tip("Drag pinion", "5–7° nose-down is a hard launch on a soft spring, not an asphalt oval setting."),
|
||||||
|
Tip("Even temps", "A simulator that wants the inside hottest on every tire is not the paved-oval target above."),
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class Note(val title: String, val body: String)
|
||||||
|
|
||||||
|
private data class Block(val heading: String = "", val notes: List<Note>)
|
||||||
|
|
||||||
|
private data class Chapter(val title: String, val intro: String = "", val blocks: List<Block> = emptyList())
|
||||||
|
|
||||||
|
private fun notesOf(tips: List<Tip>) = listOf(Block(notes = tips.map { Note(it.see, it.doThis) }))
|
||||||
|
|
||||||
|
private fun blocksOf(cases: List<HandlingCase>) =
|
||||||
|
cases.map { Block(it.title, it.moves.map { move -> Note(move.change, move.why) }) }
|
||||||
|
|
||||||
|
private const val OPENING =
|
||||||
|
"Leaf-spring Camaro on asphalt. Hobby stock or thunder car. If the rule book disagrees, follow the rule book. Change one thing, write it in the log, and read the tires before the next guess."
|
||||||
|
|
||||||
|
private val book = listOf(
|
||||||
|
Chapter(
|
||||||
|
"Heat",
|
||||||
|
"Probe inside, middle, outside right after a full-speed run, before the car sits in the stall. Start at the right front, then the right rear, the left rear, and the left front. The right sides cool first. One person probes and one writes. Same order every time. A needle in the tread beats a surface gun. The surface is usually 20–40° cooler than the probe, and brake heat or engine heat can fool the gun. A spread across one tire past about 10% is the one to fix. A new tire runs hotter than a used one, so read camber and pressure from the spread across that tire. Temps on a car that is way off are a weak signal until it is at least drivable.",
|
||||||
|
notesOf(heatTips),
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
"Air pressure",
|
||||||
|
"Air is a chassis change. It moves spring rate, the footprint, and crossweight. On a bias-ply tire it also moves stagger. The Hoosier 970 in the tri-state book is a bias ply, so air changes its rollout. A radial holds its size, and air on a radial changes rate and footprint only. An American Racer take-off can be either, so read the tire before you use air for stagger. A few pounds is the whole window. Buy the rollout you need, and let air fine-tune it. Stay at or above the tire maker's minimum. Air under that can tear the sidewall.",
|
||||||
|
notesOf(airEffects),
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
"When the set is done",
|
||||||
|
"Write the tire set on the run. Temps, depth, and heat cycles from that set tell you when another week is wishful.",
|
||||||
|
notesOf(tireLife),
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
"How to tell",
|
||||||
|
"Fix the middle before the entry or the exit. A tight car can feel loose: the driver adds steering, the front bites, and the rear snaps when the throttle comes in. If the wheel is turned farther at speed than on a moderate lap, the car is tight. If it is turned less, it is loose. If you are steering right in the middle, bring it in.",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
"What to change",
|
||||||
|
"One change, then another run. Skip a move the rule book does not allow.",
|
||||||
|
blocksOf(handling),
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
"Air for handling",
|
||||||
|
"Longacre's order when the only change is air on a bias-ply tire. The same change moves rate, footprint, stagger, and cross. If you only wanted the stagger, put the cross back on the scales. Size rear stagger for the radius, then leave it. Extra past that gets loose on power, so do not add rear stagger just to free a tight middle.",
|
||||||
|
blocksOf(airHandling),
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
"Order of work",
|
||||||
|
"Geometry and alignment come before brakes and aero. On a hobby-stock F-body a lot of the geometry is frozen. Do the items the book allows, in this order, and stop when the car is balanced.",
|
||||||
|
notesOf(order),
|
||||||
|
),
|
||||||
|
Chapter("Where to start", blocks = notesOf(starts)),
|
||||||
|
Chapter(
|
||||||
|
"In the shop",
|
||||||
|
"Do this before the first race, and again when something hits the wall or the car starts doing something new. An alignment with a couple of people is an hour or two.",
|
||||||
|
notesOf(shop),
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
"On the scales",
|
||||||
|
"Write the four corner weights in the notes and the cross on the setup. Same routine every time, or the number does not compare with last week.",
|
||||||
|
notesOf(scales),
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
"These tracks",
|
||||||
|
"Tri-City Raceway, Stateline Speedway, South Sound Speedway, and Wenatchee Valley Super Oval. Hobby stock or thunder car. A second-generation Camaro is not a Gen 3, so the Gen 3 left-side cap does not apply. The book at that gate wins. A weekly bulletin wins over this page.",
|
||||||
|
notesOf(tracks),
|
||||||
|
),
|
||||||
|
Chapter("Leave these alone", blocks = notesOf(leaveAlone)),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun holds(words: List<String>, text: String) = words.all { text.contains(it, ignoreCase = true) }
|
||||||
|
|
||||||
|
private fun Chapter.narrowed(words: List<String>): Chapter? {
|
||||||
|
if (holds(words, title)) return this
|
||||||
|
val introShown = intro.isNotBlank() && holds(words, intro)
|
||||||
|
val kept = blocks.mapNotNull { block ->
|
||||||
|
if (block.heading.isNotBlank() && holds(words, block.heading)) {
|
||||||
|
block
|
||||||
|
} else {
|
||||||
|
val notes = block.notes.filter { holds(words, it.title + "\n" + it.body) }
|
||||||
|
if (notes.isEmpty()) null else block.copy(notes = notes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!introShown && kept.isEmpty()) return null
|
||||||
|
return copy(intro = if (introShown) intro else "", blocks = kept)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SetupGuideScreen(onBack: () -> Unit) {
|
fun SetupGuideScreen(onBack: () -> Unit) {
|
||||||
|
var query by remember { mutableStateOf("") }
|
||||||
|
val words = query.trim().split(Regex("\\s+")).filter { it.isNotEmpty() }
|
||||||
|
val shown = if (words.isEmpty()) book else book.mapNotNull { it.narrowed(words) }
|
||||||
|
val showOpening = words.isEmpty() || holds(words, OPENING)
|
||||||
|
val scroll = rememberScrollState()
|
||||||
|
val keyboard = LocalSoftwareKeyboardController.current
|
||||||
|
LaunchedEffect(query) { scroll.scrollTo(0) }
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -224,126 +363,87 @@ fun SetupGuideScreen(onBack: () -> Unit) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
PitScroll(padding) {
|
Column(
|
||||||
Text(
|
Modifier
|
||||||
"Leaf-spring Camaro on asphalt. Hobby stock or thunder car. If the rule book disagrees, follow the rule book. Change one thing, write it in the log, and read the tires before the next guess.",
|
.fillMaxSize()
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
.padding(padding),
|
||||||
)
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
SectionLabel("Heat")
|
) {
|
||||||
Text(
|
OutlinedTextField(
|
||||||
"Probe inside, middle, outside right after a full-speed run, before the car sits in the stall. Start at the right front, then the right rear, the left rear, and the left front. The right sides cool first. One person probes and one writes. Same order every time. A needle in the tread beats a surface gun. The surface is usually 20–40° cooler than the probe, and brake heat or engine heat can fool the gun. A spread across one tire past about 10% is the one to fix. A new tire runs hotter than a used one, so read camber and pressure from the spread across that tire. Temps on a car that is way off are a weak signal until it is at least drivable.",
|
value = query,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
onValueChange = { query = it },
|
||||||
)
|
label = { Text("Look up") },
|
||||||
Column(Modifier.padding(top = 8.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
placeholder = { Text("stagger, camber, tight") },
|
||||||
heatTips.forEach { tip ->
|
leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
|
||||||
TipLine(tip.see, tip.doThis)
|
trailingIcon = {
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
IconButton(onClick = { query = "" }) {
|
||||||
|
Icon(Icons.Filled.Close, contentDescription = "Clear lookup")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SectionLabel("Air pressure")
|
},
|
||||||
Text(
|
singleLine = true,
|
||||||
"Air is a chassis change. It moves spring rate, the footprint, and crossweight. On a bias-ply tire it also moves stagger. The Hoosier 970 in the tri-state book is a bias ply, so air changes its rollout. A radial holds its size, and air on a radial changes rate and footprint only. An American Racer take-off can be either, so read the tire before you use air for stagger. A few pounds is the whole window. Buy the rollout you need, and let air fine-tune it. Stay at or above the tire maker's minimum. Air under that can tear the sidewall.",
|
keyboardOptions = KeyboardOptions(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Search,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(onSearch = { keyboard?.hide() }),
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 640.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
)
|
)
|
||||||
airEffects.forEach { tip ->
|
Column(
|
||||||
TipLine(tip.see, tip.doThis)
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(scroll),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.widthIn(max = 640.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
) {
|
||||||
|
if (showOpening) {
|
||||||
|
Text(OPENING, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
SectionLabel("When the set is done")
|
if (shown.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
"Write the tire set on the run. Temps, depth, and heat cycles from that set tell you when another week is wishful.",
|
"Nothing in the book matches that.",
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
)
|
)
|
||||||
tireLife.forEach { tip ->
|
|
||||||
TipLine(tip.see, tip.doThis)
|
|
||||||
}
|
}
|
||||||
SectionLabel("How to tell")
|
shown.forEach { chapter ->
|
||||||
|
SectionLabel(chapter.title)
|
||||||
|
if (chapter.intro.isNotEmpty()) {
|
||||||
|
Text(chapter.intro, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
chapter.blocks.forEach { block ->
|
||||||
|
if (block.heading.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
"Fix the middle before the entry or the exit. A tight car can feel loose: the driver adds steering, the front bites, and the rear snaps when the throttle comes in. If the wheel is turned farther at speed than on a moderate lap, the car is tight. If it is turned less, it is loose. If you are steering right in the middle, bring it in.",
|
block.heading,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
SectionLabel("What to change")
|
|
||||||
Text(
|
|
||||||
"One change, then another run. Skip a move the rule book does not allow.",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
handling.forEach { item ->
|
|
||||||
Text(
|
|
||||||
item.title,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
)
|
)
|
||||||
item.moves.forEach { move ->
|
}
|
||||||
TipLine(move.change, move.why)
|
if (chapter.title == "Heat" && block.heading.isEmpty()) {
|
||||||
|
Column(
|
||||||
|
Modifier.padding(top = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
block.notes.forEach { TipLine(it.title, it.body) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
block.notes.forEach { TipLine(it.title, it.body) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SectionLabel("Air for handling")
|
|
||||||
Text(
|
|
||||||
"Longacre's order when the only change is air on a bias-ply tire. The same change moves rate, footprint, stagger, and cross. If you only wanted the stagger, put the cross back on the scales. Size rear stagger for the radius, then leave it. Extra past that gets loose on power, so do not add rear stagger just to free a tight middle.",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
airHandling.forEach { item ->
|
|
||||||
Text(
|
|
||||||
item.title,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
|
||||||
)
|
|
||||||
item.moves.forEach { move ->
|
|
||||||
TipLine(move.change, move.why)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SectionLabel("Order of work")
|
|
||||||
Text(
|
|
||||||
"Geometry and alignment come before brakes and aero. On a hobby-stock F-body a lot of the geometry is frozen. Do the items the book allows, in this order, and stop when the car is balanced.",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
order.forEach { tip ->
|
|
||||||
TipLine(tip.see, tip.doThis)
|
|
||||||
}
|
}
|
||||||
SectionLabel("Where to start")
|
|
||||||
TipLine("Crossweight", "52–55% on a leaf-spring car. Less than a coil car usually wants.")
|
|
||||||
TipLine("Front springs", "Rate each spring at the height it sits in the car. Replace a bent or tired one. Keep the split under about 100 lb. Softening the front adds front traction, so the cross has to come up or the car gets loose. If the front out-rolls the rear, stiffen the front springs and use less bar.")
|
|
||||||
TipLine("Caster", "Top of the ball-joint line tilted back is positive. Tilted forward is negative. Positive caster holds a line. Too much makes the wheel heavy. Negative caster lets the car wander on the straight. A short track often wants about 2–4° of split, left front around 1–2° and right front around 3–5°. More banking wants less caster. A tighter radius wants more split. At mid-turn the wheel should pull slightly. Check the toe after you change caster.")
|
|
||||||
TipLine("Blocks", "Under the leaves. More block tightens the entry and the middle. Over 3 inches cuts forward bite. South Sound allows steel leaves, at least four, with blocks, shackles, and sliders. The tri-state book allows steel or poly leaves and shackles.")
|
|
||||||
TipLine("Sway bar", "Less bar if the car is tight. More bar if it is loose. The tri-state book allows a 1-1/4 inch bar in the stock saddles, adjusted with all-thread, no heim joints. South Sound's book does not list a bar.")
|
|
||||||
TipLine("Stagger", "Right rear minus left rear, around the center of the tread, with a flexible 1/4-inch tape, at the cold pressure you will race. On a bias-ply tire, more air on the right, or less on the left, grows the stagger before you change tires. That rear-stagger gain takes cross out. More air that grows the right front adds cross. A tighter radius wants more. More banking wants a little less. Too much makes the car turn left on the straight, so the driver steers right and scrubs speed. On a bias-ply tire a sliding right rear heats, grows, and the loose gets worse as the run goes on. Too little heats the right front and the push gets worse the same way.")
|
|
||||||
TipLine("Toe", "1/16 to 1/8 inch out in front on asphalt. A tighter track wants the larger end. Tracks under a half mile are published out to 1/4 inch. Rear wheels straight. Toe out takes the slack out of the tie-rod ends. Check it after a camber change, a caster change, a ride-height change, or any wheel contact.")
|
|
||||||
TipLine("Added toe", "Toe that grows as the wheel turns is Ackermann. Toe that shrinks is reverse Ackermann. One degree is about 1/2 inch of toe on an 85-inch tire. A tight quarter-mile only needs about 1/16 inch more on the left front. A half-mile needs about 1/64 inch. Stock boxes already add toe in a left turn, so measure it before you add any. Another note calls about 3° of extra left-front steer, in 10° of wheel turn, a common amount, and more of it on a 3/8-mile and under. Past what the corner needs, the left front grains, runs hot, and drags, and the car gets loose off. A little can free a tight middle. You feel it at the apex.")
|
|
||||||
TipLine("String check", "Fronts straight. Mark the floor 10 feet ahead of each spindle. Turn the wheel about as far as the driver does in the corner and mark again. If the left mark moved farther than the right, the car gained toe. If the right mark moved farther, the car lost toe. Tie rods that angle forward from the inner pivot add Ackermann. If the spindle arm is slotted and the book allows it, moving the left tie-rod end closer to the ball joint adds more.")
|
|
||||||
TipLine("Brakes", "About 60–65% of the braking force on the front. A gauge in the bleeder tells you the pressure at that wheel. More rear brake loosens a hard entry. If the adjuster ends up slammed to one side, the master-cylinder size or the pads are doing the work the knob cannot do.")
|
|
||||||
TipLine("Lead", "Look for quarter pounds when you build the car, and leave the safety parts alone. Stay at the book's minimum total. Running heavy to buy left-side percent makes the tires work harder. One low clump, left of center, near the middle of the car, as close to the left-side maximum as that weight allows. Lead split front to rear, or left rail to right rail, reads the same on the scales and transfers more once the car is moving. Battery, tanks, and other heavy parts go on the left. Paint the lead white with the car number.")
|
|
||||||
TipLine("Pinion", "A leaf car needs more angle than a coil car because the spring wraps and the pinion rises. Measure pinion versus driveshaft at ride height, car weighted. The U-joint wants at least 1/2° so the needles turn, and most shops want it under 3°. About 2° nose-down from the transmission centerline is a circle-track leaf note so the joints oppose. On asphalt, make a rear-steer change small, then leave it and tune the car with the other pieces. The 5–7° nose-down figure is a drag-racing launch number.")
|
|
||||||
TipLine("Tires", "Hot pressure has to land within about 0.5 psi of the tire maker's target. The tread, inside middle outside, says whether that tire was high or low once it was hot. Air is also rate, footprint, and cross. Two tires with the same size stamp can still differ by an inch or more around. Buy that rollout. Air only fine-tunes it.")
|
|
||||||
SectionLabel("In the shop")
|
|
||||||
Text(
|
|
||||||
"Do this before the first race, and again when something hits the wall or the car starts doing something new. An alignment with a couple of people is an hour or two.",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
shop.forEach { tip ->
|
|
||||||
TipLine(tip.see, tip.doThis)
|
|
||||||
}
|
|
||||||
SectionLabel("On the scales")
|
|
||||||
Text(
|
|
||||||
"Write the four corner weights in the notes and the cross on the setup. Same routine every time, or the number does not compare with last week.",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
scales.forEach { tip ->
|
|
||||||
TipLine(tip.see, tip.doThis)
|
|
||||||
}
|
|
||||||
SectionLabel("These tracks")
|
|
||||||
Text(
|
|
||||||
"Tri-City Raceway, Stateline Speedway, South Sound Speedway, and Wenatchee Valley Super Oval. Hobby stock or thunder car. A second-generation Camaro is not a Gen 3, so the Gen 3 left-side cap does not apply. The book at that gate wins. A weekly bulletin wins over this page.",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
TipLine("Camber", "2 inches on the right and 1 inch on the left, measured with a square at the top of the wheel.")
|
|
||||||
TipLine("Tri-state book", "Stateline hobby stock and Wenatchee thunder cars. A second-gen Camaro: 3,200 lb, 55% left maximum, 48% rear maximum, on a stock-dimension engine under 360 cubic inches. Hoosier 970, and the tire you qualify on starts the main. Sway bar 1-1/4 inch maximum, stock saddles, all-thread adjuster. No weight jacks. Pocket spring adjusters, steel or poly leaves, and shackles are allowed. Wenatchee allows an open rear. Stateline is spool or welded. No limited slip and no Detroit Locker. Ride height 5 inches. Lead on 1/2-inch grade 5 bolts.")
|
|
||||||
TipLine("South Sound book", "3,250 lb, 54% left maximum, 49% rear maximum. American Racer take-off slicks. Steel leaves, at least four, with blocks, shackles, and sliders. Open, welded, or a spool. Same 2-inch and 1-inch camber, and the same 5-inch ride height. The book does not list a sway bar.")
|
|
||||||
TipLine("Ride height", "5 inches from the lowest frame at the driver's door.")
|
|
||||||
SectionLabel("Leave these alone")
|
|
||||||
TipLine("Big-bar soft-spring", "A late-model package. Soft front springs and a big bar are there to hold a sleek nose on the ground so the aero can work. The bar adds rate, so the car is stiffer, and the tires have to live with that load. A stock-body leaf-spring car does not get that aero, and a bigger bar on a leaf-spring Camaro faded after a few laps. Banking already holds the nose down, so the bar does less of that job.")
|
|
||||||
TipLine("Dirt sheets", "Clay numbers, including a half inch of front toe-out, are for a slick rutted track.")
|
|
||||||
TipLine("Drag pinion", "5–7° nose-down is a hard launch on a soft spring, not an asphalt oval setting.")
|
|
||||||
TipLine("Even temps", "A simulator that wants the inside hottest on every tire is not the paved-oval target above.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,10 +153,6 @@ fun CarsScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
Text(car.carName, style = MaterialTheme.typography.titleLarge)
|
Text(car.carName, style = MaterialTheme.typography.titleLarge)
|
||||||
Text(
|
|
||||||
"${car.raceClass} · ${car.discipline}",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
IconButton(onClick = { pendingDelete = car }) {
|
IconButton(onClick = { pendingDelete = car }) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.racingapp.pitlog.data.Car
|
import com.racingapp.pitlog.data.Car
|
||||||
import com.racingapp.pitlog.data.DEFAULT_CLASS
|
|
||||||
import com.racingapp.pitlog.data.DEFAULT_DISCIPLINE
|
|
||||||
import com.racingapp.pitlog.data.NightWithRuns
|
import com.racingapp.pitlog.data.NightWithRuns
|
||||||
import com.racingapp.pitlog.data.Repository
|
import com.racingapp.pitlog.data.Repository
|
||||||
import com.racingapp.pitlog.model.Corner
|
import com.racingapp.pitlog.model.Corner
|
||||||
@@ -160,7 +158,6 @@ fun HomeScreen(
|
|||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
HomeHeader(
|
HomeHeader(
|
||||||
profile = state.profile,
|
|
||||||
tonight = tonight,
|
tonight = tonight,
|
||||||
onLogRun = onLogRun,
|
onLogRun = onLogRun,
|
||||||
onNewNight = onNewNight,
|
onNewNight = onNewNight,
|
||||||
@@ -189,24 +186,13 @@ fun HomeScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HomeHeader(
|
private fun HomeHeader(
|
||||||
profile: Car?,
|
|
||||||
tonight: NightWithRuns?,
|
tonight: NightWithRuns?,
|
||||||
onLogRun: (Long) -> Unit,
|
onLogRun: (Long) -> Unit,
|
||||||
onNewNight: () -> Unit,
|
onNewNight: () -> Unit,
|
||||||
onCarSetup: () -> Unit,
|
onCarSetup: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val raceClass = profile?.raceClass?.ifBlank { DEFAULT_CLASS } ?: DEFAULT_CLASS
|
|
||||||
val discipline = profile?.discipline?.ifBlank { DEFAULT_DISCIPLINE } ?: DEFAULT_DISCIPLINE
|
|
||||||
Column(Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
Column(Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
CarMark()
|
CarMark()
|
||||||
Column(Modifier.padding(start = 12.dp)) {
|
|
||||||
Text(
|
|
||||||
"$raceClass · $discipline",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tonight != null) {
|
if (tonight != null) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { onLogRun(tonight.night.id) },
|
onClick = { onLogRun(tonight.night.id) },
|
||||||
|
|||||||
@@ -286,12 +286,9 @@ private fun NightHeader(night: RaceNight) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
Text(formatLongDate(night.dateEpochDay), style = MaterialTheme.typography.titleMedium)
|
Text(formatLongDate(night.dateEpochDay), style = MaterialTheme.typography.titleMedium)
|
||||||
Text(
|
if (night.carName.isNotBlank()) {
|
||||||
listOf(night.carName, night.raceClass, night.discipline)
|
Text(night.carName, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
.filter { it.isNotBlank() }
|
}
|
||||||
.joinToString(" · "),
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
night.airTempF?.let { Text("Air $it°F") }
|
night.airTempF?.let { Text("Air $it°F") }
|
||||||
if (night.notes.isNotBlank()) {
|
if (night.notes.isNotBlank()) {
|
||||||
Text(night.notes, modifier = Modifier.padding(top = 4.dp))
|
Text(night.notes, modifier = Modifier.padding(top = 4.dp))
|
||||||
|
|||||||
@@ -356,11 +356,6 @@ fun NightEditorScreen(
|
|||||||
value = form.carName,
|
value = form.carName,
|
||||||
onValue = { value -> viewModel.update { it.copy(carName = value.take(80)) } },
|
onValue = { value -> viewModel.update { it.copy(carName = value.take(80)) } },
|
||||||
)
|
)
|
||||||
WordsField(
|
|
||||||
label = "Class",
|
|
||||||
value = form.raceClass,
|
|
||||||
onValue = { value -> viewModel.update { it.copy(raceClass = value.take(80)) } },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,16 +232,6 @@ fun SettingsScreen(showCar: Boolean, onBack: () -> Unit, onCarRemoved: () -> Uni
|
|||||||
value = viewModel.carName,
|
value = viewModel.carName,
|
||||||
onValue = { viewModel.carName = it.take(80) },
|
onValue = { viewModel.carName = it.take(80) },
|
||||||
)
|
)
|
||||||
WordsField(
|
|
||||||
label = "Class",
|
|
||||||
value = viewModel.raceClass,
|
|
||||||
onValue = { viewModel.raceClass = it.take(80) },
|
|
||||||
)
|
|
||||||
WordsField(
|
|
||||||
label = "Type of racing",
|
|
||||||
value = viewModel.discipline,
|
|
||||||
onValue = { viewModel.discipline = it.take(40) },
|
|
||||||
)
|
|
||||||
SectionLabel("This car's token")
|
SectionLabel("This car's token")
|
||||||
Text(
|
Text(
|
||||||
"This token is 8 digits. Share it with another phone, then on that phone use Add a car from a token. That phone syncs this car. A different car gets a different token.",
|
"This token is 8 digits. Share it with another phone, then on that phone use Add a car from a token. That phone syncs this car. A different car gets a different token.",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import com.racingapp.pitlog.model.sanitizeDecimal
|
|||||||
import com.racingapp.pitlog.model.sanitizeInt
|
import com.racingapp.pitlog.model.sanitizeInt
|
||||||
import com.racingapp.pitlog.model.sanitizeSignedDecimal
|
import com.racingapp.pitlog.model.sanitizeSignedDecimal
|
||||||
import com.racingapp.pitlog.model.setupDiffs
|
import com.racingapp.pitlog.model.setupDiffs
|
||||||
|
import com.racingapp.pitlog.model.weightSplit
|
||||||
import com.racingapp.pitlog.ui.FieldRow
|
import com.racingapp.pitlog.ui.FieldRow
|
||||||
import com.racingapp.pitlog.ui.NumberField
|
import com.racingapp.pitlog.ui.NumberField
|
||||||
import com.racingapp.pitlog.ui.PitActionButton
|
import com.racingapp.pitlog.ui.PitActionButton
|
||||||
@@ -64,6 +65,13 @@ private data class CarSetupForm(
|
|||||||
val blockRr: String = "",
|
val blockRr: String = "",
|
||||||
val toeFront: String = "",
|
val toeFront: String = "",
|
||||||
val toeRear: String = "",
|
val toeRear: String = "",
|
||||||
|
val weightLf: String = "",
|
||||||
|
val weightRf: String = "",
|
||||||
|
val weightLr: String = "",
|
||||||
|
val weightRr: String = "",
|
||||||
|
val rideHeight: String = "",
|
||||||
|
val pinion: String = "",
|
||||||
|
val brakeFront: String = "",
|
||||||
val note: String = "",
|
val note: String = "",
|
||||||
) {
|
) {
|
||||||
fun toSheet(id: Long, recordedAt: Long) = SetupSheet(
|
fun toSheet(id: Long, recordedAt: Long) = SetupSheet(
|
||||||
@@ -84,6 +92,13 @@ private data class CarSetupForm(
|
|||||||
blockRr = parseDecimal(blockRr),
|
blockRr = parseDecimal(blockRr),
|
||||||
toeFront = parseDecimal(toeFront),
|
toeFront = parseDecimal(toeFront),
|
||||||
toeRear = parseDecimal(toeRear),
|
toeRear = parseDecimal(toeRear),
|
||||||
|
weightLf = parseIntField(weightLf),
|
||||||
|
weightRf = parseIntField(weightRf),
|
||||||
|
weightLr = parseIntField(weightLr),
|
||||||
|
weightRr = parseIntField(weightRr),
|
||||||
|
rideHeight = parseDecimal(rideHeight),
|
||||||
|
pinion = parseDecimal(pinion),
|
||||||
|
brakeFront = parseDecimal(brakeFront),
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -102,6 +117,13 @@ private data class CarSetupForm(
|
|||||||
blockRr = decimal(sheet.blockRr, 1),
|
blockRr = decimal(sheet.blockRr, 1),
|
||||||
toeFront = decimal(sheet.toeFront, 3),
|
toeFront = decimal(sheet.toeFront, 3),
|
||||||
toeRear = decimal(sheet.toeRear, 3),
|
toeRear = decimal(sheet.toeRear, 3),
|
||||||
|
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),
|
||||||
|
pinion = decimal(sheet.pinion, 1),
|
||||||
|
brakeFront = decimal(sheet.brakeFront, 1),
|
||||||
note = if (keepNote) sheet.note else "",
|
note = if (keepNote) sheet.note else "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -335,6 +357,84 @@ fun CarSetupEditorScreen(
|
|||||||
SignedField("Front", form.toeFront, { value -> viewModel.update { it.copy(toeFront = value) } }, Modifier.weight(1f), 1, 3)
|
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("Rear", form.toeRear, { value -> viewModel.update { it.copy(toeRear = value) } }, Modifier.weight(1f), 1, 3)
|
||||||
}
|
}
|
||||||
|
SectionLabel("Corner weights, lb")
|
||||||
|
Text(
|
||||||
|
"Pounds on the pads. Left, rear, and cross are figured from these four.",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
FieldRow {
|
||||||
|
NumberField(
|
||||||
|
label = "LF",
|
||||||
|
value = form.weightLf,
|
||||||
|
onValue = { value -> viewModel.update { it.copy(weightLf = sanitizeInt(value, 4)) } },
|
||||||
|
decimal = false,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
NumberField(
|
||||||
|
label = "RF",
|
||||||
|
value = form.weightRf,
|
||||||
|
onValue = { value -> viewModel.update { it.copy(weightRf = sanitizeInt(value, 4)) } },
|
||||||
|
decimal = false,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FieldRow {
|
||||||
|
NumberField(
|
||||||
|
label = "LR",
|
||||||
|
value = form.weightLr,
|
||||||
|
onValue = { value -> viewModel.update { it.copy(weightLr = sanitizeInt(value, 4)) } },
|
||||||
|
decimal = false,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
NumberField(
|
||||||
|
label = "RR",
|
||||||
|
value = form.weightRr,
|
||||||
|
onValue = { value -> viewModel.update { it.copy(weightRr = sanitizeInt(value, 4)) } },
|
||||||
|
decimal = false,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
weightSplit(
|
||||||
|
form.weightLf.toIntOrNull(),
|
||||||
|
form.weightRf.toIntOrNull(),
|
||||||
|
form.weightLr.toIntOrNull(),
|
||||||
|
form.weightRr.toIntOrNull(),
|
||||||
|
)?.let { split ->
|
||||||
|
Text(split, modifier = Modifier.padding(top = 8.dp))
|
||||||
|
}
|
||||||
|
SectionLabel("Ride height")
|
||||||
|
Text(
|
||||||
|
"Inches from the lowest frame at the driver's door.",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
NumberField(
|
||||||
|
label = "Inches",
|
||||||
|
value = form.rideHeight,
|
||||||
|
onValue = { value -> viewModel.update { it.copy(rideHeight = sanitizeDecimal(value, 2, 1)) } },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
SectionLabel("Pinion")
|
||||||
|
Text(
|
||||||
|
"Degrees at ride height. Negative is nose-down.",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
SignedField(
|
||||||
|
"Degrees",
|
||||||
|
form.pinion,
|
||||||
|
{ value -> viewModel.update { it.copy(pinion = value) } },
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
SectionLabel("Front brake")
|
||||||
|
Text(
|
||||||
|
"Percent of the braking force on the front.",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
NumberField(
|
||||||
|
label = "Front %",
|
||||||
|
value = form.brakeFront,
|
||||||
|
onValue = { value -> viewModel.update { it.copy(brakeFront = sanitizeDecimal(value, 2, 1)) } },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
SectionLabel(if (loggingChange) "What changed" else "Notes")
|
SectionLabel(if (loggingChange) "What changed" else "Notes")
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = form.note,
|
value = form.note,
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class SyncMigrationTest {
|
|||||||
raw.close()
|
raw.close()
|
||||||
|
|
||||||
val database = Room.databaseBuilder(context, AppDatabase::class.java, name)
|
val database = Room.databaseBuilder(context, AppDatabase::class.java, name)
|
||||||
.addMigrations(MIGRATION_7_8, MIGRATION_8_9)
|
.addMigrations(MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10)
|
||||||
.build()
|
.build()
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val night = database.raceDao().nightById(1)
|
val night = database.raceDao().nightById(1)
|
||||||
|
|||||||
@@ -35,6 +35,45 @@ class SetupLogTest {
|
|||||||
assertTrue(setupDiffs(sheet, sheet.copy(note = "perch tweak")).isEmpty())
|
assertTrue(setupDiffs(sheet, sheet.copy(note = "perch tweak")).isEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deepSetupShowsWeightsHeightPinionAndBrake() {
|
||||||
|
val original = SetupSheet(
|
||||||
|
recordedAt = 1L,
|
||||||
|
weightLf = 800,
|
||||||
|
weightRf = 760,
|
||||||
|
weightLr = 740,
|
||||||
|
weightRr = 700,
|
||||||
|
rideHeight = 5.0,
|
||||||
|
pinion = -2.0,
|
||||||
|
brakeFront = 62.0,
|
||||||
|
)
|
||||||
|
val later = original.copy(
|
||||||
|
weightLf = 820,
|
||||||
|
rideHeight = 5.2,
|
||||||
|
pinion = -1.5,
|
||||||
|
brakeFront = 65.0,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
"Weights LF 800 RF 760 LR 740 RR 700 lb",
|
||||||
|
"From the weights Left 51.3% Rear 48% Cross 50%",
|
||||||
|
"Ride height 5 in",
|
||||||
|
"Pinion -2.0°",
|
||||||
|
"Front brake 62%",
|
||||||
|
),
|
||||||
|
setupLines(original),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
"LF weight 800 lb → 820 lb",
|
||||||
|
"Ride height 5 in → 5.2 in",
|
||||||
|
"Pinion -2.0° → -1.5°",
|
||||||
|
"Front brake 62% → 65%",
|
||||||
|
),
|
||||||
|
setupDiffs(original, later),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun originalLinesSkipBlankFields() {
|
fun originalLinesSkipBlankFields() {
|
||||||
val lines = setupLines(SetupSheet(recordedAt = 1L, crossPercent = 53.5, blockLr = 2.0))
|
val lines = setupLines(SetupSheet(recordedAt = 1L, crossPercent = 53.5, blockLr = 2.0))
|
||||||
|
|||||||
+48
-2
@@ -27,6 +27,8 @@ SHEET_FIELDS = [
|
|||||||
"recordedAt", "note", "crossPercent", "springLf", "springRf", "springLr", "springRr",
|
"recordedAt", "note", "crossPercent", "springLf", "springRf", "springLr", "springRr",
|
||||||
"camberLf", "camberRf", "casterLf", "casterRf", "swayBarHole", "blockLr", "blockRr",
|
"camberLf", "camberRf", "casterLf", "casterRf", "swayBarHole", "blockLr", "blockRr",
|
||||||
"toeFront", "toeRear",
|
"toeFront", "toeRear",
|
||||||
|
"weightLf", "weightRf", "weightLr", "weightRr",
|
||||||
|
"rideHeight", "pinion", "brakeFront",
|
||||||
]
|
]
|
||||||
|
|
||||||
_lock = threading.Lock()
|
_lock = threading.Lock()
|
||||||
@@ -89,10 +91,30 @@ def open_db(path):
|
|||||||
db.row_factory = sqlite3.Row
|
db.row_factory = sqlite3.Row
|
||||||
db.execute("PRAGMA journal_mode=WAL")
|
db.execute("PRAGMA journal_mode=WAL")
|
||||||
db.executescript(SCHEMA)
|
db.executescript(SCHEMA)
|
||||||
|
ensure_columns(
|
||||||
|
db,
|
||||||
|
"sheets",
|
||||||
|
{
|
||||||
|
"weightLf": "INTEGER",
|
||||||
|
"weightRf": "INTEGER",
|
||||||
|
"weightLr": "INTEGER",
|
||||||
|
"weightRr": "INTEGER",
|
||||||
|
"rideHeight": "REAL",
|
||||||
|
"pinion": "REAL",
|
||||||
|
"brakeFront": "REAL",
|
||||||
|
},
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
return db
|
return db
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_columns(db, table, columns):
|
||||||
|
present = {row["name"] for row in db.execute(f"PRAGMA table_info({table})")}
|
||||||
|
for name, kind in columns.items():
|
||||||
|
if name not in present:
|
||||||
|
db.execute(f"ALTER TABLE {table} ADD COLUMN {name} {kind}")
|
||||||
|
|
||||||
|
|
||||||
def space(bearer):
|
def space(bearer):
|
||||||
path = space_path(bearer)
|
path = space_path(bearer)
|
||||||
with _lock:
|
with _lock:
|
||||||
@@ -182,7 +204,14 @@ SCHEMA = """
|
|||||||
blockLr REAL,
|
blockLr REAL,
|
||||||
blockRr REAL,
|
blockRr REAL,
|
||||||
toeFront REAL,
|
toeFront REAL,
|
||||||
toeRear REAL
|
toeRear REAL,
|
||||||
|
weightLf INTEGER,
|
||||||
|
weightRf INTEGER,
|
||||||
|
weightLr INTEGER,
|
||||||
|
weightRr INTEGER,
|
||||||
|
rideHeight REAL,
|
||||||
|
pinion REAL,
|
||||||
|
brakeFront REAL
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -503,7 +532,22 @@ def selftest():
|
|||||||
"recordedAt": 11,
|
"recordedAt": 11,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sheets": [],
|
"sheets": [
|
||||||
|
{
|
||||||
|
"syncId": "sheet-a",
|
||||||
|
"updatedAt": 12,
|
||||||
|
"deleted": False,
|
||||||
|
"recordedAt": 12,
|
||||||
|
"note": "",
|
||||||
|
"weightLf": 800,
|
||||||
|
"weightRf": 760,
|
||||||
|
"weightLr": 740,
|
||||||
|
"weightRr": 700,
|
||||||
|
"rideHeight": 5.0,
|
||||||
|
"pinion": -2.0,
|
||||||
|
"brakeFront": 62.0,
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
second = sync_body({"rev": 0, "profile": {"carName": "1976 Chevy Camaro", "raceClass": "Hobby Stock", "discipline": "Oval", "updatedAt": 0}, "nights": [], "runs": [], "sheets": []})
|
second = sync_body({"rev": 0, "profile": {"carName": "1976 Chevy Camaro", "raceClass": "Hobby Stock", "discipline": "Oval", "updatedAt": 0}, "nights": [], "runs": [], "sheets": []})
|
||||||
@@ -511,6 +555,8 @@ def selftest():
|
|||||||
raise SystemExit("second phone did not receive the night")
|
raise SystemExit("second phone did not receive the night")
|
||||||
if second["runs"][0]["nightSyncId"] != "night-a":
|
if second["runs"][0]["nightSyncId"] != "night-a":
|
||||||
raise SystemExit("second phone did not receive the run")
|
raise SystemExit("second phone did not receive the run")
|
||||||
|
if second["sheets"][0]["weightLf"] != 800 or second["sheets"][0]["pinion"] != -2.0:
|
||||||
|
raise SystemExit("second phone did not receive the setup")
|
||||||
older = sync_body(
|
older = sync_body(
|
||||||
{
|
{
|
||||||
"rev": first["rev"],
|
"rev": first["rev"],
|
||||||
|
|||||||
Reference in new issue
Block a user