#include "shot_control.h" #include "diagnostic_queue.h" #include "heater_controller.h" #include "power_schedule.h" #include "program_memory.h" #include "serial_output.h" #if defined(MECOFFEE_RUST_SHOT) #include #include #include struct RustShotSettings {uint16_t enabled,closed,pump,pause,maximum;}; extern "C" { void mecoffee_rust_shot_event(meCoffee::ShotState*,uint8_t,uint32_t,uint32_t,void*); void mecoffee_rust_shot_active(meCoffee::ShotState*,const RustShotSettings*,uint8_t,uint32_t,uint32_t,void*); void mecoffee_rust_poll_input(meCoffee::DebouncedInput*,uint8_t,uint8_t,uint32_t,void*); uint16_t mecoffee_rust_shot_layout(uint8_t); } static_assert(sizeof(meCoffee::ShotState)==5,"shot state ABI"); static_assert(sizeof(meCoffee::DebouncedInput)==15,"input state ABI"); static_assert(offsetof(meCoffee::DebouncedInput,beginTimestamp)==3,"input begin ABI"); #endif namespace meCoffee { namespace { #if defined(MECOFFEE_RUST_TELEMETRY) extern "C" void mecoffee_rust_telemetry(void*,uint8_t); void reportShotBegin(FirmwareState& state) { mecoffee_rust_telemetry(&state,3); } void reportShotEnd(FirmwareState& state) { mecoffee_rust_telemetry(&state,4); } #else void reportShotBegin(FirmwareState& state) { if (state.alternateModeSelection != 0) { return; } writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("sht ")); writeUnsignedDecimalText(state.sensor.aggregateGeneration); writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT(" 0 OK")); writeCarriageReturnLineFeed(); } void reportShotEnd(FirmwareState& state) { if (state.alternateModeSelection != 0) { return; } writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT("sht ")); writeUnsignedDecimalText(state.sensor.aggregateGeneration); writeSpace(); writeUnsignedDecimalText(state.shot.lastShotDuration); writeProgramMemoryText(MECOFFEE_PROGRAM_TEXT(" OK")); writeCarriageReturnLineFeed(); } #endif void beginShotEvent(FirmwareState* state, uint32_t timestamp) { #if defined(MECOFFEE_RUST_SHOT) mecoffee_rust_shot_event(&state->shot,0,timestamp,0,state); #else // Observed in the original: a shot begin counts as user activity for the inactivity timer. state->powerSchedule.lastActivityTimestamp = timestamp; state->shot.isTimeoutEventLatched = false; beginShotFeedForward(state, timestamp); enqueueDiagnosticCallback(&state->diagnosticQueue, reportShotBegin); #endif } void endShotEvent(FirmwareState* state, uint32_t endTimestamp, uint32_t beginTimestamp) { #if defined(MECOFFEE_RUST_SHOT) mecoffee_rust_shot_event(&state->shot,1,endTimestamp,beginTimestamp,state); #else setGroupEnabled(&groupForRole(&state->outputChannels, OutputGroupRole::Pump), false); setGroupEnabled(&groupForRole(&state->outputChannels, OutputGroupRole::Valve), false); state->shot.lastShotDuration = endTimestamp - beginTimestamp; endShotFeedForward(state, endTimestamp); enqueueDiagnosticCallback(&state->diagnosticQueue, reportShotEnd); #endif } void runShotActiveCallback(FirmwareState* state, uint32_t foregroundSnapshotMilliseconds, uint32_t beginTimestamp) { #if defined(MECOFFEE_RUST_SHOT) const auto& s=state->settings; const RustShotSettings settings={s.preinfusionEnabled,s.preinfusionValveClosed,s.preinfusionPumpDuration,s.preinfusionPauseDuration,s.maximumShotDuration}; mecoffee_rust_shot_active(&state->shot,&settings,state->alternateModeSelection,foregroundSnapshotMilliseconds,beginTimestamp,state); #else const uint32_t elapsed = foregroundSnapshotMilliseconds - beginTimestamp; const ShotOutputRequest request = evaluateShotOutputRequest(state->settings, state->alternateModeSelection, elapsed); OutputGroup& pump = groupForRole(&state->outputChannels, OutputGroupRole::Pump); OutputGroup& valve = groupForRole(&state->outputChannels, OutputGroupRole::Valve); if (request.isTimedOut) { setGroupEnabled(&pump, false); setGroupEnabled(&valve, false); if (!state->shot.isTimeoutEventLatched) { state->shot.isTimeoutEventLatched = true; endShotEvent(state, foregroundSnapshotMilliseconds, beginTimestamp); } return; } state->shot.isTimeoutEventLatched = false; setGroupEnabled(&pump, request.isPumpRequested); setGroupEnabled(&valve, request.isValveRequested); #endif } // Begin (argument 1) and end (argument 0) of an input burst. void runInputStateChange(FirmwareState* state, InputRole role, uint8_t argument, uint32_t timestamp, uint32_t beginTimestamp) { switch (role) { case InputRole::Power: handlePowerInputChange(state, argument, timestamp); break; case InputRole::Mode: handleModeInputChange(state, argument, timestamp); break; case InputRole::Shot: if (argument != 0) { beginShotEvent(state, timestamp); } else { endShotEvent(state, timestamp, beginTimestamp); } break; } } } // namespace bool isInputQualified(const DebouncedInput& input) { return input.risingEdgeCount >= kRequiredEdgesPerLevel && input.fallingEdgeCount >= kRequiredEdgesPerLevel; } void initializeDebouncedInput(DebouncedInput* input) { #if defined(MECOFFEE_RUST_SHOT) if(mecoffee_rust_shot_layout(0)!=5 || mecoffee_rust_shot_layout(1)!=15 || mecoffee_rust_shot_layout(2)!=10 || mecoffee_rust_shot_layout(3)!=3) {cli();for(;;){}} #endif *input = DebouncedInput{}; input->isLastLevelHigh = true; } ShotOutputRequest evaluateShotOutputRequest(const SettingsValues& settings, uint8_t alternateMode, uint32_t elapsedMilliseconds) { const uint16_t gapEnd = static_cast(settings.preinfusionPumpDuration + settings.preinfusionPauseDuration); const bool isInGap = settings.preinfusionEnabled != 0 && elapsedMilliseconds > settings.preinfusionPumpDuration && elapsedMilliseconds < gapEnd; const uint16_t timeoutThreshold = static_cast(static_cast(settings.maximumShotDuration) * 1000u); ShotOutputRequest request; request.isTimedOut = settings.maximumShotDuration != 0 && elapsedMilliseconds > timeoutThreshold; request.isPumpRequested = !isInGap && !request.isTimedOut; request.isValveRequested = !request.isTimedOut && alternateMode == 0 && (!isInGap || settings.preinfusionValveClosed == 0); return request; } void pollDebouncedInput(FirmwareState* state, DebouncedInput* input, InputRole role, bool isPinHigh, uint32_t foregroundSnapshotMilliseconds) { #if defined(MECOFFEE_RUST_SHOT) mecoffee_rust_poll_input(input,static_cast(role),isPinHigh,foregroundSnapshotMilliseconds,state); #else if (isPinHigh != input->isLastLevelHigh) { input->isLastLevelHigh = isPinHigh; input->lastChangeTimestamp = foregroundSnapshotMilliseconds; uint8_t& edgeCount = isPinHigh ? input->risingEdgeCount : input->fallingEdgeCount; if (edgeCount < 255) { ++edgeCount; } if (!isInputQualified(*input)) { return; } // The begin guard and the active callback run in the same call, so the first qualified // falling edge both begins the burst and requests the outputs. if (!isPinHigh && input->beginTimestamp == 0) { input->beginTimestamp = foregroundSnapshotMilliseconds; runInputStateChange(state, role, 1, foregroundSnapshotMilliseconds, 0); } if (role == InputRole::Shot) { runShotActiveCallback(state, foregroundSnapshotMilliseconds, input->beginTimestamp); } return; } if (input->lastChangeTimestamp != 0 && static_cast(foregroundSnapshotMilliseconds - input->lastChangeTimestamp) >= kInputStableDuration) { const bool wasQualified = isInputQualified(*input); const uint32_t beginTimestamp = input->beginTimestamp; input->risingEdgeCount = 0; input->fallingEdgeCount = 0; input->beginTimestamp = 0; input->lastChangeTimestamp = 0; if (wasQualified) { input->endTimestamp = foregroundSnapshotMilliseconds; runInputStateChange(state, role, 0, foregroundSnapshotMilliseconds, beginTimestamp); } } #endif } #if defined(MECOFFEE_RUST_SHOT) extern "C" void mecoffee_shot_effect(void* context,uint8_t operation,uint32_t value) { auto* state=static_cast(context); switch(operation) { case 0: state->powerSchedule.lastActivityTimestamp=value;break; case 1: { const uint8_t saved=SREG;cli(); setGroupEnabled(&groupForRole(&state->outputChannels,static_cast(value&255)),(value&256)!=0); SREG=saved;break; } case 2: beginShotFeedForward(state,value);break; case 3: endShotFeedForward(state,value);break; case 4: enqueueDiagnosticCallback(&state->diagnosticQueue,reportShotBegin);break; case 5: enqueueDiagnosticCallback(&state->diagnosticQueue,reportShotEnd);break; } } extern "C" void mecoffee_input_event(void* context,uint8_t role,uint8_t active,uint32_t now,uint32_t begin) { auto* state=static_cast(context); // Use the supplied timestamp, never read the Rust-borrowed DebouncedInput. if(active==2) runShotActiveCallback(state,now,begin); else runInputStateChange(state,static_cast(role),active,now,begin); } #endif } // namespace meCoffee