init
This commit is contained in:
BIN
.foo.txt.swp
BIN
.foo.txt.swp
Binary file not shown.
111
REFACTORING_SUMMARY.md
Normal file
111
REFACTORING_SUMMARY.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Water Maker UI Refactoring Summary
|
||||
|
||||
## Overview
|
||||
Successfully refactored the monolithic `App.js` file into a well-organized, modular React application with routing capabilities and separation of concerns.
|
||||
|
||||
## New Project Structure
|
||||
|
||||
### 📁 Root Components
|
||||
- **`src/App.js`** - Main application with React Router setup
|
||||
- **`src/components/Layout.js`** - Navigation layout with header and routing
|
||||
|
||||
### 📁 Screens (Pages)
|
||||
- **`src/screens/ControlScreen.js`** - Main watermaker control interface (original functionality)
|
||||
- **`src/screens/StatsScreen.js`** - Statistics and analytics (placeholder)
|
||||
- **`src/screens/ConfigScreen.js`** - System configuration (placeholder)
|
||||
- **`src/screens/MaintenanceScreen.js`** - Maintenance management (placeholder)
|
||||
|
||||
### 📁 Components (UI Building Blocks)
|
||||
- **`src/components/ConnectionStatus.js`** - API/PLC connection status display
|
||||
- **`src/components/SystemStatus.js`** - System mode and status information
|
||||
- **`src/components/DTSOperationStatus.js`** - DTS task progress tracking
|
||||
- **`src/components/ControlButtons.js`** - Start/Stop/Skip control buttons
|
||||
- **`src/components/ProcessProgress.js`** - Process step progress visualization
|
||||
- **`src/components/SensorGrid.js`** - Sensor data display grid
|
||||
- **`src/components/StartDialog.js`** - Modal dialog for start parameters
|
||||
|
||||
### 📁 Hooks (State & Logic)
|
||||
- **`src/hooks/useWaterMakerState.js`** - Centralized state management
|
||||
- **`src/hooks/useWaterMakerAPI.js`** - API calls and data fetching
|
||||
|
||||
## Key Improvements
|
||||
|
||||
### 🔧 Modularity
|
||||
- **Single Responsibility**: Each component has a specific, focused purpose
|
||||
- **Reusability**: Components can be easily reused across different screens
|
||||
- **Maintainability**: Changes to specific functionality are isolated to relevant files
|
||||
|
||||
### 🚀 Scalability
|
||||
- **Easy Screen Addition**: New screens (Stats, Config, Maintenance) can be fully implemented
|
||||
- **Component Library**: Reusable components for consistent UI patterns
|
||||
- **Hook-based Architecture**: Custom hooks for shared logic and state
|
||||
|
||||
### 📱 Navigation
|
||||
- **React Router**: Professional routing with URL-based navigation
|
||||
- **Tab Navigation**: Clean header navigation between different screens
|
||||
- **Responsive Design**: Mobile-friendly navigation layout
|
||||
|
||||
### 🔄 State Management
|
||||
- **Centralized State**: All watermaker state in dedicated hook
|
||||
- **API Separation**: Clean separation of API logic from UI components
|
||||
- **Hook Composition**: Proper dependency injection pattern
|
||||
|
||||
## Navigation Structure
|
||||
|
||||
```
|
||||
/ (Default) → Control Screen
|
||||
/control → Control Screen (Watermaker operation)
|
||||
/stats → Statistics Screen (Performance metrics)
|
||||
/config → Configuration Screen (System settings)
|
||||
/maintenance → Maintenance Screen (Service & diagnostics)
|
||||
```
|
||||
|
||||
## Benefits for Future Development
|
||||
|
||||
### ✅ Easy Feature Addition
|
||||
- **New Screens**: Simply add new screen components and routes
|
||||
- **New Functionality**: Add components to the shared component library
|
||||
- **API Extensions**: Extend the API hook with new endpoints
|
||||
|
||||
### ✅ Team Development
|
||||
- **Clear Boundaries**: Developers can work on different screens simultaneously
|
||||
- **Consistent Patterns**: Established patterns for components and hooks
|
||||
- **Easy Onboarding**: Clear project structure for new team members
|
||||
|
||||
### ✅ Testing & Debugging
|
||||
- **Isolated Testing**: Test individual components in isolation
|
||||
- **Focused Debugging**: Issues are contained to specific modules
|
||||
- **Component Documentation**: Each component has a clear interface
|
||||
|
||||
## Next Steps for Future Screens
|
||||
|
||||
### 📊 Statistics Screen
|
||||
- Implement charts and graphs for performance metrics
|
||||
- Add historical data visualization
|
||||
- Create efficiency reporting
|
||||
|
||||
### ⚙️ Configuration Screen
|
||||
- Add system parameter controls
|
||||
- Implement settings persistence
|
||||
- Create configuration validation
|
||||
|
||||
### 🔧 Maintenance Screen
|
||||
- Add maintenance scheduling
|
||||
- Implement diagnostic tools
|
||||
- Create service history tracking
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- **React Router v6+**: Modern routing with declarative route definitions
|
||||
- **Custom Hooks**: Proper React patterns for state and side effects
|
||||
- **Component Composition**: Flexible, reusable component architecture
|
||||
- **TypeScript Ready**: Structure supports easy TypeScript migration
|
||||
- **Performance Optimized**: Efficient re-rendering with proper dependency management
|
||||
|
||||
## Running the Application
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
The application will run on `http://localhost:3000` with full navigation between all screens.
|
||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"lucide-react": "^0.511.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
@@ -12894,6 +12895,50 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz",
|
||||
"integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz",
|
||||
"integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==",
|
||||
"dependencies": {
|
||||
"react-router": "7.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
||||
@@ -13778,6 +13823,11 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"lucide-react": "^0.511.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
|
||||
1021
src/App.js
1021
src/App.js
File diff suppressed because it is too large
Load Diff
24
src/components/ConnectionStatus.js
Normal file
24
src/components/ConnectionStatus.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Wifi, WifiOff, AlertCircle } from 'lucide-react';
|
||||
|
||||
const ConnectionStatus = ({ apiStatus, error }) => {
|
||||
return (
|
||||
<div className={`mb-4 p-3 rounded-lg flex items-center gap-2 ${
|
||||
apiStatus.connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{apiStatus.connected ? <Wifi size={16} /> : <WifiOff size={16} />}
|
||||
<span className="text-sm font-medium">
|
||||
{apiStatus.connected
|
||||
? `Connected to PLC${apiStatus.plcIp ? ` (${apiStatus.plcIp})` : ''}`
|
||||
: 'Connection Lost'
|
||||
}
|
||||
</span>
|
||||
{error && <AlertCircle size={16} className="ml-auto" />}
|
||||
<span className="text-xs ml-auto">
|
||||
{apiStatus.lastUpdate?.toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionStatus;
|
||||
84
src/components/ControlButtons.js
Normal file
84
src/components/ControlButtons.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { Play, Square } from 'lucide-react';
|
||||
|
||||
const ControlButtons = ({
|
||||
isRunning,
|
||||
dtsTask,
|
||||
apiStatus,
|
||||
currentStep,
|
||||
onStart,
|
||||
onStop,
|
||||
onSkipStep,
|
||||
onCancelStartup,
|
||||
plcTimers
|
||||
}) => {
|
||||
// Check if we have active timers indicating a process is running
|
||||
const hasActiveTimers = plcTimers && Object.values(plcTimers).some(timer => timer > 0);
|
||||
const systemIsActive = isRunning || dtsTask.isRunning || hasActiveTimers;
|
||||
|
||||
if (!systemIsActive) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<button
|
||||
onClick={onStart}
|
||||
disabled={!apiStatus.connected}
|
||||
className={`flex items-center gap-3 px-8 py-4 rounded-lg font-semibold text-xl transition-all transform hover:scale-105 shadow-lg ${
|
||||
apiStatus.connected
|
||||
? 'bg-green-500 hover:bg-green-600 text-white shadow-green-200'
|
||||
: 'bg-gray-400 text-gray-200 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<Play size={24} />
|
||||
START
|
||||
</button>
|
||||
<p className="text-gray-500 text-center">
|
||||
{apiStatus.connected ? 'Press START to begin DTS process' : 'Waiting for PLC connection...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (dtsTask.isRunning) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex justify-center gap-4">
|
||||
<button
|
||||
onClick={onCancelStartup}
|
||||
className="flex items-center gap-3 px-6 py-3 rounded-lg text-white font-semibold text-lg transition-all transform hover:scale-105 bg-red-500 hover:bg-red-600 shadow-red-200 shadow-lg"
|
||||
>
|
||||
<Square size={20} />
|
||||
CANCEL STARTUP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex justify-center gap-4">
|
||||
<button
|
||||
onClick={onStop}
|
||||
className="flex items-center gap-3 px-6 py-3 rounded-lg text-white font-semibold text-lg transition-all transform hover:scale-105 bg-red-500 hover:bg-red-600 shadow-red-200 shadow-lg"
|
||||
>
|
||||
<Square size={20} />
|
||||
{currentStep === 4 ? 'END PROCESS' : 'STOP'}
|
||||
</button>
|
||||
|
||||
{/* Skip button - only available for steps 2 and 3 (currentStep 1 and 2) */}
|
||||
{(currentStep === 1 || currentStep === 2) && !dtsTask.isRunning && (
|
||||
<button
|
||||
onClick={onSkipStep}
|
||||
className="flex items-center gap-2 px-4 py-3 rounded-lg border-2 border-blue-500 text-blue-500 font-semibold transition-all hover:bg-blue-50"
|
||||
>
|
||||
SKIP STEP
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControlButtons;
|
||||
68
src/components/DTSOperationStatus.js
Normal file
68
src/components/DTSOperationStatus.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
|
||||
const DTSOperationStatus = ({ dtsTask }) => {
|
||||
if (!dtsTask.isRunning) return null;
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg shadow p-4 mb-4 border ${
|
||||
dtsTask.status === 'stopping'
|
||||
? 'bg-red-50 border-red-200'
|
||||
: dtsTask.status === 'skipping'
|
||||
? 'bg-yellow-50 border-yellow-200'
|
||||
: 'bg-blue-50 border-blue-200'
|
||||
}`}>
|
||||
<h3 className={`font-semibold mb-3 text-center ${
|
||||
dtsTask.status === 'stopping'
|
||||
? 'text-red-700'
|
||||
: dtsTask.status === 'skipping'
|
||||
? 'text-yellow-700'
|
||||
: 'text-blue-700'
|
||||
}`}>
|
||||
DTS {dtsTask.status === 'stopping' ? 'Stop' : dtsTask.status === 'skipping' ? 'Skip' : 'Startup'} Progress
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className={`text-2xl font-bold ${
|
||||
dtsTask.status === 'stopping'
|
||||
? 'text-red-600'
|
||||
: dtsTask.status === 'skipping'
|
||||
? 'text-yellow-600'
|
||||
: 'text-blue-600'
|
||||
}`}>
|
||||
{dtsTask.progressPercent.toFixed(0)}%
|
||||
</div>
|
||||
<div className={`text-sm ${
|
||||
dtsTask.status === 'stopping'
|
||||
? 'text-red-500'
|
||||
: dtsTask.status === 'skipping'
|
||||
? 'text-yellow-500'
|
||||
: 'text-blue-500'
|
||||
}`}>
|
||||
Complete
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-medium text-gray-700">{dtsTask.currentStep}</div>
|
||||
<div className="text-sm text-gray-500">Current Step</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-medium text-gray-700">{dtsTask.status}</div>
|
||||
<div className="text-sm text-gray-500">Status</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-center">
|
||||
<div className={`text-sm font-medium ${
|
||||
dtsTask.status === 'stopping'
|
||||
? 'text-red-600'
|
||||
: dtsTask.status === 'skipping'
|
||||
? 'text-yellow-600'
|
||||
: 'text-blue-600'
|
||||
}`}>
|
||||
{dtsTask.stepDescription}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DTSOperationStatus;
|
||||
54
src/components/Layout.js
Normal file
54
src/components/Layout.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Settings, BarChart3, Wrench, Gauge } from 'lucide-react';
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const navItems = [
|
||||
{ path: '/control', label: 'Control', icon: Gauge },
|
||||
{ path: '/stats', label: 'Statistics', icon: BarChart3 },
|
||||
{ path: '/config', label: 'Configuration', icon: Settings },
|
||||
{ path: '/maintenance', label: 'Maintenance', icon: Wrench }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
{/* Navigation Header */}
|
||||
<nav className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Gauge className="text-blue-600" size={24} />
|
||||
<h1 className="text-xl font-bold text-gray-800">Water Maker Control</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-1">
|
||||
{navItems.map(({ path, label, icon: Icon }) => (
|
||||
<NavLink
|
||||
key={path}
|
||||
to={path}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center space-x-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon size={16} />
|
||||
<span>{label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Main Content */}
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
172
src/components/ProcessProgress.js
Normal file
172
src/components/ProcessProgress.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import React from 'react';
|
||||
|
||||
const ProcessProgress = ({
|
||||
isRunning,
|
||||
dtsTask,
|
||||
totalRuntime,
|
||||
gallonsProduced,
|
||||
currentStep,
|
||||
plcTimers
|
||||
}) => {
|
||||
const steps = [
|
||||
{ name: 'DTS Requested', duration: null, color: 'bg-blue-500' },
|
||||
{ name: 'Shore Pressure Flush', duration: 180, color: 'bg-yellow-500' },
|
||||
{ name: 'High Pressure Init', duration: 60, color: 'bg-orange-500' },
|
||||
{ name: 'Water Production', duration: null, color: 'bg-green-500' },
|
||||
{ name: 'Fresh Water Flush', duration: 60, color: 'bg-purple-500' }
|
||||
];
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// Calculate progress percentage based on current step and PLC timer
|
||||
const getProgressPercentage = () => {
|
||||
// If we have an active DTS startup task, use its progress
|
||||
if (dtsTask.isRunning && dtsTask.progressPercent > 0) {
|
||||
return dtsTask.progressPercent;
|
||||
}
|
||||
|
||||
if (!isRunning) return 0;
|
||||
|
||||
switch (currentStep) {
|
||||
case 0: // DTS Requested
|
||||
return plcTimers.step0Timer > 0 ? 25 : 10; // Small progress when timer starts
|
||||
case 1: // Shore Pressure Flush (180s)
|
||||
return Math.min(((180 - plcTimers.step1Timer) / 180) * 100, 100);
|
||||
case 2: // High Pressure Init (60s)
|
||||
return Math.min(((60 - plcTimers.step2Timer) / 60) * 100, 100);
|
||||
case 3: // Water Production (indefinite)
|
||||
return Math.min((plcTimers.step3Timer / 300) * 100, 100); // Show progress up to 5 minutes
|
||||
case 4: // Fresh Water Flush (60s)
|
||||
return Math.min(((60 - plcTimers.step4Timer) / 60) * 100, 100);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Get current step name for display
|
||||
const getCurrentStepName = () => {
|
||||
// If we have an active DTS startup task, use its step description
|
||||
if (dtsTask.isRunning && dtsTask.stepDescription) {
|
||||
return dtsTask.stepDescription;
|
||||
}
|
||||
|
||||
return isRunning && currentStep < steps.length ? steps[currentStep].name : 'Ready';
|
||||
};
|
||||
|
||||
// Get current step color
|
||||
const getCurrentStepColor = () => {
|
||||
// Use different colors for different DTS operations
|
||||
if (dtsTask.isRunning) {
|
||||
switch (dtsTask.status) {
|
||||
case 'stopping': return 'bg-red-500';
|
||||
case 'skipping': return 'bg-yellow-500';
|
||||
default: return 'bg-blue-500';
|
||||
}
|
||||
}
|
||||
|
||||
return isRunning && currentStep < steps.length ? steps[currentStep].color : 'bg-gray-300';
|
||||
};
|
||||
|
||||
// Get current step timer value
|
||||
const getCurrentStepTimer = () => {
|
||||
switch (currentStep) {
|
||||
case 0: return plcTimers.step0Timer;
|
||||
case 1: return plcTimers.step1Timer;
|
||||
case 2: return plcTimers.step2Timer;
|
||||
case 3: return plcTimers.step3Timer;
|
||||
case 4: return plcTimers.step4Timer;
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Get remaining time for current step
|
||||
const getRemainingTime = () => {
|
||||
const currentTimer = getCurrentStepTimer();
|
||||
switch (currentStep) {
|
||||
case 0: return null; // No fixed duration
|
||||
case 1: return Math.max(0, currentTimer); // Timer counts down
|
||||
case 2: return Math.max(0, currentTimer); // Timer counts down
|
||||
case 3: return null; // Indefinite duration
|
||||
case 4: return Math.max(0, currentTimer); // Timer counts down
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Show progress if running, DTS task is active, or if we have non-zero timers (indicating active process)
|
||||
const hasActiveTimers = Object.values(plcTimers).some(timer => timer > 0);
|
||||
const shouldShow = isRunning || dtsTask.isRunning || hasActiveTimers;
|
||||
|
||||
if (!shouldShow) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="text-center mb-4">
|
||||
<div className="inline-flex gap-6 text-lg">
|
||||
<span className="text-gray-600">Runtime: <strong>{formatTime(totalRuntime)}</strong></span>
|
||||
<span className="text-blue-600">Gallons: <strong>{gallonsProduced.toFixed(1)}</strong></span>
|
||||
{dtsTask.isRunning && (
|
||||
<span className={`font-medium ${
|
||||
dtsTask.status === 'stopping'
|
||||
? 'text-red-600'
|
||||
: dtsTask.status === 'skipping'
|
||||
? 'text-yellow-600'
|
||||
: 'text-purple-600'
|
||||
}`}>
|
||||
{dtsTask.status === 'stopping' ? 'Stopping' : dtsTask.status === 'skipping' ? 'Skipping' : 'Startup'}: <strong>{dtsTask.progressPercent.toFixed(0)}%</strong>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative bg-gray-600 rounded-lg overflow-hidden h-20">
|
||||
<div
|
||||
className={`absolute top-0 left-0 h-full transition-all duration-1000 ${getCurrentStepColor()}`}
|
||||
style={{
|
||||
width: `${getProgressPercentage()}%`
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className="relative z-10 h-full flex items-center justify-between px-6">
|
||||
<div className="flex-1 text-center">
|
||||
<div className="font-bold text-white drop-shadow-lg">
|
||||
{getCurrentStepName()}
|
||||
</div>
|
||||
<div className="text-sm text-white drop-shadow-lg">
|
||||
{dtsTask.isRunning ? (
|
||||
`${dtsTask.progressPercent.toFixed(0)}% complete`
|
||||
) : isRunning && currentStep < steps.length ? (
|
||||
getRemainingTime() !== null ?
|
||||
`${formatTime(Math.round(getRemainingTime()))} remaining` :
|
||||
`${formatTime(Math.round(getCurrentStepTimer()))} running`
|
||||
) : 'Press Start to Begin'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold text-white drop-shadow-lg">
|
||||
{dtsTask.isRunning
|
||||
? dtsTask.status === 'stopping'
|
||||
? 'Stopping'
|
||||
: dtsTask.status === 'skipping'
|
||||
? 'Skipping Step'
|
||||
: 'Starting Up'
|
||||
: `Step ${currentStep + 1} of ${steps.length}`
|
||||
}
|
||||
</div>
|
||||
{dtsTask.taskId && (
|
||||
<div className="text-xs text-white drop-shadow-lg">
|
||||
Task: {dtsTask.taskId.substring(0, 8)}...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessProgress;
|
||||
105
src/components/SensorGrid.js
Normal file
105
src/components/SensorGrid.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { Droplets, Gauge, Zap } from 'lucide-react';
|
||||
|
||||
const SensorGrid = ({ sensorData }) => {
|
||||
const getStatusColor = (value, min, max) => {
|
||||
if (value >= min && value <= max) return 'text-green-500';
|
||||
return 'text-red-500';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Water Input System */}
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<h3 className="font-semibold text-gray-700 mb-4 text-center">Water Input System</h3>
|
||||
<div className="flex justify-between items-center">
|
||||
{/* Shore Input */}
|
||||
<div className="flex-1 text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<Gauge className={sensorData.shoreValveOpen ? 'text-blue-500' : 'text-gray-400'} size={20} />
|
||||
<h4 className="font-semibold text-gray-700">Shore Input</h4>
|
||||
</div>
|
||||
<div className="text-2xl font-bold mb-1">
|
||||
<span className={getStatusColor(sensorData.shorePressure, 20, 40)}>
|
||||
{sensorData.shorePressure.toFixed(0)} PSI
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
Valve: {sensorData.shoreValveOpen ? 'OPEN' : 'CLOSED'}<br/>Normal: 20-40 PSI
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* High Pressure Pump */}
|
||||
<div className="flex-1 text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<Zap className={sensorData.pumpOn ? 'text-green-500' : 'text-gray-400'} size={20} />
|
||||
<h4 className="font-semibold text-gray-700">High Pressure Pump</h4>
|
||||
</div>
|
||||
<div className="flex justify-center gap-3 items-center mb-1">
|
||||
<div className="text-xl font-bold">
|
||||
<span className={getStatusColor(sensorData.highPressure, 50, 70)}>
|
||||
{sensorData.highPressure.toFixed(0)} PSI
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold">
|
||||
<span className={getStatusColor(sensorData.waterTemp, 50, 90)}>
|
||||
{sensorData.waterTemp.toFixed(0)}°F
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
Status: {sensorData.pumpOn ? 'ON' : 'OFF'}<br/>Pressure: 50-70 PSI | Temp: 50-90°F
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Water Output System */}
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<h3 className="font-semibold text-gray-700 mb-4 text-center">Water Output System</h3>
|
||||
<div className="flex justify-between items-center">
|
||||
{/* Brine Flow */}
|
||||
<div className="flex-1 text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<Droplets className="text-orange-500" size={20} />
|
||||
<h4 className="font-semibold text-gray-700">Brine Flow</h4>
|
||||
</div>
|
||||
<div className="text-2xl font-bold mb-1">
|
||||
<span className={getStatusColor(sensorData.brineFlow, 0, 1)}>
|
||||
{sensorData.brineFlow.toFixed(1)} GPM
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
Normal: <1.0 GPM<br/>Range: 0-2 GPM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Water */}
|
||||
<div className="flex-1 text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<Droplets className="text-blue-500" size={20} />
|
||||
<h4 className="font-semibold text-gray-700">Product Water</h4>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 items-center mb-1">
|
||||
<div className="text-xl font-bold">
|
||||
<span className={getStatusColor(sensorData.productFlow, 1.5, 2.5)}>
|
||||
{sensorData.productFlow.toFixed(1)} GPM
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold">
|
||||
<span className={getStatusColor(sensorData.tds, 0, 12)}>
|
||||
{sensorData.tds.toFixed(0)} PPM
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
Flow: 1.5-2.5 GPM<br/>TDS: 0-12 PPM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SensorGrid;
|
||||
88
src/components/StartDialog.js
Normal file
88
src/components/StartDialog.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
|
||||
const StartDialog = ({
|
||||
showStartDialog,
|
||||
runMode,
|
||||
targetGallons,
|
||||
targetTime,
|
||||
apiStatus,
|
||||
onClose,
|
||||
onRunModeChange,
|
||||
onTargetGallonsChange,
|
||||
onTargetTimeChange,
|
||||
onConfirm
|
||||
}) => {
|
||||
if (!showStartDialog) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
<h2 className="text-xl font-bold mb-4">Start Water Maker</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Run Mode:</label>
|
||||
<select
|
||||
value={runMode}
|
||||
onChange={(e) => onRunModeChange(e.target.value)}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
>
|
||||
<option value="continuous">Run Until Stopped</option>
|
||||
<option value="gallons">Target Gallons</option>
|
||||
<option value="time">Target Time</option>
|
||||
<option value="tanks">Fill Tanks</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{runMode === 'gallons' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Target Gallons:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={targetGallons}
|
||||
onChange={(e) => onTargetGallonsChange(e.target.value)}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
placeholder="Enter gallons"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{runMode === 'time' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Target Time (minutes):</label>
|
||||
<input
|
||||
type="number"
|
||||
value={targetTime}
|
||||
onChange={(e) => onTargetTimeChange(e.target.value)}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
placeholder="Enter minutes"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
disabled={!apiStatus.connected}
|
||||
className={`flex-1 px-4 py-2 rounded-lg ${
|
||||
apiStatus.connected
|
||||
? 'bg-green-500 text-white hover:bg-green-600'
|
||||
: 'bg-gray-400 text-gray-200 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StartDialog;
|
||||
49
src/components/SystemStatus.js
Normal file
49
src/components/SystemStatus.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
|
||||
const SystemStatus = ({ apiStatus, sensorData, currentStep, isRunning, plcTimers }) => {
|
||||
const getSystemStatus = (mode) => {
|
||||
const statusMap = {
|
||||
34: { text: "DTS Requested - Press START", step: 0, color: "bg-blue-100 text-blue-800" },
|
||||
5: { text: "DTS Startup - Shore Pressure Flush (180s)", step: 1, color: "bg-yellow-100 text-yellow-800" },
|
||||
6: { text: "DTS Startup - High Pressure Init (60s)", step: 2, color: "bg-orange-100 text-orange-800" },
|
||||
7: { text: "DTS Running - Water Production", step: 3, color: "bg-green-100 text-green-800" },
|
||||
8: { text: "Fresh Water Flush", step: 4, color: "bg-purple-100 text-purple-800" }
|
||||
};
|
||||
return statusMap[mode] || { text: `Unknown Mode: ${mode}`, step: 0, color: "bg-gray-100 text-gray-800" };
|
||||
};
|
||||
|
||||
const getCurrentStepTimer = () => {
|
||||
switch (currentStep) {
|
||||
case 0: return plcTimers.step0Timer;
|
||||
case 1: return plcTimers.step1Timer;
|
||||
case 2: return plcTimers.step2Timer;
|
||||
case 3: return plcTimers.step3Timer;
|
||||
case 4: return plcTimers.step4Timer;
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
|
||||
if (!apiStatus.connected) return null;
|
||||
|
||||
return (
|
||||
<div className={`mb-4 p-4 rounded-lg ${getSystemStatus(sensorData.systemMode).color}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">Water Maker Status</h3>
|
||||
<p className="text-sm">{getSystemStatus(sensorData.systemMode).text}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-medium">System Mode: {sensorData.systemMode}</div>
|
||||
<div className="text-sm">Step {currentStep + 1} of 5</div>
|
||||
{isRunning && (
|
||||
<div className="text-xs mt-1">
|
||||
Timer: {getCurrentStepTimer().toFixed(1)}s
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemStatus;
|
||||
394
src/hooks/useWaterMakerAPI.js
Normal file
394
src/hooks/useWaterMakerAPI.js
Normal file
@@ -0,0 +1,394 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useWaterMakerAPI = (stateSetters) => {
|
||||
const {
|
||||
setSensorData,
|
||||
setPlcTimers,
|
||||
setTotalRuntime,
|
||||
setGallonsProduced,
|
||||
setApiStatus,
|
||||
setError,
|
||||
setIsRunning,
|
||||
setCurrentStep,
|
||||
setDtsTask,
|
||||
sensorData,
|
||||
getSystemStatus
|
||||
} = stateSetters;
|
||||
|
||||
const API_HOST = 'http://localhost:5000';
|
||||
|
||||
const fetchApiStatus = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_HOST}/api/status`);
|
||||
const data = await response.json();
|
||||
|
||||
const isConnected = data.connection_status === 'connected' && data.plc_config?.connected;
|
||||
setApiStatus({
|
||||
connected: isConnected,
|
||||
lastUpdate: new Date(data.timestamp || data.last_update),
|
||||
plcIp: data.plc_config?.ip
|
||||
});
|
||||
return isConnected;
|
||||
} catch (err) {
|
||||
setApiStatus({ connected: false, lastUpdate: new Date(), error: err.message });
|
||||
return false;
|
||||
}
|
||||
}, [setApiStatus]);
|
||||
|
||||
const fetchAllData = useCallback(async () => {
|
||||
try {
|
||||
// Base keys - always needed for UI display
|
||||
const baseKeys = [
|
||||
// Critical sensors
|
||||
'1000', // System Mode
|
||||
'1003', // Feed Pressure (Shore)
|
||||
'1007', // High Pressure #2
|
||||
'1017', // Water Temperature
|
||||
'1120', // Brine Flowmeter
|
||||
'1122', // 2nd Pass Product Flowmeter
|
||||
'1124', // Product TDS #2
|
||||
|
||||
// Digital Outputs
|
||||
'257', // Low Pressure Pump
|
||||
'258', // High Pressure Pump
|
||||
'259', // Product Water/Quality
|
||||
'260', // Flush Solenoid
|
||||
'264', // Double Pass Solenoid
|
||||
'265' // Shore Feed Solenoid
|
||||
];
|
||||
|
||||
// Conditionally add timer keys only when needed
|
||||
const timerKeys = [];
|
||||
const currentMode = sensorData.systemMode;
|
||||
|
||||
// Only query timers during active DTS steps that use them
|
||||
if (currentMode === 5) {
|
||||
// Shore Pressure Flush - need Step 2 timer
|
||||
timerKeys.push('128'); // DTS Step 2 Priming Timer
|
||||
} else if (currentMode === 6) {
|
||||
// High Pressure Init - need Step 3 timer
|
||||
timerKeys.push('129'); // DTS Step 3 Init Timer
|
||||
} else if (currentMode === 8) {
|
||||
// Fresh Water Flush - need Step 6 timer
|
||||
timerKeys.push('139'); // DTS Step 6 Flush Timer
|
||||
}
|
||||
// Mode 34 (DTS Requested) and Mode 7 (Water Production) don't need timers
|
||||
|
||||
const allKeys = [...baseKeys, ...timerKeys].join(',');
|
||||
|
||||
const response = await fetch(`${API_HOST}/api/select?keys=${allKeys}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch data');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Helper function to find output by name
|
||||
const findOutputByName = (outputs, name) => {
|
||||
for (const register of Object.values(outputs)) {
|
||||
const bit = register.bits?.find(bit => bit.name === name);
|
||||
if (bit) return bit.active;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Helper function to find sensor by name or ID
|
||||
const getSensorValue = (sensors, id) => {
|
||||
const sensor = sensors[id];
|
||||
return sensor ? sensor.descriptive_value : 0;
|
||||
};
|
||||
|
||||
// Update sensor data from actual API response
|
||||
if (data.sensors) {
|
||||
const newSystemMode = getSensorValue(data.sensors, '1000');
|
||||
|
||||
setSensorData(prevData => {
|
||||
const newSensorData = {
|
||||
highPressure: getSensorValue(data.sensors, '1007'), // High Pressure #2
|
||||
waterTemp: getSensorValue(data.sensors, '1017'), // Water Temperature
|
||||
shorePressure: getSensorValue(data.sensors, '1003'), // Feed Pressure
|
||||
shoreValveOpen: findOutputByName(data.outputs, 'Shore Feed Solenoid'), // Shore valve status
|
||||
tds: getSensorValue(data.sensors, '1124'), // Product TDS #2
|
||||
brineFlow: getSensorValue(data.sensors, '1120'), // Brine Flowmeter
|
||||
productFlow: getSensorValue(data.sensors, '1122'), // 2nd Pass Product Flowmeter
|
||||
pumpOn: findOutputByName(data.outputs, 'High Pressure Pump'), // High Pressure Pump output
|
||||
systemMode: newSystemMode
|
||||
};
|
||||
|
||||
// If system mode changed, log it for debugging
|
||||
if (prevData.systemMode !== newSystemMode) {
|
||||
console.log(`System mode changed: ${prevData.systemMode} -> ${newSystemMode}`);
|
||||
}
|
||||
|
||||
return newSensorData;
|
||||
});
|
||||
|
||||
// Always sync UI state with system mode
|
||||
const systemStatus = getSystemStatus(newSystemMode);
|
||||
const systemIsRunning = [5, 6, 7, 8].includes(newSystemMode);
|
||||
|
||||
// Keep running state true if we're in mode 34 but have an active DTS task
|
||||
// This handles the case where system briefly returns to mode 34 between steps
|
||||
setIsRunning(prevRunning => {
|
||||
// If we have an active DTS task, stay running even in mode 34
|
||||
if (data.timers?.dts?.is_active || prevRunning && newSystemMode === 34) {
|
||||
return true;
|
||||
}
|
||||
return systemIsRunning;
|
||||
});
|
||||
|
||||
setCurrentStep(systemStatus.step);
|
||||
|
||||
// Extract PLC timer values only for active timers
|
||||
if (data.timers && Object.keys(data.timers).length > 0) {
|
||||
const getTimerValue = (timerId) => {
|
||||
const timer = data.timers[timerId];
|
||||
return timer && timer.active ? timer.scaled_value : 0;
|
||||
};
|
||||
|
||||
setPlcTimers(prevTimers => {
|
||||
const newTimers = { ...prevTimers };
|
||||
|
||||
// Update only the timers we queried
|
||||
if (newSystemMode === 5) {
|
||||
newTimers.step1Timer = getTimerValue('128'); // DTS Step 2 Priming Timer
|
||||
} else if (newSystemMode === 6) {
|
||||
newTimers.step2Timer = getTimerValue('129'); // DTS Step 3 Init Timer
|
||||
} else if (newSystemMode === 8) {
|
||||
newTimers.step4Timer = getTimerValue('139'); // DTS Step 6 Flush Timer
|
||||
}
|
||||
// Only clear timers if we're truly idle (no DTS task active)
|
||||
else if (newSystemMode === 34 && !data.timers?.dts?.is_active) {
|
||||
// Reset timers only when completely idle
|
||||
newTimers.step0Timer = 0;
|
||||
newTimers.step1Timer = 0;
|
||||
newTimers.step2Timer = 0;
|
||||
newTimers.step3Timer = 0;
|
||||
newTimers.step4Timer = 0;
|
||||
}
|
||||
// Keep current timers for mode 7 (water production) - don't reset
|
||||
|
||||
return newTimers;
|
||||
});
|
||||
|
||||
// Handle legacy timer data if available for gallons/runtime
|
||||
const dtsData = data.timers.dts;
|
||||
if (dtsData) {
|
||||
setTotalRuntime(dtsData.total_runtime || 0);
|
||||
setGallonsProduced(dtsData.gallons_produced || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setApiStatus({
|
||||
connected: true,
|
||||
lastUpdate: new Date(),
|
||||
plcIp: data.status?.connection_status === 'connected' ? '192.168.1.15' : null
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
setApiStatus({ connected: false, lastUpdate: new Date(), error: err.message });
|
||||
}
|
||||
}, [sensorData.systemMode, setSensorData, setIsRunning, setCurrentStep, setPlcTimers, setTotalRuntime, setGallonsProduced, setError, setApiStatus, getSystemStatus]); // Depend on system mode to rebuild query when needed
|
||||
|
||||
// New async DTS start function
|
||||
const startDTSAsync = async (params = {}) => {
|
||||
try {
|
||||
const response = await fetch(`${API_HOST}/api/dts/start`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(params)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to start DTS');
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setDtsTask({
|
||||
taskId: result.task_id,
|
||||
status: 'running',
|
||||
progressPercent: 0,
|
||||
currentStep: 'initializing',
|
||||
stepDescription: 'Starting DTS sequence...',
|
||||
isComplete: false,
|
||||
isRunning: true
|
||||
});
|
||||
|
||||
// Start polling for progress
|
||||
pollDTSProgress(result.task_id);
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to start DTS');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Start failed: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Poll for DTS progress updates
|
||||
const pollDTSProgress = async (taskId) => {
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_HOST}/api/dts/status/${taskId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
clearInterval(pollInterval);
|
||||
setError('Lost connection to DTS task');
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await response.json();
|
||||
const task = status.task;
|
||||
|
||||
setDtsTask({
|
||||
taskId: task.task_id,
|
||||
status: task.status,
|
||||
progressPercent: task.progress_percent || 0,
|
||||
currentStep: task.current_step || '',
|
||||
stepDescription: task.step_description || '',
|
||||
isComplete: task.is_complete || false,
|
||||
isRunning: task.is_running || false
|
||||
});
|
||||
|
||||
// Stop polling when complete or failed
|
||||
if (task.is_complete || task.status === 'failed' || task.status === 'cancelled') {
|
||||
clearInterval(pollInterval);
|
||||
|
||||
if (task.status === 'failed') {
|
||||
setError(`DTS failed: ${task.step_description}`);
|
||||
} else if (task.status === 'cancelled') {
|
||||
setError('DTS was cancelled');
|
||||
}
|
||||
|
||||
// Clear task after a delay to show completion
|
||||
setTimeout(() => {
|
||||
setDtsTask({
|
||||
taskId: null,
|
||||
status: null,
|
||||
progressPercent: 0,
|
||||
currentStep: '',
|
||||
stepDescription: '',
|
||||
isComplete: false,
|
||||
isRunning: false
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
} catch (err) {
|
||||
clearInterval(pollInterval);
|
||||
setError(`Progress polling failed: ${err.message}`);
|
||||
}
|
||||
}, 1000); // Poll every second
|
||||
|
||||
// Store interval ID for cleanup
|
||||
return pollInterval;
|
||||
};
|
||||
|
||||
// New async DTS stop function
|
||||
const stopDTSAsync = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_HOST}/api/dts/stop`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to stop DTS');
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// If we get a task ID, poll for progress
|
||||
if (result.task_id) {
|
||||
setDtsTask({
|
||||
taskId: result.task_id,
|
||||
status: 'stopping',
|
||||
progressPercent: 0,
|
||||
currentStep: 'stopping',
|
||||
stepDescription: 'Stopping DTS sequence...',
|
||||
isComplete: false,
|
||||
isRunning: true
|
||||
});
|
||||
|
||||
pollDTSProgress(result.task_id);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to stop DTS');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Stop failed: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// New async DTS skip function
|
||||
const skipDTSAsync = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_HOST}/api/dts/skip`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to skip step');
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// If we get a task ID, poll for progress
|
||||
if (result.task_id) {
|
||||
setDtsTask({
|
||||
taskId: result.task_id,
|
||||
status: 'skipping',
|
||||
progressPercent: 0,
|
||||
currentStep: 'skipping',
|
||||
stepDescription: 'Skipping current step...',
|
||||
isComplete: false,
|
||||
isRunning: true
|
||||
});
|
||||
|
||||
pollDTSProgress(result.task_id);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to skip step');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Skip failed: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Cancel DTS task (for startup cancellation)
|
||||
const cancelDTSTask = async (taskId) => {
|
||||
if (!taskId) return false;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_HOST}/api/dts/cancel/${taskId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to cancel DTS');
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(`Cancel failed: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fetchApiStatus,
|
||||
fetchAllData,
|
||||
startDTSAsync,
|
||||
stopDTSAsync,
|
||||
skipDTSAsync,
|
||||
cancelDTSTask,
|
||||
pollDTSProgress
|
||||
};
|
||||
};
|
||||
185
src/hooks/useWaterMakerState.js
Normal file
185
src/hooks/useWaterMakerState.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export const useWaterMakerState = () => {
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [totalRuntime, setTotalRuntime] = useState(0);
|
||||
const [gallonsProduced, setGallonsProduced] = useState(0);
|
||||
|
||||
// DTS Task tracking
|
||||
const [dtsTask, setDtsTask] = useState({
|
||||
taskId: null,
|
||||
status: null,
|
||||
progressPercent: 0,
|
||||
currentStep: '',
|
||||
stepDescription: '',
|
||||
isComplete: false,
|
||||
isRunning: false
|
||||
});
|
||||
|
||||
// API connection state
|
||||
const [apiStatus, setApiStatus] = useState({ connected: false, lastUpdate: null });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// Real sensor data from API
|
||||
const [sensorData, setSensorData] = useState({
|
||||
highPressure: 0,
|
||||
waterTemp: 0,
|
||||
shorePressure: 0,
|
||||
shoreValveOpen: false,
|
||||
tds: 0,
|
||||
brineFlow: 0,
|
||||
productFlow: 0,
|
||||
pumpOn: false,
|
||||
systemMode: 34 // Start with DTS Requested state
|
||||
});
|
||||
|
||||
// PLC Timer data
|
||||
const [plcTimers, setPlcTimers] = useState({
|
||||
step0Timer: 0, // DTS Step 1 Timer (138)
|
||||
step1Timer: 0, // DTS Step 2 Priming Timer (128)
|
||||
step2Timer: 0, // DTS Step 3 Init Timer (129)
|
||||
step3Timer: 0, // DTS Step 4 Timer (133)
|
||||
step4Timer: 0, // DTS Step 6 Flush Timer (139)
|
||||
});
|
||||
|
||||
const steps = [
|
||||
{ name: 'DTS Requested', duration: null, color: 'bg-blue-500' },
|
||||
{ name: 'Shore Pressure Flush', duration: 180, color: 'bg-yellow-500' },
|
||||
{ name: 'High Pressure Init', duration: 60, color: 'bg-orange-500' },
|
||||
{ name: 'Water Production', duration: null, color: 'bg-green-500' },
|
||||
{ name: 'Fresh Water Flush', duration: 60, color: 'bg-purple-500' }
|
||||
];
|
||||
|
||||
const getSystemStatus = useCallback((mode) => {
|
||||
const statusMap = {
|
||||
34: { text: "DTS Requested - Press START", step: 0, color: "bg-blue-100 text-blue-800" },
|
||||
5: { text: "DTS Startup - Shore Pressure Flush (180s)", step: 1, color: "bg-yellow-100 text-yellow-800" },
|
||||
6: { text: "DTS Startup - High Pressure Init (60s)", step: 2, color: "bg-orange-100 text-orange-800" },
|
||||
7: { text: "DTS Running - Water Production", step: 3, color: "bg-green-100 text-green-800" },
|
||||
8: { text: "Fresh Water Flush", step: 4, color: "bg-purple-100 text-purple-800" }
|
||||
};
|
||||
return statusMap[mode] || { text: `Unknown Mode: ${mode}`, step: 0, color: "bg-gray-100 text-gray-800" };
|
||||
}, []);
|
||||
|
||||
// Calculate progress percentage based on current step and PLC timer
|
||||
const getProgressPercentage = () => {
|
||||
// If we have an active DTS startup task, use its progress
|
||||
if (dtsTask.isRunning && dtsTask.progressPercent > 0) {
|
||||
return dtsTask.progressPercent;
|
||||
}
|
||||
|
||||
if (!isRunning) return 0;
|
||||
|
||||
switch (currentStep) {
|
||||
case 0: // DTS Requested
|
||||
return plcTimers.step0Timer > 0 ? 25 : 10; // Small progress when timer starts
|
||||
case 1: // Shore Pressure Flush (180s)
|
||||
return Math.min(((180 - plcTimers.step1Timer) / 180) * 100, 100);
|
||||
case 2: // High Pressure Init (60s)
|
||||
return Math.min(((60 - plcTimers.step2Timer) / 60) * 100, 100);
|
||||
case 3: // Water Production (indefinite)
|
||||
return Math.min((plcTimers.step3Timer / 300) * 100, 100); // Show progress up to 5 minutes
|
||||
case 4: // Fresh Water Flush (60s)
|
||||
return Math.min(((60 - plcTimers.step4Timer) / 60) * 100, 100);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Get current step name for display
|
||||
const getCurrentStepName = () => {
|
||||
// If we have an active DTS startup task, use its step description
|
||||
if (dtsTask.isRunning && dtsTask.stepDescription) {
|
||||
return dtsTask.stepDescription;
|
||||
}
|
||||
|
||||
return isRunning && currentStep < steps.length ? steps[currentStep].name : 'Ready';
|
||||
};
|
||||
|
||||
// Get current step color
|
||||
const getCurrentStepColor = () => {
|
||||
// Use different colors for different DTS operations
|
||||
if (dtsTask.isRunning) {
|
||||
switch (dtsTask.status) {
|
||||
case 'stopping': return 'bg-red-500';
|
||||
case 'skipping': return 'bg-yellow-500';
|
||||
default: return 'bg-blue-500';
|
||||
}
|
||||
}
|
||||
|
||||
return isRunning && currentStep < steps.length ? steps[currentStep].color : 'bg-gray-300';
|
||||
};
|
||||
|
||||
// Get current step timer value
|
||||
const getCurrentStepTimer = () => {
|
||||
switch (currentStep) {
|
||||
case 0: return plcTimers.step0Timer;
|
||||
case 1: return plcTimers.step1Timer;
|
||||
case 2: return plcTimers.step2Timer;
|
||||
case 3: return plcTimers.step3Timer;
|
||||
case 4: return plcTimers.step4Timer;
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Get remaining time for current step
|
||||
const getRemainingTime = () => {
|
||||
const currentTimer = getCurrentStepTimer();
|
||||
switch (currentStep) {
|
||||
case 0: return null; // No fixed duration
|
||||
case 1: return Math.max(0, currentTimer); // Timer counts down
|
||||
case 2: return Math.max(0, currentTimer); // Timer counts down
|
||||
case 3: return null; // Indefinite duration
|
||||
case 4: return Math.max(0, currentTimer); // Timer counts down
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const getStatusColor = (value, min, max) => {
|
||||
if (value >= min && value <= max) return 'text-green-500';
|
||||
return 'text-red-500';
|
||||
};
|
||||
|
||||
return {
|
||||
// State values
|
||||
isRunning,
|
||||
setIsRunning,
|
||||
currentStep,
|
||||
setCurrentStep,
|
||||
totalRuntime,
|
||||
setTotalRuntime,
|
||||
gallonsProduced,
|
||||
setGallonsProduced,
|
||||
dtsTask,
|
||||
setDtsTask,
|
||||
apiStatus,
|
||||
setApiStatus,
|
||||
loading,
|
||||
setLoading,
|
||||
error,
|
||||
setError,
|
||||
sensorData,
|
||||
setSensorData,
|
||||
plcTimers,
|
||||
setPlcTimers,
|
||||
|
||||
// Helper functions
|
||||
steps,
|
||||
getSystemStatus,
|
||||
getProgressPercentage,
|
||||
getCurrentStepName,
|
||||
getCurrentStepColor,
|
||||
getCurrentStepTimer,
|
||||
getRemainingTime,
|
||||
formatTime,
|
||||
getStatusColor
|
||||
};
|
||||
};
|
||||
66
src/screens/ConfigScreen.js
Normal file
66
src/screens/ConfigScreen.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { Settings, Sliders, Wifi, Database } from 'lucide-react';
|
||||
|
||||
const ConfigScreen = () => {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Settings className="text-blue-600" size={24} />
|
||||
<h1 className="text-2xl font-bold text-gray-800">Water Maker Configuration</h1>
|
||||
</div>
|
||||
|
||||
{/* Configuration Sections */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-blue-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Sliders className="text-blue-600" size={20} />
|
||||
<h3 className="font-semibold text-blue-700">System Parameters</h3>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-blue-600">
|
||||
<div>High Pressure Range: 50-70 PSI</div>
|
||||
<div>Shore Pressure Range: 20-40 PSI</div>
|
||||
<div>Temperature Limits: 50-90°F</div>
|
||||
<div>TDS Threshold: 0-12 PPM</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Wifi className="text-green-600" size={20} />
|
||||
<h3 className="font-semibold text-green-700">Network Settings</h3>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-green-600">
|
||||
<div>PLC IP: 198.18.100.141</div>
|
||||
<div>API Port: 5000</div>
|
||||
<div>Poll Interval: 1000ms</div>
|
||||
<div>Connection Timeout: 5s</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Database className="text-purple-600" size={20} />
|
||||
<h3 className="font-semibold text-purple-700">Data Logging</h3>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-purple-600">
|
||||
<div>Log Retention: 30 days</div>
|
||||
<div>Sample Rate: 1 Hz</div>
|
||||
<div>Archive Format: CSV</div>
|
||||
<div>Backup Schedule: Daily</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-gray-500">
|
||||
<p className="text-lg mb-2">⚙️ Configuration Panel Coming Soon</p>
|
||||
<p>This screen will allow you to modify system parameters, network settings, and operational thresholds.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigScreen;
|
||||
224
src/screens/ControlScreen.js
Normal file
224
src/screens/ControlScreen.js
Normal file
@@ -0,0 +1,224 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import ConnectionStatus from '../components/ConnectionStatus';
|
||||
import SystemStatus from '../components/SystemStatus';
|
||||
import DTSOperationStatus from '../components/DTSOperationStatus';
|
||||
import ControlButtons from '../components/ControlButtons';
|
||||
import ProcessProgress from '../components/ProcessProgress';
|
||||
import SensorGrid from '../components/SensorGrid';
|
||||
import StartDialog from '../components/StartDialog';
|
||||
import { useWaterMakerAPI } from '../hooks/useWaterMakerAPI';
|
||||
import { useWaterMakerState } from '../hooks/useWaterMakerState';
|
||||
|
||||
const ControlScreen = () => {
|
||||
const [showStartDialog, setShowStartDialog] = useState(false);
|
||||
const [runMode, setRunMode] = useState('continuous');
|
||||
const [targetGallons, setTargetGallons] = useState('');
|
||||
const [targetTime, setTargetTime] = useState('');
|
||||
|
||||
// Custom hooks for state management
|
||||
const stateHook = useWaterMakerState();
|
||||
const {
|
||||
isRunning,
|
||||
currentStep,
|
||||
totalRuntime,
|
||||
gallonsProduced,
|
||||
dtsTask,
|
||||
sensorData,
|
||||
plcTimers,
|
||||
apiStatus,
|
||||
loading,
|
||||
error,
|
||||
setError,
|
||||
setSensorData,
|
||||
setPlcTimers,
|
||||
setTotalRuntime,
|
||||
setGallonsProduced,
|
||||
setApiStatus,
|
||||
setIsRunning,
|
||||
setCurrentStep,
|
||||
setDtsTask,
|
||||
getSystemStatus
|
||||
} = stateHook;
|
||||
|
||||
// API calls with state setters
|
||||
const {
|
||||
fetchApiStatus,
|
||||
fetchAllData,
|
||||
startDTSAsync,
|
||||
stopDTSAsync,
|
||||
skipDTSAsync,
|
||||
cancelDTSTask
|
||||
} = useWaterMakerAPI({
|
||||
setSensorData,
|
||||
setPlcTimers,
|
||||
setTotalRuntime,
|
||||
setGallonsProduced,
|
||||
setApiStatus,
|
||||
setError,
|
||||
setIsRunning,
|
||||
setCurrentStep,
|
||||
setDtsTask,
|
||||
sensorData,
|
||||
getSystemStatus
|
||||
});
|
||||
|
||||
// Handle start process
|
||||
const handleStart = () => {
|
||||
setShowStartDialog(true);
|
||||
};
|
||||
|
||||
const confirmStart = async () => {
|
||||
const success = await startDTSAsync({
|
||||
mode: runMode,
|
||||
targetGallons: targetGallons ? parseFloat(targetGallons) : null,
|
||||
targetTime: targetTime ? parseInt(targetTime) : null
|
||||
});
|
||||
|
||||
if (success) {
|
||||
setShowStartDialog(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle stop process
|
||||
const handleStop = async () => {
|
||||
await stopDTSAsync();
|
||||
};
|
||||
|
||||
// Handle skip step
|
||||
const handleSkipStep = async () => {
|
||||
await skipDTSAsync();
|
||||
};
|
||||
|
||||
// Data fetching effect
|
||||
useEffect(() => {
|
||||
const initializeConnection = async () => {
|
||||
stateHook.setLoading(true);
|
||||
await fetchApiStatus();
|
||||
await fetchAllData();
|
||||
stateHook.setLoading(false);
|
||||
};
|
||||
|
||||
initializeConnection();
|
||||
|
||||
// Set up polling using specific keys - very efficient, can poll frequently
|
||||
const pollInterval = setInterval(fetchAllData, 1000); // 1 second intervals with minimal PLC load
|
||||
|
||||
return () => clearInterval(pollInterval);
|
||||
}, [fetchAllData, fetchApiStatus, stateHook]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Connecting to Water Maker System...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Connection Status */}
|
||||
<ConnectionStatus apiStatus={apiStatus} error={error} />
|
||||
|
||||
{/* System Status */}
|
||||
<SystemStatus
|
||||
apiStatus={apiStatus}
|
||||
sensorData={sensorData}
|
||||
currentStep={currentStep}
|
||||
isRunning={isRunning}
|
||||
plcTimers={plcTimers}
|
||||
/>
|
||||
|
||||
{/* DTS Operation Status */}
|
||||
<DTSOperationStatus dtsTask={dtsTask} />
|
||||
|
||||
{/* Debug Timer Panel */}
|
||||
{isRunning && !dtsTask.isRunning && [5, 6, 8].includes(sensorData.systemMode) && (
|
||||
<div className="bg-gray-50 rounded-lg shadow p-4 mb-4">
|
||||
<h3 className="font-semibold text-gray-700 mb-3 text-center">Active Timer</h3>
|
||||
<div className="flex justify-center">
|
||||
{sensorData.systemMode === 5 && (
|
||||
<div className="p-3 rounded bg-yellow-200 text-center">
|
||||
<div className="font-medium">Shore Pressure Flush</div>
|
||||
<div className="text-xl font-bold">{plcTimers.step1Timer.toFixed(1)}s</div>
|
||||
<div className="text-sm text-gray-600">Remaining</div>
|
||||
</div>
|
||||
)}
|
||||
{sensorData.systemMode === 6 && (
|
||||
<div className="p-3 rounded bg-orange-200 text-center">
|
||||
<div className="font-medium">High Pressure Init</div>
|
||||
<div className="text-xl font-bold">{plcTimers.step2Timer.toFixed(1)}s</div>
|
||||
<div className="text-sm text-gray-600">Remaining</div>
|
||||
</div>
|
||||
)}
|
||||
{sensorData.systemMode === 8 && (
|
||||
<div className="p-3 rounded bg-purple-200 text-center">
|
||||
<div className="font-medium">Fresh Water Flush</div>
|
||||
<div className="text-xl font-bold">{plcTimers.step4Timer.toFixed(1)}s</div>
|
||||
<div className="text-sm text-gray-600">Remaining</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-100 border border-red-300 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-red-800">
|
||||
<AlertCircle size={16} />
|
||||
<span className="font-medium">System Error:</span>
|
||||
</div>
|
||||
<p className="text-red-700 text-sm mt-1">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Control Buttons */}
|
||||
<ControlButtons
|
||||
isRunning={isRunning}
|
||||
dtsTask={dtsTask}
|
||||
apiStatus={apiStatus}
|
||||
currentStep={currentStep}
|
||||
plcTimers={plcTimers}
|
||||
onStart={handleStart}
|
||||
onStop={handleStop}
|
||||
onSkipStep={handleSkipStep}
|
||||
onCancelStartup={() => cancelDTSTask(dtsTask.taskId)}
|
||||
/>
|
||||
|
||||
{/* Process Steps Progress */}
|
||||
<ProcessProgress
|
||||
isRunning={isRunning}
|
||||
dtsTask={dtsTask}
|
||||
totalRuntime={totalRuntime}
|
||||
gallonsProduced={gallonsProduced}
|
||||
currentStep={currentStep}
|
||||
plcTimers={plcTimers}
|
||||
/>
|
||||
|
||||
{/* Sensor Grid */}
|
||||
<SensorGrid sensorData={sensorData} />
|
||||
|
||||
{/* Start Dialog */}
|
||||
<StartDialog
|
||||
showStartDialog={showStartDialog}
|
||||
runMode={runMode}
|
||||
targetGallons={targetGallons}
|
||||
targetTime={targetTime}
|
||||
apiStatus={apiStatus}
|
||||
onClose={() => setShowStartDialog(false)}
|
||||
onRunModeChange={setRunMode}
|
||||
onTargetGallonsChange={setTargetGallons}
|
||||
onTargetTimeChange={setTargetTime}
|
||||
onConfirm={confirmStart}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControlScreen;
|
||||
81
src/screens/MaintenanceScreen.js
Normal file
81
src/screens/MaintenanceScreen.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import { Wrench, Calendar, AlertTriangle, CheckCircle, Filter } from 'lucide-react';
|
||||
|
||||
const MaintenanceScreen = () => {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Wrench className="text-blue-600" size={24} />
|
||||
<h1 className="text-2xl font-bold text-gray-800">Water Maker Maintenance</h1>
|
||||
</div>
|
||||
|
||||
{/* Maintenance Status Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-green-50 rounded-lg p-4 text-center">
|
||||
<CheckCircle className="text-green-600 mx-auto mb-2" size={24} />
|
||||
<div className="text-lg font-bold text-green-700">System Health</div>
|
||||
<div className="text-sm text-green-600">All Systems Normal</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-50 rounded-lg p-4 text-center">
|
||||
<Calendar className="text-yellow-600 mx-auto mb-2" size={24} />
|
||||
<div className="text-lg font-bold text-yellow-700">Next Service</div>
|
||||
<div className="text-sm text-yellow-600">In 15 days</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 rounded-lg p-4 text-center">
|
||||
<Filter className="text-blue-600 mx-auto mb-2" size={24} />
|
||||
<div className="text-lg font-bold text-blue-700">Filter Status</div>
|
||||
<div className="text-sm text-blue-600">75% Capacity</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 rounded-lg p-4 text-center">
|
||||
<AlertTriangle className="text-orange-600 mx-auto mb-2" size={24} />
|
||||
<div className="text-lg font-bold text-orange-700">Alerts</div>
|
||||
<div className="text-sm text-orange-600">0 Active</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Maintenance Schedule */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-700 mb-4">Maintenance Schedule</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<div className="font-medium">Filter Replacement</div>
|
||||
<div className="text-sm text-gray-500">Replace RO membrane filters</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Due: May 15, 2025</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<div className="font-medium">Pump Inspection</div>
|
||||
<div className="text-sm text-gray-500">Check high pressure pump operation</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Due: June 1, 2025</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<div className="font-medium">System Flush</div>
|
||||
<div className="text-sm text-gray-500">Complete system cleaning cycle</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Due: July 1, 2025</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-gray-500">
|
||||
<p className="text-lg mb-2">🔧 Advanced Maintenance Tools Coming Soon</p>
|
||||
<p>This screen will provide maintenance schedules, system diagnostics, and component health monitoring.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaintenanceScreen;
|
||||
51
src/screens/StatsScreen.js
Normal file
51
src/screens/StatsScreen.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { BarChart3, TrendingUp, Clock, Droplets } from 'lucide-react';
|
||||
|
||||
const StatsScreen = () => {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<BarChart3 className="text-blue-600" size={24} />
|
||||
<h1 className="text-2xl font-bold text-gray-800">Water Maker Statistics</h1>
|
||||
</div>
|
||||
|
||||
{/* Statistics Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-blue-50 rounded-lg p-4 text-center">
|
||||
<Droplets className="text-blue-600 mx-auto mb-2" size={24} />
|
||||
<div className="text-2xl font-bold text-blue-700">1,234</div>
|
||||
<div className="text-sm text-blue-600">Total Gallons Produced</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 rounded-lg p-4 text-center">
|
||||
<Clock className="text-green-600 mx-auto mb-2" size={24} />
|
||||
<div className="text-2xl font-bold text-green-700">45.2h</div>
|
||||
<div className="text-sm text-green-600">Total Runtime</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 rounded-lg p-4 text-center">
|
||||
<TrendingUp className="text-purple-600 mx-auto mb-2" size={24} />
|
||||
<div className="text-2xl font-bold text-purple-700">2.1 GPM</div>
|
||||
<div className="text-sm text-purple-600">Average Flow Rate</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 rounded-lg p-4 text-center">
|
||||
<BarChart3 className="text-orange-600 mx-auto mb-2" size={24} />
|
||||
<div className="text-2xl font-bold text-orange-700">87%</div>
|
||||
<div className="text-sm text-orange-600">System Efficiency</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-gray-500">
|
||||
<p className="text-lg mb-2">📊 Statistical Analysis Coming Soon</p>
|
||||
<p>This screen will display historical data, trends, and performance metrics for the water maker system.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsScreen;
|
||||
Reference in New Issue
Block a user