Simple CoAP Client Example#

The Simple CoAP Client example demonstrates how to create a minimal CoAP client application to send and receive CoAP messages.

To use the application, you need to specify a CoAP server. The documentation of the equivalent C++ example lists some options.

Simple CoAP Client Example Screenshot

Download this example

# Copyright (C) 2020 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import sys

from PySide6.QtWidgets import QApplication
from mainwindow import MainWindow

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()

    sys.exit(app.exec())
# Copyright (C) 2020 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtCore import QByteArray, QDateTime, QFile, QIODevice, QUrl, Qt, Slot
from PySide6.QtNetwork import QNetworkInterface, QHostInfo
from PySide6.QtWidgets import QMainWindow, QMessageBox, QDialog, QFileDialog
from PySide6.QtCoap import QCoapClient, QtCoap, QCoapMessage, QCoapRequest

from ui_mainwindow import Ui_MainWindow as ui
from optiondialog import OptionDialog


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self._ui = ui()
        self._client = QCoapClient()
        self._options = []  # QVector<QCoapOption>
        self._currentData = ""
        self._client = QCoapClient(QtCoap.SecurityMode.NoSecurity, self)

        self._client.finished.connect(self.onFinished)
        self._client.error.connect(self.onError)

        self._ui.setupUi(self)

        self._ui.methodComboBox.addItem("Get", QtCoap.Method.Get)
        self._ui.methodComboBox.addItem("Put", QtCoap.Method.Put)
        self._ui.methodComboBox.addItem("Post", QtCoap.Method.Post)
        self._ui.methodComboBox.addItem("Delete", QtCoap.Method.Delete)

        self.fillHostSelector()
        self._ui.hostComboBox.setFocus()

    @Slot()
    def fillHostSelector(self):
        networkInterfaces = QNetworkInterface.allInterfaces()
        for interface in networkInterfaces:
            for address in interface.addressEntries():
                self._ui.hostComboBox.addItem(address.ip().toString())

    @Slot()
    def addMessage(self, message, isError=False):
        try:
            _message = message.data().decode("utf-8")
        except:  # noqa: E722
            _message = message
        time = QDateTime.currentDateTime().toString()
        content = f"--------------- {time} ---------------\n{_message}\n\n\n"
        self._ui.textEdit.setTextColor(Qt.red if isError else Qt.black)
        self._ui.textEdit.insertPlainText(content)
        self._ui.textEdit.ensureCursorVisible()

    @Slot()
    def onFinished(self, reply):
        if reply.errorReceived() == QtCoap.Error.Ok:
            self.addMessage(reply.message().payload())

    def errorMessage(self, errorCode):
        error = QtCoap.Error(errorCode)
        return f"Request failed with error: {error}\n"

    @Slot()
    def onError(self, reply, error):
        errorCode = reply.errorReceived() if reply else error
        self.addMessage(self.errorMessage(errorCode), isError=True)

    @Slot()
    def onDiscovered(self, reply, resources):
        if reply.errorReceived() != QtCoap.Error.Ok:
            return

        message = ""
        for resource in resources:
            self._ui.resourceComboBox.addItem(resource.path())
            title = resource.title()
            path = resource.path()
            message += f'Discovered resource: "{title}" on path {path}\n'
        self.addMessage(message)

    @Slot()
    def onNotified(self, reply, message):
        if reply.errorReceived() == QtCoap.Error.Ok:
            payload = message.payload().data().decode("utf-8")
            self.addMessage(f"Received observe notification with payload: {payload}")

    def tryToResolveHostName(self, hostname):
        hostinfo = QHostInfo.fromName(hostname)
        if len(hostinfo.addresses()) > 0:
            return hostinfo.addresses()[0].toString()
        return hostname

    @Slot()
    def on_runButton_clicked(self):
        msgType = (QCoapMessage.Type.Confirmable if self._ui.msgTypeCheckBox.isChecked()
                   else QCoapMessage.Type.NonConfirmable)
        url = QUrl()
        url.setHost(self.tryToResolveHostName(self._ui.hostComboBox.currentText()))
        url.setPort(self._ui.portSpinBox.value())
        url.setPath(self._ui.resourceComboBox.currentText())

        request = QCoapRequest(url, msgType)
        for option in self._options:
            request.addOption(option)
        self._options.clear()

        method = QtCoap.Method(self._ui.methodComboBox.currentData(Qt.UserRole))
        if method == QtCoap.Method.Get:
            self._client.get(request)
        elif method == QtCoap.Method.Put:
            self._client.put(request, QByteArray(self._currentData.encode("utf-8")))
        elif method == QtCoap.Method.Post:
            self._client.post(request, QByteArray(self._currentData.encode("utf-8")))
        elif method == QtCoap.Method.Delete:
            self._client.deleteResource(request)
        self._currentData = ""

    @Slot()
    def on_discoverButton_clicked(self):
        url = QUrl()
        url.setHost(self.tryToResolveHostName(self._ui.hostComboBox.currentText()))
        url.setPort(self._ui.portSpinBox.value())

        self.discoverReply = self._client.discover(url, self._ui.discoveryPathEdit.text())
        if self.discoverReply:
            self.discoverReply.discovered.connect(self.onDiscovered)
        else:
            QMessageBox.critical(self, "Error", "Something went wrong, discovery request failed.")

    @Slot()
    def on_observeButton_clicked(self):
        url = QUrl()
        url.setHost(self.tryToResolveHostName(self._ui.hostComboBox.currentText()))
        url.setPort(self._ui.portSpinBox.value())
        url.setPath(self._ui.resourceComboBox.currentText())

        self.observerReply = self._client.observe(url)
        if not self.observerReply:
            QMessageBox.critical(self, "Error", "Something went wrong, observe request failed.")
            return

        self.observerReply.notified.connect(self.onNotified)

        def on_cancelObserveButton_clicked():
            self._client.cancelObserve(url)
            self._ui.cancelObserveButton.setEnabled(False)

        self._ui.cancelObserveButton.setEnabled(True)
        self._ui.cancelObserveButton.clicked.connect(on_cancelObserveButton_clicked)

    @Slot()
    def on_addOptionsButton_clicked(self):
        dialog = OptionDialog()
        if dialog.exec() == QDialog.Accepted:
            self._options = dialog.options()

    @Slot()
    def on_contentButton_clicked(self):
        dialog = QFileDialog(self)
        dialog.setFileMode(QFileDialog.AnyFile)
        if not dialog.exec():
            return

        fileName = dialog.selectedFiles().back()
        file = QFile(fileName)
        if not file.open(QIODevice.ReadOnly):
            QMessageBox.critical(self, "Error", f"Failed to read from file {fileName}")
            return
        self._currentData = file.readAll()

    @Slot(str)
    def on_resourceComboBox_editTextChanged(self, text):
        self._ui.observeButton.setEnabled(len(text) != 0)

    @Slot(int)
    def on_methodComboBox_currentIndexChanged(self, index):
        method = QtCoap.Method(self._ui.methodComboBox.currentData(Qt.UserRole))
        enabled = method == QtCoap.Method.Put or method == QtCoap.Method.Post
        self._ui.contentButton.setEnabled(enabled)
