diff --git a/.env.example b/.env.example index 0d4257f..e10b6c1 100644 --- a/.env.example +++ b/.env.example @@ -28,4 +28,4 @@ LOG_PORT=514 LOG_PROTOCOL=udp4 # Security -HASH_KEY=0123456789abcdef0123456789abcdef \ No newline at end of file +HASH_KEY=0123456789abcdef0123456789abcdef diff --git a/app/controllers/cwmp_controller.ts b/app/controllers/cwmp_controller.ts index 5d7a204..c427cbd 100644 --- a/app/controllers/cwmp_controller.ts +++ b/app/controllers/cwmp_controller.ts @@ -38,10 +38,12 @@ export default class CwmpController { try { if (isEmpty) { - manager.ensureSessionForRemote(remoteKey, sessionId) + const rebound = manager.ensureSessionForRemote(remoteKey, sessionId) const hashId = manager.getHashIdForRemote(remoteKey) if (hashId) ctx.cwmpDeviceId = hashId + logger.debug({ remoteKey, sessionId, rebound, hashId, hasSession: manager.hasSession(sessionId) }, 'CWMP Empty POST received') + if (manager.hasSession(sessionId)) { const result = await manager.continue(sessionId) if (result && result !== '') { diff --git a/app/services/cwmp/main_state_machine.ts b/app/services/cwmp/main_state_machine.ts index 5925db8..1fdecc8 100644 --- a/app/services/cwmp/main_state_machine.ts +++ b/app/services/cwmp/main_state_machine.ts @@ -117,12 +117,14 @@ export class ExecutingCommandsState extends DeviceState { this.commandsQueue = [...iWaiting, ...iPending, ...niPending] this.started = true + logger.debug({ hashId, iWaiting: iWaiting.length, iPending: iPending.length, niPending: niPending.length, includeNI: this.includeNI }, '[ExecutingCommands] Loaded commands queue') if (this.commandsQueue.length === 0) { if (this.includeNI) { return { nextState: new CompletedState(), isComplete: true, response: '' } } - return { response: '' } + // ImmediateCommandStateMachine with no commands — mark complete + return { isComplete: true, response: '' } } return { response: await this.processNextCommand(context) } diff --git a/app/services/cwmp/state_machine_manager.ts b/app/services/cwmp/state_machine_manager.ts index 1e594b1..628b838 100644 --- a/app/services/cwmp/state_machine_manager.ts +++ b/app/services/cwmp/state_machine_manager.ts @@ -136,14 +136,21 @@ export class StateMachineManager extends EventEmitter { return existingMachine } - // Rebind session to existing machine - if (existingSessionId !== session) { - this.sessionMachines.delete(existingSessionId) - this.sessionMachines.set(session, existingMachineId) - this.deviceSessions.set(deviceKey, session) - existingMachine.session = session + // For a new CONNECTION REQUEST arriving while a previous immediate session + // is still technically "running", complete the old one and create a fresh machine + if (isImmediate && lastEvent === '6 CONNECTION REQUEST') { + this.completeMachine(existingMachine) + // Fall through to create a new machine below + } else { + // Rebind session to existing machine + if (existingSessionId !== session) { + this.sessionMachines.delete(existingSessionId) + this.sessionMachines.set(session, existingMachineId) + this.deviceSessions.set(deviceKey, session) + existingMachine.session = session + } + return existingMachine } - return existingMachine } } } @@ -242,6 +249,11 @@ export class StateMachineManager extends EventEmitter { const machine = this.machines.get(machineId) if (!machine) return false + // Don't rebind to a completed/failed machine + if (machine.status !== MachineStatus.RUNNING) { + return false + } + if (existingSession !== newSession) { this.sessionMachines.delete(existingSession) this.sessionMachines.set(newSession, machineId) @@ -312,6 +324,7 @@ export class StateMachineManager extends EventEmitter { // Let the state machine continue const response = await machineInstance.stateMachine.handleResponse(true, { isFirst: false }) + logger.debug({ session, stateName: machineInstance.stateMachine.getCurrentStateName(), responseLength: response?.length || 0 }, '[Continue] State machine handleResponse result') machineInstance.currentState = machineInstance.stateMachine.getCurrentStateName() if (machineInstance.stateMachine.isCompleted()) { @@ -406,7 +419,14 @@ export class StateMachineManager extends EventEmitter { } private normalizeEvent(evt: string): string { - return (evt || '').toString().trim().replace(/\s+/g, ' ').toUpperCase() + const normalized = (evt || '').toString().trim().replace(/\s+/g, ' ').toUpperCase() + // Handle simulator variants that send only the event code number + if (normalized === '6') return '6 CONNECTION REQUEST' + if (normalized === '0') return '0 BOOTSTRAP' + if (normalized === '1') return '1 BOOT' + if (normalized === '2') return '2 PERIODIC' + if (normalized === '3') return '3 SCHEDULED' + return normalized } cleanupCompletedMachines(olderThanHours: number = 1): void { diff --git a/app/services/device_service.ts b/app/services/device_service.ts index c376aa5..408c9c9 100644 --- a/app/services/device_service.ts +++ b/app/services/device_service.ts @@ -120,8 +120,13 @@ export default class DeviceService { } const wakeSuccess = await this.sendWakeOnWlan(url, device) + await this.updatePendingCommandsStatus(identifier, wakeSuccess) + } - // Mark pending I commands as WAITING (or FAILED) + /** + * Update pending mode-I commands status based on Connection Request result + */ + private async updatePendingCommandsStatus(identifier: string, success: boolean): Promise { const collection = CommandModel.getCollection() const pendingCmds = await collection.find({ deviceIdentifier: identifier, @@ -132,7 +137,7 @@ export default class DeviceService { for (const cmd of pendingCmds) { await collection.updateOne( { _id: cmd._id }, - { $set: { status: wakeSuccess ? CommandStatus.WAITING : CommandStatus.PENDING, updatedAt: new Date() } } + { $set: { status: success ? CommandStatus.WAITING : CommandStatus.PENDING, updatedAt: new Date() } } ) } } diff --git a/inertia/pages/DeviceDetail.vue b/inertia/pages/DeviceDetail.vue index 554e1dc..358d409 100644 --- a/inertia/pages/DeviceDetail.vue +++ b/inertia/pages/DeviceDetail.vue @@ -126,6 +126,18 @@ function submitGetModel() { }) } +// Check if the device has a ConnectionRequestURL (needed for immediate mode / Refresh) +const hasConnectionRequestUrl = (() => { + const allParams = [...(device.properties || []), ...(device.parameters || [])] + const crUrl = allParams.find( + (p: any) => + p.name && + (p.name.endsWith('ManagementServer.ConnectionRequestURL') || + p.name.endsWith('.ConnectionRequestURL')) + ) + return crUrl && crUrl.value && crUrl.value.trim() !== '' +})() + function formatName(name: unknown): string { if (name == null) return '' const str = typeof name === 'string' ? name : String(name) @@ -367,9 +379,25 @@ function getPropertyValue(name: string): string {

Actions

- + + +