import { assign , createMachine , Interpreter , MachineConfig , MachineOptions , StateMachine , } from "xstate" ; import { send } from "xstate/lib/actions" ; import { MessageType } from "../message/Constants" ; import type { Message } from "../message/Message" ; import { isMultiStageCallback , isSuccessIndicator , } from "../message/SuccessIndicator" ; import { respondUnsolicited , ServiceImplementations , } from "./StateMachineShared" ; import type { ZWaveOptions } from "./ZWaveOptions" ; export interface SerialAPICommandStateSchema { states : { sending : { } ; waitForACK : { } ; waitForResponse : { } ; waitForCallback : { } ; retry : { } ; retryWait : { } ; failure : { } ; success : { } ; } ; } export type SerialAPICommandError = "send failure" | "CAN" | "NAK" | "ACK timeout" | "response timeout" | "callback timeout" | "response NOK" | "callback NOK" ; export interface SerialAPICommandContext { msg : Message ; data : Buffer ; attempts : number ; maxAttempts : number ; lastError ? : SerialAPICommandError ; result ? : Message ; txTimestamp ? : number ; } export type SerialAPICommandEvent = { type : "ACK" ; } | { type : "CAN" ; } | { type : "NAK" ; } | { type : "message" ; message : Message ; } | { type : "response" ; message : Message ; } | { type : "callback" ; message : Message ; } | { type : "unsolicited" ; message : Message ; } ; export type SerialAPICommandDoneData = { type : "success" ; txTimestamp : number ; result ? : Message ; } | ( { type : "failure" ; } & ( { reason : "send failure" | "CAN" | "NAK" | "ACK timeout" | "response timeout" | "callback timeout" ; result ? : undefined ; } | { reason : "response NOK" | "callback NOK" ; result : Message ; } ) ) ; function computeRetryDelay ( ctx ) { return 100 + 1000 * ( ctx . attempts - 1 ) ; } const forwardMessage = send ( ( _ , evt ) => { const msg = ( evt as any ) . message as Message ; return { type : msg . type === MessageType . Response ? "response" : "callback" , message : msg } as SerialAPICommandEvent ; } ) ; export type SerialAPICommandMachineConfig = MachineConfig < SerialAPICommandContext , SerialAPICommandStateSchema , SerialAPICommandEvent > ; export type SerialAPICommandMachine = StateMachine < SerialAPICommandContext , SerialAPICommandStateSchema , SerialAPICommandEvent > ; export type SerialAPICommandInterpreter = Interpreter < SerialAPICommandContext , SerialAPICommandStateSchema , SerialAPICommandEvent > ; export type SerialAPICommandMachineOptions = Partial < MachineOptions < SerialAPICommandContext , SerialAPICommandEvent > > ; export type SerialAPICommandMachineParams = { timeouts : Pick < ZWaveOptions [ "timeouts" ] , "ack" | "response" | "sendDataCallback" > ; attempts : Pick < ZWaveOptions [ "attempts" ] , "controller" > ; } ; export function getSerialAPICommandMachineConfig ( message , { timestamp , logOutgoingMessage , } , attemptsConfig ) { return { id : "serialAPICommand" , initial : "sending" , context : { msg : message , data : message . serialize ( ) , attempts : 0 , maxAttempts : attemptsConfig . controller } , on : { message : [ { cond : "isExpectedMessage" , actions : forwardMessage as any } , { actions : respondUnsolicited } , ] } , states : { sending : { entry : [ assign ( { attempts : ( ctx ) => ctx . attempts + 1 , txTimestamp : ( _ ) => timestamp ( ) } ) , ( ctx ) => logOutgoingMessage ( ctx . msg ) , ] , invoke : { id : "sendMessage" , src : "send" , onDone : "waitForACK" , onError : { target : "retry" , actions : assign ( { lastError : ( _ ) => "send failure" } ) } } } , waitForACK : { on : { CAN : { target : "retry" , actions : assign ( { lastError : ( _ ) => "CAN" } ) } , NAK : { target : "retry" , actions : assign ( { lastError : ( _ ) => "NAK" } ) } , ACK : "waitForResponse" } , after : { ACK_TIMEOUT : { target : "retry" , actions : assign ( { lastError : ( _ ) => "ACK timeout" } ) } } } , waitForResponse : { always : [ { target : "waitForCallback" , cond : "expectsNoResponse" } , ] , on : { response : [ { target : "retry" , cond : "responseIsNOK" , actions : assign ( { lastError : ( _ ) => "response NOK" , result : ( _ , evt ) => ( evt as any ) . message } ) } , { target : "waitForCallback" , actions : assign ( { result : ( _ , evt ) => ( evt as any ) . message } ) } , ] } , after : { RESPONSE_TIMEOUT : { target : "retry" , actions : assign ( { lastError : ( _ ) => "response timeout" } ) } } } , waitForCallback : { always : [ { target : "success" , cond : "expectsNoCallback" } ] , on : { callback : [ { target : "failure" , cond : "callbackIsNOK" , actions : assign ( { lastError : ( _ ) => "callback NOK" , result : ( _ , evt ) => ( evt as any ) . message } ) } , { target : "success" , cond : "callbackIsFinal" , actions : assign ( { result : ( _ , evt ) => ( evt as any ) . message } ) } , { target : "waitForCallback" } , ] } , after : { CALLBACK_TIMEOUT : { target : "failure" , actions : assign ( { lastError : ( _ ) => "callback timeout" } ) } } } , retry : { always : [ { target : "retryWait" , cond : "mayRetry" } , { target : "failure" } , ] } , retryWait : { invoke : { id : "notify" , src : "notifyRetry" } , after : { RETRY_DELAY : "sending" } } , success : { type : "final" , data : { type : "success" , txTimestamp : ( ctx ) => ctx . txTimestamp ! , result : ( ctx ) => ctx . result } } , failure : { type : "final" , data : { type : "failure" , reason : ( ctx ) => ctx . lastError , result : ( ctx ) => ctx . result ! } } } } ; } export function getSerialAPICommandMachineOptions ( { sendData , notifyRetry , } , timeoutConfig ) { return { services : { send : ( ctx ) => sendData ( ctx . data ) , notifyRetry : ( ctx ) => { notifyRetry ?. ( "SerialAPI" , ctx . lastError , ctx . msg , ctx . attempts , ctx . maxAttempts , computeRetryDelay ( ctx ) ) ; return Promise . resolve ( ) ; } } , guards : { mayRetry : ( ctx ) => ctx . attempts < ctx . maxAttempts , expectsNoResponse : ( ctx ) => ! ctx . msg . expectsResponse ( ) , expectsNoCallback : ( ctx ) => ! ctx . msg . expectsCallback ( ) , isExpectedMessage : ( ctx , evt , meta ) => meta . state . matches ( "waitForResponse" ) ? ctx . msg . isExpectedResponse ( ( evt as any ) . message ) : meta . state . matches ( "waitForCallback" ) ? ctx . msg . isExpectedCallback ( ( evt as any ) . message ) : false , responseIsNOK : ( ctx , evt ) => evt . type === "response" && isSuccessIndicator ( evt . message ) && ! evt . message . isOK ( ) , callbackIsNOK : ( ctx , evt ) => evt . type === "callback" && isSuccessIndicator ( evt . message ) && ! evt . message . isOK ( ) , callbackIsFinal : ( ctx , evt ) => evt . type === "callback" && ( ! isSuccessIndicator ( evt . message ) || evt . message . isOK ( ) ) && ( ! isMultiStageCallback ( evt . message ) || evt . message . isFinal ( ) ) } , delays : { RETRY_DELAY : ( ctx ) => computeRetryDelay ( ctx ) , RESPONSE_TIMEOUT : timeoutConfig . response , CALLBACK_TIMEOUT : ( ctx ) => { return ( ctx . msg . getCallbackTimeout ( ) || timeoutConfig . sendDataCallback ) ; } , ACK_TIMEOUT : timeoutConfig . ack } } ; } export function createSerialAPICommandMachine ( message , implementations , params ) { return createMachine ( getSerialAPICommandMachineConfig ( message , implementations , params . attempts ) , getSerialAPICommandMachineOptions ( implementations , params . timeouts ) ) ; }