# Copyright (C) 2020 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtCore import Qt, Slot
from PySide6.QtWidgets import QDialog, QHeaderView, QTableWidgetItem
from PySide6.QtCoap import QCoapOption

from ui_optiondialog import Ui_OptionDialog as ui


class OptionDialog(QDialog):
    def __init__(self, parent=None):
        super(OptionDialog, self).__init__(parent)
        self._ui = ui()
        self._ui.setupUi(self)
        self._options = []

        self.fillOptions()

        header = self._ui.tableWidget.horizontalHeader()
        header.setSectionResizeMode(QHeaderView.Stretch)

    def options(self):
        return self._options

    @Slot()
    def on_addButton_clicked(self):
        #option = ui.optionComboBox.currentData(Qt.UserRole).value<QCoapOption.OptionName>()
        option = QCoapOption.OptionName(self._ui.optionComboBox.currentData(Qt.UserRole))
        self._options.append(QCoapOption(option, self._ui.optionValueEdit.text()))

        rowCount = self._ui.tableWidget.rowCount()
        self._ui.tableWidget.insertRow(rowCount)

        optionItem = QTableWidgetItem(self._ui.optionComboBox.currentText())
        optionItem.setFlags(optionItem.flags() ^ Qt.ItemIsEditable)
        self._ui.tableWidget.setItem(rowCount, 0, optionItem)

        valueItem = QTableWidgetItem(self._ui.optionValueEdit.text())
        valueItem.setFlags(valueItem.flags() ^ Qt.ItemIsEditable)
        self._ui.tableWidget.setItem(rowCount, 1, valueItem)

    @Slot()
    def on_clearButton_clicked(self):
        self._options.clear()
        self._ui.tableWidget.setRowCount(0)

    def fillOptions(self):
        self._ui.tableWidget.setHorizontalHeaderLabels({"Name", "Value"})
        self._ui.optionComboBox.addItem("None", QCoapOption.Invalid)
        self._ui.optionComboBox.addItem("Block1", QCoapOption.Block1)
        self._ui.optionComboBox.addItem("Block2", QCoapOption.Block2)
        self._ui.optionComboBox.addItem("Content-Format", QCoapOption.ContentFormat)
        self._ui.optionComboBox.addItem("If-Match", QCoapOption.IfMatch)
        self._ui.optionComboBox.addItem("If-None-Match", QCoapOption.IfNoneMatch)
        self._ui.optionComboBox.addItem("Location-Path", QCoapOption.LocationPath)
        self._ui.optionComboBox.addItem("Location-Query", QCoapOption.LocationQuery)
        self._ui.optionComboBox.addItem("Max-Age", QCoapOption.MaxAge)
        self._ui.optionComboBox.addItem("Observe", QCoapOption.Observe)
        self._ui.optionComboBox.addItem("Proxy-Scheme", QCoapOption.ProxyScheme)
        self._ui.optionComboBox.addItem("Proxy-Uri", QCoapOption.ProxyUri)
        self._ui.optionComboBox.addItem("Size1", QCoapOption.Size1)
        self._ui.optionComboBox.addItem("Size2", QCoapOption.Size2)
        self._ui.optionComboBox.addItem("Uri-Host", QCoapOption.UriHost)
        self._ui.optionComboBox.addItem("Uri-Path", QCoapOption.UriPath)
        self._ui.optionComboBox.addItem("Uri-Port", QCoapOption.UriPort)
        self._ui.optionComboBox.addItem("Uri-Query", QCoapOption.UriQuery)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>731</width>
    <height>530</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>CoAP Client</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <layout class="QGridLayout" name="gridLayout_3">
    <item row="0" column="0">
     <layout class="QGridLayout" name="gridLayout">
      <item row="0" column="0">
       <widget class="QLabel" name="label">
        <property name="text">
         <string>Host</string>
        </property>
        <property name="alignment">
         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
        </property>
       </widget>
      </item>
      <item row="4" column="0">
       <widget class="QLabel" name="label_4">
        <property name="text">
         <string>Request Method</string>
        </property>
        <property name="alignment">
         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
        </property>
       </widget>
      </item>
      <item row="3" column="1">
       <layout class="QHBoxLayout" name="horizontalLayout_2">
        <item>
         <widget class="QComboBox" name="resourceComboBox">
          <property name="sizePolicy">
           <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
            <horstretch>0</horstretch>
            <verstretch>0</verstretch>
           </sizepolicy>
          </property>
          <property name="editable">
           <bool>true</bool>
          </property>
         </widget>
        </item>
        <item>
         <widget class="QPushButton" name="observeButton">
          <property name="enabled">
           <bool>false</bool>
          </property>
          <property name="text">
           <string>Observe</string>
          </property>
         </widget>
        </item>
        <item>
         <spacer name="horizontalSpacer_3">
          <property name="orientation">
           <enum>Qt::Horizontal</enum>
          </property>
          <property name="sizeHint" stdset="0">
           <size>
            <width>40</width>
            <height>20</height>
           </size>
          </property>
         </spacer>
        </item>
       </layout>
      </item>
      <item row="0" column="1">
       <layout class="QHBoxLayout" name="horizontalLayout_5">
        <item>
         <widget class="QComboBox" name="hostComboBox">
          <property name="sizePolicy">
           <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
            <horstretch>0</horstretch>
            <verstretch>0</verstretch>
           </sizepolicy>
          </property>
          <property name="minimumSize">
           <size>
            <width>250</width>
            <height>0</height>
           </size>
          </property>
          <property name="editable">
           <bool>true</bool>
          </property>
         </widget>
        </item>
        <item>
         <spacer name="horizontalSpacer">
          <property name="orientation">
           <enum>Qt::Horizontal</enum>
          </property>
          <property name="sizeType">
           <enum>QSizePolicy::Expanding</enum>
          </property>
          <property name="sizeHint" stdset="0">
           <size>
            <width>40</width>
            <height>20</height>
           </size>
          </property>
         </spacer>
        </item>
       </layout>
      </item>
      <item row="4" column="1">
       <layout class="QHBoxLayout" name="horizontalLayout_3">
        <item>
         <widget class="QComboBox" name="methodComboBox">
          <property name="sizePolicy">
           <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
            <horstretch>0</horstretch>
            <verstretch>0</verstretch>
           </sizepolicy>
          </property>
          <property name="minimumSize">
           <size>
            <width>150</width>
            <height>0</height>
           </size>
          </property>
         </widget>
        </item>
        <item>
         <widget class="QCheckBox" name="msgTypeCheckBox">
          <property name="text">
           <string>Confirmable</string>
          </property>
         </widget>
        </item>
        <item>
         <widget class="QPushButton" name="addOptionsButton">
          <property name="text">
           <string>Add Options</string>
          </property>
         </widget>
        </item>
        <item>
         <widget class="QPushButton" name="contentButton">
          <property name="enabled">
           <bool>false</bool>
          </property>
          <property name="text">
           <string>Add Content</string>
          </property>
         </widget>
        </item>
        <item>
         <spacer name="horizontalSpacer_4">
          <property name="orientation">
           <enum>Qt::Horizontal</enum>
          </property>
          <property name="sizeHint" stdset="0">
           <size>
            <width>40</width>
            <height>20</height>
           </size>
          </property>
         </spacer>
        </item>
       </layout>
      </item>
      <item row="2" column="0">
       <widget class="QLabel" name="label_8">
        <property name="text">
         <string>Disscovery Path:</string>
        </property>
        <property name="alignment">
         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
        </property>
       </widget>
      </item>
      <item row="6" column="0" colspan="2">
       <widget class="QGroupBox" name="groupBox">
        <property name="title">
         <string>Output</string>
        </property>
        <layout class="QGridLayout" name="gridLayout_2">
         <item row="0" column="0">
          <widget class="QTextEdit" name="textEdit">
           <property name="readOnly">
            <bool>true</bool>
           </property>
          </widget>
         </item>
        </layout>
       </widget>
      </item>
      <item row="1" column="0">
       <widget class="QLabel" name="label_2">
        <property name="text">
         <string>Port</string>
        </property>
        <property name="alignment">
         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
        </property>
       </widget>
      </item>
      <item row="5" column="1">
       <layout class="QHBoxLayout" name="horizontalLayout_12">
        <item>
         <widget class="QPushButton" name="runButton">
          <property name="text">
           <string>Run</string>
          </property>
         </widget>
        </item>
        <item>
         <widget class="QPushButton" name="cancelObserveButton">
          <property name="enabled">
           <bool>false</bool>
          </property>
          <property name="text">
           <string>Cancel Observe</string>
          </property>
         </widget>
        </item>
        <item>
         <spacer name="horizontalSpacer_6">
          <property name="orientation">
           <enum>Qt::Horizontal</enum>
          </property>
          <property name="sizeHint" stdset="0">
           <size>
            <width>40</width>
            <height>20</height>
           </size>
          </property>
         </spacer>
        </item>
       </layout>
      </item>
      <item row="3" column="0">
       <widget class="QLabel" name="label_3">
        <property name="text">
         <string>Resource</string>
        </property>
        <property name="alignment">
         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
        </property>
       </widget>
      </item>
      <item row="2" column="1">
       <layout class="QHBoxLayout" name="horizontalLayout_13">
        <item>
         <widget class="QLineEdit" name="discoveryPathEdit">
          <property name="text">
           <string>/.well-known/core</string>
          </property>
         </widget>
        </item>
        <item>
         <widget class="QPushButton" name="discoverButton">
          <property name="text">
           <string>Discover</string>
          </property>
         </widget>
        </item>
        <item>
         <spacer name="horizontalSpacer_7">
          <property name="orientation">
           <enum>Qt::Horizontal</enum>
          </property>
          <property name="sizeHint" stdset="0">
           <size>
            <width>40</width>
            <height>20</height>
           </size>
          </property>
         </spacer>
        </item>
       </layout>
      </item>
      <item row="1" column="1">
       <layout class="QHBoxLayout" name="horizontalLayout_6">
        <item>
         <widget class="QSpinBox" name="portSpinBox">
          <property name="sizePolicy">
           <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
            <horstretch>0</horstretch>
            <verstretch>0</verstretch>
           </sizepolicy>
          </property>
          <property name="minimumSize">
           <size>
            <width>100</width>
            <height>0</height>
           </size>
          </property>
          <property name="maximum">
           <number>65535</number>
          </property>
          <property name="value">
           <number>5683</number>
          </property>
         </widget>
        </item>
        <item>
         <spacer name="horizontalSpacer_2">
          <property name="orientation">
           <enum>Qt::Horizontal</enum>
          </property>
          <property name="sizeHint" stdset="0">
           <size>
            <width>40</width>
            <height>20</height>
           </size>
          </property>
         </spacer>
        </item>
       </layout>
      </item>
      <item row="5" column="0">
       <widget class="QLabel" name="label_7">
        <property name="text">
         <string/>
        </property>
       </widget>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menuBar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>731</width>
     <height>22</height>
    </rect>
   </property>
  </widget>
  <widget class="QToolBar" name="mainToolBar">
   <attribute name="toolBarArea">
    <enum>TopToolBarArea</enum>
   </attribute>
   <attribute name="toolBarBreak">
    <bool>false</bool>
   </attribute>
  </widget>
  <widget class="QStatusBar" name="statusBar"/>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources/>
 <connections/>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>OptionDialog</class>
 <widget class="QDialog" name="OptionDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>414</width>
    <height>313</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Add Options</string>
  </property>
  <layout class="QHBoxLayout" name="horizontalLayout_3">
   <item>
    <layout class="QHBoxLayout" name="horizontalLayout_2">
     <item>
      <layout class="QVBoxLayout" name="verticalLayout_2">
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout">
         <item>
          <widget class="QLabel" name="label">
           <property name="text">
            <string>Option</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QComboBox" name="optionComboBox">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="minimumSize">
            <size>
             <width>120</width>
             <height>0</height>
            </size>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="optionValueEdit"/>
         </item>
        </layout>
       </item>
       <item>
        <widget class="QTableWidget" name="tableWidget">
         <property name="columnCount">
          <number>2</number>
         </property>
         <column/>
         <column/>
        </widget>
       </item>
       <item>
        <widget class="QDialogButtonBox" name="buttonBox">
         <property name="orientation">
          <enum>Qt::Horizontal</enum>
         </property>
         <property name="standardButtons">
          <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
         </property>
        </widget>
       </item>
      </layout>
     </item>
     <item>
      <layout class="QVBoxLayout" name="verticalLayout">
       <item>
        <widget class="QPushButton" name="addButton">
         <property name="sizePolicy">
          <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
           <horstretch>0</horstretch>
           <verstretch>0</verstretch>
          </sizepolicy>
         </property>
         <property name="minimumSize">
          <size>
           <width>100</width>
           <height>0</height>
          </size>
         </property>
         <property name="text">
          <string>Add</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QPushButton" name="clearButton">
         <property name="text">
          <string>Clear</string>
         </property>
        </widget>
       </item>
       <item>
        <spacer name="verticalSpacer">
         <property name="orientation">
          <enum>Qt::Vertical</enum>
         </property>
         <property name="sizeHint" stdset="0">
          <size>
           <width>20</width>
           <height>40</height>
          </size>
         </property>
        </spacer>
       </item>
      </layout>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections>
  <connection>
   <sender>buttonBox</sender>
   <signal>accepted()</signal>
   <receiver>OptionDialog</receiver>
   <slot>accept()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>152</x>
     <y>289</y>
    </hint>
    <hint type="destinationlabel">
     <x>206</x>
     <y>156</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>buttonBox</sender>
   <signal>rejected()</signal>
   <receiver>OptionDialog</receiver>
   <slot>reject()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>152</x>
     <y>289</y>
    </hint>
    <hint type="destinationlabel">
     <x>206</x>
     <y>156</y>
    </hint>
   </hints>
  </connection>
 </connections>
</ui>