#include "DeviceConnection.h" #include "BoostLog.h" #include "StringUtility.h" #include #include #include #include #include #include #include #include #include #include #include DeviceConnection::DeviceConnection(QObject *parent) : QObject{parent} { } DeviceConnection::~DeviceConnection() { close(); } void DeviceConnection::close() { if (m_otaTimer != nullptr) { m_otaTimer->deleteLater(); m_otaTimer = nullptr; } if (m_heartbeatTimerId > 0) { killTimer(m_heartbeatTimerId); m_heartbeatTimerId = -1; } if (m_commandSocket != nullptr) { m_commandSocket->deleteLater(); m_commandSocket = nullptr; } if (m_h264Socket != nullptr) { m_h264Socket->deleteLater(); m_h264Socket = nullptr; } while (!m_requests.empty()) { m_requests.pop(); } } DeviceConnection::Infomation DeviceConnection::infomation() const { return m_infomation; } bool DeviceConnection::isConnected() const { bool ret = false; if (m_commandSocket != nullptr) { // LOG(info) << "DeviceConnection::isConnected " << m_commandSocket->isValid(); ret = m_commandSocket->isValid() && (m_commandSocket->state() == QTcpSocket::ConnectedState); } return ret; } void DeviceConnection::connect(const Infomation &infomation) { close(); m_infomation = infomation; m_commandSocket = new QTcpSocket(this); QObject::connect(m_commandSocket, &QTcpSocket::disconnected, this, &DeviceConnection::onDisconnected); QObject::connect(m_commandSocket, &QTcpSocket::errorOccurred, this, &DeviceConnection::onErrorOccurred); m_h264Socket = new QTcpSocket(this); QObject::connect(m_commandSocket, &QTcpSocket::connected, this, &DeviceConnection::onConnected); QObject::connect(m_h264Socket, &QTcpSocket::connected, this, &DeviceConnection::onConnected); QObject::connect(m_h264Socket, &QTcpSocket::readyRead, this, &DeviceConnection::onH264ReadyRead); QObject::connect(m_commandSocket, &QTcpSocket::readyRead, this, &DeviceConnection::onCommandReadyRead); LOG(info) << "connect to " << infomation.ip.toStdString(); m_commandSocket->connectToHost(infomation.ip, 8000); m_h264Socket->connectToHost(infomation.ip, 8000); } NetworkInfomation DeviceConnection::networkInfomation() const { return m_networkInfomation; } void DeviceConnection::setLiveStreamEnabled(bool enabled) { m_videoEnabled = enabled; boost::json::object request; request["func"] = enabled ? "openlivestream_setdata" : "closelivestream_setdata"; request["deviceid"] = "0"; boost::json::object data; data["value"] = "1"; request["data"] = std::move(data); auto text = boost::json::serialize(request); m_h264Socket->write(text.data(), text.size()); } void DeviceConnection::requestOpenDoorArea() { Task task; task.command = "a03opendoor1_getdata"; task.task = [this]() { boost::json::object request; request["func"] = "a03opendoor1_getdata"; request["deviceid"] = "0"; boost::json::object data; request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } QFuture DeviceConnection::updateOpenDoorAreaPoints(AreaWay way, const QList &points) { Task task; task.command = "a03opendoor1_setdata"; task.task = [this, way, points]() { boost::json::object request; request["func"] = "a03opendoor1_setdata"; request["deviceid"] = "0"; boost::json::object data; const char *value = "0"; if (way == FullArea) { value = "1"; } else if (way == Quadrangle) { value = "2"; } data["value"] = value; boost::json::array pointArray; for (auto &p : points) { boost::json::object point; point["x"] = p.x(); point["y"] = p.y(); pointArray.push_back(std::move(point)); } data["points"] = std::move(pointArray); request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); }; task.future = std::make_shared>(); if (m_requests.empty()) { task.task(); } auto ret = task.future->future(); m_requests.push(task); return ret; } void DeviceConnection::requestShieldedArea() { Task task; task.command = "a03opendoor4_getdata"; task.task = [this]() { boost::json::object request; request["func"] = "a03opendoor4_getdata"; request["deviceid"] = "0"; boost::json::object data; request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } void DeviceConnection::updateShieldedAreaPoints(bool enabled, const QList &points) { Task task; task.command = "a03opendoor4_setdata"; task.task = [this, enabled, points]() { boost::json::object request; request["func"] = "a03opendoor4_setdata"; request["deviceid"] = "0"; boost::json::object data; data["value"] = enabled ? "1" : "0"; boost::json::array pointArray; for (auto &p : points) { boost::json::object point; point["x"] = p.x(); point["y"] = p.y(); pointArray.push_back(std::move(point)); } data["points"] = std::move(pointArray); request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } void DeviceConnection::requestAntiClipArea() { Task task; task.command = "a03opendoor5_getdata"; task.task = [this]() { boost::json::object request; request["func"] = "a03opendoor5_getdata"; request["deviceid"] = "0"; boost::json::object data; request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } void DeviceConnection::updateAntiClipAreaPoints(bool enabled, const QList &points) { Task task; task.command = "a03opendoor5_setdata"; task.task = [this, enabled, points]() { boost::json::object request; request["func"] = "a03opendoor5_setdata"; request["deviceid"] = "0"; boost::json::object data; data["value"] = enabled ? "1" : "0"; boost::json::array pointArray; for (auto &p : points) { boost::json::object point; point["x"] = p.x(); point["y"] = p.y(); pointArray.push_back(std::move(point)); } data["points"] = std::move(pointArray); request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); LOG(info) << "updateAntiClipAreaPoints"; }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } void DeviceConnection::requestResolution(Resolution resolution) { Task task; task.command = "quality_setdata"; task.task = [this, resolution]() { boost::json::object request; request["func"] = "quality_setdata"; request["deviceid"] = "0"; boost::json::object data; data["value"] = static_cast(resolution); request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); LOG(info) << "requestResolution"; }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } void DeviceConnection::updateRotation(int rotation) { Task task; task.command = "a23imagerotate_setdata"; task.task = [this, rotation]() { boost::json::object request; request["func"] = "a23imagerotate_setdata"; request["deviceid"] = "0"; boost::json::object data; data["value"] = std::to_string(rotation); request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); LOG(info) << "updateRotation: " << text; }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } void DeviceConnection::updateFlip(bool flip) { Task task; task.command = "a04imageflipping_setdata"; task.task = [this, flip]() { boost::json::object request; request["func"] = "a04imageflipping_setdata"; request["deviceid"] = "0"; boost::json::object data; data["value"] = flip ? "2" : "1"; request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } void DeviceConnection::requestVersion() { Task task; task.command = "a15devicedetail_getdata"; task.task = [this]() { boost::json::object request; request["func"] = "a15devicedetail_getdata"; request["deviceid"] = "0"; boost::json::object data; request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); LOG(info) << "requestVersion"; }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } void DeviceConnection::requestVideoInformation() { constexpr const char *commands[] = {"a23imagerotate_getdata", "a04imageflipping_getdata"}; for (auto command : commands) { Task task; task.command = command; task.task = [this, command]() { boost::json::object request; request["func"] = command; request["deviceid"] = "0"; boost::json::object data; request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } } void DeviceConnection::requestNetworkInfomation() { Task task; task.command = "netconfig_getdata"; task.task = [this]() { boost::json::object request; request["func"] = "netconfig_getdata"; request["deviceid"] = "0"; boost::json::object data; request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); LOG(info) << "requestNetworkInfomation"; }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } QFuture DeviceConnection::updateNetworkInfomation(bool dhcp, const QString &ip, const QString &netmask, const QString &gateway, const QString &dns) { Task task; task.command = "netconfig_setdata"; task.task = [this, dhcp, ip, netmask, gateway, dns]() { boost::json::object request; request["func"] = "netconfig_setdata"; request["deviceid"] = "0"; boost::json::object data; data["type"] = dhcp ? "dhcp" : "static"; data["ip"] = ip.toStdString(); data["netmask"] = netmask.toStdString(); data["gateway"] = gateway.toStdString(); data["dns"] = dns.toStdString(); request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); LOG(info) << "requestUpdateNetworkInfomation"; }; task.future = std::make_shared>(); if (m_requests.empty()) { task.task(); } auto ret = task.future->future(); m_requests.push(task); return ret; } void DeviceConnection::requestOta(const QString &firmware, const QString &file) { m_otaProgress = 0; emit otaProgressChanged(true, m_otaProgress, "正在向设备发起OTA请求......"); if (m_heartbeatTimerId > 0) { killTimer(m_heartbeatTimerId); m_heartbeatTimerId = -1; } while (!m_requests.empty()) { // 清除之前的命令 m_requests.pop(); } Task task; task.command = "a22devicefirmware_setdata"; task.task = [this, file, firmware]() { std::ifstream ifs(Amass::StringUtility::UTF8ToGBK(file.toStdString()), std::ifstream::binary); m_uploadBuffer = std::vector((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); m_sendedSize = 0; unsigned char md5[16]; mbedtls_md5_context context; mbedtls_md5_init(&context); mbedtls_md5_starts(&context); mbedtls_md5_update(&context, m_uploadBuffer.data(), m_uploadBuffer.size()); mbedtls_md5_finish(&context, md5); mbedtls_md5_free(&context); std::stringstream oss; oss << std::hex << std::setfill('0'); for (int i = 0; i < 16; i++) { oss << std::setw(2) << static_cast(md5[i]); } boost::json::object request; request["func"] = "a22devicefirmware_setdata"; request["deviceid"] = "0"; boost::json::object data; data["target_linux04_firmware"] = firmware.toStdString(); QFileInfo fileInfo(file); data["datasize"] = fileInfo.size(); data["md5"] = oss.str(); request["data"] = std::move(data); auto text = boost::json::serialize(request); m_commandSocket->write(text.data(), text.size()); LOG(info) << "requestOta: " << text; }; if (m_requests.empty()) { task.task(); } m_requests.push(task); } void DeviceConnection::transferBinContent() { constexpr int SliceSize = 1024; constexpr int WaitMd5CheckTime = 3000; // ms if (m_sendedSize >= m_uploadBuffer.size()) return; char buffer[1 + sizeof(int32_t) + 1024]; int sendSize = SliceSize; if ((m_sendedSize + SliceSize) > m_uploadBuffer.size()) { sendSize = m_uploadBuffer.size() - m_sendedSize; } memcpy(buffer + 1 + sizeof(int32_t), m_uploadBuffer.data() + m_sendedSize, sendSize); buffer[0] = ':'; auto contentSize = reinterpret_cast(&buffer[1]); *contentSize = htonl(sendSize); m_commandSocket->write(buffer, sendSize + 1 + sizeof(uint32_t)); m_sendedSize += sendSize; auto fileProgress = static_cast(m_sendedSize) / m_uploadBuffer.size(); m_otaProgress = 2 + 96 * fileProgress; emit otaProgressChanged(true, m_otaProgress, m_otaProgress < 98 ? "向设备发送升级固件......" : "升级固件发送完成,等待设备校验升级固件......"); if ((m_sendedSize < m_uploadBuffer.size()) && isConnected()) { QTimer::singleShot(0, this, &DeviceConnection::transferBinContent); } else if (m_sendedSize >= m_uploadBuffer.size()) { LOG(info) << "transfer ota file finished, wait " << WaitMd5CheckTime << " ms for send check, total sended size: " << m_sendedSize; if (m_otaTimer == nullptr) { m_otaTimer = new QTimer(this); m_otaTimer->setSingleShot(true); } #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) Qt::ConnectionType type = Qt::SingleShotConnection; #else Qt::ConnectionType type = Qt::UniqueConnection; #endif m_otaTimer->callOnTimeout( this, [this]() { emit otaProgressChanged(false, m_otaProgress, "升级超时,请检查设备并重新尝试..."); m_otaProgress = -1; m_commandSocket->close(); m_h264Socket->close(); }, type); m_otaTimer->start(5 * 60 * 1000); // 固件升级五分钟,正常升级2.5分钟左右(包含算法模型) } } QString DeviceConnection::handleCommand(const std::string_view &replyText, const Task *task) { QString ret; boost::system::error_code error; auto replyValue = boost::json::parse(replyText, error); if (error) { LOG(error) << "prase [" << replyText << "] failed, message: " << error.message(); return ret; } auto &reply = replyValue.as_object(); auto &function = reply.at("func").as_string(); if (function == "a03opendoor1_getdata") { auto &data = reply.at("data").as_object(); auto &value = data.at("value").as_string(); auto &pointArray = data.at("points").as_array(); QList points; for (auto &p : pointArray) { QPointF point; auto &obj = p.as_object(); point.setX(obj.at("x").as_double()); point.setY(obj.at("y").as_double()); points.push_back(point); } AreaWay way = Diabled; if (value == "1") { way = FullArea; } else if (value == "2") { way = Quadrangle; } m_infomation.openDoorAreaWay = way; m_infomation.openDoorArea = points; emit openDoorAreaChanged(way, points); } else if (function == "a03opendoor4_getdata") { auto &data = reply.at("data").as_object(); auto &value = data.at("value").as_string(); auto &pointArray = data.at("points").as_array(); QList points; for (auto &p : pointArray) { QPointF point; auto &obj = p.as_object(); point.setX(obj.at("x").as_double()); point.setY(obj.at("y").as_double()); points.push_back(point); } m_infomation.shieldedAreaEnabled = value == "1"; m_infomation.shieldedArea = points; emit shieldedAreaChanged(value == "1", points); } else if (function == "a03opendoor5_getdata") { auto &data = reply.at("data").as_object(); auto &value = data.at("value").as_string(); auto &pointArray = data.at("points").as_array(); QList points; for (auto &p : pointArray) { QPointF point; auto &obj = p.as_object(); point.setX(obj.at("x").as_double()); point.setY(obj.at("y").as_double()); points.push_back(point); } m_infomation.antiClipAreaEnabled = value == "1"; m_infomation.antiClipArea = points; emit antiClipAreaChanged(value == "1", points); } else if (function == "netconfig_getdata") { auto &data = reply.at("data").as_object(); m_networkInfomation.dhcp = data.at("type").as_string() == "dhcp"; m_networkInfomation.ip = data.at("ip").as_string().c_str(); m_networkInfomation.gateway = data.at("gateway").as_string().c_str(); m_networkInfomation.netmask = data.at("netmask").as_string().c_str(); if (data.contains("dns")) { m_networkInfomation.dns = data.at("dns").as_string().c_str(); } emit networkInfomationChanged(m_networkInfomation); LOG(info) << replyText; } else if (function == "a15devicedetail_getdata") { auto &data = reply.at("data").as_object(); auto firmware = QString::fromStdString(std::string(data.at("linux04_firmware").as_string())); if (m_infomation.firmwareVersion != firmware) { m_infomation.firmwareVersion = firmware; emit firmwareChanged(m_infomation.firmwareVersion); } } else if (function == "a03opendoor5_setdata") { requestAntiClipArea(); } else if (function == "a03opendoor4_setdata") { requestShieldedArea(); } else if (function == "a03opendoor1_setdata") { if ((task != nullptr) && (task->command.toStdString() == function)) { if (task->timeoutTimer) { task->timeoutTimer->stop(); } bool status = true; if (task->future) { task->future->reportFinished(&status); } } requestOpenDoorArea(); } else if (function == "netconfig_setdata") { if ((task != nullptr) && (task->command.toStdString() == function)) { if (task->timeoutTimer) { task->timeoutTimer->stop(); } bool status = true; if (task->future) { task->future->reportFinished(&status); } } requestNetworkInfomation(); LOG(info) << replyText; } else if (function == "a22devicefirmware_setdata") { LOG(warning) << "ota reply: " << replyText; auto &data = reply.at("data").as_object(); auto &value = data.at("value").as_string(); if (value == "1") { m_otaProgress = 1; emit otaProgressChanged(true, m_otaProgress, "设备已进入升级状态......"); QTimer::singleShot(0, this, [this]() { transferBinContent(); }); } else if (value == "2") { bool isWireConnect = true; if (data.contains("is_eth")) { isWireConnect = data.at("is_eth").as_int64() >= 1; } LOG(info) << "md5 check finished"; m_otaProgress = 99; QTimer::singleShot(0, this, [this]() { m_commandSocket->close(); // 等待设备重新上线后,发起广播。由搜索服务触发重连 m_h264Socket->close(); }); if (isWireConnect) { emit otaProgressChanged(true, m_otaProgress, "设备正在升级中,请稍后......"); } else { m_otaTimer->stop(); // 这里不需要再超时了 emit otaProgressChanged(true, 100, "设备正在升级中,请于五分钟后重新连接wifi搜索设备"); } } else { const char *message = nullptr; if (value == "3") { message = "升级固件MD5校验错误(3)"; } else if (value == "4") { message = "升级固件大小错误(4)"; } else if (value == "5") { message = "升级固件太大(5)"; } else if (value == "6") { message = "升级固件版本不匹配(6)"; } QString tip; if (message == nullptr) { tip = QString("升级失败,错误码: %1").arg(value.c_str()); } else { tip = QString("升级失败: %1").arg(message); } emit otaProgressChanged(false, m_otaProgress, tip); } } else if (function == "a23imagerotate_getdata") { // {"data":{"value":"2"},"deviceid":"0","flag":"ok","func":"a23imagerotate_getdata"} auto &data = reply.at("data").as_object(); auto &value = data.at("value").as_string(); m_infomation.rotation = std::stoi(static_cast(value)); emit rotationChanged(m_infomation.rotation); } else if (function == "a04imageflipping_getdata") { // {"data":{"value":"1"},"deviceid":"0","flag":"ok","func":"a04imageflipping_getdata"} auto &data = reply.at("data").as_object(); auto &value = data.at("value").as_string(); m_infomation.flip = value == "2"; emit flipChanged(m_infomation.flip); } else if (function == "a04imageflipping_setdata") { if ((task != nullptr) && (task->command.toStdString() == function)) { if (task->timeoutTimer) { task->timeoutTimer->stop(); } bool status = true; if (task->future) { task->future->reportFinished(&status); } } requestVideoInformation(); } else if (function == "a23imagerotate_setdata") { if ((task != nullptr) && (task->command.toStdString() == function)) { if (task->timeoutTimer) { task->timeoutTimer->stop(); } bool status = true; if (task->future) { task->future->reportFinished(&status); } } requestVideoInformation(); } else { LOG(warning) << "unknown reply: " << replyText; } return QString::fromStdString(std::string(function)); } void DeviceConnection::onConnected() { auto socket = dynamic_cast(sender()); if (socket == m_commandSocket) { requestVersion(); requestOpenDoorArea(); requestShieldedArea(); requestAntiClipArea(); requestNetworkInfomation(); requestVideoInformation(); emit connected(); m_heartbeatTimerId = startTimer(2500); if (m_otaProgress == 99) { m_otaProgress = -1; emit otaProgressChanged(true, 100, "设备升级成功!"); } if (m_otaTimer != nullptr) { m_otaTimer->stop(); } if (m_requestTimerId < 0) { m_requestTimerId = startTimer(HeartbeatInterval); } } else if (socket == m_h264Socket) { if (m_videoEnabled) { setLiveStreamEnabled(true); } } } void DeviceConnection::onDisconnected() { auto socket = dynamic_cast(sender()); if (socket == m_commandSocket) { if (m_requestTimerId > 0) { killTimer(m_requestTimerId); m_requestTimerId = -1; } if (m_heartbeatTimerId > 0) { killTimer(m_heartbeatTimerId); m_heartbeatTimerId = -1; } emit disconnected(); if ((m_otaProgress >= 0) && (m_otaProgress <= 98)) { m_otaProgress = -1; emit otaProgressChanged(false, m_otaProgress, "网络断开,设备升级失败!"); } m_h264Socket->close(); } } void DeviceConnection::setH264FrameCallback(H264FrameCallback &&callback) { m_frameCallback = std::move(callback); } void DeviceConnection::onH264ReadyRead() { auto data = m_h264Socket->readAll(); m_h264Buffer.push_back(data); while (!m_h264Buffer.isEmpty()) { auto packageSize = ntohl(*reinterpret_cast(m_h264Buffer.data())); if (m_h264Buffer.size() < (packageSize + sizeof(uint32_t))) break; // LOG(info) << "onH264ReadyRead " << data.size() << " " << packageSize; if (m_receivedFirstJsonReply) { if (m_frameCallback) { m_frameCallback(m_h264Buffer.data() + sizeof(uint32_t), packageSize); } } else { LOG(info) << "h264 reply: " << m_h264Buffer.data() + sizeof(uint32_t); m_receivedFirstJsonReply = true; } m_h264Buffer.remove(0, packageSize + sizeof(uint32_t)); } } void DeviceConnection::onCommandReadyRead() { auto data = m_commandSocket->readAll(); m_commandBuffer.push_back(data); while (!m_commandBuffer.isEmpty()) { auto packageSize = ntohl(*reinterpret_cast(m_commandBuffer.data())); if (m_commandBuffer.size() < (packageSize + sizeof(uint32_t))) break; auto command = handleCommand(std::string_view(m_commandBuffer.data() + sizeof(uint32_t), packageSize), m_requests.empty() ? nullptr : &m_requests.front()); m_commandBuffer.remove(0, packageSize + sizeof(uint32_t)); if (!m_requests.empty()) { auto &task = m_requests.front(); if (task.command == command) { m_requests.pop(); } else { LOG(warning) << "current command[" << command.toStdString() << "] is no the task queue's head[" << task.command.toStdString() << "]"; } if (!m_requests.empty()) { auto &command = m_requests.front(); command.task(); } } } } void DeviceConnection::onErrorOccurred(QAbstractSocket::SocketError socketError) { auto socket = dynamic_cast(sender()); LOG(info) << "DeviceConnection::onErrorOccurred" << socketError; } void DeviceConnection::timerEvent(QTimerEvent *event) { using namespace std::chrono; if (event->timerId() == m_heartbeatTimerId) { if (isConnected()) { int index = heartbeats % 3; if (index == 0) { requestOpenDoorArea(); } else if (index == 1) { requestShieldedArea(); } else if (index == 2) { requestAntiClipArea(); } heartbeats++; } } else if (event->timerId() == m_requestTimerId) { if (!m_requests.empty()) { auto &command = m_requests.front(); auto elapsed = duration_cast(system_clock::now() - command.time); if (elapsed > (HeartbeatInterval * 2)) { LOG(info) << "not received command[" << command.command.toStdString() << "] more than " << (HeartbeatInterval * 2).count() << " ms, consider it failed, send next command."; m_requests.pop(); if (!m_requests.empty()) { m_requests.front().task(); } } else if (elapsed > HeartbeatInterval) { LOG(info) << "not received command[" << command.command.toStdString() << "] more than " << HeartbeatInterval.count() << " ms, resend it."; command.task(); } } } }