Coder Social home page Coder Social logo

insighio / microcoapy Goto Github PK

View Code? Open in Web Editor NEW
57.0 10.0 11.0 70 KB

A mini client/server implementation of CoAP (Constrained Application Protocol) into MicroPython

License: Apache License 2.0

Python 100.00%
coap micropython server client pycom coap-server coap-client esp32 esp8266

microcoapy's Introduction

microCoAPy

A mini implementation of CoAP (Constrained Application Protocol) into MicroPython

The main difference compared to the established Python implementations aiocoap and CoAPthon is its size and complexity since this library will be used on microcontrollers that support MicroPython such as: Pycom devices, ESP32, ESP8266.

The first goal of this implementation is to provide basic functionality to send and receive data. DTLS and/or any special features of CoAP as defined in the RFC's, will be examined and implemented in the future.

Table of contents

Tested boards

  • Pycom: all Pycom boards
  • ESP32
  • ESP8266

Supported operations

CoAP client

  • PUT
  • POST
  • GET

Example of usage

Here is an example using the CoAP client functionality to send requests and receive responses. (this example is part of examples/pycom_wifi_coap_client.py)

import microcoapy
# your code to connect to the network
#...
def receivedMessageCallback(packet, sender):
        print('Message received:', packet.toString(), ', from: ', sender)
        print('Message payload: ', packet.payload.decode('unicode_escape'))

client = microcoapy.Coap()
client.responseCallback = receivedMessageCallback
client.start()

_SERVER_IP="192.168.1.2"
_SERVER_PORT=5683
bytesTransferred = client.get(_SERVER_IP, _SERVER_PORT, "current/measure")
print("[GET] Sent bytes: ", bytesTransferred)

client.poll(2000)

client.stop()

Code explained

Lets examine the above code and explain its purpose.

def receivedMessageCallback(packet, sender):
        print('Message received:', packet.toString(), ', from: ', sender)
        print('Message payload: ', packet.payload.decode('unicode_escape'))

client = microcoapy.Coap()
client.responseCallback = receivedMessageCallback

During this step, the CoAP object get initialized. A callback handler is also created to get notifications from the server regarding our requests. It is not used for incoming requests.

When instantiating new Coap object, a custom port can be optionally configured: client = microcoapy.Coap(5683).

client.start()

The call to the start function is where the UDP socket gets created. By default it gets bind to the default CoAP port 5683. If a custom port is required, pass it as argument to the start function.

bytesTransferred = client.get(_SERVER_IP, _SERVER_PORT, "current/measure")
print("[GET] Sent bytes: ", bytesTransferred)

Having the socket ready, it is time to send our request. In this case we send a simple GET request to the specific address (ex. 192.168.1.2:5683). The get function returns the number of bytes that have been sent. So in case of error, 0 will be returned.

client.poll(2000)

Since a GET request has been posted, most likely it would be nice to receive and process the server response. For this reason we call poll function that will try to read incoming messages for 2000 milliseconds. Upon timeout the execution will continue to the next command.

If a packet gets received during that period of type that is an ACK to our request or a report (ex. 404), the callback that has been registered at the beginning will be called.

client.stop()

Finally, stop is called to gracefully close the socket. It is preferable to have a corresponding call of stop to each call of start function because in special cases such as when using mobile modems, the modem might stuck when running out of available sockets.

To send POST or PUT message replace the call of get function with:

bytesTransferred = client.put(_SERVER_IP, _SERVER_PORT, "led/turnOn", "test",
                                 None, microcoapy.COAP_CONTENT_FORMAT.COAP_TEXT_PLAIN)

or

bytesTransferred = client.post(_SERVER_IP, _SERVER_PORT, "led/turnOn", "test",
                                 None, microcoapy.COAP_CONTENT_FORMAT.COAP_TEXT_PLAIN)

For details on the arguments please advice the documentation.

CoAP server

Starts a server and calls custom callbacks upon receiving an incoming request. The response needs to be defined by the user of the library.

Example of usage

Here is an example using the CoAP server functionality to receive requests and respond back. (this example is part of examples/pycom_wifi_coap_server.py)

import microcoapy
# your code to connect to the network
#...
client = microcoapy.Coap()

def measureCurrent(packet, senderIp, senderPort):
    print('Measure-current request received: ', packet.toString(), ', from: ', senderIp, ":", senderPort)
    client.sendResponse(senderIp, senderPort, packet.messageid,
                      "222", microcoapy.COAP_RESPONSE_CODE.COAP_CONTENT,
                      microcoapy.COAP_CONTENT_FORMAT.COAP_NONE, packet.token)

client.addIncomingRequestCallback('current/measure', measureCurrent)

client.start()

# wait for incoming request for 60 seconds
timeoutMs = 60000
start_time = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), start_time) < timeoutMs:
    client.poll(60000)

client.stop()

Code explained

Lets examine the above code and explain its purpose. For details on start and stop functions advice the previous paragraph of the client example.

def measureCurrent(packet, senderIp, senderPort):
    print('Measure-current request received: ', packet.toString(), ', from: ', senderIp, ":", senderPort)
    client.sendResponse(senderIp, senderPort, packet.messageid,
                      "222", microcoapy.COAP_RESPONSE_CODE.COAP_CONTENT,
                      microcoapy.COAP_CONTENT_FORMAT.COAP_NONE, packet.token)

client.addIncomingRequestCallback('current/measure', measureCurrent)

This is the main step to prepare the CoAP instance to behave as a server: receive and handle requests. First we create a function measureCurrent that takes as arguments the incoming packet, the sender IP and Port. This function will be used as a callback and will be triggered every time a specific URI path is provided in the incoming request.

This URL is defined upon registering the callback to the CoAP instance by calling addIncomingRequestCallback function. After this call, if a CoAP GET/PUT/POST packet is received with URI path: coap:///current/measure , the callback will be triggered.

By default, the server does not send any response. This is a responsibility of the user to send (if needed) the appropriate response.

In this example, we reply with a response message packet (which has the same message id as the incoming request packet) whose payload is the actual value of the reading that has just been executed (in the example it is a hard-coded value of 222).

timeoutMs = 60000
start_time = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), start_time) < timeoutMs:
    client.poll(60000)

Finally, since the functions loop and poll can handle a since packet per run, we wrap its call to a while loop and wait for incoming messages.

Custom sockets

By using default functions microcoapy.Coap().start() and microcoapy.Coap().stop() the Coap library handles the creation of a UDP socket from usocket module at the default port 5683 (if no other is defined when Coap object gets instantiated).

If this socket type is not the appropriate for your project, custom socket instances can be used instead.

Lets consider the case of supporting an external GSM modem connected via Serial on the board and that there is no direct support of this modem from default modules like network.LTE. In this case there is no guarranty that a typical UDP socket from usocket module will be functional. Thus, a custom socket instance needs to be created.

The custom socket needs to implement the functions:

  • sendto(self, bytes, address) : returns the number of bytes transmitted
  • recvfrom(self, bufsize): returns a byte array
  • setblocking(self, flag)

Example:

## Custom socket implementation
class CustomSocket:
    def __init__(self):
        print("CustomSocket: init")

    def sendto(self, bytes, address):
        print("CustomSocket: Sending bytes to: " + str(address))
        return len(bytes)

    def recvfrom(self, bufsize):
        print("CustomSocket: receiving max bytes: " + bufsize)
        return b"test data"

    def setblocking(self, flag):
        print(".", end="")

After creating the custom socket, it is utilized by the Coap instance after calling microcoapy.Coap.setCustomSocket(customSocket).

Example:

client = microcoapy.Coap()
# setup callback for incoming response to a request
client.responseCallback = receivedMessageCallback

# Initialize custom socket
customSocket = CustomSocket()

# Use custom socket to all operations of CoAP
client.setCustomSocket(customSocket)

Pycom custom socket based on AT commands

Since most of the implementations of NBIoT networks are based on IPv6, it was essential to move to a custom implementation of UDP socket, as Pycom do not yet support natively IPv6 sockets. Thus, in examples/pycom/nbiot/pycom_at_socket.py you can find a complete implementation of a sample socket that directly uses Sequans AT commands.

NOTE: The socket to work without limitations needs one of the following Pycom firmwares:

Beta features under implementation or evaluation

