diff --git a/de1plus/bluetooth.tcl b/de1plus/bluetooth.tcl index 0eebffc2..79fcb95a 100755 --- a/de1plus/bluetooth.tcl +++ b/de1plus/bluetooth.tcl @@ -2363,6 +2363,23 @@ proc later_new_de1_connection_setup {} { #set_feature_flags 1 set_heater_tweaks + # Push the machine's autonomous inactivity-sleep timeout so it self-sleeps + # even when the tablet is off/disconnected. Reuses the tablet's existing + # "return to sleep after" (screen_saver_delay) value, in minutes. + set_sleep_timeout_minutes $::settings(screen_saver_delay) + + # Phase 2: sync the firmware wall-clock (and re-arm periodic re-sync) and push + # the weekly wake schedule, so the machine wakes/keeps-warm on schedule and + # self-sleeps off-schedule even with no tablet connected. + machine_clock_resync + set_wake_schedule + + # Cup-warmer pre-warm is firmware-side and flash-persisted; push the app's + # setting so a change made while disconnected reaches the machine. + set_cupwarmer_preheat [ifexists ::settings(cupwarmer_prewarm_enable) 0] \ + [ifexists ::settings(cupwarmer_prewarm_minutes) 30] + get_cupwarmer_status + get_refill_kit_present get_sn @@ -2781,7 +2798,15 @@ proc de1_ble_handler { event data } { if {$cuuid eq $::de1(cuuid_0D)} { set ::de1(last_ping) [clock seconds] - ::de1::state::update::from_shotvalue $value $event_time + # Additive BLE: on a Bengle the high-resolution superset + # (BengleShotSample, 0xA013) is the sole shot-sample source + # and drives the chart. The stock 0xA00D still streams (so a + # stock DE1 app works), but we must NOT also chart it here or + # the chart would be double-sampled at 2x with broken + # intersample timing. + if {![::de1::packet::use_ble_v2]} { + ::de1::state::update::from_shotvalue $value $event_time + } set do_this 0 if {$do_this == 1} { # this tries to handle bad write situations, but it might have side effects if it is not working correctly. @@ -2793,6 +2818,22 @@ proc de1_ble_handler { event data } { return } } + } elseif {$cuuid eq $::de1(cuuid_13)} { + # Additive BLE: BengleShotSample (0xA013) -- the Bengle + # self-contained high-resolution superset. On a Bengle this + # is the SOLE shot-sample source: it drives the pressure/flow/ + # temp chart AND bridges integrated-scale weight/flow + milk + # into the scale pipeline (chart / SAW / display). All of that + # lives in _apply_shotvalue, shared with the stock v1 path. + set ::de1(last_ping) [clock seconds] + # Receiving 0xA013 is definitive proof this is a Bengle, so + # pin the protocol to v2 now. This gates off the stock 0xA00D + # charting path (use_ble_v2) from the very first sample, + # closing the connect-time window -- before the model MMR read + # lands -- where both 0xA00D and 0xA013 would otherwise chart. + # set_ble_protocol_version short-circuits once already at 2. + ::de1::packet::set_ble_protocol_version 2 + ::de1::state::update::from_bengleshotvalue $value $event_time } elseif {$cuuid eq $::de1(cuuid_05)} { # MMR read ::bt::msg -INFO "MMR read: [::logging::format_mmr $value]" @@ -2925,6 +2966,17 @@ proc de1_ble_handler { event data } { } elseif {$mmr_id == "80385C"} { ::bt::msg -INFO "MMR read: steam_highflow_start: '$mmr_val'" set ::de1(refill_kit_detected) $mmr_val + } elseif {$mmr_id == "803884"} { + # ScaleCalState — packed U32: Step(31:24) DetectedCell(23:20) + # SubState(19:16) SecondsRemaining(15:8) Status(7:0) + set raw [ifexists arr2(Data0)] + ::bt::msg -INFO "MMR read: ScaleCalState raw=$raw" + ::scale_cal::on_state_read $raw + } elseif {$mmr_id == "803888"} { + # ScaleCalWeight read-back (grams * 10) + set raw [ifexists arr2(Data0)] + ::bt::msg -INFO "MMR read: ScaleCalWeight raw=$raw" + set ::scale_cal::cal_weight_readback $raw } else { ::bt::msg -ERROR "Uknown type of direct MMR read" \ [::logging::format_mmr $value] diff --git a/de1plus/de1_comms.tcl b/de1plus/de1_comms.tcl index c4fda87e..50079f9a 100644 --- a/de1plus/de1_comms.tcl +++ b/de1plus/de1_comms.tcl @@ -2,6 +2,7 @@ package provide de1_comms 1.1 package require de1_bluetooth package require de1_logging 1.2 +package require de1_scale_calibration 1.0 package require lambda ### Globals @@ -284,6 +285,7 @@ proc de1_connect_handler { handle address name} { set dothis 1 if {$dothis == 1} { de1_enable_temp_notifications + de1_enable_bengleshotsample_notifications ;# Additive: 0xA013 (no-op on a stock DE1) if {[info exists ::de1(first_connection_was_made)] != 1} { # on app startup, wake the machine up @@ -375,6 +377,21 @@ proc de1_event_handler { command_name value {update_received 0}} { # dupe copy, of what we receive via firmware so we can NOT let them change it if we did receive it via BLE set ::de1(sn) $sn + } elseif {$mmr_id == "8038AC"} { + # CupWarmerMode (0=Off, 1=On). RAM only on FW side. + ::comms::msg -INFO "MMRead: cupwarmer_mode: '$mmr_val'" + set ::de1(cupwarmer_mode) $mmr_val + + } elseif {$mmr_id == "8038B4"} { + # MatHeaterDrivePct 0-100. Shown on the cup warmer page as "Heating - N%". + set ::de1(mat_heater_drive) $mmr_val + + } elseif {$mmr_id == "8038B8"} { + # MatTempFault 0=OK, 1=OpenOrShort, 2=Runaway. Shown on the cup + # warmer page as the NTC-disconnected warning. + ::comms::msg -INFO "MMRead: mat_temp_fault: '$mmr_val'" + set ::de1(mat_temp_fault) $mmr_val + } elseif {$mmr_id == "80385C"} { ::comms::msg -NOTICE "MMRead: get_refill_kit_present: '$mmr_val'" @@ -475,6 +492,18 @@ proc de1_event_handler { command_name value {update_received 0}} { ::comms::msg -INFO "MMRead: cupwarmer_temp: '$mmr_val'" set ::de1(cupwarmer_temp) $mmr_val + } elseif {$mmr_id == "803884"} { + # ScaleCalState — packed U32: Step(31:24) DetectedCell(23:20) + # SubState(19:16) SecondsRemaining(15:8) Status(7:0) + set raw [ifexists arr2(Data0)] + ::comms::msg -INFO "MMRead: ScaleCalState raw=$raw" + ::scale_cal::on_state_read $raw + + } elseif {$mmr_id == "803888"} { + # ScaleCalWeight read-back (value * 10 = grams * 10) + set raw [ifexists arr2(Data0)] + ::comms::msg -INFO "MMRead: ScaleCalWeight raw=$raw" + set ::scale_cal::cal_weight_readback $raw } else { ::comms::msg -INFO "MMR read (undecoded): '[::logging::format_mmr $value]'" } @@ -756,6 +785,28 @@ proc de1_enable_temp_notifications {} { userdata_append "enable de1 temp notifications" [list de1_comm enable "ShotSample"] 1 } +# Additive BLE: subscribe to the Bengle high-resolution shot sample +# characteristic (BengleShotSample, 0xA013). Only a Bengle exposes it; a stock +# DE1 does not, so we must NOT enqueue the enable there. The enable is vital, +# and on a stock DE1 de1_ble would throw on the unset ::cinstance(0xA013); the +# vital-retry path then re-runs it every 500ms WITHOUT advancing the FIFO, +# permanently stalling the whole BLE command queue (blocking the version/state +# reads, profile/MMR writes, etc.). Gate on the discovered characteristic +# instance -- discovery populates ::cinstance for present characteristics before +# this connect-time enable runs (the temp/state enables rely on the same). +proc de1_enable_bengleshotsample_notifications {} { + ::comms::msg -NOTICE de1_enable_bengleshotsample_notifications + if {[ifexists ::sinstance($::de1(suuid))] == ""} { + ::comms::msg -DEBUG "DE1 not connected, cannot enable BengleShotSample notifications" + return + } + if {![info exists ::cinstance($::de1(cuuid_13))]} { + ::comms::msg -DEBUG "BengleShotSample (0xA013) not present on this machine (stock DE1); skipping enable" + return + } + userdata_append "enable de1 bengleshotsample notifications" [list de1_comm enable "BengleShotSample"] 1 +} + # status changes proc de1_enable_state_notifications {} { ::comms::msg -NOTICE de1_enable_state_notifications @@ -1109,6 +1160,35 @@ proc mmr_write { note address length value} { userdata_append "$note" [list de1_comm write "WriteToMMR" $data] 1 } +# Bengle-only: target weight at which the machine auto-ends the shot using +# its integrated scale. 0 disables. Value persists to disk in the firmware, +# so we always write on shot start to keep it aligned with the app setting. +proc set_end_of_shot_weight {weight_grams} { + if {![::de1::packet::use_ble_v2]} { return } + + if {$weight_grams eq "" || ![string is double -strict $weight_grams] || $weight_grams < 0} { + set weight_grams 0 + } + # MMR value = grams * 100; firmware clamps to 0..1_000_000 (10 kg). + set scaled [expr {int(round($weight_grams * 100))}] + if {$scaled < 0} { set scaled 0 } + if {$scaled > 1000000} { set scaled 1000000 } + + ::comms::msg -NOTICE "set_end_of_shot_weight '${weight_grams}g' (raw=${scaled})" + remove_matching_ble_queue_entries {^MMR set_end_of_shot_weight} + mmr_write "set_end_of_shot_weight ${weight_grams}g" "803864" "04" [long32_to_little_endian_hex $scaled] +} + +# Bengle integrated-scale instant tare (ScaleTare, 0x0080388C). Lives here +# rather than in the calibration wizard so the ordinary scale tare path can +# reach it without loading the wizard. Matches tareIntegratedScale in +# decentespresso/decaid, which writes the same value to the same register. +proc set_bengle_scale_tare {} { + if {![::de1::packet::use_ble_v2]} { return } + ::comms::msg -NOTICE set_bengle_scale_tare + mmr_write "ScaleTare" "80388C" "04" [long32_to_little_endian_hex 1] +} + proc set_tank_temperature_threshold {temp} { ::comms::msg -NOTICE set_tank_temperature_threshold "'$temp'" @@ -1244,6 +1324,177 @@ proc set_flush_flow_rate {rate} { mmr_write "set_flush_flow_rate" "803840" "04" [zero_pad [long_to_little_endian_hex [expr {int(10 * $rate)}] ] 2] } +# Cup warmer enable: 0=Off, 1=On. Not persisted on firmware side — must be +# (re)sent on every BLE reconnect and whenever the user toggles it. +proc set_cupwarmer_mode {mode} { + if {[is_bengle_model] != 1} { + return + } + set mode [expr {$mode ? 1 : 0}] + ::comms::msg -NOTICE set_cupwarmer_mode "'$mode'" + remove_matching_ble_queue_entries {^MMR set_cupwarmer_mode} + mmr_write "set_cupwarmer_mode" "8038AC" "04" [zero_pad [long_to_little_endian_hex $mode] 2] +} + +# Read the cup warmer's live status. Nothing polled these before, so the +# warmer page's heater percentage and NTC-fault warning could never update. +# Called on connect and whenever the cup warmer page is opened. +proc get_cupwarmer_status {} { + if {[is_bengle_model] != 1} { + return + } + remove_matching_ble_queue_entries {^MMR get_cupwarmer_status} + mmr_read "get_cupwarmer_status drive" "8038B4" "00" + mmr_read "get_cupwarmer_status fault" "8038B8" "00" +} + +# Cup-warmer pre-warm. The FIRMWARE owns the timing: with MatPreheatEnable set +# it starts the mat MatPreheatLeadMin minutes before a scheduled wake, and it +# does so with no tablet connected. Both registers are flash-persisted, so this +# only needs sending on connect and when the user changes the setting. +# +# Write order matters and matches decentespresso/decaid: when enabling, send +# the lead first so the firmware never acts on a stale one; when disabling, +# clear the enable first. +proc set_cupwarmer_preheat {enabled lead_minutes} { + if {[is_bengle_model] != 1} { + return + } + set lead [expr {int(round($lead_minutes))}] + if {$lead < 0} { set lead 0 } + if {$lead > 120} { set lead 120 } + set on [expr {$enabled ? 1 : 0}] + ::comms::msg -NOTICE set_cupwarmer_preheat "enabled=$on lead=$lead min" + remove_matching_ble_queue_entries {^MMR set_cupwarmer_preheat} + if {$on} { + mmr_write "set_cupwarmer_preheat lead" "8038D4" "04" [zero_pad [long_to_little_endian_hex $lead] 2] + mmr_write "set_cupwarmer_preheat on" "8038D0" "04" [zero_pad [long_to_little_endian_hex 1] 2] + } else { + mmr_write "set_cupwarmer_preheat off" "8038D0" "04" [zero_pad [long_to_little_endian_hex 0] 2] + mmr_write "set_cupwarmer_preheat lead" "8038D4" "04" [zero_pad [long_to_little_endian_hex $lead] 2] + } +} + +# Send the machine's autonomous inactivity-sleep timeout (whole minutes). +# The firmware self-sleeps after this long idle WHEN NO TABLET IS CONNECTED, so +# the machine sleeps even if the tablet is off/disconnected. We reuse the tablet's +# existing screen_saver_delay value (already in minutes) as the source. 0 = the +# firmware never auto-sleeps. Firmware default (when never set) is 60. +proc set_sleep_timeout_minutes {minutes} { + if {[is_bengle_model] != 1} { + return + } + set raw [expr {int(round($minutes))}] + if {$raw < 0} { set raw 0 } + if {$raw > 240} { set raw 240 } + ::comms::msg -NOTICE set_sleep_timeout_minutes "'$minutes' (raw $raw)" + remove_matching_ble_queue_entries {^MMR set_sleep_timeout_minutes} + mmr_write "set_sleep_timeout_minutes" "8038BC" "04" [zero_pad [long_to_little_endian_hex $raw] 2] +} + +# --------------------------------------------------------------------------- +# Phase 2: tablet-synced firmware clock + weekly wake schedule. +# +# The firmware keeps its own software wall-clock and weekly wake schedule so it +# can wake / keep warm on schedule and self-sleep off-schedule even with NO +# tablet connected. The tablet is just the source: it pushes the current local +# time (re-synced periodically) and the schedule (on connect and on edit). There +# is no battery-backed RTC, so the firmware clock is lost on a full power cut and +# re-synced the moment a tablet reconnects; until then only the Phase-1 +# inactivity timer runs. dow convention 0 = Sunday matches Tcl %w and the firmware. +# --------------------------------------------------------------------------- + +# Push the firmware's local wall-clock as seconds-since-Sunday-00:00:00 (local). +proc set_machine_clock {} { + if {[is_bengle_model] != 1} { + return + } + set now [clock seconds] + set w [scan [clock format $now -format %w] %d] ;# 0=Sun .. 6=Sat + set h [scan [clock format $now -format %H] %d] + set m [scan [clock format $now -format %M] %d] + set s [scan [clock format $now -format %S] %d] + set sow [expr {($w * 86400) + ($h * 3600) + ($m * 60) + $s}] + ::comms::msg -NOTICE set_machine_clock "sec-of-week=$sow" + remove_matching_ble_queue_entries {^set_machine_clock} + mmr_write "set_machine_clock" "8038C0" "04" [long32_to_little_endian_hex $sow] +} + +# Push the current clock now, and re-arm a periodic re-sync so the firmware clock +# does not drift while a tablet stays connected for a long time. +proc machine_clock_resync {} { + if {[info exists ::machine_clock_resync_handle]} { + after cancel $::machine_clock_resync_handle + } + set_machine_clock + set ::machine_clock_resync_handle [after [expr {30 * 60 * 1000}] machine_clock_resync] +} + +proc dow_from_dayname {day} { + switch -- $day { + Sun {return 0} + Mon {return 1} + Tue {return 2} + Wed {return 3} + Thu {return 4} + Fri {return 5} + Sat {return 6} + } + return -1 +} + +# Build the firmware's weekly wake schedule from the app's scheduler settings and +# push it (clear -> entries -> enable). Each firmware window is packed as +# (dow<<22)|(startMin<<11)|endMin, minutes after local midnight, endMin exclusive. +proc set_wake_schedule {} { + if {[is_bengle_model] != 1} { + return + } + remove_matching_ble_queue_entries {^sched_} + # Clear the firmware table + disable while we (re)load it. + mmr_write "sched_clear" "8038C8" "04" [long32_to_little_endian_hex 0] + + set count 0 + + # D_Scheduler per-weekday WAKE times -> short wake windows [t, t+1): the + # firmware wakes at t, then the Phase-1 inactivity timer sleeps it. + foreach day {Sun Mon Tue Wed Thu Fri Sat} { + set dow [dow_from_dayname $day] + if {[info exists ::D_scheduler_minutes($day)]} { + foreach t $::D_scheduler_minutes($day) { + if {$count >= 32} { break } + set t [scan $t %d] + if {$t eq "" || $t < 0 || $t > 1439} { continue } + set endm [expr {$t + 1}] + set packed [expr {($dow << 22) | ($t << 11) | $endm}] + mmr_write "sched_entry" "8038C4" "04" [long32_to_little_endian_hex $packed] + incr count + } + } + } + + # Built-in scheduler keep-warm window [wake,sleep) (seconds-since-midnight), + # same every day, only when enabled. The D_Scheduler plugin disables + # scheduler_enable, so in practice these two sources do not double up. + if {[info exists ::settings(scheduler_enable)] && $::settings(scheduler_enable) == 1} { + set ws [expr {int($::settings(scheduler_wake) / 60)}] + set se [expr {int($::settings(scheduler_sleep) / 60)}] + if {$ws >= 0 && $ws < $se && $se <= 1440} { + foreach dow {0 1 2 3 4 5 6} { + if {$count >= 32} { break } + set packed [expr {($dow << 22) | ($ws << 11) | $se}] + mmr_write "sched_entry" "8038C4" "04" [long32_to_little_endian_hex $packed] + incr count + } + } + } + + if {$count > 0} { + mmr_write "sched_enable" "8038C8" "04" [long32_to_little_endian_hex 1] + } + ::comms::msg -NOTICE set_wake_schedule "pushed $count window(s)" +} + proc set_flush_timeout {seconds} { ::comms::msg -NOTICE set_flush_timeout "'$seconds'" remove_matching_ble_queue_entries {^MMR set_flush_timeout} @@ -1266,6 +1517,21 @@ proc set_steam_flow {desired_flow} { mmr_write "set_steam_flow" "803828" "04" [zero_pad [int_to_hex $desired_flow] 2] } +# Milk auto-stop target in C (stored on firmware as C*10). 0 disables. +# Gated and clamped to match the reaprime/decaid contract for this register +# (TargetMilkTemp 0x008038A8, raw 0..850). +proc set_target_milk_temp {temp_c} { + if {[is_bengle_model] != 1} { + return + } + set raw [expr {int(round($temp_c * 10))}] + if {$raw < 0} { set raw 0 } + if {$raw > 850} { set raw 850 } + ::comms::msg -NOTICE set_target_milk_temp "'$temp_c' (raw $raw)" + remove_matching_ble_queue_entries {^MMR set_target_milk_temp} + mmr_write "set_target_milk_temp" "8038A8" "04" [zero_pad [long_to_little_endian_hex $raw] 2] +} + proc send_refill_kit_override {} { if {$::settings(refill_kit_override) == 0} { set_refill_kit_present 0 @@ -1533,6 +1799,18 @@ proc de1_send_shot_frames { {override {}} } { set_tank_temperature_threshold 0 } + # Bengle integrated-scale stop-at-weight. Each profile carries its own + # final_desired_shot_weight[_advanced]; push it to MMR 0x00803864 every + # time we upload frames so the machine-side target stays aligned with + # the profile currently on the app. Writing 0 when the profile has no + # target clears any value left persisted on disk by a prior profile. + if {$::settings(settings_profile_type) == "settings_2c"} { + set _saw_target [ifexists ::settings(final_desired_shot_weight_advanced) 0] + } else { + set _saw_target [ifexists ::settings(final_desired_shot_weight) 0] + } + set_end_of_shot_weight $_saw_target + userdata_append "Confirm that all shot frames were correctly sent" [list confirm_de1_send_shot_frames_worked [lindex $parts 1]] 1 return } @@ -1644,6 +1922,18 @@ proc de1_send_steam_hotwater_settings { {temporarily_disable_steam 0} } { # only works on Bengle set_cupwarmer_temperature $::settings(cupwarmer_temp) + + # Milk temp auto-stop: write the target, or 0 to disable. + if {[ifexists ::settings(steam_stop_mode)] eq "temp"} { + set_target_milk_temp $::settings(target_milk_temp) + } else { + set_target_milk_temp 0 + } + # CupWarmerMode is NOT persisted on the firmware side; must (re)send. On + # every boot and reconnect the app sends 0 here -- the mode only goes to 1 + # after the user taps the toggle, or when the pre-warm scheduler fires. + # This is intentional: no auto-heat after a blackout. + set_cupwarmer_mode [ifexists ::settings(cupwarmer_enable) 0] } diff --git a/de1plus/de1_de1.tcl b/de1plus/de1_de1.tcl index 37dbf12b..7047f8eb 100644 --- a/de1plus/de1_de1.tcl +++ b/de1plus/de1_de1.tcl @@ -520,27 +520,64 @@ namespace eval ::de1::state::update { proc from_shotvalue {packed {update_received 0}} { + # Stock DE1 v1 ShotSample (0xA00D). On a Bengle (model 128) the chart is + # driven by from_bengleshotvalue (the 0xA013 superset) instead, and the + # bluetooth dispatch skips this path -- see use_ble_v2. + if { $update_received == 0 } { set update_received [expr {[clock milliseconds] / 1000.0}] } - # TODO: Consider capturing early (on packet arrival) along with update_received - # in a generic way for all packets + array set ShotSample {} - # Capture for downstreaam consumers of events as well as local use + ::de1::packet::shotsample_parse $packed ShotSample - set this_state [::de1::state::current_state] - set this_substate [::de1::state::current_substate] - set this_flow_phase [::de1::state::flow_phase $this_state $this_substate] + if {[array size ShotSample] == 0} { + # shotsample_parse can return with a blank, if the packet is invalid + msg -WARN "Invalid shot sample received, ignoring" + return + } + + _apply_shotvalue ShotSample $update_received + } + + # Additive BLE: the Bengle self-contained high-resolution superset + # (BengleShotSample, 0xA013) is the SOLE shot-sample source on a Bengle. + # Parse it into a ShotSample-shaped array and run the identical downstream + # pipeline as the stock v1 path; the shared body already handles the + # superset-only fields (GFlow / Weight / MilkTemp) via [info exists]. + proc from_bengleshotvalue {packed {update_received 0}} { + + if { $update_received == 0 } { set update_received [expr {[clock milliseconds] / 1000.0}] } array set ShotSample {} - ::de1::packet::shotsample_parse $packed ShotSample + ::de1::packet::bengleshotsample_parse $packed ShotSample if {[array size ShotSample] == 0} { - # shotsample_parse can return with a blank, if the packet is invalid - msg -WARN "Invalid shot sample received, ignoring" + msg -WARN "Invalid Bengle shot sample received, ignoring" return } + _apply_shotvalue ShotSample $update_received + } + + # Shared post-parse pipeline for both shot-sample sources: drive the live + # chart vars, bridge integrated-scale weight/flow + milk into the scale + # pipeline, integrate dispensed volume, run stop-at-volume, and fire the + # shotvalue callbacks. The parsed sample array is passed by name. + proc _apply_shotvalue {arrName {update_received 0}} { + + upvar 1 $arrName ShotSample + + if { $update_received == 0 } { set update_received [expr {[clock milliseconds] / 1000.0}] } + + # Capture for downstreaam consumers of events as well as local use + + set this_state [::de1::state::current_state] + set this_substate [::de1::state::current_substate] + set this_flow_phase [::de1::state::flow_phase $this_state $this_substate] + + # Stale-sample guard (upstream). Lives in the shared body so it + # protects the Bengle 0xA013 path as well as the stock 0xA00D one. set previous_frame_number $::de1(current_frame_number) if { $this_flow_phase == "during" \ && $ShotSample(FrameNumber) < $previous_frame_number } { @@ -563,6 +600,44 @@ namespace eval ::de1::state::update { set ::de1(current_frame_number) $ShotSample(FrameNumber) set ::de1(steam_heater_temperature) $ShotSample(SteamTemp) + # Bengle ShotSample v2 only: milk temperature probe. + # U16D2 (x0.01 C), 0 = no probe attached. Matches the spec table in + # bengleshotsample_parse below and decodeBengleShotSample in + # decentespresso/decaid, both of which scale by 1/100. + if {[info exists ShotSample(MilkTemp)]} { + set ::de1(milk_temperature) $ShotSample(MilkTemp) + } + + + # Bengle ShotSample v2 only: integrated-scale fields. + # Always stored on their own names for dual-trace charting. + if {[info exists ShotSample(GFlow)]} { + set ::de1(integrated_scale_flow) $ShotSample(GFlow) + } + if {[info exists ShotSample(Weight)]} { + set ::de1(integrated_scale_weight) $ShotSample(Weight) + } + # Feed the integrated scale through the SAME pipeline every BLE + # scale uses, rather than writing that pipeline's output variables + # directly. process_weight_update owns the weight history, the + # filtered weight and flow estimates, the scale_stop_at_half_shot + # two-cup scaling, tare-completion detection, the watchdog, the + # app-side stop-at-weight check and the drink-weight recording. + # Assigning ::de1(scale_weight) here instead would skip all of it. + # + # Skipped when an external BLE scale is connected -- that scale is + # already driving the same pipeline and must keep priority. + # + # NOTE: the pipeline derives flow from the weight history, so the + # firmware's own gravimetric GFlow is NOT used for + # ::de1(scale_weight_rate). GFlow is kept on + # ::de1(integrated_scale_flow) above for the comparison chart + # series. Switching the app to the firmware estimate is a separate + # change and needs a bench comparison first. + if {[info exists ShotSample(Weight)] && $::de1(scale_device_handle) == 0} { + ::device::scale::process_weight_update $ShotSample(Weight) $update_received + } + # TODO: Determine if excessive change in SampleTime should invalidate all deltas @@ -666,6 +741,7 @@ namespace eval ::de1::state::update { volume_dispensed \ [expr { $::de1(preinfusion_volume) \ + $::de1(pour_volume) }] \ + intersample_time $intersample_time \ this_state $this_state \ this_substate $this_substate \ ] @@ -674,7 +750,7 @@ namespace eval ::de1::state::update { return - } ;# from_shotvalue + } ;# _apply_shotvalue } ;# ::de1::state::update @@ -811,6 +887,11 @@ namespace eval ::de1::packet { SteamTemp {char {} {} {unsigned} {}} } + # Additive BLE: ShotSample (0xA00D) is ALWAYS the stock DE1 v1 layout -- + # the Bengle high-resolution scale/milk data rides on its own + # characteristic (BengleShotSample, 0xA013; see bengleshotsample_parse), + # never by redefining 0xA00D. So even a Bengle (use_ble_v2 == 1, which + # now only selects the v2 profile encoding) reads ShotSample as v1 here. if {[use_old_ble_spec] == 1} { array set specarr $spec_old ::fields::unpack $t_shotsample $spec_old ShotSample bigeendian @@ -849,6 +930,61 @@ namespace eval ::de1::packet { } ;# shotsample_parse + # Additive BLE: parse the BengleShotSample characteristic (0xA013) -- the + # Bengle self-contained high-resolution shot sample. This SUPERSET carries + # every ShotSample field plus the integrated-scale gravimetric flow (GFlow), + # the milk probe, and a Flags byte, at the v2 precision. Wire layout + # (big-endian, 28 bytes) MUST match the firmware T_BengleShotSample. Layout + # (the v2 precision: U16D2 scalars, S16P4 SIGNED weight) with a trailing Flags byte: + # SampleTime U16 halfcycles since shot start + # GroupPressure U16D2 bar (*0.01) + # SetGroupPressure U16D2 bar (*0.01) + # GroupFlow U16D2 ml/s (*0.01) + # SetGroupFlow U16D2 ml/s (*0.01) + # GFlow U16D2 g/s (*0.01, gravimetric from integrated scale) + # MixTemp U16D2 C (*0.01) + # HeadTemp U16D2 C (*0.01) + # SetMixTemp U16D2 C (*0.01) + # SetHeadTemp U16D2 C (*0.01) + # Weight S16P4 g (*0.0625, SIGNED -- net of tare, may be negative) + # FrameNumber U8 + # SteamTemp U16D2 C (*0.01) + # MilkTemp U16D2 C (*0.01, 0 = no probe) + # Flags U8 bit0 = firmware TAREd + # Populates the SAME array keys as shotsample_parse so _apply_shotvalue + # consumes it unchanged (the superset-only keys GFlow/Weight/MilkTemp are + # picked up there via [info exists]). + proc bengleshotsample_parse {packet target_array_name} { + upvar $target_array_name ShotSample + if {[string length $packet] < 28} { + msg -ERROR "bengleshotsample_parse: short packet [string length $packet] < 28" + return + } + set spec { + SampleTime {Short {} {} {unsigned} {}} + GroupPressure {Short {} {} {unsigned} {$val * 0.01}} + SetGroupPressure {Short {} {} {unsigned} {$val * 0.01}} + GroupFlow {Short {} {} {unsigned} {$val * 0.01}} + SetGroupFlow {Short {} {} {unsigned} {$val * 0.01}} + GFlow {Short {} {} {unsigned} {$val * 0.01}} + MixTemp {Short {} {} {unsigned} {$val * 0.01}} + HeadTemp {Short {} {} {unsigned} {$val * 0.01}} + SetMixTemp {Short {} {} {unsigned} {$val * 0.01}} + SetHeadTemp {Short {} {} {unsigned} {$val * 0.01}} + Weight {Short {} {} {signed} {$val * 0.0625}} + FrameNumber {char {} {} {unsigned} {}} + SteamTemp {Short {} {} {unsigned} {$val * 0.01}} + MilkTemp {Short {} {} {unsigned} {$val * 0.01}} + Flags {char {} {} {unsigned} {}} + } + array set specarr $spec + ::fields::unpack $packet $spec ShotSample bigeendian + foreach {field val} [array get ShotSample] { + set extra [lindex $specarr($field) 4] + if {$extra != ""} { set ShotSample($field) [expr $extra] } + } + } + } ;# ::de1::packet diff --git a/de1plus/device_scale.tcl b/de1plus/device_scale.tcl index a47697be..dad34cae 100644 --- a/de1plus/device_scale.tcl +++ b/de1plus/device_scale.tcl @@ -128,6 +128,12 @@ namespace eval ::device::scale { proc is_connected {} { + # Bengle v2 has an integrated scale that is always available + # (no separate BLE peripheral, no handle to manage). Treat the + # v2 protocol being active as "scale connected" so all + # scale-aware UI (SAW slider, weight readouts, scale-derived + # notifications) and SAW vs SAV mode-picking work naturally. + if { [::de1::packet::use_ble_v2] } { return 1 } expr { [info exists ::de1(scale_device_handle)] == 1 && $::de1(scale_device_handle) != 0 } } @@ -138,6 +144,8 @@ namespace eval ::device::scale { # Boolean to determine if should be a "problem" that the scale isn't connected and reporting proc expecting_present {} { + # True if a BLE scale address is paired (no v2 special case + # needed -- is_connected handles that). expr { [::device::scale::bluetooth_address] != "" } } @@ -343,6 +351,16 @@ namespace eval ::device::scale { msg -INFO "tare request" } + # Bengle integrated scale: no BLE peripheral and no scale_type, so + # the switch below cannot reach it. Tare over MMR instead. Without + # this the auto-tare before every pour silently does nothing while + # still arming _tare_awaiting_zero. + if { [::de1::packet::use_ble_v2] && $::de1(scale_device_handle) == 0 } { + + set_bengle_scale_tare + + } else { + switch -exact $::settings(scale_type) { atomaxskale { skale_tare } @@ -372,6 +390,8 @@ namespace eval ::device::scale { timemore_dot { timemore_dot_tare } } + } + set ::device::scale::_tare_last_requested [clock milliseconds] set ::device::scale::_tare_awaiting_zero True diff --git a/de1plus/gui.tcl b/de1plus/gui.tcl index 350cd7c9..0c8a35ab 100644 --- a/de1plus/gui.tcl +++ b/de1plus/gui.tcl @@ -6,6 +6,7 @@ package require de1_logging 1.1 package require de1_plugins 1.0 package require de1_dui 1.0 package require de1_history_viewer 1.1 +package require de1_scale_calibration 1.0 ### ### ::gui namespace defined after globals @@ -3410,10 +3411,31 @@ namespace eval ::gui::update { # TODO: Decide how to make these dimensionally meaningful # Probably should be "per second", which would divide by 4 or about 5 (50/60 Hz) - set ::gui::state::_delta_flow \ - [expr { $this_flow - $::gui::state::_previous_flow }] - set ::gui::state::_delta_pressure \ - [expr { $this_pressure - $::gui::state::_previous_pressure }] + # Normalise sample-to-sample deltas to "5 Hz-equivalent + # magnitude" so that downstream chart math (e.g. the + # (10.0 * $flow_delta) negative-flow chart constant) and + # the diff_flow_rate_text display read identically + # regardless of the firmware's BLE notify rate. + # intersample_time is the actual elapsed time between + # samples (computed in de1_de1.tcl::from_shotvalue from + # ShotSample(SampleTime), the DE1's hardware tick counter). + # 0.2 s is the legacy 5 Hz period the chart code was + # implicitly tuned for. Guards: first sample has + # intersample_time = 0; treat as zero delta. Backwards- + # compat: if event_dict lacks the key (older firmware), + # fall back to raw delta (legacy behaviour). + set _ist [expr {[dict exists $event_dict intersample_time] \ + ? [dict get $event_dict intersample_time] : 0.2}] + if { $_ist > 0 } { + set _scale [expr { 0.2 / $_ist }] + set ::gui::state::_delta_flow \ + [expr { ($this_flow - $::gui::state::_previous_flow) * $_scale }] + set ::gui::state::_delta_pressure \ + [expr { ($this_pressure - $::gui::state::_previous_pressure) * $_scale }] + } else { + set ::gui::state::_delta_flow 0 + set ::gui::state::_delta_pressure 0 + } set ::gui::state::_previous_flow $this_flow set ::gui::state::_previous_pressure $this_pressure @@ -3455,6 +3477,18 @@ namespace eval ::gui::update { espresso_weight append [round_to_two_digits $::de1(scale_weight)] espresso_weight_chartable append [round_to_two_digits [expr {0.10 * $::de1(scale_weight)}]] + # Bengle integrated-scale series: recorded side-by-side with + # the external-BLE-scale data so noise/reliability can be + # compared. Only populated on v2 — on v1 (DE1) the vars stay + # at 0 and the Insight skin hides the chart lines. + if {[::de1::packet::use_ble_v2]} { + set _iw [ifexists ::de1(integrated_scale_weight) 0] + set _if [ifexists ::de1(integrated_scale_flow) 0] + espresso_weight_integrated append [round_to_two_digits $_iw] + espresso_weight_integrated_chartable append [round_to_two_digits [expr {0.10 * $_iw}]] + espresso_flow_weight_integrated append [round_to_two_digits $_if] + } + espresso_pressure append [round_to_two_digits $GroupPressure] espresso_flow append [round_to_two_digits $GroupFlow] espresso_flow_2x append [round_to_two_digits [expr {2.0 * $GroupFlow}]] @@ -3635,6 +3669,10 @@ namespace eval ::gui::update { } steam_elapsed append [expr {[steam_pour_millitimer $update_received]/1000.0}] + + # Milk temperature probe (Bengle v2 only, 0 = no probe) + set _mt [ifexists ::de1(milk_temperature) 0] + steam_milk_temperature append [round_to_two_digits $_mt] } } diff --git a/de1plus/machine.tcl b/de1plus/machine.tcl index ac502ccc..be5eb667 100644 --- a/de1plus/machine.tcl +++ b/de1plus/machine.tcl @@ -70,6 +70,7 @@ array set ::de1 { cuuid_10 "0000A010-0000-1000-8000-00805F9B34FB" cuuid_11 "0000A011-0000-1000-8000-00805F9B34FB" cuuid_12 "0000A012-0000-1000-8000-00805F9B34FB" + cuuid_13 "0000A013-0000-1000-8000-00805F9B34FB" cuuid_skale_EF80 "0000EF80-0000-1000-8000-00805F9B34FB" cuuid_skale_EF81 "0000EF81-0000-1000-8000-00805F9B34FB" cuuid_skale_EF82 "0000EF82-0000-1000-8000-00805F9B34FB" @@ -152,6 +153,8 @@ array set ::de1 { scale_timestamp 0 scale_weight_rate 0 scale_weight_rate_raw 0 + integrated_scale_flow 0 + integrated_scale_weight 0 final_water_weight 0 voltage 110 has_catering_kit 0 @@ -221,6 +224,7 @@ array set ::de1_cuuids_to_command_names { $::de1(cuuid_10) FrameWrite $::de1(cuuid_11) WaterLevels $::de1(cuuid_12) Calibration + $::de1(cuuid_13) BengleShotSample } array set ::de1_command_names_to_cuuids [reverse_array ::de1_cuuids_to_command_names] @@ -237,6 +241,9 @@ array set ::settings { steam_two_tap_stop 0 ble_debug 0 cupwarmer_temp 70 + cupwarmer_enable 0 + cupwarmer_prewarm_enable 0 + cupwarmer_prewarm_minutes 30 tank_desired_water_temperature 0 screen_size_height {} log_enabled True diff --git a/de1plus/pkgIndex.tcl b/de1plus/pkgIndex.tcl index 39b3d3fe..cb44b44e 100644 --- a/de1plus/pkgIndex.tcl +++ b/de1plus/pkgIndex.tcl @@ -32,6 +32,8 @@ package ifneeded de1_shot 2.0 [list source [file join "./" shot.tcl]] package ifneeded de1_de1 1.4 [list source [file join "./" de1_de1.tcl]] package ifneeded de1_device_scale 1.5 [list source [file join "./" device_scale.tcl]] +package ifneeded de1_scale_calibration 1.0 [list source [file join "./" scale_calibration.tcl]] + package ifneeded de1_history_viewer 1.1 [list source [file join "./" history_viewer.tcl]] package ifneeded de1_metadata 1.0 [list source [file join "./" metadata.tcl]] package ifneeded de1_profiler 1.0 [list source [file join "./" profiler.tcl]] diff --git a/de1plus/scale_calibration.tcl b/de1plus/scale_calibration.tcl new file mode 100644 index 00000000..44c44e0f --- /dev/null +++ b/de1plus/scale_calibration.tcl @@ -0,0 +1,410 @@ +package provide de1_scale_calibration 1.0 + +######################################################################## +# scale_calibration.tcl — Bengle integrated scale calibration wizard +# +# Drives the 7-step calibration procedure via MMR commands: +# ScaleCalCmd (W) 0x803880 — start a calibration step +# ScaleCalState (R) 0x803884 — poll progress +# ScaleCalWeight (RW) 0x803888 — set/read known weight (grams*10) +# ScaleTare (W) 0x80388C — instant tare +# +# The UI lives on the "calibrate4" page. This file provides the +# ::scale_cal namespace with state management and MMR interaction. +######################################################################## + +namespace eval ::scale_cal { + + # --- Wizard step (1-7) --- + variable step 1 + + # --- Firmware state from last ScaleCalState poll --- + variable fw_step 0 ;# 0=idle,1=zeroing,2=latching,4=taring,5=complete,6=error + variable fw_status 255 ;# E_CalStatus low byte (255 = none/in-progress) + variable fw_substate 0 ;# 0=settling,1=averaging,2=done,3=error + variable fw_remaining 0 ;# seconds remaining in current phase + variable fw_cell -1 ;# detected cell: 0=left, 1=right, -1=none + ;# wire nibble is 0=none, 1=cell A, 2=cell B + + # --- Calibration tracking --- + variable CAL_WEIGHT_MIN_G 1.0 + variable CAL_WEIGHT_MAX_G 10000.0 + variable cal_weight 200.0 ;# known weight in grams (validated) + variable cal_weight_entry "200.0" ;# user entry field (string, validated on focus-out) + variable cal_weight_readback 0 ;# read-back from firmware + variable cells_done {} ;# list of cells calibrated so far (0 and/or 1) + variable poll_after_id "" ;# after ID for polling timer + variable busy 0 ;# 1 while a measurement is running + + # --- UI status text (bound via textvariable) --- + variable status_text "" + variable progress_pct 0 + variable weight_display "" + variable weight_error "" + variable error_colour "#4CAF50" ;# green/yellow/red + + # ====================================================================== + # Public API + # ====================================================================== + + # Called when navigating to calibrate4 + proc enter_page {} { + variable step + variable cells_done + variable busy + variable cal_weight + variable cal_weight_entry + set step 1 + set cells_done {} + set busy 0 + set cal_weight_entry [format "%.1f" $cal_weight] + update_status_for_step + } + + # Called when leaving calibrate4 (Ok button or page navigation) + proc abort_if_running {} { + variable busy + variable poll_after_id + if {$busy} { + # Send abort command + mmr_write "ScaleCalCmd abort" "803880" "04" [long32_to_little_endian_hex 0] + set busy 0 + } + stop_polling + } + + # ====================================================================== + # Step navigation + # ====================================================================== + + proc next_step {} { + variable step + variable busy + if {$busy} return + incr step + if {$step > 7} { set step 7 } + update_status_for_step + } + + proc prev_step {} { + variable step + variable busy + if {$busy} return + incr step -1 + if {$step < 1} { set step 1 } + update_status_for_step + } + + proc go_to_step {n} { + variable step + variable busy + if {$busy} return + set step $n + update_status_for_step + } + + # ====================================================================== + # Step actions + # ====================================================================== + + # Step 2: Start precision zero + proc start_zero {} { + variable busy + variable fw_step + set busy 1 + set fw_step 0 + update_status "Zeroing load cells..." + mmr_write "ScaleCalCmd zero" "803880" "04" [long32_to_little_endian_hex 1] + start_polling + } + + # Step 3/4: Start weight calibration (auto-detect cell) + proc start_weight_cal {} { + variable busy + variable cal_weight + variable fw_step + set busy 1 + set fw_step 0 + + # Write the known weight first (grams * 10) + set weight_val [expr {int($cal_weight * 10)}] + mmr_write "ScaleCalWeight $cal_weight" "803888" "04" [long32_to_little_endian_hex $weight_val] + + # Small delay then start calibration + after 200 { + ::scale_cal::update_status "Calibrating..." + mmr_write "ScaleCalCmd weight_cal" "803880" "04" [long32_to_little_endian_hex 2] + ::scale_cal::start_polling + } + } + + # Step 6: Instant tare. Polls for the firmware's own completion rather + # than assuming success one second after the write. + proc do_tare {} { + variable busy + variable fw_step + set busy 1 + set fw_step 0 + set_bengle_scale_tare + update_status "Taring..." + start_polling + } + + # Step 7: Start live weight display + proc start_verify {} { + variable poll_after_id + update_verify_display + } + + # ====================================================================== + # Polling + # ====================================================================== + + proc start_polling {} { + variable poll_after_id + stop_polling + poll_once + } + + proc stop_polling {} { + variable poll_after_id + if {$poll_after_id ne ""} { + after cancel $poll_after_id + set poll_after_id "" + } + } + + proc poll_once {} { + variable poll_after_id + mmr_read "ScaleCalState" "803884" "00" + set poll_after_id [after 1000 ::scale_cal::poll_once] + } + + # Called from the MMR read handler in de1_comms.tcl + proc on_state_read {raw} { + variable fw_step + variable fw_substate + variable fw_remaining + variable fw_cell + variable busy + variable step + variable cells_done + + variable fw_status + set fw_step [expr {($raw >> 24) & 0xFF}] + # SubState byte: phase in the low nibble, auto-detected cell in the + # high nibble (0 = none, 1 = cell A/left, 2 = cell B/right). + set fw_substate [expr {($raw >> 16) & 0x0F}] + set cell_nibble [expr {($raw >> 20) & 0x0F}] + set fw_cell [expr {$cell_nibble == 0 ? -1 : $cell_nibble - 1}] + set fw_remaining [expr {($raw >> 8) & 0xFF}] + set fw_status [expr {$raw & 0xFF}] + + # Update progress display. + # Firmware step values are 0 idle, 1 zeroing, 2 latching, 4 taring, + # 5 complete, 6 error. There is no step 3 -- an earlier version of + # this file polled for one. Matches ScaleCalibrationStep in + # decentespresso/decaid. + if {$fw_step == 1 || $fw_step == 2 || $fw_step == 4} { + # Active measurement + set total_seconds 15 + set elapsed [expr {$total_seconds - $fw_remaining}] + variable progress_pct + set progress_pct [expr {int(100.0 * $elapsed / $total_seconds)}] + + if {$fw_substate == 0} { + update_status "Settling... ${fw_remaining}s" + } elseif {$fw_substate == 1} { + update_status "Averaging... ${fw_remaining}s" + } + } elseif {$fw_step == 5} { + # Complete + stop_polling + set busy 0 + variable progress_pct + set progress_pct 100 + + if {$step == 2} { + # Zero complete + update_status "Zero complete!" + after 1500 [list ::scale_cal::next_step] + } elseif {$step == 6} { + # Tare complete -- confirmed by the firmware rather than + # assumed one second after the write. + update_status "Tared!" + after 1500 [list ::scale_cal::next_step] + } elseif {$step == 3 || $step == 4} { + # Weight cal complete — track which cell was done + if {$fw_cell == 0} { + update_status "Left cell calibrated!" + } elseif {$fw_cell == 1} { + update_status "Right cell calibrated!" + } + if {$fw_cell ni $cells_done} { + lappend cells_done $fw_cell + } + after 1500 [list ::scale_cal::next_step] + } + } elseif {$fw_step == 6} { + # Error — the low byte carries the firmware's reject reason. + stop_polling + set busy 0 + # Codes match ScaleCalibrationStatus in decentespresso/decaid. + switch -- $fw_status { + 1 { update_status "Error: calibration incomplete. Start again from the zero step." } + 2 { update_status "Error: zero the cells first." } + 3 { update_status "Error: reading drifted. Keep everything still and retry." } + 4 { update_status "Error: weight value implausible. Check the entered weight." } + 5 { update_status "Error: cell reading implausible. Check the weight and placement." } + 6 { update_status "Error: cells too alike to separate. Place the weight further to one side." } + 7 { update_status "Error: reading out of range. Check the load cell wiring." } + 8 { update_status "Error: weight not on a single cell. Remove the platform and retry." } + default { update_status "Error! Check weight placement and retry." } + } + } + } + + # ====================================================================== + # Verify step — live weight display + # ====================================================================== + + proc update_verify_display {} { + variable cal_weight + variable weight_display + variable weight_error + variable error_colour + variable poll_after_id + + # Read weight from integrated scale + set w [ifexists ::de1(integrated_scale_weight)] + if {$w eq ""} { set w 0.0 } + + set weight_display [format "%.1fg" $w] + set err [expr {$w - $cal_weight}] + set weight_error [format "%+.1fg (%.1f%%)" $err [expr {abs($err) / $cal_weight * 100.0}]] + + if {abs($err) <= 1.0} { + set error_colour "#4CAF50" ;# green + } elseif {abs($err) <= 5.0} { + set error_colour "#FF9800" ;# yellow/orange + } else { + set error_colour "#F44336" ;# red + } + + # Keep updating at ~2 Hz + set poll_after_id [after 500 ::scale_cal::update_verify_display] + } + + # ====================================================================== + # UI helpers + # ====================================================================== + + proc update_status {msg} { + variable status_text + set status_text $msg + } + + proc update_status_for_step {} { + variable step + variable progress_pct + variable status_text + set progress_pct 0 + + switch -- $step { + 1 { set status_text "Remove the drip tray platform and drip tray." } + 2 { set status_text "Keep the scale area clear. Press Start to zero." } + 3 { set status_text "Place weight directly on EITHER bare cell. Press Start." } + 4 { set status_text "Move weight to the OTHER cell. Press Start." } + 5 { set status_text "Replace the drip tray and platform." } + 6 { set status_text "Taring with empty platform..." } + 7 { + set status_text "Place weight on platform to verify." + start_verify + } + } + } + + # Return the step title for the UI + proc step_title {} { + variable step + switch -- $step { + 1 { return "Remove Platform" } + 2 { return "Zero Load Cells" } + 3 { return "Calibrate First Cell" } + 4 { return "Calibrate Second Cell" } + 5 { return "Replace Platform" } + 6 { return "Tare" } + 7 { return "Verify" } + } + } + + # Is the current step an action step needing a "Start" button? + proc step_needs_start {} { + variable step + return [expr {$step == 2 || $step == 3 || $step == 4}] + } + + # Is the current step a "Done, Next" step? + proc step_needs_done {} { + variable step + return [expr {$step == 1 || $step == 5}] + } + + # Handle the main action button press + proc action_button_pressed {} { + variable step + variable busy + if {$busy} return + + switch -- $step { + 1 { next_step } + 2 { start_zero } + 3 { start_weight_cal } + 4 { start_weight_cal } + 5 { next_step; do_tare } + 6 { do_tare } + 7 { } + } + } + + # Handle retry after error + proc retry {} { + variable step + switch -- $step { + 2 { start_zero } + 3 { start_weight_cal } + 4 { start_weight_cal } + } + } + + # Restart entire calibration + proc recalibrate {} { + stop_polling + enter_page + } + + # Validate and apply the user-entered calibration weight + proc validate_weight {} { + variable cal_weight_entry + variable cal_weight + + # Strip whitespace and trailing 'g' if user typed it + set entry [string trim $cal_weight_entry] + set entry [string trimright $entry "gG"] + set entry [string trim $entry] + + # Bounds match ScaleCalibrationCapability in decentespresso/decaid + # and the reaprime skins: 1 g to 10 kg. Without an upper bound a + # mistyped entry is sent to the firmware as a real reference mass. + if {[string is double -strict $entry] + && $entry >= $::scale_cal::CAL_WEIGHT_MIN_G + && $entry <= $::scale_cal::CAL_WEIGHT_MAX_G} { + set cal_weight [expr {double($entry)}] + set cal_weight_entry [format "%.1f" $cal_weight] + } else { + # Revert to current valid value + set cal_weight_entry [format "%.1f" $cal_weight] + } + } + +} diff --git a/de1plus/shot.tcl b/de1plus/shot.tcl index 598eb657..646da942 100644 --- a/de1plus/shot.tcl +++ b/de1plus/shot.tcl @@ -120,12 +120,34 @@ namespace eval ::shot { goal [huddle list {*}[get_vector espresso_pressure_goal]] \ ] - set flow [huddle create \ + # Scale-source mapping for the Visualizer payload: + # v1: a single scale exists (BLE if paired). by_weight/weight come + # from the legacy series; no _external fields. + # v2: Bengle's integrated scale is always primary -> by_weight/weight + # come from the *_integrated series. If an external BLE scale + # is *also* paired, emit by_weight_external/weight_external from + # the legacy series (which the de1_de1 bridge leaves untouched + # when an external handle is live, so it holds the BLE values). + if {[::de1::packet::use_ble_v2]} { + set _by_weight_series espresso_flow_weight_integrated + set _weight_series espresso_weight_integrated + set _emit_external [expr {$::settings(scale_bluetooth_address) != ""}] + } else { + set _by_weight_series espresso_flow_weight + set _weight_series espresso_weight + set _emit_external 0 + } + + set _flow_pairs [list \ flow [huddle list {*}[get_vector espresso_flow]] \ - by_weight [huddle list {*}[get_vector espresso_flow_weight]] \ + by_weight [huddle list {*}[get_vector $_by_weight_series]] \ by_weight_raw [huddle list {*}[get_vector espresso_flow_weight_raw]] \ goal [huddle list {*}[get_vector espresso_flow_goal]] \ ] + if {$_emit_external} { + lappend _flow_pairs by_weight_external [huddle list {*}[get_vector espresso_flow_weight]] + } + set flow [huddle create {*}$_flow_pairs] set temperature [huddle create \ basket [huddle list {*}[get_vector espresso_temperature_basket]] \ @@ -133,10 +155,14 @@ namespace eval ::shot { goal [huddle list {*}[get_vector espresso_temperature_goal]] \ ] - set totals [huddle create \ - weight [huddle list {*}[get_vector espresso_weight]] \ + set _totals_pairs [list \ + weight [huddle list {*}[get_vector $_weight_series]] \ water_dispensed [huddle list {*}[get_vector espresso_water_dispensed]] \ ] + if {$_emit_external} { + lappend _totals_pairs weight_external [huddle list {*}[get_vector espresso_weight]] + } + set totals [huddle create {*}$_totals_pairs] set resistance [huddle create \ resistance [huddle list {*}[get_vector espresso_resistance]] \ @@ -307,7 +333,7 @@ namespace eval ::shot { popup [translate_toast "Converting old shot files"] - blt::vector create espresso_elapsed god_espresso_elapsed god_espresso_pressure steam_pressure steam_temperature steam_temperature100th steam_flow steam_elapsed espresso_pressure espresso_flow god_espresso_flow espresso_flow_weight god_espresso_flow_weight espresso_flow_weight_2x god_espresso_flow_weight_2x espresso_flow_2x god_espresso_flow_2x espresso_flow_delta espresso_pressure_delta espresso_temperature_mix espresso_temperature_basket god_espresso_temperature_basket espresso_state_change espresso_pressure_goal espresso_flow_goal espresso_flow_goal_2x espresso_temperature_goal espresso_weight espresso_weight_chartable espresso_resistance_weight espresso_resistance + blt::vector create espresso_elapsed god_espresso_elapsed god_espresso_pressure steam_pressure steam_temperature steam_temperature100th steam_flow steam_elapsed steam_milk_temperature espresso_pressure espresso_flow god_espresso_flow espresso_flow_weight god_espresso_flow_weight espresso_flow_weight_2x god_espresso_flow_weight_2x espresso_flow_2x god_espresso_flow_2x espresso_flow_delta espresso_pressure_delta espresso_temperature_mix espresso_temperature_basket god_espresso_temperature_basket espresso_state_change espresso_pressure_goal espresso_flow_goal espresso_flow_goal_2x espresso_temperature_goal espresso_weight espresso_weight_chartable espresso_resistance_weight espresso_resistance espresso_flow_weight_integrated espresso_weight_integrated espresso_weight_integrated_chartable blt::vector create espresso_de1_explanation_chart_pressure espresso_de1_explanation_chart_flow espresso_de1_explanation_chart_elapsed espresso_de1_explanation_chart_elapsed_flow espresso_water_dispensed espresso_flow_weight_raw espresso_de1_explanation_chart_temperature espresso_de1_explanation_chart_temperature_10 espresso_de1_explanation_chart_selected_step blt::vector create espresso_de1_explanation_chart_flow_1 espresso_de1_explanation_chart_elapsed_flow_1 espresso_de1_explanation_chart_flow_2 espresso_de1_explanation_chart_elapsed_flow_2 espresso_de1_explanation_chart_flow_3 espresso_de1_explanation_chart_elapsed_flow_3 blt::vector create espresso_de1_explanation_chart_elapsed_1 espresso_de1_explanation_chart_elapsed_2 espresso_de1_explanation_chart_elapsed_3 espresso_de1_explanation_chart_pressure_1 espresso_de1_explanation_chart_pressure_2 espresso_de1_explanation_chart_pressure_3 @@ -329,7 +355,7 @@ namespace eval ::shot { array set ::settings [array get ::settings_backup] unset -nocomplain ::settings_backup - blt::vector destroy espresso_elapsed god_espresso_elapsed god_espresso_pressure steam_pressure steam_temperature steam_temperature100th steam_flow steam_elapsed espresso_pressure espresso_flow god_espresso_flow espresso_flow_weight god_espresso_flow_weight espresso_flow_weight_2x god_espresso_flow_weight_2x espresso_flow_2x god_espresso_flow_2x espresso_flow_delta espresso_pressure_delta espresso_temperature_mix espresso_temperature_basket god_espresso_temperature_basket espresso_state_change espresso_pressure_goal espresso_flow_goal espresso_flow_goal_2x espresso_temperature_goal espresso_weight espresso_weight_chartable espresso_resistance_weight espresso_resistance + blt::vector destroy espresso_elapsed god_espresso_elapsed god_espresso_pressure steam_pressure steam_temperature steam_temperature100th steam_flow steam_elapsed steam_milk_temperature espresso_pressure espresso_flow god_espresso_flow espresso_flow_weight god_espresso_flow_weight espresso_flow_weight_2x god_espresso_flow_weight_2x espresso_flow_2x god_espresso_flow_2x espresso_flow_delta espresso_pressure_delta espresso_temperature_mix espresso_temperature_basket god_espresso_temperature_basket espresso_state_change espresso_pressure_goal espresso_flow_goal espresso_flow_goal_2x espresso_temperature_goal espresso_weight espresso_weight_chartable espresso_resistance_weight espresso_resistance espresso_flow_weight_integrated espresso_weight_integrated espresso_weight_integrated_chartable blt::vector destroy espresso_de1_explanation_chart_pressure espresso_de1_explanation_chart_flow espresso_de1_explanation_chart_elapsed espresso_de1_explanation_chart_elapsed_flow espresso_water_dispensed espresso_flow_weight_raw espresso_de1_explanation_chart_temperature espresso_de1_explanation_chart_temperature_10 espresso_de1_explanation_chart_selected_step blt::vector destroy espresso_de1_explanation_chart_flow_1 espresso_de1_explanation_chart_elapsed_flow_1 espresso_de1_explanation_chart_flow_2 espresso_de1_explanation_chart_elapsed_flow_2 espresso_de1_explanation_chart_flow_3 espresso_de1_explanation_chart_elapsed_flow_3 blt::vector destroy espresso_de1_explanation_chart_elapsed_1 espresso_de1_explanation_chart_elapsed_2 espresso_de1_explanation_chart_elapsed_3 espresso_de1_explanation_chart_pressure_1 espresso_de1_explanation_chart_pressure_2 espresso_de1_explanation_chart_pressure_3 diff --git a/de1plus/skins/default/de1_skin_settings.tcl b/de1plus/skins/default/de1_skin_settings.tcl index 9f4f972d..0c64090b 100644 --- a/de1plus/skins/default/de1_skin_settings.tcl +++ b/de1plus/skins/default/de1_skin_settings.tcl @@ -481,7 +481,9 @@ add_de1_button "settings_2c2" { profile_has_changed_set; dui page open_dialog du # (beta) weight based shot ending, only displayed if a skale is connected -if {$::settings(scale_bluetooth_address) != ""} { +# Stop-at-weight needs a scale of some kind: a paired BLE scale, +# or a Bengle integrated scale reached over the v2 protocol. +if {$::settings(scale_bluetooth_address) != "" || [::de1::packet::use_ble_v2]} { add_de1_text "settings_2a settings_2b" 1730 1100 -text [translate "4: stop at weight:"] -font Helv_10_bold -fill "#7f879a" -anchor "nw" -width [rescale_x_skin 1600] -justify "center" add_de1_widget "settings_2a settings_2b" scale 1730 1175 {} -to 2000 -from 0 -background $::settings(color_stage_3) -showvalue 0 -borderwidth 1 -bigincrement 1 -resolution 0.2 -length [rescale_x_skin 546] -width [rescale_x_skin 150] -variable ::settings(final_desired_shot_weight) -font Helv_15_bold -sliderlength [rescale_x_skin 125] -relief flat -command "profile_has_changed_set; update_de1_explanation_chart_soon" -foreground #FFFFFF -troughcolor $slider_trough_color -borderwidth 0 -highlightthickness 0 -orient horizontal add_de1_variable "settings_2a settings_2b" 1730 1325 -text "" -font Helv_8 -fill "#4e85f4" -anchor "nw" -width [rescale_x_skin 1200] -justify "left" -textvariable {[return_stop_at_weight_measurement $::settings(final_desired_shot_weight)]} @@ -1730,8 +1732,8 @@ proc scheduler_feature_hide_show_refresh { } { #add_de1_widget "settings_2c" checkbutton 1538 830 {} -text [translate "4: Move on if..."] -padx 0 -pady 0 -indicatoron true -font Helv_9_bold -anchor nw -foreground #7f879a -activeforeground #7f879a -variable ::current_adv_step(exit_if) -borderwidth 0 -highlightthickness 0 -command save_current_adv_shot_step -selectcolor #f9f9f9 -activebackground #f9f9f9 -bg #f9f9f9 -relief flat # scheduled power up/down add_de1_text "settings_3" 180 1134 -justify left -anchor "nw" -font $optionfont -text [translate "Keep hot"] -fill "#4e85f4" -width [rescale_x_skin 1000] -dui add dtoggle "settings_3" 50 1140 -height 50 -width 100 -anchor nw -variable ::settings(scheduler_enable) -command scheduler_feature_hide_show_refresh -add_de1_button "settings_3" { set ::settings(scheduler_enable) [expr {! $::settings(scheduler_enable)}]; scheduler_feature_hide_show_refresh } 50 1140 500 1190 +dui add dtoggle "settings_3" 50 1140 -height 50 -width 100 -anchor nw -variable ::settings(scheduler_enable) -command { scheduler_feature_hide_show_refresh; set_alarms_for_de1_wake_sleep } +add_de1_button "settings_3" { set ::settings(scheduler_enable) [expr {! $::settings(scheduler_enable)}]; scheduler_feature_hide_show_refresh; set_alarms_for_de1_wake_sleep } 50 1140 500 1190 scheduler_feature_hide_show_refresh @@ -2188,15 +2190,178 @@ set calibration_row_spacing 115 # (re)calibration page -add_de1_text "calibrate calibrate2 calibrate3" 1280 290 -text [translate "Calibrate"] -font Helv_20_bold -width [rescale_x_skin 2400] -fill "#444444" -anchor "center" -justify "center" +add_de1_text "calibrate calibrate2 calibrate3 calibrate4" 1280 290 -text [translate "Calibrate"] -font Helv_20_bold -width [rescale_x_skin 2400] -fill "#444444" -anchor "center" -justify "center" #add_de1_text "calibrate" 2520 1510 -text [subst {\[ [translate "Page 1 of 3"] \]}] -font Helv_10_bold -fill "#666666" -anchor "ne" #add_de1_text "calibrate2" 2520 1510 -text [subst {\[ [translate "Page 2 of 3"] \]}] -font Helv_10_bold -fill "#666666" -anchor "ne" #add_de1_text "calibrate3" 2520 1510 -text [subst {\[ [translate "Page 3 of 3"] \]}] -font Helv_10_bold -fill "#666666" -anchor "ne" - dui add dbutton "calibrate" 2050 1460 -style insight_ok -anchor nw -command show_page_calibrate_2 -label [subst {[translate "Page 1 of 3"] >}] - dui add dbutton "calibrate2" 2050 1460 -style insight_ok -anchor nw -command show_page_calibrate_3 -label [subst {[translate "Page 2 of 3"] >}] - dui add dbutton "calibrate3" 2050 1460 -style insight_ok -anchor nw -command show_page_calibrate -label [subst {[translate "Page 3 of 3"] >}] + dui add dbutton "calibrate" 2050 1460 -style insight_ok -anchor nw -command show_page_calibrate_2 -label [subst {[translate "Page 1"] >}] + dui add dbutton "calibrate2" 2050 1460 -style insight_ok -anchor nw -command show_page_calibrate_3 -label [subst {[translate "Page 2"] >}] + # Page 5 = calibrate4 = Scale Calibration; loops back to page 1 (calibrate). + dui add dbutton "calibrate4" 2050 1460 -style insight_ok -anchor nw -command show_page_calibrate -label [subst {[translate "Page 5"] >}] + dui add dbutton "calibrate3" 2050 1460 -style insight_ok -anchor nw -command show_page_calibrate_3_next -label [subst {[translate "Page 3"] >}] + +proc show_page_calibrate_3_next {} { + say [translate {Done}] $::settings(sound_button_in) + if {[is_bengle_model]} { + # Bengle adds a Cup Warmer page after page 3. + page_to_show_when_off cupwarmer + } else { + set_heater_tweaks + page_to_show_when_off calibrate + } +} + +if {[is_bengle_model]} { + ######################################## + # Cup warmer sub-page (Bengle only) + # Lives in the Calibration workflow as Page 4 of 5 (between calibrate3 + # flow). Navigation in and out is via the + # "Page N >" button on the bottom right, matching the other calibrate + # pages. + # + # Firmware side (addresses per the firmware's MMR.def): + # MatSetPoint (0x00803874) - RWD, persisted, °C + # CupWarmerMode (0x008038AC) - RW, NOT persisted (0 on every boot) + # MatHeaterDrivePct (0x008038B4) - R, 0-100 live drive + # MatTempFault (0x008038B8) - R, 0=OK / 1=OpenOrShort + # MatPreheatEnable (0x008038D0) - RWD, persisted + # MatPreheatLeadMin (0x008038D4) - RWD, persisted, minutes + # The firmware gates the heater on API state: it runs only in + # Idle/SchedIdle, never in Sleep. Pre-warm timing is the firmware's too -- + # with MatPreheatEnable set it starts the mat MatPreheatLeadMin minutes + # before a scheduled wake, with no tablet connected. + ######################################## + + # Pushes the current UI state to firmware + persists settings.tdb. + # Throttled so a slider drag doesn't flood the BLE queue. + namespace eval ::cupwarmer {} + set ::cupwarmer::_commit_after "" + proc ::cupwarmer::_commit {} { + set ::cupwarmer::_commit_after "" + set_cupwarmer_temperature $::settings(cupwarmer_temp) + set_cupwarmer_mode $::settings(cupwarmer_enable) + # Pre-warm timing lives in the firmware (MatPreheatEnable / + # MatPreheatLeadMin), so the toggle and the lead slider must reach the + # machine, not only be saved locally. + set_cupwarmer_preheat [ifexists ::settings(cupwarmer_prewarm_enable) 0] \ + [ifexists ::settings(cupwarmer_prewarm_minutes) 30] + set_alarms_for_de1_wake_sleep + after idle save_settings + } + proc ::cupwarmer::_request_commit {} { + if {$::cupwarmer::_commit_after ne ""} { + after cancel $::cupwarmer::_commit_after + } + set ::cupwarmer::_commit_after [after 250 ::cupwarmer::_commit] + } + + proc ::cupwarmer::toggle_enable {} { + set ::settings(cupwarmer_enable) [expr {!$::settings(cupwarmer_enable)}] + ::cupwarmer::_request_commit + } + + proc ::cupwarmer::toggle_prewarm {} { + set ::settings(cupwarmer_prewarm_enable) [expr {!$::settings(cupwarmer_prewarm_enable)}] + ::cupwarmer::_request_commit + } + + proc ::cupwarmer::status_text {} { + set fault [ifexists ::de1(mat_temp_fault) 0] + if {$fault == 1} { return [translate "NTC disconnected — warmer disabled"] } + if {[ifexists ::settings(cupwarmer_enable) 0] != 1} { + return [translate "Warmer is off"] + } + set pct [ifexists ::de1(mat_heater_drive) 0] + if {$pct > 0} { + return "[translate "Heating"] — ${pct}%" + } + return [translate "At target"] + } + + proc ::cupwarmer::prewarm_preview {} { + if {[ifexists ::settings(scheduler_enable) 0] != 1} { + return [translate "Enable the wake schedule first"] + } + if {[ifexists ::settings(cupwarmer_prewarm_enable) 0] != 1} { + return "" + } + set mins [ifexists ::settings(cupwarmer_prewarm_minutes) 0] + set wake_at [next_alarm_time $::settings(scheduler_wake)] + set prewarm_at [expr {$wake_at - 60 * $mins}] + return "[translate "Heater turns on at"] [time_format $prewarm_at]" + } + + # --- Page registration + layout ------------------------------------------- + # Shares settings_message.png background with the other calibrate pages + # so the cupwarmer page blends into the workflow. + add_de1_page "cupwarmer" "settings_message.png" "default" + + # Matches the calibrate-page title style. + add_de1_text "cupwarmer" 1280 290 -text [translate "Cup Warmer"] -font Helv_20_bold -width 1200 -fill "#444444" -anchor "center" -justify "center" + + # Two white cards: left = main control, right = schedule + status. + dui add canvas_item rect cupwarmer 60 400 1260 1400 -style insight_front_box + dui add canvas_item rect cupwarmer 1300 400 2500 1400 -style insight_front_box + + # ------------- Left card: enable + target temperature -------------------- + add_de1_text "cupwarmer" 100 440 -text [translate "Cup Warmer"] -font Helv_15_bold -fill "#2d3046" -anchor "nw" + add_de1_variable "cupwarmer" 100 510 -text "" -font Helv_9 -fill "#7f879a" -anchor "nw" -width [rescale_y_skin 1100] -textvariable {[::cupwarmer::status_text]} + + # Enable toggle. The button is drawn on top of the dtoggle so it catches + # all taps in the overlapping area; the dtoggle exists purely for the + # visual. Keep it -command-less so we don't have two handlers on the + # same click. The dtoggle's -variable trace still repaints when + # toggle_enable flips the setting, so the visual stays in sync. + add_de1_text "cupwarmer" 100 610 -text [translate "Warmer on"] -font Helv_10_bold -fill "#4e85f4" -anchor "nw" + dui add dtoggle "cupwarmer" 540 620 -height 60 -width 120 -anchor nw -variable ::settings(cupwarmer_enable) + add_de1_button "cupwarmer" {::cupwarmer::toggle_enable} 540 610 720 690 + + # Target temperature section + add_de1_text "cupwarmer" 100 780 -text [translate "Target temperature"] -font Helv_10_bold -fill "#7f879a" -anchor "nw" + add_de1_variable "cupwarmer" 100 860 -text "" -font Helv_20_bold -fill "#2d3046" -anchor "nw" -textvariable {[format "%d °C" [round_to_integer $::settings(cupwarmer_temp)]]} + + add_de1_widget "cupwarmer" scale 100 1050 {} -from 20 -to 80 -background #e4d1c1 -borderwidth 1 -bigincrement 5 -showvalue 0 -resolution 1 -length [rescale_x_skin 1100] -width [rescale_y_skin 135] -variable ::settings(cupwarmer_temp) -font Helv_10_bold -sliderlength [rescale_x_skin 125] -relief flat -orient horizontal -foreground #FFFFFF -troughcolor "#d9cfbf" -borderwidth 0 -highlightthickness 0 -command {::cupwarmer::_request_commit; list} + + add_de1_text "cupwarmer" 100 1200 -text [translate "20 °C"] -font Helv_7 -fill "#7f879a" -anchor "nw" + add_de1_text "cupwarmer" 1200 1200 -text [translate "80 °C"] -font Helv_7 -fill "#7f879a" -anchor "ne" + + add_de1_variable "cupwarmer" 100 1290 -text "" -font Helv_8 -fill "#7f879a" -anchor "nw" -width [rescale_y_skin 1100] -justify "left" -textvariable {[if {[ifexists ::de1(mat_temp_fault) 0] == 1} { return [translate "⚠ NTC sensor disconnected. Check the J7 probe."] } else { return [translate "Cups warm to target while the machine is in Idle. The warmer turns off when the machine is asleep, pulling a shot, steaming, or dispensing hot water."] }]} + + # ------------- Right card: pre-warm schedule ----------------------------- + add_de1_text "cupwarmer" 1340 440 -text [translate "Pre-warm on schedule"] -font Helv_15_bold -fill "#2d3046" -anchor "nw" + add_de1_variable "cupwarmer" 1340 510 -text "" -font Helv_8 -fill "#7f879a" -anchor "nw" -width [rescale_y_skin 1100] -justify "left" -textvariable {[if {[ifexists ::settings(scheduler_enable) 0] == 1} { return [translate "Turns the warmer on automatically a few minutes before the daily wake-up time, so the cups are hot when you are."] } else { return [translate "The daily wake-up schedule is currently disabled. Enable it on the Settings tab to use this feature."] }]} + + # Pre-warm enable toggle. + add_de1_text "cupwarmer" 1340 650 -text [translate "Pre-warm cups before wake"] -font Helv_10_bold -fill "#4e85f4" -anchor "nw" + dui add dtoggle "cupwarmer" 2340 650 -height 60 -width 120 -anchor nw -variable ::settings(cupwarmer_prewarm_enable) + add_de1_button "cupwarmer" {::cupwarmer::toggle_prewarm} 2340 640 2490 720 + + # Minutes-before-wake slider + add_de1_text "cupwarmer" 1340 800 -text [translate "Start heater before wake"] -font Helv_10_bold -fill "#7f879a" -anchor "nw" + add_de1_variable "cupwarmer" 1340 880 -text "" -font Helv_20_bold -fill "#2d3046" -anchor "nw" -textvariable {[format "%d [translate "min"]" [round_to_integer $::settings(cupwarmer_prewarm_minutes)]]} + + add_de1_widget "cupwarmer" scale 1340 1070 {} -from 0 -to 120 -background #e4d1c1 -borderwidth 1 -bigincrement 5 -showvalue 0 -resolution 5 -length [rescale_x_skin 1100] -width [rescale_y_skin 135] -variable ::settings(cupwarmer_prewarm_minutes) -font Helv_10_bold -sliderlength [rescale_x_skin 125] -relief flat -orient horizontal -foreground #FFFFFF -troughcolor "#d9cfbf" -borderwidth 0 -highlightthickness 0 -command {::cupwarmer::_request_commit; list} + + add_de1_text "cupwarmer" 1340 1220 -text [translate "0 min"] -font Helv_7 -fill "#7f879a" -anchor "nw" + add_de1_text "cupwarmer" 2440 1220 -text [translate "2 h"] -font Helv_7 -fill "#7f879a" -anchor "ne" + + add_de1_variable "cupwarmer" 1340 1310 -text "" -font Helv_8 -fill "#4e85f4" -anchor "nw" -width [rescale_y_skin 1100] -justify "left" -textvariable {[::cupwarmer::prewarm_preview]} + + # "Page 4 >" advance button — lives at the same coordinates as the + # calibrate pages so the navigation feels continuous. + # (Scale Calibration, page 5). The label numbering here matches the + # pattern set by the other calibrate pages: the button on page N says + # "Page N >" and takes you to page N+1. + dui add dbutton "cupwarmer" 2050 1460 -style insight_ok -anchor nw -command show_page_calibrate_4 -label [subst {[translate "Page 4"] >}] +} ;# end is_bengle_model + +proc show_page_calibrate_4 {} { + say [translate {Done}] $::settings(sound_button_in) + ::scale_cal::enter_page + page_to_show_when_off calibrate4 +} proc show_page_calibrate {} { say [translate {Done}] $::settings(sound_button_in) @@ -2324,14 +2489,59 @@ proc send_refill_kit_override_from_gui {args} { } - add_de1_text "calibrate calibrate2 calibrate3" 1280 1310 -text [translate "Ok"] -font Helv_10_bold -fill "#fAfBff" -anchor "center" - add_de1_button "calibrate calibrate2 calibrate3" {say [translate {Done}] $::settings(sound_button_in); + ############################################################################################### + # Calibrate page 4: Scale Calibration (Bengle only) + ############################################################################################### + + add_de1_text "calibrate4" 1280 370 -text [translate "Scale Calibration"] -font Helv_10_bold -fill "#7f879a" -anchor "center" -justify "center" + + # Step indicator: "Step N of 7: Title" + add_de1_variable "calibrate4" 1280 440 -text "" -font Helv_9_bold -fill "#444444" -anchor "center" -justify "center" -textvariable {[translate "Step $::scale_cal::step of 7: [::scale_cal::step_title]"]} + + # Status / instruction text + add_de1_variable "calibrate4" 1280 550 -text "" -font Helv_8 -fill "#7f879a" -anchor "center" -justify "center" -width 1800 -textvariable {[translate $::scale_cal::status_text]} + + # Progress bar area (shown during active measurement steps 2,3,4) + add_de1_variable "calibrate4" 1280 700 -text "" -font Helv_15_bold -fill "#4e85f4" -anchor "center" -justify "center" -textvariable {[if {$::scale_cal::busy} {return "$::scale_cal::progress_pct%"} else {return ""}]} + + # Verification display — large weight reading (step 7 only) + add_de1_variable "calibrate4" 1280 680 -text "" -font Helv_20_bold -fill "#444444" -anchor "center" -justify "center" -textvariable {[if {$::scale_cal::step == 7} {return $::scale_cal::weight_display} else {return ""}]} + + # Verification error line (step 7 only) + add_de1_variable "calibrate4" 1280 780 -text "" -font Helv_9_bold -fill "#7f879a" -anchor "center" -justify "center" -textvariable {[if {$::scale_cal::step == 7} {return "Expected: ${::scale_cal::cal_weight}g Error: $::scale_cal::weight_error"} else {return ""}]} + + # Main action button — "Done, Next" for steps 1,5 or "Start" for steps 2,3,4 + add_de1_variable "calibrate4" 1280 900 -text "" -font Helv_10_bold -fill "#4e85f4" -anchor "center" -textvariable {[if {$::scale_cal::step == 7} {return ""} elseif {$::scale_cal::busy} {return ""} elseif {[::scale_cal::step_needs_done]} {return "\[ [translate {Done, Next}] \]"} elseif {[::scale_cal::step_needs_start]} {return "\[ [translate {Start}] \]"} else {return "\[ [translate {Next}] \]"}]} + add_de1_button "calibrate4" {::scale_cal::action_button_pressed} 880 860 1680 960 "" + + # Retry button (shown after error) + add_de1_variable "calibrate4" 1280 1000 -text "" -font Helv_8_bold -fill "#F44336" -anchor "center" -textvariable {[if {$::scale_cal::fw_step == 6 && !$::scale_cal::busy} {return "\[ [translate {Retry}] \]"} else {return ""}]} + add_de1_button "calibrate4" {if {$::scale_cal::fw_step == 6} {::scale_cal::retry}} 880 960 1680 1060 "" + + # Recalibrate button (step 7 only) + add_de1_variable "calibrate4" 700 1100 -text "" -font Helv_8_bold -fill "#4e85f4" -anchor "center" -textvariable {[if {$::scale_cal::step == 7} {return "\[ [translate {Recalibrate}] \]"} else {return ""}]} + add_de1_button "calibrate4" {if {$::scale_cal::step == 7} {::scale_cal::recalibrate}} 400 1060 1000 1160 "" + + # "Looks Good" button (step 7 only) — exits calibration + add_de1_variable "calibrate4" 1860 1100 -text "" -font Helv_8_bold -fill "#4CAF50" -anchor "center" -textvariable {[if {$::scale_cal::step == 7} {return "\[ [translate {Looks Good}] \]"} else {return ""}]} + add_de1_button "calibrate4" {if {$::scale_cal::step == 7} {say [translate {Done}] $::settings(sound_button_in); ::scale_cal::abort_if_running; save_settings; set_next_page off settings_3; page_show settings_3}} 1560 1060 2160 1160 "" + + # Calibration weight entry — user can type any weight value + add_de1_text "calibrate4" 1080 1170 -text [translate "Calibration weight (g):"] -font Helv_7_bold -fill "#7f879a" -anchor "ne" + add_de1_widget "calibrate4" entry 1100 1155 { + bind $widget { ::scale_cal::validate_weight; hide_android_keyboard } + bind $widget { ::scale_cal::validate_weight; hide_android_keyboard } + } -width [expr {int(6 * $::globals(entry_length_multiplier))}] -font Helv_8 -borderwidth 1 -bg #FFFFFF -foreground #4e85f4 -textvariable ::scale_cal::cal_weight_entry -relief flat -highlightthickness 1 -highlightcolor #000000 + + add_de1_text "calibrate calibrate2 calibrate3 calibrate4" 1280 1310 -text [translate "Ok"] -font Helv_10_bold -fill "#fAfBff" -anchor "center" + add_de1_button "calibrate calibrate2 calibrate3 calibrate4" {say [translate {Done}] $::settings(sound_button_in); if {[ifexists ::calibration_disabled_fahrenheit] == 1} { set ::settings(enable_fahrenheit) 1 unset -nocomplain ::calibration_disabled_fahrenheit msg "Calibration re-enabled Fahrenheit" } + ::scale_cal::abort_if_running save_settings; set_next_page off settings_3; set_heater_tweaks; page_show settings_3;} 980 1210 1580 1410 "" @@ -2508,3 +2718,11 @@ proc flush_log_loop {} { #after 2 show_settings decent_login #after 2 show_settings email_support + +# Arm the wake / prewarm timers once on app start so a tablet reboot mid-day +# still fires the scheduled wake (and the cup-warmer pre-warm) correctly. The +# previous version only rearmed when scheduler_wake naturally fired or when +# the user hit OK on the settings pages (exit_settings_pages at line 40). Use +# `after idle` to defer until the event loop is running, so this works no +# matter the source-order of vars.tcl. +after idle set_alarms_for_de1_wake_sleep diff --git a/de1plus/skins/default/standard_includes.tcl b/de1plus/skins/default/standard_includes.tcl index 296d9cff..c9b50412 100644 --- a/de1plus/skins/default/standard_includes.tcl +++ b/de1plus/skins/default/standard_includes.tcl @@ -9,7 +9,7 @@ add_de1_page "sleep" "sleep.jpg" "default" add_de1_page "tankfilling" "filling_tank.jpg" "default" add_de1_page "tankempty refill" "fill_tank.jpg" "default" add_de1_page "cleaning" "cleaning.jpg" "default" -add_de1_page "message calibrate calibrate2 calibrate3 infopage versionpage tabletstyles languages extensions profile_notes measurements temperature_steps bev_type enter_de1_sn decent_login email_support" "settings_message.png" "default" +add_de1_page "message calibrate calibrate2 calibrate3 calibrate4 infopage versionpage tabletstyles languages extensions profile_notes measurements temperature_steps bev_type enter_de1_sn decent_login email_support" "settings_message.png" "default" add_de1_page "create_preset" "settings_3_choices.png" "default" add_de1_page "firmware_update_3" "firmware_upgrade.jpg" "default" add_de1_page "firmware_update_1 firmware_update_4" "firmware_upgrade_off.jpg" "default" diff --git a/de1plus/utils.tcl b/de1plus/utils.tcl index d634136d..caf45656 100644 --- a/de1plus/utils.tcl +++ b/de1plus/utils.tcl @@ -1546,6 +1546,12 @@ proc load_settings {} { set ::settings(stress_test) 0 + # Cup warmer must not auto-activate after a power cut / app restart. The + # target temperature and pre-warm settings persist, but the enable flag + # always returns to 0 on app start. User / scheduler explicitly turns it + # back on. Mirrors the firmware's RAM-only CupWarmerMode behaviour. + set ::settings(cupwarmer_enable) 0 + # rao request to increase these defaults to 300 (from 120) to aid in pour-overs. Will remove this settings.tdb override in the future, once # everyone's settings.tdb has had time to save this new default @@ -1592,7 +1598,7 @@ proc load_settings {} { set ::settings(log_enabled) True - blt::vector create espresso_elapsed god_espresso_elapsed god_espresso_pressure steam_pressure steam_temperature steam_temperature100th steam_flow steam_flow_goal steam_elapsed espresso_pressure espresso_flow god_espresso_flow espresso_flow_weight god_espresso_flow_weight espresso_flow_weight_2x god_espresso_flow_weight_2x espresso_flow_2x god_espresso_flow_2x espresso_flow_delta espresso_pressure_delta espresso_temperature_mix espresso_temperature_basket espresso_temperature_basket10th god_espresso_temperature_basket espresso_state_change espresso_pressure_goal espresso_flow_goal espresso_flow_goal_2x espresso_temperature_goal espresso_temperature_goal10th espresso_weight espresso_weight_chartable espresso_resistance_weight espresso_resistance + blt::vector create espresso_elapsed god_espresso_elapsed god_espresso_pressure steam_pressure steam_temperature steam_temperature100th steam_flow steam_flow_goal steam_elapsed steam_milk_temperature espresso_pressure espresso_flow god_espresso_flow espresso_flow_weight god_espresso_flow_weight espresso_flow_weight_2x god_espresso_flow_weight_2x espresso_flow_2x god_espresso_flow_2x espresso_flow_delta espresso_pressure_delta espresso_temperature_mix espresso_temperature_basket espresso_temperature_basket10th god_espresso_temperature_basket espresso_state_change espresso_pressure_goal espresso_flow_goal espresso_flow_goal_2x espresso_temperature_goal espresso_temperature_goal10th espresso_weight espresso_weight_chartable espresso_resistance_weight espresso_resistance espresso_flow_weight_integrated espresso_weight_integrated espresso_weight_integrated_chartable blt::vector create espresso_de1_explanation_chart_pressure espresso_de1_explanation_chart_flow espresso_de1_explanation_chart_elapsed espresso_de1_explanation_chart_elapsed_flow espresso_water_dispensed espresso_flow_weight_raw espresso_de1_explanation_chart_temperature espresso_de1_explanation_chart_temperature_10 espresso_de1_explanation_chart_selected_step blt::vector create espresso_de1_explanation_chart_flow_1 espresso_de1_explanation_chart_elapsed_flow_1 espresso_de1_explanation_chart_flow_2 espresso_de1_explanation_chart_elapsed_flow_2 espresso_de1_explanation_chart_flow_3 espresso_de1_explanation_chart_elapsed_flow_3 blt::vector create espresso_de1_explanation_chart_elapsed_1 espresso_de1_explanation_chart_elapsed_2 espresso_de1_explanation_chart_elapsed_3 espresso_de1_explanation_chart_pressure_1 espresso_de1_explanation_chart_pressure_2 espresso_de1_explanation_chart_pressure_3 diff --git a/de1plus/vars.tcl b/de1plus/vars.tcl index 40df752f..e20078fb 100644 --- a/de1plus/vars.tcl +++ b/de1plus/vars.tcl @@ -39,6 +39,9 @@ proc clear_espresso_chart {} { espresso_flow_goal_2x length 0 espresso_temperature_goal length 0 espresso_temperature_goal10th length 0 + espresso_flow_weight_integrated length 0 + espresso_weight_integrated length 0 + espresso_weight_integrated_chartable length 0 espresso_de1_explanation_chart_elapsed length 0 espresso_de1_explanation_chart_elapsed_1 length 0 @@ -77,6 +80,10 @@ proc clear_espresso_chart {} { espresso_temperature_goal append [return_temperature_number $::settings(espresso_temperature)] espresso_temperature_goal10th append [round_to_two_digits [expr {[return_temperature_number $::settings(espresso_temperature)] / 10.0}]] + espresso_flow_weight_integrated append 0 + espresso_weight_integrated append 0 + espresso_weight_integrated_chartable append 0 + god_shot_reference_reset catch { @@ -87,7 +94,7 @@ proc clear_espresso_chart {} { } proc espresso_chart_structures {} { - return [list espresso_elapsed espresso_pressure espresso_weight espresso_weight_chartable espresso_flow espresso_flow_weight espresso_flow_weight_raw espresso_water_dispensed espresso_flow_weight_2x espresso_flow_2x espresso_resistance espresso_resistance_weight espresso_pressure_delta espresso_flow_delta espresso_flow_delta_negative espresso_flow_delta_negative_2x espresso_temperature_mix espresso_temperature_basket espresso_state_change espresso_pressure_goal espresso_flow_goal espresso_flow_goal_2x espresso_temperature_goal espresso_temperature_goal10th espresso_de1_explanation_chart_flow espresso_de1_explanation_chart_elapsed_flow espresso_de1_explanation_chart_flow_2x espresso_de1_explanation_chart_flow_1_2x espresso_de1_explanation_chart_flow_2_2x espresso_de1_explanation_chart_flow_3_2x espresso_de1_explanation_chart_pressure espresso_de1_explanation_chart_temperature espresso_de1_explanation_chart_temperature_10 espresso_de1_explanation_chart_pressure_1 espresso_de1_explanation_chart_pressure_2 espresso_de1_explanation_chart_pressure_3 espresso_de1_explanation_chart_elapsed_flow espresso_de1_explanation_chart_elapsed_flow_1 espresso_de1_explanation_chart_elapsed_flow_2 espresso_de1_explanation_chart_elapsed_flow_3 espresso_de1_explanation_chart_elapsed espresso_de1_explanation_chart_elapsed_1 espresso_de1_explanation_chart_elapsed_2 espresso_de1_explanation_chart_elapsed_3] + return [list espresso_elapsed espresso_pressure espresso_weight espresso_weight_chartable espresso_flow espresso_flow_weight espresso_flow_weight_raw espresso_water_dispensed espresso_flow_weight_2x espresso_flow_2x espresso_resistance espresso_resistance_weight espresso_pressure_delta espresso_flow_delta espresso_flow_delta_negative espresso_flow_delta_negative_2x espresso_temperature_mix espresso_temperature_basket espresso_state_change espresso_pressure_goal espresso_flow_goal espresso_flow_goal_2x espresso_temperature_goal espresso_temperature_goal10th espresso_flow_weight_integrated espresso_weight_integrated espresso_weight_integrated_chartable espresso_de1_explanation_chart_flow espresso_de1_explanation_chart_elapsed_flow espresso_de1_explanation_chart_flow_2x espresso_de1_explanation_chart_flow_1_2x espresso_de1_explanation_chart_flow_2_2x espresso_de1_explanation_chart_flow_3_2x espresso_de1_explanation_chart_pressure espresso_de1_explanation_chart_temperature espresso_de1_explanation_chart_temperature_10 espresso_de1_explanation_chart_pressure_1 espresso_de1_explanation_chart_pressure_2 espresso_de1_explanation_chart_pressure_3 espresso_de1_explanation_chart_elapsed_flow espresso_de1_explanation_chart_elapsed_flow_1 espresso_de1_explanation_chart_elapsed_flow_2 espresso_de1_explanation_chart_elapsed_flow_3 espresso_de1_explanation_chart_elapsed espresso_de1_explanation_chart_elapsed_1 espresso_de1_explanation_chart_elapsed_2 espresso_de1_explanation_chart_elapsed_3] } proc backup_espresso_chart {} { @@ -1112,7 +1119,9 @@ proc pouring_timer_text {} { return "[return_liquid_measurement [round_to_integer $::settings(final_desired_shot_volume_advanced)]] < [translate {pouring}] [translate {s}][espresso_elapsed_timer]" } - if {$::settings(scale_bluetooth_address) == "" && $::settings(final_desired_shot_volume) > 0 && ($::settings(settings_profile_type) == "settings_2a" || $::settings(settings_profile_type) == "settings_2b")} { + # Volume-based fallback only when there's no scale AT ALL + # (no external BLE scale AND no Bengle integrated scale). + if {$::settings(scale_bluetooth_address) == "" && ![::de1::packet::use_ble_v2] && $::settings(final_desired_shot_volume) > 0 && ($::settings(settings_profile_type) == "settings_2a" || $::settings(settings_profile_type) == "settings_2b")} { return "[translate {s}][espresso_pour_timer] [translate {pouring}] < [return_liquid_measurement [round_to_integer $::settings(final_desired_shot_volume)]]" } else { @@ -1124,7 +1133,7 @@ proc pouring_timer_text {} { return "[espresso_elapsed_timer][translate {s}] [translate {pouring}] < [return_liquid_measurement [round_to_integer $::settings(final_desired_shot_volume_advanced)]]" } - if {$::settings(scale_bluetooth_address) == "" && $::settings(final_desired_shot_volume) > 0 && ($::settings(settings_profile_type) == "settings_2a" || $::settings(settings_profile_type) == "settings_2b")} { + if {$::settings(scale_bluetooth_address) == "" && ![::de1::packet::use_ble_v2] && $::settings(final_desired_shot_volume) > 0 && ($::settings(settings_profile_type) == "settings_2a" || $::settings(settings_profile_type) == "settings_2b")} { return "[espresso_pour_timer][translate {s}] [translate {pouring}] < [return_liquid_measurement [round_to_integer $::settings(final_desired_shot_volume)]]" } return "[espresso_pour_timer][translate {s}] [translate {pouring}]" @@ -1197,14 +1206,14 @@ proc waterweightflow_text {} { #return [return_flow_weight_measurement [expr {(rand() * 6)}]] } - if {$::de1(scale_weight) == "" || [ifexists ::settings(scale_bluetooth_address)] == ""} { + if {$::de1(scale_weight) == "" || ([ifexists ::settings(scale_bluetooth_address)] == "" && ![::de1::packet::use_ble_v2])} { return "" } return [return_flow_weight_measurement $::de1(scale_weight_rate)] } proc finalwaterweight_text {} { - if {$::de1(scale_weight) == "" || [ifexists ::settings(scale_bluetooth_address)] == ""} { + if {$::de1(scale_weight) == "" || ([ifexists ::settings(scale_bluetooth_address)] == "" && ![::de1::packet::use_ble_v2])} { return "" } @@ -1235,7 +1244,7 @@ proc dump_stack {args} { #trace add variable de1(final_water_weight) write dump_stack proc waterweight_text {} { - if {$::de1(scale_weight) == "" || [ifexists ::settings(scale_bluetooth_address)] == ""} { + if {$::de1(scale_weight) == "" || ([ifexists ::settings(scale_bluetooth_address)] == "" && ![::de1::packet::use_ble_v2])} { return "" } @@ -1259,7 +1268,7 @@ proc waterweight_text {} { #return [return_weight_measurement [expr {round((rand() * 20))}]] } - if {$::de1(scale_device_handle) == "0"} { + if {$::de1(scale_device_handle) == "0" && ![::de1::packet::use_ble_v2]} { return [translate "Disconnected"] } @@ -1269,6 +1278,10 @@ proc waterweight_text {} { } proc waterweight_label_text {} { + # Bengle v2: integrated scale is always present — skip BLE-scale checks + if {[::de1::packet::use_ble_v2]} { + return [translate "Weight"] + } if {[ifexists ::settings(scale_bluetooth_address)] == ""} { return "" } @@ -1365,6 +1378,18 @@ proc steamtemp_text {{integer 0}} { return [return_temperature_measurement [steamtemp] $integer] } +proc milktemp {} { + return [ifexists ::de1(milk_temperature)] +} + +proc milktemp_text {{integer 0}} { + set t [milktemp] + if {$t == "" || $t == 0} { + return "" + } + return [return_temperature_measurement $t $integer] +} + proc pressure_text {} { if {$::de1(language_rtl) == 1} { return [subst {[translate "bar"] [commify [round_to_one_digits [pressure]]]}]