diff --git a/backend/HMIWeb/MachineBridge.cs b/backend/HMIWeb/MachineBridge.cs index 9161fc8..0ff195a 100644 --- a/backend/HMIWeb/MachineBridge.cs +++ b/backend/HMIWeb/MachineBridge.cs @@ -2,6 +2,9 @@ using System.Runtime.InteropServices; using System.Windows.Forms; using Newtonsoft.Json; +using System.IO; +using System.Linq; +using System.Collections.Generic; namespace HMIWeb { @@ -13,9 +16,83 @@ namespace HMIWeb // Reference to the main form to update logic private MainForm _host; + // Data folder paths + private readonly string _dataFolder; + private readonly string _settingsPath; + private readonly string _recipeFolder; + public MachineBridge(MainForm host) { _host = host; + + // Initialize data folder paths + _dataFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data"); + _settingsPath = Path.Combine(_dataFolder, "settings.json"); + _recipeFolder = Path.Combine(_dataFolder, "recipe"); + + // Ensure folders exist + EnsureDataFoldersExist(); + } + + private void EnsureDataFoldersExist() + { + try + { + if (!Directory.Exists(_dataFolder)) + Directory.CreateDirectory(_dataFolder); + + if (!Directory.Exists(_recipeFolder)) + Directory.CreateDirectory(_recipeFolder); + + // Initialize default settings if not exists + if (!File.Exists(_settingsPath)) + InitializeDefaultSettings(); + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to create data folders: {ex.Message}"); + } + } + + private void InitializeDefaultSettings() + { + var defaultSettings = new List + { + new { Key = "Site Name", Value = "Smart Factory A-1", Group = "System Information", Type = "String", Description = "The display name of the factory site." }, + new { Key = "Line ID", Value = "L-2024-001", Group = "System Information", Type = "String", Description = "Unique identifier for this production line." }, + new { Key = "API Endpoint", Value = "https://api.factory.local/v1", Group = "Network Configuration", Type = "String", Description = "Base URL for the backend API." }, + new { Key = "Support Contact", Value = "010-1234-5678", Group = "System Information", Type = "String", Description = "Emergency contact number for maintenance." }, + new { Key = "Debug Mode", Value = "false", Group = "System Information", Type = "Boolean", Description = "Enable detailed logging for debugging." }, + new { Key = "Max Speed", Value = "1500", Group = "Motion Control", Type = "Number", Description = "Maximum velocity in mm/s." }, + new { Key = "Acceleration", Value = "500", Group = "Motion Control", Type = "Number", Description = "Acceleration ramp in mm/s²." } + }; + + for (int i = 1; i <= 5; i++) + { + defaultSettings.Add(new + { + Key = $"Sensor_{i}_Threshold", + Value = (i * 10).ToString(), + Group = "Sensor Calibration", + Type = "Number", + Description = $"Trigger threshold for Sensor {i}." + }); + } + + for (int i = 1; i <= 3; i++) + { + defaultSettings.Add(new + { + Key = $"Safety_Zone_{i}", + Value = "true", + Group = "Safety Settings", + Type = "Boolean", + Description = $"Enable monitoring for Safety Zone {i}." + }); + } + + File.WriteAllText(_settingsPath, JsonConvert.SerializeObject(defaultSettings, Formatting.Indented)); + Console.WriteLine("[INFO] Default settings.json created"); } // --- Methods called from React (App.tsx) --- @@ -45,18 +122,31 @@ namespace HMIWeb { Console.WriteLine($"[C#] Selecting Recipe: {recipeId}"); - // In real app, load recipe settings here - // For now, just return success try { - // Simulate recipe loading logic + string recipePath = Path.Combine(_recipeFolder, $"{recipeId}.json"); + + if (!File.Exists(recipePath)) + { + var response = new { success = false, message = "Recipe not found" }; + return JsonConvert.SerializeObject(response); + } + + // Load recipe data from file + string json = File.ReadAllText(recipePath); + var recipeData = JsonConvert.DeserializeObject(json); + + // Set as current recipe _host.SetCurrentRecipe(recipeId); - var response = new { success = true, message = "Recipe selected successfully", recipeId = recipeId }; - return JsonConvert.SerializeObject(response); + Console.WriteLine($"[INFO] Recipe {recipeId} selected successfully"); + + var response2 = new { success = true, message = "Recipe selected successfully", recipeId = recipeId, recipe = recipeData }; + return JsonConvert.SerializeObject(response2); } catch (Exception ex) { + Console.WriteLine($"[ERROR] Failed to select recipe: {ex.Message}"); var response = new { success = false, message = ex.Message }; return JsonConvert.SerializeObject(response); } @@ -64,55 +154,48 @@ namespace HMIWeb public string GetConfig() { - // Generate 20 Mock Settings - var settings = new System.Collections.Generic.List(); - - // Core Settings - settings.Add(new { Key = "Site Name", Value = "Smart Factory A-1", Group = "System Information", Type = "String", Description = "The display name of the factory site." }); - settings.Add(new { Key = "Line ID", Value = "L-2024-001", Group = "System Information", Type = "String", Description = "Unique identifier for this production line." }); - settings.Add(new { Key = "API Endpoint", Value = "https://api.factory.local/v1", Group = "Network Configuration", Type = "String", Description = "Base URL for the backend API." }); - settings.Add(new { Key = "Support Contact", Value = "010-1234-5678", Group = "System Information", Type = "String", Description = "Emergency contact number for maintenance." }); - settings.Add(new { Key = "Debug Mode", Value = "false", Group = "System Information", Type = "Boolean", Description = "Enable detailed logging for debugging." }); - settings.Add(new { Key = "Max Speed", Value = "1500", Group = "Motion Control", Type = "Number", Description = "Maximum velocity in mm/s." }); - settings.Add(new { Key = "Acceleration", Value = "500", Group = "Motion Control", Type = "Number", Description = "Acceleration ramp in mm/s²." }); - - // Generated Settings - for (int i = 1; i <= 5; i++) + try { - settings.Add(new { - Key = $"Sensor_{i}_Threshold", - Value = (i * 10).ToString(), - Group = "Sensor Calibration", - Type = "Number", - Description = $"Trigger threshold for Sensor {i}." - }); - } - - for (int i = 1; i <= 3; i++) - { - settings.Add(new { - Key = $"Safety_Zone_{i}", - Value = "true", - Group = "Safety Settings", - Type = "Boolean", - Description = $"Enable monitoring for Safety Zone {i}." - }); - } + if (!File.Exists(_settingsPath)) + { + InitializeDefaultSettings(); + } - Console.WriteLine("get config (20 items)"); - return Newtonsoft.Json.JsonConvert.SerializeObject(settings); + string json = File.ReadAllText(_settingsPath); + Console.WriteLine($"[INFO] Loaded settings from {_settingsPath}"); + return json; + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to load settings: {ex.Message}"); + return "[]"; + } } public void SaveConfig(string configJson) { - Console.WriteLine($"[Backend] SAVE CONFIG REQUEST RECEIVED"); - Console.WriteLine($"[Backend] Data: {configJson}"); - // In a real app, we would save this to a file or database + try + { + Console.WriteLine($"[Backend] SAVE CONFIG REQUEST RECEIVED"); + + // Validate JSON format + var settings = JsonConvert.DeserializeObject(configJson); + + // Save to file with formatting + File.WriteAllText(_settingsPath, JsonConvert.SerializeObject(settings, Formatting.Indented)); + + Console.WriteLine($"[INFO] Settings saved successfully to {_settingsPath}"); + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to save settings: {ex.Message}"); + } } public string GetIOList() { var ioList = new System.Collections.Generic.List(); + Console.WriteLine("GetIOList"); // Outputs (0-31) for (int i = 0; i < 32; i++) { @@ -144,14 +227,98 @@ namespace HMIWeb } public string GetRecipeList() { - var recipes = new System.Collections.Generic.List(); - recipes.Add(new { id = "1", name = "Wafer_Proc_300_Au", lastModified = "2023-10-25" }); - recipes.Add(new { id = "2", name = "Wafer_Insp_200_Adv", lastModified = "2023-10-26" }); - recipes.Add(new { id = "3", name = "Glass_Gen5_Bonding", lastModified = "2023-10-27" }); - recipes.Add(new { id = "4", name = "Solar_Cell_Cut_A", lastModified = "2023-11-01" }); - recipes.Add(new { id = "5", name = "LED_Mount_HighSpeed", lastModified = "2023-11-15" }); + try + { + var recipes = new List(); - return Newtonsoft.Json.JsonConvert.SerializeObject(recipes); + if (!Directory.Exists(_recipeFolder)) + { + Directory.CreateDirectory(_recipeFolder); + return JsonConvert.SerializeObject(recipes); + } + + var jsonFiles = Directory.GetFiles(_recipeFolder, "*.json"); + + foreach (var filePath in jsonFiles) + { + try + { + string fileName = Path.GetFileNameWithoutExtension(filePath); + string json = File.ReadAllText(filePath); + var recipeData = JsonConvert.DeserializeObject(json); + + var lastModified = File.GetLastWriteTime(filePath).ToString("yyyy-MM-dd"); + + recipes.Add(new + { + id = fileName, + name = recipeData?.name ?? fileName, + lastModified = lastModified + }); + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to read recipe {filePath}: {ex.Message}"); + } + } + + Console.WriteLine($"[INFO] Loaded {recipes.Count} recipes from {_recipeFolder}"); + return JsonConvert.SerializeObject(recipes); + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to get recipe list: {ex.Message}"); + return "[]"; + } + } + + public string GetRecipe(string recipeId) + { + try + { + string recipePath = Path.Combine(_recipeFolder, $"{recipeId}.json"); + + if (!File.Exists(recipePath)) + { + var response = new { success = false, message = "Recipe not found" }; + return JsonConvert.SerializeObject(response); + } + + string json = File.ReadAllText(recipePath); + Console.WriteLine($"[INFO] Loaded recipe {recipeId}"); + return json; + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to get recipe {recipeId}: {ex.Message}"); + var response = new { success = false, message = ex.Message }; + return JsonConvert.SerializeObject(response); + } + } + + public string SaveRecipe(string recipeId, string recipeData) + { + try + { + string recipePath = Path.Combine(_recipeFolder, $"{recipeId}.json"); + + // Validate JSON format + var recipe = JsonConvert.DeserializeObject(recipeData); + + // Save to file with formatting + File.WriteAllText(recipePath, JsonConvert.SerializeObject(recipe, Formatting.Indented)); + + Console.WriteLine($"[INFO] Recipe {recipeId} saved successfully to {recipePath}"); + + var response = new { success = true, message = "Recipe saved successfully", recipeId = recipeId }; + return JsonConvert.SerializeObject(response); + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to save recipe {recipeId}: {ex.Message}"); + var response = new { success = false, message = ex.Message }; + return JsonConvert.SerializeObject(response); + } } public string CopyRecipe(string recipeId, string newName) @@ -160,12 +327,33 @@ namespace HMIWeb try { - // In real app, copy recipe data from database/file - // Generate new ID - string newId = System.Guid.NewGuid().ToString().Substring(0, 8); - string timestamp = System.DateTime.Now.ToString("yyyy-MM-dd"); + string sourcePath = Path.Combine(_recipeFolder, $"{recipeId}.json"); - var response = new { + if (!File.Exists(sourcePath)) + { + var response = new { success = false, message = "Source recipe not found" }; + return JsonConvert.SerializeObject(response); + } + + // Generate new ID + string newId = Guid.NewGuid().ToString().Substring(0, 8); + string destPath = Path.Combine(_recipeFolder, $"{newId}.json"); + + // Read source recipe + string json = File.ReadAllText(sourcePath); + var recipeData = JsonConvert.DeserializeObject(json); + + // Update name in recipe data + recipeData.name = newName; + + // Save to new file + File.WriteAllText(destPath, JsonConvert.SerializeObject(recipeData, Formatting.Indented)); + + string timestamp = DateTime.Now.ToString("yyyy-MM-dd"); + + Console.WriteLine($"[INFO] Recipe copied from {recipeId} to {newId}"); + + var response2 = new { success = true, message = "Recipe copied successfully", newRecipe = new { @@ -174,10 +362,11 @@ namespace HMIWeb lastModified = timestamp } }; - return JsonConvert.SerializeObject(response); + return JsonConvert.SerializeObject(response2); } catch (Exception ex) { + Console.WriteLine($"[ERROR] Failed to copy recipe: {ex.Message}"); var response = new { success = false, message = ex.Message }; return JsonConvert.SerializeObject(response); } @@ -189,19 +378,32 @@ namespace HMIWeb try { - // In real app, delete recipe from database/file // Check if recipe is in use if (recipeId == _host.GetCurrentRecipe()) { - var response = new { success = false, message = "Cannot delete currently selected recipe" }; + var response1 = new { success = false, message = "Cannot delete currently selected recipe" }; + return JsonConvert.SerializeObject(response1); + } + + string recipePath = Path.Combine(_recipeFolder, $"{recipeId}.json"); + + if (!File.Exists(recipePath)) + { + var response = new { success = false, message = "Recipe not found" }; return JsonConvert.SerializeObject(response); } - var response = new { success = true, message = "Recipe deleted successfully", recipeId = recipeId }; - return JsonConvert.SerializeObject(response); + // Delete the file + File.Delete(recipePath); + + Console.WriteLine($"[INFO] Recipe {recipeId} deleted successfully"); + + var response2 = new { success = true, message = "Recipe deleted successfully", recipeId = recipeId }; + return JsonConvert.SerializeObject(response2); } catch (Exception ex) { + Console.WriteLine($"[ERROR] Failed to delete recipe: {ex.Message}"); var response = new { success = false, message = ex.Message }; return JsonConvert.SerializeObject(response); } diff --git a/backend/HMIWeb/MainForm.Designer.cs b/backend/HMIWeb/MainForm.Designer.cs index 599727b..e454699 100644 --- a/backend/HMIWeb/MainForm.Designer.cs +++ b/backend/HMIWeb/MainForm.Designer.cs @@ -32,10 +32,12 @@ // // MainForm // - this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 15F); + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(800, 450); + this.ClientSize = new System.Drawing.Size(1284, 961); + this.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.Name = "MainForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; this.Text = "Form1"; this.Load += new System.EventHandler(this.MainForm_Load); this.ResumeLayout(false); diff --git a/frontend/App.tsx b/frontend/App.tsx index ee1da5e..5ea0e96 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -35,17 +35,13 @@ const INITIAL_IO: IOPoint[] = [ // --- MAIN APP --- export default function App() { - const [activeTab, setActiveTab] = useState<'recipe' | 'motion' | 'camera' | 'setting' | null>(null); - const [activeIOTab, setActiveIOTab] = useState<'in' | 'out'>('in'); + const [activeTab, setActiveTab] = useState<'recipe' | 'motion' | 'camera' | 'setting' | 'initialize' | null>(null); const [systemState, setSystemState] = useState(SystemState.IDLE); - const [recipes, setRecipes] = useState([]); - const [currentRecipe, setCurrentRecipe] = useState(null); + const [currentRecipe, setCurrentRecipe] = useState({ id: '0', name: 'No Recipe', lastModified: '-' }); const [robotTarget, setRobotTarget] = useState({ x: 0, y: 0, z: 0 }); const [logs, setLogs] = useState([]); const [ioPoints, setIoPoints] = useState([]); const [currentTime, setCurrentTime] = useState(new Date()); - const [config, setConfig] = useState(null); - const [isConfigRefreshing, setIsConfigRefreshing] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isHostConnected, setIsHostConnected] = useState(false); const videoRef = useRef(null); @@ -94,39 +90,20 @@ export default function App() { useEffect(() => { const initSystem = async () => { addLog("SYSTEM STARTED", "info"); + // Initial IO data will be loaded by HomePage when it mounts try { const ioStr = await comms.getIOList(); const ioData = JSON.parse(ioStr); setIoPoints(ioData); addLog("IO LIST LOADED", "info"); - - const recipeStr = await comms.getRecipeList(); - const recipeData = JSON.parse(recipeStr); - setRecipes(recipeData); - if (recipeData.length > 0) setCurrentRecipe(recipeData[0]); - addLog("RECIPE LIST LOADED", "info"); } catch (e) { - addLog("FAILED TO LOAD SYSTEM DATA", "error"); - // Fallback to empty or keep initial if needed + addLog("FAILED TO LOAD IO DATA", "error"); } setIsLoading(false); }; initSystem(); }, []); - // -- CONFIG FETCHING (for settings modal) -- - const fetchConfig = React.useCallback(async () => { - setIsConfigRefreshing(true); - try { - const configStr = await comms.getConfig(); - setConfig(JSON.parse(configStr)); - addLog("CONFIG REFRESHED", "info"); - } catch (e) { - addLog("CONFIG REFRESH FAILED", "error"); - } - setIsConfigRefreshing(false); - }, []); - const addLog = (msg: string, type: 'info' | 'warning' | 'error' = 'info') => { setLogs(prev => [{ id: Date.now() + Math.random(), timestamp: new Date().toLocaleTimeString(), message: msg, type }, ...prev].slice(0, 50)); }; @@ -171,7 +148,6 @@ export default function App() { const handleSaveConfig = async (newConfig: ConfigItem[]) => { try { await comms.saveConfig(JSON.stringify(newConfig)); - setConfig(newConfig); addLog("CONFIGURATION SAVED", "info"); } catch (e) { console.error(e); @@ -202,10 +178,7 @@ export default function App() { currentTime={currentTime} isHostConnected={isHostConnected} robotTarget={robotTarget} - onTabChange={(tab) => { - setActiveTab(tab); - if (tab === null) setActiveIOTab('in'); // Reset IO tab when closing - }} + onTabChange={setActiveTab} activeTab={activeTab} isLoading={isLoading} > @@ -215,13 +188,10 @@ export default function App() { element={ setActiveTab(null)} videoRef={videoRef} /> @@ -240,10 +209,7 @@ export default function App() { path="/io-monitor" element={ } /> diff --git a/frontend/components/InitializeModal.tsx b/frontend/components/InitializeModal.tsx new file mode 100644 index 0000000..b8ed218 --- /dev/null +++ b/frontend/components/InitializeModal.tsx @@ -0,0 +1,181 @@ +import React, { useState, useEffect } from 'react'; +import { Target, CheckCircle2, Loader2 } from 'lucide-react'; +import { TechButton } from './common/TechButton'; + +interface InitializeModalProps { + isOpen: boolean; + onClose: () => void; +} + +type AxisStatus = 'pending' | 'initializing' | 'completed'; + +interface AxisState { + name: string; + status: AxisStatus; + progress: number; +} + +export const InitializeModal: React.FC = ({ isOpen, onClose }) => { + const [axes, setAxes] = useState([ + { name: 'X-Axis', status: 'pending', progress: 0 }, + { name: 'Y-Axis', status: 'pending', progress: 0 }, + { name: 'Z-Axis', status: 'pending', progress: 0 }, + ]); + const [isInitializing, setIsInitializing] = useState(false); + + // Reset state when modal closes + useEffect(() => { + if (!isOpen) { + setAxes([ + { name: 'X-Axis', status: 'pending', progress: 0 }, + { name: 'Y-Axis', status: 'pending', progress: 0 }, + { name: 'Z-Axis', status: 'pending', progress: 0 }, + ]); + setIsInitializing(false); + } + }, [isOpen]); + + const startInitialization = () => { + setIsInitializing(true); + + // Initialize each axis with 3 second delay between them + axes.forEach((axis, index) => { + const delay = index * 3000; // 0s, 3s, 6s + + // Start initialization after delay + setTimeout(() => { + setAxes(prev => { + const next = [...prev]; + next[index] = { ...next[index], status: 'initializing', progress: 0 }; + return next; + }); + + // Progress animation (3 seconds) + const startTime = Date.now(); + const duration = 3000; + const interval = setInterval(() => { + const elapsed = Date.now() - startTime; + const progress = Math.min((elapsed / duration) * 100, 100); + + setAxes(prev => { + const next = [...prev]; + next[index] = { ...next[index], progress }; + return next; + }); + + if (progress >= 100) { + clearInterval(interval); + setAxes(prev => { + const next = [...prev]; + next[index] = { ...next[index], status: 'completed', progress: 100 }; + return next; + }); + + // Check if all axes are completed + setAxes(prev => { + if (prev.every(a => a.status === 'completed')) { + // Auto close after 500ms + setTimeout(() => { + onClose(); + }, 500); + } + return prev; + }); + } + }, 50); + }, delay); + }); + }; + + if (!isOpen) return null; + + const allCompleted = axes.every(a => a.status === 'completed'); + + return ( +
+
+ + +

+ AXIS INITIALIZATION +

+ +
+ {axes.map((axis, index) => ( +
+
+
+ {axis.status === 'completed' ? ( + + ) : axis.status === 'initializing' ? ( + + ) : ( +
+ )} + + {axis.name} + +
+ + {axis.status === 'completed' + ? 'COMPLETED' + : axis.status === 'initializing' + ? `${Math.round(axis.progress)}%` + : 'WAITING'} + +
+ + {/* Progress Bar */} +
+
+
+
+ ))} +
+ + {allCompleted && ( +
+ + ✓ ALL AXES INITIALIZED SUCCESSFULLY + +
+ )} + +
+ + CANCEL + + + {isInitializing ? 'INITIALIZING...' : 'START INITIALIZATION'} + +
+
+
+ ); +}; diff --git a/frontend/components/RecipePanel.tsx b/frontend/components/RecipePanel.tsx index c7ff6c6..17cf25c 100644 --- a/frontend/components/RecipePanel.tsx +++ b/frontend/components/RecipePanel.tsx @@ -1,20 +1,46 @@ -import React, { useState } from 'react'; -import { Layers, Check, Settings, X } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import { Layers, Check, Settings, X, RotateCw } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { Recipe } from '../types'; import { TechButton } from './common/TechButton'; +import { comms } from '../communication'; interface RecipePanelProps { - recipes: Recipe[]; + isOpen: boolean; currentRecipe: Recipe; onSelectRecipe: (r: Recipe) => void; onClose: () => void; } -export const RecipePanel: React.FC = ({ recipes, currentRecipe, onSelectRecipe, onClose }) => { +export const RecipePanel: React.FC = ({ isOpen, currentRecipe, onSelectRecipe, onClose }) => { const navigate = useNavigate(); + const [recipes, setRecipes] = useState([]); + const [isLoading, setIsLoading] = useState(false); const [selectedId, setSelectedId] = useState(currentRecipe.id); + // Fetch recipes when panel opens + useEffect(() => { + if (isOpen) { + const fetchRecipes = async () => { + setIsLoading(true); + try { + const recipeStr = await comms.getRecipeList(); + const recipeData: Recipe[] = JSON.parse(recipeStr); + setRecipes(recipeData); + } catch (e) { + console.error('Failed to fetch recipes:', e); + } + setIsLoading(false); + }; + fetchRecipes(); + } + }, [isOpen]); + + // Update selected ID when currentRecipe changes + useEffect(() => { + setSelectedId(currentRecipe.id); + }, [currentRecipe.id]); + const handleConfirm = () => { const selected = recipes.find(r => r.id === selectedId); if (selected) { @@ -23,6 +49,8 @@ export const RecipePanel: React.FC = ({ recipes, currentRecipe } }; + if (!isOpen) return null; + return (
@@ -38,26 +66,33 @@ export const RecipePanel: React.FC = ({ recipes, currentRecipe {/* List (Max height for ~5 items) */}
-
- {recipes.map(recipe => ( -
setSelectedId(recipe.id)} - className={` - p-3 rounded border cursor-pointer transition-all flex items-center justify-between - ${selectedId === recipe.id - ? 'bg-neon-blue/20 border-neon-blue text-white shadow-[0_0_10px_rgba(0,243,255,0.2)]' - : 'bg-white/5 border-white/10 text-slate-400 hover:bg-white/10 hover:border-white/20'} - `} - > -
-
{recipe.name}
-
ID: {recipe.id} | MOD: {recipe.lastModified}
+ {isLoading ? ( +
+ +
LOADING RECIPES...
+
+ ) : ( +
+ {recipes.map(recipe => ( +
setSelectedId(recipe.id)} + className={` + p-3 rounded border cursor-pointer transition-all flex items-center justify-between + ${selectedId === recipe.id + ? 'bg-neon-blue/20 border-neon-blue text-white shadow-[0_0_10px_rgba(0,243,255,0.2)]' + : 'bg-white/5 border-white/10 text-slate-400 hover:bg-white/10 hover:border-white/20'} + `} + > +
+
{recipe.name}
+
ID: {recipe.id} | MOD: {recipe.lastModified}
+
+ {selectedId === recipe.id && }
- {selectedId === recipe.id && } -
- ))} -
+ ))} +
+ )}
{/* Footer Actions */} diff --git a/frontend/components/SettingsModal.tsx b/frontend/components/SettingsModal.tsx index 28ef4c7..86b59b6 100644 --- a/frontend/components/SettingsModal.tsx +++ b/frontend/components/SettingsModal.tsx @@ -1,12 +1,11 @@ import React from 'react'; import { Settings, RotateCw, ChevronDown, ChevronRight } from 'lucide-react'; import { ConfigItem } from '../types'; +import { comms } from '../communication'; interface SettingsModalProps { isOpen: boolean; onClose: () => void; - config: ConfigItem[] | null; - isRefreshing: boolean; onSave: (config: ConfigItem[]) => void; } @@ -34,18 +33,31 @@ const TechButton = ({ children, onClick, active = false, variant = 'blue', class ); }; -export const SettingsModal: React.FC = ({ isOpen, onClose, config, isRefreshing, onSave }) => { +export const SettingsModal: React.FC = ({ isOpen, onClose, onSave }) => { const [localConfig, setLocalConfig] = React.useState([]); const [expandedGroups, setExpandedGroups] = React.useState>(new Set()); + const [isRefreshing, setIsRefreshing] = React.useState(false); + // Fetch config data when modal opens React.useEffect(() => { - if (config) { - setLocalConfig(config); - // Auto-expand all groups initially - const groups = new Set(config.map(c => c.Group)); - setExpandedGroups(groups); + if (isOpen) { + const fetchConfig = async () => { + setIsRefreshing(true); + try { + const configStr = await comms.getConfig(); + const config: ConfigItem[] = JSON.parse(configStr); + setLocalConfig(config); + // Auto-expand all groups initially + const groups = new Set(config.map(c => c.Group)); + setExpandedGroups(groups); + } catch (e) { + console.error('Failed to fetch config:', e); + } + setIsRefreshing(false); + }; + fetchConfig(); } - }, [config]); + }, [isOpen]); const handleChange = (idx: number, newValue: string) => { setLocalConfig(prev => { diff --git a/frontend/components/layout/Header.tsx b/frontend/components/layout/Header.tsx index 4037085..048f870 100644 --- a/frontend/components/layout/Header.tsx +++ b/frontend/components/layout/Header.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; -import { Activity, Settings, Move, Camera, Layers, Cpu } from 'lucide-react'; +import { Activity, Settings, Move, Camera, Layers, Cpu, Target } from 'lucide-react'; interface HeaderProps { currentTime: Date; - onTabChange: (tab: 'recipe' | 'motion' | 'camera' | 'setting' | null) => void; - activeTab: 'recipe' | 'motion' | 'camera' | 'setting' | null; + onTabChange: (tab: 'recipe' | 'motion' | 'camera' | 'setting' | 'initialize' | null) => void; + activeTab: 'recipe' | 'motion' | 'camera' | 'setting' | 'initialize' | null; } @@ -53,7 +53,8 @@ export const Header: React.FC = ({ currentTime, onTabChange, active { id: 'io', icon: Activity, label: 'I/O MONITOR', path: '/io-monitor' }, { id: 'motion', icon: Move, label: 'MOTION', path: '/' }, { id: 'camera', icon: Camera, label: 'VISION', path: '/' }, - { id: 'setting', icon: Settings, label: 'CONFIG', path: '/' } + { id: 'setting', icon: Settings, label: 'CONFIG', path: '/' }, + { id: 'initialize', icon: Target, label: 'INITIALIZE', path: '/' } ].map(item => { const isActive = item.id === 'io' ? location.pathname === '/io-monitor' diff --git a/frontend/components/layout/Layout.tsx b/frontend/components/layout/Layout.tsx index 4c18e09..eb0aaa1 100644 --- a/frontend/components/layout/Layout.tsx +++ b/frontend/components/layout/Layout.tsx @@ -8,8 +8,8 @@ interface LayoutProps { currentTime: Date; isHostConnected: boolean; robotTarget: RobotTarget; - onTabChange: (tab: 'recipe' | 'motion' | 'camera' | 'setting' | null) => void; - activeTab: 'recipe' | 'motion' | 'camera' | 'setting' | null; + onTabChange: (tab: 'recipe' | 'motion' | 'camera' | 'setting' | 'initialize' | null) => void; + activeTab: 'recipe' | 'motion' | 'camera' | 'setting' | 'initialize' | null; isLoading: boolean; } diff --git a/frontend/pages/HomePage.tsx b/frontend/pages/HomePage.tsx index cd7e0c6..d13252a 100644 --- a/frontend/pages/HomePage.tsx +++ b/frontend/pages/HomePage.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Play, Square, RotateCw, AlertTriangle, Siren, Terminal } from 'lucide-react'; import { Machine3D } from '../components/Machine3D'; import { SettingsModal } from '../components/SettingsModal'; +import { InitializeModal } from '../components/InitializeModal'; import { RecipePanel } from '../components/RecipePanel'; import { MotionPanel } from '../components/MotionPanel'; import { CameraPanel } from '../components/CameraPanel'; @@ -13,21 +14,17 @@ import { SystemState, Recipe, IOPoint, LogEntry, RobotTarget, ConfigItem } from interface HomePageProps { systemState: SystemState; currentRecipe: Recipe; - recipes: Recipe[]; robotTarget: RobotTarget; logs: LogEntry[]; ioPoints: IOPoint[]; - config: ConfigItem[] | null; - isConfigRefreshing: boolean; doorStates: { front: boolean; right: boolean; left: boolean; back: boolean }; isLowPressure: boolean; isEmergencyStop: boolean; - activeTab: 'recipe' | 'motion' | 'camera' | 'setting' | null; + activeTab: 'recipe' | 'motion' | 'camera' | 'setting' | 'initialize' | null; onSelectRecipe: (r: Recipe) => void; onMove: (axis: 'X' | 'Y' | 'Z', val: number) => void; onControl: (action: 'start' | 'stop' | 'reset') => void; onSaveConfig: (config: ConfigItem[]) => void; - onFetchConfig: () => void; onCloseTab: () => void; videoRef: React.RefObject; } @@ -35,12 +32,9 @@ interface HomePageProps { export const HomePage: React.FC = ({ systemState, currentRecipe, - recipes, robotTarget, logs, ioPoints, - config, - isConfigRefreshing, doorStates, isLowPressure, isEmergencyStop, @@ -49,7 +43,6 @@ export const HomePage: React.FC = ({ onMove, onControl, onSaveConfig, - onFetchConfig, onCloseTab, videoRef }) => { @@ -59,12 +52,6 @@ export const HomePage: React.FC = ({ } }, [activeTab, videoRef]); - useEffect(() => { - if (activeTab === 'setting') { - onFetchConfig(); - } - }, [activeTab, onFetchConfig]); - return (
{/* 3D Canvas (Background Layer) */} @@ -101,24 +88,26 @@ export const HomePage: React.FC = ({ )} {/* Recipe Selection Modal */} - {activeTab === 'recipe' && ( - - )} + {/* Settings Modal */} + {/* Initialize Modal */} + + {/* Right Sidebar (Dashboard) */}
diff --git a/frontend/pages/IOMonitorPage.tsx b/frontend/pages/IOMonitorPage.tsx index 54441c7..3b6605f 100644 --- a/frontend/pages/IOMonitorPage.tsx +++ b/frontend/pages/IOMonitorPage.tsx @@ -1,14 +1,51 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { RotateCw } from 'lucide-react'; import { IOPoint } from '../types'; +import { comms } from '../communication'; interface IOMonitorPageProps { - ioPoints: IOPoint[]; onToggle: (id: number, type: 'input' | 'output') => void; - activeIOTab: 'in' | 'out'; - onIOTabChange: (tab: 'in' | 'out') => void; } -export const IOMonitorPage: React.FC = ({ ioPoints, onToggle, activeIOTab, onIOTabChange }) => { +export const IOMonitorPage: React.FC = ({ onToggle }) => { + const [ioPoints, setIoPoints] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [activeIOTab, setActiveIOTab] = useState<'in' | 'out'>('in'); + + // Fetch initial IO list when page mounts + useEffect(() => { + const fetchIOList = async () => { + setIsLoading(true); + try { + const ioStr = await comms.getIOList(); + const ioData: IOPoint[] = JSON.parse(ioStr); + setIoPoints(ioData); + } catch (e) { + console.error('Failed to fetch IO list:', e); + } + setIsLoading(false); + }; + fetchIOList(); + + // Subscribe to real-time IO updates + const unsubscribe = comms.subscribe((msg: any) => { + if (msg?.type === 'STATUS_UPDATE' && msg.ioState) { + setIoPoints(prev => { + const newIO = [...prev]; + msg.ioState.forEach((update: { id: number, type: string, state: boolean }) => { + const idx = newIO.findIndex(p => p.id === update.id && p.type === update.type); + if (idx >= 0) newIO[idx] = { ...newIO[idx], state: update.state }; + }); + return newIO; + }); + } + }); + + return () => { + unsubscribe(); + }; + }, []); + const points = ioPoints.filter(p => p.type === (activeIOTab === 'in' ? 'input' : 'output')); return ( @@ -29,13 +66,13 @@ export const IOMonitorPage: React.FC = ({ ioPoints, onToggle
- {/* Grid Layout - More columns for full screen */} - {/* Grid Layout - 2 Columns for list view */} -
- {points.map(p => ( -
onToggle(p.id, p.type)} - className={` - flex items-center gap-4 px-4 py-3 cursor-pointer transition-all border - clip-tech-sm group hover:translate-x-1 - ${p.state - ? (p.type === 'output' - ? 'bg-neon-green/10 border-neon-green text-neon-green shadow-[0_0_15px_rgba(10,255,0,0.2)]' - : 'bg-neon-yellow/10 border-neon-yellow text-neon-yellow shadow-[0_0_15px_rgba(255,230,0,0.2)]') - : 'bg-slate-900/40 border-slate-800 text-slate-500 hover:border-slate-600 hover:bg-slate-800/40'} - `} - > - {/* LED Indicator */} -
+ {isLoading ? ( +
+ +
LOADING IO POINTS...
+
+ ) : ( +
+ {points.map(p => ( +
onToggle(p.id, p.type)} + className={` + flex items-center gap-4 px-4 py-3 cursor-pointer transition-all border + clip-tech-sm group hover:translate-x-1 + ${p.state + ? (p.type === 'output' + ? 'bg-neon-green/10 border-neon-green text-neon-green shadow-[0_0_15px_rgba(10,255,0,0.2)]' + : 'bg-neon-yellow/10 border-neon-yellow text-neon-yellow shadow-[0_0_15px_rgba(255,230,0,0.2)]') + : 'bg-slate-900/40 border-slate-800 text-slate-500 hover:border-slate-600 hover:bg-slate-800/40'} + `} + > + {/* LED Indicator */} +
- {/* ID Badge */} -
- {p.type === 'input' ? 'I' : 'Q'}{p.id.toString().padStart(2, '0')} -
+ {/* ID Badge */} +
+ {p.type === 'input' ? 'I' : 'Q'}{p.id.toString().padStart(2, '0')} +
- {/* Name */} -
- {p.name} + {/* Name */} +
+ {p.name} +
-
- ))} -
+ ))} +
+ )}