Переглянути джерело

calibration: auto-restart controller after direction fix

Direction pin changes only take effect after a FluidNC reboot, so
"Fix Automatically" now sends $Bye after saving. Wrong axes are
patched in a single request (the toggle semantics make per-axis
restarts unsafe when both are inverted), the manual restart button
is removed, and verification is gated on reconnection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tuanchris 1 місяць тому
батько
коміт
968d9163bc

+ 34 - 56
frontend/src/pages/SetupPage.tsx

@@ -107,37 +107,29 @@ function CalibrationWizard() {
     }
   }, [])
 
-  const fixDirection = useCallback(
-    async (axis: 'x' | 'y') => {
-      setWizard((w) => ({ ...w, fixing: true }))
-      try {
-        // The backend treats direction_inverted as a toggle command — the
-        // value is ignored and the pin's :low modifier is flipped each call
-        await apiClient.patch('/api/fluidnc/config', {
-          axes: { [axis]: { direction_inverted: true } },
-        })
-        toast.success(`${axis.toUpperCase()} direction toggled and saved`)
-      } catch (err) {
-        toast.error(`Fix failed: ${err instanceof Error ? err.message : 'Unknown'}`)
-      } finally {
-        setWizard((w) => ({ ...w, fixing: false }))
-      }
-    },
-    []
-  )
-
-  const restartController = useCallback(async () => {
+  const fixDirections = useCallback(async () => {
     setWizard((w) => ({ ...w, fixing: true }))
     try {
-      await apiClient.post('/api/fluidnc/command', { command: '$Bye', timeout: 2.0 })
-      toast.success('Restart command sent. Reconnect when ready.')
-    } catch {
-      // $Bye causes disconnect, so errors are expected
-      toast.info('Restart command sent. The controller will reboot.')
+      // The backend treats direction_inverted as a toggle command — the
+      // value is ignored and the pin's :low modifier is flipped each call.
+      // Patch every wrong axis in one request so a single restart suffices.
+      const axes: Record<string, { direction_inverted: boolean }> = {}
+      if (wizard.yCorrect === false) axes.y = { direction_inverted: true }
+      if (wizard.xCorrect === false) axes.x = { direction_inverted: true }
+      await apiClient.patch('/api/fluidnc/config', { axes })
+      try {
+        // Direction pin changes only take effect after a reboot
+        await apiClient.post('/api/fluidnc/command', { command: '$Bye', timeout: 2.0 })
+      } catch {
+        // $Bye drops the connection, so an error here is expected
+      }
+      toast.success('Direction toggled and saved. Controller is restarting — wait for it to reconnect.')
+    } catch (err) {
+      toast.error(`Fix failed: ${err instanceof Error ? err.message : 'Unknown'}`)
     } finally {
       setWizard((w) => ({ ...w, fixing: false }))
     }
-  }, [])
+  }, [wizard.yCorrect, wizard.xCorrect])
 
   const waitForIdle = useCallback(async (timeoutMs = 30000) => {
     const start = Date.now()
@@ -350,52 +342,38 @@ function CalibrationWizard() {
         <div className="space-y-4">
           <h3 className="font-semibold">Fix Motor Directions</h3>
           <p className="text-sm text-muted-foreground">
-            The following axes need their direction inverted. Click to auto-fix by toggling the <code>:low</code> flag on the direction pin.
+            The following axes need their direction inverted. Auto-fix toggles the <code>:low</code> flag on the direction pin.
           </p>
           <div className="space-y-3">
             {wizard.yCorrect === false && (
-              <div className="flex items-center justify-between p-3 rounded-lg border">
-                <div>
-                  <p className="font-medium text-sm">Y Axis (Radial)</p>
-                  <p className="text-xs text-muted-foreground">Direction is inverted</p>
-                </div>
-                <Button
-                  size="sm"
-                  onClick={() => fixDirection('y')}
-                  disabled={wizard.fixing}
-                >
-                  {wizard.fixing ? 'Fixing...' : 'Fix Automatically'}
-                </Button>
+              <div className="p-3 rounded-lg border">
+                <p className="font-medium text-sm">Y Axis (Radial)</p>
+                <p className="text-xs text-muted-foreground">Direction is inverted</p>
               </div>
             )}
             {wizard.xCorrect === false && (
-              <div className="flex items-center justify-between p-3 rounded-lg border">
-                <div>
-                  <p className="font-medium text-sm">X Axis (Angular)</p>
-                  <p className="text-xs text-muted-foreground">Direction is inverted</p>
-                </div>
-                <Button
-                  size="sm"
-                  onClick={() => fixDirection('x')}
-                  disabled={wizard.fixing}
-                >
-                  {wizard.fixing ? 'Fixing...' : 'Fix Automatically'}
-                </Button>
+              <div className="p-3 rounded-lg border">
+                <p className="font-medium text-sm">X Axis (Angular)</p>
+                <p className="text-xs text-muted-foreground">Direction is inverted</p>
               </div>
             )}
           </div>
           <Alert>
             <span className="material-icons-outlined text-base mr-2 shrink-0">warning</span>
             <AlertDescription>
-              Direction pin changes require a controller restart to take effect. After fixing, restart the controller below, then re-run the wizard to verify.
+              Direction pin changes require a controller restart to take effect. Fixing automatically restarts the controller — wait for it to reconnect before continuing.
             </AlertDescription>
           </Alert>
           <div className="flex gap-3">
-            <Button variant="outline" onClick={restartController} disabled={wizard.fixing}>
-              <span className="material-icons-outlined mr-2 text-base">restart_alt</span>
-              Restart Controller
+            <Button onClick={fixDirections} disabled={wizard.fixing}>
+              <span className="material-icons-outlined mr-2 text-base">build</span>
+              {wizard.fixing ? 'Fixing...' : 'Fix Automatically'}
             </Button>
-            <Button onClick={() => setWizard((w) => ({ ...w, step: 'sanity-y' }))}>
+            <Button
+              variant="outline"
+              onClick={() => setWizard((w) => ({ ...w, step: 'sanity-y' }))}
+              disabled={wizard.fixing || !isConnected}
+            >
               Continue to Verification
             </Button>
           </div>

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/BrowsePage-CCSAcHrC.js


+ 1 - 1
static/dist/assets/ConnectionBanner-CCnOHE5N.js → static/dist/assets/ConnectionBanner-BwHQi9kR.js

@@ -1 +1 @@
-import{j as e,L as t}from"./react-DnK_pEU5.js";import{u as s}from"./index-B3c97KGp.js";function r(){return s(n=>!!n.status?.connection_status)}function c(){return r()?null:e.jsxs("div",{role:"status",className:"flex items-center gap-2.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2.5 text-sm text-amber-700 dark:text-amber-400",children:[e.jsx("span",{className:"material-icons-outlined text-base shrink-0","aria-hidden":"true",children:"usb_off"}),e.jsxs("span",{className:"min-w-0",children:["Table not connected — patterns can't play."," ",e.jsx(t,{to:"/settings?section=connection",className:"font-medium underline underline-offset-2 hover:text-amber-800 dark:hover:text-amber-300",children:"Connect"})]})]})}export{c as C,r as u};
+import{j as e,L as t}from"./react-DnK_pEU5.js";import{u as s}from"./index-LUV-rsLw.js";function r(){return s(n=>!!n.status?.connection_status)}function c(){return r()?null:e.jsxs("div",{role:"status",className:"flex items-center gap-2.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2.5 text-sm text-amber-700 dark:text-amber-400",children:[e.jsx("span",{className:"material-icons-outlined text-base shrink-0","aria-hidden":"true",children:"usb_off"}),e.jsxs("span",{className:"min-w-0",children:["Table not connected — patterns can't play."," ",e.jsx(t,{to:"/settings?section=connection",className:"font-medium underline underline-offset-2 hover:text-amber-800 dark:hover:text-amber-300",children:"Connect"})]})]})}export{c as C,r as u};

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/LEDPage-CFgLVr-r.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/PlaylistsPage-CNDzttwl.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/SettingsPage-CGwFLj86.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/SetupPage-C5AL1838.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/SetupPage-w_FtYlgJ.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/TableControlPage-Cd_LBo1I.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/WiFiSetupPage-CuL2IPBv.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/accordion-tcZ-azK3.js


+ 1 - 1
static/dist/assets/alert-CjHh5uAz.js → static/dist/assets/alert-nibgKJd5.js

@@ -1 +1 @@
-import{r as a,j as s}from"./react-DnK_pEU5.js";import{c as i,b as d}from"./index-B3c97KGp.js";const o=d("relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",{variants:{variant:{default:"bg-muted/50 text-foreground",destructive:"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"}},defaultVariants:{variant:"default"}}),n=a.forwardRef(({className:t,variant:e,...r},l)=>s.jsx("div",{ref:l,role:"alert",className:i(o({variant:e}),t),...r}));n.displayName="Alert";const v=a.forwardRef(({className:t,...e},r)=>s.jsx("h5",{ref:r,className:i("mb-1 font-medium leading-none tracking-tight",t),...e}));v.displayName="AlertTitle";const c=a.forwardRef(({className:t,...e},r)=>s.jsx("div",{ref:r,className:i("text-sm [&_p]:leading-relaxed",t),...e}));c.displayName="AlertDescription";export{n as A,c as a};
+import{r as a,j as s}from"./react-DnK_pEU5.js";import{c as i,b as d}from"./index-LUV-rsLw.js";const o=d("relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",{variants:{variant:{default:"bg-muted/50 text-foreground",destructive:"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"}},defaultVariants:{variant:"default"}}),n=a.forwardRef(({className:t,variant:e,...r},l)=>s.jsx("div",{ref:l,role:"alert",className:i(o({variant:e}),t),...r}));n.displayName="Alert";const v=a.forwardRef(({className:t,...e},r)=>s.jsx("h5",{ref:r,className:i("mb-1 font-medium leading-none tracking-tight",t),...e}));v.displayName="AlertTitle";const c=a.forwardRef(({className:t,...e},r)=>s.jsx("div",{ref:r,className:i("text-sm [&_p]:leading-relaxed",t),...e}));c.displayName="AlertDescription";export{n as A,c as a};

+ 1 - 1
static/dist/assets/card-DweoKf_j.js → static/dist/assets/card-CF36l7bR.js

@@ -1 +1 @@
-import{r as s,j as d}from"./react-DnK_pEU5.js";import{c as t}from"./index-B3c97KGp.js";const o=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("rounded-lg border bg-card text-card-foreground shadow-sm",a),...e}));o.displayName="Card";const i=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("flex flex-col space-y-1.5 p-6",a),...e}));i.displayName="CardHeader";const c=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("text-2xl font-semibold leading-none tracking-tight",a),...e}));c.displayName="CardTitle";const n=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("text-sm text-muted-foreground",a),...e}));n.displayName="CardDescription";const l=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("p-6 pt-0",a),...e}));l.displayName="CardContent";const m=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("flex items-center p-6 pt-0",a),...e}));m.displayName="CardFooter";export{o as C,i as a,c as b,n as c,l as d};
+import{r as s,j as d}from"./react-DnK_pEU5.js";import{c as t}from"./index-LUV-rsLw.js";const o=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("rounded-lg border bg-card text-card-foreground shadow-sm",a),...e}));o.displayName="Card";const i=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("flex flex-col space-y-1.5 p-6",a),...e}));i.displayName="CardHeader";const c=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("text-2xl font-semibold leading-none tracking-tight",a),...e}));c.displayName="CardTitle";const n=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("text-sm text-muted-foreground",a),...e}));n.displayName="CardDescription";const l=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("p-6 pt-0",a),...e}));l.displayName="CardContent";const m=s.forwardRef(({className:a,...e},r)=>d.jsx("div",{ref:r,className:t("flex items-center p-6 pt-0",a),...e}));m.displayName="CardFooter";export{o as C,i as a,c as b,n as c,l as d};

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/index-CLGWHFf2.js


