constructor(options) {
        this.codec = new Codec({token: options.token});
        this.deviceId = options.deviceId;
        this.socket = options.socket;
        this.rinfo = options.rinfo;
        this.timeout = options.timeout ?? 500; // default timeout: 0.5s
        this.name = options.name;
        this.nextId = 1; //Only used by cloud sockets

        /**
         * @type {Object.<string, {
         *          timeout_id?: NodeJS.Timeout,
         *          onTimeoutCallback: () => void,
         *          resolve: (result: any) => void,
         *          reject: (err: any) => void,
         *          method: string
         *      }>}
         */
        this.pendingRequests = {};
        this.onIncomingRequestMessage = options.onIncomingRequestMessage;
        this.onConnected = options.onConnected;
        this.connected = false;
        this.isCloudSocket = options.isCloudSocket;
        /** @type {null | ((msg: import("./DecodedMiioPacket")) => void)} */
        this.onEmptyPacket = null;


        this.socket.on("message", (incomingMsg, rinfo) => {
            this.rinfo = rinfo;
            const decodedIncomingPacket = this.codec.decodeIncomingMiioPacket(incomingMsg);

            if (
                this.deviceId !== decodedIncomingPacket.deviceId &&
                decodedIncomingPacket.deviceId !== 0xffffffff // Handshake response did is always 0xffffffff and not the real one
            ) {
                this.deviceId = decodedIncomingPacket.deviceId;

                Logger.info(`Got new DeviceID: ${this.deviceId}`);
            }

            const msg = decodedIncomingPacket.msg;

            Logger.debug(`<<< ${this.name}${msg ? ":" : "*"}`, msg ?? {stamp: decodedIncomingPacket.stamp});


            if (msg === null) {
                if (this.isCloudSocket) {
                    if (decodedIncomingPacket.stamp === 0) {
                        //Important note: Responding with a time sync packet causes the miio_client to restart

                        const response = createMiioHeader({timestamp: new Date().getTime() / 1000});
                        Logger.debug(">>> Responding to time sync request");

                        this.socket.send(response, 0, response.length, this.rinfo.port, this.rinfo.address);

                    } else if (
                        this.codec.stamp.val === undefined ||
                        this.codec.stamp.val < decodedIncomingPacket.stamp
                    ) {
                        // Keep-alive packet. Update our stamp and respond with echo
                        this.codec.updateStamp(decodedIncomingPacket.stamp);
                        Logger.debug(">>> " + this.name + "*", {stamp: decodedIncomingPacket.stamp});


                        this.socket.send(incomingMsg, 0, incomingMsg.length, this.rinfo.port, this.rinfo.address);
                    }
                } else {
                    this.codec.updateStamp(decodedIncomingPacket.stamp);

                    /*
                        This exists so that the RetryWrapper can hook the message processing so that it
                        knows when a successful handshake happened
                     */
                    if (typeof this.onEmptyPacket === "function") {
                        this.onEmptyPacket(decodedIncomingPacket);
                    }
                }
            } else {
                this.codec.updateStamp(decodedIncomingPacket.stamp);

                if (msg["id"] && (msg["result"] !== undefined || msg["error"] !== undefined)) {
                    const pendingRequestWithMatchingMsgId = this.pendingRequests[msg["id"]];

                    if (pendingRequestWithMatchingMsgId) {
                        clearTimeout(pendingRequestWithMatchingMsgId.timeout_id);

                        if (msg["error"] !== undefined) {
                            /*
                                "user ack timeout" is sent by the miio_client if the robots business logic
                                fails to respond to a request from us in a timely fashion
                             */
                            if (msg["error"].message !== "user ack timeout") {
                                Logger.info("Miio error response", msg);
                            } else {
                                Logger.trace("Miio error response", msg);
                            }

                            pendingRequestWithMatchingMsgId.reject(
                                new MiioErrorResponseRobotFirmwareError(
                                    msg["error"].message,
                                    msg["error"]
                                )
                            );
                        } else {
                            pendingRequestWithMatchingMsgId.resolve(msg["result"]);
                        }

                        delete this.pendingRequests[msg["id"]];
                    } else {
                        Logger.debug("<< " + this.name + ": ignoring response for non-pending request", msg);
                    }
                } else if (msg["error"]) {
                    Logger.warn("unhandled error response", msg);
                } else {
                    if (typeof this.onIncomingRequestMessage === "function") {
                        this.onIncomingRequestMessage(msg);
                    }
                }
            }

            if (!this.connected && typeof this.onConnected === "function") {
                this.connected = true;

                this.onConnected();
            }
        });
    }
@param {object} options
@param {import("dgram").Socket} options.socket
@param {Buffer} options.token The crypto key for this connection.
@param {number=} options.deviceId The unique Device-id of your robot
@param {{address: string, port: number}=} options.rinfo address and port of what we're talking to
@param {number=} options.timeout timeout in milliseconds to wait for a response
@param {(msg: any) => void} [options.onIncomingRequestMessage]
@param {(() => void)=} [options.onConnected] function to call after completing a handshake
@param {string} options.name Name used to disambiguate logging messages
@param {boolean=} options.isCloudSocket Cloud sockets send time sync replies