| // 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 "fake-ot-radio.h" |
| |
| #include <lib/async-loop/default.h> |
| #include <lib/async/cpp/task.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <sys/types.h> |
| #include <zircon/compiler.h> |
| #include <zircon/status.h> |
| |
| #include <ddk/debug.h> |
| #include <ddk/driver.h> |
| #include <ddk/metadata.h> |
| #include <ddktl/fidl.h> |
| #include <fbl/algorithm.h> |
| #include <fbl/auto_call.h> |
| #include <fbl/auto_lock.h> |
| #include <fbl/ref_counted.h> |
| #include <fbl/ref_ptr.h> |
| |
| #include "src/connectivity/openthread/tests/fake-drivers/fake-ot-radio/fake_ot_radio_bind.h" |
| |
| namespace fake_ot { |
| namespace lowpan_spinel_fidl = ::llcpp::fuchsia::lowpan::spinel; |
| |
| enum { |
| PORT_KEY_EXIT_THREAD, |
| PORT_KEY_INBOUND_FRAME, |
| PORT_KEY_INBOUND_ALLOWANCE, |
| }; |
| |
| constexpr uint8_t kNcpResetEvent[] = {0x80, 0x06, 0x0, 0x70}; |
| constexpr uint8_t kCmdLoc = 0x01; |
| constexpr uint8_t kNcpSoftResetRequest = 0x01; |
| constexpr uint8_t kPropValueGet = 0x02; |
| constexpr uint8_t kPropValueSet = 0x03; |
| constexpr uint8_t kPropValueIs = 0x06; |
| constexpr uint8_t kNcpVer = 0x02; |
| constexpr uint8_t kProtocolVer = 0x01; |
| constexpr uint8_t kPropCaps = 0x5; |
| constexpr uint8_t kPropHwAddr = 0x8; |
| constexpr uint8_t kPhyRxSensitivity = 0x27; |
| constexpr uint8_t kPropGetRadioCap[] = {0x8b, 0x24}; |
| |
| constexpr uint8_t kNcpVerReply[] = { |
| 0x80, 0x06, 0x02, 0x4F, 0x50, 0x45, 0x4E, 0x54, 0x48, 0x52, 0x45, 0x41, 0x44, 0x2F, 0x31, |
| 0x2E, 0x30, 0x64, 0x37, 0x32, 0x35, 0x3B, 0x20, 0x52, 0x43, 0x50, 0x2D, 0x4E, 0x65, 0x77, |
| 0x6D, 0x61, 0x6E, 0x31, 0x3B, 0x20, 0x46, 0x65, 0x62, 0x20, 0x32, 0x34, 0x20, 0x32, 0x30, |
| 0x31, 0x39, 0x20, 0x31, 0x33, 0x3A, 0x33, 0x38, 0x3A, 0x32, 0x32, 0x00}; |
| constexpr uint8_t kProtocolVerReply[] = {0x80, 0x6, 0x1, 0x4, 0x3}; |
| constexpr uint8_t kPropCapsReply[] = {0x80, 0x6, 0x5, 0x5, 0xc, 0xd, 0x18, 0x22, 0x81, 0x4}; |
| constexpr uint8_t kPropHwAddrReply[] = {0x80, 0x6, 0x8, 0x64, 0x16, 0x66, |
| 0x0, 0x47, 0x34, 0xaf, 0x1a}; |
| constexpr uint8_t kPhyRxSensitivityReply[] = {0x80, 0x6, 0x27, 0x9c}; |
| constexpr uint8_t kPropGetRadioCapReply[] = {0x80, 0x6, 0x8b, 0x24, 0xd}; |
| |
| constexpr uint8_t kSpinelFrameHeader = 0x80; |
| constexpr uint8_t kSpinelHeaderInvalid = 0xFF; |
| |
| FakeOtRadioDevice::LowpanSpinelDeviceFidlImpl::LowpanSpinelDeviceFidlImpl( |
| FakeOtRadioDevice& ot_radio) |
| : ot_radio_obj_(ot_radio) {} |
| |
| zx_status_t FakeOtRadioDevice::LowpanSpinelDeviceFidlImpl::Bind(async_dispatcher_t* dispatcher, |
| zx::channel channel) { |
| fidl::OnUnboundFn<LowpanSpinelDeviceFidlImpl> on_unbound = [](LowpanSpinelDeviceFidlImpl* server, |
| fidl::UnbindInfo, zx::channel) { |
| server->ot_radio_obj_.fidl_impl_obj_.release(); |
| }; |
| auto res = fidl::BindServer(dispatcher, std::move(channel), this, std::move(on_unbound)); |
| if (res.is_error()) |
| return res.error(); |
| ot_radio_obj_.fidl_binding_ = res.take_value(); |
| return ZX_OK; |
| } |
| |
| void FakeOtRadioDevice::LowpanSpinelDeviceFidlImpl::Open(OpenCompleter::Sync& completer) { |
| zx_status_t res = ot_radio_obj_.Reset(); |
| if (res == ZX_OK) { |
| zxlogf(DEBUG, "open succeed, returning"); |
| ot_radio_obj_.power_status_ = OT_SPINEL_DEVICE_ON; |
| (*ot_radio_obj_.fidl_binding_)->OnReadyForSendFrames(kOutboundAllowanceInit); |
| ot_radio_obj_.inbound_allowance_ = 0; |
| ot_radio_obj_.outbound_allowance_ = kOutboundAllowanceInit; |
| ot_radio_obj_.inbound_cnt_ = 0; |
| ot_radio_obj_.outbound_cnt_ = 0; |
| completer.ReplySuccess(); |
| } else { |
| zxlogf(ERROR, "Error in handling FIDL close req: %s, power status: %u", |
| zx_status_get_string(res), ot_radio_obj_.power_status_); |
| completer.ReplyError(lowpan_spinel_fidl::Error::UNSPECIFIED); |
| } |
| } |
| |
| void FakeOtRadioDevice::LowpanSpinelDeviceFidlImpl::Close(CloseCompleter::Sync& completer) { |
| zx_status_t res = ot_radio_obj_.Reset(); |
| if (res == ZX_OK) { |
| ot_radio_obj_.power_status_ = OT_SPINEL_DEVICE_OFF; |
| completer.ReplySuccess(); |
| } else { |
| zxlogf(ERROR, "Error in handling FIDL close req: %s, power status: %u", |
| zx_status_get_string(res), ot_radio_obj_.power_status_); |
| completer.ReplyError(lowpan_spinel_fidl::Error::UNSPECIFIED); |
| } |
| } |
| |
| void FakeOtRadioDevice::LowpanSpinelDeviceFidlImpl::GetMaxFrameSize( |
| GetMaxFrameSizeCompleter::Sync& completer) { |
| completer.Reply(kMaxFrameSize); |
| } |
| |
| void FakeOtRadioDevice::LowpanSpinelDeviceFidlImpl::SendFrame(::fidl::VectorView<uint8_t> data, |
| SendFrameCompleter::Sync& completer) { |
| if (ot_radio_obj_.power_status_ == OT_SPINEL_DEVICE_OFF) { |
| (*ot_radio_obj_.fidl_binding_)->OnError(lowpan_spinel_fidl::Error::CLOSED, false); |
| } else if (data.count() > kMaxFrameSize) { |
| (*ot_radio_obj_.fidl_binding_) |
| ->OnError(lowpan_spinel_fidl::Error::OUTBOUND_FRAME_TOO_LARGE, false); |
| } else if (ot_radio_obj_.outbound_allowance_ == 0) { |
| // Client violates the protocol, close FIDL channel and device. Will not send OnError event. |
| ot_radio_obj_.power_status_ = OT_SPINEL_DEVICE_OFF; |
| ot_radio_obj_.Reset(); |
| ot_radio_obj_.fidl_binding_->Close(ZX_ERR_IO_OVERRUN); |
| completer.Close(ZX_ERR_IO_OVERRUN); |
| } else { |
| // Send out the frame. |
| fbl::AutoLock lock(&ot_radio_obj_.outbound_lock_); |
| ot_radio_obj_.outbound_queue_.push(std::move(data)); |
| lock.release(); |
| async::PostTask(ot_radio_obj_.loop_.dispatcher(), |
| [this]() { this->ot_radio_obj_.TryHandleOutboundFrame(); }); |
| |
| ot_radio_obj_.outbound_allowance_--; |
| ot_radio_obj_.outbound_cnt_++; |
| |
| if ((ot_radio_obj_.outbound_cnt_ & 1) == 0) { |
| (*ot_radio_obj_.fidl_binding_)->OnReadyForSendFrames(kOutboundAllowanceInc); |
| ot_radio_obj_.outbound_allowance_ += kOutboundAllowanceInc; |
| } |
| } |
| } |
| |
| void FakeOtRadioDevice::LowpanSpinelDeviceFidlImpl::ReadyToReceiveFrames( |
| uint32_t number_of_frames, ReadyToReceiveFramesCompleter::Sync& completer) { |
| zxlogf(DEBUG, "allow to receive %u frame", number_of_frames); |
| bool prev_no_inbound_allowance = (ot_radio_obj_.inbound_allowance_ == 0) ? true : false; |
| ot_radio_obj_.inbound_allowance_ += number_of_frames; |
| |
| if (prev_no_inbound_allowance && ot_radio_obj_.inbound_allowance_ > 0) { |
| zx_port_packet packet = {PORT_KEY_INBOUND_ALLOWANCE, ZX_PKT_TYPE_USER, ZX_OK, {}}; |
| ot_radio_obj_.port_.queue(&packet); |
| } |
| } |
| |
| FakeOtRadioDevice::FakeOtRadioDevice(zx_device_t* device) |
| : ddk::Device<FakeOtRadioDevice, ddk::Unbindable, ddk::Messageable>(device), |
| loop_(&kAsyncLoopConfigNoAttachToCurrentThread) {} |
| |
| zx_status_t FakeOtRadioDevice::DdkMessage(fidl_incoming_msg_t* msg, fidl_txn_t* txn) { |
| DdkTransaction transaction(txn); |
| lowpan_spinel_fidl::DeviceSetup::Dispatch(this, msg, &transaction); |
| return transaction.Status(); |
| } |
| |
| void FakeOtRadioDevice::SetChannel(zx::channel channel, SetChannelCompleter::Sync& completer) { |
| if (fidl_impl_obj_ != nullptr) { |
| zxlogf(ERROR, "ot-audio: channel already set"); |
| completer.ReplyError(ZX_ERR_ALREADY_BOUND); |
| return; |
| } |
| if (!channel.is_valid()) { |
| completer.ReplyError(ZX_ERR_BAD_HANDLE); |
| return; |
| } |
| fidl_impl_obj_ = std::make_unique<LowpanSpinelDeviceFidlImpl>(*this); |
| auto status = fidl_impl_obj_->Bind(loop_.dispatcher(), std::move(channel)); |
| if (status == ZX_OK) { |
| completer.ReplySuccess(); |
| } else { |
| fidl_impl_obj_ = nullptr; |
| completer.ReplyError(status); |
| } |
| } |
| |
| zx_status_t FakeOtRadioDevice::StartLoopThread() { |
| zxlogf(DEBUG, "Start loop thread"); |
| return loop_.StartThread("ot-stack-loop"); |
| } |
| |
| zx_status_t FakeOtRadioDevice::Reset() { |
| zx_status_t status = ZX_OK; |
| zxlogf(INFO, "fake-ot-radio: reset"); |
| |
| fbl::AutoLock lock_in(&inbound_lock_); |
| std::queue<std::vector<uint8_t>> empty_inbound_queue; |
| std::swap(inbound_queue_, empty_inbound_queue); |
| lock_in.release(); |
| |
| fbl::AutoLock lock_out(&outbound_lock_); |
| std::queue<::fidl::VectorView<uint8_t>> empty_outbound_queue; |
| std::swap(outbound_queue_, empty_outbound_queue); |
| lock_out.release(); |
| |
| zx::nanosleep(zx::deadline_after(zx::msec(kResetMsDelay))); |
| |
| std::vector<uint8_t> event; |
| event.assign(std::begin(kNcpResetEvent), std::end(kNcpResetEvent)); |
| PostSendInboundFrameTask(std::move(event)); |
| |
| return status; |
| } |
| |
| void FakeOtRadioDevice::TryHandleOutboundFrame() { |
| fbl::AutoLock lock(&outbound_lock_); |
| if (outbound_queue_.size() > 0) { |
| zxlogf(DEBUG, "fake-ot-stack: TryHandleOutboundFrame() outbound_queue_.size():%lu", |
| outbound_queue_.size()); |
| FrameHandler(std::move(outbound_queue_.front())); |
| outbound_queue_.pop(); |
| } |
| } |
| |
| uint8_t FakeOtRadioDevice::ValidateSpinelHeaderAndGetTid(const uint8_t* data, uint32_t len) { |
| if ((len == 0) || ((data[0] & kBitMaskHigherFourBits) != kSpinelFrameHeader)) { |
| return false; |
| } |
| |
| return data[0] & kBitMaskLowerFourBits; |
| } |
| |
| void FakeOtRadioDevice::FrameHandler(::fidl::VectorView<uint8_t> data) { |
| if (power_status_ != OT_SPINEL_DEVICE_ON) { |
| zxlogf(ERROR, "fake-ot-radio: failed to handle frame due to device off"); |
| return; |
| } |
| |
| uint8_t tid = ValidateSpinelHeaderAndGetTid(data.data(), data.count()); |
| if (tid == kSpinelHeaderInvalid) { |
| return; |
| } |
| |
| if (data.data()[kCmdLoc] == kNcpSoftResetRequest) { |
| async::PostTask(loop_.dispatcher(), [this]() { this->Reset(); }); |
| } else if (data.data()[kCmdLoc] == kPropValueGet) { |
| // Handle prop value get |
| std::vector<uint8_t> reply; |
| switch (data.data()[kCmdLoc + 1]) { |
| case kNcpVer: |
| reply.assign(std::begin(kNcpVerReply), std::end(kNcpVerReply)); |
| break; |
| case kProtocolVer: |
| reply.assign(std::begin(kProtocolVerReply), std::end(kProtocolVerReply)); |
| break; |
| case kPropCaps: |
| reply.assign(std::begin(kPropCapsReply), std::end(kPropCapsReply)); |
| break; |
| case kPropHwAddr: |
| reply.assign(std::begin(kPropHwAddrReply), std::end(kPropHwAddrReply)); |
| break; |
| case kPhyRxSensitivity: |
| reply.assign(std::begin(kPhyRxSensitivityReply), std::end(kPhyRxSensitivityReply)); |
| break; |
| default: |
| if (memcmp(kPropGetRadioCap, &data.data()[kCmdLoc + 1], sizeof(kPropGetRadioCap)) == 0) { |
| reply.assign(std::begin(kPropGetRadioCapReply), std::end(kPropGetRadioCapReply)); |
| } else { |
| zxlogf(ERROR, "fake-ot-radio: not supported prop value get cmd"); |
| } |
| break; |
| } |
| reply.data()[0] |= tid; |
| PostSendInboundFrameTask(std::move(reply)); |
| } else if (data.data()[kCmdLoc] == kPropValueSet) { |
| // Handle prop value set |
| // now we just reply what is being set |
| // TODO (jiamingw): make rcp stateful |
| std::vector<uint8_t> reply; |
| reply.assign(data.cbegin(), data.cend()); |
| reply.data()[1] = kPropValueIs; |
| PostSendInboundFrameTask(std::move(reply)); |
| } else { |
| // TODO (jiamingw): Send back response for invalid request. |
| zxlogf(ERROR, "fake-ot-radio: received invalid spinel frame"); |
| } |
| } |
| |
| uint32_t FakeOtRadioDevice::GetTimeoutMs() { |
| int timeout_ms = kLoopTimeOutMsOneDay; |
| |
| fbl::AutoLock lock(&inbound_lock_); |
| if (inbound_queue_.size() > 0 && inbound_allowance_ > 0) { |
| timeout_ms = 0; |
| } |
| |
| return timeout_ms; |
| } |
| |
| void FakeOtRadioDevice::PostSendInboundFrameTask(std::vector<uint8_t> frame) { |
| fbl::AutoLock lock(&inbound_lock_); |
| inbound_queue_.push(std::move(frame)); |
| zx_port_packet packet = {PORT_KEY_INBOUND_FRAME, ZX_PKT_TYPE_USER, ZX_OK, {}}; |
| port_.queue(&packet); |
| } |
| |
| zx_status_t FakeOtRadioDevice::TrySendInboundFrame() { |
| fbl::AutoLock lock(&inbound_lock_); |
| if (power_status_ == OT_SPINEL_DEVICE_ON && inbound_allowance_ > 0 && inbound_queue_.size() > 0) { |
| // send out 1 packet |
| auto spinel_frame = inbound_queue_.front(); |
| ::fidl::VectorView<uint8_t> data; |
| data.set_count(spinel_frame.size()); |
| data.set_data(fidl::unowned_ptr(spinel_frame.data())); |
| zx_status_t res = (*fidl_binding_)->OnReceiveFrame(std::move(data)); |
| if (res != ZX_OK) { |
| zxlogf(ERROR, "fake-ot-radio: failed to send OnReceive() event due to %s", |
| zx_status_get_string(res)); |
| return res; |
| } |
| |
| inbound_allowance_--; |
| inbound_cnt_++; |
| inbound_queue_.pop(); |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t FakeOtRadioDevice::RadioThread() { |
| zx_status_t status = ZX_OK; |
| zxlogf(INFO, "fake-ot-radio: entered thread"); |
| |
| while (true) { |
| zx_port_packet_t packet = {}; |
| int timeout_ms = GetTimeoutMs(); |
| status = port_.wait(zx::deadline_after(zx::msec(timeout_ms)), &packet); |
| |
| if (status == ZX_ERR_TIMED_OUT) { |
| zx_status_t send_status = TrySendInboundFrame(); |
| if (send_status != ZX_OK) { |
| power_status_ = OT_SPINEL_DEVICE_OFF; |
| } |
| continue; |
| } else if (status != ZX_OK) { |
| zxlogf(ERROR, "fake-ot-radio: port wait failed: %d", status); |
| return thrd_error; |
| } |
| |
| if (packet.key == PORT_KEY_INBOUND_FRAME || packet.key == PORT_KEY_INBOUND_ALLOWANCE) { |
| zx_status_t send_status = TrySendInboundFrame(); |
| if (send_status != ZX_OK) { |
| power_status_ = OT_SPINEL_DEVICE_OFF; |
| } |
| } else if (packet.key == PORT_KEY_EXIT_THREAD) { |
| break; |
| } |
| } |
| zxlogf(DEBUG, "fake-ot-radio: exiting"); |
| |
| return status; |
| } |
| |
| zx_status_t FakeOtRadioDevice::CreateBindAndStart(void* ctx, zx_device_t* parent) { |
| std::unique_ptr<FakeOtRadioDevice> ot_radio_dev; |
| zx_status_t status = Create(ctx, parent, &ot_radio_dev); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| status = ot_radio_dev->Bind(); |
| if (status != ZX_OK) { |
| return status; |
| } |
| // device intentionally leaked as it is now held by DevMgr |
| auto dev_ptr = ot_radio_dev.release(); |
| |
| status = dev_ptr->Start(); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| return status; |
| } |
| |
| zx_status_t FakeOtRadioDevice::Create(void* ctx, zx_device_t* parent, |
| std::unique_ptr<FakeOtRadioDevice>* out) { |
| auto dev = std::make_unique<FakeOtRadioDevice>(parent); |
| |
| *out = std::move(dev); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t FakeOtRadioDevice::Bind() { |
| zx_status_t status = |
| DdkAdd(ddk::DeviceAddArgs("fake-ot-radio").set_proto_id(ZX_PROTOCOL_OT_RADIO)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "fake-ot-radio: Could not create device: %d", status); |
| return status; |
| } else { |
| zxlogf(DEBUG, "fake-ot-radio: Added device"); |
| } |
| return status; |
| } |
| |
| zx_status_t FakeOtRadioDevice::Start() { |
| zx_status_t status = zx::port::create(0, &port_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "fake-ot-radio: port create failed %d", status); |
| return status; |
| } |
| |
| auto cleanup = fbl::MakeAutoCall([&]() { ShutDown(); }); |
| |
| auto callback = [](void* cookie) { |
| return reinterpret_cast<FakeOtRadioDevice*>(cookie)->RadioThread(); |
| }; |
| event_loop_thread_ = std::thread(callback, this); |
| |
| status = StartLoopThread(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "fake-ot-radio: Could not start loop thread"); |
| return status; |
| } |
| |
| zxlogf(DEBUG, "fake-ot-radio: Started thread"); |
| |
| cleanup.cancel(); |
| |
| return status; |
| } |
| |
| void FakeOtRadioDevice::DdkRelease() { delete this; } |
| |
| void FakeOtRadioDevice::DdkUnbind(ddk::UnbindTxn txn) { |
| ShutDown(); |
| txn.Reply(); |
| } |
| |
| zx_status_t FakeOtRadioDevice::ShutDown() { |
| zx_port_packet packet = {PORT_KEY_EXIT_THREAD, ZX_PKT_TYPE_USER, ZX_OK, {}}; |
| port_.queue(&packet); |
| event_loop_thread_.join(); |
| loop_.Shutdown(); |
| return ZX_OK; |
| } |
| |
| static constexpr zx_driver_ops_t device_ops = []() { |
| zx_driver_ops_t ops = {}; |
| ops.version = DRIVER_OPS_VERSION; |
| ops.bind = FakeOtRadioDevice::CreateBindAndStart; |
| return ops; |
| }(); |
| |
| } // namespace fake_ot |
| |
| ZIRCON_DRIVER(fake_ot, fake_ot::device_ops, "zircon", "0.1"); |