Просмотр исходного кода

Add table type override setting and collapsible settings sections

- Add table_type_override to allow manual override of auto-detected machine type
- Override affects gear ratio (6.25 for mini variants, 10 for standard) and homing behavior
- Add Machine Settings section to settings page with dropdown for table type selection
- Make all settings sections collapsible with toggle headers
- Device Connection expanded by default, all other sections collapsed
- Smooth CSS transitions for expand/collapse animations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 6 месяцев назад
Родитель
Сommit
28f966d310
5 измененных файлов с 310 добавлено и 33 удалено
  1. 25 0
      main.py
  2. 12 6
      modules/connection/connection_manager.py
  3. 3 0
      modules/core/state.py
  4. 123 0
      static/js/settings.js
  5. 147 27
      templates/settings.html

+ 25 - 0
main.py

@@ -387,6 +387,9 @@ class MqttSettingsUpdate(BaseModel):
     device_id: Optional[str] = None
     device_name: Optional[str] = None
 
+class MachineSettingsUpdate(BaseModel):
+    table_type_override: Optional[str] = None  # Override detected table type, or empty string/"auto" to clear
+
 class SettingsUpdate(BaseModel):
     """Request model for PATCH /api/settings - all fields optional for partial updates"""
     app: Optional[AppSettingsUpdate] = None
@@ -397,6 +400,7 @@ class SettingsUpdate(BaseModel):
     homing: Optional[HomingSettingsUpdate] = None
     led: Optional[LedSettingsUpdate] = None
     mqtt: Optional[MqttSettingsUpdate] = None
+    machine: Optional[MachineSettingsUpdate] = None
 
 # Store active WebSocket connections
 active_status_connections = set()
@@ -553,6 +557,19 @@ async def get_all_settings():
             "discovery_prefix": state.mqtt_discovery_prefix,
             "device_id": state.mqtt_device_id,
             "device_name": state.mqtt_device_name
+        },
+        "machine": {
+            "detected_table_type": state.table_type,
+            "table_type_override": state.table_type_override,
+            "effective_table_type": state.table_type_override or state.table_type,
+            "available_table_types": [
+                {"value": "dune_weaver_mini", "label": "Dune Weaver Mini"},
+                {"value": "dune_weaver_mini_pro", "label": "Dune Weaver Mini Pro"},
+                {"value": "dune_weaver_mini_pro_byj", "label": "Dune Weaver Mini Pro (BYJ)"},
+                {"value": "dune_weaver_gold", "label": "Dune Weaver Gold"},
+                {"value": "dune_weaver", "label": "Dune Weaver"},
+                {"value": "dune_weaver_pro", "label": "Dune Weaver Pro"}
+            ]
         }
     }
 
@@ -704,6 +721,14 @@ async def update_settings(settings_update: SettingsUpdate):
         updated_categories.append("mqtt")
         requires_restart = True
 
+    # Machine settings
+    if settings_update.machine:
+        m = settings_update.machine
+        if m.table_type_override is not None:
+            # Empty string or "auto" clears the override
+            state.table_type_override = None if m.table_type_override in ("", "auto") else m.table_type_override
+        updated_categories.append("machine")
+
     # Save state
     state.save()
 

+ 12 - 6
modules/connection/connection_manager.py

@@ -517,8 +517,11 @@ def get_machine_steps(timeout=10):
             state.table_type = None
             logger.warning(f"Unknown table type with Y steps/mm: {y_steps_per_mm}")
 
-        # Set gear ratio based on table type (hardcoded)
-        if state.table_type in ['dune_weaver_mini', 'dune_weaver_mini_pro', 'dune_weaver_mini_pro_byj', 'dune_weaver_gold']:
+        # Use override if set, otherwise use detected table type
+        effective_table_type = state.table_type_override or state.table_type
+
+        # Set gear ratio based on effective table type (hardcoded)
+        if effective_table_type in ['dune_weaver_mini', 'dune_weaver_mini_pro', 'dune_weaver_mini_pro_byj', 'dune_weaver_gold']:
             state.gear_ratio = 6.25
         else:
             state.gear_ratio = 10
@@ -528,10 +531,12 @@ def get_machine_steps(timeout=10):
         if gear_ratio_override is not None:
             try:
                 state.gear_ratio = float(gear_ratio_override)
-                logger.info(f"Machine type detected: {state.table_type}, gear ratio: {state.gear_ratio} (from GEAR_RATIO env var)")
+                logger.info(f"Machine type detected: {state.table_type}, effective: {effective_table_type}, gear ratio: {state.gear_ratio} (from GEAR_RATIO env var)")
             except ValueError:
                 logger.error(f"Invalid GEAR_RATIO env var value: {gear_ratio_override}, using default: {state.gear_ratio}")
