feat: immediate mode (I) commands via Connection Request
- Hide Refresh button when ConnectionRequestURL is unavailable - Fix ImmediateCommandStateMachine session lifecycle (mark complete when no commands) - Fix preemption: new CONNECTION REQUEST replaces stale immediate session - Protect ensureSessionForRemote from rebinding to completed machines - Normalize numeric event codes from simulators (6 -> 6 CONNECTION REQUEST) - Extract updatePendingCommandsStatus for clarity
This commit is contained in:
1 parent
713a043625
commit
4eb3f0795a
6 files changed
+71
-14
No files matched your search
+1
-1
@@ -28,4 +28,4 @@ LOG_PORT=514
|
||||
LOG_PROTOCOL=udp4
|
||||
|
||||
# Security
|
||||
HASH_KEY=0123456789abcdef0123456789abcdef
|
||||
HASH_KEY=0123456789abcdef0123456789abcdef
|
||||
@@ -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 !== '') {
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<void> {
|
||||
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() } }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
<div class="card-body p-4">
|
||||
<h2 class="h6 fw-bold mb-3">Actions</h2>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-outline-dark btn-sm" @click="refreshDevice" :disabled="isLoading">
|
||||
<button
|
||||
v-if="hasConnectionRequestUrl"
|
||||
class="btn btn-outline-dark btn-sm"
|
||||
@click="refreshDevice"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ t('button.refresh') }}
|
||||
</button>
|
||||
<span
|
||||
v-else
|
||||
class="d-inline-block"
|
||||
tabindex="0"
|
||||
data-bs-toggle="tooltip"
|
||||
:title="t('tooltip.refreshUnavailable', 'ConnectionRequestURL non disponible sur cet équipement')"
|
||||
>
|
||||
<button class="btn btn-outline-dark btn-sm" disabled>
|
||||
{{ t('button.refresh') }}
|
||||
</button>
|
||||
</span>
|
||||
<button class="btn btn-outline-dark btn-sm" @click="openGetModelPopup" :disabled="isLoading">
|
||||
{{ t('button.getModel') }}
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user