blob: 4e80b1857be65040f55bdbcd38b3054e58c858bf [file] [log] [blame] [edit]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "app.h"
#include <alarm.h>
#include <fcntl.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fidl/epitaph.h>
#include <lib/fidl/llcpp/server.h>
#include <lib/svc/dir.h>
#include <radio.h>
#include <zircon/compiler.h>
#include <zircon/process.h>
#include <zircon/processargs.h>
#include <zircon/status.h>
#include <zircon/syscalls.h>
#include <fbl/auto_lock.h>
#include <openthread/tasklet.h>
#define OT_STACK_ASSERT assert
namespace otstack {
constexpr uint8_t kSpinelResetFrame[]{0x80, 0x06, 0x0};
OtStackApp::LowpanSpinelDeviceFidlImpl::LowpanSpinelDeviceFidlImpl(OtStackApp& app) : app_(app) {}
void OtStackApp::ClientAllowanceInit() {
client_outbound_allowance_ = kOutboundAllowanceInit;
client_inbound_allowance_ = 0;
(*binding_)->OnReadyForSendFrames(kOutboundAllowanceInit);
}
void OtStackApp::RadioAllowanceInit() {
radio_inbound_allowance_ = kInboundAllowanceInit;
radio_outbound_allowance_ = 0;
// try to open the device
auto fidl_result = device_client_ptr_->Open();
if (fidl_result.status() != ZX_OK) {
FX_LOGS(ERROR) << "FIDL error while sending open() req to ot-radio";
Shutdown();
return;
}
auto* result = fidl_result.Unwrap();
if (result->result.is_err() == false) {
// send inbound allowance
device_client_ptr_->ReadyToReceiveFrames(kInboundAllowanceInit);
} else {
FX_LOGS(DEBUG) << "ot-stack: radio returned err in spinel Open()";
}
}
void OtStackApp::HandleRadioOnReadyForSendFrame(uint32_t allowance) {
radio_outbound_allowance_ += allowance;
}
void OtStackApp::HandleClientReadyToReceiveFrames(uint32_t allowance) {
if (client_inbound_allowance_ == 0 && client_inbound_queue_.size() > 0) {
async::PostTask(loop_.dispatcher(), [this]() { this->SendOneFrameToClient(); });
}
client_inbound_allowance_ += allowance;
}
void OtStackApp::UpdateRadioOutboundAllowance() {
OT_STACK_ASSERT(radio_outbound_allowance_ > 0);
radio_outbound_allowance_--;
radio_outbound_cnt++;
FX_LOGS(DEBUG) << "ot-stack: updated radio_outbound_allowance_:" << radio_outbound_allowance_;
}
void OtStackApp::UpdateRadioInboundAllowance() {
OT_STACK_ASSERT(radio_inbound_allowance_ > 0);
radio_inbound_allowance_--;
radio_inbound_cnt++;
if (((radio_inbound_allowance_ & 1) == 0) && device_client_ptr_.get()) {
device_client_ptr_->ReadyToReceiveFrames(kInboundAllowanceInc);
radio_inbound_allowance_ += kInboundAllowanceInc;
}
FX_LOGS(DEBUG) << "ot-stack: updated radio_inbound_allowance_:" << radio_inbound_allowance_;
}
void OtStackApp::UpdateClientOutboundAllowance() {
OT_STACK_ASSERT(client_outbound_allowance_ > 0);
client_outbound_allowance_--;
client_outbound_cnt++;
if (((client_outbound_allowance_ & 1) == 0) && device_client_ptr_.get()) {
FX_LOGS(DEBUG) << "ot-stack: OnReadyForSendFrames: " << client_outbound_allowance_;
(*binding_)->OnReadyForSendFrames(kOutboundAllowanceInc);
client_outbound_allowance_ += kOutboundAllowanceInc;
}
FX_LOGS(DEBUG) << "ot-stack: updated client_outbound_allowance_:" << client_outbound_allowance_;
}
void OtStackApp::UpdateClientInboundAllowance() {
OT_STACK_ASSERT(client_inbound_allowance_ > 0);
client_inbound_allowance_--;
client_inbound_cnt++;
FX_LOGS(DEBUG) << "ot-stack: updated client_inbound_allowance_:" << client_inbound_allowance_;
}
void OtStackApp::LowpanSpinelDeviceFidlImpl::Bind(async_dispatcher_t* dispatcher,
const char* service_name,
zx_handle_t service_request) {
fidl::OnUnboundFn<OtStackApp::LowpanSpinelDeviceFidlImpl> on_unbound =
[](LowpanSpinelDeviceFidlImpl*, fidl::UnbindInfo info, zx::channel channel) {
FX_LOGS(INFO) << "channel handle " << channel.get() << " unbound with reason "
<< static_cast<uint32_t>(info.reason);
};
auto res =
fidl::BindServer(dispatcher, zx::channel(service_request), this, std::move(on_unbound));
if (res.is_error()) {
FX_LOGS(ERROR) << "Failed to bind FIDL server with status: " << res.error();
return;
}
app_.binding_ = res.take_value();
}
void OtStackApp::LowpanSpinelDeviceFidlImpl::Open(OpenCompleter::Sync& completer) {
if (app_.connected_to_device_ == false) {
FX_LOGS(ERROR) << "ot-radio not connected when client called Open()";
completer.ReplyError(fidl_spinel::Error::UNSPECIFIED);
app_.Shutdown();
return;
}
FX_LOGS(INFO) << "FIDL request Open got";
app_.ClientAllowanceInit();
// Send out the reset frame
app_.client_inbound_queue_.push_back(std::vector<uint8_t>{0x80, 0x06, 0x0, 0x70});
completer.ReplySuccess();
}
void OtStackApp::LowpanSpinelDeviceFidlImpl::Close(CloseCompleter::Sync& completer) {
if (app_.connected_to_device_ == false) {
FX_LOGS(ERROR) << "ot-radio not connected";
completer.ReplyError(fidl_spinel::Error::UNSPECIFIED);
app_.Shutdown();
return;
}
auto fidl_result = app_.device_client_ptr_->Close();
if (fidl_result.status() != ZX_OK) {
FX_LOGS(ERROR) << "FIDL error while sending req to ot-radio";
completer.ReplyError(fidl_spinel::Error::UNSPECIFIED);
app_.Shutdown();
return;
}
auto* result = fidl_result.Unwrap();
completer.Reply(std::move(result->result));
}
void OtStackApp::LowpanSpinelDeviceFidlImpl::GetMaxFrameSize(
GetMaxFrameSizeCompleter::Sync& completer) {
if (app_.connected_to_device_ == false) {
FX_LOGS(ERROR) << "ot-stack: ot-radio not connected";
app_.Shutdown();
return;
}
auto fidl_result = app_.device_client_ptr_->GetMaxFrameSize();
if (fidl_result.status() != ZX_OK) {
FX_LOGS(ERROR) << "ot-stack: FIDL error while sending req to ot-radio";
app_.Shutdown();
return;
}
auto* result = fidl_result.Unwrap();
completer.Reply(result->size);
}
void OtStackApp::PushFrameToOtLib() {
FX_LOGS(INFO) << "ot-stack: entering push frame to ot-lib task";
assert(client_outbound_queue_.size() > 0);
ot::Ncp::otNcpGetInstance()->HandleFidlReceiveDone(client_outbound_queue_.front().data(),
client_outbound_queue_.front().size());
client_outbound_queue_.pop_front();
FX_LOGS(INFO) << "ot-stack: leaving push frame to ot-lib task";
}
void OtStackApp::LowpanSpinelDeviceFidlImpl::SendFrame(::fidl::VectorView<uint8_t> data,
SendFrameCompleter::Sync& completer) {
if (app_.connected_to_device_ == false) {
FX_LOGS(ERROR) << "ot-radio not connected";
return;
}
FX_LOGS(INFO) << "ot-stack: SendFrame() received";
app_.UpdateClientOutboundAllowance();
// Invoke ot-lib
app_.client_outbound_queue_.push_back(std::vector<uint8_t>(data.cbegin(), data.cend()));
async::PostTask(app_.loop_.dispatcher(), [this]() { this->app_.PushFrameToOtLib(); });
}
void OtStackApp::LowpanSpinelDeviceFidlImpl::ReadyToReceiveFrames(
uint32_t number_of_frames, ReadyToReceiveFramesCompleter::Sync& completer) {
if (app_.connected_to_device_ == false) {
FX_LOGS(ERROR) << "ot-radio not connected";
return;
}
app_.HandleClientReadyToReceiveFrames(number_of_frames);
}
OtStackApp::OtStackCallBackImpl::OtStackCallBackImpl(OtStackApp& app) : app_(app) {}
// TODO (jiamingw): flow control, and timeout when it is unable to send out the packet
void OtStackApp::OtStackCallBackImpl::SendOneFrameToRadio(uint8_t* buffer, uint32_t size) {
::fidl::VectorView<uint8_t> data;
data.set_count(size);
data.set_data(fidl::unowned_ptr_t<uint8_t>(buffer));
if (app_.radio_outbound_allowance_ == 0) {
FX_LOGS(ERROR) << "ot-stack: radio_outbound_allowance_ is 0, cannot send packet";
return;
}
app_.device_client_ptr_->SendFrame(std::move(data));
app_.UpdateRadioOutboundAllowance();
}
std::vector<uint8_t> OtStackApp::OtStackCallBackImpl::WaitForFrameFromRadio(uint64_t timeout_us) {
FX_LOGS(INFO) << "ot-stack-callbackform: radio-callback: waiting for frame";
{
fbl::AutoLock lock(&app_.radio_q_mtx_);
if (app_.radio_inbound_queue_.size() == 0) {
sync_completion_reset(&app_.radio_rx_complete_);
}
}
zx_status_t res = sync_completion_wait(&app_.radio_rx_complete_, ZX_USEC(timeout_us));
sync_completion_reset(&app_.radio_rx_complete_);
FX_PLOGS(INFO, res) << "ot-stack-callbackform: radio-callback: waiting end";
if (res == ZX_ERR_TIMED_OUT) {
// This method will be called multiple times by ot-lib. It is okay to timeout here.
return std::vector<uint8_t>{};
} else if (res != ZX_OK) {
FX_PLOGS(ERROR, res) << "ot-stack-callbackform: radio-callback: waiting frame end with err";
return std::vector<uint8_t>{};
}
fbl::AutoLock lock0(&app_.radio_q_mtx_);
assert(app_.radio_inbound_queue_.size() > 0);
std::vector<uint8_t> vec = std::move(app_.radio_inbound_queue_.front());
app_.radio_inbound_queue_.pop_front();
return vec;
}
std::vector<uint8_t> OtStackApp::OtStackCallBackImpl::Process() {
FX_LOGS(INFO) << "ot-stack-callbackform: radio-callback: checking for frame";
std::vector<uint8_t> vec;
fbl::AutoLock lock(&app_.radio_q_mtx_);
if (app_.radio_inbound_queue_.size() > 0) {
vec = std::move(app_.radio_inbound_queue_.front());
app_.radio_inbound_queue_.pop_front();
FX_LOGS(INFO) << "ot-stack-callbackform: radio-callback: check for frame: new frame";
}
return vec;
}
void OtStackApp::OtStackCallBackImpl::SendOneFrameToClient(uint8_t* buffer, uint32_t size) {
if (memcmp(buffer, kSpinelResetFrame, sizeof(kSpinelResetFrame)) == 0) {
// Reset frame
FX_LOGS(WARNING) << "ot-stack: reset frame received from ot-radio";
return;
}
app_.client_inbound_queue_.push_back(std::vector<uint8_t>(buffer, buffer + size));
app_.SendOneFrameToClient();
}
void OtStackApp::OtStackCallBackImpl::PostNcpFidlInboundTask() {
async::PostTask(app_.loop_.dispatcher(),
[]() { ot::Ncp::otNcpGetInstance()->HandleFrameAddedToNcpBuffer(); });
}
void OtStackApp::OtStackCallBackImpl::PostOtLibTaskletProcessTask() {
async::PostTask(app_.loop_.dispatcher(), [this]() {
otTaskletsProcess(static_cast<otInstance*>(this->app_.ot_instance_ptr_.value()));
});
}
void OtStackApp::OtStackCallBackImpl::PostDelayedAlarmTask(zx::duration delay) {
async::PostDelayedTask(
app_.loop_.dispatcher(), [this]() { this->app_.AlarmTask(); }, delay);
}
static void connect(void* untyped_context, const char* service_name, zx_handle_t service_request) {
auto app = static_cast<OtStackApp*>(untyped_context);
app->AddFidlRequestHandler(service_name, service_request);
}
void OtStackApp::AddFidlRequestHandler(const char* service_name, zx_handle_t service_request) {
if (binding_) { // TODO (jiamingw) add support for multiple clients
FX_LOGS(ERROR) << "FIDL connect request rejected: already bound";
return;
}
fidl_request_handler_ptr_->Bind(loop_.dispatcher(), service_name, service_request);
}
// Setup FIDL server side which handle requests from upper layer components.
zx_status_t OtStackApp::SetupFidlService() {
zx_handle_t directory_request = zx_take_startup_handle(PA_DIRECTORY_REQUEST);
if (directory_request == ZX_HANDLE_INVALID) {
FX_LOGS(ERROR) << "Got invalid directory_request channel";
return ZX_ERR_INTERNAL;
}
svc_dir_t* dir = nullptr;
zx_status_t status = svc_dir_create(loop_.dispatcher(), directory_request, &dir);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Error creating service directory";
return status;
}
fidl_request_handler_ptr_ = std::make_unique<LowpanSpinelDeviceFidlImpl>(*this);
status = svc_dir_add_service(dir, "svc", "fuchsia.lowpan.spinel.Device", this, connect);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Error adding service in ot-stack";
return status;
}
return ZX_OK;
}
void OtStackApp::SendOneFrameToClient() {
if (!binding_.has_value()) {
FX_LOGS(ERROR) << "ot-stack: Sending frame to client, but client is not connected";
assert(0);
}
if (client_inbound_queue_.size() > 0 && client_inbound_allowance_ > 0) {
::fidl::VectorView<uint8_t> data;
uint8_t* ptr = client_inbound_queue_.front().data();
data.set_data(fidl::unowned_ptr_t<uint8_t>(ptr));
data.set_count(client_inbound_queue_.front().size());
(*binding_)->OnReceiveFrame(std::move(data));
UpdateClientInboundAllowance();
client_inbound_queue_.pop_front();
if (client_inbound_queue_.size() > 0 && client_inbound_allowance_ > 0) {
async::PostTask(loop_.dispatcher(), [this]() { this->SendOneFrameToClient(); });
}
FX_LOGS(DEBUG) << "ot-stack: sent one frame to the client of ot-stack";
} else {
FX_LOGS(WARNING) << "ot-stack: unable to sent one frame to the client of ot-stack, q size:"
<< client_inbound_queue_.size()
<< " client_inbound_allowance_:" << client_inbound_allowance_;
}
}
zx_status_t OtStackApp::Init(const std::string& path, bool is_test_env) {
is_test_env_ = is_test_env;
device_path_ = path;
zx_status_t result = ConnectToOtRadioDev();
if (result != ZX_OK) {
return result;
}
RadioAllowanceInit();
lowpan_spinel_ptr_ = std::make_unique<OtStackCallBackImpl>(*this);
otPlatformConfig config;
config.callback_ptr = lowpan_spinel_ptr_.get();
config.m_speed_up_factor = 1;
ot_instance_ptr_ = static_cast<void*>(otSysInit(&config));
ot::Ncp::otNcpInit(static_cast<otInstance*>(ot_instance_ptr_.value()));
ot::Ncp::otNcpGetInstance()->Init(lowpan_spinel_ptr_.get());
auto status = SetupFidlService();
if (status != ZX_OK) {
return status;
}
// Init bootstrap fidl:
auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory();
bootstrap_impl_ = std::make_unique<ot::Fuchsia::BootstrapImpl>(context.get());
status = bootstrap_impl_->Init();
if (status != ZX_OK) {
FX_LOGS(ERROR) << "BootstrapImpl Init() failed with status = " << zx_status_get_string(status);
return status;
}
return ZX_OK;
}
void OtStackApp::AlarmTask() {
zx_time_t remaining;
platformAlarmUpdateTimeout(&remaining);
if (remaining == 0) {
FX_LOGS(DEBUG) << "ot-stack: calling platformAlarmProcess()";
platformAlarmProcess(static_cast<otInstance*>(ot_instance_ptr_.value()));
} else {
// If remaining is not 0, then the alarm is likely already being reset.
// do not need to do anything here
FX_LOGS(DEBUG) << "ot-stack: alarm process not called, remaining: " << remaining;
}
}
// Connect to ot-radio device driver which allows ot-stack to talk to lower layer
zx_status_t OtStackApp::ConnectToOtRadioDev() {
zx_status_t result = ZX_OK;
if (is_test_env_) {
result = SetDeviceSetupClientInIsolatedDevmgr(device_path_);
} else {
result = SetDeviceSetupClientInDevmgr(device_path_);
}
if (result != ZX_OK) {
FX_LOGS(ERROR) << "failed to set device setup client";
return result;
}
return SetupOtRadioDev();
}
// Get the spinel setup client from a file path. Set `client_ptr_` on success.
zx_status_t OtStackApp::SetDeviceSetupClientInDevmgr(const std::string& path) {
fbl::unique_fd fd(open(path.c_str(), O_RDWR));
if (!fd.is_valid()) {
FX_LOGS(ERROR) << "failed to connect to device\n";
return ZX_ERR_INTERNAL;
}
zx::channel chan;
zx_status_t status = fdio_get_service_handle(fd.release(), chan.reset_and_get_address());
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Fdio get handle failed";
return status;
}
device_setup_client_ptr_ =
std::make_unique<fidl_spinel::DeviceSetup::SyncClient>(std::move(chan));
return ZX_OK;
}
zx_status_t OtStackApp::ConnectServiceByName(const char name[], zx::channel* out) {
static zx_handle_t service_root;
{
static std::once_flag once;
static zx_status_t status;
std::call_once(once, [&]() {
zx::channel client_end, server_end;
status = zx::channel::create(0, &client_end, &server_end);
if (status != ZX_OK) {
return;
}
status = fdio_service_connect("/svc/.", server_end.release());
if (status != ZX_OK) {
return;
}
service_root = client_end.release();
});
if (status != ZX_OK) {
return status;
}
}
zx::channel client_end, server_end;
zx_status_t status = zx::channel::create(0, &client_end, &server_end);
if (status != ZX_OK) {
return status;
}
status = fdio_service_connect_at(service_root, name, server_end.release());
if (status != ZX_OK) {
return status;
}
*out = std::move(client_end);
return ZX_OK;
}
// Get the spinel setup client from a file path. Set `client_ptr_` on success.
zx_status_t OtStackApp::SetDeviceSetupClientInIsolatedDevmgr(const std::string& path) {
zx_status_t res = ZX_OK;
res = ConnectServiceByName("fuchsia.openthread.devmgr.IsolatedDevmgr", &isolated_devfs_);
if (res != ZX_OK) {
FX_LOGS(ERROR) << "fdio_service_connect_by_name failed";
return res;
}
zx::channel fake_device_server_side, fake_device_client_side;
res = zx::channel::create(0, &fake_device_server_side, &fake_device_client_side);
if (res != ZX_OK) {
FX_LOGS(ERROR) << "zx::channel::create failed";
return res;
}
res = fdio_service_connect_at(isolated_devfs_.get(), path.c_str(),
fake_device_server_side.release());
if (res != ZX_OK) {
FX_LOGS(ERROR) << "fdio_service_connect_at failed";
return res;
}
device_setup_client_ptr_ =
std::make_unique<fidl_spinel::DeviceSetup::SyncClient>(std::move(fake_device_client_side));
return ZX_OK;
}
zx_status_t OtStackApp::SetupOtRadioDev() {
if (device_setup_client_ptr_.get() == nullptr) {
return ZX_ERR_BAD_STATE;
}
zx::channel server_end;
zx::channel client_end;
zx_status_t status = zx::channel::create(0, &client_end, &server_end);
if (status != ZX_OK) {
return status;
}
auto fidl_result = device_setup_client_ptr_->SetChannel(std::move(server_end));
if (fidl_result.status() != ZX_OK) {
FX_LOGS(ERROR) << "Cannot set the channel to device";
return fidl_result.status();
}
auto* result = fidl_result.Unwrap();
if (result->result.is_err()) {
return ZX_ERR_INTERNAL;
}
FX_LOGS(INFO) << "successfully connected to driver";
event_thread_ = std::thread(
[](void* cookie) { return reinterpret_cast<OtStackApp*>(cookie)->EventThread(); }, this);
status = zx::port::create(0, &port_);
if (status != ZX_OK) {
return status;
}
device_channel_ = zx::unowned_channel(client_end);
device_client_ptr_ = std::make_unique<fidl_spinel::Device::SyncClient>(std::move(client_end));
connected_to_device_ = true;
status = zx_object_wait_async(device_channel_->get(), port_.get(), kPortRadioChannelRead,
ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, 0);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "failed to wait for events";
}
return status;
}
void OtStackApp::EventLoopHandleInboundFrame(::fidl::VectorView<uint8_t> data) {
fbl::AutoLock lock(&radio_q_mtx_);
radio_inbound_queue_.push_back(std::vector<uint8_t>(data.cbegin(), data.cend()));
sync_completion_signal(&radio_rx_complete_);
async::PostTask(loop_.dispatcher(), [this]() {
platformRadioProcess(static_cast<otInstance*>(this->ot_instance_ptr_.value()));
});
FX_LOGS(INFO) << "signaled ot-stack-callbackform";
}
void OtStackApp::OnReadyForSendFrames(fidl_spinel::Device::OnReadyForSendFramesResponse* event) {
HandleRadioOnReadyForSendFrame(event->number_of_frames);
}
void OtStackApp::OnReceiveFrame(fidl_spinel::Device::OnReceiveFrameResponse* event) {
EventLoopHandleInboundFrame(std::move(event->data));
UpdateRadioInboundAllowance();
}
void OtStackApp::OnError(fidl_spinel::Device::OnErrorResponse* event) {
handler_status_ = (*binding_)->OnError(event->error, event->did_close);
}
zx_status_t OtStackApp::Unknown() {
(*binding_)->OnError(fidl_spinel::Error::IO_ERROR, true);
DisconnectDevice();
return ZX_ERR_IO;
}
void OtStackApp::EventThread() {
while (true) {
zx_port_packet_t packet = {};
port_.wait(zx::time::infinite(), &packet);
switch (packet.key) {
case kPortRadioChannelRead: {
if (packet.signal.observed & ZX_CHANNEL_PEER_CLOSED) {
FX_LOGS(ERROR) << "ot-radio channel closed, terminating event thread";
return;
}
::fidl::Result result = HandleOneEvent(zx::unowned_channel(device_client_ptr_->channel()));
if (!result.ok() || (handler_status_ != ZX_OK)) {
FX_PLOGS(ERROR, result.ok() ? handler_status_ : result.status())
<< "error calling fidl_spinel::Device::SyncClient::HandleEvents(), terminating event "
"thread";
DisconnectDevice();
loop_.Shutdown();
return;
}
zx_status_t status =
zx_object_wait_async(device_channel_->get(), port_.get(), kPortRadioChannelRead,
ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, 0);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "failed to wait for events, terminating event thread";
return;
}
} break;
case kPortTerminate:
FX_LOGS(INFO) << "terminating event thread";
return;
}
}
}
void OtStackApp::TerminateEventThread() {
zx_port_packet packet = {kPortTerminate, ZX_PKT_TYPE_USER, ZX_OK, {}};
port_.queue(&packet);
event_thread_.join();
}
void OtStackApp::DisconnectDevice() {
device_channel_ = zx::unowned_channel(ZX_HANDLE_INVALID);
device_client_ptr_.release();
device_setup_client_ptr_.release();
connected_to_device_ = false;
}
void OtStackApp::Shutdown() {
FX_LOGS(ERROR) << "terminating message loop in ot-stack";
if (binding_) {
binding_->Close(ZX_ERR_INTERNAL);
}
TerminateEventThread();
DisconnectDevice();
loop_.Quit();
}
} // namespace otstack