Skip to content

PIOT-CDA-09-002-A: Add GET and DISCOVERY functionality to AsyncCoapClientConnector #213

@labbenchstudios

Description

@labbenchstudios

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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Lab Module 09 - CoAP Clients

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions