26 KiB
Architecture Research: RTU Web Interface
Domain: Embedded IoT/SCADA Web Interface for Rainfall Monitoring
Researched: 2026-03-13
Confidence: HIGH
System Overview
Modern RTU (Remote Terminal Unit) web interfaces are embedded applications that bridge physical sensors with human operators. For a Raspberry Pi-based rainfall monitoring system, the architecture must balance real-time data visualization, configuration management, and remote accessibility while operating on constrained hardware.
┌─────────────────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Dashboard UI │ │ Settings UI │ │ Calibration UI │ │
│ │ (Home Screen) │ │ (11 Submenus) │ │ (ADC/Levels) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ Navigation/Router │ │
│ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────────┤
│ STATE MANAGEMENT LAYER │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Sensor Data Store │ │
│ │ (Rainfall, Voltage, ADC, Status) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Configuration Store │ │
│ │ (Settings, Calibration, Network, Station Info) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────────┤
│ DATA ACCESS LAYER │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Sensor API │ │ Config API │ │ File Manager │ │ Network API │ │
│ │ (Real-time) │ │ (CRUD) │ │ (CSV/Flash) │ │ (FTP/SCP) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │ │
├───────────┴──────────────┴──────────────┴──────────────┴───────────────────┤
│ HARDWARE INTERFACE LAYER │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Rainfall │ │ ADC Inputs │ │ GPIO/Power │ │ Network │ │
│ │ Sensor │ │ (4-20mA) │ │ Monitoring │ │ Interface │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Component Responsibilities
| Component | Responsibility | Implementation Notes |
|---|---|---|
| Dashboard UI | Real-time sensor visualization, status indicators, quick actions | Optimized for 1024x600, refresh every 1-5s |
| Settings UI | 11 configuration views (Utility, Calibration, Flash, Mobile, ADC, Rain, EVAP, GPRS, Level, Siren, Network) | Form-based, validation, persistence |
| Calibration UI | ADC channel calibration, level sensor calibration | Wizard-style, step-by-step |
| Navigation | Route management between views, breadcrumb trails | React Router, conditional rendering |
| Sensor Data Store | Real-time sensor readings, historical data buffering | In-memory + localStorage for resilience |
| Config Store | Application settings, station configuration, calibration values | localStorage/IndexedDB |
| API Layer | Abstract hardware communication, RESTful interface to backend | Fetch API, error handling, retries |
| Hardware Interface | GPIO access, serial communication, ADC reading | Python backend or Node.js native addons |
Recommended Project Structure
src/
├── components/
│ ├── ui/ # shadcn/ui base components
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── input.tsx
│ │ ├── select.tsx
│ │ └── ...
│ ├── dashboard/ # Dashboard-specific components
│ │ ├── SensorCard.tsx
│ │ ├── RainfallDisplay.tsx
│ │ ├── VoltageGauge.tsx
│ │ └── StatusBar.tsx
│ ├── settings/ # Settings view components
│ │ ├── SettingsLayout.tsx
│ │ ├── SettingsNav.tsx
│ │ └── forms/
│ │ ├── AdcSettingsForm.tsx
│ │ ├── NetworkSettingsForm.tsx
│ │ └── ...
│ └── layout/ # App shell components
│ ├── AppLayout.tsx
│ ├── Header.tsx
│ ├── Sidebar.tsx
│ └── KioskWrapper.tsx
├── hooks/
│ ├── useSensorData.ts # Real-time sensor data hook
│ ├── useConfig.ts # Configuration management hook
│ ├── useHardware.ts # Hardware status hook
│ └── useNetwork.ts # Network status hook
├── stores/
│ ├── sensorStore.ts # Sensor data state management
│ ├── configStore.ts # Configuration state management
│ └── uiStore.ts # UI state (theme, layout mode)
├── services/
│ ├── api.ts # API client configuration
│ ├── sensorService.ts # Sensor data API calls
│ ├── configService.ts # Configuration API calls
│ └── fileService.ts # File management API calls
├── types/
│ ├── sensor.ts # Sensor data types
│ ├── config.ts # Configuration types
│ └── api.ts # API response types
├── utils/
│ ├── formatters.ts # Data formatting utilities
│ ├── validators.ts # Form validation
│ └── constants.ts # App constants
├── views/
│ ├── Dashboard.tsx # Main dashboard view
│ ├── Settings/ # Settings views
│ │ ├── index.tsx
│ │ ├── AdcSettings.tsx
│ │ ├── NetworkSettings.tsx
│ │ └── ...
│ ├── Calibration.tsx # Calibration view
│ └── FlashMemory.tsx # File manager view
├── App.tsx # Root component
├── main.tsx # Entry point
└── index.css # Global styles + Tailwind
Structure Rationale
- components/ui/: shadcn/ui components are copy-paste ready, open for modification
- components/dashboard/: Dashboard-specific visualization components, optimized for 7" display
- components/settings/: Modular settings forms, one per configuration category
- hooks/: Custom React hooks for data fetching and hardware interaction
- stores/: State management (Zustand recommended for minimal overhead)
- services/: API abstraction layer for hardware communication
- views/: Page-level components corresponding to routes
Architectural Patterns
Pattern 1: Dual-Mode Display Architecture
What: Single codebase supporting two display modes:
- Kiosk Mode (1024x600): Fixed layout, touch-optimized, no browser chrome
- Remote Mode (Full HD): Responsive layout, desktop-friendly, expanded details
When to use: When RTU has both local touchscreen display and remote web access requirements.
Trade-offs:
- Pros: Single codebase, consistent UX, easier maintenance
- Cons: Conditional complexity, testing matrix doubles
Implementation:
// Detect display mode based on port or viewport
const useDisplayMode = () => {
const [mode, setMode] = useState<'kiosk' | 'remote'>('kiosk');
useEffect(() => {
// Detect from window location (port 8080 = kiosk, 9090 = remote)
const port = window.location.port;
setMode(port === '9090' ? 'remote' : 'kiosk');
}, []);
return mode;
};
// Usage in components
const SensorCard = () => {
const mode = useDisplayMode();
return mode === 'kiosk' ? <CompactCard /> : <DetailedCard />;
};
Pattern 2: Polling-Based Real-Time Updates
What: Periodic data fetching with optimistic UI updates for sensor readings.
When to use: When WebSocket is not available or too resource-intensive for embedded hardware.
Trade-offs:
- Pros: Simple, works on constrained hardware, battery-friendly
- Cons: Not truly real-time, potential for stale data
Implementation:
const useSensorPolling = (interval = 5000) => {
const [data, setData] = useState<SensorData | null>(null);
const [isStale, setIsStale] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
const response = await sensorService.getCurrent();
setData(response);
setIsStale(false);
} catch (error) {
setIsStale(true);
}
};
fetchData();
const intervalId = setInterval(fetchData, interval);
return () => clearInterval(intervalId);
}, [interval]);
return { data, isStale };
};
Pattern 3: Offline-First Configuration
What: Configuration changes persisted locally first, synced to backend asynchronously.
When to use: When network connectivity is intermittent or when critical settings must survive power cycles.
Trade-offs:
- Pros: Resilient to failures, fast UI response, works offline
- Cons: Conflict resolution complexity, potential for desync
Implementation:
interface ConfigStore {
local: Config;
remote: Config | null;
syncStatus: 'synced' | 'pending' | 'error';
updateConfig: (update: Partial<Config>) => Promise<void>;
}
const useConfigStore = create<ConfigStore>((set, get) => ({
local: loadFromLocalStorage(),
remote: null,
syncStatus: 'synced',
updateConfig: async (update) => {
// Update local immediately
const newConfig = { ...get().local, ...update };
set({ local: newConfig, syncStatus: 'pending' });
saveToLocalStorage(newConfig);
// Sync to backend
try {
await configService.save(newConfig);
set({ remote: newConfig, syncStatus: 'synced' });
} catch (error) {
set({ syncStatus: 'error' });
// Will retry on next connection
}
}
}));
Data Flow
Request Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ USER ACTION │
│ (Touch/Button Click) │
└─────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ REACT COMPONENT │
│ (Event Handler Triggered) │
└─────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ CUSTOM HOOK │
│ (Business Logic / Validation) │
└─────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ SERVICE LAYER │
│ (API Call to Python Backend) │
└─────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ HARDWARE LAYER │
│ (GPIO / Serial / ADC Read/Write) │
└─────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ RESPONSE FLOW │
│ (Data → Store → Component Re-render) │
└─────────────────────────────────────────────────────────────────────────────┘
State Management
┌─────────────────────────────────────────────────────────────────────────────┐
│ ZUSTAND STORE │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Sensor Data │ │ Configuration │ │ UI State │ │
│ │ (Real-time) │ │ (Persistent) │ │ (Transient) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ Selectors / Hooks │ │
│ └─────────────────────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ React Components │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Key Data Flows
-
Sensor Data Flow:
- Hardware (GPIO/ADC) → Python Backend → REST API → React Hook → Component
- Update frequency: 1-5 seconds for display, configurable for logging
-
Configuration Flow:
- User Input → Validation → Local Storage → API Sync → Hardware Apply
- Two-phase commit: UI optimistic, backend async
-
File Management Flow:
- CSV files stored on Flash/USB → Python File Service → Frontend Display
- Upload/Download via HTTP multipart or WebDAV/FTP/SCP for remote
Build Order Implications
Based on component dependencies, suggested build order:
Phase 1: Foundation
├── Core UI components (shadcn/ui setup)
├── Type definitions
├── API client infrastructure
└── Basic routing
Phase 2: Dashboard
├── Sensor data hooks
├── Dashboard layout
├── Sensor card components
└── Real-time data display
Phase 3: Settings Framework
├── Settings navigation
├── Form components
├── Config store
└── Settings persistence
Phase 4: Individual Settings
├── Station Info
├── Date/Time
├── Network Setup
├── Mobile Settings
└── ADC Settings
Phase 5: Advanced Features
├── Calibration views
├── File manager
├── Network stack (FTP/SCP/SFTP/WebDAV)
└── CSV processing
Phase 6: Dual-Mode Support
├── Kiosk mode optimization (1024x600)
├── Remote mode layout (Full HD)
├── Responsive adaptations
└── Performance tuning
Critical Path:
- Dashboard requires sensor API → Build sensor service first
- Settings require config API → Build config service before settings forms
- File manager requires backend file service → Coordinate with Python backend team
Scaling Considerations
| Scale | Users | Architecture Adjustments |
|---|---|---|
| Single RTU | 1 local + few remote | Current architecture sufficient |
| Multi-RTU Fleet | 10-100 devices | Add centralized management dashboard |
| Enterprise | 1000+ devices | Separate management server, device fleet API |
Scaling Priorities for Single RTU
-
First bottleneck: UI responsiveness on Pi Zero 2 W
- Fix: Code splitting, lazy loading, virtual scrolling for large datasets
-
Second bottleneck: Storage for historical data
- Fix: Implement data retention policies, CSV rotation, external storage
-
Third bottleneck: Network throughput for file transfers
- Fix: Compression, chunked transfers, background sync
Anti-Patterns to Avoid
Anti-Pattern 1: Heavy Frameworks on Embedded
What people do: Use Next.js, heavy state management (Redux), or large component libraries.
Why it's wrong: Pi Zero 2 W has limited RAM (512MB) and single-core performance. Bundle size directly impacts load time and runtime performance.
Do this instead:
- Use Vite for minimal overhead build
- Zustand or React Context for state (not Redux)
- Copy-paste components (shadcn/ui) instead of importing entire libraries
- Tree-shake aggressively, analyze bundle size
Anti-Pattern 2: Real-Time Everything
What people do: WebSocket connections for all data, constant polling at high frequency.
Why it's wrong: Drains battery (if solar-powered), saturates network, unnecessary for rainfall data that changes slowly.
Do this instead:
- Adaptive polling: fast during events, slow during idle
- Only subscribe to actively viewed sensors
- Use caching and display stale data with indicators
Anti-Pattern 3: Tight Hardware Coupling
What people do: Direct GPIO access from frontend JavaScript (via unsafe methods).
Why it's wrong: Security risk, platform lock-in, hard to test.
Do this instead:
- Clean API boundary between UI and hardware
- Python backend service for hardware access
- Mock API for development/testing
Anti-Pattern 4: Ignoring Kiosk Constraints
What people do: Design for desktop, shoehorn into kiosk mode.
Why it's wrong: 1024x600 is cramped, touch targets must be large, no right-click context menus.
Do this instead:
- Design mobile-first, even for kiosk
- Minimum 44px touch targets
- Large fonts, high contrast
- No hover-dependent interactions
Integration Points
External Services
| Service | Integration Pattern | Notes |
|---|---|---|
| Sensor Hardware | REST API via Python backend | GPIO, I2C, SPI abstraction |
| Network Stack | Background service + API | FTP/SCP/SFTP/WebDAV daemons |
| CSV Export | File system + HTTP download | Scheduled generation, on-demand download |
| Remote Server | HTTP POST / MQTT | Data transmission, status heartbeat |
Internal Boundaries
| Boundary | Communication | Notes |
|---|---|---|
| Frontend ↔ Backend | HTTP REST API | JSON, stateless, retry logic |
| Backend ↔ Hardware | Python libraries | GPIOZero, smbus2, spidev |
| Config ↔ Storage | localStorage/IndexedDB | Offline resilience |
| UI ↔ State | Zustand hooks | Minimal re-renders |
Sources
- Wikipedia: Remote Terminal Unit (RTU) - https://en.wikipedia.org/wiki/Remote_terminal_unit
- React Documentation - https://react.dev/learn
- Vite Documentation - https://vitejs.dev/guide/
- shadcn/ui Documentation - https://ui.shadcn.com/docs
- MDN: Progressive Web Apps - https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps
- Raspberry Pi GPIO Documentation (training data)
- SCADA System Architecture Patterns (training data)
Architecture research for: TCKRTUIYO RTU Rainfall Monitoring System
Researched: 2026-03-13