-                logger.info(f"Machine type detected: {state.table_type}, gear ratio: {state.gear_ratio} (hardcoded)")
+                logger.info(f"Machine type detected: {state.table_type}, effective: {effective_table_type}, gear ratio: {state.gear_ratio} (hardcoded)")
+        elif state.table_type_override:
+            logger.info(f"Machine type detected: {state.table_type}, overridden to: {effective_table_type}, gear ratio: {state.gear_ratio}")
         else:
             logger.info(f"Machine type detected: {state.table_type}, gear ratio: {state.gear_ratio} (hardcoded)")
 
@@ -574,8 +579,9 @@ def home(timeout=90):
 
     def home_internal():
         nonlocal homing_success
+        effective_table_type = state.table_type_override or state.table_type
         homing_speed = 400
-        if state.table_type == 'dune_weaver_mini':
+        if effective_table_type == 'dune_weaver_mini':
             homing_speed = 100
         try:
             if state.homing == 1:
@@ -670,7 +676,7 @@ def home(timeout=90):
                 loop = asyncio.new_event_loop()
                 asyncio.set_event_loop(loop)
                 try:
-                    if state.table_type == 'dune_weaver_mini':
+                    if effective_table_type == 'dune_weaver_mini':
                         result = loop.run_until_complete(send_grbl_coordinates(0, -30, homing_speed, home=True))
                         if result == False:
                             logger.error("Crash homing failed - send_grbl_coordinates returned False")

+ 3 - 0
modules/core/state.py

@@ -84,6 +84,7 @@ class AppState:
         self.dw_led_last_activity_time = None  # Last activity timestamp (runtime only, not persisted)
         self.skip_requested = False
         self.table_type = None
+        self.table_type_override = None  # User override for table type detection
         self._playlist_mode = "loop"
         self._pause_time = 0
         self._clear_pattern = "none"
@@ -285,6 +286,7 @@ class AppState:
             "mqtt_discovery_prefix": self.mqtt_discovery_prefix,
             "mqtt_device_id": self.mqtt_device_id,
             "mqtt_device_name": self.mqtt_device_name,
+            "table_type_override": self.table_type_override,
         }
 
     def from_dict(self, data):
@@ -369,6 +371,7 @@ class AppState:
         self.mqtt_discovery_prefix = data.get("mqtt_discovery_prefix", "homeassistant")
         self.mqtt_device_id = data.get("mqtt_device_id", "dune_weaver")
         self.mqtt_device_name = data.get("mqtt_device_name", "Dune Weaver")
+        self.table_type_override = data.get("table_type_override", None)
 
     def save(self):
         """Save the current state to a JSON file."""

+ 123 - 0
static/js/settings.js

