je_nfc_sdk
Version:
A comprehensive React Native SDK for NFC-based device control and communication
982 lines ⢠54.7 kB
JavaScript
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