This commit is contained in:
2025-06-10 18:18:25 +00:00
parent 45e68e181f
commit 3a11db1432
19 changed files with 1829 additions and 999 deletions

Binary file not shown.

111
REFACTORING_SUMMARY.md Normal file
View 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
View File

@@ -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",

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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;

View 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;

View 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
View 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;

View 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;

View 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: &lt;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;

View 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;

View 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;

View 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
};
};

View 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
};
};

View 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;

View 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;

View 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;

View 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;