UNPKG

je_nfc_sdk

Version:

A comprehensive React Native SDK for NFC-based device control and communication

982 lines • 54.7 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import React, { useState, useEffect, useRef, useImperativeHandle } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Alert, StatusBar, Animated, Image, SafeAreaView, Modal, ActivityIndicator, Dimensions } from 'react-native'; import NfcManager, { NfcTech } from 'react-native-nfc-manager'; // @ts-ignore import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import LoggerService from '../utils/LoggerService'; import LinearGradient from 'react-native-linear-gradient'; import FrameServer from '../logic/NfcServer'; import useNfcOperations from '../utils/useNfcOperations'; // Default operation configurations export const DEFAULT_OPERATION_CONFIGS = { solenoid_status: { writeCmd: 0x12, writeSubCmd: 0x10, readCmd: 0x13, writeApdu: [0x01], readApdu: [0x00], displayName: 'Solenoid Status', processingMessage: 'Device is reading solenoid status. Wait for the LED indicator and try again.', icon: 'šŸ”„' }, solenoid_1: { writeCmd: 0x13, writeSubCmd: 0x10, readCmd: 0x12, writeApdu: [0x11], readApdu: [0x00], displayName: 'Solenoid 1', processingMessage: 'Device is reading solenoid data. Wait for the LED indicator and try again.', icon: 'šŸŽšļø' }, solenoid_2: { writeCmd: 0x13, writeSubCmd: 0x10, readCmd: 0x12, writeApdu: [0x22], readApdu: [0x00], displayName: 'Solenoid 2', processingMessage: 'Device is reading water flow data. Wait for the LED indicator and try again.', icon: 'šŸŽšļø' }, solenoid_3: { writeCmd: 0x13, writeSubCmd: 0x10, readCmd: 0x12, writeApdu: [0x44], readApdu: [0x00], displayName: 'Solenoid 3', processingMessage: 'Device is reading temperature data. Wait for the LED indicator and try again.', icon: 'šŸŽšļø' }, solenoid_4: { writeCmd: 0x13, writeSubCmd: 0x10, readCmd: 0x12, writeApdu: [0x88], readApdu: [0x00], displayName: 'Solenoid 4', processingMessage: 'Device is reading pressure data. Wait for the LED indicator and try again.', icon: 'šŸŽšļø' }, time_get: { writeCmd: 0x0E, writeSubCmd: 0x0A, readCmd: 0x0E, writeApdu: [0x00, 0x00, 0x00, 0x00], readApdu: [0x00, 0x00, 0x00, 0x00], displayName: 'Read Time', processingMessage: 'Reading device time...', icon: 'šŸ•’' }, time_set: { writeCmd: 0x82, writeSubCmd: 0x16, readCmd: 0x82, writeApdu: [0x00, 0x00, 0x00, 0x00], readApdu: [0x00, 0x00, 0x00, 0x00], displayName: 'Set Time', processingMessage: 'Setting device time...', icon: 'ā°' }, pressure_get: { writeCmd: 0x03, writeSubCmd: 0x01, readCmd: 0x03, writeApdu: [0x00, 0xD6, 0x03, 0x00, 0x07], readApdu: [0x00, 0xB0, 0x03, 0x00, 0x07], displayName: 'Read Pressure', processingMessage: 'Reading Water Pressure...', icon: 'šŸ’Ø' }, flowrate_get: { writeCmd: 0x03, writeSubCmd: 0x01, readCmd: 0x03, writeApdu: [0x00, 0xD6, 0x03, 0x00, 0x07], readApdu: [0x00, 0xB0, 0x03, 0x00, 0x07], displayName: 'Water Flowrate', processingMessage: 'Reading Water Flowrate...', icon: '🌊' }, sensors_update: { writeCmd: 0x88, writeSubCmd: 0x15, readCmd: 0x88, writeApdu: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], readApdu: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], displayName: 'Get Sensors Data', processingMessage: 'Getting sensors Data...', icon: 'sensors', } }; // Default icons const DEFAULT_ICONS = { connection: require('../assets/icons/connection.png'), solenoid: require('../assets/icons/solenoid.png'), schedule: require('../assets/icons/schedule.png'), time: require('../assets/icons/time.png'), pressure: require('../assets/icons/pressure.png'), waterflow: require('../assets/icons/flow.png'), testing: require('../assets/icons/connection.png'), }; export const ControlScreen = React.forwardRef(({ translate = (key) => key, operationConfigs = {}, icons = {}, onTagConnected, onOperationComplete, onError, initialSolenoidStates = { 1: false, 2: false, 3: false, 4: false }, initialSchedules = [], showMenu = true, categories = ['connection', 'solenoid', 'schedule', 'time', 'pressure', 'waterflow'], theme = 'light', enableScheduling = true, enableSensorData = true, enableTesting = false }, ref) => { // Expose refreshSolenoidStates via ref useImperativeHandle(ref, () => ({ refreshSolenoidStates })); // Merge configurations const OPERATION_CONFIGS = Object.assign(Object.assign({}, DEFAULT_OPERATION_CONFIGS), operationConfigs); const ICONS = Object.assign(Object.assign({}, DEFAULT_ICONS), icons); // Screen dimensions const screenWidth = Dimensions.get('window').width; const menuWidth = 80; // State management const [menuVisible, setMenuVisible] = useState(showMenu); const menuAnimation = useRef(new Animated.Value(showMenu ? 1 : 0)).current; const [operationType, setOperationType] = useState(null); const [isTagConnected, setIsTagConnected] = useState(false); const [isScanning, setIsScanning] = useState(false); const [selectedCategory, setSelectedCategory] = useState(categories[0]); const [currentTime, setCurrentTime] = useState(new Date()); const [processingModal, setProcessingModal] = useState(false); const [processingMessage, setProcessingMessage] = useState(''); const [currentProcessingOperation, setCurrentProcessingOperation] = useState(null); // Solenoid states const [selectedZone, setSelectedZone] = useState(1); const [solenoidState, setSolenoidState] = useState(translate('CLOSED')); const [isOn, setIsOn] = useState(false); const { refreshSolenoidStates, solenoidRefreshStep, solenoidStatesFetched, solenoidStates, // ...other values from useNfcOperations... } = useNfcOperations({ operationConfigs: OPERATION_CONFIGS, onOperationComplete, onError, translate, }); // Schedule states const [schedules, setSchedules] = useState(initialSchedules); const [scheduleEnabled, setScheduleEnabled] = useState(true); const [showTimePicker, setShowTimePicker] = useState(false); const [scheduleTime, setScheduleTime] = useState(() => { const now = new Date(); now.setSeconds(0, 0); return now; }); const [scheduleDuration, setScheduleDuration] = useState('10'); const [selectedDate, setSelectedDate] = useState(() => { const today = new Date(); today.setHours(0, 0, 0, 0); return today; }); const [editingScheduleId, setEditingScheduleId] = useState(null); const [showZonePicker, setShowZonePicker] = useState(false); const [showDatePicker, setShowDatePicker] = useState(false); // Sensor data states const [pressureValue, setPressureValue] = useState(45); const [waterFlowRate, setWaterFlowRate] = useState(0.0); const [waterVolume, setWaterVolume] = useState(0.0); // Operation state management const [operations, setOperations] = useState(() => { const initialOperations = {}; Object.keys(OPERATION_CONFIGS).forEach(key => { initialOperations[key] = { state: 'IDLE', lastOperation: { type: null, status: null, timestamp: null }, processingBit: 0x00, data: '', resetRequested: false, processingStartTime: undefined }; }); return initialOperations; }); // Long press detection const [pressTimers, setPressTimers] = useState({}); // Utility functions const transceiveWithTimeout = (apdu, timeoutMs = 5000) => { return Promise.race([ NfcManager.transceive(apdu), new Promise((_, reject) => setTimeout(() => reject(new Error('ā° NFC transceive timed out')), timeoutMs)) ]); }; const showProcessingModal = (message, operationType) => { setProcessingMessage(message); setCurrentProcessingOperation(operationType); setProcessingModal(true); }; const hideProcessingModal = () => { setProcessingModal(false); setProcessingMessage(''); setCurrentProcessingOperation(null); }; const updateOperationStatus = (operationType, updates) => { setOperations(prev => (Object.assign(Object.assign({}, prev), { [operationType]: Object.assign(Object.assign({}, prev[operationType]), updates) }))); }; const resetOperation = (operationType) => { updateOperationStatus(operationType, { state: 'IDLE', lastOperation: { type: null, status: null, timestamp: null }, processingBit: 0x00, data: '', resetRequested: false, processingStartTime: undefined, timer: undefined }); LoggerService.log(`${operationType} operation reset by user`); }; // Helper: Get current time in minutes since epoch, return as 4-byte array (big-endian) // Main operation function for write-only operations const handleWriteOperation = async (operationType) => { if (isScanning) return; if (!OPERATION_CONFIGS[operationType]) return; // Check if solenoid states have been fetched for solenoid operations if (operationType.startsWith('solenoid_') && !solenoidStatesFetched) { Alert.alert(translate('Action Required'), translate('Please refresh solenoid states first'), [{ text: translate('OK'), style: 'default' }]); return; } const operation = operations[operationType]; // Handle reset request if (operation.resetRequested) { resetOperation(operationType); return; } try { setIsScanning(true); // Set animation type based on current solenoid state if (operationType.startsWith('solenoid_')) { const zoneNumber = parseInt(operationType.split('_')[1]); const currentState = solenoidStates[zoneNumber]; const animationType = currentState ? 'solenoid-close' : 'solenoid-open'; setOperationType(animationType); } await NfcManager.requestTechnology(NfcTech.IsoDep); const config = OPERATION_CONFIGS[operationType]; const currentState = operation.state; // Correct state machine: only write on IDLE, ERROR, or READ_READY_WRITE; read on READ_READY_READ if (currentState === 'IDLE' || currentState === 'ERROR') { await performWriteOperation(operationType, config); } else if (currentState === 'READ_READY_READ') { if (operationType === 'time_set') { await performReadOperation(operationType, config); } else { await performStatusWriteOperation(operationType, config); } } else if (currentState === 'READ_READY_WRITE') { await performReadOperation(operationType, config); } else if (currentState === 'PROCESSING' || currentState === 'WRITE_PENDING' || currentState === 'READ_PENDING') { LoggerService.log(`${operationType} operation already in progress or processing`); return; } } catch (e) { LoggerService.log(`${operationType} operation error: ${e}`); const errorMessage = (e === null || e === void 0 ? void 0 : e.toString()) || translate('Operation failed'); if (onError) { onError(errorMessage); } else { Alert.alert(translate('Error'), errorMessage, [{ text: translate('OK'), style: 'default' }]); } updateOperationStatus(operationType, { state: 'ERROR', lastOperation: { type: 'WRITE', status: 'FAILED', timestamp: Date.now() } }); } finally { setIsScanning(false); NfcManager.cancelTechnologyRequest(); } }; const getTimeDifference = () => { const now = Math.floor(Date.now() / 1000); const jan1_2025 = 1735689600; const diff = now - jan1_2025; LoggerService.success(`Current Unix time: ${now}`); LoggerService.success(`Difference from Jan 1, 2025: ${diff} seconds`); return [ (diff >> 24) & 0xff, (diff >> 16) & 0xff, (diff >> 8) & 0xff, diff & 0xff, ]; }; // Main operation handler const handleOperation = async (operationType) => { if (isScanning) return; if (!OPERATION_CONFIGS[operationType]) return; if (operationType.startsWith('solenoid_') && !solenoidStatesFetched) { Alert.alert(translate('Action Required'), translate('Please refresh solenoid states first'), [{ text: translate('OK'), style: 'default' }]); return; } const operation = operations[operationType]; if (operation.resetRequested) { resetOperation(operationType); return; } try { setIsScanning(true); if (operationType.startsWith('solenoid_')) { const zoneNumber = parseInt(operationType.split('_')[1]); const currentState = solenoidStates[zoneNumber]; const animationType = currentState ? 'solenoid-close' : 'solenoid-open'; setOperationType(animationType); } await NfcManager.requestTechnology(NfcTech.IsoDep); const config = OPERATION_CONFIGS[operationType]; const currentState = operation.state; if (currentState === 'IDLE' || currentState === 'ERROR') { await performStatusWriteOperation(operationType, config); } else if (currentState === 'READ_READY_WRITE') { await performReadOperation(operationType, config); } else if (currentState === 'PROCESSING') { await performReadOperation(operationType, config); } else if (currentState === 'WRITE_PENDING' || currentState === 'READ_PENDING') { LoggerService.log(`${operationType} operation already in progress`); return; } // Call completion callback if (onOperationComplete) { onOperationComplete(operationType, operations[operationType]); } } catch (e) { LoggerService.log(`${operationType} operation error: ${e}`); const errorMessage = (e === null || e === void 0 ? void 0 : e.toString()) || translate('Operation failed'); if (onError) { onError(errorMessage); } else { Alert.alert(translate('Error'), errorMessage, [{ text: translate('OK'), style: 'default' }]); } updateOperationStatus(operationType, { state: 'ERROR', lastOperation: { type: null, status: 'FAILED', timestamp: Date.now() } }); } finally { setIsScanning(false); NfcManager.cancelTechnologyRequest(); } }; const performStatusWriteOperation = async (operationType, config) => { updateOperationStatus(operationType, { state: 'WRITE_PENDING', lastOperation: { type: 'WRITE', status: null, timestamp: Date.now() } }); const result = await FrameServer.writeData(config.readCmd, config.writeSubCmd, config.readApdu); if (result.success) { LoggerService.log(`${config.displayName} write operation successful`); const timerUpdate = {}; if (operationType.startsWith('time_')) { timerUpdate.timer = { startTime: Date.now(), currentValue: 0, isRunning: true }; LoggerService.log(`Timer started for ${operationType}`); } updateOperationStatus(operationType, Object.assign({ state: 'READ_READY', lastOperation: { type: 'WRITE', status: 'SUCCESS', timestamp: Date.now() } }, timerUpdate)); setTimeout(() => { updateOperationStatus(operationType, { state: 'READ_READY_WRITE' }); }, 0); } else { LoggerService.log(`${config.displayName} write operation failed`); updateOperationStatus(operationType, { state: 'ERROR', lastOperation: { type: 'WRITE', status: 'FAILED', timestamp: Date.now() } }); Alert.alert(translate('Write Failed'), result.errorMessage || translate('Failed to initiate read request'), [{ text: translate('OK'), style: 'default' }]); } }; const performReadOperation = async (operationType, config) => { var _a, _b, _c; updateOperationStatus(operationType, { state: 'READ_PENDING', lastOperation: { type: 'READ', status: null, timestamp: Date.now() } }); const result = await FrameServer.readReq(config.readCmd, config.writeSubCmd, config.readApdu); if (result.success && result.data && 'payload' in result.data) { const parsedFrame = result.data; const response = parsedFrame.actualData; const processingBit = response.length > 0 ? response[0] : 0x00; updateOperationStatus(operationType, { processingBit }); if (processingBit === 0xFF) { updateOperationStatus(operationType, { state: 'PROCESSING', processingStartTime: Date.now() }); showProcessingModal(config.processingMessage, operationType); setTimeout(() => { hideProcessingModal(); }, 3000); return; } // Update solenoid state if this is a solenoid operation if (operationType.startsWith('solenoid_')) { const zoneNumber = parseInt(operationType.split('_')[1]); const shift = (zoneNumber - 1) * 2; const pairBits = (response[0] >> shift) & 0b11; // Solenoid state is managed by useNfcOperations, just use solenoidStates and solenoidStatesFetched } // Stop timer for time operations let timerSeconds = 0; const timerUpdate = {}; if (operationType.startsWith('time_') && ((_b = (_a = operations[operationType]) === null || _a === void 0 ? void 0 : _a.timer) === null || _b === void 0 ? void 0 : _b.isRunning)) { const currentTimer = operations[operationType].timer; const finalTimerValue = Math.floor((Date.now() - currentTimer.startTime) / 1000); timerUpdate.timer = Object.assign(Object.assign({}, currentTimer), { currentValue: finalTimerValue, isRunning: false }); timerSeconds = finalTimerValue; LoggerService.log(`Timer stopped for ${operationType} at ${finalTimerValue} seconds`); } else if (operationType.startsWith('time_') && ((_c = operations[operationType]) === null || _c === void 0 ? void 0 : _c.timer) && !operations[operationType].timer.isRunning) { timerSeconds = operations[operationType].timer.currentValue || 0; } const parsedData = parseOperationResponse(operationType, response, timerSeconds); updateOperationStatus(operationType, Object.assign({ state: 'READ_READY', lastOperation: { type: 'READ', status: 'COMPLETED', timestamp: Date.now() }, data: parsedData, processingStartTime: undefined }, timerUpdate)); setTimeout(() => { updateOperationStatus(operationType, { state: 'READ_READY_WRITE' }); setTimeout(() => { resetOperation(operationType); }, 5000); }, 0); } else { LoggerService.log(`${config.displayName} read operation failed`); updateOperationStatus(operationType, { state: 'ERROR', lastOperation: { type: 'READ', status: 'FAILED', timestamp: Date.now() } }); Alert.alert(translate('Read Failed'), result.errorMessage || translate('Failed to read data'), [{ text: translate('OK'), style: 'default' }]); } }; // Use refreshSolenoidStates from useNfcOperations hook // Write operation logic matching screens/ControlScreen.tsx const performWriteOperation = async (operationType, config) => { updateOperationStatus(operationType, { state: 'WRITE_PENDING', lastOperation: { type: 'WRITE', status: null, timestamp: Date.now() } }); // Dynamically set writeApdu for solenoid operations based on current state if (operationType.startsWith('solenoid_')) { const solenoidNumber = parseInt(operationType.split('_')[1]); const isOpen = solenoidStates[solenoidNumber]; let openCmd = 0x00; let closeCmd = 0x00; switch (solenoidNumber) { case 1: openCmd = 0x11; closeCmd = 0x10; break; case 2: openCmd = 0x22; closeCmd = 0x20; break; case 3: openCmd = 0x44; closeCmd = 0x40; break; case 4: openCmd = 0x88; closeCmd = 0x80; break; default: openCmd = 0x11; closeCmd = 0x10; } config.writeApdu = [isOpen ? closeCmd : openCmd]; } else if (operationType.startsWith('time_set')) { config.writeApdu = getTimeDifference(); LoggerService.success(`Set Time: ${config.writeApdu}`); } const result = await FrameServer.writeData(config.writeCmd, config.writeSubCmd, config.writeApdu); if (result.success) { LoggerService.log(`${config.displayName} write operation successful`); const timerUpdate = {}; if (operationType.startsWith('time_get')) { timerUpdate.timer = { startTime: Date.now(), currentValue: 0, isRunning: true }; LoggerService.log(`Timer started for ${operationType}`); } updateOperationStatus(operationType, Object.assign({ state: 'READ_READY', lastOperation: { type: 'WRITE', status: 'SUCCESS', timestamp: Date.now() } }, timerUpdate)); setTimeout(() => { updateOperationStatus(operationType, { state: 'READ_READY_READ' }); }, 0); } else { LoggerService.log(`${config.displayName} write operation failed`); updateOperationStatus(operationType, { state: 'ERROR', lastOperation: { type: 'WRITE', status: 'FAILED', timestamp: Date.now() } }); const errorMessage = result.errorMessage || translate('Failed to write data'); if (onError) { onError(errorMessage); } else { Alert.alert(translate('Write Failed'), errorMessage, [{ text: translate('OK'), style: 'default' }]); } } }; const parseOperationResponse = (operationType, response, timerSeconds = 0) => { switch (operationType) { case 'solenoid_1': case 'solenoid_2': case 'solenoid_3': case 'solenoid_4': { if (response.length >= 1) { const solenoidBits = response[0]; LoggerService.success(`Solenoid response[0]: ${response[0]}`); const solenoidIndex = parseInt(operationType.split('_')[1]); const shift = (solenoidIndex - 1) * 2; const pairBits = (solenoidBits >> shift) & 0b11; let stateText = ''; if (pairBits === 0b01) { stateText = translate('🟢 Solenoid OPEN'); setIsOn(true); } else if (pairBits === 0b10) { stateText = translate('šŸ”“ Solenoid CLOSED'); setIsOn(false); } else { stateText = translate('āš ļø Unknown solenoid state'); setIsOn(false); } return stateText; } return translate('āš ļø Unexpected solenoid data format'); } case 'time_get': case 'time_set': { if (response.length >= 4) { const diffSeconds = ((response[0] << 24) | (response[1] << 16) | (response[2] << 8) | response[3]) >>> 0; const baseUnix = 1735689600; const adjustedUnixSeconds = baseUnix + diffSeconds + timerSeconds; const utcDate = new Date(adjustedUnixSeconds * 1000); const localDate = new Date(adjustedUnixSeconds * 1000); LoggerService.success(`Device Time: UTC = ${utcDate.toUTCString()}, Local = ${localDate.toString()}`); return `Device Time:\nšŸŒ UTC: ${utcDate.toUTCString()}\nšŸ  Local: ${localDate.toString()}`; } return translate('āš ļø Unexpected time data format'); } case 'pressure_get': if (response.length >= 4) { const pressure = ((response[2] << 8) | response[3]) / 10; return `šŸ”§ Pressure: ${pressure} bar`; } return translate('āš ļø Unexpected pressure data format'); case 'flowrate_get': if (response.length >= 8) { const flowRate = (response[4] << 24) | (response[5] << 16) | (response[6] << 8) | response[7]; return `šŸ’§ Flow Rate: ${flowRate} L/min`; } return translate('āš ļø Unexpected water flow data format'); case 'sensors_update': if (response.length >= 10) { const batteryVoltage = response[0]; const temperature = (response[1] - 55); const humidity = response[2]; const batteryRawVoltage = (response[7] << 8) | response[8]; const pressures = [response[3], response[4], response[5], response[6]]; const latchesByte = response[9]; const latches = [0, 1, 2, 3].map(i => { const openBit = (latchesByte >> i) & 1; const closeBit = (latchesByte >> (i + 4)) & 1; return openBit === 1 && closeBit === 0; }); return `Sensors Data:\nšŸ”‹ Battery Voltage: ${batteryVoltage}V\nšŸŒ”ļø Temperature: ${temperature}°C\nšŸ’§ Humidity: ${humidity}%\nšŸ”Œ Battery Raw Voltage: ${batteryRawVoltage}mV\nšŸ’Ø Pressures: ${pressures.map(p => `${p} bar`).join(', ')}\nšŸ”’ Latches: ${latches.map((isOpen, i) => `Zone ${i + 1}: ${isOpen ? 'OPEN' : 'CLOSED'}`).join(', ')}`; } default: return translate('āš ļø Unknown data format'); } }; // Connect to NFC tag const connectToTag = async () => { try { setIsScanning(true); setOperationType('nfc'); await NfcManager.start(); await NfcManager.requestTechnology(NfcTech.IsoDep); const tag = await NfcManager.getTag(); const maxTransiveLen = await NfcManager.getMaxTransceiveLength(); LoggerService.info(translate(`Tag connected: ${tag === null || tag === void 0 ? void 0 : tag.id}`)); LoggerService.info(translate(`Maximum transceive length: ${maxTransiveLen}`)); const apduSelect = [0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00]; const response = await transceiveWithTimeout(apduSelect, 5000); LoggerService.info(`APDU Select response: ${response.map(b => b.toString(16).padStart(2, '0')).join(' ')}`); const statusWord = response.slice(-2); if (statusWord[0] === 0x90 && statusWord[1] === 0x00) { setIsTagConnected(true); LoggerService.success(translate('Tag connected successfully')); setIsScanning(false); setOperationType(null); if (onTagConnected) { onTagConnected(tag); } } else { LoggerService.error(translate('Tag connection failed')); Alert.alert(translate('Error'), translate('Failed to connect to device')); setIsScanning(false); setOperationType(null); } } catch (error) { LoggerService.error(translate(`Connection error: ${error}`)); const errorMessage = translate((error === null || error === void 0 ? void 0 : error.toString()) || 'Unknown error'); if (onError) { onError(errorMessage); } else { Alert.alert(translate('Connection Error'), errorMessage); } setIsScanning(false); setOperationType(null); } finally { setIsScanning(false); setOperationType(null); NfcManager.cancelTechnologyRequest().catch(() => { }); } }; // Get button text based on state const getOperationButtonText = (operationType) => { const operation = operations[operationType]; const config = OPERATION_CONFIGS[operationType]; const isSolenoidOperation = operationType.startsWith('solenoid_'); const solenoidNumber = isSolenoidOperation ? parseInt(operationType.split('_')[1]) : null; const isOpen = solenoidNumber ? solenoidStates[solenoidNumber] : false; if (operation.resetRequested) { return translate('šŸ”„ Reset Operation'); } switch (operation.state) { case 'WRITE_PENDING': return translate('šŸ“ Writing...'); case 'READ_PENDING': return translate('šŸ“– Reading...'); case 'PROCESSING': return translate('šŸ”„ Processing...'); case 'IDLE': const lastOp = operation.lastOperation; if (!lastOp.type || lastOp.type !== 'WRITE' || lastOp.status !== 'SUCCESS') { if (isSolenoidOperation && solenoidStatesFetched) { return isOpen ? `šŸ”“ Close ${config.displayName}` : `🟢 Open ${config.displayName}`; } return `${config.icon} Start ${config.displayName}`; } else { return `šŸ“– Read ${config.displayName}`; } case 'READ_READY': return `šŸ“– Read ${config.displayName}`; case 'READ_READY_READ': return `šŸ“– Update ${config.displayName}`; case 'READ_READY_WRITE': return `šŸ“– Get ${config.displayName}`; case 'ERROR': return `āŒ Retry ${config.displayName}`; default: return `${config.icon} ${config.displayName}`; } }; // Get status color const getStatusColor = (state) => { switch (state) { case 'IDLE': return '#6c757d'; case 'WRITE_PENDING': return '#0056b3'; case 'READ_READY': return '#218838'; case 'READ_READY_WRITE': return '#28a745'; case 'READ_READY_READ': return '#20c997'; case 'READ_PENDING': return '#b8860b'; case 'PROCESSING': return '#e67e22'; case 'ERROR': return '#c82333'; default: return '#6c757d'; } }; // Toggle menu function const toggleMenu = () => { const toValue = menuVisible ? 0 : 1; setMenuVisible(!menuVisible); Animated.timing(menuAnimation, { toValue: toValue, duration: 300, useNativeDriver: false, }).start(); }; // Calculate menu position based on animation value const menuTranslateX = menuAnimation.interpolate({ inputRange: [0, 1], outputRange: [-menuWidth, 0], }); const contentWidth = menuAnimation.interpolate({ inputRange: [0, 1], outputRange: [screenWidth, screenWidth - menuWidth], }); const contentMarginLeft = menuAnimation.interpolate({ inputRange: [0, 1], outputRange: [0, menuWidth], }); // Effects useEffect(() => { const timer = setInterval(() => { setCurrentTime(new Date()); }, 1000); return () => clearInterval(timer); }, []); useEffect(() => { if (isScanning) { setIsScanning(false); setOperationType(null); NfcManager.cancelTechnologyRequest().catch(() => { }); } if (selectedCategory !== 'solenoid') { // solenoidStatesFetched is managed by useNfcOperations } }, [selectedCategory]); useEffect(() => { return () => { if (isScanning) { NfcManager.cancelTechnologyRequest().catch(() => { }); } }; }, [isScanning]); // Render operation card const renderOperationCard = (operationType) => { const config = OPERATION_CONFIGS[operationType]; const operation = operations[operationType]; const isSolenoidOperation = operationType.startsWith('solenoid_'); const solenoidNumber = isSolenoidOperation ? parseInt(operationType.split('_')[1]) : null; const isOpen = solenoidNumber ? solenoidStates[solenoidNumber] : false; const isButtonDisabled = isScanning || operation.state === 'WRITE_PENDING' || operation.state === 'READ_PENDING' || (isSolenoidOperation && !solenoidStatesFetched); const isTimeOperation = operationType.startsWith('time_'); const isWaterFlowOperation = operationType.startsWith('flowrate_'); const isPressureOperation = operationType.startsWith('pressure_'); const CardWrapper = ({ children }) => { if (isSolenoidOperation) { const colors = !solenoidStatesFetched ? ['rgba(108, 117, 125, 0.4)', 'rgba(108, 117, 125, 0.1)', 'rgba(0, 0, 0, 0.05)'] : isOpen ? ['rgba(40, 167, 69, 0.4)', 'rgba(40, 167, 69, 0.1)', 'rgba(0, 0, 0, 0.05)'] : ['rgba(220, 53, 69, 0.4)', 'rgba(220, 53, 69, 0.1)', 'rgba(0, 0, 0, 0.05)']; return (_jsx(LinearGradient, Object.assign({ colors: colors, start: { x: 0, y: 0 }, end: { x: 1, y: 1 }, style: [styles.card, !solenoidStatesFetched && styles.cardInactive], pointerEvents: "auto" }, { children: children }))); } else if (isTimeOperation) { return (_jsx(LinearGradient, Object.assign({ colors: ['rgba(245, 245, 220, 0.4)', 'rgba(245, 245, 220, 0.1)', 'rgba(0, 0, 0, 0.05)'], start: { x: 0, y: 0 }, end: { x: 1, y: 1 }, style: styles.card, pointerEvents: "auto" }, { children: children }))); } else if (isWaterFlowOperation) { return (_jsx(LinearGradient, Object.assign({ colors: ['rgba(64, 164, 223, 0.6)', 'rgba(100, 200, 255, 0.3)', 'rgba(0, 100, 180, 0.2)'], start: { x: 0, y: 0 }, end: { x: 1, y: 1 }, style: styles.card, pointerEvents: "auto" }, { children: children }))); } else if (isPressureOperation) { return (_jsx(LinearGradient, Object.assign({ colors: ['rgba(0, 70, 140, 0.7)', 'rgba(0, 120, 200, 0.5)', 'rgba(173, 216, 230, 0.2)'], start: { x: 0, y: 0 }, end: { x: 1, y: 1 }, style: styles.card, pointerEvents: "auto" }, { children: children }))); } return _jsx(View, Object.assign({ style: styles.card }, { children: children })); }; return (_jsx(View, { children: _jsxs(CardWrapper, { children: [_jsxs(View, Object.assign({ style: styles.cardHeader }, { children: [_jsx(Text, Object.assign({ style: styles.cardTitle }, { children: translate(`${config.icon} ${config.displayName}`) })), _jsx(View, Object.assign({ style: [styles.statusBadge, { backgroundColor: getStatusColor(operation.state) }] }, { children: _jsx(Text, Object.assign({ style: styles.statusText }, { children: operation.state.replace('_', ' ') })) }))] })), _jsxs(View, Object.assign({ style: [styles.progressContainer, isSolenoidOperation && !solenoidStatesFetched && styles.progressContainerInactive] }, { children: [_jsxs(View, Object.assign({ style: styles.progressStep }, { children: [_jsx(View, Object.assign({ style: [ styles.progressDot, (operation.state !== 'IDLE' && operation.state !== 'ERROR') ? styles.progressDotActive : styles.progressDotInactive ] }, { children: _jsx(Text, Object.assign({ style: styles.progressDotText }, { children: "1" })) })), _jsx(Text, Object.assign({ style: styles.progressLabel }, { children: "Write" }))] })), _jsx(View, { style: styles.progressLine }), _jsxs(View, Object.assign({ style: styles.progressStep }, { children: [_jsx(View, Object.assign({ style: [ styles.progressDot, (operation.state === 'READ_READY' || operation.state === 'READ_PENDING' || operation.state === 'PROCESSING') ? styles.progressDotActive : styles.progressDotInactive ] }, { children: _jsx(Text, Object.assign({ style: styles.progressDotText }, { children: "2" })) })), _jsx(Text, Object.assign({ style: styles.progressLabel }, { children: "Read" }))] })), _jsx(View, { style: styles.progressLine }), _jsxs(View, Object.assign({ style: styles.progressStep }, { children: [_jsx(View, Object.assign({ style: [ styles.progressDot, operation.state === 'PROCESSING' ? styles.progressDotProcessing : (operation.lastOperation.status === 'COMPLETED') ? styles.progressDotComplete : styles.progressDotInactive ] }, { children: _jsx(Text, Object.assign({ style: styles.progressDotText }, { children: operation.state === 'PROCESSING' ? 'šŸ”„' : 'āœ“' })) })), _jsx(Text, Object.assign({ style: styles.progressLabel }, { children: operation.state === 'PROCESSING' ? 'Processing' : 'Done' }))] }))] })), operation.data && (_jsx(View, Object.assign({ style: styles.dataContainer }, { children: _jsx(Text, Object.assign({ style: styles.dataText }, { children: operation.data })) }))), _jsx(TouchableOpacity, Object.assign({ style: [ styles.operationButton, { backgroundColor: getStatusColor(operation.state) }, isButtonDisabled && styles.operationButtonDisabled ], onPress: () => handleOperation(operationType), disabled: isButtonDisabled }, { children: _jsx(Text, Object.assign({ style: styles.operationButtonText }, { children: getOperationButtonText(operationType) })) }))] }) }, operationType)); }; // Render category content const renderCategoryContent = () => { switch (selectedCategory) { case 'connection': return (_jsxs(View, Object.assign({ style: styles.categoryContent }, { children: [_jsx(TouchableOpacity, Object.assign({ style: [ styles.connectButton, isTagConnected && styles.connectButtonConnected ], onPress: connectToTag, disabled: isScanning }, { children: _jsx(Text, Object.assign({ style: styles.connectButtonText }, { children: isScanning ? translate('šŸ”„ Connecting...') : isTagConnected ? translate('āœ… Connected') : translate('šŸ“± Connect to Device') })) })), isTagConnected && (_jsxs(View, Object.assign({ style: styles.connectionInfo }, { children: [_jsx(Text, Object.assign({ style: styles.connectionInfoText }, { children: translate('Device Status: Connected') })), _jsx(Text, Object.assign({ style: styles.connectionInfoText }, { children: translate(`Current Time: ${currentTime.toLocaleTimeString()}`) }))] })))] }))); case 'solenoid': return (_jsxs(View, Object.assign({ style: styles.categoryContent }, { children: [_jsx(View, { children: _jsxs(LinearGradient, Object.assign({ colors: ['rgba(0,123,255,0.2)', 'rgba(40,167,69,0.1)', 'rgba(220,53,69,0.05)'], start: { x: 0, y: 0 }, end: { x: 1, y: 1 }, style: styles.card, pointerEvents: "auto" }, { children: [_jsxs(View, Object.assign({ style: styles.cardHeader }, { children: [_jsx(Text, Object.assign({ style: styles.cardTitle }, { children: translate('šŸ”„ Refresh Solenoid States') })), _jsx(View, Object.assign({ style: [styles.statusBadge, { backgroundColor: solenoidStatesFetched ? '#28a745' : '#6c757d' }] }, { children: _jsx(Text, Object.assign({ style: styles.statusText }, { children: solenoidStatesFetched ? translate('READY') : translate('IDLE') })) }))] })), _jsx(View, Object.assign({ style: styles.dataContainer }, { children: _jsx(Text, Object.assign({ style: styles.dataText }, { children: solenoidStatesFetched ? translate('Solenoid states are up to date.') : translate('Press to refresh solenoid states from device.') })) })), _jsx(TouchableOpacity, Object.assign({ style: [ styles.operationButton, { backgroundColor: solenoidStatesFetched ? '#28a745' : '#007bff' }, isScanning && styles.operationButtonDisabled ], onPress: refreshSolenoidStates, disabled: isScanning }, { children: _jsx(Text, Object.assign({ style: styles.operationButtonText }, { children: isScanning ? translate('šŸ”„ Refreshing...') : translate('šŸ”„ Refresh Solenoid States') })) }))] })) }), ['solenoid_1', 'solenoid_2', 'solenoid_3', 'solenoid_4'].map(renderOperationCard)] }))); case 'time': return (_jsx(View, Object.assign({ style: styles.categoryContent }, { children: ['time_get', 'time_set'].map(renderOperationCard) }))); case 'pressure': return (_jsx(View, Object.assign({ style: styles.categoryContent }, { children: renderOperationCard('pressure_get') }))); case 'waterflow': return (_jsx(View, Object.assign({ style: styles.categoryContent }, { children: renderOperationCard('flowrate_get') }))); case 'schedule': if (!enableScheduling) return null; return (_jsxs(View, Object.assign({ style: styles.categoryContent }, { children: [_jsx(Text, Object.assign({ style: styles.categoryTitle }, { children: translate('Irrigation Schedules') })), _jsx(Text, Object.assign({ style: styles.placeholderText }, { children: translate('Schedule management functionality coming soon...') }))] }))); default: return (_jsx(View, Object.assign({ style: styles.categoryContent }, { children: _jsx(Text, Object.assign({ style: styles.placeholderText }, { children: translate('Select a category from the menu') })) }))); } }; return (_jsxs(SafeAreaView, Object.assign({ style: [styles.container, theme === 'dark' && styles.containerDark] }, { children: [_jsx(StatusBar, { barStyle: theme === 'dark' ? 'light-content' : 'dark-content' }), showMenu && (_jsxs(Animated.View, Object.assign({ style: [ styles.menu, { transform: [{ translateX: menuTranslateX }] }, theme === 'dark' && styles.menuDark ] }, { children: [_jsx(TouchableOpacity, Object.assign({ style: styles.menuToggle, onPress: toggleMenu }, { children: _jsx(MaterialIcons, { name: menuVisible ? 'chevron-left' : 'chevron-right', size: 24, color: theme === 'dark' ? '#fff' : '#333' }) })), _jsx(ScrollView, Object.assign({ style: styles.menuContent }, { children: categories.map((category) => (_jsxs(TouchableOpacity, Object.assign({ style: [ styles.menuItem, selectedCategory === category && styles.menuItemActive, theme === 'dark' && styles.menuItemDark ], onPress: () => setSelectedCategory(category) }, { children: [_jsx(Image, { source: ICONS[category] || ICONS.connection, style: styles.menuIcon, resizeMode: "contain" }), _jsx(Text, Object.assign({ style: [ styles.menuText, selectedCategory === category && styles.menuTextActive, theme === 'dark' && styles.menuTextDark ] }, { children: translate(category) }))] }), category))) }))] }))), _jsx(Animated.View, Object.assign({ style: [ styles.content, { width: showMenu ? contentWidth : screenWidth, marginLeft: showMenu ? contentMarginLeft : 0 }, theme === 'dark' && styles.contentDark ] }, { children: _jsx(ScrollView, Object.assign({ style: styles.scrollView }, { children: renderCategoryContent() })) })), _jsx(Modal, Object.assign({ visible: processingModal, transparent: true, animationType: "fade" }, { children: _jsx(View, Object.assign({ style: styles.modalOverlay }, { children: _jsxs(View, Object.assign({ style: [styles.modalContent, theme === 'dark' && styles.modalContentDark] }, { children: [_jsx(ActivityIndicator, { size: "large", color: "#007bff" }), _jsx(Text, Object.assign({ style: [styles.modalText, theme === 'dark' && styles.modalTextDark] }, { children: processingMessage }))] })) })) }))] }))); // Expose refreshSolenoidStates via ref useImperativeHandle(ref, () => ({ refreshSolenoidStates })); }); const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f8f9fa', }, containerDark: { backgroundColor: '#1a1a1a', }, menu: { position: 'absolute', left: 0, top: 0, bottom: 0, width: 80, backgroundColor: '#fff', borderRightWidth: 1, borderRightColor: '#e0e0e0', zIndex: 1000, elevation: 5, shadowColor: '#000', shadowOffset: { width: 2, height: 0 }, shadowOpacity: 0.1, shadowRadius: 4, }, menuDark: { backgroundColor: '#2a2a2a', borderRightColor: '#444', }, menuToggle: { padding: 15, alignItems: 'center', borderBottomWidth: 1, borderBottomColor: '#e0e0e0', }, menuContent: { flex: 1, }, menuItem: { padding: 15, alignItems: 'center', borderBottomWidth: 1, borderBottomColor: '#f0f0f0', }, menuItemActive: { backgroundColor: '#007bff', }, menuItem