UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

640 lines 33 kB
import { MGRPExtension, MPANExtension, Security2CCMessageEncapsulation, Security2CCNonceGet, Security2CCNonceReport, SupervisionCC, SupervisionCCReport, SupervisionCommand, getInnermostCommandClass, } from "@zwave-js/cc"; import { SecurityCCCommandEncapsulation, SecurityCCNonceGet, } from "@zwave-js/cc/SecurityCC"; import { MAX_SEGMENT_SIZE, RELAXED_TIMING_THRESHOLD, TransportServiceCCFirstSegment, TransportServiceCCSegmentComplete, TransportServiceCCSegmentRequest, TransportServiceCCSegmentWait, TransportServiceCCSubsequentSegment, TransportServiceTimeouts, } from "@zwave-js/cc/TransportServiceCC"; import { CommandClasses, EncapsulationFlags, MessagePriority, NODE_ID_BROADCAST, SPANState, SecurityClass, SupervisionStatus, TransmitOptions, ZWaveError, ZWaveErrorCodes, mergeSupervisionResults, } from "@zwave-js/core"; import { containsCC, isSendData, isTransmitReport, } from "@zwave-js/serial/serialapi"; import { getErrorMessage } from "@zwave-js/shared"; import { wait } from "alcalzone-shared/async"; import { createDeferredPromise, } from "alcalzone-shared/deferred-promise"; function maybePartialNodeUpdate(ctx, sent, received) { // Some commands are returned in multiple segments, which may take longer than // the configured timeout. if (!containsCC(sent) || !containsCC(received)) { return false; } if (sent.getNodeId() !== received.getNodeId()) return false; if (received.command.ccId === CommandClasses["Transport Service"]) { // We don't know what's in there. It may be the expected update return true; } // Let the sent CC test if the received one is a match. // These predicates don't check if the received CC is complete, we can use them here. // This also doesn't check for correct encapsulation, but that is good enough to refresh the timer. const sentCommand = getInnermostCommandClass(sent.command); const receivedCommand = getInnermostCommandClass(received.command); return sentCommand.isExpectedCCResponse(ctx, receivedCommand); } export async function waitForNodeUpdate(driver, msg, timeoutMs) { try { return await driver.waitForMessage((received) => msg.isExpectedNodeUpdate(driver, received), timeoutMs, (received) => maybePartialNodeUpdate(driver, msg, received)); } catch { throw new ZWaveError(`Timed out while waiting for a response from the node`, ZWaveErrorCodes.Controller_NodeTimeout); } } function getNodeUpdateTimeout(driver, msg, additionalCommandTimeoutMs = 0) { const commandTimeMs = Math.ceil((msg.rtt ?? 0) / 1e6); return (commandTimeMs + driver.getReportTimeout(msg) + additionalCommandTimeoutMs); } /** A simple message generator that simply sends a message, waits for the ACK (and the response if one is expected) */ export const simpleMessageGenerator = async function* (driver, ctx, msg, onMessageSent, additionalCommandTimeoutMs = 0) { // Make sure we can send this message if (isSendData(msg) && await driver.exceedsMaxPayloadLength(msg)) { // We use explorer frames by default, but this reduces the maximum payload length by 2 bytes compared to AUTO_ROUTE // Try disabling explorer frames for this message and see if it fits now. function fail() { throw new ZWaveError("Cannot send this message because it would exceed the maximum payload length!", ZWaveErrorCodes.Controller_MessageTooLarge); } if (msg.transmitOptions & TransmitOptions.Explore) { msg.transmitOptions &= ~TransmitOptions.Explore; if (await driver.exceedsMaxPayloadLength(msg)) { // Still too large fail(); } driver.controllerLog.logNode(msg.getNodeId(), { message: "Disabling explorer frames for this message due to its size", level: "warn", }); } else { fail(); } } // Pass this message to the send thread and wait for it to be sent let result; // At this point we can't have received a premature update yet msg.prematureNodeUpdate = undefined; try { // The yield can throw and must be handled here result = yield msg; // Figure out how long the message took to be handled msg.markAsCompleted(); onMessageSent(msg, result); } catch (e) { msg.markAsCompleted(); throw e; } // If the message was sent to a node and came back with a NOK callback, // we want to inspect the callback, for example to look at TX statistics // or update the node status. // // There is one exception: If we aborted the transaction with SendDataAbort // because of a premature response, we treat this as a successful transmission anyways. if (msg.expectsNodeUpdate(driver) && msg.prematureNodeUpdate) { return msg.prematureNodeUpdate; } if (isTransmitReport(result) && !result.isOK()) { // Throw the message in order to short-circuit all possible generators // eslint-disable-next-line @typescript-eslint/only-throw-error throw result; } // If the sent message expects an update from the node, wait for it if (msg.expectsNodeUpdate(driver)) { // CommandTime is measured by the application // ReportTime timeout SHOULD be set to CommandTime + 1 second. const timeout = getNodeUpdateTimeout(driver, msg, additionalCommandTimeoutMs); return waitForNodeUpdate(driver, msg, timeout); } return result; }; /** A generator for singlecast SendData messages that automatically uses Transport Service when necessary */ export const maybeTransportServiceGenerator = async function* (driver, ctx, msg, onMessageSent, additionalCommandTimeoutMs) { // Make sure we can send this message /*if (!isSendData(msg) || !containsCC(msg)) { throw new ZWaveError( "Cannot use the Transport Service message generator for messages that are not SendData!", ZWaveErrorCodes.Argument_Invalid, ); } else*/ if (typeof msg.command.nodeId !== "number") { throw new ZWaveError("Cannot use the Transport Service message generator for multicast commands!", ZWaveErrorCodes.Argument_Invalid); } const node = msg.tryGetNode(driver); const mayUseTransportService = node?.supportsCC(CommandClasses["Transport Service"]) && node.getCCVersion(CommandClasses["Transport Service"]) >= 2; if (!mayUseTransportService || !(await driver.exceedsMaxPayloadLength(msg))) { // Transport Service isn't needed for this message return yield* simpleMessageGenerator(driver, ctx, msg, onMessageSent, additionalCommandTimeoutMs); } // Send the command split into multiple segments const payload = await msg.serializeCC(ctx); const numSegments = Math.ceil(payload.length / MAX_SEGMENT_SIZE); const segmentDelay = numSegments > RELAXED_TIMING_THRESHOLD ? TransportServiceTimeouts.relaxedTimingDelayR2 : 0; const sessionId = driver.getNextTransportServiceSessionId(); const nodeId = msg.command.nodeId; // Since the command is never logged, we do it here driver.driverLog.print("The following message is too large, using Transport Service to transmit it:"); driver.driverLog.logMessage(msg, { direction: "outbound", }); // I don't see an elegant way to wait for possible responses, so we just register a handler in the driver // and remember the received commands let unhandledResponses = []; const { unregister: unregisterHandler } = driver.registerCommandHandler((cc) => cc.nodeId === nodeId && (cc instanceof TransportServiceCCSegmentWait || (cc instanceof TransportServiceCCSegmentRequest && cc.sessionId === sessionId)), (cc) => { unhandledResponses.push(cc); }); const receivedSegmentWait = () => { const index = unhandledResponses.findIndex((cc) => cc instanceof TransportServiceCCSegmentWait); if (index >= 0) { const cc = unhandledResponses[index]; unhandledResponses.splice(index, 1); return cc; } }; const receivedSegmentRequest = () => { const index = unhandledResponses.findIndex((cc) => cc instanceof TransportServiceCCSegmentRequest); if (index >= 0) { const cc = unhandledResponses[index]; unhandledResponses.splice(index, 1); return cc; } }; // We have to deal with multiple messages, but can only return a single result. // Therefore we use the last one as the result. let result; try { attempts: for (let attempt = 1; attempt <= 2; attempt++) { driver.controllerLog.logNode(nodeId, { message: `Beginning Transport Service TX session #${sessionId}...`, level: "debug", direction: "outbound", }); // Clear the list of unhandled responses unhandledResponses = []; // Fill the list of unsent segments const unsentSegments = Array.from({ length: numSegments }) .fill(0) .map((_, i) => i); let didRetryLastSegment = false; let isFirstTransferredSegment = true; while (unsentSegments.length > 0) { // Wait if necessary if (isFirstTransferredSegment) { isFirstTransferredSegment = false; } else if (segmentDelay) { await wait(segmentDelay, true); } const segment = unsentSegments.shift(); const chunk = payload.subarray(segment * MAX_SEGMENT_SIZE, (segment + 1) * MAX_SEGMENT_SIZE); let cc; if (segment === 0) { cc = new TransportServiceCCFirstSegment({ nodeId, sessionId, datagramSize: payload.length, partialDatagram: chunk, }); } else { cc = new TransportServiceCCSubsequentSegment({ nodeId, sessionId, datagramSize: payload.length, datagramOffset: segment * MAX_SEGMENT_SIZE, partialDatagram: chunk, }); } const tmsg = driver.createSendDataMessage(cc, { autoEncapsulate: false, maxSendAttempts: msg.maxSendAttempts, transmitOptions: msg.transmitOptions, }); result = yield* simpleMessageGenerator(driver, ctx, tmsg, onMessageSent); let segmentComplete = undefined; // After sending the last segment, wait for a SegmentComplete response, at the same time // give the node a chance to send a SegmentWait or SegmentRequest(s) if (segment === numSegments - 1) { segmentComplete = await driver .waitForCommand((cc) => cc.nodeId === nodeId && cc instanceof TransportServiceCCSegmentComplete && cc.sessionId === sessionId, TransportServiceTimeouts.segmentCompleteR2) .catch(() => undefined); } if (segmentComplete) { // We're done! driver.controllerLog.logNode(nodeId, { message: `Transport Service TX session #${sessionId} complete`, level: "debug", direction: "outbound", }); break attempts; } // If we received a SegmentWait, we need to wait and restart const segmentWait = receivedSegmentWait(); if (segmentWait) { const waitTime = segmentWait.pendingSegments * 100; driver.controllerLog.logNode(nodeId, { message: `Restarting Transport Service TX session #${sessionId} in ${waitTime} ms...`, level: "debug", }); await wait(waitTime, true); continue attempts; } // If the node requested missing segments, add them to the list of unsent segments and continue transmitting let segmentRequest = undefined; let readdedSegments = false; while ((segmentRequest = receivedSegmentRequest())) { unsentSegments.push(segmentRequest.datagramOffset / MAX_SEGMENT_SIZE); readdedSegments = true; } if (readdedSegments) continue; // If we didn't receive anything after sending the last segment, retry the last segment if (segment === numSegments - 1) { if (didRetryLastSegment) { driver.controllerLog.logNode(nodeId, { message: `Transport Service TX session #${sessionId} failed`, level: "debug", direction: "outbound", }); break attempts; } else { // Try the last segment again driver.controllerLog.logNode(nodeId, { message: `Transport Service TX session #${sessionId}: Segment Complete missing - re-transmitting last segment...`, level: "debug", direction: "outbound", }); didRetryLastSegment = true; unsentSegments.unshift(segment); continue; } } } } } finally { // We're done, unregister the handler unregisterHandler(); } // Transport Service CCs do not expect a node update and have no knowledge about the encapsulated CC. // Therefore we need to replicate the waiting from simpleMessageGenerator here // If the sent message expects an update from the node, wait for it if (msg.expectsNodeUpdate(driver)) { // TODO: Figure out if we can handle premature updates with Transport Service CC const timeout = getNodeUpdateTimeout(driver, msg, additionalCommandTimeoutMs); return waitForNodeUpdate(driver, msg, timeout); } return result; }; /** A simple (internal) generator that simply sends a command, and optionally returns the response command */ async function* sendCommandGenerator(driver, ctx, command, onMessageSent, options) { const msg = driver.createSendDataMessage(command, options); const resp = yield* maybeTransportServiceGenerator(driver, ctx, msg, onMessageSent); if (resp && containsCC(resp)) { driver.unwrapCommands(resp); return resp.command; } } /** A message generator for security encapsulated messages (S0) */ export const secureMessageGeneratorS0 = async function* (driver, ctx, msg, onMessageSent) { /*if (!isSendData(msg)) { throw new ZWaveError( "Cannot use the S0 message generator for a command that's not a SendData message!", ZWaveErrorCodes.Argument_Invalid, ); } else*/ if (typeof msg.command.nodeId !== "number") { throw new ZWaveError("Cannot use the S0 message generator for multicast commands!", ZWaveErrorCodes.Argument_Invalid); } else if (!(msg.command instanceof SecurityCCCommandEncapsulation)) { throw new ZWaveError("The S0 message generator can only be used for Security S0 command encapsulation!", ZWaveErrorCodes.Argument_Invalid); } // Step 1: Acquire a nonce const secMan = driver.securityManager; const nodeId = msg.command.nodeId; let additionalTimeoutMs; // Try to get a free nonce before requesting a new one let nonce = secMan.getFreeNonce(nodeId); if (!nonce) { // No free nonce, request a new one const cc = new SecurityCCNonceGet({ nodeId: nodeId, endpointIndex: msg.command.endpointIndex, }); const nonceResp = yield* sendCommandGenerator(driver, ctx, cc, (msg, result) => { additionalTimeoutMs = Math.ceil(msg.rtt / 1e6); onMessageSent(msg, result); }, { // Only try getting a nonce once maxSendAttempts: 1, }); if (!nonceResp) { throw new ZWaveError("No nonce received from the node, cannot send secure command!", ZWaveErrorCodes.SecurityCC_NoNonce); } nonce = nonceResp.nonce; } msg.command.nonce = nonce; // Now send the actual secure command return yield* simpleMessageGenerator(driver, ctx, msg, onMessageSent, additionalTimeoutMs); }; /** A message generator for security encapsulated messages (S2) */ export const secureMessageGeneratorS2 = async function* (driver, ctx, msg, onMessageSent) { if (!isSendData(msg) || !containsCC(msg)) { throw new ZWaveError("Cannot use the S2 message generator for a command that's not a SendData message!", ZWaveErrorCodes.Argument_Invalid); } else if (typeof msg.command.nodeId !== "number") { throw new ZWaveError("Cannot use the S2 message generator for multicast commands!", ZWaveErrorCodes.Argument_Invalid); } else if (!(msg.command instanceof Security2CCMessageEncapsulation)) { throw new ZWaveError("The S2 message generator can only be used for Security S2 command encapsulation!", ZWaveErrorCodes.Argument_Invalid); } const nodeId = msg.command.nodeId; const secMan = driver.getSecurityManager2(nodeId); const spanState = secMan.getSPANState(nodeId); let additionalTimeoutMs; // We need a new nonce when there is no shared SPAN state, or the SPAN state is for a lower security class // than the command we want to send const expectedSecurityClass = msg.command.securityClass ?? driver.getHighestSecurityClass(nodeId); if (spanState.type === SPANState.None || spanState.type === SPANState.LocalEI || (spanState.type === SPANState.SPAN && spanState.securityClass !== SecurityClass.Temporary && spanState.securityClass !== expectedSecurityClass)) { // Request a new nonce // No free nonce, request a new one const cc = new Security2CCNonceGet({ nodeId: nodeId, endpointIndex: msg.command.endpointIndex, }); const nonceResp = yield* sendCommandGenerator(driver, ctx, cc, (msg, result) => { additionalTimeoutMs = Math.ceil(msg.rtt / 1e6); onMessageSent(msg, result); }, { // Only try getting a nonce once maxSendAttempts: 1, }); if (!nonceResp) { throw new ZWaveError("No nonce received from the node, cannot send secure command!", ZWaveErrorCodes.Security2CC_NoSPAN); } // Storing the nonce is not necessary, this will be done automatically when the nonce is received } // Now send the actual secure command let response = yield* maybeTransportServiceGenerator(driver, ctx, msg, onMessageSent, additionalTimeoutMs); // If we want to make sure that a node understood a SET-type S2-encapsulated message, we either need to use // Supervision and wait for the Supervision Report (handled by the simpleMessageGenerator), or we need to add a // short delay between commands and wait if a NonceReport is received. // However, in situations where timing is critical (e.g. S2 bootstrapping), verifyDelivery is set to false, and we don't do this. let nonceReport; if (isTransmitReport(response) && msg.command.verifyDelivery && !msg.command.expectsCCResponse(driver) && !msg.command.getEncapsulatedCC(CommandClasses.Supervision, SupervisionCommand.Get)) { nonceReport = await driver .waitForCommand((cc) => cc.nodeId === nodeId && cc instanceof Security2CCNonceReport, 500) .catch(() => undefined); } else if (containsCC(response) && response.command instanceof Security2CCNonceReport) { nonceReport = response.command; } if (nonceReport) { if (nonceReport.SOS && nonceReport.receiverEI) { // The node couldn't decrypt the last command we sent it. Invalidate // the shared SPAN, since it did the same secMan.storeRemoteEI(nodeId, nonceReport.receiverEI); } if (nonceReport.MOS) { const multicastGroupId = msg.command.getMulticastGroupId(); if (multicastGroupId != undefined) { // The node couldn't decrypt the previous S2 multicast. Tell it the MPAN (again) const mpan = secMan.getInnerMPANState(multicastGroupId); if (mpan) { // Replace the MGRP extension with an MPAN extension msg.command.extensions = msg.command.extensions.filter((e) => !(e instanceof MGRPExtension)); msg.command.extensions.push(new MPANExtension({ groupId: multicastGroupId, innerMPANState: mpan, })); } } } driver.controllerLog.logNode(nodeId, { message: `failed to decode the message, retrying with SPAN extension...`, direction: "none", }); // Send the message again msg.prepareRetransmission(); response = yield* maybeTransportServiceGenerator(driver, ctx, msg, onMessageSent, additionalTimeoutMs); if (containsCC(response) && response.command instanceof Security2CCNonceReport) { // No dice driver.controllerLog.logNode(nodeId, { message: `failed to decode the message after re-transmission with SPAN extension, dropping the message.`, direction: "none", level: "warn", }); throw new ZWaveError("The node failed to decode the message.", ZWaveErrorCodes.Security2CC_CannotDecode); } } return response; }; /** A message generator for security encapsulated messages (S2 Multicast) */ export const secureMessageGeneratorS2Multicast = async function* (driver, ctx, msg, onMessageSent) { if (!isSendData(msg) || !containsCC(msg)) { throw new ZWaveError("Cannot use the S2 multicast message generator for a command that's not a SendData message!", ZWaveErrorCodes.Argument_Invalid); } else if (msg.command.isSinglecast()) { throw new ZWaveError("Cannot use the S2 multicast message generator for singlecast commands!", ZWaveErrorCodes.Argument_Invalid); } else if (!(msg.command instanceof Security2CCMessageEncapsulation)) { throw new ZWaveError("The S2 multicast message generator can only be used for Security S2 command encapsulation!", ZWaveErrorCodes.Argument_Invalid); } const groupId = msg.command.getMulticastGroupId(); if (groupId == undefined) { throw new ZWaveError("Cannot use the S2 multicast message generator without a multicast group ID!", ZWaveErrorCodes.Argument_Invalid); } const secMan = driver.getSecurityManager2(msg.command.nodeId); const group = secMan.getMulticastGroup(groupId); if (!group) { throw new ZWaveError(`Multicast group ${groupId} does not exist!`, ZWaveErrorCodes.Argument_Invalid); } // Send the multicast command. We remember the transmit report and treat it as the result of the multicast command const response = yield* simpleMessageGenerator(driver, ctx, msg, onMessageSent); // If a node in the group is out of sync, we need to transfer the MPAN state we're going to use for the next command. // Therefore increment the MPAN state now and not after the followups like the specs mention secMan.tryIncrementMPAN(groupId); // Unwrap the command again, so we can make the following encapsulation depend on the target node driver.unwrapCommands(msg); const command = msg.command; // Remember the original encapsulation flags const encapsulationFlags = command.encapsulationFlags; // In case someone sneaked a node ID into the group multiple times, remove duplicates for the singlecast followups // Otherwise, the node will increase its MPAN multiple times, going out of sync. const distinctNodeIDs = [...new Set(group.nodeIDs)]; const supervisionResults = []; // Now do singlecast followups with every node in the group for (const nodeId of distinctNodeIDs) { // Point the CC at the target node command.nodeId = nodeId; // Figure out if supervision should be used command.encapsulationFlags = encapsulationFlags; command.toggleEncapsulationFlag(EncapsulationFlags.Supervision, SupervisionCC.mayUseSupervision(driver, command)); const scMsg = driver.createSendDataMessage(command, { transmitOptions: msg.transmitOptions, maxSendAttempts: msg.maxSendAttempts, }); // The outermost command is a Security2CCMessageEncapsulation, we need to set the MGRP extension on this again scMsg.command.extensions.push(new MGRPExtension({ groupId })); // Reuse the S2 singlecast message generator for sending this new message try { const scResponse = yield* secureMessageGeneratorS2(driver, ctx, scMsg, onMessageSent); if (containsCC(scResponse) && scResponse.command instanceof Security2CCMessageEncapsulation && scResponse.command.hasMOSExtension()) { // The node understood the S2 singlecast followup, but told us that its MPAN is out of sync const innerMPANState = secMan.getInnerMPANState(groupId); // This should always be defined, but better not throw unnecessarily here if (innerMPANState) { const cc = new Security2CCMessageEncapsulation({ nodeId, extensions: [ new MPANExtension({ groupId, innerMPANState, }), ], }); // Send it the MPAN yield* sendCommandGenerator(driver, ctx, cc, onMessageSent, { // Seems we need these options or some nodes won't accept the nonce transmitOptions: TransmitOptions.ACK | TransmitOptions.AutoRoute, // Only try sending a nonce once maxSendAttempts: 1, // Nonce requests must be handled immediately priority: MessagePriority.Immediate, // We don't want failures causing us to treat the node as asleep or dead changeNodeStatusOnMissingACK: false, }); } } // Collect supervision results if possible if (containsCC(scResponse)) { const supervisionReport = scResponse.command .getEncapsulatedCC(CommandClasses.Supervision, SupervisionCommand.Report); supervisionResults.push(supervisionReport?.toSupervisionResult()); } } catch (e) { driver.driverLog.print(getErrorMessage(e), "error"); // TODO: Figure out how we got here, and what to do now. // In any case, keep going with the next nodes // Report that there was a failure, so the application can show it supervisionResults.push({ status: SupervisionStatus.Fail, }); } } const finalSupervisionResult = mergeSupervisionResults(supervisionResults); if (finalSupervisionResult) { // We can return return information about the success of this multicast - so we should // TODO: Not sure if we need to "wrap" the response for something. For now, try faking it const cc = new SupervisionCCReport({ nodeId: NODE_ID_BROADCAST, sessionId: 0, // fake moreUpdatesFollow: false, // fake ...finalSupervisionResult, }); const ret = new (driver.getSendDataSinglecastConstructor())({ sourceNodeId: driver.ownNodeId, command: cc, }); return ret; } else { return response; } }; export function createMessageGenerator(driver, ctx, msg, onMessageSent) { const resultPromise = createDeferredPromise(); const generator = { parent: undefined, // The transaction will set this field on creation current: undefined, self: undefined, reset: () => { generator.current = undefined; generator.self = undefined; }, start: () => { async function* gen() { // Determine which message generator implementation should be used let implementation = simpleMessageGenerator; if (isSendData(msg)) { if (!containsCC(msg)) { throw new ZWaveError("Cannot create a message generator for a message that doesn't contain a command class", ZWaveErrorCodes.Argument_Invalid); } if (msg.command instanceof Security2CCMessageEncapsulation) { implementation = (msg.command.isSinglecast() ? secureMessageGeneratorS2 : secureMessageGeneratorS2Multicast); } else if (msg.command instanceof SecurityCCCommandEncapsulation) { implementation = secureMessageGeneratorS0; } else if (msg.command.isSinglecast()) { implementation = maybeTransportServiceGenerator; } } // Step through the generator so we can easily cancel it and don't // accidentally forget to unset this.current at the end const gen = implementation(driver, ctx, msg, onMessageSent); let sendResult; let result; while (true) { // This call passes the previous send result (if it exists already) to the generator and saves the // generated or returned message in `value`. When `done` is true, `value` contains the returned result of the message generator try { const { value, done } = await gen.next(sendResult); if (done) { result = value; break; } // Pass the generated message to the driver and remember the result for the next iteration generator.current = value; sendResult = yield generator.current; } catch (e) { if (e instanceof Error) { // There was an actual error, reject the transaction resultPromise.reject(e); } else if (isTransmitReport(e) && !e.isOK()) { // The generator was prematurely ended by throwing a NOK transmit report. // The driver may want to retry it, so reset the generator generator.reset(); return; } else { // The generator was prematurely ended by throwing a Message resultPromise.resolve(e); } break; } } resultPromise.resolve(result); generator.reset(); return; } generator.self = gen(); return generator.self; }, }; return { resultPromise, generator }; } //# sourceMappingURL=MessageGenerators.js.map