@@ -1,3 +1,25 @@
+// ============================================================================
+// Collapsible Section Toggle
+// ============================================================================
+
+function toggleSection(headerElement) {
+    const contentElement = headerElement.nextElementSibling;
+
+    if (headerElement.classList.contains('collapsed')) {
+        // Expand
+        headerElement.classList.remove('collapsed');
+        contentElement.classList.remove('collapsed');
+    } else {
+        // Collapse
+        headerElement.classList.add('collapsed');
+        contentElement.classList.add('collapsed');
+    }
+}
+
+// ============================================================================
+// Constants and Utilities
+// ============================================================================
+
 // Constants for log message types
 const LOG_TYPE = {
     SUCCESS: 'success',
@@ -2282,4 +2304,105 @@ async function initializeMqttConfig() {
 // Initialize MQTT config when DOM is ready
 document.addEventListener('DOMContentLoaded', function() {
     initializeMqttConfig();
+    initializeTableTypeConfig();
 });
+
+// ============================================================================
+// Table Type Configuration
+// ============================================================================
+
+function initializeTableTypeConfig() {
+    const tableTypeSelect = document.getElementById('tableTypeSelect');
+    const saveTableTypeButton = document.getElementById('saveTableType');
+    const detectedTableType = document.getElementById('detectedTableType');
+
+    if (!tableTypeSelect || !saveTableTypeButton) {
+        logMessage('Table type elements not found', LOG_TYPE.WARNING);
+        return;
+    }
+
+    // Load current settings
+    loadTableTypeSettings();
+
+    // Save button click handler
+    saveTableTypeButton.addEventListener('click', saveTableTypeConfig);
+
+    async function loadTableTypeSettings() {
+        try {
+            const response = await fetch('/api/settings');
+            if (!response.ok) throw new Error('Failed to fetch settings');
+
+            const settings = await response.json();
+            const machine = settings.machine || {};
+
+            // Populate dropdown with available table types
+            tableTypeSelect.innerHTML = '<option value="">Auto-detect (use detected type)</option>';
+            if (machine.available_table_types) {
+                machine.available_table_types.forEach(type => {
+                    const option = document.createElement('option');
+                    option.value = type.value;
+                    option.textContent = type.label;
+                    tableTypeSelect.appendChild(option);
+                });
+            }
+
+            // Set current override value
+            if (machine.table_type_override) {
+                tableTypeSelect.value = machine.table_type_override;
+            } else {
+                tableTypeSelect.value = '';
+            }
+
+            // Update detected type display
+            if (detectedTableType) {
+                const detected = machine.detected_table_type;
+                if (detected) {
+                    // Find the label for the detected type
+                    const typeInfo = machine.available_table_types?.find(t => t.value === detected);
+                    detectedTableType.textContent = typeInfo ? typeInfo.label : detected;
+                } else {
+                    detectedTableType.textContent = 'Not connected';
+                }
+            }
+
+            logMessage('Table type settings loaded', LOG_TYPE.DEBUG);
+        } catch (error) {
+            logMessage(`Error loading table type settings: ${error.message}`, LOG_TYPE.ERROR);
+        }
+    }
+
+    async function saveTableTypeConfig() {
+        const originalButtonHTML = saveTableTypeButton.innerHTML;
+        saveTableTypeButton.disabled = true;
+        saveTableTypeButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
+
+        try {
+            const response = await fetch('/api/settings', {
+                method: 'PATCH',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({
+                    machine: {
+                        table_type_override: tableTypeSelect.value || ''
+                    }
+                })
+            });
+
+            if (!response.ok) throw new Error('Failed to save settings');
+
+            const result = await response.json();
+            if (result.success) {
+                showStatusMessage('Table type settings saved. Changes will take effect on next connection.', 'success');
+                // Reload to show updated effective type
+                await loadTableTypeSettings();
+            } else {
+                throw new Error('Save failed');
+            }
+        } catch (error) {
+            logMessage(`Error saving table type: ${error.message}`, LOG_TYPE.ERROR);
+            showStatusMessage(`Failed to save table type: ${error.message}`, 'error');
+        } finally {
+            saveTableTypeButton.innerHTML = originalButtonHTML;
+            saveTableTypeButton.disabled = false;
+        }
+    }
+}

+ 147 - 27
templates/settings.html

@@ -203,6 +203,49 @@ input:checked + .slider:before {
   animation: spin 1s linear infinite;
 }
 
+/* Collapsible section styles */
+.section-header {
+  cursor: pointer;
+  user-select: none;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.section-header:hover {
+  background-color: #f8fafc;
+}
+
+.dark .section-header:hover {
+  background-color: #333333;
+}
+
+.section-toggle-icon {
+  transition: transform 0.2s ease-in-out;
+}
+
+.section-header.collapsed .section-toggle-icon {
+  transform: rotate(-90deg);
+}
+
+.section-header.collapsed {
+  border-bottom: none;
+}
+
+.section-content {
+  overflow: hidden;
+  transition: max-height 0.3s ease-in-out, opacity 0.2s ease-in-out, padding 0.2s ease-in-out;
+  max-height: 2000px;
+  opacity: 1;
+}
+
+.section-content.collapsed {
+  max-height: 0;
+  opacity: 0;
+  padding-top: 0 !important;
+  padding-bottom: 0 !important;
+}
+
 /* Time slot specific styles */
 .time-slot-item {
   background-color: #f8fafc;
@@ -303,11 +346,13 @@ input:checked + .slider:before {
   </div>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
-      Device Connection
+      <span>Device Connection</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
-    <div>
+    <div class="section-content">
       <div
         class="flex items-center gap-4 px-6 py-5 hover:bg-slate-50 transition-colors"
       >
@@ -393,14 +438,75 @@ input:checked + .slider:before {
       </div>
     </div>
   </section>
+  <!-- Machine Settings Section -->
+  <section id="machineSection" class="bg-white rounded-xl shadow-sm overflow-hidden">
+    <h2
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
+    >
+      <span>Machine Settings</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
+    </h2>
+    <div class="section-content collapsed px-6 py-5 space-y-6">
+      <!-- Table Type Override -->
+      <div class="space-y-3">
+        <label class="text-sm font-medium text-slate-700 flex items-center gap-2">
+          <span class="material-icons text-slate-600 text-base">precision_manufacturing</span>
+          Table Type
+        </label>
+
+        <!-- Current detected type display -->
+        <div id="detectedTableTypeContainer" class="flex items-center gap-2 text-sm text-slate-600 bg-slate-50 rounded-lg p-3">
+          <span class="material-icons text-slate-500 text-base">info</span>
+          <span>Detected: <span id="detectedTableType" class="font-medium text-slate-800">Unknown</span></span>
+        </div>
+
+        <div class="flex gap-3 items-center">
+          <select
+            id="tableTypeSelect"
+            class="form-select flex-1 resize-none overflow-hidden rounded-lg text-slate-900 focus:outline-0 focus:ring-2 focus:ring-sky-500 border border-slate-300 bg-white focus:border-sky-500 h-10 placeholder:text-slate-400 px-4 text-base font-medium leading-normal transition-colors"
+          >
+            <option value="">Auto-detect (use detected type)</option>
+          </select>
+          <button
+            id="saveTableType"
+            class="flex items-center justify-center gap-2 min-w-[100px] cursor-pointer rounded-lg h-10 px-4 bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors flex-shrink-0"
+          >
+            <span class="material-icons text-lg">save</span>
+            <span class="truncate">Save</span>
+          </button>
+        </div>
+        <p class="text-xs text-slate-500">
+          Override the automatically detected table type. This affects gear ratio calculations and homing behavior. Leave as "Auto-detect" unless you need to manually specify your table type.
+        </p>
+      </div>
+
+      <!-- Info box -->
+      <div class="text-xs text-slate-600 bg-blue-50 border border-blue-200 rounded-lg p-3">
+        <div class="flex items-start gap-2">
+          <span class="material-icons text-blue-600 text-base">info</span>
+          <div>
+            <p class="font-medium text-blue-800">Table Type Detection</p>
+            <ul class="mt-1 space-y-1 text-blue-700">
+              <li>• Table type is normally detected automatically from GRBL settings</li>
+              <li>• Use override if auto-detection is incorrect for your hardware</li>
+              <li>• Changes take effect on next connection/homing</li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  </section>
   <!-- Homing Configuration Section -->
   <section id="homingSection" class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
-      Homing Configuration
+      <span>Homing Configuration</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <!-- Homing Mode Selection -->
       <div class="space-y-3">
         <label class="text-sm font-medium text-slate-700 flex items-center gap-2">
@@ -533,11 +639,13 @@ input:checked + .slider:before {
   </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
-      Application Settings
+      <span>Application Settings</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <label class="flex flex-col gap-1.5">
         <span class="text-slate-700 text-sm font-medium leading-normal"
           >Application Name</span
@@ -613,11 +721,13 @@ input:checked + .slider:before {
   </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
-      Pattern Clearing
+      <span>Pattern Clearing</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <p class="text-sm text-slate-600">
         Customize the clearing behavior used when transitioning between patterns. Set custom patterns and speed to control how sand is distributed.
       </p>
@@ -730,11 +840,13 @@ input:checked + .slider:before {
   </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
-      LED Controller Configuration
+      <span>LED Controller Configuration</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <!-- LED Provider Selection -->
       <div class="flex flex-col gap-2">
         <span class="text-slate-700 text-sm font-medium leading-normal">LED Provider</span>
@@ -865,11 +977,13 @@ input:checked + .slider:before {
   <!-- MQTT Configuration Section -->
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
-      Home Assistant Integration
+      <span>Home Assistant Integration</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <!-- MQTT Enable Toggle -->
       <div class="flex items-center justify-between">
         <div class="flex-1">
@@ -1057,11 +1171,13 @@ input:checked + .slider:before {
 
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
-      Auto-play on Boot
+      <span>Auto-play on Boot</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <div class="flex items-center justify-between">
         <div class="flex-1">
           <h3 class="text-slate-700 text-base font-medium leading-normal">Enable Auto-play on Boot</h3>
@@ -1167,11 +1283,13 @@ input:checked + .slider:before {
   </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
-      Still Sands
+      <span>Still Sands</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <div class="flex items-center justify-between">
         <div class="flex-1">
           <h3 class="text-slate-700 text-base font-medium leading-normal">Enable Still Sands</h3>
@@ -1364,11 +1482,13 @@ input:checked + .slider:before {
   </section>
   <section id="software-version-section" class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
-      Software Version
+      <span>Software Version</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
-    <div>
+    <div class="section-content collapsed">
       <div class="flex items-center gap-4 px-6 py-5">
         <div
           class="text-slate-600 flex items-center justify-center rounded-lg bg-slate-100 shrink-0 size-12"