mirror of
https://github.com/espressif/openthread.git
synced 2026-08-20 17:39:51 +00:00
[coap] add support for RFC7641 (#4396)
* [coap] Add option scanning functions
In a few places in the OpenThread code-base, the following construct is
seen:
```
otCoapOptionIterator iterator;
otCoapOptionIteratorInit(&iterator, message);
for (const otCoapOption *opt = otCoapOptionIteratorGetFirstOption(&iterator);
opt != NULL;
opt = otCoapOptionIteratorGetNextOption(&iterator))
{
if (opt->mNumber == OT_COAP_OPTION_DESIREDOPTION)
{
doSomethingWith(opt);
break;
}
}
```
or its equivalent. In cases where multiple options are sought, the
above is still the better approach (using a `switch` statement), but
otherwise this seems repetitive.
`otCoapOptionIteratorGetFirstOptionMatching` basically implements the
above loop, returning the first option that matches. This same code can
also be used for testing for presence too by comparing the resultant
pointer to the `NULL` singleton.
For completeness in cases where a software developer may want to
*resume* looking for further matching options,
`otCoapOptionIteratorGetNextOptionMatching` is also provided.
* [coap] Add function for decoding Uint options
To properly implement features such as RFC7641 and RFC7959, one must be
able to decode unsigned integer options. It seems silly to have an
encoder but not a decoder, so here is the missing piece.
Future considerations may be a `uint32_t` version since the majority of
use cases do not call for a `uint64_t`; the type was chosen since it was
the most general case.
* [coap] Reference the RFC for the Observe option.
For the sake of completeness, reference the relevant RFC in the
documentation so that developers can read up more on how it should work.
* [coap] Add mObserve flag to metadata.
When we're implementing observations, a few exceptions to the usual CoAP
conventions apply, notably:
- when we receive the initial reply, the request is _not_ finished, we
will receive subsequent requests until we tell the server to stop.
- when we send a confirmable notification, the observing client will
reply with an `ACK` with no further traffic: we need to differentiate
this from an empty `ACK` to a request meaning we should expect a reply
later.
While we could just repeatedly scan for the `Observe` option on the
request, that is wasteful in terms of processing time. This allows us
to scan the request just once, and set a single-bit flag to save
processing later.
* [coap] Don't expect traffic after ACK to notification
If the client sends an empty `ACK` in reply to a confirmable RFC7641
notification, we will _NOT_ see further replies with additional
information. Consider the notification as acknowledged and pass the
details back to the application.
* [coap] Pass through notifications to handler
Ensure that all notifications sent by the CoAP server are passed back to
the application's CoAP request handler. When we receive the first one,
the request continues -- it does not finish until the server drops the
`Observe` flag or we tell the server to stop.
* [coap] Disable expiry timer once subscribed
If a request carries `Observe=0` and we receive the first reply, ignore
future time-out events as the transaction will finish only when
explicitly terminated.
* [coap] Support eager subscription cancellation
A CoAP client may signal its intention to unsubscribe from a resource by
sending a `GET` request with a token matching that of the original
request and setting the `Observe` option to the value 1.
In the event the application does this, we should pick this up and
cancel the pending transaction locally. The handler will be notified of
this by a final call where `aError=OT_ERROR_NONE` and no message given.
* [cli] Add RFC7641 support to CLI example
As an aide to those developing code that uses RFC7641, implement an
example client and server that supports this feature.
This commit is contained in:
@@ -38,6 +38,7 @@ DEBUG ?= 0
|
||||
BORDER_AGENT ?= 1
|
||||
BORDER_ROUTER ?= 1
|
||||
COAP ?= 1
|
||||
COAP_OBSERVE ?= 1
|
||||
COAPS ?= 1
|
||||
COMMISSIONER ?= 1
|
||||
CHANNEL_MANAGER ?= 1
|
||||
|
||||
@@ -32,6 +32,7 @@ BIG_ENDIAN ?= 0
|
||||
BORDER_AGENT ?= 0
|
||||
BORDER_ROUTER ?= 0
|
||||
COAP ?= 0
|
||||
COAP_OBSERVE ?= 0
|
||||
COAPS ?= 0
|
||||
COMMISSIONER ?= 0
|
||||
COVERAGE ?= 0
|
||||
@@ -90,6 +91,10 @@ ifeq ($(COAPS),1)
|
||||
COMMONCFLAGS += -DOPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE=1
|
||||
endif
|
||||
|
||||
ifeq ($(COAP_OBSERVE),1)
|
||||
COMMONCFLAGS += -DOPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE=1
|
||||
endif
|
||||
|
||||
ifeq ($(COMMISSIONER),1)
|
||||
COMMONCFLAGS += -DOPENTHREAD_CONFIG_COMMISSIONER_ENABLE=1
|
||||
endif
|
||||
|
||||
@@ -127,7 +127,7 @@ typedef enum otCoapOptionType
|
||||
OT_COAP_OPTION_URI_HOST = 3, ///< Uri-Host
|
||||
OT_COAP_OPTION_E_TAG = 4, ///< ETag
|
||||
OT_COAP_OPTION_IF_NONE_MATCH = 5, ///< If-None-Match
|
||||
OT_COAP_OPTION_OBSERVE = 6, ///< Observe
|
||||
OT_COAP_OPTION_OBSERVE = 6, ///< Observe [RFC7641]
|
||||
OT_COAP_OPTION_URI_PORT = 7, ///< Uri-Port
|
||||
OT_COAP_OPTION_LOCATION_PATH = 8, ///< Location-Path
|
||||
OT_COAP_OPTION_URI_PATH = 11, ///< Uri-Path
|
||||
@@ -491,6 +491,7 @@ otError otCoapMessageAppendOption(otMessage *aMessage, uint16_t aNumber, uint16_
|
||||
* @retval OT_ERROR_INVALID_ARGS The option type is not equal or greater than the last option type.
|
||||
* @retval OT_ERROR_NO_BUFS The option length exceeds the buffer size.
|
||||
*
|
||||
* @see otCoapMessageGetOptionUintValue
|
||||
*/
|
||||
otError otCoapMessageAppendUintOption(otMessage *aMessage, uint16_t aNumber, uint32_t aValue);
|
||||
|
||||
@@ -681,6 +682,18 @@ const uint8_t *otCoapMessageGetToken(const otMessage *aMessage);
|
||||
*/
|
||||
otError otCoapOptionIteratorInit(otCoapOptionIterator *aIterator, const otMessage *aMessage);
|
||||
|
||||
/**
|
||||
* This function returns a pointer to the first option matching the specified option number.
|
||||
*
|
||||
* @param[in] aIterator A pointer to the CoAP message option iterator.
|
||||
* @param[in] aOption The option number sought.
|
||||
*
|
||||
* @returns A pointer to the first matching option. If no matching option is present NULL pointer is returned.
|
||||
*
|
||||
*/
|
||||
const otCoapOption *otCoapOptionIteratorGetFirstOptionMatching(otCoapOptionIterator *aIterator,
|
||||
otCoapOptionType aOption);
|
||||
|
||||
/**
|
||||
* This function returns a pointer to the first option.
|
||||
*
|
||||
@@ -691,6 +704,18 @@ otError otCoapOptionIteratorInit(otCoapOptionIterator *aIterator, const otMessag
|
||||
*/
|
||||
const otCoapOption *otCoapOptionIteratorGetFirstOption(otCoapOptionIterator *aIterator);
|
||||
|
||||
/**
|
||||
* This function returns a pointer to the next option matching the specified option number.
|
||||
*
|
||||
* @param[in] aIterator A pointer to the CoAP message option iterator.
|
||||
* @param[in] aOption The option number sought.
|
||||
*
|
||||
* @returns A pointer to the next matching option. If no further matching option is present NULL pointer is returned.
|
||||
*
|
||||
*/
|
||||
const otCoapOption *otCoapOptionIteratorGetNextOptionMatching(otCoapOptionIterator *aIterator,
|
||||
otCoapOptionType aOption);
|
||||
|
||||
/**
|
||||
* This function returns a pointer to the next option.
|
||||
*
|
||||
@@ -701,6 +726,21 @@ const otCoapOption *otCoapOptionIteratorGetFirstOption(otCoapOptionIterator *aIt
|
||||
*/
|
||||
const otCoapOption *otCoapOptionIteratorGetNextOption(otCoapOptionIterator *aIterator);
|
||||
|
||||
/**
|
||||
* This function fills current option value into @p aValue assuming the current value is an unsigned integer encoded
|
||||
* according to https://tools.ietf.org/html/rfc7252#section-3.2
|
||||
*
|
||||
* @param[inout] aIterator A pointer to the CoAP message option iterator.
|
||||
* @param[out] aValue A pointer to an unsigned integer to receive the option value.
|
||||
*
|
||||
* @retval OT_ERROR_NONE Successfully filled value.
|
||||
* @retval OT_ERROR_NOT_FOUND No current option.
|
||||
* @retval OT_ERROR_NO_BUFS Value is too long to fit in a uint64_t.
|
||||
*
|
||||
* @see otCoapMessageAppendUintOption
|
||||
*/
|
||||
otError otCoapOptionIteratorGetOptionUintValue(otCoapOptionIterator *aIterator, uint64_t *const aValue);
|
||||
|
||||
/**
|
||||
* This function fills current option value into @p aValue.
|
||||
*
|
||||
|
||||
@@ -55,12 +55,15 @@ coap response sent
|
||||
## Command List
|
||||
|
||||
* [help](#help)
|
||||
* [cancel](#cancel)
|
||||
* [delete](#delete-address-uri-path-type-payload)
|
||||
* [get](#get-address-uri-path-type)
|
||||
* [observe](#observe-address-uri-path-type)
|
||||
* [parameters](#parameters)
|
||||
* [post](#post-address-uri-path-type-payload)
|
||||
* [put](#put-address-uri-path-type-payload)
|
||||
* [resource](#resource-uri-path)
|
||||
* [set](#set-new-content)
|
||||
* [start](#start)
|
||||
* [stop](#stop)
|
||||
|
||||
@@ -71,12 +74,15 @@ coap response sent
|
||||
```bash
|
||||
> coap help
|
||||
help
|
||||
cancel
|
||||
delete
|
||||
get
|
||||
observe
|
||||
parameters
|
||||
post
|
||||
put
|
||||
resource
|
||||
set
|
||||
start
|
||||
stop
|
||||
Done
|
||||
@@ -84,6 +90,16 @@ Done
|
||||
|
||||
List the CoAP CLI commands.
|
||||
|
||||
### cancel
|
||||
|
||||
Request the cancellation of an existing observation subscription to a remote
|
||||
resource.
|
||||
|
||||
```bash
|
||||
> coap cancel
|
||||
Done
|
||||
```
|
||||
|
||||
### delete \<address\> \<uri-path\> \[type\] \[payload\]
|
||||
|
||||
* address: IPv6 address of the CoAP server.
|
||||
@@ -107,6 +123,20 @@ Done
|
||||
Done
|
||||
```
|
||||
|
||||
### observe \<address\> \<uri-path\> \[type\]
|
||||
|
||||
This is the same a `get`, but the `Observe` parameter will be sent, set to 0
|
||||
triggering a subscription request.
|
||||
|
||||
* address: IPv6 address of the CoAP server.
|
||||
* uri-path: URI path of the resource.
|
||||
* type: "con" for Confirmable or "non-con" for Non-confirmable (default).
|
||||
|
||||
```bash
|
||||
> coap observe fdde:ad00:beef:0:2780:9423:166c:1aac test-resource
|
||||
Done
|
||||
```
|
||||
|
||||
### parameters \<type\> \["default"|<ack\_timeout\> <ack\_random\_factor\_numerator\> <ack\_random\_factor\_denominator\> <max\_retransmit\>\]
|
||||
|
||||
Sets transmission parameters for the following interactions.
|
||||
@@ -180,6 +210,15 @@ test-resource
|
||||
Done
|
||||
```
|
||||
|
||||
### set \[new-content\]
|
||||
|
||||
Sets the content sent by the test resource. If a CoAP client is observing the resource, a notification is sent to that client.
|
||||
|
||||
```bash
|
||||
> coap set Testing123
|
||||
Done
|
||||
```
|
||||
|
||||
### start
|
||||
|
||||
Starts the application coap service.
|
||||
|
||||
+308
-9
@@ -45,10 +45,21 @@ namespace ot {
|
||||
namespace Cli {
|
||||
|
||||
const struct Coap::Command Coap::sCommands[] = {
|
||||
{"help", &Coap::ProcessHelp}, {"delete", &Coap::ProcessRequest},
|
||||
{"get", &Coap::ProcessRequest}, {"parameters", &Coap::ProcessParameters},
|
||||
{"post", &Coap::ProcessRequest}, {"put", &Coap::ProcessRequest},
|
||||
{"resource", &Coap::ProcessResource}, {"start", &Coap::ProcessStart},
|
||||
{"help", &Coap::ProcessHelp},
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
{"cancel", &Coap::ProcessCancel},
|
||||
#endif
|
||||
{"delete", &Coap::ProcessRequest},
|
||||
{"get", &Coap::ProcessRequest},
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
{"observe", &Coap::ProcessRequest},
|
||||
#endif
|
||||
{"parameters", &Coap::ProcessParameters},
|
||||
{"post", &Coap::ProcessRequest},
|
||||
{"put", &Coap::ProcessRequest},
|
||||
{"resource", &Coap::ProcessResource},
|
||||
{"set", &Coap::ProcessSet},
|
||||
{"start", &Coap::ProcessStart},
|
||||
{"stop", &Coap::ProcessStop},
|
||||
};
|
||||
|
||||
@@ -56,10 +67,70 @@ Coap::Coap(Interpreter &aInterpreter)
|
||||
: mInterpreter(aInterpreter)
|
||||
, mUseDefaultRequestTxParameters(true)
|
||||
, mUseDefaultResponseTxParameters(true)
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
, mObserveSerial(0)
|
||||
, mRequestTokenLength(0)
|
||||
, mSubscriberTokenLength(0)
|
||||
, mSubscriberConfirmableNotifications(false)
|
||||
#endif
|
||||
{
|
||||
memset(&mResource, 0, sizeof(mResource));
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
memset(&mRequestAddr, 0, sizeof(mRequestAddr));
|
||||
memset(&mSubscriberSock, 0, sizeof(mSubscriberSock));
|
||||
memset(&mRequestToken, 0, sizeof(mRequestToken));
|
||||
memset(&mSubscriberToken, 0, sizeof(mSubscriberToken));
|
||||
memset(&mRequestUri, 0, sizeof(mRequestUri));
|
||||
#endif
|
||||
memset(&mUriPath, 0, sizeof(mUriPath));
|
||||
strncpy(mResourceContent, "0", sizeof(mResourceContent) - 1);
|
||||
}
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
otError Coap::CancelResourceSubscription(void)
|
||||
{
|
||||
otError error = OT_ERROR_NONE;
|
||||
otMessage * message = NULL;
|
||||
otMessageInfo messageInfo;
|
||||
|
||||
memset(&messageInfo, 0, sizeof(messageInfo));
|
||||
messageInfo.mPeerAddr = mRequestAddr;
|
||||
messageInfo.mPeerPort = OT_DEFAULT_COAP_PORT;
|
||||
|
||||
VerifyOrExit(mRequestTokenLength != 0, error = OT_ERROR_INVALID_STATE);
|
||||
|
||||
message = otCoapNewMessage(mInterpreter.mInstance, NULL);
|
||||
VerifyOrExit(message != NULL, error = OT_ERROR_NO_BUFS);
|
||||
|
||||
otCoapMessageInit(message, OT_COAP_TYPE_CONFIRMABLE, OT_COAP_CODE_GET);
|
||||
|
||||
SuccessOrExit(error = otCoapMessageSetToken(message, mRequestToken, mRequestTokenLength));
|
||||
SuccessOrExit(error = otCoapMessageAppendObserveOption(message, 1));
|
||||
SuccessOrExit(error = otCoapMessageAppendUriPathOptions(message, mRequestUri));
|
||||
SuccessOrExit(error =
|
||||
otCoapSendRequest(mInterpreter.mInstance, message, &messageInfo, &Coap::HandleResponse, this));
|
||||
|
||||
memset(&mRequestAddr, 0, sizeof(mRequestAddr));
|
||||
memset(&mRequestUri, 0, sizeof(mRequestUri));
|
||||
mRequestTokenLength = 0;
|
||||
|
||||
exit:
|
||||
|
||||
if ((error != OT_ERROR_NONE) && (message != NULL))
|
||||
{
|
||||
otMessageFree(message);
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
void Coap::CancelSubscriber(void)
|
||||
{
|
||||
memset(&mSubscriberSock, 0, sizeof(mSubscriberSock));
|
||||
mSubscriberTokenLength = 0;
|
||||
}
|
||||
#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
|
||||
void Coap::PrintPayload(otMessage *aMessage) const
|
||||
{
|
||||
uint8_t buf[kMaxBufferSize];
|
||||
@@ -86,6 +157,16 @@ void Coap::PrintPayload(otMessage *aMessage) const
|
||||
mInterpreter.mServer->OutputFormat("\r\n");
|
||||
}
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
otError Coap::ProcessCancel(int argc, char *argv[])
|
||||
{
|
||||
OT_UNUSED_VARIABLE(argc);
|
||||
OT_UNUSED_VARIABLE(argv);
|
||||
|
||||
return CancelResourceSubscription();
|
||||
}
|
||||
#endif
|
||||
|
||||
otError Coap::ProcessHelp(int argc, char *argv[])
|
||||
{
|
||||
OT_UNUSED_VARIABLE(argc);
|
||||
@@ -123,6 +204,67 @@ exit:
|
||||
return OT_ERROR_NONE;
|
||||
}
|
||||
|
||||
otError Coap::ProcessSet(int argc, char *argv[])
|
||||
{
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
otMessage * notificationMessage = NULL;
|
||||
otMessageInfo messageInfo;
|
||||
#endif
|
||||
otError error = OT_ERROR_NONE;
|
||||
|
||||
if (argc > 1)
|
||||
{
|
||||
VerifyOrExit(strlen(argv[1]) < (kMaxBufferSize - 1), error = OT_ERROR_INVALID_ARGS);
|
||||
strncpy(mResourceContent, argv[1], sizeof(mResourceContent) - 1);
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (mSubscriberTokenLength > 0)
|
||||
{
|
||||
// Notify the subscriber
|
||||
memset(&messageInfo, 0, sizeof(messageInfo));
|
||||
messageInfo.mPeerAddr = mSubscriberSock.mAddress;
|
||||
messageInfo.mPeerPort = mSubscriberSock.mPort;
|
||||
|
||||
mInterpreter.mServer->OutputFormat("sending coap notification to ");
|
||||
mInterpreter.OutputIp6Address(mSubscriberSock.mAddress);
|
||||
mInterpreter.mServer->OutputFormat("\r\n");
|
||||
|
||||
notificationMessage = otCoapNewMessage(mInterpreter.mInstance, NULL);
|
||||
VerifyOrExit(notificationMessage != NULL, error = OT_ERROR_NO_BUFS);
|
||||
|
||||
otCoapMessageInit(
|
||||
notificationMessage,
|
||||
((mSubscriberConfirmableNotifications) ? OT_COAP_TYPE_CONFIRMABLE : OT_COAP_TYPE_NON_CONFIRMABLE),
|
||||
OT_COAP_CODE_CONTENT);
|
||||
|
||||
SuccessOrExit(error = otCoapMessageSetToken(notificationMessage, mSubscriberToken, mSubscriberTokenLength));
|
||||
SuccessOrExit(error = otCoapMessageAppendObserveOption(notificationMessage, mObserveSerial++));
|
||||
SuccessOrExit(error = otCoapMessageSetPayloadMarker(notificationMessage));
|
||||
SuccessOrExit(error = otMessageAppend(notificationMessage, mResourceContent,
|
||||
static_cast<uint16_t>(strlen(mResourceContent))));
|
||||
|
||||
SuccessOrExit(error = otCoapSendRequest(mInterpreter.mInstance, notificationMessage, &messageInfo,
|
||||
&Coap::HandleNotificationResponse, this));
|
||||
}
|
||||
#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
}
|
||||
else
|
||||
{
|
||||
mInterpreter.mServer->OutputFormat("%s\r\n", mResourceContent);
|
||||
}
|
||||
|
||||
exit:
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if ((error != OT_ERROR_NONE) && (notificationMessage != NULL))
|
||||
{
|
||||
otMessageFree(notificationMessage);
|
||||
}
|
||||
#endif
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
otError Coap::ProcessStart(int argc, char *argv[])
|
||||
{
|
||||
OT_UNUSED_VARIABLE(argc);
|
||||
@@ -226,6 +368,9 @@ otError Coap::ProcessRequest(int argc, char *argv[])
|
||||
otCoapType coapType = OT_COAP_TYPE_NON_CONFIRMABLE;
|
||||
otCoapCode coapCode = OT_COAP_CODE_GET;
|
||||
otIp6Address coapDestinationIp;
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
bool coapObserve = false;
|
||||
#endif
|
||||
|
||||
VerifyOrExit(argc > 0, error = OT_ERROR_INVALID_ARGS);
|
||||
|
||||
@@ -234,6 +379,14 @@ otError Coap::ProcessRequest(int argc, char *argv[])
|
||||
{
|
||||
coapCode = OT_COAP_CODE_GET;
|
||||
}
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
else if (strcmp(argv[0], "observe") == 0)
|
||||
{
|
||||
// Observe request. This is a GET with Observe=0
|
||||
coapCode = OT_COAP_CODE_GET;
|
||||
coapObserve = true;
|
||||
}
|
||||
#endif
|
||||
else if (strcmp(argv[0], "post") == 0)
|
||||
{
|
||||
coapCode = OT_COAP_CODE_POST;
|
||||
@@ -281,11 +434,27 @@ otError Coap::ProcessRequest(int argc, char *argv[])
|
||||
}
|
||||
}
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (coapObserve && mRequestTokenLength)
|
||||
{
|
||||
// New observe request, cancel any existing observation
|
||||
SuccessOrExit(error = CancelResourceSubscription());
|
||||
}
|
||||
#endif
|
||||
|
||||
message = otCoapNewMessage(mInterpreter.mInstance, NULL);
|
||||
VerifyOrExit(message != NULL, error = OT_ERROR_NO_BUFS);
|
||||
|
||||
otCoapMessageInit(message, coapType, coapCode);
|
||||
otCoapMessageGenerateToken(message, ot::Coap::Message::kDefaultTokenLength);
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (coapObserve)
|
||||
{
|
||||
SuccessOrExit(error = otCoapMessageAppendObserveOption(message, 0));
|
||||
}
|
||||
#endif
|
||||
|
||||
SuccessOrExit(error = otCoapMessageAppendUriPathOptions(message, coapUri));
|
||||
|
||||
if (argc > 4)
|
||||
@@ -308,6 +477,18 @@ otError Coap::ProcessRequest(int argc, char *argv[])
|
||||
messageInfo.mPeerAddr = coapDestinationIp;
|
||||
messageInfo.mPeerPort = OT_DEFAULT_COAP_PORT;
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (coapObserve)
|
||||
{
|
||||
// Make a note of the message details for later so we can cancel it later.
|
||||
memcpy(&mRequestAddr, &coapDestinationIp, sizeof(mRequestAddr));
|
||||
mRequestTokenLength = otCoapMessageGetTokenLength(message);
|
||||
memcpy(mRequestToken, otCoapMessageGetToken(message), mRequestTokenLength);
|
||||
strncpy(mRequestUri, coapUri, sizeof(mRequestUri) - 1);
|
||||
mRequestUri[sizeof(mRequestUri) - 1] = '\0'; // Fix gcc-9.2 warning
|
||||
}
|
||||
#endif
|
||||
|
||||
if ((coapType == OT_COAP_TYPE_CONFIRMABLE) || (coapCode == OT_COAP_CODE_GET))
|
||||
{
|
||||
error = otCoapSendRequestWithParameters(mInterpreter.mInstance, message, &messageInfo, &Coap::HandleResponse,
|
||||
@@ -363,7 +544,11 @@ void Coap::HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo)
|
||||
otError error = OT_ERROR_NONE;
|
||||
otMessage *responseMessage = NULL;
|
||||
otCoapCode responseCode = OT_COAP_CODE_EMPTY;
|
||||
char responseContent = '0';
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
uint64_t observe = 0;
|
||||
bool observePresent = false;
|
||||
otCoapOptionIterator iterator;
|
||||
#endif
|
||||
|
||||
mInterpreter.mServer->OutputFormat("coap request from ");
|
||||
mInterpreter.OutputIp6Address(aMessageInfo->mPeerAddr);
|
||||
@@ -373,6 +558,16 @@ void Coap::HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo)
|
||||
{
|
||||
case OT_COAP_CODE_GET:
|
||||
mInterpreter.mServer->OutputFormat("GET");
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
SuccessOrExit(error = otCoapOptionIteratorInit(&iterator, aMessage));
|
||||
if (otCoapOptionIteratorGetFirstOptionMatching(&iterator, OT_COAP_OPTION_OBSERVE) != NULL)
|
||||
{
|
||||
SuccessOrExit(error = otCoapOptionIteratorGetOptionUintValue(&iterator, &observe));
|
||||
observePresent = true;
|
||||
|
||||
mInterpreter.mServer->OutputFormat(" OBS=%lu", static_cast<uint32_t>(observe));
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
|
||||
case OT_COAP_CODE_DELETE:
|
||||
@@ -397,9 +592,51 @@ void Coap::HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo)
|
||||
if (otCoapMessageGetType(aMessage) == OT_COAP_TYPE_CONFIRMABLE ||
|
||||
otCoapMessageGetCode(aMessage) == OT_COAP_CODE_GET)
|
||||
{
|
||||
if (otCoapMessageGetCode(aMessage) == OT_COAP_CODE_GET)
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (observePresent && (mSubscriberTokenLength > 0) && (observe == 0))
|
||||
{
|
||||
// There is already a subscriber
|
||||
responseCode = OT_COAP_CODE_SERVICE_UNAVAILABLE;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
if (otCoapMessageGetCode(aMessage) == OT_COAP_CODE_GET)
|
||||
{
|
||||
responseCode = OT_COAP_CODE_CONTENT;
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (observePresent)
|
||||
{
|
||||
if (observe == 0)
|
||||
{
|
||||
// New subscriber
|
||||
mInterpreter.mServer->OutputFormat("Subscribing client\r\n");
|
||||
mSubscriberSock.mAddress = aMessageInfo->mPeerAddr;
|
||||
mSubscriberSock.mPort = aMessageInfo->mPeerPort;
|
||||
mSubscriberTokenLength = otCoapMessageGetTokenLength(aMessage);
|
||||
memcpy(mSubscriberToken, otCoapMessageGetToken(aMessage), mSubscriberTokenLength);
|
||||
|
||||
/*
|
||||
* Implementer note.
|
||||
*
|
||||
* Here, we try to match a confirmable GET request with confirmable
|
||||
* notifications, however this is not a requirement of RFC7641:
|
||||
* the server can send notifications of either type regardless of
|
||||
* what the client used to subscribe initially.
|
||||
*/
|
||||
mSubscriberConfirmableNotifications = (otCoapMessageGetType(aMessage) == OT_COAP_TYPE_CONFIRMABLE);
|
||||
}
|
||||
else if (observe == 1)
|
||||
{
|
||||
// See if it matches our subscriber token
|
||||
if ((otCoapMessageGetTokenLength(aMessage) == mSubscriberTokenLength) &&
|
||||
(memcmp(otCoapMessageGetToken(aMessage), mSubscriberToken, mSubscriberTokenLength) == 0))
|
||||
{
|
||||
// Unsubscribe request
|
||||
CancelSubscriber();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -412,10 +649,17 @@ void Coap::HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo)
|
||||
SuccessOrExit(
|
||||
error = otCoapMessageInitResponse(responseMessage, aMessage, OT_COAP_TYPE_ACKNOWLEDGMENT, responseCode));
|
||||
|
||||
if (otCoapMessageGetCode(aMessage) == OT_COAP_CODE_GET)
|
||||
if (responseCode == OT_COAP_CODE_CONTENT)
|
||||
{
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (observePresent && (observe == 0))
|
||||
{
|
||||
SuccessOrExit(error = otCoapMessageAppendObserveOption(responseMessage, mObserveSerial++));
|
||||
}
|
||||
#endif
|
||||
SuccessOrExit(error = otCoapMessageSetPayloadMarker(responseMessage));
|
||||
SuccessOrExit(error = otMessageAppend(responseMessage, &responseContent, sizeof(responseContent)));
|
||||
SuccessOrExit(error = otMessageAppend(responseMessage, mResourceContent,
|
||||
static_cast<uint16_t>(strlen(mResourceContent))));
|
||||
}
|
||||
|
||||
SuccessOrExit(error = otCoapSendResponseWithParameters(mInterpreter.mInstance, responseMessage, aMessageInfo,
|
||||
@@ -439,6 +683,39 @@ exit:
|
||||
}
|
||||
}
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
void Coap::HandleNotificationResponse(void * aContext,
|
||||
otMessage * aMessage,
|
||||
const otMessageInfo *aMessageInfo,
|
||||
otError aError)
|
||||
{
|
||||
static_cast<Coap *>(aContext)->HandleNotificationResponse(aMessage, aMessageInfo, aError);
|
||||
}
|
||||
|
||||
void Coap::HandleNotificationResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError)
|
||||
{
|
||||
OT_UNUSED_VARIABLE(aMessage);
|
||||
|
||||
switch (aError)
|
||||
{
|
||||
case OT_ERROR_NONE:
|
||||
if (aMessageInfo != NULL)
|
||||
{
|
||||
mInterpreter.mServer->OutputFormat("Received ACK in reply to notification from ");
|
||||
mInterpreter.OutputIp6Address(aMessageInfo->mPeerAddr);
|
||||
mInterpreter.mServer->OutputFormat("\r\n");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
mInterpreter.mServer->OutputFormat("coap receive notification response error %d: %s\r\n", aError,
|
||||
otThreadErrorToString(aError));
|
||||
CancelSubscriber();
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
|
||||
void Coap::HandleResponse(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError)
|
||||
{
|
||||
static_cast<Coap *>(aContext)->HandleResponse(aMessage, aMessageInfo, aError);
|
||||
@@ -451,11 +728,33 @@ void Coap::HandleResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo
|
||||
mInterpreter.mServer->OutputFormat("coap receive response error %d: %s\r\n", aError,
|
||||
otThreadErrorToString(aError));
|
||||
}
|
||||
else
|
||||
else if ((aMessageInfo != NULL) && (aMessage != NULL))
|
||||
{
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
otCoapOptionIterator iterator;
|
||||
#endif
|
||||
|
||||
mInterpreter.mServer->OutputFormat("coap response from ");
|
||||
mInterpreter.OutputIp6Address(aMessageInfo->mPeerAddr);
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (otCoapOptionIteratorInit(&iterator, aMessage) == OT_ERROR_NONE)
|
||||
{
|
||||
const otCoapOption *observeOpt =
|
||||
otCoapOptionIteratorGetFirstOptionMatching(&iterator, OT_COAP_OPTION_OBSERVE);
|
||||
|
||||
if (observeOpt != NULL)
|
||||
{
|
||||
uint64_t observeVal = 0;
|
||||
otError error = otCoapOptionIteratorGetOptionUintValue(&iterator, &observeVal);
|
||||
|
||||
if (error == OT_ERROR_NONE)
|
||||
{
|
||||
mInterpreter.mServer->OutputFormat(" OBS=%u", observeVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
PrintPayload(aMessage);
|
||||
}
|
||||
}
|
||||
|
||||
+32
-1
@@ -82,18 +82,35 @@ private:
|
||||
otError (Coap::*mCommand)(int argc, char *argv[]);
|
||||
};
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
otError CancelResourceSubscription(void);
|
||||
void CancelSubscriber(void);
|
||||
#endif
|
||||
|
||||
void PrintPayload(otMessage *aMessage) const;
|
||||
|
||||
otError ProcessHelp(int argc, char *argv[]);
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
otError ProcessCancel(int argc, char *argv[]);
|
||||
#endif
|
||||
otError ProcessParameters(int argc, char *argv[]);
|
||||
otError ProcessRequest(int argc, char *argv[]);
|
||||
otError ProcessResource(int argc, char *argv[]);
|
||||
otError ProcessSet(int argc, char *argv[]);
|
||||
otError ProcessStart(int argc, char *argv[]);
|
||||
otError ProcessStop(int argc, char *argv[]);
|
||||
|
||||
static void HandleRequest(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
|
||||
void HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo);
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
static void HandleNotificationResponse(void * aContext,
|
||||
otMessage * aMessage,
|
||||
const otMessageInfo *aMessageInfo,
|
||||
otError aError);
|
||||
void HandleNotificationResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError);
|
||||
#endif
|
||||
|
||||
static void HandleResponse(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError);
|
||||
void HandleResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError);
|
||||
|
||||
@@ -117,7 +134,21 @@ private:
|
||||
otCoapTxParameters mResponseTxParameters;
|
||||
|
||||
otCoapResource mResource;
|
||||
char mUriPath[kMaxUriLength];
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
otIp6Address mRequestAddr;
|
||||
otSockAddr mSubscriberSock;
|
||||
char mRequestUri[kMaxUriLength];
|
||||
uint8_t mRequestToken[OT_COAP_MAX_TOKEN_LENGTH];
|
||||
uint8_t mSubscriberToken[OT_COAP_MAX_TOKEN_LENGTH];
|
||||
#endif
|
||||
char mUriPath[kMaxUriLength];
|
||||
char mResourceContent[kMaxBufferSize];
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
uint32_t mObserveSerial;
|
||||
uint8_t mRequestTokenLength;
|
||||
uint8_t mSubscriberTokenLength;
|
||||
bool mSubscriberConfirmableNotifications;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace Cli
|
||||
|
||||
@@ -180,16 +180,32 @@ otError otCoapOptionIteratorInit(otCoapOptionIterator *aIterator, const otMessag
|
||||
return static_cast<Coap::OptionIterator *>(aIterator)->Init(static_cast<const Coap::Message *>(aMessage));
|
||||
}
|
||||
|
||||
const otCoapOption *otCoapOptionIteratorGetFirstOptionMatching(otCoapOptionIterator *aIterator,
|
||||
otCoapOptionType aOption)
|
||||
{
|
||||
return static_cast<Coap::OptionIterator *>(aIterator)->GetFirstOptionMatching(aOption);
|
||||
}
|
||||
|
||||
const otCoapOption *otCoapOptionIteratorGetFirstOption(otCoapOptionIterator *aIterator)
|
||||
{
|
||||
return static_cast<Coap::OptionIterator *>(aIterator)->GetFirstOption();
|
||||
}
|
||||
|
||||
const otCoapOption *otCoapOptionIteratorGetNextOptionMatching(otCoapOptionIterator *aIterator, otCoapOptionType aOption)
|
||||
{
|
||||
return static_cast<Coap::OptionIterator *>(aIterator)->GetNextOptionMatching(aOption);
|
||||
}
|
||||
|
||||
const otCoapOption *otCoapOptionIteratorGetNextOption(otCoapOptionIterator *aIterator)
|
||||
{
|
||||
return static_cast<Coap::OptionIterator *>(aIterator)->GetNextOption();
|
||||
}
|
||||
|
||||
otError otCoapOptionIteratorGetOptionUintValue(otCoapOptionIterator *aIterator, uint64_t *const aValue)
|
||||
{
|
||||
return static_cast<Coap::OptionIterator *>(aIterator)->GetOptionValue(*aValue);
|
||||
}
|
||||
|
||||
otError otCoapOptionIteratorGetOptionValue(otCoapOptionIterator *aIterator, void *aValue)
|
||||
{
|
||||
return static_cast<Coap::OptionIterator *>(aIterator)->GetOptionValue(aValue);
|
||||
|
||||
+119
-17
@@ -169,7 +169,45 @@ otError CoapBase::SendMessage(Message & aMessage,
|
||||
{
|
||||
Metadata metadata;
|
||||
|
||||
metadata.Init(aMessage.IsConfirmable(), aMessageInfo, aHandler, aContext, aTxParameters);
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
// Whether or not to turn on special "Observe" handling.
|
||||
OptionIterator iterator;
|
||||
bool observe;
|
||||
|
||||
SuccessOrExit(error = iterator.Init(&aMessage));
|
||||
observe = (iterator.GetFirstOptionMatching(OT_COAP_OPTION_OBSERVE) != NULL);
|
||||
|
||||
// Special case, if we're sending a GET with Observe=1, that is a cancellation.
|
||||
if (observe && (aMessage.GetCode() == OT_COAP_CODE_GET))
|
||||
{
|
||||
uint64_t observeVal = 0;
|
||||
|
||||
SuccessOrExit(error = iterator.GetOptionValue(observeVal));
|
||||
|
||||
if (observeVal == 1)
|
||||
{
|
||||
Metadata handlerMetadata;
|
||||
|
||||
// We're cancelling our subscription, so disable special-case handling on this request.
|
||||
observe = false;
|
||||
|
||||
// If we can find the previous handler context, cancel that too. Peer address
|
||||
// and tokens, etc should all match.
|
||||
Message *origRequest = FindRelatedRequest(aMessage, aMessageInfo, handlerMetadata);
|
||||
if (origRequest != NULL)
|
||||
{
|
||||
FinalizeCoapTransaction(*origRequest, handlerMetadata, NULL, NULL, OT_ERROR_NONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
|
||||
// Enqueue and send
|
||||
metadata.Init(aMessage.IsConfirmable(),
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
observe,
|
||||
#endif
|
||||
aMessageInfo, aHandler, aContext, aTxParameters);
|
||||
storedCopy = CopyAndEnqueueMessage(aMessage, copyLength, metadata);
|
||||
VerifyOrExit(storedCopy != NULL, error = OT_ERROR_NO_BUFS);
|
||||
}
|
||||
@@ -300,6 +338,14 @@ void CoapBase::HandleRetransmissionTimer(void)
|
||||
|
||||
if (now >= metadata.mNextTimerShot)
|
||||
{
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (message->IsRequest() && metadata.mObserve && metadata.mAcknowledged)
|
||||
{
|
||||
// This is a RFC7641 subscription. Do not time out.
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!metadata.mConfirmable || (metadata.mRetransmissionsRemaining == 0))
|
||||
{
|
||||
// No expected response or acknowledgment.
|
||||
@@ -501,10 +547,24 @@ void CoapBase::ProcessReceivedResponse(Message &aMessage, const Ip6::MessageInfo
|
||||
Metadata metadata;
|
||||
Message *request = NULL;
|
||||
otError error = OT_ERROR_NONE;
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
bool responseObserve = false;
|
||||
#endif
|
||||
|
||||
request = FindRelatedRequest(aMessage, aMessageInfo, metadata);
|
||||
VerifyOrExit(request != NULL);
|
||||
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (metadata.mObserve && request->IsRequest())
|
||||
{
|
||||
// We sent Observe in our request, see if we received Observe in the response too.
|
||||
OptionIterator iterator;
|
||||
|
||||
SuccessOrExit(error = iterator.Init(&aMessage));
|
||||
responseObserve = (iterator.GetFirstOptionMatching(OT_COAP_OPTION_OBSERVE) != NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (aMessage.GetType())
|
||||
{
|
||||
case OT_COAP_TYPE_RESET:
|
||||
@@ -520,22 +580,54 @@ void CoapBase::ProcessReceivedResponse(Message &aMessage, const Ip6::MessageInfo
|
||||
if (aMessage.IsEmpty())
|
||||
{
|
||||
// Empty acknowledgment.
|
||||
if (metadata.mConfirmable)
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (metadata.mObserve && !request->IsRequest())
|
||||
{
|
||||
metadata.mAcknowledged = true;
|
||||
metadata.UpdateIn(*request);
|
||||
// This is the ACK to our RFC7641 notification. There will be no
|
||||
// "separate" response so pass it back as if it were a piggy-backed
|
||||
// response so we can stop re-sending and the application can move on.
|
||||
FinalizeCoapTransaction(*request, metadata, &aMessage, &aMessageInfo, OT_ERROR_NONE);
|
||||
}
|
||||
|
||||
// Remove the message if response is not expected, otherwise await response.
|
||||
if (metadata.mResponseHandler == NULL)
|
||||
else
|
||||
#endif
|
||||
{
|
||||
DequeueMessage(*request);
|
||||
// This is not related to RFC7641 or the outgoing "request" was not a
|
||||
// notification.
|
||||
if (metadata.mConfirmable)
|
||||
{
|
||||
metadata.mAcknowledged = true;
|
||||
metadata.UpdateIn(*request);
|
||||
}
|
||||
|
||||
// Remove the message if response is not expected, otherwise await
|
||||
// response.
|
||||
if (metadata.mResponseHandler == NULL)
|
||||
{
|
||||
DequeueMessage(*request);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (aMessage.IsResponse() && aMessage.IsTokenEqual(*request))
|
||||
{
|
||||
// Piggybacked response.
|
||||
FinalizeCoapTransaction(*request, metadata, &aMessage, &aMessageInfo, OT_ERROR_NONE);
|
||||
// Piggybacked response. If there's an Observe option present in both
|
||||
// request and response, and we have a response handler; then we're
|
||||
// dealing with RFC7641 rules here.
|
||||
// (If there is no response handler, then we're wasting our time!)
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
if (metadata.mObserve && responseObserve && (metadata.mResponseHandler != NULL))
|
||||
{
|
||||
// This is a RFC7641 notification. The request is *not* done!
|
||||
metadata.mResponseHandler(metadata.mResponseContext, &aMessage, &aMessageInfo, OT_ERROR_NONE);
|
||||
|
||||
// Consider the message acknowledged at this point.
|
||||
metadata.mAcknowledged = true;
|
||||
metadata.UpdateIn(*request);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
FinalizeCoapTransaction(*request, metadata, &aMessage, &aMessageInfo, OT_ERROR_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
// Silently ignore acknowledgments carrying requests (RFC 7252, p. 4.2)
|
||||
@@ -545,13 +637,17 @@ void CoapBase::ProcessReceivedResponse(Message &aMessage, const Ip6::MessageInfo
|
||||
case OT_COAP_TYPE_CONFIRMABLE:
|
||||
// Send empty ACK if it is a CON message.
|
||||
SendAck(aMessage, aMessageInfo);
|
||||
FinalizeCoapTransaction(*request, metadata, &aMessage, &aMessageInfo, OT_ERROR_NONE);
|
||||
break;
|
||||
|
||||
// Fall through
|
||||
// Handling of RFC7641 and multicast is below.
|
||||
case OT_COAP_TYPE_NON_CONFIRMABLE:
|
||||
// Separate response.
|
||||
|
||||
if (metadata.mDestinationAddress.IsMulticast() && metadata.mResponseHandler != NULL)
|
||||
// Separate response or observation notification. If the request was to a multicast
|
||||
// address, OR both the request and response carry Observe options, then this is NOT
|
||||
// the final message, we may see multiples.
|
||||
if ((metadata.mResponseHandler != NULL) && (metadata.mDestinationAddress.IsMulticast()
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
|| (metadata.mObserve && responseObserve)
|
||||
#endif
|
||||
))
|
||||
{
|
||||
// If multicast non-confirmable request, allow multiple responses
|
||||
metadata.mResponseHandler(metadata.mResponseContext, &aMessage, &aMessageInfo, OT_ERROR_NONE);
|
||||
@@ -664,7 +760,10 @@ exit:
|
||||
}
|
||||
}
|
||||
|
||||
void CoapBase::Metadata::Init(bool aConfirmable,
|
||||
void CoapBase::Metadata::Init(bool aConfirmable,
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
bool aObserve,
|
||||
#endif
|
||||
const Ip6::MessageInfo &aMessageInfo,
|
||||
ResponseHandler aHandler,
|
||||
void * aContext,
|
||||
@@ -679,6 +778,9 @@ void CoapBase::Metadata::Init(bool aConfirmable,
|
||||
mRetransmissionTimeout = aTxParameters.CalculateInitialRetransmissionTimeout();
|
||||
mAcknowledged = false;
|
||||
mConfirmable = aConfirmable;
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
mObserve = aObserve;
|
||||
#endif
|
||||
mNextTimerShot =
|
||||
TimerMilli::GetNow() + (aConfirmable ? mRetransmissionTimeout : aTxParameters.CalculateMaxTransmitWait());
|
||||
}
|
||||
|
||||
@@ -525,7 +525,10 @@ protected:
|
||||
private:
|
||||
struct Metadata
|
||||
{
|
||||
void Init(bool aConfirmable,
|
||||
void Init(bool aConfirmable,
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
bool aObserve,
|
||||
#endif
|
||||
const Ip6::MessageInfo &aMessageInfo,
|
||||
ResponseHandler aHandler,
|
||||
void * aContext,
|
||||
@@ -545,6 +548,9 @@ private:
|
||||
uint8_t mRetransmissionsRemaining; // Number of retransmissions remaining.
|
||||
bool mAcknowledged : 1; // Information that request was acknowledged.
|
||||
bool mConfirmable : 1; // Information that message is confirmable.
|
||||
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
bool mObserve : 1; // Information that this request involves Observations.
|
||||
#endif
|
||||
};
|
||||
|
||||
static void HandleRetransmissionTimer(Timer &aTimer);
|
||||
|
||||
@@ -432,6 +432,23 @@ exit:
|
||||
return err;
|
||||
}
|
||||
|
||||
const otCoapOption *OptionIterator::GetFirstOptionMatching(otCoapOptionType aOption)
|
||||
{
|
||||
const otCoapOption *rval = NULL;
|
||||
|
||||
for (const otCoapOption *option = GetFirstOption(); option != NULL; option = GetNextOption())
|
||||
{
|
||||
if (option->mNumber == aOption)
|
||||
{
|
||||
// Found, stop searching
|
||||
rval = option;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return rval;
|
||||
}
|
||||
|
||||
const otCoapOption *OptionIterator::GetFirstOption(void)
|
||||
{
|
||||
const otCoapOption *option = NULL;
|
||||
@@ -449,6 +466,23 @@ const otCoapOption *OptionIterator::GetFirstOption(void)
|
||||
return option;
|
||||
}
|
||||
|
||||
const otCoapOption *OptionIterator::GetNextOptionMatching(otCoapOptionType aOption)
|
||||
{
|
||||
const otCoapOption *rval = NULL;
|
||||
|
||||
for (const otCoapOption *option = GetNextOption(); option != NULL; option = GetNextOption())
|
||||
{
|
||||
if (option->mNumber == aOption)
|
||||
{
|
||||
// Found, stop searching
|
||||
rval = option;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return rval;
|
||||
}
|
||||
|
||||
const otCoapOption *OptionIterator::GetNextOption(void)
|
||||
{
|
||||
otError error = OT_ERROR_NONE;
|
||||
@@ -532,6 +566,26 @@ exit:
|
||||
return rval;
|
||||
}
|
||||
|
||||
otError OptionIterator::GetOptionValue(uint64_t &aValue) const
|
||||
{
|
||||
otError error = OT_ERROR_NONE;
|
||||
uint8_t value[sizeof(aValue)];
|
||||
uint8_t pos = 0;
|
||||
|
||||
VerifyOrExit(mOption.mLength <= sizeof(aValue), error = OT_ERROR_NO_BUFS);
|
||||
SuccessOrExit(error = GetOptionValue(value));
|
||||
|
||||
aValue = 0;
|
||||
for (pos = 0; pos < mOption.mLength; pos++)
|
||||
{
|
||||
aValue <<= 8;
|
||||
aValue |= value[pos];
|
||||
}
|
||||
|
||||
exit:
|
||||
return error;
|
||||
}
|
||||
|
||||
otError OptionIterator::GetOptionValue(void *aValue) const
|
||||
{
|
||||
otError error = OT_ERROR_NONE;
|
||||
|
||||
@@ -639,6 +639,19 @@ public:
|
||||
*/
|
||||
otError Init(const Message *aMessage);
|
||||
|
||||
/**
|
||||
* This method returns a pointer to the first option matching the given option number.
|
||||
*
|
||||
* The internal option pointer is advanced until matching option is seen, if no matching
|
||||
* option is seen, the iterator will advance to the end of the options block.
|
||||
*
|
||||
* @param[in] aOption Option number to look for.
|
||||
*
|
||||
* @returns A pointer to the first matching option. If no option matching @p aOption is seen, NULL pointer is
|
||||
* returned.
|
||||
*/
|
||||
const otCoapOption *GetFirstOptionMatching(otCoapOptionType aOption);
|
||||
|
||||
/**
|
||||
* This method returns a pointer to the first option.
|
||||
*
|
||||
@@ -646,6 +659,19 @@ public:
|
||||
*/
|
||||
const otCoapOption *GetFirstOption(void);
|
||||
|
||||
/**
|
||||
* This method returns a pointer to the next option matching the given option number.
|
||||
*
|
||||
* The internal option pointer is advanced until matching option is seen, if no matching
|
||||
* option is seen, the iterator will advance to the end of the options block.
|
||||
*
|
||||
* @param[in] aOption Option number to look for.
|
||||
*
|
||||
* @returns A pointer to the next matching option (relative to current iterator position). If no option matching @p
|
||||
* aOption is seen, NULL pointer is returned.
|
||||
*/
|
||||
const otCoapOption *GetNextOptionMatching(otCoapOptionType aOption);
|
||||
|
||||
/**
|
||||
* This method returns a pointer to the next option.
|
||||
*
|
||||
@@ -653,9 +679,24 @@ public:
|
||||
*/
|
||||
const otCoapOption *GetNextOption(void);
|
||||
|
||||
/**
|
||||
* This function fills current option value into @p aValue. The option is assumed to be an unsigned integer.
|
||||
*
|
||||
* @param[out] aValue Buffer to store the option value.
|
||||
*
|
||||
* @retval OT_ERROR_NONE Successfully filled value.
|
||||
* @retval OT_ERROR_NOT_FOUND No more options, aIterator->mNextOptionOffset is set to offset of payload.
|
||||
* @retval OT_ERROR_NO_BUFS Value is too long to fit in a uint64_t.
|
||||
*
|
||||
*/
|
||||
otError GetOptionValue(uint64_t &aValue) const;
|
||||
|
||||
/**
|
||||
* This function fills current option value into @p aValue.
|
||||
*
|
||||
* @param[out] aValue Buffer to store the option value. This buffer is assumed to be sufficiently large
|
||||
* (see @ref otCoapOption::mLength).
|
||||
*
|
||||
* @retval OT_ERROR_NONE Successfully filled value.
|
||||
* @retval OT_ERROR_NOT_FOUND No more options, mNextOptionOffset is set to offset of payload.
|
||||
*
|
||||
|
||||
@@ -100,6 +100,16 @@
|
||||
#define OPENTHREAD_CONFIG_COAP_API_ENABLE 0
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @def OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
*
|
||||
* Define to 1 to enable the CoAP Observe (RFC7641) API.
|
||||
*
|
||||
*/
|
||||
#ifndef OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
|
||||
#define OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE 0
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @def OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE
|
||||
*
|
||||
|
||||
@@ -38,6 +38,7 @@ DEBUG ?= 0
|
||||
BORDER_AGENT ?= 1
|
||||
BORDER_ROUTER ?= 1
|
||||
COAP ?= 1
|
||||
COAP_OBSERVE ?= 1
|
||||
COAPS ?= 1
|
||||
COMMISSIONER ?= 1
|
||||
CHANNEL_MANAGER ?= 1
|
||||
|
||||
@@ -143,6 +143,7 @@ EXTRA_DIST = \
|
||||
sniffer.py \
|
||||
sniffer_transport.py \
|
||||
test_coap.py \
|
||||
test_coap_observe.py \
|
||||
test_coaps.py \
|
||||
test_common.py \
|
||||
test_crypto.py \
|
||||
@@ -164,6 +165,7 @@ check_PROGRAMS = \
|
||||
|
||||
check_SCRIPTS = \
|
||||
test_coap.py \
|
||||
test_coap_observe.py \
|
||||
test_coaps.py \
|
||||
test_common.py \
|
||||
test_crypto.py \
|
||||
@@ -284,6 +286,7 @@ TESTS = \
|
||||
|
||||
XFAIL_NCP_TESTS = \
|
||||
test_coaps.py \
|
||||
test_coap_observe.py \
|
||||
test_diag.py \
|
||||
test_ipv6_fragmentation.py \
|
||||
test_ipv6_source_selection.py \
|
||||
|
||||
@@ -460,6 +460,7 @@ def create_default_based_on_src_dst_ports_udp_payload_factory(master_key):
|
||||
19788: mle_message_factory,
|
||||
61631: coap_message_factory,
|
||||
1000: dtls_message_factory,
|
||||
5683: coap_message_factory,
|
||||
5684: dtls_message_factory,
|
||||
})
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import simulator
|
||||
import socket
|
||||
import time
|
||||
import unittest
|
||||
import binascii
|
||||
|
||||
|
||||
class Node:
|
||||
@@ -1015,6 +1016,182 @@ class Node:
|
||||
self.send_command(cmd)
|
||||
self._expect('Done')
|
||||
|
||||
def coap_cancel(self):
|
||||
"""
|
||||
Cancel a CoAP subscription.
|
||||
"""
|
||||
cmd = 'coap cancel'
|
||||
self.send_command(cmd)
|
||||
self._expect('Done')
|
||||
|
||||
def coap_delete(self, ipaddr, uri, con=False, payload=None):
|
||||
"""
|
||||
Send a DELETE request via CoAP.
|
||||
"""
|
||||
return self._coap_rq('delete', ipaddr, uri, con, payload)
|
||||
|
||||
def coap_get(self, ipaddr, uri, con=False, payload=None):
|
||||
"""
|
||||
Send a GET request via CoAP.
|
||||
"""
|
||||
return self._coap_rq('get', ipaddr, uri, con, payload)
|
||||
|
||||
def coap_observe(self, ipaddr, uri, con=False, payload=None):
|
||||
"""
|
||||
Send a GET request via CoAP with Observe set.
|
||||
"""
|
||||
return self._coap_rq('observe', ipaddr, uri, con, payload)
|
||||
|
||||
def coap_post(self, ipaddr, uri, con=False, payload=None):
|
||||
"""
|
||||
Send a POST request via CoAP.
|
||||
"""
|
||||
return self._coap_rq('post', ipaddr, uri, con, payload)
|
||||
|
||||
def coap_put(self, ipaddr, uri, con=False, payload=None):
|
||||
"""
|
||||
Send a PUT request via CoAP.
|
||||
"""
|
||||
return self._coap_rq('put', ipaddr, uri, con, payload)
|
||||
|
||||
def _coap_rq(self, method, ipaddr, uri, con=False, payload=None):
|
||||
"""
|
||||
Issue a GET/POST/PUT/DELETE/GET OBSERVE request.
|
||||
"""
|
||||
cmd = 'coap %s %s %s' % (method, ipaddr, uri)
|
||||
if con:
|
||||
cmd += ' con'
|
||||
else:
|
||||
cmd += ' non'
|
||||
|
||||
if payload is not None:
|
||||
cmd += ' %s' % payload
|
||||
|
||||
self.send_command(cmd)
|
||||
return self.coap_wait_response()
|
||||
|
||||
def coap_wait_response(self):
|
||||
"""
|
||||
Wait for a CoAP response, and return it.
|
||||
"""
|
||||
if isinstance(self.simulator, simulator.VirtualTime):
|
||||
self.simulator.go(5)
|
||||
timeout = 1
|
||||
else:
|
||||
timeout = 5
|
||||
|
||||
self._expect(
|
||||
r'coap response from ([\da-f:]+)(?: OBS=(\d+))?'
|
||||
r'(?: with payload: ([\da-f]+))?\b',
|
||||
timeout=timeout)
|
||||
(source, observe, payload) = self.pexpect.match.groups()
|
||||
source = source.decode('UTF-8')
|
||||
|
||||
if observe is not None:
|
||||
observe = int(observe, base=10)
|
||||
|
||||
if payload is not None:
|
||||
payload = binascii.a2b_hex(payload).decode('UTF-8')
|
||||
|
||||
# Return the values received
|
||||
return dict(source=source, observe=observe, payload=payload)
|
||||
|
||||
def coap_wait_request(self):
|
||||
"""
|
||||
Wait for a CoAP request to be made.
|
||||
"""
|
||||
if isinstance(self.simulator, simulator.VirtualTime):
|
||||
self.simulator.go(5)
|
||||
timeout = 1
|
||||
else:
|
||||
timeout = 5
|
||||
|
||||
self._expect(
|
||||
r'coap request from ([\da-f:]+)(?: OBS=(\d+))?'
|
||||
r'(?: with payload: ([\da-f]+))?\b',
|
||||
timeout=timeout)
|
||||
(source, observe, payload) = self.pexpect.match.groups()
|
||||
source = source.decode('UTF-8')
|
||||
|
||||
if observe is not None:
|
||||
observe = int(observe, base=10)
|
||||
|
||||
if payload is not None:
|
||||
payload = binascii.a2b_hex(payload).decode('UTF-8')
|
||||
|
||||
# Return the values received
|
||||
return dict(source=source, observe=observe, payload=payload)
|
||||
|
||||
def coap_wait_subscribe(self):
|
||||
"""
|
||||
Wait for a CoAP client to be subscribed.
|
||||
"""
|
||||
if isinstance(self.simulator, simulator.VirtualTime):
|
||||
self.simulator.go(5)
|
||||
timeout = 1
|
||||
else:
|
||||
timeout = 5
|
||||
|
||||
self._expect(r'Subscribing client\b', timeout=timeout)
|
||||
|
||||
def coap_wait_ack(self):
|
||||
"""
|
||||
Wait for a CoAP notification ACK.
|
||||
"""
|
||||
if isinstance(self.simulator, simulator.VirtualTime):
|
||||
self.simulator.go(5)
|
||||
timeout = 1
|
||||
else:
|
||||
timeout = 5
|
||||
|
||||
self._expect(
|
||||
r'Received ACK in reply to notification '
|
||||
r'from ([\da-f:]+)\b',
|
||||
timeout=timeout)
|
||||
(source,) = self.pexpect.match.groups()
|
||||
source = source.decode('UTF-8')
|
||||
|
||||
return source
|
||||
|
||||
def coap_set_resource_path(self, path):
|
||||
"""
|
||||
Set the path for the CoAP resource.
|
||||
"""
|
||||
cmd = 'coap resource %s' % path
|
||||
self.send_command(cmd)
|
||||
self._expect('Done')
|
||||
|
||||
def coap_set_content(self, content):
|
||||
"""
|
||||
Set the content of the CoAP resource.
|
||||
"""
|
||||
cmd = 'coap set %s' % content
|
||||
self.send_command(cmd)
|
||||
self._expect('Done')
|
||||
|
||||
def coap_start(self):
|
||||
"""
|
||||
Start the CoAP service.
|
||||
"""
|
||||
cmd = 'coap start'
|
||||
self.send_command(cmd)
|
||||
self._expect('Done')
|
||||
|
||||
def coap_stop(self):
|
||||
"""
|
||||
Stop the CoAP service.
|
||||
"""
|
||||
cmd = 'coap stop'
|
||||
self.send_command(cmd)
|
||||
|
||||
if isinstance(self.simulator, simulator.VirtualTime):
|
||||
self.simulator.go(5)
|
||||
timeout = 1
|
||||
else:
|
||||
timeout = 5
|
||||
|
||||
self._expect('Done', timeout=timeout)
|
||||
|
||||
def coaps_start_psk(self, psk, pskIdentity):
|
||||
cmd = 'coaps psk %s %s' % (psk, pskIdentity)
|
||||
self.send_command(cmd)
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2020, 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.
|
||||
#
|
||||
|
||||
import unittest
|
||||
|
||||
import pexpect
|
||||
import config
|
||||
import node
|
||||
|
||||
LEADER = 1
|
||||
ROUTER = 2
|
||||
|
||||
|
||||
class TestCoapObserve(unittest.TestCase):
|
||||
"""
|
||||
Test suite for CoAP Observations (RFC7641).
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Start up two nodes and get them on the virtual network.
|
||||
"""
|
||||
self.simulator = config.create_default_simulator()
|
||||
|
||||
self.nodes = {}
|
||||
for i in range(1, 3):
|
||||
self.nodes[i] = node.Node(i, simulator=self.simulator)
|
||||
|
||||
self.nodes[LEADER].set_panid(0xface)
|
||||
self.nodes[LEADER].set_mode('rsdn')
|
||||
self.nodes[LEADER].add_whitelist(self.nodes[ROUTER].get_addr64())
|
||||
self.nodes[LEADER].enable_whitelist()
|
||||
|
||||
self.nodes[ROUTER].set_panid(0xface)
|
||||
self.nodes[ROUTER].set_mode('rsdn')
|
||||
self.nodes[ROUTER].add_whitelist(self.nodes[LEADER].get_addr64())
|
||||
self.nodes[ROUTER].enable_whitelist()
|
||||
self.nodes[ROUTER].set_router_selection_jitter(1)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Tear down the nodes created.
|
||||
"""
|
||||
for n in list(self.nodes.values()):
|
||||
n.stop()
|
||||
n.destroy()
|
||||
self.simulator.stop()
|
||||
|
||||
def _do_notification_test(self, con):
|
||||
self.nodes[LEADER].start()
|
||||
self.simulator.go(5)
|
||||
self.assertEqual(self.nodes[LEADER].get_state(), 'leader')
|
||||
|
||||
self.nodes[ROUTER].start()
|
||||
self.simulator.go(5)
|
||||
self.assertEqual(self.nodes[ROUTER].get_state(), 'router')
|
||||
|
||||
mleid = self.nodes[LEADER].get_ip6_address(config.ADDRESS_TYPE.ML_EID)
|
||||
|
||||
self.nodes[LEADER].coap_start()
|
||||
self.nodes[LEADER].coap_set_resource_path('test')
|
||||
self.nodes[LEADER].coap_set_content('Test123')
|
||||
|
||||
self.nodes[ROUTER].coap_start()
|
||||
response = self.nodes[ROUTER].coap_observe(mleid, 'test', con=con)
|
||||
|
||||
first_observe = response['observe']
|
||||
self.assertIsNotNone(first_observe)
|
||||
self.assertEqual(response['payload'], 'Test123')
|
||||
self.assertEqual(response['source'], mleid)
|
||||
|
||||
# This should have been emitted already, so should return immediately
|
||||
self.nodes[LEADER].coap_wait_subscribe()
|
||||
|
||||
# Now change the content on the leader and wait for it to show up
|
||||
# on the router. We will do this a few times with a short delay.
|
||||
for n in range(0, 5):
|
||||
content = 'msg%d' % n
|
||||
|
||||
self.nodes[LEADER].coap_set_content(content)
|
||||
|
||||
response = self.nodes[ROUTER].coap_wait_response()
|
||||
self.assertGreater(response['observe'], first_observe)
|
||||
self.assertEqual(response['payload'], content)
|
||||
self.assertEqual(response['source'], mleid)
|
||||
|
||||
# Stop subscription
|
||||
self.nodes[ROUTER].coap_cancel()
|
||||
|
||||
# We should see the response, but with no Observe option
|
||||
response = self.nodes[ROUTER].coap_wait_response()
|
||||
self.assertIsNone(response['observe'])
|
||||
# Content won't have changed.
|
||||
self.assertEqual(response['payload'], content)
|
||||
|
||||
# Make another change, no notification should be sent
|
||||
self.nodes[LEADER].coap_set_content('LastNote')
|
||||
|
||||
# This should time out!
|
||||
try:
|
||||
self.nodes[ROUTER].coap_wait_response()
|
||||
self.fail('Should not have received notification')
|
||||
except pexpect.exceptions.TIMEOUT:
|
||||
pass
|
||||
|
||||
self.nodes[ROUTER].coap_stop()
|
||||
self.nodes[LEADER].coap_stop()
|
||||
|
||||
def test_con(self):
|
||||
"""
|
||||
Test notification using CON messages.
|
||||
"""
|
||||
for trial in range(0, 3):
|
||||
try:
|
||||
self._do_notification_test(con=True)
|
||||
break
|
||||
except (AssertionError, pexpect.exceptions.TIMEOUT):
|
||||
continue
|
||||
|
||||
def test_non(self):
|
||||
"""
|
||||
Test notification using NON messages.
|
||||
"""
|
||||
for trial in range(0, 3):
|
||||
try:
|
||||
self._do_notification_test(con=False)
|
||||
break
|
||||
except (AssertionError, pexpect.exceptions.TIMEOUT):
|
||||
continue
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user