Різницю між файлами не показано, бо вона завелика
+ 1 - 1
static/dist/assets/index-LUV-rsLw.js


+ 1 - 1
static/dist/assets/label-CasawWzc.js → static/dist/assets/label-CyGXWE6X.js

@@ -1 +1 @@
-import{r as o,j as i}from"./react-DnK_pEU5.js";import{a4 as c,c as p,b}from"./index-B3c97KGp.js";var v=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],w=v.reduce((t,a)=>{const e=c(`Primitive.${a}`),r=o.forwardRef((n,d)=>{const{asChild:m,...f}=n,u=m?e:a;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),i.jsx(u,{...f,ref:d})});return r.displayName=`Primitive.${a}`,{...t,[a]:r}},{}),x="Label",s=o.forwardRef((t,a)=>i.jsx(w.label,{...t,ref:a,onMouseDown:e=>{e.target.closest("button, input, select, textarea")||(t.onMouseDown?.(e),!e.defaultPrevented&&e.detail>1&&e.preventDefault())}}));s.displayName=x;var l=s;const N=b("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"),y=o.forwardRef(({className:t,...a},e)=>i.jsx(l,{ref:e,className:p(N(),t),...a}));y.displayName=l.displayName;export{y as L};
+import{r as o,j as i}from"./react-DnK_pEU5.js";import{a4 as c,c as p,b}from"./index-LUV-rsLw.js";var v=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],w=v.reduce((t,a)=>{const e=c(`Primitive.${a}`),r=o.forwardRef((n,d)=>{const{asChild:m,...f}=n,u=m?e:a;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),i.jsx(u,{...f,ref:d})});return r.displayName=`Primitive.${a}`,{...t,[a]:r}},{}),x="Label",s=o.forwardRef((t,a)=>i.jsx(w.label,{...t,ref:a,onMouseDown:e=>{e.target.closest("button, input, select, textarea")||(t.onMouseDown?.(e),!e.defaultPrevented&&e.detail>1&&e.preventDefault())}}));s.displayName=x;var l=s;const N=b("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"),y=o.forwardRef(({className:t,...a},e)=>i.jsx(l,{ref:e,className:p(N(),t),...a}));y.displayName=l.displayName;export{y as L};

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/select-B_ypzzK2.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/slider-C6r01RE5.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/assets/switch-BTqCTolc.js


+ 1 - 1
static/dist/index.html

@@ -58,7 +58,7 @@
           .catch(function() {});
       })();
     </script>
-    <script type="module" crossorigin src="/assets/index-B3c97KGp.js"></script>
+    <script type="module" crossorigin src="/assets/index-LUV-rsLw.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/react-DnK_pEU5.js">
     <link rel="modulepreload" crossorigin href="/assets/dnd-Bgdma8TD.js">
     <link rel="modulepreload" crossorigin href="/assets/motion-8nKk6_RD.js">

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
static/dist/sw.js


Деякі файли не було показано, через те що забагато файлів було змінено