/* * Copyright (c) 2019, The OpenThread Authors. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /** * @file * This file implements a simple CLI for the Commissioner role. */ #include "cli_commissioner.hpp" #include "cli/cli.hpp" #if OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD namespace ot { namespace Cli { /** * @cli commissioner announce * @code * commissioner announce 0x00050000 2 32 fdde:ad00:beef:0:0:ff:fe00:c00 * Done * @endcode * @cparam commissioner announce @ca{mask} @ca{count} @ca{period} @ca{destination} * * `mask`: Bitmask that identifies channels for sending MLE `Announce` messages. * * `count`: Number of MLE `Announce` transmissions per channel. * * `period`: Number of milliseconds between successive MLE `Announce` transmissions. * * `destination`: Destination IPv6 address for the message. The message may be multicast. * @par * Sends an Announce Begin message. * @note Use this command only after successfully starting the %Commissioner role * with the `commissioner start` command. * @csa{commissioner start} * @sa otCommissionerAnnounceBegin */ template <> otError Commissioner::Process(Arg aArgs[]) { otError error; uint32_t mask; uint8_t count; uint16_t period; otIp6Address address; SuccessOrExit(error = aArgs[0].ParseAsUint32(mask)); SuccessOrExit(error = aArgs[1].ParseAsUint8(count)); SuccessOrExit(error = aArgs[2].ParseAsUint16(period)); SuccessOrExit(error = aArgs[3].ParseAsIp6Address(address)); error = otCommissionerAnnounceBegin(GetInstancePtr(), mask, count, period, &address); exit: return error; } /** * @cli commissioner energy * @code * commissioner energy 0x00050000 2 32 1000 fdde:ad00:beef:0:0:ff:fe00:c00 * Done * Energy: 00050000 0 0 0 0 * @endcode * @cparam commissioner energy @ca{mask} @ca{count} @ca{period} @ca{scanDuration} @ca{destination} * * `mask`: Bitmask that identifies channels for performing IEEE 802.15.4 energy scans. * * `count`: Number of IEEE 802.15.4 energy scans per channel. * * `period`: Number of milliseconds between successive IEEE 802.15.4 energy scans. * * `scanDuration`: Scan duration in milliseconds to use when * performing an IEEE 802.15.4 energy scan. * * `destination`: Destination IPv6 address for the message. The message may be multicast. * @par * Sends an Energy Scan Query message. Command output is printed as it is received. * @note Use this command only after successfully starting the %Commissioner role * with the `commissioner start` command. * @csa{commissioner start} * @sa otCommissionerEnergyScan */ template <> otError Commissioner::Process(Arg aArgs[]) { otError error; uint32_t mask; uint8_t count; uint16_t period; uint16_t scanDuration; otIp6Address address; SuccessOrExit(error = aArgs[0].ParseAsUint32(mask)); SuccessOrExit(error = aArgs[1].ParseAsUint8(count)); SuccessOrExit(error = aArgs[2].ParseAsUint16(period)); SuccessOrExit(error = aArgs[3].ParseAsUint16(scanDuration)); SuccessOrExit(error = aArgs[4].ParseAsIp6Address(address)); error = otCommissionerEnergyScan(GetInstancePtr(), mask, count, period, scanDuration, &address, &Commissioner::HandleEnergyReport, this); exit: return error; } template <> otError Commissioner::Process(Arg aArgs[]) { otError error = OT_ERROR_NONE; otExtAddress addr; const otExtAddress *addrPtr = nullptr; otJoinerDiscerner discerner; /** * @cli commissioner joiner table * @code * commissioner joiner table * | ID | PSKd | Expiration | * +-----------------------+----------------------------------+------------+ * | * | J01NME | 81015 | * | d45e64fa83f81cf7 | J01NME | 101204 | * | 0x0000000000000abc/12 | J01NME | 114360 | * Done * @endcode * @par * Lists all %Joiner entries in table format. */ if (aArgs[0] == "table") { uint16_t iter = 0; otJoinerInfo joinerInfo; static const char *const kJoinerTableTitles[] = {"ID", "PSKd", "Expiration"}; static const uint8_t kJoinerTableColumnWidths[] = { 23, 34, 12, }; OutputTableHeader(kJoinerTableTitles, kJoinerTableColumnWidths); while (otCommissionerGetNextJoinerInfo(GetInstancePtr(), &iter, &joinerInfo) == OT_ERROR_NONE) { switch (joinerInfo.mType) { case OT_JOINER_INFO_TYPE_ANY: OutputFormat("| %21s", "*"); break; case OT_JOINER_INFO_TYPE_EUI64: OutputFormat("| "); OutputExtAddress(joinerInfo.mSharedId.mEui64); break; case OT_JOINER_INFO_TYPE_DISCERNER: OutputFormat("| 0x%08lx%08lx/%2u", static_cast(joinerInfo.mSharedId.mDiscerner.mValue >> 32), static_cast(joinerInfo.mSharedId.mDiscerner.mValue & 0xffffffff), joinerInfo.mSharedId.mDiscerner.mLength); break; } OutputFormat(" | %32s | %10lu |", joinerInfo.mPskd.m8, ToUlong(joinerInfo.mExpirationTime)); OutputNewLine(); } ExitNow(error = OT_ERROR_NONE); } VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS); ClearAllBytes(discerner); if (aArgs[1] == "*") { // Intentionally empty } else { error = ParseJoinerDiscerner(aArgs[1], discerner); if (error == OT_ERROR_NOT_FOUND) { error = aArgs[1].ParseAsHexString(addr.m8); addrPtr = &addr; } SuccessOrExit(error); } /** * @cli commissioner joiner add * @code * commissioner joiner add d45e64fa83f81cf7 J01NME * Done * @endcode * @code * commissioner joiner add 0xabc/12 J01NME * Done * @endcode * @cparam commissioner joiner add @ca{eui64}|@ca{discerner pksd} [@ca{timeout}] * * `eui64`: IEEE EUI-64 of the %Joiner. To match any joiner, use `*`. * * `discerner`: The %Joiner discerner in the format `number/length`. * * `pksd`: Pre-Shared Key for the joiner. * * `timeout`: The %Joiner timeout in seconds. * @par * Adds a joiner entry. * @note Use this command only after successfully starting the %Commissioner role * with the `commissioner start` command. * @csa{commissioner start} * @sa otCommissionerAddJoiner * @sa otCommissionerAddJoinerWithDiscerner */ if (aArgs[0] == "add") { uint32_t timeout = kDefaultJoinerTimeout; VerifyOrExit(!aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS); if (!aArgs[3].IsEmpty()) { SuccessOrExit(error = aArgs[3].ParseAsUint32(timeout)); } if (discerner.mLength) { error = otCommissionerAddJoinerWithDiscerner(GetInstancePtr(), &discerner, aArgs[2].GetCString(), timeout); } else { error = otCommissionerAddJoiner(GetInstancePtr(), addrPtr, aArgs[2].GetCString(), timeout); } /** * @cli commissioner joiner remove * @code * commissioner joiner remove d45e64fa83f81cf7 * Done * @endcode * @code * commissioner joiner remove 0xabc/12 * Done * @endcode * @cparam commissioner joiner remove @ca{eui64}|@ca{discerner} * * `eui64`: IEEE EUI-64 of the joiner. To match any joiner, use `*`. * * `discerner`: The joiner discerner in the format `number/length`. * @par * Removes a %Joiner entry. * @note Use this command only after successfully starting the %Commissioner role * with the `commissioner start` command. * @csa{commissioner start} * @sa otCommissionerRemoveJoiner * @sa otCommissionerRemoveJoinerWithDiscerner */ } else if (aArgs[0] == "remove") { if (discerner.mLength) { error = otCommissionerRemoveJoinerWithDiscerner(GetInstancePtr(), &discerner); } else { error = otCommissionerRemoveJoiner(GetInstancePtr(), addrPtr); } } else { error = OT_ERROR_INVALID_ARGS; } exit: return error; } /** * @cli commissioner mgmtget * @code * commissioner mgmtget locator sessionid * Done * @endcode * @cparam commissioner mgmtget [locator] [sessionid] [steeringdata] [joinerudpport] [-x @ca{TLVs}] * * `locator`: Border Router RLOC16. * * `sessionid`: Session ID of the %Commissioner. * * `steeringdata`: Steering data. * * `joinerudpport`: %Joiner UDP port. * * `TLVs`: The set of TLVs to be retrieved. * @par * Sends a `MGMT_GET` (Management Get) message to the Leader. * Variable values that have been set using the `commissioner mgmtset` command are returned. * @sa otCommissionerSendMgmtGet */ template <> otError Commissioner::Process(Arg aArgs[]) { otError error = OT_ERROR_NONE; uint8_t tlvs[32]; uint8_t length = 0; for (; !aArgs->IsEmpty(); aArgs++) { VerifyOrExit(static_cast(length) < sizeof(tlvs), error = OT_ERROR_NO_BUFS); if (*aArgs == "locator") { tlvs[length++] = OT_MESHCOP_TLV_BORDER_AGENT_RLOC; } else if (*aArgs == "sessionid") { tlvs[length++] = OT_MESHCOP_TLV_COMM_SESSION_ID; } else if (*aArgs == "steeringdata") { tlvs[length++] = OT_MESHCOP_TLV_STEERING_DATA; } else if (*aArgs == "joinerudpport") { tlvs[length++] = OT_MESHCOP_TLV_JOINER_UDP_PORT; } else if (*aArgs == "-x") { uint16_t readLength; aArgs++; readLength = static_cast(sizeof(tlvs) - length); SuccessOrExit(error = aArgs->ParseAsHexString(readLength, tlvs + length)); length += static_cast(readLength); } else { ExitNow(error = OT_ERROR_INVALID_ARGS); } } error = otCommissionerSendMgmtGet(GetInstancePtr(), tlvs, static_cast(length)); exit: return error; } /** * @cli commissioner mgmtset * @code * commissioner mgmtset joinerudpport 9988 * Done * @endcode * @cparam commissioner mgmtset [locator @ca{locator}] [sessionid @ca{sessionid}] [steeringdata @ca{steeringdata}] [joinerudpport @ca{joinerudpport}] [-x @ca{TLVs}] * * `locator`: Border Router RLOC16. * * `sessionid`: Session ID of the %Commissioner. * * `steeringdata`: Steering data. * * `joinerudpport`: %Joiner UDP port. * * `TLVs`: The set of TLVs to be retrieved. * @par * Sends a `MGMT_SET` (Management Set) message to the Leader, and sets the * variables to the values specified. * @sa otCommissionerSendMgmtSet */ template <> otError Commissioner::Process(Arg aArgs[]) { otError error; otCommissioningDataset dataset; uint8_t tlvs[32]; uint8_t tlvsLength = 0; VerifyOrExit(!aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS); ClearAllBytes(dataset); for (; !aArgs->IsEmpty(); aArgs++) { if (*aArgs == "locator") { aArgs++; dataset.mIsLocatorSet = true; SuccessOrExit(error = aArgs->ParseAsUint16(dataset.mLocator)); } else if (*aArgs == "sessionid") { aArgs++; dataset.mIsSessionIdSet = true; SuccessOrExit(error = aArgs->ParseAsUint16(dataset.mSessionId)); } else if (*aArgs == "steeringdata") { uint16_t length; aArgs++; dataset.mIsSteeringDataSet = true; length = sizeof(dataset.mSteeringData.m8); SuccessOrExit(error = aArgs->ParseAsHexString(length, dataset.mSteeringData.m8)); dataset.mSteeringData.mLength = static_cast(length); } else if (*aArgs == "joinerudpport") { aArgs++; dataset.mIsJoinerUdpPortSet = true; SuccessOrExit(error = aArgs->ParseAsUint16(dataset.mJoinerUdpPort)); } else if (*aArgs == "-x") { uint16_t length; aArgs++; length = sizeof(tlvs); SuccessOrExit(error = aArgs->ParseAsHexString(length, tlvs)); tlvsLength = static_cast(length); } else { ExitNow(error = OT_ERROR_INVALID_ARGS); } } error = otCommissionerSendMgmtSet(GetInstancePtr(), &dataset, tlvs, tlvsLength); exit: return error; } /** * @cli commissioner panid * @code * commissioner panid 0xdead 0x7fff800 fdde:ad00:beef:0:0:ff:fe00:c00 * Done * Conflict: dead, 00000800 * @endcode * @cparam commissioner panid @ca{panid} @ca{mask} @ca{destination} * * `paind`: PAN ID to use to check for conflicts. * * `mask`; Bitmask that identifies channels to perform IEEE 802.15.4 * Active Scans. * * `destination`: IPv6 destination address for the message. The message may be multicast. * @par * Sends a PAN ID query. Command output is returned as it is received. * @note Use this command only after successfully starting the %Commissioner role * with the `commissioner start` command. * @csa{commissioner start} * @sa otCommissionerPanIdQuery */ template <> otError Commissioner::Process(Arg aArgs[]) { otError error; uint16_t panId; uint32_t mask; otIp6Address address; SuccessOrExit(error = aArgs[0].ParseAsUint16(panId)); SuccessOrExit(error = aArgs[1].ParseAsUint32(mask)); SuccessOrExit(error = aArgs[2].ParseAsIp6Address(address)); error = otCommissionerPanIdQuery(GetInstancePtr(), panId, mask, &address, &Commissioner::HandlePanIdConflict, this); exit: return error; } /** * @cli commissioner provisioningurl * @code * commissioner provisioningurl http://github.com/openthread/openthread * Done * @endcode * @cparam commissioner provisioningurl @ca{provisioningurl} * @par * Sets the %Commissioner provisioning URL. * @sa otCommissionerSetProvisioningUrl */ template <> otError Commissioner::Process(Arg aArgs[]) { // If aArgs[0] is empty, `GetCString() will return `nullptr` /// which will correctly clear the provisioning URL. return otCommissionerSetProvisioningUrl(GetInstancePtr(), aArgs[0].GetCString()); } /** * @cli commissioner sessionid * @code * commissioner sessionid * 0 * Done * @endcode * @par * Gets the current %Commissioner session ID. * @sa otCommissionerGetSessionId */ template <> otError Commissioner::Process(Arg aArgs[]) { OT_UNUSED_VARIABLE(aArgs); OutputLine("%d", otCommissionerGetSessionId(GetInstancePtr())); return OT_ERROR_NONE; } /** * @cli commissioner id (get,set) * @code * commissioner id OpenThread Commissioner * Done * @endcode * @code * commissioner id * OpenThread Commissioner * Done * @endcode * @cparam commissioner id @ca{name} * @par * Gets or sets the OpenThread %Commissioner ID name. * @sa otCommissionerSetId */ template <> otError Commissioner::Process(Arg aArgs[]) { otError error; if (aArgs[0].IsEmpty()) { OutputLine("%s", otCommissionerGetId(GetInstancePtr())); error = OT_ERROR_NONE; } else { error = otCommissionerSetId(GetInstancePtr(), aArgs[0].GetCString()); } return error; } /** * @cli commissioner start * @code * commissioner start * Commissioner: petitioning * Done * Commissioner: active * @endcode * @par * Starts the Thread %Commissioner role. * @note The `commissioner` commands are available only when * `OPENTHREAD_CONFIG_COMMISSIONER_ENABLE` and `OPENTHREAD_FTD` are set. * @sa otCommissionerStart */ template <> otError Commissioner::Process(Arg aArgs[]) { OT_UNUSED_VARIABLE(aArgs); return otCommissionerStart(GetInstancePtr(), &Commissioner::HandleStateChanged, &Commissioner::HandleJoinerEvent, this); } void Commissioner::HandleStateChanged(otCommissionerState aState, void *aContext) { static_cast(aContext)->HandleStateChanged(aState); } void Commissioner::HandleStateChanged(otCommissionerState aState) { OutputLine("Commissioner: %s", StateToString(aState)); } const char *Commissioner::StateToString(otCommissionerState aState) { static const char *const kStateString[] = { "disabled", // (0) OT_COMMISSIONER_STATE_DISABLED "petitioning", // (1) OT_COMMISSIONER_STATE_PETITION "active", // (2) OT_COMMISSIONER_STATE_ACTIVE }; static_assert(0 == OT_COMMISSIONER_STATE_DISABLED, "OT_COMMISSIONER_STATE_DISABLED value is incorrect"); static_assert(1 == OT_COMMISSIONER_STATE_PETITION, "OT_COMMISSIONER_STATE_PETITION value is incorrect"); static_assert(2 == OT_COMMISSIONER_STATE_ACTIVE, "OT_COMMISSIONER_STATE_ACTIVE value is incorrect"); return Stringify(aState, kStateString); } void Commissioner::HandleJoinerEvent(otCommissionerJoinerEvent aEvent, const otJoinerInfo *aJoinerInfo, const otExtAddress *aJoinerId, void *aContext) { static_cast(aContext)->HandleJoinerEvent(aEvent, aJoinerInfo, aJoinerId); } void Commissioner::HandleJoinerEvent(otCommissionerJoinerEvent aEvent, const otJoinerInfo *aJoinerInfo, const otExtAddress *aJoinerId) { static const char *const kEventStrings[] = { "start", // (0) OT_COMMISSIONER_JOINER_START "connect", // (1) OT_COMMISSIONER_JOINER_CONNECTED "finalize", // (2) OT_COMMISSIONER_JOINER_FINALIZE "end", // (3) OT_COMMISSIONER_JOINER_END "remove", // (4) OT_COMMISSIONER_JOINER_REMOVED }; static_assert(0 == OT_COMMISSIONER_JOINER_START, "OT_COMMISSIONER_JOINER_START value is incorrect"); static_assert(1 == OT_COMMISSIONER_JOINER_CONNECTED, "OT_COMMISSIONER_JOINER_CONNECTED value is incorrect"); static_assert(2 == OT_COMMISSIONER_JOINER_FINALIZE, "OT_COMMISSIONER_JOINER_FINALIZE value is incorrect"); static_assert(3 == OT_COMMISSIONER_JOINER_END, "OT_COMMISSIONER_JOINER_END value is incorrect"); static_assert(4 == OT_COMMISSIONER_JOINER_REMOVED, "OT_COMMISSIONER_JOINER_REMOVED value is incorrect"); OT_UNUSED_VARIABLE(aJoinerInfo); OutputFormat("Commissioner: Joiner %s ", Stringify(aEvent, kEventStrings)); if (aJoinerId != nullptr) { OutputExtAddress(*aJoinerId); } OutputNewLine(); } /** * @cli commissioner stop * @code * commissioner stop * Done * @endcode * @par * Stops the Thread %Commissioner role. * @sa otCommissionerStop */ template <> otError Commissioner::Process(Arg aArgs[]) { OT_UNUSED_VARIABLE(aArgs); return otCommissionerStop(GetInstancePtr()); } /** * @cli commissioner state * @code * commissioner state * active * Done * @endcode * @par * Returns the current state of the %Commissioner. Possible values are * `active`, `disabled`, or `petition` (petitioning to become %Commissioner). * @sa otCommissionerState */ template <> otError Commissioner::Process(Arg aArgs[]) { OT_UNUSED_VARIABLE(aArgs); OutputLine("%s", StateToString(otCommissionerGetState(GetInstancePtr()))); return OT_ERROR_NONE; } otError Commissioner::Process(Arg aArgs[]) { #define CmdEntry(aCommandString) \ { \ aCommandString, &Commissioner::Process \ } static constexpr Command kCommands[] = { CmdEntry("announce"), CmdEntry("energy"), CmdEntry("id"), CmdEntry("joiner"), CmdEntry("mgmtget"), CmdEntry("mgmtset"), CmdEntry("panid"), CmdEntry("provisioningurl"), CmdEntry("sessionid"), CmdEntry("start"), CmdEntry("state"), CmdEntry("stop"), }; #undef CmdEntry static_assert(BinarySearch::IsSorted(kCommands), "kCommands is not sorted"); otError error = OT_ERROR_INVALID_COMMAND; const Command *command; if (aArgs[0].IsEmpty() || (aArgs[0] == "help")) { OutputCommandTable(kCommands); ExitNow(error = aArgs[0].IsEmpty() ? error : OT_ERROR_NONE); } command = BinarySearch::Find(aArgs[0].GetCString(), kCommands); VerifyOrExit(command != nullptr); error = (this->*command->mHandler)(aArgs + 1); exit: return error; } void Commissioner::HandleEnergyReport(uint32_t aChannelMask, const uint8_t *aEnergyList, uint8_t aEnergyListLength, void *aContext) { static_cast(aContext)->HandleEnergyReport(aChannelMask, aEnergyList, aEnergyListLength); } void Commissioner::HandleEnergyReport(uint32_t aChannelMask, const uint8_t *aEnergyList, uint8_t aEnergyListLength) { OutputFormat("Energy: %08lx ", ToUlong(aChannelMask)); for (uint8_t i = 0; i < aEnergyListLength; i++) { OutputFormat("%d ", static_cast(aEnergyList[i])); } OutputNewLine(); } void Commissioner::HandlePanIdConflict(uint16_t aPanId, uint32_t aChannelMask, void *aContext) { static_cast(aContext)->HandlePanIdConflict(aPanId, aChannelMask); } void Commissioner::HandlePanIdConflict(uint16_t aPanId, uint32_t aChannelMask) { OutputLine("Conflict: %04x, %08lx", aPanId, ToUlong(aChannelMask)); } } // namespace Cli } // namespace ot #endif // OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD