Description
- Update the Python module named
AsyncCoapClientConnector with support for GET requests, using the existing method definitions within IRequestResponseHandler to support this request type.
- NOTE: These instructions make use of the following CoAP library:
- The aiocoap open source CoAP library, located at: aiocoap. Reference: Amsüss, Christian and Wasilak, Maciej. aiocoap: Python CoAP Library. Energy Harvesting Solutions, 2013–. http://github.com/chrysn/aiocoap/.
Review the README
- Please see README.md for further information on, and use of, this content.
- License for embedded documentation and source codes: PIOT-DOC-LIC
Estimated effort may vary greatly
- The estimated level of effort for this exercise shown in the 'Estimate' section below is a very rough approximation. The actual level of effort may vary greatly depending on your development and test environment, experience with the requisite technologies, and many other factors.
Actions
Step 1: Create and implement a callable GET request method
- Implement the public
sendGetRequest() method
- This method will take the following arguments:
- resource (
ResourceNameEnum): Used to create the general request URL
- name (
str): Used to extend the general request URL with further detail (if warranted)
- enableCON (
bool): If True, use a CONFIRMABLE request; else, use NONCONFIRMABLE
- A general implementation may look like the following:
def sendGetRequest(self, resource: ResourceNameEnum = None, name: str = None, enableCON: bool = False, timeout: int = IRequestResponseClient.DEFAULT_TIMEOUT) -> bool:
if resource or name:
resourcePath = self._createResourcePath(resource, name)
logging.info(f"Issuing Async GET to path: {resourcePath}")
future = asyncio.run_coroutine_threadsafe(
self._handleGetRequest(resourcePath, enableCON),
self._eventLoopThread
)
return future.result()
else:
logging.warning("Can't issue Async GET - no path provided.")
Step 2: Create and implement the internal GET request callback method and its response handler
- Implement the internal
_onGetResponse() method
- This method will take the following arguments:
- response (``): This will be the CoAPthon3 response from the request
- resourcePath (
str): This will be the full resource path used for the request
- A genera implementation may look like the following:
async def _handleGetRequest(self, resourcePath: str = None, enableCON: bool = False):
try:
uriAndResourcePath = self.uriPath + resourcePath
msgType = NON
if enableCON:
msgType = CON
msg = Message(mtype = msgType, code = Code.GET, uri = uriAndResourcePath)
responseData = await self.clientContext.request(request_message = msg).response
self._onGetResponse(responseData)
except Exception as e:
# NOTE: for debugging, you may want to optionally include the stack trace, as shown
logging.warning(f"Failed to process Async GET request for path: {uriAndResourcePath}")
traceback.print_exception(type(e), e, e.__traceback__)
- Implement the internal
_onGetResponse() method
- This method will process the incoming response from the CoAP server, parse the payload, determine which data type is part of the payload, and react accordingly.
- It will take the following argument:
- response: This will be the response object from the request
- A general implementation may look like the following:
def _onGetResponse(self, response):
if not response:
logging.warning("Async GET response invalid. Ignoring.")
return
logging.info("Async GET response received.")
jsonData = response.payload.decode("utf-8")
if len(response.requested_path) >= 2:
logging.info(f"Response: {response.requested_path}")
dataType = response.requested_path[1]
if dataType == ConfigConst.ACTUATOR_CMD:
# NOTE: convert payload to ActuatorData and verify!
logging.info(f"ActuatorData received: {jsonData}")
try:
ad = DataUtil().jsonToActuatorData(jsonData)
if self.dataMsgListener:
self.dataMsgListener.handleActuatorCommandMessage(ad)
except:
logging.warning(f"Failed to decode actuator data. Ignoring: : {jsonData}")
return
else:
logging.info(f"Response data received. Payload: : {jsonData}")
else:
logging.info(f"Response data received. Payload: : {jsonData}")
Step 3: Implement the resource DISCOVERY request
NOTE 1: For more information on CoAP Discovery, see RFC7252 and its reference of RFC6690.
NOTE 2: You may recall from the book content that a Discovery in CoAP is a specialized 'GET' that requests the data at the resource path .well-known/core from the server. The examples below follow this pattern (Option 2 from earlier), although you can also use the client library's discovery API if preferred.
- Implement the
sendDiscoveryRequest() method.
- This will issue a discovery request to the server, which will provide the list of resource names (fully qualified) that are currently registered with the server.
- One option for implementing a CoAP discovery request is to simply use the 'well-known' path as part of a typical GET request:
.well-known/core.
- Here's a general implementation approach for this method:
def sendDiscoveryRequest(self, timeout: int = IRequestResponseClient.DEFAULT_TIMEOUT) -> bool:
logging.info("Discovering remote resources...")
resourcePath = self._createResourcePath(None, ".well-known/core")
logging.info(f"Issuing Async GET - DISCOVERY to path: {resourcePath}")
future = asyncio.run_coroutine_threadsafe(
self._handleDiscoveryRequest(resourcePath),
self._eventLoopThread
)
return future.result()
Step 4: Create and implement the internal DISCOVERY request callback method and response handler
- Implement the internal
_handleDiscoveryRequest() and _onDiscoveryResponse() methods
- These will look similar to the GET helper methods, except - for now - they'll just log the output to the console, since there's no other action to take.
async def _handleDiscoveryRequest(self, resourcePath: str = None, enableCON: bool = False):
try:
uriAndResourcePath = self.uriPath + resourcePath
msgType = NON
if enableCON:
msgType = CON
msg = Message(mtype = msgType, code = Code.GET, uri = uriAndResourcePath)
responseData = await self.clientContext.request(request_message = msg).response
self._onDiscoveryResponse(responseData)
def _onDiscoveryResponse(self, response):
if not response:
logging.warning("Async GET - DISCOVERY response invalid. Ignoring.")
return
logging.info("Async GET - DISCOVERY response received.")
if len(response.requested_path) >= 2:
logging.info("Resources: " + str(response.payload))
else:
logging.info("Response: " + str(response))
Tests
-
Edit / add test cases
- Update the
test_CoapAsyncClientConnectorTest.py module (containing the CoapAsyncClientConnectorTest class) in CDA_HOME/tests/integration/connection as indicated below.
-
If not already done for you within the code, disable the existing tests by UNCOMMENTING the skip test annotation before each test case (change #@unittest.skip("Ignore for now.") to @unittest.skip("Ignore for now.")).
-
If not already done for you within the code, create a two tests to retrieve the latest ActuatorData from the CoAP server using both CON and NON requests. For now, leave them disabled.
-
The code for each should look similar to the following:
@unittest.skip("Ignore for now.")
def testGetActuatorCommandCon(self):
self.coapClient.sendGetRequest(resource = ResourceNameEnum.CDA_ACTUATOR_CMD_RESOURCE, enableCON = True, timeout = 5)
@unittest.skip("Ignore for now.")
def testGetActuatorCommandNon(self):
self.coapClient.sendGetRequest(resource = ResourceNameEnum.CDA_ACTUATOR_CMD_RESOURCE, enableCON = False, timeout = 5)
-
Setup
- Start Wireshark, and ensure it's watching the loopback adapter.
- You can filter on 'coap' to track only CoAP messages, which is recommended for this test.
- Start your GDA application with the CoAP server enabled. Make sure it runs for a couple of minutes - long enough for you to run the integration tests listed below.
- To run your GDA from the command line, you just need to do the following from a shell (assuming you're starting from the parent directory containing your java-components, and that the path is named 'piot-java-components':
- NOTE: Be sure to run your GDA Java app for a few minutes so you have time to run the tests!!
cd piot-java-components
mvn clean install -DskipTests
java -jar target/gateway-device-app-0.0.1-jar-with-dependencies.jar
- Run the Discovery test
- Make sure all tests in
AsyncCoapClientConnectorTest EXCEPT for the one named testConnectAndDiscover() are disabled. You can do this by ensuring the annotation before all the other methods @unittest.skip("Ignore for now.") is NOT commented out, and the annotation before testConnectoAndDiscover()` IS commented out.
- Run the
CoapClientConnectorTest from within your IDE or the command line. Your output will contain log content similar to the following:
- Run the GET Actuator Command tests
- Enable the
testGetActuatorCommandCon() and testGetActuatorCommandNon() tests, and disable the others as indicated in the previous instructions.
- Run the tests and watch the output in the console for the client as well as within Wireshark.
- NOTE: If you're a student in the Connected Devices course, be sure to follow the instructions in PIOT-INF-09-003.
- Notice that you'll get NO data in return to the request. This is because the initial implementation of the GDA's CoAP server and the
GetActuatorCommandResourceHandler class (which will respond to your GET request) doesn't create an ActuatorData instance. You can leave this test until the GDA section of this lab module, or implement it now. If you choose to run this test now, do the following:
- Follow the instructions for the GDA in PIOT-GDA-09-000 to ensure your working in the correct branch.
- Update your GDA's
GetActuatorCommandResourceHandler to return a test ActuatorData instance in your handler's GET implementation, re-compile and re-start your GDA, and run the tests again.
- If all goes well, your test output may look similar to the following:
Description
AsyncCoapClientConnectorwith support for GET requests, using the existing method definitions withinIRequestResponseHandlerto support this request type.Review the README
Estimated effort may vary greatly
Actions
Step 1: Create and implement a callable GET request method
sendGetRequest()methodResourceNameEnum): Used to create the general request URLstr): Used to extend the general request URL with further detail (if warranted)bool): If True, use a CONFIRMABLE request; else, use NONCONFIRMABLEStep 2: Create and implement the internal GET request callback method and its response handler
_onGetResponse()methodstr): This will be the full resource path used for the request_onGetResponse()methodStep 3: Implement the resource DISCOVERY request
NOTE 1: For more information on CoAP Discovery, see RFC7252 and its reference of RFC6690.
NOTE 2: You may recall from the book content that a Discovery in CoAP is a specialized 'GET' that requests the data at the resource path
.well-known/corefrom the server. The examples below follow this pattern (Option 2 from earlier), although you can also use the client library's discovery API if preferred.sendDiscoveryRequest()method..well-known/core.Step 4: Create and implement the internal DISCOVERY request callback method and response handler
_handleDiscoveryRequest()and_onDiscoveryResponse()methodsTests
Edit / add test cases
test_CoapAsyncClientConnectorTest.pymodule (containing theCoapAsyncClientConnectorTestclass) in CDA_HOME/tests/integration/connection as indicated below.If not already done for you within the code, disable the existing tests by UNCOMMENTING the skip test annotation before each test case (change
#@unittest.skip("Ignore for now.")to@unittest.skip("Ignore for now.")).If not already done for you within the code, create a two tests to retrieve the latest
ActuatorDatafrom the CoAP server using both CON and NON requests. For now, leave them disabled.The code for each should look similar to the following:
Setup
AsyncCoapClientConnectorTestEXCEPT for the one namedtestConnectAndDiscover()are disabled. You can do this by ensuring the annotation before all the other methods@unittest.skip("Ignore for now.") is NOT commented out, and the annotation beforetestConnectoAndDiscover()` IS commented out.CoapClientConnectorTestfrom within your IDE or the command line. Your output will contain log content similar to the following:testGetActuatorCommandCon()andtestGetActuatorCommandNon()tests, and disable the others as indicated in the previous instructions.GetActuatorCommandResourceHandlerclass (which will respond to your GET request) doesn't create anActuatorDatainstance. You can leave this test until the GDA section of this lab module, or implement it now. If you choose to run this test now, do the following:GetActuatorCommandResourceHandlerto return a testActuatorDatainstance in your handler's GET implementation, re-compile and re-start your GDA, and run the tests again.