From 4013143e95f553febc32ee37c72dc2c18b334267 Mon Sep 17 00:00:00 2001 From: Markus Becker Date: Thu, 2 Dec 2021 08:16:46 +0100 Subject: [PATCH] Initial matter Python end-device (#11116) This PR facilitates to create a Python based matter end-device. * Adds sample with README in examples/lighting-app/python * This sample requires a DALI-USB-interface * build-chip-wheel.py was auto-formatted * Typo fixed in operational-credentials-server.cpp * Correct IP commissioning cluster docs * Add python-dali as a requirement * Add Options and re-enable BLE and WiFi * Use lighting-app.zap for client python device. * Restyled --- .github/.wordlist.txt | 1 + examples/lighting-app/python/README.md | 48 ++++ examples/lighting-app/python/lighting.py | 238 ++++++++++++++++++ examples/lighting-app/python/requirements.txt | 2 + .../python/third_party/connectedhomeip | 1 + scripts/build_python.sh | 2 +- scripts/build_python_device.sh | 137 ++++++++++ src/app/chip_data_model.gni | 10 +- src/controller/BUILD.gn | 39 +-- .../data_model/controller-clusters.zap | 2 +- src/controller/python/BUILD.gn | 86 +++++-- src/controller/python/build-chip-wheel.py | 58 +++-- src/controller/python/chip/server/Options.cpp | 112 +++++++++ src/controller/python/chip/server/Options.h | 40 +++ .../python/chip/server/ServerInit.cpp | 206 +++++++++++++++ src/controller/python/chip/server/__init__.py | 91 +++++++ src/controller/python/chip/server/types.py | 14 ++ src/platform/python.gni | 23 ++ 18 files changed, 1046 insertions(+), 64 deletions(-) create mode 100644 examples/lighting-app/python/README.md create mode 100644 examples/lighting-app/python/lighting.py create mode 100644 examples/lighting-app/python/requirements.txt create mode 120000 examples/lighting-app/python/third_party/connectedhomeip create mode 100755 scripts/build_python_device.sh create mode 100644 src/controller/python/chip/server/Options.cpp create mode 100644 src/controller/python/chip/server/Options.h create mode 100644 src/controller/python/chip/server/ServerInit.cpp create mode 100644 src/controller/python/chip/server/__init__.py create mode 100644 src/controller/python/chip/server/types.py create mode 100644 src/platform/python.gni diff --git a/.github/.wordlist.txt b/.github/.wordlist.txt index ac58409264db79..f6ced831c7bcb5 100644 --- a/.github/.wordlist.txt +++ b/.github/.wordlist.txt @@ -277,6 +277,7 @@ DevKitC DevKitM df dfu +dhclient DHCP DHCPC DHCPv diff --git a/examples/lighting-app/python/README.md b/examples/lighting-app/python/README.md new file mode 100644 index 00000000000000..f7ebf0ba71684d --- /dev/null +++ b/examples/lighting-app/python/README.md @@ -0,0 +1,48 @@ +# Python based lighting example (bridge) device to DALI. + +## Installation + +Build the Python/C library: + +```shell +cd ~/connectedhomeip/ +git submodule update --init +source scripts/activate.sh + +./scripts/build_python_device.sh --chip_detail_logging true + +sudo su # dhclient is called, needs root +source ./out/python_env/bin/activate +``` + +Install the python dependencies: + +```shell +pip3 install python-dali +``` + +Plug-in a python-dali compatible USB-DALI interface. + +## Usage + +Run the Python lighting matter device: + +```shell +cd examples/lighting-app/python +python lighting.py +``` + +Control the Python lighting matter device: + +```shell +source ./out/python_env/bin/activate + +chip-device-ctrl + +chip-device-ctrl > connect -ble 3840 20202021 12344321 +chip-device-ctrl > zcl NetworkCommissioning AddWiFiNetwork 12344321 0 0 ssid=str:YOUR_SSID credentials=str:YOUR_PASSWORD breadcrumb=0 timeoutMs=1000 +chip-device-ctrl > zcl NetworkCommissioning EnableNetwork 12344321 0 0 networkID=str:YOUR_SSID breadcrumb=0 timeoutMs=1000 +chip-device-ctrl > close-ble +chip-device-ctrl > resolve 5544332211 1 (pass appropriate fabric ID and node ID, you can get this from get-fabricid) +chip-device-ctrl > zcl OnOff Toggle 12344321 1 0 +``` diff --git a/examples/lighting-app/python/lighting.py b/examples/lighting-app/python/lighting.py new file mode 100644 index 00000000000000..8de8c440d855a1 --- /dev/null +++ b/examples/lighting-app/python/lighting.py @@ -0,0 +1,238 @@ +# +# Copyright (c) 2021 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from chip.server import ( + GetLibraryHandle, + NativeLibraryHandleMethodArguments, + PostAttributeChangeCallback, +) + +from chip.exceptions import ChipStackError + +from ctypes import CFUNCTYPE, c_char_p, c_int32, c_uint8 + +import sys +import os + +import textwrap +import string + +from cmd import Cmd + +import asyncio +import threading + +from dali.driver.hid import tridonic +from dali.gear.general import RecallMaxLevel, Off, DAPC +from dali.address import Broadcast, Short + +dali_loop = None +dev = None + + +async def dali_on(is_on: bool): + global dali_loop + global dev + + await dev.connected.wait() + if (is_on): + await dev.send(RecallMaxLevel(Broadcast())) + else: + await dev.send(Off(Broadcast())) + + +async def dali_level(level: int): + global dali_loop + global dev + + await dev.connected.wait() + await dev.send(DAPC(Broadcast(), level)) + + +def daliworker(): + global dali_loop + global dev + + dali_loop = asyncio.new_event_loop() + dev = tridonic("/dev/dali/daliusb-*", glob=True, loop=dali_loop) + dev.connect() + + asyncio.set_event_loop(dali_loop) + dali_loop.run_forever() + + +class LightingMgrCmd(Cmd): + def __init__(self, rendezvousAddr=None, controllerNodeId=0, bluetoothAdapter=None): + self.lastNetworkId = None + + Cmd.__init__(self) + + Cmd.identchars = string.ascii_letters + string.digits + "-" + + if sys.stdin.isatty(): + self.prompt = "chip-lighting > " + else: + self.use_rawinput = 0 + self.prompt = "" + + LightingMgrCmd.command_names.sort() + + self.historyFileName = os.path.expanduser("~/.chip-lighting-history") + + try: + import readline + + if "libedit" in readline.__doc__: + readline.parse_and_bind("bind ^I rl_complete") + readline.set_completer_delims(" ") + try: + readline.read_history_file(self.historyFileName) + except IOError: + pass + except ImportError: + pass + + command_names = [ + "help" + ] + + def parseline(self, line): + cmd, arg, line = Cmd.parseline(self, line) + if cmd: + cmd = self.shortCommandName(cmd) + line = cmd + " " + arg + return cmd, arg, line + + def completenames(self, text, *ignored): + return [ + name + " " + for name in LightingMgrCmd.command_names + if name.startswith(text) or self.shortCommandName(name).startswith(text) + ] + + def shortCommandName(self, cmd): + return cmd.replace("-", "") + + def precmd(self, line): + if not self.use_rawinput and line != "EOF" and line != "": + print(">>> " + line) + return line + + def postcmd(self, stop, line): + if not stop and self.use_rawinput: + self.prompt = "chip-lighting > " + return stop + + def postloop(self): + try: + import readline + + try: + readline.write_history_file(self.historyFileName) + except IOError: + pass + except ImportError: + pass + + def do_help(self, line): + """ + help + + Print the help + """ + if line: + cmd, arg, unused = self.parseline(line) + try: + doc = getattr(self, "do_" + cmd).__doc__ + except AttributeError: + doc = None + if doc: + self.stdout.write("%s\n" % textwrap.dedent(doc)) + else: + self.stdout.write("No help on %s\n" % (line)) + else: + self.print_topics( + "\nAvailable commands (type help for more information):", + LightingMgrCmd.command_names, + 15, + 80, + ) + + +@PostAttributeChangeCallback +def attributeChangeCallback( + endpoint: int, + clusterId: int, + attributeId: int, + mask: int, + manufacturerCode: int, + xx_type: int, + size: int, + value: bytes, +): + global dali_loop + if endpoint == 1: + if clusterId == 6 and attributeId == 0: + if len(value) == 1 and value[0] == 1: + # print("[PY] light on") + future = asyncio.run_coroutine_threadsafe( + dali_on(True), dali_loop) + future.result() + else: + # print("[PY] light off") + future = asyncio.run_coroutine_threadsafe( + dali_on(False), dali_loop) + future.result() + elif clusterId == 8 and attributeId == 0: + if len(value) == 2: + # print("[PY] level {}".format(value[0])) + future = asyncio.run_coroutine_threadsafe( + dali_level(value[0]), dali_loop) + future.result() + else: + print("[PY] no level") + else: + # print("[PY] [ERR] unhandled cluster {} or attribute {}".format( + # clusterId, attributeId)) + pass + else: + print("[PY] [ERR] unhandled endpoint {} ".format(endpoint)) + + +class Lighting: + def __init__(self): + self.chipLib = GetLibraryHandle(attributeChangeCallback) + + +if __name__ == "__main__": + l = Lighting() + + lightMgrCmd = LightingMgrCmd() + print("Chip Lighting Device Shell") + print() + + print("Starting DALI async") + threads = [] + t = threading.Thread(target=daliworker) + threads.append(t) + t.start() + + try: + lightMgrCmd.cmdloop() + except KeyboardInterrupt: + print("\nQuitting") + + sys.exit(0) diff --git a/examples/lighting-app/python/requirements.txt b/examples/lighting-app/python/requirements.txt new file mode 100644 index 00000000000000..dc27da9af953b3 --- /dev/null +++ b/examples/lighting-app/python/requirements.txt @@ -0,0 +1,2 @@ +# dali +python-dali diff --git a/examples/lighting-app/python/third_party/connectedhomeip b/examples/lighting-app/python/third_party/connectedhomeip new file mode 120000 index 00000000000000..c866b86874994d --- /dev/null +++ b/examples/lighting-app/python/third_party/connectedhomeip @@ -0,0 +1 @@ +../../../.. \ No newline at end of file diff --git a/scripts/build_python.sh b/scripts/build_python.sh index 61b1e331fbd3cf..7949f04644ca6f 100755 --- a/scripts/build_python.sh +++ b/scripts/build_python.sh @@ -117,7 +117,7 @@ fi # Create a virtual environment that has access to the built python tools virtualenv --clear "$ENVIRONMENT_ROOT" -# Activate the new enviroment to register the python WHL +# Activate the new environment to register the python WHL if [ "$enable_pybindings" == true ]; then WHEEL=$(ls "$OUTPUT_ROOT"/pybindings/pycontroller/pychip-*.whl | head -n 1) diff --git a/scripts/build_python_device.sh b/scripts/build_python_device.sh new file mode 100755 index 00000000000000..14fce377a6cdad --- /dev/null +++ b/scripts/build_python_device.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash + +# +# Copyright (c) 2021 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -e + +_normpath() { + python -c "import os.path; print(os.path.normpath('$@'))" +} + +echo_green() { + echo -e "\033[0;32m$*\033[0m" +} + +echo_blue() { + echo -e "\033[1;34m$*\033[0m" +} + +echo_bold_white() { + echo -e "\033[1;37m$*\033[0m" +} + +CHIP_ROOT=$(_normpath "$(dirname "$0")/..") +OUTPUT_ROOT="$CHIP_ROOT/out/python_lib" +ENVIRONMENT_ROOT="$CHIP_ROOT/out/python_env" + +declare chip_detail_logging=false +declare enable_pybindings=false +declare chip_mdns +declare clusters=true + +help() { + + echo "Usage: $file_name [ options ... ] [ -chip_detail_logging ChipDetailLoggingValue ] [ -chip_mdns ChipMDNSValue ] [-enable_pybindings EnableValue]" + + echo "General Options: + -h, --help Display this information. +Input Options: + -d, --chip_detail_logging ChipDetailLoggingValue Specify ChipDetailLoggingValue as true or false. + By default it is false. + -m, --chip_mdns ChipMDNSValue Specify ChipMDNSValue as platform or minimal. + By default it is minimal. + -c, --clusters_for_ip_commissioning true/false Specify whether to use clusters for IP commissioning. + By default it is true. + -p, --enable_pybindings EnableValue Specify whether to enable pybindings as python controller. +" +} + +file_name=${0##*/} + +while (($#)); do + case $1 in + --help | -h) + help + exit 1 + ;; + --chip_detail_logging | -d) + chip_detail_logging=$2 + shift + ;; + --chip_mdns | -m) + chip_mdns=$2 + shift + ;; + --clusters_for_ip_commissioning | -c) + clusters=$2 + shift + ;; + --enable_pybindings | -p) + enable_pybindings=$2 + shift + ;; + -*) + help + echo "Unknown Option \"$1\"" + exit 1 + ;; + esac + shift +done + +# Print input values +echo "Input values: chip_detail_logging = $chip_detail_logging , chip_mdns = \"$chip_mdns\", enable_pybindings = $enable_pybindings" + +# Ensure we have a compilation environment +source "$CHIP_ROOT/scripts/activate.sh" + +# Generates ninja files +[[ -n "$chip_mdns" ]] && chip_mdns_arg="chip_mdns=\"$chip_mdns\"" || chip_mdns_arg="" + +chip_data_model_arg="chip_data_model=\"///examples/lighting-app/lighting-common\"" + +gn --root="$CHIP_ROOT" gen "$OUTPUT_ROOT" --args="chip_detail_logging=$chip_detail_logging enable_pylib=$enable_pybindings enable_rtti=$enable_pybindings chip_use_clusters_for_ip_commissioning=$clusters $chip_mdns_arg chip_controller=false $chip_data_model_arg" + +# Compiles python files +# Check pybindings was requested +if [ "$enable_pybindings" == true ]; then + ninja -v -C "$OUTPUT_ROOT" pycontroller +else + ninja -v -C "$OUTPUT_ROOT" python +fi + +# Create a virtual environment that has access to the built python tools +virtualenv --clear "$ENVIRONMENT_ROOT" + +# Activate the new environment to register the python WHL + +if [ "$enable_pybindings" == true ]; then + WHEEL=$(ls "$OUTPUT_ROOT"/pybindings/pycontroller/pychip-*.whl | head -n 1) +else + WHEEL=$(ls "$OUTPUT_ROOT"/controller/python/chip-*.whl | head -n 1) +fi + +source "$ENVIRONMENT_ROOT"/bin/activate +"$ENVIRONMENT_ROOT"/bin/python -m pip install --upgrade pip +"$ENVIRONMENT_ROOT"/bin/pip install --upgrade --force-reinstall --no-cache-dir "$WHEEL" + +echo "" +echo_green "Compilation completed and WHL package installed in: " +echo_blue " $ENVIRONMENT_ROOT" +echo "" +echo_green "To use please run:" +echo_bold_white " source $ENVIRONMENT_ROOT/bin/activate" diff --git a/src/app/chip_data_model.gni b/src/app/chip_data_model.gni index 96205d8b0ca96b..15daf51f5b0188 100644 --- a/src/app/chip_data_model.gni +++ b/src/app/chip_data_model.gni @@ -14,6 +14,7 @@ import("//build_overrides/build.gni") import("//build_overrides/chip.gni") +import("${chip_root}/src/platform/python.gni") import("${chip_root}/src/lib/core/core.gni") @@ -154,12 +155,17 @@ template("chip_data_model") { if (defined(invoker.zap_pregenerated_dir)) { sources += [ - "${invoker.zap_pregenerated_dir}/CHIPClusters.cpp", - "${invoker.zap_pregenerated_dir}/CHIPClusters.h", "${invoker.zap_pregenerated_dir}/attribute-size.cpp", "${invoker.zap_pregenerated_dir}/callback-stub.cpp", ] + if (chip_controller) { + sources += [ + "${invoker.zap_pregenerated_dir}/CHIPClusters.cpp", + "${invoker.zap_pregenerated_dir}/CHIPClusters.h", + ] + } + if (_use_default_client_callbacks) { sources += [ "${_app_root}/util/im-client-callbacks.cpp", diff --git a/src/controller/BUILD.gn b/src/controller/BUILD.gn index 33874bd47a60b6..6339f2dfddd7a2 100644 --- a/src/controller/BUILD.gn +++ b/src/controller/BUILD.gn @@ -13,6 +13,8 @@ # limitations under the License. import("//build_overrides/chip.gni") +import("${chip_root}/src/platform/device.gni") +import("${chip_root}/src/platform/python.gni") config("config") { # TODO: this should be a dependency on src/controller/data_model however this is circular: @@ -25,26 +27,31 @@ static_library("controller") { output_name = "libChipController" sources = [ - "AbstractDnssdDiscoveryController.cpp", "CHIPCluster.cpp", "CHIPCluster.h", - "CHIPCommissionableNodeController.cpp", - "CHIPCommissionableNodeController.h", - "CHIPDeviceController.cpp", - "CHIPDeviceController.h", - "CHIPDeviceControllerFactory.cpp", - "CHIPDeviceControllerFactory.h", - "CommissioneeDeviceProxy.cpp", - "CommissioneeDeviceProxy.h", - "DeviceAddressUpdateDelegate.h", - "DeviceDiscoveryDelegate.h", - "EmptyDataModelHandler.cpp", - "ExampleOperationalCredentialsIssuer.cpp", - "ExampleOperationalCredentialsIssuer.h", - "SetUpCodePairer.cpp", - "SetUpCodePairer.h", ] + if (chip_controller) { + sources += [ + "AbstractDnssdDiscoveryController.cpp", + "CHIPCommissionableNodeController.cpp", + "CHIPCommissionableNodeController.h", + "CHIPDeviceController.cpp", + "CHIPDeviceController.h", + "CHIPDeviceControllerFactory.cpp", + "CHIPDeviceControllerFactory.h", + "CommissioneeDeviceProxy.cpp", + "CommissioneeDeviceProxy.h", + "DeviceAddressUpdateDelegate.h", + "DeviceDiscoveryDelegate.h", + "EmptyDataModelHandler.cpp", + "ExampleOperationalCredentialsIssuer.cpp", + "ExampleOperationalCredentialsIssuer.h", + "SetUpCodePairer.cpp", + "SetUpCodePairer.h", + ] + } + cflags = [ "-Wconversion" ] public_deps = [ diff --git a/src/controller/data_model/controller-clusters.zap b/src/controller/data_model/controller-clusters.zap index 2a02c37f4be815..b975b554fdf496 100644 --- a/src/controller/data_model/controller-clusters.zap +++ b/src/controller/data_model/controller-clusters.zap @@ -13264,4 +13264,4 @@ } ], "log": [] -} \ No newline at end of file +} diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn index 5bc715fdd802a4..38b6536d560739 100644 --- a/src/controller/python/BUILD.gn +++ b/src/controller/python/BUILD.gn @@ -19,7 +19,7 @@ import("//build_overrides/pigweed.gni") import("$dir_pw_build/python.gni") import("${chip_root}/build/chip/tools.gni") -import("${chip_root}/src/platform/device.gni") +import("${chip_root}/src/platform/python.gni") import("${dir_pw_unit_test}/test.gni") if (current_os == "mac") { @@ -31,33 +31,47 @@ config("controller_wno_deprecate") { } shared_library("ChipDeviceCtrl") { - output_name = "_ChipDeviceCtrl" + if (chip_controller) { + output_name = "_ChipDeviceCtrl" + } else { + output_name = "_ChipServer" + } output_dir = "${target_out_dir}/chip" include_dirs = [ "." ] sources = [ - "ChipCommissionableNodeController-ScriptBinding.cpp", - "ChipDeviceController-ScriptBinding.cpp", - "ChipDeviceController-ScriptDeviceAddressUpdateDelegate.cpp", - "ChipDeviceController-ScriptDeviceAddressUpdateDelegate.h", - "ChipDeviceController-ScriptDevicePairingDelegate.cpp", - "ChipDeviceController-ScriptDevicePairingDelegate.h", - "ChipDeviceController-StorageDelegate.cpp", - "ChipDeviceController-StorageDelegate.h", - "chip/clusters/attribute.cpp", - "chip/clusters/command.cpp", - "chip/discovery/NodeResolution.cpp", - "chip/interaction_model/Delegate.cpp", - "chip/interaction_model/Delegate.h", - "chip/internal/ChipThreadWork.cpp", - "chip/internal/ChipThreadWork.h", - "chip/internal/CommissionerImpl.cpp", - "chip/logging/LoggingRedirect.cpp", - "chip/native/StackInit.cpp", "chip/setup_payload/Generator.cpp", "chip/setup_payload/Parser.cpp", ] + if (chip_controller) { + sources += [ + "ChipCommissionableNodeController-ScriptBinding.cpp", + "ChipDeviceController-ScriptBinding.cpp", + "ChipDeviceController-ScriptDeviceAddressUpdateDelegate.cpp", + "ChipDeviceController-ScriptDeviceAddressUpdateDelegate.h", + "ChipDeviceController-ScriptDevicePairingDelegate.cpp", + "ChipDeviceController-ScriptDevicePairingDelegate.h", + "ChipDeviceController-StorageDelegate.cpp", + "ChipDeviceController-StorageDelegate.h", + "chip/clusters/attribute.cpp", + "chip/clusters/command.cpp", + "chip/discovery/NodeResolution.cpp", + "chip/interaction_model/Delegate.cpp", + "chip/interaction_model/Delegate.h", + "chip/internal/ChipThreadWork.cpp", + "chip/internal/ChipThreadWork.h", + "chip/internal/CommissionerImpl.cpp", + "chip/logging/LoggingRedirect.cpp", + "chip/native/StackInit.cpp", + ] + } else { + sources += [ + "chip/server/Options.cpp", + "chip/server/ServerInit.cpp", + ] + } + if (chip_enable_ble) { if (current_os == "linux") { sources += [ "chip/ble/LinuxImpl.cpp" ] @@ -75,7 +89,6 @@ shared_library("ChipDeviceCtrl") { public_deps = [ "${chip_root}/src/app", "${chip_root}/src/app/server", - "${chip_root}/src/controller/data_model", "${chip_root}/src/lib", "${chip_root}/src/lib/core", "${chip_root}/src/lib/dnssd", @@ -84,6 +97,13 @@ shared_library("ChipDeviceCtrl") { "${chip_root}/src/setup_payload", "${chip_root}/src/transport", ] + + if (chip_controller) { + public_deps += [ "${chip_root}/src/controller/data_model" ] + } else { + public_deps += [ "$chip_data_model" ] + } + configs += [ ":controller_wno_deprecate" ] } @@ -101,7 +121,6 @@ pw_python_action("python") { "chip/ChipBluezMgr.py", "chip/ChipCommissionableNodeCtrl.py", "chip/ChipCoreBluetoothMgr.py", - "chip/ChipDeviceCtrl.py", "chip/ChipReplStartup.py", "chip/ChipStack.py", "chip/ChipUtility.py", @@ -139,10 +158,23 @@ pw_python_action("python") { "chip/setup_payload/setup_payload.py", "chip/tlv/__init__.py", ] + + if (chip_controller) { + sources += [ "chip/ChipDeviceCtrl.py" ] + } else { + sources += [ + "chip/server/__init__.py", + "chip/server/types.py", + ] + } }, { src_dir = target_out_dir - sources = [ "${target_out_dir}/chip/_ChipDeviceCtrl.so" ] + if (chip_controller) { + sources = [ "${target_out_dir}/chip/_ChipDeviceCtrl.so" ] + } else { + sources = [ "${target_out_dir}/chip/_ChipServer.so" ] + } }, { src_dir = "//" @@ -206,6 +238,14 @@ pw_python_action("python") { platform_tag, ] + if (chip_controller) { + } else { + args += [ + "--server", + "True", + ] + } + public_deps = [ ":ChipDeviceCtrl" ] output_name = "chip-0.0-${tags}.whl" diff --git a/src/controller/python/build-chip-wheel.py b/src/controller/python/build-chip-wheel.py index 565fec7329bac2..3a4c5cc12efabb 100644 --- a/src/controller/python/build-chip-wheel.py +++ b/src/controller/python/build-chip-wheel.py @@ -44,6 +44,8 @@ parser.add_argument('--manifest', help='list of files to package') parser.add_argument( '--plat-name', help='platform name to embed in generated filenames') +parser.add_argument( + '--server', help='build the server variant', default=False, type=bool) args = parser.parse_args() @@ -56,14 +58,20 @@ def __init__(self, name): self.installName = os.path.splitext(name)[0] -chipDLLName = '_ChipDeviceCtrl.so' +if args.server: + chipDLLName = "_ChipServer.so" +else: + chipDLLName = "_ChipDeviceCtrl.so" packageName = args.package_name chipPackageVer = args.build_number -installScripts = [ - InstalledScriptInfo('chip-device-ctrl.py'), - InstalledScriptInfo('chip-repl.py'), -] +if args.server: + installScripts = [] +else: + installScripts = [ + InstalledScriptInfo("chip-device-ctrl.py"), + InstalledScriptInfo("chip-repl.py"), + ] # Record the current directory at the start of execution. curDir = os.curdir @@ -74,9 +82,9 @@ def __init__(self, name): # Use a temporary directory within the build directory to assemble the components # for the installable package. -tmpDir = os.path.join(buildDir, 'chip-wheel-components') +tmpDir = os.path.join(buildDir, "chip-wheel-components") -manifest = json.load(open(manifestFile, 'r')) +manifest = json.load(open(manifestFile, "r")) try: @@ -128,12 +136,12 @@ def finalize_options(self): 'pyyaml', ] - if platform.system() == 'Darwin': - requiredPackages.append('pyobjc-framework-corebluetooth') + if platform.system() == "Darwin": + requiredPackages.append("pyobjc-framework-corebluetooth") - if platform.system() == 'Linux': - requiredPackages.append('dbus-python') - requiredPackages.append('pygobject') + if platform.system() == "Linux": + requiredPackages.append("dbus-python") + requiredPackages.append("pygobject") # # Build the chip package... @@ -154,23 +162,31 @@ def finalize_options(self): 'chip.tlv', 'chip.setup_payload', ] + #print ("Server: {}".format(args.server)) + if args.server: + packages.append('chip.server') + + #print("packages: {}".format(packages)) + + print("packageName: {}".format(packageName)) + print("chipDLLName: {}".format(chipDLLName)) # Invoke the setuptools 'bdist_wheel' command to generate a wheel containing # the CHIP python packages, shared libraries and scripts. setup( name=packageName, version=chipPackageVer, - description='Python-base APIs and tools for CHIP.', - url='https://github.com/project-chip/connectedhomeip', - license='Apache', + description="Python-base APIs and tools for CHIP.", + url="https://github.com/project-chip/connectedhomeip", + license="Apache", classifiers=[ - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", ], - python_requires='>=2.7', + python_requires=">=2.7", packages=packages, package_dir={ # By default, look in the tmp directory for packages/modules to be included. diff --git a/src/controller/python/chip/server/Options.cpp b/src/controller/python/chip/server/Options.cpp new file mode 100644 index 00000000000000..05efe2e49b14f0 --- /dev/null +++ b/src/controller/python/chip/server/Options.cpp @@ -0,0 +1,112 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Options.h" + +#include + +#include +#include + +using namespace chip; +using namespace chip::ArgParser; + +namespace { +LinuxDeviceOptions gDeviceOptions; + +// Follow the code style of command line arguments in case we need to add more options in the future. +enum +{ + kDeviceOption_BleDevice = 0x1000, + kDeviceOption_WiFi = 0x1001, + kDeviceOption_Thread = 0x1002, +}; + +OptionDef sDeviceOptionDefs[] = { { "ble-device", kArgumentRequired, kDeviceOption_BleDevice }, +#if CHIP_DEVICE_CONFIG_ENABLE_WPA + { "wifi", kNoArgument, kDeviceOption_WiFi }, +#endif // CHIP_DEVICE_CONFIG_ENABLE_WPA +#if CHIP_ENABLE_OPENTHREAD + { "thread", kNoArgument, kDeviceOption_Thread }, +#endif // CHIP_ENABLE_OPENTHREAD + {} }; + +const char * sDeviceOptionHelp = " --ble-device \n" + " The device number for CHIPoBLE, without 'hci' prefix, can be found by hciconfig.\n" +#if CHIP_DEVICE_CONFIG_ENABLE_WPA + "\n" + " --wifi\n" + " Enable WiFi management via wpa_supplicant.\n" +#endif // CHIP_DEVICE_CONFIG_ENABLE_WPA +#if CHIP_ENABLE_OPENTHREAD + "\n" + " --thread\n" + " Enable Thread management via ot-agent.\n" +#endif // CHIP_ENABLE_OPENTHREAD + "\n"; + +bool HandleOption(const char * aProgram, OptionSet * aOptions, int aIdentifier, const char * aName, const char * aValue) +{ + bool retval = true; + + switch (aIdentifier) + { + + case kDeviceOption_BleDevice: + if (!ParseInt(aValue, LinuxDeviceOptions::GetInstance().mBleDevice)) + { + PrintArgError("%s: invalid value specified for ble device number: %s\n", aProgram, aValue); + retval = false; + } + break; + + case kDeviceOption_WiFi: + LinuxDeviceOptions::GetInstance().mWiFi = true; + break; + + case kDeviceOption_Thread: + LinuxDeviceOptions::GetInstance().mThread = true; + break; + + default: + PrintArgError("%s: INTERNAL ERROR: Unhandled option: %s\n", aProgram, aName); + retval = false; + break; + } + + return (retval); +} + +OptionSet sDeviceOptions = { HandleOption, sDeviceOptionDefs, "GENERAL OPTIONS", sDeviceOptionHelp }; + +OptionSet * sLinuxDeviceOptionSets[] = { &sDeviceOptions, nullptr }; +} // namespace + +CHIP_ERROR ParseArguments(int argc, char * argv[]) +{ + if (!ParseArgs(argv[0], argc, argv, sLinuxDeviceOptionSets)) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + return CHIP_NO_ERROR; +} + +LinuxDeviceOptions & LinuxDeviceOptions::GetInstance() +{ + return gDeviceOptions; +} diff --git a/src/controller/python/chip/server/Options.h b/src/controller/python/chip/server/Options.h new file mode 100644 index 00000000000000..a7d08dfa5cddf4 --- /dev/null +++ b/src/controller/python/chip/server/Options.h @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2020 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @file + * Support functions for parsing command-line arguments. + * + */ + +#pragma once + +#include + +#include + +struct LinuxDeviceOptions +{ + uint32_t mBleDevice = 0; + bool mWiFi = true; + bool mThread = false; + + static LinuxDeviceOptions & GetInstance(); +}; + +CHIP_ERROR ParseArguments(int argc, char * argv[]); diff --git a/src/controller/python/chip/server/ServerInit.cpp b/src/controller/python/chip/server/ServerInit.cpp new file mode 100644 index 00000000000000..529c28a377a5ee --- /dev/null +++ b/src/controller/python/chip/server/ServerInit.cpp @@ -0,0 +1,206 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include + +#include +#include + +#include +#include + +#include +#include + +#include +#include + +// #include +// #include + +// #include + +#include "Options.h" + +using namespace chip; +using namespace chip::Inet; +using namespace chip::Transport; +using namespace chip::DeviceLayer; + +namespace { + +void EventHandler(const chip::DeviceLayer::ChipDeviceEvent * event, intptr_t arg) +{ + (void) arg; + if (event->Type == chip::DeviceLayer::DeviceEventType::kCHIPoBLEConnectionEstablished) + { + ChipLogProgress(DeviceLayer, "Receive kCHIPoBLEConnectionEstablished"); + } +} + +pthread_t sPlatformMainThread; + +#if CHIP_DEVICE_LAYER_TARGET_LINUX && CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE +// uint32_t sBluetoothAdapterId = 0; +#endif + +void * PlatformMainLoop(void *) +{ + ChipLogProgress(DeviceLayer, "Platform main loop started."); + chip::DeviceLayer::PlatformMgr().RunEventLoop(); + ChipLogProgress(DeviceLayer, "Platform main loop completed."); + return nullptr; +} + +} // namespace + +extern "C" { + +#if CHIP_DEVICE_CONFIG_ENABLE_WPA +/* + * The device shall check every kWifiStartCheckTimeUsec whether Wi-Fi management + * has been fully initialized. If after kWifiStartCheckAttempts Wi-Fi management + * still hasn't been initialized, the device configuration is reset, and device + * needs to be paired again. + */ +static constexpr useconds_t kWifiStartCheckTimeUsec = 100 * 1000; // 100 ms +static constexpr uint8_t kWifiStartCheckAttempts = 5; +#endif + +#if CHIP_DEVICE_CONFIG_ENABLE_WPA +static bool EnsureWifiIsStarted() +{ + for (int cnt = 0; cnt < kWifiStartCheckAttempts; cnt++) + { + if (chip::DeviceLayer::ConnectivityMgrImpl().IsWiFiManagementStarted()) + { + return true; + } + + usleep(kWifiStartCheckTimeUsec); + } + + return chip::DeviceLayer::ConnectivityMgrImpl().IsWiFiManagementStarted(); +} +#endif + +using PostAttributeChangeCallback = void (*)(EndpointId endpoint, ClusterId clusterId, AttributeId attributeId, uint8_t mask, + uint16_t manufacturerCode, uint8_t type, uint16_t size, uint8_t * value); + +class PythonServerDelegate // : public ServerDelegate +{ +public: + void SetPostAttributeChangeCallback(PostAttributeChangeCallback cb) + { + // ChipLogProgress(NotSpecified, "callback %p", cb); + mPostAttributeChangeCallback = cb; + }; + PostAttributeChangeCallback mPostAttributeChangeCallback = nullptr; +}; + +PythonServerDelegate gPythonServerDelegate; + +void pychip_server_set_callbacks(PostAttributeChangeCallback cb) +{ + // ChipLogProgress(NotSpecified, "setting cb"); + gPythonServerDelegate.SetPostAttributeChangeCallback(cb); +} + +void pychip_server_native_init() +{ + CHIP_ERROR err = CHIP_NO_ERROR; + int result; + int tmpErrno; + + err = chip::Platform::MemoryInit(); + if (err != CHIP_NO_ERROR) + { + ChipLogError(DeviceLayer, "Failed to initialize CHIP stack: memory init failed: %s", chip::ErrorStr(err)); + } + + err = chip::DeviceLayer::PlatformMgr().InitChipStack(); + if (err != CHIP_NO_ERROR) + { + ChipLogError(DeviceLayer, "Failed to initialize CHIP stack: platform init failed: %s", chip::ErrorStr(err)); + } + + ConfigurationMgr().LogDeviceConfig(); + + PrintOnboardingCodes(chip::RendezvousInformationFlag(chip::RendezvousInformationFlag::kBLE)); + +#if defined(PW_RPC_ENABLED) + chip::rpc::Init(); + ChipLogProgress(NotSpecified, "PW_RPC initialized."); +#endif // defined(PW_RPC_ENABLED) + + chip::DeviceLayer::PlatformMgrImpl().AddEventHandler(EventHandler, 0); + +#if CONFIG_NETWORK_LAYER_BLE + chip::DeviceLayer::ConnectivityMgr().SetBLEDeviceName("RpiMatterDali"); // Use default device name (CHIP-XXXX) + chip::DeviceLayer::Internal::BLEMgrImpl().ConfigureBle(LinuxDeviceOptions::GetInstance().mBleDevice, false); + chip::DeviceLayer::ConnectivityMgr().SetBLEAdvertisingEnabled(true); +#endif + +#if CHIP_DEVICE_CONFIG_ENABLE_WPA + if (LinuxDeviceOptions::GetInstance().mWiFi) + { + chip::DeviceLayer::ConnectivityMgrImpl().StartWiFiManagement(); + if (!EnsureWifiIsStarted()) + { + ChipLogError(NotSpecified, "Wi-Fi Management taking too long to start - device configuration will be reset."); + } + } +#endif // CHIP_DEVICE_CONFIG_ENABLE_WPA + + // parts from ChipLinuxAppMainLoop + + uint16_t securePort = CHIP_PORT; + uint16_t unsecurePort = CHIP_UDC_PORT; + + // Init ZCL Data Model and CHIP App Server + chip::Server::GetInstance().Init(nullptr, securePort, unsecurePort); + + // Initialize device attestation config + // SetDeviceAttestationCredentialsProvider(Examples::GetExampleDACProvider()); + + result = pthread_create(&sPlatformMainThread, nullptr, PlatformMainLoop, nullptr); + tmpErrno = errno; + + if (result != 0) + { + ChipLogError(DeviceLayer, "Failed to initialize CHIP stack: pthread_create failed: %s", strerror(tmpErrno)); + } + + return /*err*/; +} +} + +void emberAfPostAttributeChangeCallback(chip::EndpointId endpoint, chip::ClusterId clusterId, chip::AttributeId attributeId, + uint8_t mask, uint16_t manufacturerCode, uint8_t type, uint16_t size, uint8_t * value) +{ + // ChipLogProgress(NotSpecified, "emberAfPostAttributeChangeCallback()"); + if (gPythonServerDelegate.mPostAttributeChangeCallback != nullptr) + { + // ChipLogProgress(NotSpecified, "callback %p", gPythonServerDelegate.mPostAttributeChangeCallback); + gPythonServerDelegate.mPostAttributeChangeCallback(endpoint, clusterId, attributeId, mask, manufacturerCode, type, size, + value); + } + else + { + // ChipLogProgress(NotSpecified, "callback nullptr"); + } +}; diff --git a/src/controller/python/chip/server/__init__.py b/src/controller/python/chip/server/__init__.py new file mode 100644 index 00000000000000..d0b046ffe689d5 --- /dev/null +++ b/src/controller/python/chip/server/__init__.py @@ -0,0 +1,91 @@ +import ctypes +import glob +import os +import platform + +from chip.server.types import PostAttributeChangeCallback + +NATIVE_LIBRARY_BASE_NAME = "_ChipServer.so" + + +def _AllDirsToRoot(dir): + """Return all parent paths of a directory.""" + dir = os.path.abspath(dir) + while True: + yield dir + parent = os.path.dirname(dir) + if parent == "" or parent == dir: + break + dir = parent + + +def FindNativeLibraryPath() -> str: + """Find the native CHIP dll/so path.""" + + scriptDir = os.path.dirname(os.path.abspath(__file__)) + + # When properly installed in the chip package, the Chip Device Manager DLL will + # be located in the package root directory, along side the package's + # modules. + dmDLLPath = os.path.join( + os.path.dirname(scriptDir), # file should be inside 'chip' + NATIVE_LIBRARY_BASE_NAME, + ) + if os.path.exists(dmDLLPath): + return dmDLLPath + + # For the convenience of developers, search the list of parent paths relative to the + # running script looking for an CHIP build directory containing the Chip Device + # Manager DLL. This makes it possible to import and use the ChipDeviceMgr module + # directly from a built copy of the CHIP source tree. + buildMachineGlob = "%s-*-%s*" % (platform.machine(), + platform.system().lower()) + relDMDLLPathGlob = os.path.join( + "build", + buildMachineGlob, + "src/controller/python/.libs", + NATIVE_LIBRARY_BASE_NAME, + ) + for dir in _AllDirsToRoot(scriptDir): + dmDLLPathGlob = os.path.join(dir, relDMDLLPathGlob) + for dmDLLPath in glob.glob(dmDLLPathGlob): + if os.path.exists(dmDLLPath): + return dmDLLPath + + raise Exception( + "Unable to locate Chip Server DLL (%s); expected location: %s" + % (NATIVE_LIBRARY_BASE_NAME, scriptDir) + ) + + +class NativeLibraryHandleMethodArguments: + """Convenience wrapper to set native method argtype and restype for methods.""" + + def __init__(self, handle): + self.handle = handle + + def Set(self, methodName: str, resultType, argumentTypes: list): + method = getattr(self.handle, methodName) + method.restype = resultType + method.argtype = argumentTypes + + +_nativeLibraryHandle: ctypes.CDLL = None + + +def GetLibraryHandle(cb: PostAttributeChangeCallback) -> ctypes.CDLL: + """Get a memoized handle to the chip native code dll.""" + + global _nativeLibraryHandle + if _nativeLibraryHandle is None: + _nativeLibraryHandle = ctypes.CDLL(FindNativeLibraryPath()) + + setter = NativeLibraryHandleMethodArguments(_nativeLibraryHandle) + setter.Set("pychip_server_native_init", None, []) + setter.Set("pychip_server_set_callbacks", + None, [PostAttributeChangeCallback]) + + _nativeLibraryHandle.pychip_server_native_init() + _nativeLibraryHandle.pychip_server_set_callbacks(cb) + + return _nativeLibraryHandle diff --git a/src/controller/python/chip/server/types.py b/src/controller/python/chip/server/types.py new file mode 100644 index 00000000000000..9aaeb60e10c8ea --- /dev/null +++ b/src/controller/python/chip/server/types.py @@ -0,0 +1,14 @@ +from ctypes import CFUNCTYPE, py_object, c_char_p, c_uint8, c_uint16 + +PostAttributeChangeCallback = CFUNCTYPE( + None, + # py_object, + c_uint16, + c_uint16, + c_uint16, + c_uint8, + c_uint16, + c_uint8, + c_uint16, + c_char_p, +) diff --git a/src/platform/python.gni b/src/platform/python.gni new file mode 100644 index 00000000000000..9a0de04afb449e --- /dev/null +++ b/src/platform/python.gni @@ -0,0 +1,23 @@ +# Copyright (c) 2021 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import("//build_overrides/chip.gni") + +declare_args() { + # Build controller (set to false for a device) + chip_controller = true + + # Pass the directory to the BUILD.gn that pulls in the generated data-model files, e.g. ///examples/lighting-app/lighting-common + chip_data_model = "" +}