UNPKG

je_nfc_sdk

Version:

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

682 lines (681 loc) • 31.2 kB
import { useState, useCallback, useRef } from 'react'; import NfcManager, { NfcTech } from 'react-native-nfc-manager'; import { Alert } from 'react-native'; import LoggerService from './LoggerService'; import FrameServer from '../logic/NfcServer'; export const useNfcOperations = ({ // ...existing code... operationConfigs, onOperationComplete, onError, translate = (key) => key }) => { // State const [operations, setOperations] = useState(() => { const initialOperations = {}; Object.keys(operationConfigs).forEach(key => { initialOperations[key] = { state: 'IDLE', lastOperation: { type: null, status: null, timestamp: null }, processingBit: 0x00, data: '', resetRequested: false, processingStartTime: undefined }; }); return initialOperations; }); const [isScanning, setIsScanning] = useState(false); const [isTagConnected, setIsTagConnected] = useState(false); // Refs const processingModalRef = useRef(null); // Utility functions const transceiveWithTimeout = useCallback((apdu, timeoutMs = 5000) => { return Promise.race([ NfcManager.transceive(apdu), new Promise((_, reject) => setTimeout(() => reject(new Error('ā° NFC transceive timed out')), timeoutMs)) ]); }, []); const updateOperationStatus = useCallback((operationType, updates) => { setOperations(prev => (Object.assign(Object.assign({}, prev), { [operationType]: Object.assign(Object.assign({}, prev[operationType]), updates) }))); }, []); const resetOperation = useCallback((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`); }, [updateOperationStatus]); const [solenoidRefreshStep, setSolenoidRefreshStep] = useState('write'); const [solenoidStatesFetched, setSolenoidStatesFetched] = useState(false); const [solenoidStates, setSolenoidStates] = useState({ 1: false, 2: false, 3: false, 4: false }); const refreshSolenoidStates = useCallback(async () => { if (!isTagConnected) { Alert.alert(translate('Error'), translate('Please connect to a device first')); return; } try { setIsScanning(true); const config = operationConfigs['solenoid_status']; if (!config) return; if (solenoidRefreshStep === 'write') { const writeresult = await FrameServer.writeData(config.writeCmd, config.writeSubCmd, config.writeApdu); if (writeresult.success && writeresult.data) { LoggerService.success(`Write command sent successfully: ${JSON.stringify(writeresult.data)}`); setSolenoidRefreshStep('read'); Alert.alert(translate('Info'), translate('Write successful. Press again to read states.')); } else { setSolenoidRefreshStep('write'); setSolenoidStatesFetched(false); LoggerService.error('Write failed. Try again.'); Alert.alert(translate('Error'), translate('Write failed. Please try again.')); } } else if (solenoidRefreshStep === 'read') { const result = await FrameServer.readReq(config.readCmd, config.writeSubCmd, config.readApdu); if (result.success && result.data && 'payload' in result.data) { const response = result.data.actualData; const statesByte = response[0]; const newStates = {}; newStates[1] = ((statesByte & 0x03) === 0x01); newStates[2] = ((statesByte & 0x0C) === 0x04); newStates[3] = ((statesByte & 0x30) === 0x10); newStates[4] = ((statesByte & 0xC0) === 0x40); setSolenoidStates(newStates); setSolenoidStatesFetched(true); setSolenoidRefreshStep('write'); Alert.alert(translate('Success'), translate('Solenoid states refreshed. Press again to write.')); } else { setSolenoidRefreshStep('write'); setSolenoidStatesFetched(false); LoggerService.error('Read failed. Try again.'); Alert.alert(translate('Error'), translate('Read failed. Please try again.')); } } LoggerService.success('Solenoid refresh step complete.'); } catch (error) { LoggerService.error(`Refresh error: ${error}`); Alert.alert(translate('Error'), translate('Failed to refresh solenoid states')); setSolenoidRefreshStep('write'); } finally { setIsScanning(false); } }, [isTagConnected, solenoidRefreshStep, operationConfigs, translate]); // Helper: Get current time in minutes since epoch, return as 4-byte array (big-endian) const getTimeDifference = useCallback(() => { 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, ]; }, []); // Parse operation response const parseOperationResponse = useCallback((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'); } else if (pairBits === 0b10) { stateText = translate('šŸ”“ Solenoid CLOSED'); } else { stateText = translate('āš ļø Unknown solenoid state'); } 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'); } }, [translate]); // Perform status write operation const performStatusWriteOperation = useCallback(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() } }); const errorMessage = result.errorMessage || translate('Failed to initiate read request'); if (onError) { onError(errorMessage); } else { Alert.alert(translate('Write Failed'), errorMessage, [{ text: translate('OK'), style: 'default' }]); } } }, [updateOperationStatus, onError, translate]); // Perform read operation const performReadOperation = useCallback(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() }); // Show processing modal if available if (processingModalRef.current) { processingModalRef.current.show(config.processingMessage, operationType); setTimeout(() => { var _a; (_a = processingModalRef.current) === null || _a === void 0 ? void 0 : _a.hide(); }, 3000); } return; } // 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); // Call completion callback if (onOperationComplete) { onOperationComplete(operationType, { data: parsedData, success: true }); } } else { LoggerService.log(`${config.displayName} read operation failed`); updateOperationStatus(operationType, { state: 'ERROR', lastOperation: { type: 'READ', status: 'FAILED', timestamp: Date.now() } }); const errorMessage = result.errorMessage || translate('Failed to read data'); if (onError) { onError(errorMessage); } else { Alert.alert(translate('Read Failed'), errorMessage, [{ text: translate('OK'), style: 'default' }]); } } }, [updateOperationStatus, operations, parseOperationResponse, resetOperation, onOperationComplete, onError, translate]); // Main operation handler const handleOperation = useCallback(async (operationType) => { if (isScanning) return; if (!operationConfigs[operationType]) return; const operation = operations[operationType]; if (operation.resetRequested) { resetOperation(operationType); return; } try { setIsScanning(true); await NfcManager.requestTechnology(NfcTech.IsoDep); const config = operationConfigs[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; } } 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(); } }, [isScanning, operationConfigs, operations, resetOperation, performStatusWriteOperation, performReadOperation, updateOperationStatus, onError, translate]); // Direct write operation handler (matches ControlScreen.tsx logic) // Write operation logic matching ControlScreen.tsx const performWriteOperation = useCallback(async (operationType, config, options) => { 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_') && (options === null || options === void 0 ? void 0 : options.solenoidStates)) { const solenoidNumber = parseInt(operationType.split('_')[1]); const isOpen = options.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' }]); } } }, [updateOperationStatus, onError, translate, getTimeDifference]); const handleWriteOperation = useCallback(async (operationType, options) => { if (isScanning) return; if (!operationConfigs[operationType]) return; // Check if solenoid states have been fetched for solenoid operations if (operationType.startsWith('solenoid_') && (options === null || options === void 0 ? void 0 : options.solenoidStatesFetched) === false) { 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 === null || operation === void 0 ? void 0 : operation.resetRequested) { resetOperation(operationType); return; } try { setIsScanning(true); // Set animation type based on current solenoid state if (operationType.startsWith('solenoid_') && (options === null || options === void 0 ? void 0 : options.solenoidStates) && (options === null || options === void 0 ? void 0 : options.setOperationType)) { const zoneNumber = parseInt(operationType.split('_')[1]); const currentState = options.solenoidStates[zoneNumber]; const animationType = currentState ? 'solenoid-close' : 'solenoid-open'; options.setOperationType(animationType); } await NfcManager.requestTechnology(NfcTech.IsoDep); const config = operationConfigs[operationType]; const currentState = operation === null || operation === void 0 ? void 0 : 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, options); } 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} write operation error: ${e}`); const errorMessage = (e === null || e === void 0 ? void 0 : e.toString()) || translate('Write 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(); } }, [isScanning, operationConfigs, operations, resetOperation, performStatusWriteOperation, performReadOperation, updateOperationStatus, onError, translate]); // Connect to NFC tag const connectToTag = useCallback(async () => { try { setIsScanning(true); 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); if (onOperationComplete) { onOperationComplete('connect', { tag, success: true }); } } else { LoggerService.error(translate('Tag connection failed')); const errorMessage = translate('Failed to connect to device'); if (onError) { onError(errorMessage); } else { Alert.alert(translate('Error'), errorMessage); } setIsScanning(false); } } 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); } finally { setIsScanning(false); NfcManager.cancelTechnologyRequest().catch(() => { }); } }, [transceiveWithTimeout, onOperationComplete, onError, translate]); // Get button text based on state const getOperationButtonText = useCallback((operationType) => { const operation = operations[operationType]; const config = operationConfigs[operationType]; if (!operation || !config) return translate('Unknown Operation'); 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') { 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}`; } }, [operations, operationConfigs, translate]); // Get status color const getStatusColor = useCallback((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'; } }, []); return { // State operations, isScanning, isTagConnected, //Solenoid States refreshSolenoidStates, solenoidRefreshStep, solenoidStatesFetched, solenoidStates, // Actions handleOperation, handleWriteOperation, connectToTag, resetOperation, updateOperationStatus, // Utilities getOperationButtonText, getStatusColor, transceiveWithTimeout, }; }; export default useNfcOperations;