Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,18 @@ ESP32, `esp32-s3-*` for the S3). Flashing S3 firmware onto a classic ESP32

| Stage | Theme | Status |
| ----- | ----- | ------ |
| **1** | Foundation + Tier 1 (Oster Xpert temp control) | in progress |
| **2** | BLE GATT spec + Android app MVP + Tier 2 | planned |
| **3** | Pressure & flow profiling + Tier 3 | planned |
| **1** | Foundation + Tier 1 (Oster Xpert temp control) | working on the bench machine |
| **2** | BLE GATT spec + Android app MVP + Tier 2 | working on the bench machine |
| **3** | Pressure & flow profiling + Tier 3 | in progress |
| **4** | Scales, OTA, profile sharing, multi-machine, docs site + Tier 4 | planned |

Stage 3 currently runs a Standard AC phase-dimmer on the pump, executes the
bundled pressure/flow profiles (transcribed from gaggimate, gaggiuino and
Decent), and can hold a fixed operator-set pressure for a whole shot. Duty is
mapped to pressure through a measured pump curve; see
[`pump-dimmer-calibration.md`](hardware/oster-xpert/pump-dimmer-calibration.md)
for the sweep behind it.

The detailed roadmap lives in [`docs/roadmap.md`](docs/roadmap.md).

## Repository layout
Expand Down
386 changes: 386 additions & 0 deletions app-android/app/src/main/java/com/esp32esso/tier1/BrewProfiles.kt

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions app-android/app/src/main/java/com/esp32esso/tier1/DemoData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ object DemoData {
machineState = machine,
settings = settings,
preinfusionSec = 2f,
pumpRampSec = 3f,
history = idleHistory,
)

Expand Down Expand Up @@ -134,6 +135,8 @@ object DemoData {
avgGroupC = if (groupTemps.isEmpty()) Float.NaN else groupTemps.average().toFloat(),
finalGroupC = groupTemps.lastOrNull() ?: Float.NaN,
peakGroupC = groupTemps.maxOrNull() ?: Float.NaN,
thermoblockOffsetC = machine.thermoblockOffsetC,
thermoblockManual = machine.thermoblockManual,
samples = shotSamples,
shotStartUptimeMs = shotStart,
shotEndUptimeMs = shotEnd,
Expand Down
29 changes: 24 additions & 5 deletions app-android/app/src/main/java/com/esp32esso/tier1/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ class MainActivity : ComponentActivity() {
val uiState by viewModel.uiState.collectAsState()
val displayShotTimeMs by viewModel.displayShotTimeMs.collectAsState()
val pendingReport by viewModel.pendingReport.collectAsState()
val lastReport by viewModel.lastReport.collectAsState()
val lastReportShotId by viewModel.lastReportShotId.collectAsState()
val demoMode by viewModel.demoMode.collectAsState()
val selectedProfileId by viewModel.selectedProfileId.collectAsState()
val selectedProfile = BrewProfileLibrary.byId(selectedProfileId)
val beans by viewModel.beans.collectAsState()
val grinders by viewModel.grinders.collectAsState()
val waters by viewModel.waters.collectAsState()
Expand Down Expand Up @@ -101,9 +105,6 @@ class MainActivity : ComponentActivity() {
)
Screen.Ranking -> RankingScreen(
shots = shots,
beans = beans,
grinders = grinders,
onDelete = viewModel::deleteShot,
onOpen = { shot ->
// Ranking taps reopen the report in read-only mode
// with the persisted shot as the source of truth,
Expand All @@ -114,6 +115,17 @@ class MainActivity : ComponentActivity() {
reportReadOnly = true
screen = Screen.ShotReport
},
// Only offered while the last brew hasn't been saved
// yet (see MainViewModel.lastReportShotId) -- once it
// has, it already has its own card in the list above.
onOpenLastReport = lastReport?.takeIf { lastReportShotId == null }?.let { last ->
{
report = last
currentShotId = null
reportReadOnly = false
screen = Screen.ShotReport
}
},
onBack = { screen = Screen.Main },
)
Screen.ShotReport -> {
Expand All @@ -136,6 +148,7 @@ class MainActivity : ComponentActivity() {
if (!reportReadOnly && currentShotId == null) {
viewModel.autoSaveShot(current) { id ->
currentShotId = id
viewModel.markLastReportSaved(id)
}
}
},
Expand Down Expand Up @@ -168,11 +181,13 @@ class MainActivity : ComponentActivity() {
onConnect = { requestPermissionsAndConnect() },
onDisconnect = viewModel::disconnect,
onSetpointChanged = viewModel::applySetpoint,
onOffsetChanged = viewModel::applyThermoblockOffset,
onThermoblockSetpointChanged = viewModel::applyThermoblockSetpoint,
onPressureSetpointChanged = viewModel::applyPressureSetpoint,
onBrewToggle = viewModel::setBrewing,
onProfileBrewToggle = viewModel::setProfileBrewing,
onHeaterToggle = viewModel::setHeaterEnabled,
onPreinfusionChanged = viewModel::applyPreinfusion,
onPumpRampChanged = viewModel::applyPumpRamp,
onFlush = viewModel::triggerFlush,
onOpenSettings = { screen = Screen.Settings },
onOpenLibrary = { screen = Screen.Library },
Expand All @@ -181,6 +196,8 @@ class MainActivity : ComponentActivity() {
onSecretShowReport = viewModel::injectDemoReport,
demoMode = demoMode,
displayShotTimeMs = displayShotTimeMs,
selectedProfile = selectedProfile,
onProfileSelected = viewModel::applyBrewProfile,
)
}

Expand Down Expand Up @@ -261,6 +278,8 @@ private fun shotToReport(shot: ShotEntity): ShotReport {
avgGroupC = shot.avgGroupC ?: Float.NaN,
finalGroupC = shot.finalGroupC,
peakGroupC = shot.peakGroupC,
thermoblockOffsetC = shot.thermoblockOffsetC ?: Float.NaN,
thermoblockManual = shot.thermoblockManual,
samples = samples,
shotStartUptimeMs = startUptime,
shotEndUptimeMs = endUptime,
Expand All @@ -271,7 +290,7 @@ private fun shotToReport(shot: ShotEntity): ShotReport {
private fun ShotReportPrompt(onShow: () -> Unit, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Shot finished") },
title = { Text("Brew finished") },
text = { Text("Show the brew report?") },
confirmButton = { TextButton(onClick = onShow) { Text("Show brew report") } },
dismissButton = { TextButton(onClick = onDismiss) { Text("Not now") } },
Expand Down
Loading
Loading