diff --git a/scripts/auto_firmware_version.py b/scripts/auto_firmware_version.py
index b617df9aa..907b592f0 100644
--- a/scripts/auto_firmware_version.py
+++ b/scripts/auto_firmware_version.py
@@ -11,7 +11,7 @@ def get_firmware_specifier_build_flag():
return build_flag
def get_time_specifier_build_flag():
- build_timestamp = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
+ build_timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
build_flag = "#define BUILD_TIMESTAMP \"" + build_timestamp + "\""
print ("Build date: " + build_timestamp)
return build_flag
diff --git a/web/src/components/Navigation.jsx b/web/src/components/Navigation.jsx
index ba0f7a591..445fbafd8 100644
--- a/web/src/components/Navigation.jsx
+++ b/web/src/components/Navigation.jsx
@@ -3,10 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHome } from '@fortawesome/free-solid-svg-icons/faHome';
import { faList } from '@fortawesome/free-solid-svg-icons/faList';
import { faTimeline } from '@fortawesome/free-solid-svg-icons/faTimeline';
-import { faTemperatureHalf } from '@fortawesome/free-solid-svg-icons/faTemperatureHalf';
-import { faBluetoothB } from '@fortawesome/free-brands-svg-icons/faBluetoothB';
import { faCog } from '@fortawesome/free-solid-svg-icons/faCog';
-import { faRotate } from '@fortawesome/free-solid-svg-icons/faRotate';
import { faMagnifyingGlassChart } from '@fortawesome/free-solid-svg-icons/faMagnifyingGlassChart';
import { faChartSimple } from '@fortawesome/free-solid-svg-icons/faChartSimple';
import { faCircleChevronLeft } from '@fortawesome/free-solid-svg-icons/faCircleChevronLeft';
@@ -60,16 +57,9 @@ const NAVIGATION_SECTIONS = [
id: 'devices',
showDivider: true,
items: [
- { label: 'PID Autotune', link: '/pidtune', icon: faTemperatureHalf },
- { label: 'Bluetooth Devices', link: '/scales', icon: faBluetoothB },
{ label: 'Settings', link: '/settings', icon: faCog },
],
},
- {
- id: 'updates',
- showDivider: true,
- items: [{ label: 'System & Updates', link: '/ota', icon: faRotate }],
- },
];
function MenuItem({ collapsed = false, icon, isNew = false, label, link }) {
diff --git a/web/src/index.jsx b/web/src/index.jsx
index caaf883e9..da67a323b 100644
--- a/web/src/index.jsx
+++ b/web/src/index.jsx
@@ -23,11 +23,8 @@ import { faBars } from '@fortawesome/free-solid-svg-icons/faBars';
const Home = lazy(() => import('./pages/Home/index.jsx').then(m => m.Home));
const NotFound = lazy(() => import('./pages/_404.jsx').then(m => m.NotFound));
const Settings = lazy(() => import('./pages/Settings/index.jsx').then(m => m.Settings));
-const OTA = lazy(() => import('./pages/OTA/index.jsx').then(m => m.OTA));
-const Scales = lazy(() => import('./pages/Scales/index.jsx').then(m => m.Scales));
const ProfileList = lazy(() => import('./pages/ProfileList/index.jsx').then(m => m.ProfileList));
const ProfileEdit = lazy(() => import('./pages/ProfileEdit/index.jsx').then(m => m.ProfileEdit));
-const Autotune = lazy(() => import('./pages/Autotune/index.jsx').then(m => m.Autotune));
const ShotHistory = lazy(() => import('./pages/ShotHistory/index.jsx').then(m => m.ShotHistory));
const ShotAnalyzer = lazy(() => import('./pages/ShotAnalyzer/index.jsx').then(m => m.ShotAnalyzer));
const StatisticsPage = lazy(() =>
@@ -88,9 +85,6 @@ export function App() {
-
-
-
diff --git a/web/src/pages/Autotune/index.jsx b/web/src/pages/Autotune/index.jsx
deleted file mode 100644
index ac8ff43b4..000000000
--- a/web/src/pages/Autotune/index.jsx
+++ /dev/null
@@ -1,218 +0,0 @@
-import { useState, useEffect, useCallback, useContext } from 'preact/hooks';
-import { ApiServiceContext } from '../../services/ApiService.js';
-import { OverviewChart } from '../../components/OverviewChart.jsx';
-import { Spinner } from '../../components/Spinner.jsx';
-import Card from '../../components/Card.jsx';
-
-export function Autotune() {
- const apiService = useContext(ApiServiceContext);
- const [active, setActive] = useState(false);
- const [result, setResult] = useState(null);
- const [failed, setFailed] = useState(false);
- const [time, setTime] = useState(120);
- const [samples, setSamples] = useState(6);
- // Default 680 W matches Gaggia Classic Pro 2019 / E24 boiler element on
- // 230 V. (120 V variant is 570 W; cafeparts.com EF0030-A datasheet.) Used
- // controller-side as combinedKff = TUNER_OUTPUT_SPAN / wattage so the
- // Thermal Feedforward Gain is auto-populated on completion.
- const [wattage, setWattage] = useState(680);
-
- const onStart = useCallback(() => {
- apiService.send({
- tp: 'req:autotune-start',
- time,
- samples,
- wattage,
- });
- setFailed(false);
- setResult(null);
- setActive(true);
- }, [time, samples, wattage, apiService]);
-
- useEffect(() => {
- const resultListener = apiService.on('evt:autotune-result', msg => {
- setActive(false);
- setFailed(false);
- setResult(msg.pid);
- });
- const failedListener = apiService.on('evt:autotune-failed', () => {
- setActive(false);
- setResult(null);
- setFailed(true);
- });
- return () => {
- apiService.off('evt:autotune-result', resultListener);
- apiService.off('evt:autotune-failed', failedListener);
- };
- }, [apiService]);
-
- return (
- <>
-
-
PID Autotune
-
-
-
-
- {active && (
-
-
-
-
-
-
-
- Autotune in Progress
-
-
-
- The boiler will heat at full power until its temperature inflection is detected,
- then the SIMC tuning rule derives PID gains. Typically 1–3 minutes depending on
- machine.
-
-
-
-
- )}
-
- {result && (
-
-
-
-
Autotune Complete!
-
Your new PID values have been saved successfully.
-
-
-
-
- )}
-
- {failed && (
-
-
-
-
Autotune Failed
-
- No valid gains were produced. Your existing PID settings have been preserved.
- Try increasing Test Duration or confirm the boiler was cold at start.
-
-
-
-
- )}
-
- {!active && !result && !failed && (
-
-
-
- Please ensure the boiler temperature is below 50°C before starting the autotune
- process.
-
-
-
-
-
-
- Test Duration (seconds)
-
-
setTime(Number.parseInt(e.target.value, 10) || 0)}
- placeholder='120'
- />
-
- Upper bound on the identification test. Most espresso boilers resolve within
- 60–120 s. Extend if Autotune fails before peak slope is detected.
-
-
-
-
-
- Slope Window
-
-
setSamples(Number.parseInt(e.target.value, 10) || 4)}
- placeholder='6'
- />
-
- Moving-window length (samples) used for slope estimation. Larger values smooth
- MAX31855 quantisation but lag the inflection. 6 is the sweet spot.
-
-
-
-
-
- Heater Wattage (W)
-
-
setWattage(Number.parseInt(e.target.value, 10) || 0)}
- placeholder='680'
- />
-
- Boiler heating element wattage. Defaults: Gaggia Classic Pro 2019 / E24 = 1370
- W; Rancilio Silvia ≈ 1100 W. Used to derive Thermal Feedforward Gain after
- autotune completes.
-
-
-
-
- )}
-
-
-
-
-
- {!active && !result && !failed && (
- 300 ||
- samples < 4 ||
- samples > 20 ||
- wattage < 300 ||
- wattage > 1500
- }
- >
- Start Autotune
-
- )}
-
- {result && (
- setResult(null)}>
- Back to Settings
-
- )}
-
- {failed && (
- setFailed(false)}>
- Back to Settings
-
- )}
-
-
- >
- );
-}
diff --git a/web/src/pages/OTA/index.jsx b/web/src/pages/OTA/index.jsx
deleted file mode 100644
index 924c9245c..000000000
--- a/web/src/pages/OTA/index.jsx
+++ /dev/null
@@ -1,370 +0,0 @@
-import { useCallback, useContext, useEffect, useRef, useState } from 'preact/hooks';
-import Card from '../../components/Card.jsx';
-import { Spinner } from '../../components/Spinner.jsx';
-import { ApiServiceContext } from '../../services/ApiService.js';
-import { downloadJson } from '../../utils/download.js';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faCheck } from '@fortawesome/free-solid-svg-icons/faCheck';
-import { machine } from '../../services/ApiService.js';
-import { computed } from '@preact/signals';
-
-const imageUrlToBase64 = async blob => {
- return new Promise((onSuccess, onError) => {
- try {
- const reader = new FileReader();
- reader.onload = function () {
- onSuccess(this.result);
- };
- reader.readAsDataURL(blob);
- } catch (e) {
- onError(e);
- }
- });
-};
-
-const connected = computed(() => machine.value.connected);
-
-export function OTA() {
- const apiService = useContext(ApiServiceContext);
- const [isLoading, setIsLoading] = useState(true);
- const [submitting, setSubmitting] = useState(false);
- const [formData, setFormData] = useState({});
- const [phase, setPhase] = useState(0);
- const [progress, setProgress] = useState(0);
- const rssi = machine.value.status.rssi;
- const lat = machine.value.status.lat;
-
- const downloadSupportData = useCallback(async () => {
- const settingsResponse = await fetch(`/api/settings`);
- const data = await settingsResponse.json();
- delete data.wifiPassword;
- delete data.haPassword;
- const coredumpBlob = await fetch(`/api/core-dump`).then(r => r.blob());
- let coredump = await imageUrlToBase64(coredumpBlob);
- coredump = coredump.substring(coredump.indexOf('base64,') + 7);
- const supportFile = {
- settings: data,
- versions: formData,
- coredump,
- };
- const ts = Date.now();
- downloadJson(supportFile, `support-${ts}.dat`);
- }, [formData]);
- useEffect(() => {
- const listenerId = apiService.on('res:ota-settings', msg => {
- setFormData(msg);
- setIsLoading(false);
- setSubmitting(false);
- });
- return () => {
- apiService.off('res:ota-settings', listenerId);
- };
- }, [apiService]);
- useEffect(() => {
- const listenerId = apiService.on('evt:ota-progress', msg => {
- setProgress(msg.progress);
- setPhase(msg.phase);
- });
- return () => {
- apiService.off('evt:ota-progress', listenerId);
- };
- }, [apiService]);
-
- useEffect(() => {
- const listenerId = apiService.on('evt:history-rebuild-progress', msg => {
- setRebuildProgress({
- total: msg.total || 0,
- current: msg.current || 0,
- status: msg.status || '',
- });
-
- if (msg.status === 'completed' || msg.status === 'error') {
- setRebuilding(false);
- setRebuilt(msg.status === 'completed');
- }
- });
- return () => {
- apiService.off('evt:history-rebuild-progress', listenerId);
- };
- }, [apiService]);
- useEffect(() => {
- if (connected.value) {
- apiService.send({ tp: 'req:ota-settings' });
- }
- }, [apiService, connected.value]);
-
- const formRef = useRef();
-
- const onSubmit = useCallback(
- async e => {
- e.preventDefault();
- setSubmitting(true);
- const form = formRef.current;
- const formData = new FormData(form);
- apiService.send({ tp: 'req:ota-settings', update: true, channel: formData.get('channel') });
- setSubmitting(true);
- },
- [setFormData, formRef],
- );
-
- const onUpdate = useCallback(
- component => {
- apiService.send({ tp: 'req:ota-start', cp: component });
- },
- [apiService],
- );
-
- const [rebuilding, setRebuilding] = useState(false);
- const [rebuilt, setRebuilt] = useState(false);
- const [rebuildProgress, setRebuildProgress] = useState({ total: 0, current: 0, status: '' });
- const onHistoryRebuild = useCallback(async () => {
- setRebuilt(false);
- setRebuilding(true);
- setRebuildProgress({ total: 0, current: 0, status: 'starting' });
- apiService.send({ tp: 'req:history:rebuild' });
- }, [apiService]);
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (phase > 0) {
- return (
-
-
-
- {phase === 1
- ? 'Updating Display firmware'
- : phase === 2
- ? 'Updating Display filesystem'
- : phase === 3
- ? 'Updating controller firmware'
- : 'Finished'}
-
-
{phase === 4 ? 100 : progress}%
- {phase === 4 && (
-
- Back
-
- )}
-
- );
- }
-
- return (
- <>
-
-
System & Updates
-
-
-