diff --git a/examples/Makefile-simulation b/examples/Makefile-simulation
index 2ee983eed..0a71ab0e5 100644
--- a/examples/Makefile-simulation
+++ b/examples/Makefile-simulation
@@ -38,6 +38,7 @@ DEBUG ?= 0
BORDER_AGENT ?= 1
BORDER_ROUTER ?= 1
COAP ?= 1
+COAP_OBSERVE ?= 1
COAPS ?= 1
COMMISSIONER ?= 1
CHANNEL_MANAGER ?= 1
diff --git a/examples/common-switches.mk b/examples/common-switches.mk
index c5506469e..e8e7dd646 100644
--- a/examples/common-switches.mk
+++ b/examples/common-switches.mk
@@ -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
diff --git a/include/openthread/coap.h b/include/openthread/coap.h
index 22ba3d247..8bbea6291 100644
--- a/include/openthread/coap.h
+++ b/include/openthread/coap.h
@@ -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.
*
diff --git a/src/cli/README_COAP.md b/src/cli/README_COAP.md
index d335ce18a..209746b9c 100644
--- a/src/cli/README_COAP.md
+++ b/src/cli/README_COAP.md
@@ -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 \
\ \[type\] \[payload\]
* address: IPv6 address of the CoAP server.
@@ -107,6 +123,20 @@ Done
Done
```
+### observe \ \ \[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 \ \["default"| \]
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.
diff --git a/src/cli/cli_coap.cpp b/src/cli/cli_coap.cpp
index 2828a32b5..106d7a677 100644
--- a/src/cli/cli_coap.cpp
+++ b/src/cli/cli_coap.cpp
@@ -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(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(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(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(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(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);
}
}
diff --git a/src/cli/cli_coap.hpp b/src/cli/cli_coap.hpp
index b3d84b046..2029f2829 100644
--- a/src/cli/cli_coap.hpp
+++ b/src/cli/cli_coap.hpp
@@ -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
diff --git a/src/core/api/coap_api.cpp b/src/core/api/coap_api.cpp
index 9844ead9e..0c8dce530 100644
--- a/src/core/api/coap_api.cpp
+++ b/src/core/api/coap_api.cpp
@@ -180,16 +180,32 @@ otError otCoapOptionIteratorInit(otCoapOptionIterator *aIterator, const otMessag
return static_cast(aIterator)->Init(static_cast(aMessage));
}
+const otCoapOption *otCoapOptionIteratorGetFirstOptionMatching(otCoapOptionIterator *aIterator,
+ otCoapOptionType aOption)
+{
+ return static_cast(aIterator)->GetFirstOptionMatching(aOption);
+}
+
const otCoapOption *otCoapOptionIteratorGetFirstOption(otCoapOptionIterator *aIterator)
{
return static_cast(aIterator)->GetFirstOption();
}
+const otCoapOption *otCoapOptionIteratorGetNextOptionMatching(otCoapOptionIterator *aIterator, otCoapOptionType aOption)
+{
+ return static_cast(aIterator)->GetNextOptionMatching(aOption);
+}
+
const otCoapOption *otCoapOptionIteratorGetNextOption(otCoapOptionIterator *aIterator)
{
return static_cast(aIterator)->GetNextOption();
}
+otError otCoapOptionIteratorGetOptionUintValue(otCoapOptionIterator *aIterator, uint64_t *const aValue)
+{
+ return static_cast(aIterator)->GetOptionValue(*aValue);
+}
+
otError otCoapOptionIteratorGetOptionValue(otCoapOptionIterator *aIterator, void *aValue)
{
return static_cast(aIterator)->GetOptionValue(aValue);
diff --git a/src/core/coap/coap.cpp b/src/core/coap/coap.cpp
index f700df8cd..3ab3f1cc2 100644
--- a/src/core/coap/coap.cpp
+++ b/src/core/coap/coap.cpp
@@ -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());
}
diff --git a/src/core/coap/coap.hpp b/src/core/coap/coap.hpp
index bde7aa62a..259815196 100644
--- a/src/core/coap/coap.hpp
+++ b/src/core/coap/coap.hpp
@@ -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);
diff --git a/src/core/coap/coap_message.cpp b/src/core/coap/coap_message.cpp
index a32722a5d..00b115512 100644
--- a/src/core/coap/coap_message.cpp
+++ b/src/core/coap/coap_message.cpp
@@ -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;
diff --git a/src/core/coap/coap_message.hpp b/src/core/coap/coap_message.hpp
index 236ef8483..5bf5b1d12 100644
--- a/src/core/coap/coap_message.hpp
+++ b/src/core/coap/coap_message.hpp
@@ -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.
*
diff --git a/src/core/config/coap.h b/src/core/config/coap.h
index 6daa7c7fe..5484d02d2 100644
--- a/src/core/config/coap.h
+++ b/src/core/config/coap.h
@@ -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
*
diff --git a/src/posix/Makefile-posix b/src/posix/Makefile-posix
index 2c9a4d7b0..fbfc9d9d8 100644
--- a/src/posix/Makefile-posix
+++ b/src/posix/Makefile-posix
@@ -38,6 +38,7 @@ DEBUG ?= 0
BORDER_AGENT ?= 1
BORDER_ROUTER ?= 1
COAP ?= 1
+COAP_OBSERVE ?= 1
COAPS ?= 1
COMMISSIONER ?= 1
CHANNEL_MANAGER ?= 1
diff --git a/tests/scripts/thread-cert/Makefile.am b/tests/scripts/thread-cert/Makefile.am
index 72db53ff3..908d2dad0 100644
--- a/tests/scripts/thread-cert/Makefile.am
+++ b/tests/scripts/thread-cert/Makefile.am
@@ -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 \
diff --git a/tests/scripts/thread-cert/config.py b/tests/scripts/thread-cert/config.py
index ad05ab444..fb973aaa0 100644
--- a/tests/scripts/thread-cert/config.py
+++ b/tests/scripts/thread-cert/config.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,
})
diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py
index 0d2cce5d9..d04f8e5af 100755
--- a/tests/scripts/thread-cert/node.py
+++ b/tests/scripts/thread-cert/node.py
@@ -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)
diff --git a/tests/scripts/thread-cert/test_coap_observe.py b/tests/scripts/thread-cert/test_coap_observe.py
new file mode 100755
index 000000000..bec9d57a1
--- /dev/null
+++ b/tests/scripts/thread-cert/test_coap_observe.py
@@ -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()