Discard incoming retransmission

If a received message is the same as the previously message received, it can be discarded. In that case, the poll function will not return at the time of retrieval and will continue to listen for futher incomming messages. Finally the defined responseCallback will not be called.

By default this simplistic feature is disabled. To enable:

client = microcoapy.Coap()
client.discardRetransmissions = True

Activate debug messages

By default, debug prints in microcoapy are enabled. Though, the user can deactivate the prints per Coap instance:

client = microcoapy.Coap()
client.debug = False

Future work

  • Since this library is quite fresh, the next period will be full of testing.
  • enhancments on funtionality as needed

Issues and contributions

It would be our pleasure to receive comments, bug reports and/or contributions. To do this use the Issues and Pull requests of GitHub.

microcoapy's People

Contributors

frederic-bonjour avatar ftylitak avatar przemobe avatar talkinpotato avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

microcoapy's Issues

resposeCallback not called for NONCON requests

The default options for methods get, post, delete are set to CON (confirmable) in the implementation of the microCoAPy.

To send NONCON (Non-Confirmable) message someone needs to use the generic function microcoapy.Coap.send().

#example
requestId = client.send(_SERVER_IP, _SERVER_PORT, url,  microcoapy.coap_macros.COAP_TYPE.COAP_NONCON,  microcoapy.coap_macros.COAP_METHOD.COAP_GET, bytearray(), None,  microcoapy.coap_macros.COAP_CONTENT_FORMAT.COAP_NONE, None)
    

When this function is used with NONCON flag, the response Callback is never called because of the following lines in the microcoapy.py

if packet.type == macros.COAP_TYPE.COAP_ACK or\
packet.method == macros.COAP_RESPONSE_CODE.COAP_NOT_FOUND:
if self.resposeCallback is not None:
self.resposeCallback(packet, remoteAddress)
else:
self.handleIncomingRequest(packet, remoteAddress[0], remoteAddress[1])

Needs to be addressed.

MessageID

Hi,
I am using your lib as a client for a small project of mine --> https://github.com/Trifunik/condensation_alert.
Do you know any elegant way to get the coapPacket.messageid from the get function?
I want to be sure that the payload is the response to the wright GET request, because I have sometimes a race condition between two GET requests.

Many thanks in advance!
Nikola

Question about Coap.handleIncomingRequest

Hello,

I'd like to ask if there is any scenario that handleIncomingRequest is called for client side?
It seems that function purpose is to handle requests (eg. GET) on server side. Am I right?
However handleIncomingRequest is checking self.responseCallback which is for client side.

If Coap can act as an server and client in a single instance (can't it?) and if self.responseCallback is set and if requested url is unknown (not in self.callbacks) then the function returns False and sendResponse indication "not found" is not called.

        if urlCallback is None:
            if self.responseCallback:
                # The incoming request may be a response, let the responseCallback handle it.
                return False
            print('Callback for url [', url, "] not found")
            self.sendResponse(sourceIp, sourcePort, requestPacket.messageid,
                              None, macros.COAP_RESPONSE_CODE.COAP_NOT_FOUND,
                              macros.COAP_CONTENT_FORMAT.COAP_NONE, requestPacket.token)

So my point is that either handleIncomingRequest shall not be called for non requests (ie. GET) or above logic shall be changed to exclude server requests:

            if self.responseCallback and macros.COAP_METHOD.COAP_GET != requestPacket.method:
                # The incoming request may be a response, let the responseCallback handle it.
                return False

Please share your thoughts.

Best regards

INSTALLATION

hi have some issue installing external package on micropython.
Can yoyu give some int ?

OOP

I saw a method sendPacket in the code, I wanted to use it, but I had to look at the code further, it turns out this method has privacy. Do you think this is a bad method to use? ๐Ÿค”

Installation issues - no setup.py nor pyproject.toml

Hello!

In an attempt to test and play around with CoAP on the ESP32 I stumbled across this project.

However, when trying to use the example files my IDEs are all unable to obtain or install the library, stating a missing setup.py or "pyproject.toml".

I have attempted to use both Thonny, vscode, and straight CLI.

Would you be able to look at the issue or provide installation instructions?

Thank you!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.