diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..594e11c --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf +*.pyc + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# emacs +.dir-locals.el + +# emacs temp file suffixes +*~ +.#* +\#*# + +# eclipse setting +.settings + +# MacOS directory files +.DS_Store + +# Application project files +**/sdkconfig +**/sdkconfig.old +**/build +**/cloud_cfg/*.key +**/cloud_cfg/*.crt +**/cloud_cfg/*.info + +# node_modules +docs/docusaurus/website/node_modules + +# cli logs +**/logs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..c08408b --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,69 @@ +stages: + - build + +variables: + BATCH_BUILD: "1" + V: "0" + MAKEFLAGS: "-j5 --no-keep-going" + IDF_PATH: "$CI_PROJECT_DIR/esp-idf" + IDF_CI_BUILD: "1" + +build_demo: + stage: build + image: $CI_DOCKER_REGISTRY/esp32-ci-env + tags: + - build + artifacts: + paths: + - examples/gpio/build/gpio.bin + - examples/switch/build/switch.bin + - examples/led_light/build/led_light.bin + - examples/fan/build/fan.bin + - examples/temperature_sensor/build/temperature_sensor.bin + - examples/multi_device/build/multi_device.bin + expire_in: 6 mos + + script: + # add gitlab ssh key + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - echo -n $GITLAB_KEY > ~/.ssh/id_rsa_base64 + - base64 --decode --ignore-garbage ~/.ssh/id_rsa_base64 > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - echo -e "Host gitlab.espressif.cn\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config + - git --version + - git clone --branch master --depth 1 $GITLAB_SSH_SERVER/idf/esp-idf.git + - cd esp-idf + - export IDF_PATH=$PWD + - ./install.sh + - . export.sh + - cd .. + - cd examples/gpio + - idf.py build + - rm -rf build/ sdkconfig + - make -j8 + - cd ../switch + - idf.py build + - rm -rf build/ sdkconfig + - make defconfig + - make -j8 + - cd ../led_light + - idf.py build + - rm -rf build/ sdkconfig + - make defconfig + - make -j8 + - cd ../fan + - idf.py build + - rm -rf build/ sdkconfig + - make defconfig + - make -j8 + - cd ../temperature_sensor + - idf.py build + - rm -rf build/ sdkconfig + - make defconfig + - make -j8 + - cd ../multi_device + - idf.py build + - rm -rf build/ sdkconfig + - make defconfig + - make -j8 diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..f63ddd7 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,17 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Optionally build your docs in additional formats such as PDF and ePub +formats: + - htmlzip + - pdf + +# Optionally set the version of Python and requirements required to build your docs +python: + version: 3.7 + install: + - requirements: docs/requirements.txt diff --git a/README.md b/README.md index 3441142..a2916a6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,37 @@ -# ESP RainMaker +# ESP RainMaker (Beta) -Coming Soon! +## Introduction + +ESP RainMaker is an end-to-end solution offered by Espressif to enable remote control and monitoring for ESP32-S2 and ESP32 based products without any configuration required in the Cloud. The primary components of this solution are: + +- Claiming Service (to get the Cloud connectivity credentials) +- RainMaker Agent (i.e. this repo, to develop the firmware) +- RainMaker Cloud (backend, offering remote connectivity) +- RainMaker Phone App/CLI (Client utilities for remote access) + + +The key features of ESP RainMaker are: + +1. Ability to define own devices and parameters, of any type, in the firmware. +2. Zero configuration required on the Cloud. +3. Phone apps that dynamically render the UI as per the device information. + +## Phone Apps + +### Android + +- [Google PlayStore](https://play.google.com/store/apps/details?id=com.espressif.rainmaker) +- [Direct APK](https://github.com/espressif/esp-rainmaker/wiki) + +### iOS +- [Apple App Store](https://apps.apple.com/app/esp-rainmaker/id1497491540) + +## Documentation + +Please check the ESP RainMaker documentation [here](http://rainmaker.espressif.com/docs/get-started.html) to get started. + +Each example has its own README with additional information about using the example. + +### API Documentation Build Status + +[![Documentation Status](https://readthedocs.com/projects/espressif-esp-rainmaker/badge/?version=latest)](https://docs.espressif.com/projects/esp-rainmaker/en/latest/) diff --git a/cli/html/welcome.html b/cli/html/welcome.html new file mode 100644 index 0000000..c7bf4e6 --- /dev/null +++ b/cli/html/welcome.html @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + Rainmaker Login + + + + + + +
+
+
+ +
+ +
+
+
+
+ + + diff --git a/cli/rainmaker.py b/cli/rainmaker.py new file mode 100755 index 0000000..f9334d8 --- /dev/null +++ b/cli/rainmaker.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 argparse +from rmaker_cmd.node import get_nodes, get_node_config, get_node_status,\ + set_params, get_params, remove_node, get_mqtt_host +from rmaker_cmd.user import signup, login, forgot_password +from rmaker_cmd.provision import provision +from rmaker_cmd.test import test +from rmaker_lib.logger import log + + +def main(): + parser = argparse.ArgumentParser() + parser.set_defaults(func=None) + subparsers = parser.add_subparsers(help='Functions') + + signup_parser = subparsers.add_parser("signup", + help="Sign up for ESP Rainmaker") + signup_parser.add_argument('email', + type=str, + metavar='', + help='Email address of the user') + signup_parser.set_defaults(func=signup) + + login_parser = subparsers.add_parser("login", + help="Login to ESP Rainmaker") + login_parser.add_argument('--email', + type=str, + help='Email address of the user') + login_parser.set_defaults(func=login) + + forgot_password_parser = subparsers.add_parser("forgotpassword", + help="Reset the password") + forgot_password_parser.add_argument('email', + type=str, + metavar='', + help='Email address of the user') + forgot_password_parser.set_defaults(func=forgot_password) + + getnodes_parser = subparsers.add_parser('getnodes', + help='List all nodes associated\ + with the user') + getnodes_parser.set_defaults(func=get_nodes) + + getnodeconfig_parser = subparsers.add_parser('getnodeconfig', + help='Get node configuration') + getnodeconfig_parser.add_argument('nodeid', + type=str, + metavar='', + help='Node ID for the node') + getnodeconfig_parser.set_defaults(func=get_node_config) + + getnodestatus_parser = subparsers.add_parser('getnodestatus', + help='Get online/offline\ + status of the node') + getnodestatus_parser.add_argument('nodeid', + type=str, + metavar='', + help='Node ID for the node') + getnodestatus_parser.set_defaults(func=get_node_status) + + setparams_parser = subparsers.add_parser('setparams', + help='Set node parameters.\ + Note: Enter JSON data in\ + single quotes') + setparams_parser.add_argument('nodeid', + metavar='', + help='Node ID for the node') + setparams_parser = setparams_parser.add_mutually_exclusive_group( + required=True) + setparams_parser.add_argument('--filepath', + help='Path of the JSON file\ + containing parameters to be set') + setparams_parser.add_argument('--data', + help='JSON data containing parameters\ + to be set. Note: Enter JSON data\ + in single quotes') + setparams_parser.set_defaults(func=set_params) + + getparams_parser = subparsers.add_parser('getparams', + help='Get node parameters') + getparams_parser.add_argument('nodeid', + type=str, + metavar='', + help='Node ID for the node') + getparams_parser.set_defaults(func=get_params) + + remove_node_parser = subparsers.add_parser('removenode', + help='Remove user node mapping') + remove_node_parser.add_argument('nodeid', + type=str, + metavar='', + help='Node ID for the node') + remove_node_parser.set_defaults(func=remove_node) + + provision_parser = subparsers.add_parser('provision', + help='Provision the node\ + to join Wi-Fi network') + provision_parser.add_argument('pop', + type=str, + metavar='', + help='Proof of possesion for the node') + provision_parser.set_defaults(func=provision) + + getmqtthost_parser = subparsers.add_parser('getmqtthost', + help='Get the MQTT Host URL\ + to be used in the\ + firmware') + getmqtthost_parser.set_defaults(func=get_mqtt_host) + + test_parser = subparsers.add_parser('test', + help='Test commands to check\ + user node mapping') + test_parser.add_argument('--addnode', + metavar='', + help='Add user node mapping') + test_parser.set_defaults(func=test) + + args = parser.parse_args() + + if args.func is not None: + try: + args.func(vars=vars(args)) + except KeyboardInterrupt: + log.debug('KeyboardInterrupt occurred. Login session is aborted.') + print("\nExiting...") + except Exception as err: + log.error(err) + else: + parser.print_help() + + +if __name__ == '__main__': + main() diff --git a/cli/requirements.txt b/cli/requirements.txt new file mode 100644 index 0000000..3f8947a --- /dev/null +++ b/cli/requirements.txt @@ -0,0 +1,12 @@ +argparse +requests>=2.22.0 +cmd2>=0.9.18 +protobuf>=3.10.0 +packaging>=19.2 +oauth2client>=4.1.3 +click>=5.0 +pyserial>=3.0 +future>=0.15.2 +cryptography==2.4.2 +pyparsing>=2.0.3,<2.4.0 +pyelftools>=0.22 \ No newline at end of file diff --git a/cli/rmaker_cmd/__init__.py b/cli/rmaker_cmd/__init__.py new file mode 100644 index 0000000..3b0d78c --- /dev/null +++ b/cli/rmaker_cmd/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. diff --git a/cli/rmaker_cmd/browserlogin.py b/cli/rmaker_cmd/browserlogin.py new file mode 100644 index 0000000..ebc8e1a --- /dev/null +++ b/cli/rmaker_cmd/browserlogin.py @@ -0,0 +1,211 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 webbrowser +import socket +import requests +from oauth2client import _helpers +from six.moves import BaseHTTPServer, http_client, urllib +import os +import sys + +try: + from rmaker_lib import serverconfig, configmanager + from rmaker_lib.exceptions import SSLError, NetworkError + from rmaker_lib.logger import log +except ImportError as err: + print("Failed to import ESP Rainmaker library. " + str(err)) + raise err + + +class HttpdServer(BaseHTTPServer.HTTPServer): + """ + A server to handle requests on localhost. + + Waits for a single request and parses the query parameters + and then stops serving. + """ + query_params = {} + + +class HttpdRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """ + A HTTP handler of requests on localhost. + """ + + def do_GET(self): + """ + Handle a GET request and + writes the ESP Rainmaker Welcome HTML page(response) + back to HTTP Client + + :raises Exception: If there is any File Handling Issue + + :return: None on Success and Failure + :rtype: None + """ + log.debug('Loading the welcome page after successful login.') + self.send_response(http_client.OK) + self.send_header('Content-type', 'text/html') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + parts = urllib.parse.urlparse(self.path) + query = _helpers.parse_unique_urlencoded(parts.query) + self.server.query_params = query + index_file = os.path.join(os.path.expanduser('.'), 'html/welcome.html') + + try: + with open(index_file, 'rb') as home_page: + self.wfile.write(home_page.read()) + except Exception as file_err: + log.error(file_err) + sys.exit(1) + + def log_message(self, format, *args): + """ + Do not log messages to the command prompt. + """ + + +def get_free_port(): + """ + Get Free port + + :return: port on Success + :rtype: int + """ + tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + tcp.bind(('', 0)) + addr, port = tcp.getsockname() + tcp.close() + return port + + +def browser_login(): + """ + Opens browser with login url using Httpd Server. + + :raises Exception: If there is an HTTP issue while + logging in through browser + + :return: None on Success and Failure + :rtype: None + """ + log.info('Logging in through browser') + server_instance = None + for attempt in range(10): + try: + port = get_free_port() + server_instance = HttpdServer(('localhost', port), + HttpdRequestHandler) + # Added timeout to handle keyboard interrupts for browser login. + server_instance.timeout = 0.5 + break + except socket.error as err: + log.warn('Error %s. Port %s is not available.' + 'Trying with next port.', err, port) + + if server_instance is None: + log.error('Error: Could not launch local webserver.' + 'Use --email option instead.') + return + + url = serverconfig.LOGIN_URL + str(port) +\ + '&host_url=' + serverconfig.HOST + 'login' +\ + '&github_url=' + serverconfig.EXTERNAL_LOGIN_URL +\ + str(port) + + print('Opening browser window for login...') + open_status = webbrowser.open(url) + if open_status is False: + log.error('Failed to open login page. Please try again.') + return + else: + print('Use the browser for login. Press ctrl+C to abort.') + log.debug('Web browser opened. Waiting for user login.') + try: + while True: + server_instance.handle_request() + if 'error' in server_instance.query_params: + log.error('Authentication Error: "%s". Description: "%s" ' + + server_instance.query_params['error'] + + server_instance.query_params.ge('error_description')) + return + if 'code' in server_instance.query_params: + log.debug('Login successful. Received authorization code.') + code = server_instance.query_params['code'] + get_tokens(code) + print('Login successful') + return + + if 'id_token' in server_instance.query_params and \ + 'refresh_token' in server_instance.query_params: + log.debug('Login successful.' + 'Received idtoken and refresh token.') + config_data = {} + config_data['idtoken'] = server_instance.query_params[ + 'id_token' + ] + config_data['refreshtoken'] = server_instance.query_params[ + 'refresh_token' + ] + config_data['accesstoken'] = server_instance.query_params[ + 'access_token' + ] + configmanager.Config().set_config(config_data) + print('Login successful') + return + except Exception as browser_login_err: + log.error(browser_login_err) + + +def get_tokens(code): + """ + Get access token and set the config file after successful browser login. + + :raises Exception: If there is an HTTP issue in getting access token + :raises SystemExit: If Exception is raised + + :return: None on Success and Failure + :rtype: None + """ + log.info('Getting access tokens using authorization code.') + client_id = serverconfig.CLIENT_ID + request_data = 'grant_type=authorization_code&client_id=' + client_id +\ + '&code=' + code + '&redirect_uri=' +\ + serverconfig.REDIRECT_URL + + request_header = {'content-type': 'application/x-www-form-urlencoded'} + try: + response = requests.post(url=serverconfig.TOKEN_URL, + data=request_data, + headers=request_header, + verify=configmanager.CERT_FILE) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception as get_token_err: + log.error(get_token_err) + sys.exit(1) + else: + config_data = {} + result = response.json() + config_data['idtoken'] = result['id_token'] + config_data['refreshtoken'] = result['refresh_token'] + config_data['accesstoken'] = result['access_token'] + log.debug('Received access tokens using authorization code.') + configmanager.Config().set_config(config_data) + return diff --git a/cli/rmaker_cmd/node.py b/cli/rmaker_cmd/node.py new file mode 100644 index 0000000..1b232b3 --- /dev/null +++ b/cli/rmaker_cmd/node.py @@ -0,0 +1,252 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 json +import re +import requests +from pathlib import Path + +try: + from rmaker_lib import session, node, serverconfig, configmanager + from rmaker_lib.exceptions import NetworkError, InvalidJSONError, SSLError + from rmaker_lib.logger import log +except ImportError as err: + print("Failed to import ESP Rainmaker library. " + str(err)) + raise err + + +def get_nodes(vars=None): + """ + List all nodes associated with the user. + + :param vars: No Parameters passed, defaults to `None` + :type vars: dict | None + + :raises Exception: If there is an HTTP issue while getting nodes + + :return: None on Success + :rtype: None + """ + try: + s = session.Session() + nodes = s.get_nodes() + except Exception as get_nodes_err: + log.error(get_nodes_err) + else: + if len(nodes.keys()) == 0: + print('User is not associated with any nodes.') + return + for key in nodes.keys(): + print(nodes[key].get_nodeid()) + return + + +def get_node_config(vars=None): + """ + Shows the configuration of the node. + + :param vars: `nodeid` as key - Node ID for the node, defaults to `None` + :type vars: dict | None + + :raises Exception: If there is an HTTP issue while getting node config + + :return: None on Success + :rtype: None + """ + try: + n = node.Node(vars['nodeid'], session.Session()) + node_config = n.get_node_config() + except Exception as get_nodes_err: + log.error(get_nodes_err) + else: + print(json.dumps(node_config, indent=4)) + return + + +def get_node_status(vars=None): + """ + Shows the online/offline status of the node. + + :param vars: `nodeid` as key - Node ID for the node, defaults to `None` + :type vars: dict | None + + :raises Exception: If there is an HTTP issue while getting node status + + :return: None on Success + :rtype: None + """ + try: + n = node.Node(vars['nodeid'], session.Session()) + node_status = n.get_node_status() + except Exception as get_node_status_err: + log.error(get_node_status_err) + else: + print(json.dumps(node_status, indent=4)) + return + + +def set_params(vars=None): + """ + Set parameters of the node. + + :param vars: + `nodeid` as key - Node ID for the node,\n + `data` as key - JSON data containing parameters to be set `or`\n + `filepath` as key - Path of the JSON file containing parameters + to be set,\n + defaults to `None` + :type vars: dict | None + + :raises Exception: If there is an HTTP issue while setting params or + JSON format issue in HTTP response + + :return: None on Success + :rtype: None + """ + log.info('Setting params of the node with nodeid : ' + vars['nodeid']) + data = vars['data'] + filepath = vars['filepath'] + + if data is not None: + log.debug('Setting node parameters using JSON data.') + # Trimming white spaces except the ones between two strings + data = re.sub(r"(? 0: + try: + # If session is expired then to initialise the new session + # internet connection is required. + node_object = node.Node(node_id, session.Session()) + except SSLError: + log.error(SSLError()) + print(PROVISION_FAILURE_MSG) + return + except NetworkError: + time.sleep(5) + log.warn("Session is expired. Initialising new session.") + pass + except Exception as node_init_err: + log.error(node_init_err) + print(PROVISION_FAILURE_MSG) + return + else: + break + retries -= 1 + + if node_object is None: + print('Please check the internet connectivity.') + print(PROVISION_FAILURE_MSG) + return + retries = MAX_HTTP_CONNECTION_RETRIES + request_id = None + while retries > 0: + try: + log.debug('Adding user-node association.') + request_id = node_object.add_user_node_mapping(secret_key) + except SSLError: + log.error(SSLError()) + print(PROVISION_FAILURE_MSG) + return + except Exception as user_node_mapping_err: + print('Sending User-Node association request to ' + 'ESP RainMaker Cloud - Failed\nRetrying...') + log.warn(user_node_mapping_err) + pass + else: + if request_id is not None: + log.debug('User-node mapping added successfully' + 'with request_id' + + request_id) + break + time.sleep(5) + retries -= 1 + + if request_id is None: + print('Sending User-Node association request to' + 'ESP RainMaker Cloud - Failed') + print(PROVISION_FAILURE_MSG) + return + print('Sending User-Node association request to' + 'ESP RainMaker Cloud - Successful') + + status = None + while True: + log.debug('Checking user-node association status.') + try: + status = node_object.get_mapping_status(request_id) + except SSLError: + log.error(SSLError()) + print(PROVISION_FAILURE_MSG) + return + except Exception as mapping_status_err: + log.warn(mapping_status_err) + pass + else: + log.debug('User-node association status ' + status) + if status == 'requested': + print('Checking User Node association status -' + 'Requested\nRetrying...') + elif status == 'confirmed': + print('Checking User Node association status - Confirmed') + print('Provisioning was Successful.') + return + elif status == 'timedout': + print('Checking User Node association status - Timeout') + print(PROVISION_FAILURE_MSG) + return + elif status == 'discarded': + print('Checking User Node association status - Discarded') + print(PROVISION_FAILURE_MSG) + return + time.sleep(5) + + if status is None: + print(PROVISION_FAILURE_MSG) + print('Checking User Node association status failed. ' + 'Please check the internet connectivity.') + return + return diff --git a/cli/rmaker_cmd/test.py b/cli/rmaker_cmd/test.py new file mode 100644 index 0000000..1ccd1b8 --- /dev/null +++ b/cli/rmaker_cmd/test.py @@ -0,0 +1,85 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 uuid +import time +import sys + +try: + from rmaker_lib import session, node, configmanager + from rmaker_lib.logger import log +except ImportError as err: + print("Failed to import ESP Rainmaker library. " + str(err)) + raise err + + +def add_node(node_object): + secret_key = str(uuid.uuid4()) + request_id = node_object.add_user_node_mapping(secret_key) + return request_id, secret_key + + +def check_status(node_object, request_id): + status = None + while True: + log.debug('Checking user-node association status.') + try: + status = node_object.get_mapping_status(request_id) + except Exception as mapping_status_err: + log.error(mapping_status_err) + return + else: + log.debug('User-node association status ' + status) + if status == 'requested': + print('Checking User Node association status - Requested\n' + 'Retrying...') + elif status == 'confirmed': + print('Checking User Node association status - Confirmed') + return + elif status == 'timedout': + print('Checking User Node association status - Timeout') + return + elif status == 'discarded': + print('Checking User Node association status - Discarded') + return + time.sleep(5) + return + + +def test(vars=None): + """ + Check user node mapping + + :param vars: + `addnode` as key - Node ID of node to be mapped to user,\n + defaults to `None` + :type vars: dict + + """ + node_id = vars['addnode'] + if node_id is None or not vars.get('addnode'): + print('Error: The following arguments are required: --addnode\n' + 'Check usage: rainmaker.py [-h]\n') + sys.exit(0) + node_object = node.Node(node_id, session.Session()) + request_id, secret_key = add_node(node_object) + config = configmanager.Config() + user_id = config.get_user_id() + print('Use following command on node simulator or ' + 'node CLI to confirm user node mapping:') + print(" add-user " + user_id + " " + secret_key) + print("---------------------------------------------------") + print("RequestId for user node mapping request : " + request_id) + check_status(node_object, request_id) + return diff --git a/cli/rmaker_cmd/user.py b/cli/rmaker_cmd/user.py new file mode 100644 index 0000000..0fe5479 --- /dev/null +++ b/cli/rmaker_cmd/user.py @@ -0,0 +1,163 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 sys +import re +import getpass +try: + from rmaker_lib import user + from rmaker_lib.logger import log +except ImportError as err: + print("Failed to import ESP Rainmaker library. " + str(err)) + raise err + + +from rmaker_cmd.browserlogin import browser_login + +MAX_PASSWORD_CHANGE_ATTEMPTS = 3 + + +def signup(vars=None): + """ + User signup to the ESP Rainmaker. + + :param vars: `email` as key - Email address of the user, defaults to `None` + :type vars: dict + + :raises Exception: If there is any issue in signup for user + + :return: None on Success + :rtype: None + """ + log.info('Signing up the user ' + vars['email']) + u = user.User(vars['email']) + password = get_password() + try: + status = u.signup_request(password) + except Exception as signup_err: + log.error(signup_err) + else: + if status is True: + verification_code = input('Enter verification code sent on your' + 'Email.\n Verification Code : ') + try: + status = u.signup(verification_code) + except Exception as signup_err: + log.error(signup_err) + return + print('Signup Successful\n' + 'Please login to continue with ESP Rainmaker CLI') + else: + log.error('Signup failed. Please try again.') + return + + +def login(vars=None): + """ + First time login of the user. + + :param vars: `email` as key - Email address of the user, defaults to `None` + :type vars: dict + + :raises Exception: If there is any issue in login for user + + :return: None on Success + :rtype: None + """ + log.info('Signing in the user. Username ' + str(vars['email'])) + if vars['email'] is None: + browser_login() + return + u = user.User(vars['email']) + try: + u.login() + except Exception as login_err: + log.error(login_err) + else: + print('Login Successful') + + +def forgot_password(vars=None): + """ + Forgot password request to reset the password. + + :param vars: `email` as key - Email address of the user, defaults to `None` + :type vars: dict + + :raises Exception: If there is an HTTP issue while + changing password for user + + :return: None on Success and Failure + :rtype: None + """ + log.info('Changing user password. Username ' + vars['email']) + u = user.User(vars['email']) + status = False + try: + status = u.forgot_password() + except Exception as forgot_pwd_err: + log.error(forgot_pwd_err) + else: + verification_code = input('Enter verification code sent on your Email.' + '\n Verification Code : ') + password = get_password() + if status is True: + try: + log.debug('Received verification code on email ' + + vars['email']) + status = u.forgot_password(password, verification_code) + except Exception as forgot_pwd_err: + log.error(forgot_pwd_err) + else: + print('Password changed successfully.' + 'Please login with the new password.') + else: + log.error('Failed to reset password. Please try again.') + return + + +def get_password(): + """ + Get Password as input and perform basic password validation checks. + + :raises SystemExit: If there is an issue in getting password + + :return: Password for User on Success + :rtype: str + """ + log.info('Doing basic password confirmation checks.') + password_policy = '8 characters, 1 digit, 1 uppercase and 1 lowercase.' + password_change_attempt = 0 + + print('Choose a password') + while password_change_attempt < MAX_PASSWORD_CHANGE_ATTEMPTS: + log.debug('Password change attempt number ' + + str(password_change_attempt+1)) + password = getpass.getpass('Password : ') + if len(password) < 8 or re.search(r"\d", password) is None or\ + re.search(r"[A-Z]", password) is None or\ + re.search(r"[a-z]", password) is None: + print('Password should contain at least', password_policy) + password_change_attempt += 1 + continue + confirm_password = getpass.getpass('Confirm Password : ') + if password == confirm_password: + return password + else: + print('Passwords do not match!\n' + 'Please enter the password again ..') + password_change_attempt += 1 + + log.error('Maximum attempts to change password over. Please try again.') + sys.exit(1) diff --git a/cli/rmaker_lib/__init__.py b/cli/rmaker_lib/__init__.py new file mode 100644 index 0000000..3b0d78c --- /dev/null +++ b/cli/rmaker_lib/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. diff --git a/cli/rmaker_lib/configmanager.py b/cli/rmaker_lib/configmanager.py new file mode 100644 index 0000000..73d080f --- /dev/null +++ b/cli/rmaker_lib/configmanager.py @@ -0,0 +1,379 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 json +import errno +import os +import base64 +import time +import requests +from pathlib import Path +from os import path + +from rmaker_lib import serverconfig +from rmaker_lib.exceptions import NetworkError,\ + InvalidConfigError,\ + InvalidUserError,\ + InvalidApiVersionError,\ + ExpiredSessionError,\ + SSLError +from rmaker_lib.logger import log + +CONFIG_DIRECTORY = '.espressif/rainmaker' +CONFIG_FILE = CONFIG_DIRECTORY + '/rainmaker_config.json' +HOME_DIRECTORY = '~/' +CURR_DIR = os.path.dirname(__file__) +CERT_FILE = CURR_DIR + '/../server_cert/server_cert.pem' + + +class Config: + """ + Config class used to instantiate instances of config to + perform various get/set configuration operations + """ + def set_config(self, data, config_file=CONFIG_FILE): + """ + Set the configuration file. + + :params data: Config Data to write to file + :type data: dict + + :params config_file: Config filename to write config data to + :type data: str + + :raises OSError: If there is an OS issue while creating new directory + for config file + :raises Exception: If there is a File Handling error while saving + config to file + + :return: None on Success and Failure + :rtype: None + """ + log.info("Configuring config file.") + file_dir = Path(path.expanduser(HOME_DIRECTORY + CONFIG_DIRECTORY)) + file = Path(path.expanduser(HOME_DIRECTORY) + config_file) + if not file.exists(): + try: + if not file_dir.exists(): + log.debug('Config directory does not exist,' + 'creating new directory.') + os.makedirs(path.expanduser(HOME_DIRECTORY) + + CONFIG_DIRECTORY) + except OSError as set_config_err: + log.error(set_config_err) + if set_config_err.errno != errno.EEXIST: + raise set_config_err + try: + with open(path.join(path.expanduser(HOME_DIRECTORY), + config_file), 'w') as config_file: + json.dump(data, config_file) + except Exception as set_config_err: + raise set_config_err + log.info("Configured config file successfully.") + + def get_config(self, node=None, config_file=CONFIG_FILE): + """ + Get the configuration details from config file. + + :params node: Name of node + :type node: str + + :params config_file: Config filename to read config data from + :type data: str + + :raises Exception: If there is a File Handling error while reading + from config file + + :return: + idtoken - Id Token from config saved\n + refreshtoken - Refresh Token from config saved\n + accesstoken - Access Token from config saved\n + :rtype: str + """ + file = Path(path.expanduser(HOME_DIRECTORY) + config_file) + if not file.exists(): + if node and node.lower() == 'esp32': + return None + else: + raise InvalidUserError + try: + with open(path.join(path.expanduser(HOME_DIRECTORY), + config_file), 'r') as config_file: + data = json.load(config_file) + if node and node.lower() == "esp32": + return data + else: + idtoken = data['idtoken'] + refresh_token = data['refreshtoken'] + access_token = data['accesstoken'] + except Exception as get_config_err: + raise get_config_err + return idtoken, refresh_token, access_token + + def get_binary_config(self, node=None, config_file=CONFIG_FILE): + """ + Get the configuration details from binary config file. + + :params node: Name of node + :type node: str + + :params config_file: Config filename to read config data from + :type data: str + + :raises Exception: If there is a File Handling error while reading + from config file + + :return: Config data read from file on Success, None on Failure + :rtype: str | None + """ + file = Path(path.expanduser(HOME_DIRECTORY) + config_file) + if not file.exists(): + if node and node.lower() == 'esp32': + return None + try: + with open(path.join(path.expanduser(HOME_DIRECTORY), + config_file), 'rb') as config_file: + data = config_file.read() + if node and node.lower() == "esp32": + return data + except Exception as get_config_err: + raise get_config_err + return + + def update_config(self, access_token, id_token): + """ + Update the configuration file. + + :params access_token: Access Token to update in config file + :type access_token: str + + :params id_token: Id Token to update in config file + :type id_token: str + + :raises OSError: If there is an OS issue while creating new directory + for config file + :raises Exception: If there is a FILE Handling error while reading + from/writing config to file + + :return: None on Success and Failure + :rtype: None + """ + file = Path(path.expanduser(HOME_DIRECTORY) + CONFIG_FILE) + if not file.exists(): + try: + os.makedirs(path.expanduser(HOME_DIRECTORY) + CONFIG_DIRECTORY) + except OSError as set_config_err: + if set_config_err.errno != errno.EEXIST: + raise set_config_err + try: + with open(path.join(path.expanduser(HOME_DIRECTORY), + CONFIG_FILE), 'r') as config_file: + config_data = json.load(config_file) + config_data['accesstoken'] = access_token + config_data['idtoken'] = id_token + with open(path.join(path.expanduser(HOME_DIRECTORY), + CONFIG_FILE), 'w') as config_file: + json.dump(config_data, config_file) + except Exception as set_config_err: + raise set_config_err + + def get_token_attribute(self, attribute_name, is_access_token=False): + """ + Get access token attributes. + + :params attribute_name: Attribute Name + :type attribute_name: str + + :params is_access_token: Is Access Token + :type is_access_token: bool + + :raises InvalidConfigError: If there is an error in the config + :raises Exception: If there is a File Handling error while reading + from/writing config to file + + :return: Attribute Value on Success, None on Failure + :rtype: int | str | None + """ + if is_access_token: + log.debug('Getting access token for attribute ' + attribute_name) + _, _, token = self.get_config() + else: + log.debug('Getting idtoken for attribute ' + attribute_name) + token, _, _ = self.get_config() + token_payload = token.split('.')[1] + if len(token_payload) % 4: + token_payload += '=' * (4 - len(token_payload) % 4) + try: + str_token_payload = base64.b64decode(token_payload).decode("utf-8") + # If user is logged in through github then to extend session + # we need 'cognito:username' (Github generated username) as email + if attribute_name == 'email': + if 'identities' in json.loads(str_token_payload): + return json.loads(str_token_payload)['cognito:username'] + attribute_value = json.loads(str_token_payload)[attribute_name] + except Exception: + raise InvalidConfigError + if attribute_value is None: + raise InvalidConfigError + return attribute_value + + def get_access_token(self): + """ + Get Access Token for User + + :raises InvalidConfigError: If there is an issue in getting config + from file + + :return: Access Token on Success + :rtype: str + """ + _, _, access_token = self.get_config() + if access_token is None: + raise InvalidConfigError + if self.__is_valid_token() is False: + username = self.get_token_attribute('email') + refresh_token = self.get_refresh_token() + access_token, id_token = self.__get_new_token(username, + refresh_token) + self.update_config(access_token, id_token) + return access_token + + def get_user_id(self): + """ + Get User Id + + :return: Attribute value for attribute name passed + :rtype: str + """ + return self.get_token_attribute('custom:user_id') + + def get_refresh_token(self): + """ + Get Refresh Token + + :raises InvalidApiVersionError: If current API version is not supported + + :return: Refresh Token + :rtype: str + """ + if self.__is_valid_version() is False: + raise InvalidApiVersionError + _, refresh_token, _ = self.get_config() + return refresh_token + + def __is_valid_token(self): + """ + Check if access token is valid i.e. login session is still active + or session is expired + + :return True on Success and False on Failure + :rtype: bool + """ + log.info("Checking for session timeout.") + exp_timestamp = self.get_token_attribute('exp', is_access_token=True) + current_timestamp = int(time.time()) + if exp_timestamp > current_timestamp: + return True + log.info("Session expired.") + return False + + def __is_valid_version(self): + """ + Check if API Version is valid + + :raises NetworkError: If there is a network connection issue during + HTTP request for getting version + :raises Exception: If there is an HTTP issue or JSON format issue in + HTTP response + + :return: True on Success, False on Failure + :rtype: bool + """ + log.info("Checking for supported version.") + path = 'apiversions' + request_url = serverconfig.HOST.split(serverconfig.VERSION)[0] + path + try: + log.debug("Version check request url : " + request_url) + response = requests.get(url=request_url, verify=CERT_FILE) + log.debug("Version check response : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception as ver_err: + raise ver_err + + try: + response = json.loads(response.text) + except Exception as json_decode_err: + raise json_decode_err + + if 'supported_versions' in response: + supported_versions = response['supported_versions'] + if serverconfig.VERSION in supported_versions: + supported_versions.sort() + latest_version = supported_versions[len(supported_versions) + - 1] + if serverconfig.VERSION < latest_version: + print('Please check the updates on GitHub for newer' + 'functionality enabled by ' + latest_version + + ' APIs.') + return True + return False + + def __get_new_token(self, username, refresh_token): + """ + Get new token for User Login Session + + :raises NetworkError: If there is a network connection issue during + HTTP request for getting token + :raises Exception: If there is an HTTP issue or JSON format issue in + HTTP response + + :return: accesstoken and idtoken on Success, None on Failure + :rtype: str | None + + """ + log.info("Extending user login session.") + path = 'login' + request_payload = { + 'user_name': username, + 'refreshtoken': refresh_token + } + + request_url = serverconfig.HOST + path + try: + log.debug("Extend session url : " + request_url) + response = requests.post(url=request_url, + data=json.dumps(request_payload), + verify=CERT_FILE) + response.raise_for_status() + log.debug("Extend session response : " + response.text) + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception: + raise ExpiredSessionError + + try: + response = json.loads(response.text) + except Exception: + raise ExpiredSessionError + + if 'accesstoken' in response and 'idtoken' in response: + log.info("User session extended successfully.") + return response['accesstoken'], response['idtoken'] + return None diff --git a/cli/rmaker_lib/device.py b/cli/rmaker_lib/device.py new file mode 100644 index 0000000..d010c5e --- /dev/null +++ b/cli/rmaker_lib/device.py @@ -0,0 +1,61 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 rmaker_lib.logger import log + + +class Device: + """ + Device class used to instantiate instances of device + to perform various device operations. + """ + def __init__(self, node, device): + """ + Instantiate device object. + """ + log.info("Initialising device " + device['name']) + self.__node = node + self.__name = device['name'] + self.__params = {} + for param in device['params']: + self.__params[param["name"]] = '' + + def get_device_name(self): + """ + Get the device name. + """ + return self.__name + + def get_params(self): + """ + Get parameters of the device. + """ + params = {} + node_params = self.__node.get_node_params() + if node_params is None: + return params + for key in self.__params.keys(): + params[key] = node_params[self.__name + '.' + key] + return params + + def set_params(self, data): + + """ + Set parameters of the device. + Input data contains the dictionary of device parameters. + """ + request_payload = {} + for key in data: + request_payload[self.__name + '.' + key] = data[key] + return self.__node.set_node_params(request_payload) diff --git a/cli/rmaker_lib/exceptions.py b/cli/rmaker_lib/exceptions.py new file mode 100644 index 0000000..c88d920 --- /dev/null +++ b/cli/rmaker_lib/exceptions.py @@ -0,0 +1,72 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. + + +class NetworkError(Exception): + """ Raised when internet connection is not available """ + def __str__(self): + return 'Please check the internet connectivity.\ + No internet connection available.' + + +class InvalidJSONError(Exception): + """ Raised for invalid JSON input """ + def __str__(self): + return 'Invalid JSON received.' + + +class ExpiredSessionError(Exception): + """ Raised when user session expires """ + def __str__(self): + return 'User session is expired. Please login again.' + + +class InvalidConfigError(Exception): + """ Raised for invalid configuration """ + def __str__(self): + return 'Invalid configuration. Please login again.' + + +class InvalidUserError(Exception): + """ Raised when config file does not exists """ + def __str__(self): + return 'User not logged in. Please use login command.' + + +class AuthenticationError(Exception): + """ Raised when user login fails """ + def __str__(self): + return 'Login failed. Please try again' + + +class InvalidApiVersionError(Exception): + """ Raised when current API version is not supported """ + def __str__(self): + return 'API Version not supported. Please upgrade ESP Rainmaker CLI.' + + +class InvalidClassInput(Exception): + """ Raised for invalid Session input """ + def __init__(self, input_arg, err_str): + self.arg = input_arg + self.err_str = err_str + + def __str__(self): + return '{} {}'.format(self.err_str, self.arg) + + +class SSLError(Exception): + """ Raised when invalid SSL certificate is passed """ + def __str__(self): + return 'Unable to verify SSL certificate.' diff --git a/cli/rmaker_lib/logger.py b/cli/rmaker_lib/logger.py new file mode 100644 index 0000000..14a5654 --- /dev/null +++ b/cli/rmaker_lib/logger.py @@ -0,0 +1,43 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 os +import logging +from logging import handlers +from datetime import datetime + +if not os.path.exists('logs'): + os.makedirs('logs') + +date_time_obj = datetime.now() +log_filename = "logs/log_" + date_time_obj.strftime("%d-%m-%Y") + ".log" + +log = logging.getLogger("CLI_LOGS") +file_formatter = logging.Formatter('%(asctime)s:[%(funcName)s]:\ + [%(levelname)s]:%(message)s') +console_formatter = logging.Formatter('[%(levelname)s]:%(message)s') +log.setLevel(logging.DEBUG) + +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.ERROR) +console_handler.setFormatter(console_formatter) + +file_handler = handlers.RotatingFileHandler(log_filename, + maxBytes=51200, + backupCount=16) +file_handler.setFormatter(file_formatter) +file_handler.setLevel(logging.DEBUG) + +log.addHandler(file_handler) +log.addHandler(console_handler) diff --git a/cli/rmaker_lib/node.py b/cli/rmaker_lib/node.py new file mode 100644 index 0000000..a3cf9f2 --- /dev/null +++ b/cli/rmaker_lib/node.py @@ -0,0 +1,336 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 requests +import json +from rmaker_lib import serverconfig, configmanager +from rmaker_lib.exceptions import NetworkError, InvalidClassInput, SSLError +from rmaker_lib.logger import log + + +class Node: + """ + Node class used to instantiate instances of node to perform various + node operations. + + :param nodeid: Node Id of node + :type nodeid: str + + :param session: :class:`rmaker_lib.session.Session` + :type session: object + """ + def __init__(self, nodeid, session): + """ + Instantiate node with nodeid and session object. + """ + log.info("Initialising node with nodeid : " + nodeid) + self.__nodeid = nodeid + self.__session = session + try: + self.__request_header = {'content-type': 'application/json', + 'Authorization': session.id_token} + except AttributeError: + raise InvalidClassInput(session, 'Invalid Session Input.\ + Expected: type .\ + Received: ') + + def get_nodeid(self): + """ + Get nodeid of device + + :return: Node Id of node on Success + :rtype: str + """ + return self.__nodeid + + def get_node_status(self): + """ + Get online/offline status of the node. + + :raises NetworkError: If there is a network connection issue while + getting node status + :raises Exception: If there is an HTTP issue while getting node status + + :return: Status of node on Success + :rtype: dict + """ + log.info("Getting online/offline status of the node : " + + self.__nodeid) + path = 'user/nodes/status' + query_parameters = 'nodeid=' + self.__nodeid + getnodestatus_url = serverconfig.HOST + path + '?' + query_parameters + try: + log.debug("Get node status request url : " + getnodestatus_url) + response = requests.get(url=getnodestatus_url, + headers=self.__request_header, + verify=configmanager.CERT_FILE) + log.debug("Get node status response : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception: + raise Exception(response.text) + log.info("Received node status successfully.") + return response.json() + + def get_node_config(self): + """ + Get node configuration. + + :raises NetworkError: If there is a network connection issue while + getting node configuration + :raises Exception: If there is an HTTP issue while getting node config + + :return: Configuration of node on Success + :rtype: dict + """ + log.info("Getting node config for node : " + self.__nodeid) + path = 'user/nodes/config' + query_parameters = 'nodeid=' + self.__nodeid + getnodeconfig_url = serverconfig.HOST + path + '?' + query_parameters + try: + log.debug("Get node config request url : " + getnodeconfig_url) + response = requests.get(url=getnodeconfig_url, + headers=self.__request_header, + verify=configmanager.CERT_FILE) + log.debug("Get node config response : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception: + raise Exception(response.text) + log.info("Received node config successfully.") + return response.json() + + def get_node_params(self): + """ + Get parameters of the node. + + :raises NetworkError: If there is a network connection issue while + getting node params + :raises Exception: If there is an HTTP issue while getting node params + or JSON format issue in HTTP response + + :return: Node Parameters on Success, None on Failure + :rtype: dict | None + + """ + log.info("Getting parameters of the node with nodeid : " + + self.__nodeid) + path = 'user/nodes/params' + query_parameters = 'nodeid=' + self.__nodeid + getparams_url = serverconfig.HOST + path + '?' + query_parameters + try: + log.debug("Get node params request url : " + getparams_url) + response = requests.get(url=getparams_url, + headers=self.__request_header, + verify=configmanager.CERT_FILE) + log.debug("Get node params response : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception: + raise Exception(response.text) + + response = json.loads(response.text) + if 'status' in response and response['status'] == 'failure': + return None + log.info("Received node parameters successfully.") + return response + + def set_node_params(self, data): + """ + Set parameters of the node. + + :param data: Parameters to be set for the node + :type data: dict + + :raises NetworkError: If there is a network connection issue while + setting node params + :raises Exception: If there is an HTTP issue while setting node params + or JSON format issue in HTTP response + + :return: True on Success + :rtype: bool + """ + log.info("Updating parameters of the node with nodeid : " + + self.__nodeid) + path = 'user/nodes/params' + query_parameters = 'nodeid=' + self.__nodeid + setparams_url = serverconfig.HOST + path + '?' + query_parameters + try: + log.debug("Set node params request url : " + setparams_url) + log.debug("Set node params request payload : " + json.dumps(data)) + response = requests.put(url=setparams_url, + data=json.dumps(data), + headers=self.__request_header, + verify=configmanager.CERT_FILE) + log.debug("Set node params response : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception: + raise Exception(response.text) + log.info("Updated node parameters successfully.") + return True + + def __user_node_mapping(self, secret_key, operation): + """ + Add or remove the user node mapping. + + :param secret_key: The randomly generated secret key that will be + used for User-Node mapping + :type secret_key: str + + :param operation: Operation to be performed, can take values + 'add' or 'remove' + :type operation: str + + :raises NetworkError: If there is a network connection issue + while adding user node mapping + :raises Exception: If there is an HTTP issue or JSON format issue + in HTTP response + + :return: Request Id if Success, None if Failure + :rtype: str | None + """ + path = 'user/nodes/mapping' + config = configmanager.Config() + userid = config.get_user_id() + request_payload = { + 'user_id': userid, + 'node_id': self.__nodeid, + 'secret_key': secret_key, + 'operation': operation + } + + request_url = serverconfig.HOST + path + try: + log.debug("User node mapping request url : " + request_url) + log.debug("User node mapping request payload : " + + str(request_payload)) + response = requests.put(url=request_url, + data=json.dumps(request_payload), + headers=self.__request_header, + verify=configmanager.CERT_FILE) + log.debug("User node mapping response : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception: + raise Exception(response.text) + + try: + response = json.loads(response.text) + except Exception as user_node_mapping_err: + raise user_node_mapping_err + + if 'request_id' in response: + return response['request_id'] + return None + + def add_user_node_mapping(self, secret_key): + """ + Add user node mapping. + + :param secret_key: The randomly generated secret key that will be + used for User-Node mapping + :type secret_key: str + + :raises NetworkError: If there is a network connection issue while + adding user node mapping + :raises Exception: If there is an HTTP issue while + adding user node mapping or + JSON format issue in HTTP response + + :return: Request Id on Success, None on Failure + :rtype: str | None + """ + log.info("Adding user node mapping request with nodeid : " + + self.__nodeid) + return self.__user_node_mapping(secret_key, 'add') + + def remove_user_node_mapping(self): + """ + Remove user node mapping request. + + :raises NetworkError: If there is a network connection issue while + removing user node mapping + :raises Exception: If there is an HTTP issue while + removing user node mapping or + JSON format issue in HTTP response + + :return: Request Id on Success, None on Failure + :rtype: str | None + """ + log.info("Removing user node mapping with nodeid : " + self.__nodeid) + secret_key = "" + return self.__user_node_mapping(secret_key, 'remove') + + def get_mapping_status(self, request_id): + """ + Check status of user node mapping request. + + :param requestId: Request Id + :type requestId: str + + :raises NetworkError: If there is a network connection issue while + getting user node mapping status + :raises Exception: If there is an HTTP issue while getting + user node mapping status or JSON format issue + in HTTP response + + :return: Request Status on Success, None on Failure + :type: str | None + """ + log.info("Checking status of user node mapping with request_id : " + + request_id) + path = 'user/nodes/mapping' + query_parameters = "&request_id=" + request_id + + request_url = serverconfig.HOST + path + '?' + query_parameters + try: + log.debug("Check user node mapping status request url : " + + request_url) + response = requests.get(url=request_url, + headers=self.__request_header, + verify=configmanager.CERT_FILE) + log.debug("Check user node mapping status response : " + + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception as mapping_status_err: + raise mapping_status_err + + try: + response = json.loads(response.text) + except Exception as mapping_status_err: + raise mapping_status_err + + if 'request_status' in response: + return response['request_status'] + return None diff --git a/cli/rmaker_lib/serverconfig.py b/cli/rmaker_lib/serverconfig.py new file mode 100644 index 0000000..d66f1c5 --- /dev/null +++ b/cli/rmaker_lib/serverconfig.py @@ -0,0 +1,34 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +VERSION = 'v1' + +HOST = 'https://api.rainmaker.espressif.com/' + VERSION + '/' + +CLIENT_ID = '60i6kac5f9rjuetqnq5mnmaqv6' + +LOGIN_URL = 'https://rainmaker-login-ui.s3.amazonaws.com/index.html?port=' + +TOKEN_URL = ('https://rainmaker-prod.auth.us-east-1.amazoncognito.com/' + 'oauth2/token') + +REDIRECT_URL = 'https://rainmaker-login-ui.s3.amazonaws.com/welcome.html' + +EXTERNAL_LOGIN_URL = ( + 'https://rainmaker-prod.auth.us-east-1.amazoncognito.com/' + 'oauth2/authorize?&redirect_uri=' + + REDIRECT_URL + '&response_type=CODE&client_id=' + + CLIENT_ID + '&scope=aws.cognito.signin.user.' + 'admin%20email%20openid%20phone%20profile&state=port:' + ) diff --git a/cli/rmaker_lib/session.py b/cli/rmaker_lib/session.py new file mode 100644 index 0000000..9487f16 --- /dev/null +++ b/cli/rmaker_lib/session.py @@ -0,0 +1,110 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 requests +import json +from rmaker_lib import serverconfig, configmanager +from rmaker_lib import node +from rmaker_lib.exceptions import NetworkError, InvalidConfigError, SSLError +from rmaker_lib.logger import log + + +class Session: + """ + Session class for logged in user. + """ + def __init__(self): + """ + Instantiate session for logged in user. + """ + config = configmanager.Config() + log.info("Initialising session for user " + + config.get_token_attribute('email')) + self.id_token = config.get_access_token() + if self.id_token is None: + raise InvalidConfigError + self.request_header = {'content-type': 'application/json', + 'Authorization': self.id_token} + + def get_nodes(self): + """ + Get list of all nodes associated with the user. + + :raises NetworkError: If there is a network connection issue + while getting nodes associated with user + :raises Exception: If there is an HTTP issue while getting nodes + + :return: Nodes associated with user on Success + :rtype: dict + """ + log.info("Getting nodes associated with the user.") + path = 'user/nodes' + getnodes_url = serverconfig.HOST + path + try: + log.debug("Get nodes request url : " + getnodes_url) + response = requests.get(url=getnodes_url, + headers=self.request_header, + verify=configmanager.CERT_FILE) + log.debug("Get nodes request response : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception: + raise Exception(response.text) + + node_map = {} + for nodeid in json.loads(response.text)['nodes']: + node_map[nodeid] = node.Node(nodeid, self) + log.info("Received nodes for user successfully.") + return node_map + + def get_mqtt_host(self): + """ + Get the MQTT Host endpoint. + + :raises NetworkError: If there is a network connection issue + while getting MQTT Host endpoint + :raises Exception: If there is an HTTP issue while getting MQTT host + or JSON format issue in HTTP response + + :return: MQTT Host on Success, None on Failure + :rtype: str | None + """ + log.info("Getting MQTT Host endpoint.") + path = 'mqtt_host' + request_url = serverconfig.HOST.split(serverconfig.VERSION)[0] + path + try: + log.debug("Get MQTT Host request url : " + request_url) + response = requests.get(url=request_url, + verify=configmanager.CERT_FILE) + log.debug("Get MQTT Host resonse : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception as mqtt_host_err: + raise mqtt_host_err + + try: + response = json.loads(response.text) + except Exception as json_decode_err: + raise json_decode_err + + if 'mqtt_host' in response: + log.info("Received MQTT Host endpoint successfully.") + return response['mqtt_host'] + return None diff --git a/cli/rmaker_lib/user.py b/cli/rmaker_lib/user.py new file mode 100644 index 0000000..4d10963 --- /dev/null +++ b/cli/rmaker_lib/user.py @@ -0,0 +1,219 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 json +import requests +import getpass +from rmaker_lib import serverconfig, configmanager, session +from rmaker_lib.exceptions import NetworkError, AuthenticationError, SSLError +from rmaker_lib.logger import log + + +class User: + """ + User class used to instantiate instances of user to perform various + user signup/login operations. + + :param username: Name of User + :type username: str + """ + def __init__(self, username): + """ + Instantiate user with username. + """ + log.info("Initialising user " + username) + self.__username = username + self.__passwd_change_token = '' + self.__request_header = {'content-type': 'application/json'} + + def signup_request(self, password): + """ + Sign up request of new User for ESP Rainmaker. + + :param password: Password to set for new user + :type password: str + + :raises NetworkError: If there is a network connection issue + during signup request + :raises Exception: If there is an HTTP issue during signup request + + :return: True on Success + :rtype: bool + """ + log.info("Creating new user with username : " + self.__username) + path = 'user' + signup_info = { + 'user_name': self.__username, + "password": password + } + signup_url = serverconfig.HOST + path + + try: + log.debug("Signup request url : " + signup_url) + response = requests.post(url=signup_url, + data=json.dumps(signup_info), + headers=self.__request_header, + verify=configmanager.CERT_FILE) + log.debug("Signup request response : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception: + raise Exception(response.text) + log.info("Signup request sent successfully.") + return True + + def signup(self, code): + """ + Sign up of new User for ESP Rainmaker. + + :param code: Verification code received in signup request for user + :type code: int + + :raises NetworkError: If there is a network connection issue + during signup + :raises Exception: If there is an HTTP issue during signup + + :return: True on Success + :rtype: bool + """ + log.info("Confirming user with username : " + self.__username) + path = 'user' + signup_info = { + 'user_name': self.__username, + "verification_code": code + } + signup_url = serverconfig.HOST + path + + try: + log.debug("Confirm user request url : " + signup_url) + response = requests.post(url=signup_url, + data=json.dumps(signup_info), + headers=self.__request_header, + verify=configmanager.CERT_FILE) + log.debug("Confirm user response : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception: + raise Exception(response.text) + log.info("Signup successful.") + return True + + def login(self, password=None): + """ + User login to the ESP Rainmaker. + + :param password: Password of user, defaults to `None` + :type password: str + + :raises NetworkError: If there is a network connection issue + during login + :raises Exception: If there is an HTTP issue during login or + JSON format issue in HTTP response + :raises AuthenticationError: If login failed with the given parameters + + :return: :class:`rmaker_lib.session.Session` on Success + :rtype: object + """ + log.info("User login with username : " + self.__username) + if password is None: + password = getpass.getpass() + path = 'login/' + login_info = { + 'user_name': self.__username, + 'password': password + } + login_url = serverconfig.HOST + path + + try: + log.debug("Login request url : " + login_url) + response = requests.post(url=login_url, + data=json.dumps(login_info), + headers=self.__request_header, + verify=configmanager.CERT_FILE) + log.debug("Login response : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception: + raise Exception(response.text) + + try: + result = json.loads(response.text) + except Exception as json_decode_err: + raise json_decode_err + + if 'status' in result and result['status'] == 'success': + log.info("Login successful.") + config_data = {} + config_data['idtoken'] = result['idtoken'] + config_data['refreshtoken'] = result['refreshtoken'] + config_data['accesstoken'] = result['accesstoken'] + configmanager.Config().set_config(config_data) + return session.Session() + raise AuthenticationError + + # User has to call forgot_password two times + # First call without arguments to request forgot password + # Second call to reset the password + # with arguments password and verification_code + def forgot_password(self, password=None, verification_code=None): + """ + Forgot password request to reset the password. + + :param password: Password of user, defaults to `None` + :type password: str + + :param verification_code: Verification code received during + forgot password request, defaults to `None` + :type verification_code: int + + :raises NetworkError: If there is a network connection issue + during password reset + :raises Exception: If there is an HTTP issue during forgot password + + :return: True on Success + :rtype: bool + """ + log.info("Forgot password request for user : " + self.__username) + path = 'forgotpassword' + forgot_password_info = { + 'user_name': self.__username, + "password": password, + "verification_code": verification_code + } + forgot_password_url = serverconfig.HOST + path + try: + log.debug("Forgot password request url : " + forgot_password_url) + response = requests.put(url=forgot_password_url, + data=json.dumps(forgot_password_info), + headers=self.__request_header, + verify=configmanager.CERT_FILE) + log.debug("Forgot password response : " + response.text) + response.raise_for_status() + except requests.exceptions.SSLError: + raise SSLError + except requests.exceptions.ConnectionError: + raise NetworkError + except Exception: + raise Exception(response.text) + log.info("Changed password successfully.") + return True diff --git a/cli/rmaker_tools/__init__.py b/cli/rmaker_tools/__init__.py new file mode 100644 index 0000000..3b0d78c --- /dev/null +++ b/cli/rmaker_tools/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. diff --git a/cli/rmaker_tools/rmaker_prov/README.md b/cli/rmaker_tools/rmaker_prov/README.md new file mode 100644 index 0000000..f8785b6 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/README.md @@ -0,0 +1,33 @@ +# Proto files copied from IDF to ESP Rainmaker + +This folder contains proto files copied from ESP-IDF for provisioning + +Files copied are: + +protocomm/ +├── proto +│   ├── constants.proto +│   ├── sec0.proto +│   ├── sec1.proto +│   └── session.proto +└── python + ├── constants_pb2.py + ├── sec0_pb2.py + ├── sec1_pb2.py + └── session_pb2.py + + +wifi_provisioning/ +├── proto +│   ├── wifi_config.proto +│   ├── wifi_constants.proto +│   └── wifi_scan.proto +└── python + ├── wifi_config_pb2.py + ├── wifi_constants_pb2.py + └── wifi_scan_pb2.py + +# Info + +IDF Branch Copied From: master +IDF Branch Latest Commit: 604360b98c14e48e73b810cb01ca294c7d275ce3 \ No newline at end of file diff --git a/cli/rmaker_tools/rmaker_prov/__init__.py b/cli/rmaker_tools/rmaker_prov/__init__.py new file mode 100644 index 0000000..3b0d78c --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. diff --git a/cli/rmaker_tools/rmaker_prov/config/custom_cloud_config.proto b/cli/rmaker_tools/rmaker_prov/config/custom_cloud_config.proto new file mode 100644 index 0000000..ac47dca --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/config/custom_cloud_config.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package cloud; + +enum CloudConfigStatus { + Success = 0; + InvalidParam = 1; + InvalidState = 2; +} + +message CmdGetSetDetails { + string UserID = 1; + string SecretKey = 2; +} +message RespGetSetDetails{ + CloudConfigStatus Status = 1; + string DeviceSecret = 2; +} + +enum CloudConfigMsgType { + TypeCmdGetSetDetails = 0; + TypeRespGetSetDetails = 1; +} + +message CloudConfigPayload { + CloudConfigMsgType msg = 1; + oneof payload { + CmdGetSetDetails cmd_get_set_details = 10; + RespGetSetDetails resp_get_set_details = 11; + } +} diff --git a/cli/rmaker_tools/rmaker_prov/config/custom_cloud_config_pb2.py b/cli/rmaker_tools/rmaker_prov/config/custom_cloud_config_pb2.py new file mode 100644 index 0000000..f57e8cd --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/config/custom_cloud_config_pb2.py @@ -0,0 +1,261 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: cloud.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='cloud.proto', + package='cloud', + syntax='proto3', + serialized_options=None, + serialized_pb=_b('\n\x0b\x63loud.proto\x12\x05\x63loud\"5\n\x10\x43mdGetSetDetails\x12\x0e\n\x06UserID\x18\x01 \x01(\t\x12\x11\n\tSecretKey\x18\x02 \x01(\t\"S\n\x11RespGetSetDetails\x12(\n\x06Status\x18\x01 \x01(\x0e\x32\x18.cloud.CloudConfigStatus\x12\x14\n\x0c\x44\x65viceSecret\x18\x02 \x01(\t\"\xb9\x01\n\x12\x43loudConfigPayload\x12&\n\x03msg\x18\x01 \x01(\x0e\x32\x19.cloud.CloudConfigMsgType\x12\x36\n\x13\x63md_get_set_details\x18\n \x01(\x0b\x32\x17.cloud.CmdGetSetDetailsH\x00\x12\x38\n\x14resp_get_set_details\x18\x0b \x01(\x0b\x32\x18.cloud.RespGetSetDetailsH\x00\x42\t\n\x07payload*D\n\x11\x43loudConfigStatus\x12\x0b\n\x07Success\x10\x00\x12\x10\n\x0cInvalidParam\x10\x01\x12\x10\n\x0cInvalidState\x10\x02*I\n\x12\x43loudConfigMsgType\x12\x18\n\x14TypeCmdGetSetDetails\x10\x00\x12\x19\n\x15TypeRespGetSetDetails\x10\x01\x62\x06proto3') +) + +_CLOUDCONFIGSTATUS = _descriptor.EnumDescriptor( + name='CloudConfigStatus', + full_name='cloud.CloudConfigStatus', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='Success', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='InvalidParam', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='InvalidState', index=2, number=2, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=350, + serialized_end=418, +) +_sym_db.RegisterEnumDescriptor(_CLOUDCONFIGSTATUS) + +CloudConfigStatus = enum_type_wrapper.EnumTypeWrapper(_CLOUDCONFIGSTATUS) +_CLOUDCONFIGMSGTYPE = _descriptor.EnumDescriptor( + name='CloudConfigMsgType', + full_name='cloud.CloudConfigMsgType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='TypeCmdGetSetDetails', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TypeRespGetSetDetails', index=1, number=1, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=420, + serialized_end=493, +) +_sym_db.RegisterEnumDescriptor(_CLOUDCONFIGMSGTYPE) + +CloudConfigMsgType = enum_type_wrapper.EnumTypeWrapper(_CLOUDCONFIGMSGTYPE) +Success = 0 +InvalidParam = 1 +InvalidState = 2 +TypeCmdGetSetDetails = 0 +TypeRespGetSetDetails = 1 + + + +_CMDGETSETDETAILS = _descriptor.Descriptor( + name='CmdGetSetDetails', + full_name='cloud.CmdGetSetDetails', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='UserID', full_name='cloud.CmdGetSetDetails.UserID', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SecretKey', full_name='cloud.CmdGetSetDetails.SecretKey', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=22, + serialized_end=75, +) + + +_RESPGETSETDETAILS = _descriptor.Descriptor( + name='RespGetSetDetails', + full_name='cloud.RespGetSetDetails', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Status', full_name='cloud.RespGetSetDetails.Status', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='DeviceSecret', full_name='cloud.RespGetSetDetails.DeviceSecret', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=77, + serialized_end=160, +) + + +_CLOUDCONFIGPAYLOAD = _descriptor.Descriptor( + name='CloudConfigPayload', + full_name='cloud.CloudConfigPayload', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='msg', full_name='cloud.CloudConfigPayload.msg', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='cmd_get_set_details', full_name='cloud.CloudConfigPayload.cmd_get_set_details', index=1, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='resp_get_set_details', full_name='cloud.CloudConfigPayload.resp_get_set_details', index=2, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='payload', full_name='cloud.CloudConfigPayload.payload', + index=0, containing_type=None, fields=[]), + ], + serialized_start=163, + serialized_end=348, +) + +_RESPGETSETDETAILS.fields_by_name['Status'].enum_type = _CLOUDCONFIGSTATUS +_CLOUDCONFIGPAYLOAD.fields_by_name['msg'].enum_type = _CLOUDCONFIGMSGTYPE +_CLOUDCONFIGPAYLOAD.fields_by_name['cmd_get_set_details'].message_type = _CMDGETSETDETAILS +_CLOUDCONFIGPAYLOAD.fields_by_name['resp_get_set_details'].message_type = _RESPGETSETDETAILS +_CLOUDCONFIGPAYLOAD.oneofs_by_name['payload'].fields.append( + _CLOUDCONFIGPAYLOAD.fields_by_name['cmd_get_set_details']) +_CLOUDCONFIGPAYLOAD.fields_by_name['cmd_get_set_details'].containing_oneof = _CLOUDCONFIGPAYLOAD.oneofs_by_name['payload'] +_CLOUDCONFIGPAYLOAD.oneofs_by_name['payload'].fields.append( + _CLOUDCONFIGPAYLOAD.fields_by_name['resp_get_set_details']) +_CLOUDCONFIGPAYLOAD.fields_by_name['resp_get_set_details'].containing_oneof = _CLOUDCONFIGPAYLOAD.oneofs_by_name['payload'] +DESCRIPTOR.message_types_by_name['CmdGetSetDetails'] = _CMDGETSETDETAILS +DESCRIPTOR.message_types_by_name['RespGetSetDetails'] = _RESPGETSETDETAILS +DESCRIPTOR.message_types_by_name['CloudConfigPayload'] = _CLOUDCONFIGPAYLOAD +DESCRIPTOR.enum_types_by_name['CloudConfigStatus'] = _CLOUDCONFIGSTATUS +DESCRIPTOR.enum_types_by_name['CloudConfigMsgType'] = _CLOUDCONFIGMSGTYPE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +CmdGetSetDetails = _reflection.GeneratedProtocolMessageType('CmdGetSetDetails', (_message.Message,), dict( + DESCRIPTOR = _CMDGETSETDETAILS, + __module__ = 'cloud_pb2' + # @@protoc_insertion_point(class_scope:cloud.CmdGetSetDetails) + )) +_sym_db.RegisterMessage(CmdGetSetDetails) + +RespGetSetDetails = _reflection.GeneratedProtocolMessageType('RespGetSetDetails', (_message.Message,), dict( + DESCRIPTOR = _RESPGETSETDETAILS, + __module__ = 'cloud_pb2' + # @@protoc_insertion_point(class_scope:cloud.RespGetSetDetails) + )) +_sym_db.RegisterMessage(RespGetSetDetails) + +CloudConfigPayload = _reflection.GeneratedProtocolMessageType('CloudConfigPayload', (_message.Message,), dict( + DESCRIPTOR = _CLOUDCONFIGPAYLOAD, + __module__ = 'cloud_pb2' + # @@protoc_insertion_point(class_scope:cloud.CloudConfigPayload) + )) +_sym_db.RegisterMessage(CloudConfigPayload) + + +# @@protoc_insertion_point(module_scope) diff --git a/cli/rmaker_tools/rmaker_prov/esp_rainmaker_prov.py b/cli/rmaker_tools/rmaker_prov/esp_rainmaker_prov.py new file mode 100644 index 0000000..3a9a465 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/esp_rainmaker_prov.py @@ -0,0 +1,311 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 __future__ import print_function +from builtins import input +import argparse +import textwrap +import time +import os +import sys +from getpass import getpass + + +try: + prov_path = os.path.dirname(__file__) + sys.path.insert(0, prov_path) + import prov.user_mapping as cloud_config_prov + import prov.prov_util as esp_prov +except ImportError as err: + raise err + +# Set this to true to allow exceptions to be thrown +config_throw_except = True + + +def on_except(err): + if config_throw_except: + raise RuntimeError(err) + else: + print(err) + + +def custom_config(tp, sec, custom_id, custom_key): + """ + Send custom config data + """ + try: + message = cloud_config_prov.custom_cloud_config_request(sec, + custom_id, + custom_key) + response = tp.send_data('cloud_user_assoc', message) + return cloud_config_prov.custom_cloud_config_response(sec, response) + except RuntimeError as e: + on_except(e) + return None + + +def desc_format(*args): + """ + Text Format to print the CLI help section for arguments + """ + desc = '' + for arg in args: + desc += textwrap.fill(replace_whitespace=False, text=arg) + "\n" + return desc + + +def get_wifi_creds_from_scanlist(transport_mode, obj_transport, + obj_security, userid, secretkey): + """ + Displays a Wi-Fi scanlist and gets Wi-Fi creds as input from user + """ + if not esp_prov.has_capability(obj_transport, 'wifi_scan'): + print("Wi-Fi Scan List is not supported by provisioning service") + print("Rerun esp_prov with SSID and Passphrase as argument") + exit(3) + + while True: + print("Scanning Wi-Fi AP's...") + access_points = esp_prov.scan_wifi_APs(transport_mode, + obj_transport, + obj_security) + len_access_points = len(access_points) + end_time = time.time() + if access_points is None: + print("Scanning Wi-Fi AP's - Failed") + exit(8) + + if len_access_points == 0: + print("No access_points found") + exit(9) + + print("Select the Wi-Fi network from the following list:") + print("{0: >4} {1: <33} {2: <12} {3: >4} {4: <4} {5: <16}".format( + "S.N.", "SSID", "BSSID", "CHN", "RSSI", "AUTH")) + for i in range(len_access_points): + print("[{0: >2}] {1: <33} {2: <12} {3: >4} {4: <4} {5: <16}" + .format(i + 1, access_points[i]["ssid"], + access_points[i]["bssid"], + access_points[i]["channel"], + access_points[i]["rssi"], + access_points[i]["auth"])) + + # Add option to join a new network which is not part of scan list + print("[{0: >2}] {1: <33}".format(len_access_points + 1, + "Join another network")) + while True: + try: + select = int(input("Select AP by number (0 to rescan) : ")) + if select < 0 or select > len_access_points + 1: + raise ValueError + break + except ValueError: + print("Invalid selection! Retry") + + if select != 0: + break + + if select == len_access_points + 1: + ssid = input("Enter ssid :") + else: + ssid = access_points[select - 1]["ssid"] + prompt_str = "Enter passphrase for {0} : ".format(ssid) + passphrase = getpass(prompt_str) + + return ssid, passphrase + + +def provision_device(transport_mode, pop, userid, secretkey, + ssid=None, passphrase=None): + """ + Wi-Fi Provision a device + + :param transport_mode: The transport mode for communicating + with the device. + Can be either ble or softap. + :type transport_mode: str + + :param pop: The Proof of Possession pin for the device. + :type pop: str + + :param userid: The User's ID that will be used for User-Node mapping. + :type userid: str + + :param secretkey: The randomly generated secret key that will be used + for User-Node mapping. + :type secretkey: str + + :param ssid: Target network SSID. Can be used if you want to + skip the Wi-Fi scan list., + defaults to 'None' i.e. uses Wi-Fi Scan list + :type ssid: str, optional + + :param passphrase: Password for the network whose SSID has been provided.\ + Required only if an SSID has been provided and the network + is not Open., + defaults to 'None' + :type passphrase: str, optional + + :return: nodeid (Node Identifier) on Success, None on Failure + :rtype: str | None + """ + security_version = 1 # this utility should run with Security1 + service_name = None # will use default (192.168.4.1:80) + + obj_transport = esp_prov.get_transport(transport_mode, service_name) + if obj_transport is None: + print("Establishing connection to node - Failed") + return None + + # First check if capabilities are supported or not + if not esp_prov.has_capability(obj_transport): + print('Security capabilities could not be determined.') + return None + + # Ensure no_pop capability is not supported for Security Version1 + if not esp_prov.has_capability(obj_transport, 'no_pop'): + if len(pop) == 0: + print("Proof of Possession argument not provided") + return None + else: + print("Invalid: no_pop capability is supported for Security Version 1") + return None + + obj_security = esp_prov.get_security(security_version, pop) + if obj_security is None: + print("Invalid Security Version") + return None + + if not esp_prov.establish_session(obj_transport, obj_security): + print("Establishing session - Failed") + print("Ensure that security scheme and\ + proof of possession are correct") + return None + print("Establishing session - Successful") + + if not (ssid and passphrase): + ssid, passphrase = get_wifi_creds_from_scanlist(transport_mode, + obj_transport, + obj_security, + userid, + secretkey) + + status, nodeid = custom_config(obj_transport, + obj_security, + userid, + secretkey) + if status != 0: + print("Sending user information to node - Failed") + return None + print("Sending user information to node - Successful") + + if not esp_prov.send_wifi_config(obj_transport, + obj_security, + ssid, + passphrase): + print("Sending Wi-Fi credentials to node - Failed") + return None + print("Sending Wi-Fi credentials to node - Successful") + + if not esp_prov.apply_wifi_config(obj_transport, obj_security): + print("Applying Wi-Fi config to node - Failed") + return None + print("Applying Wi-Fi config to node - Successful") + + while True: + time.sleep(5) + ret = esp_prov.get_wifi_config(obj_transport, obj_security) + if (ret == 1): + continue + elif (ret == 0): + print("Wi-Fi Provisioning Successful.") + return nodeid + else: + print("Wi-Fi Provisioning Failed.") + return None + break + print("Exiting Wi-Fi Provisioning.") + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=desc_format( + 'ESP RainMaker Provisioning tool\ + for configuring node ' + 'running protocomm based\ + provisioning service.'), + formatter_class=argparse. + RawTextHelpFormatter) + + parser.add_argument("--transport", required=True, dest='mode', type=str, + help=desc_format( + 'Mode of transport over which provisioning\ + is to be performed.', + 'This should be one of "softap" or "ble"')) + + parser.add_argument("--pop", required=True, + dest='pop', type=str, default='', + help=desc_format( + 'This specifies the Proof of possession (PoP)')) + + parser.add_argument("--userid", dest='userid', + required=True, type=str, default='', + help=desc_format( + 'Custom config data to be sent\ + to device: UserID')) + + parser.add_argument("--secretkey", dest='secretkey', + required=True, type=str, default='', + help=desc_format( + 'Custom config data to be sent\ + to device: SecretKey')) + + parser.add_argument("--ssid", dest='ssid', type=str, default='', + help=desc_format( + 'This configures the device to use\ + SSID of the Wi-Fi network to which ' + 'we would like it to connect to permanently,\ + once provisioning is complete. ' + 'If would prefer to use Wi-Fi scanning\ + if supported by the provisioning service,\ + this need not ' + 'be specified.')) + # Eg. --ssid "MySSID" (double quotes needed if ssid has special characters) + + parser.add_argument("--passphrase", dest='passphrase', + type=str, default='', + help=desc_format( + 'This configures the device to use Passphrase\ + for the Wi-Fi network to which ' + 'we would like it to connect to permanently,\ + once provisioning is complete. ' + 'If would prefer to use Wi-Fi scanning\ + if supported by the provisioning service,\ + this need not ' + 'be specified')) + + args = parser.parse_args() + print(args.ssid) + print(args.passphrase) + print(args.ssid and args.passphrase) + + if not (args.userid and args.secretkey): + parser.error("Error. --userid and --secretkey are required.") + + if (args.ssid or args.passphrase) and not (args.ssid and args.passphrase): + parser.error("Error. --ssid and --passphrase are required.") + + provision_device(args.mode, args.pop, args.userid, + args.secretkey, args.ssid, + args.passphrase) diff --git a/cli/rmaker_tools/rmaker_prov/proto/__init__.py b/cli/rmaker_tools/rmaker_prov/proto/__init__.py new file mode 100644 index 0000000..03d51e3 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/proto/__init__.py @@ -0,0 +1,45 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 os, sys +from rmaker_lib.logger import log + +def _load_source(name, path): + try: + from importlib.machinery import SourceFileLoader + return SourceFileLoader(name, path).load_module() + except ImportError: + # importlib.machinery doesn't exists in Python 2 so we will use imp (deprecated in Python 3) + import imp + return imp.load_source(name, path) + except Exception as load_source_err: + log.error(load_source_err) + sys.exit(1) + +protocomm_path = os.path.dirname(__file__) + "/../protocomm/" +wifi_prov_path = os.path.dirname(__file__) + "/../wifi_provisioning/" + +# protocomm component related python files generated from .proto files +constants_pb2 = _load_source("constants_pb2", protocomm_path + "python/constants_pb2.py") +sec0_pb2 = _load_source("sec0_pb2", protocomm_path + "python/sec0_pb2.py") +sec1_pb2 = _load_source("sec1_pb2", protocomm_path + "python/sec1_pb2.py") +session_pb2 = _load_source("session_pb2", protocomm_path + "python/session_pb2.py") + +# wifi_provisioning component related python files generated from .proto files +wifi_constants_pb2 = _load_source("wifi_constants_pb2", wifi_prov_path + "python/wifi_constants_pb2.py") +wifi_config_pb2 = _load_source("wifi_config_pb2", wifi_prov_path + "python/wifi_config_pb2.py") +wifi_scan_pb2 = _load_source("wifi_scan_pb2", wifi_prov_path + "python/wifi_scan_pb2.py") + +# custom_provisioning component related python files generated from .proto files +custom_cloud_config_pb2 = _load_source("custom_cloud_config_pb2", os.path.join(os.path.dirname(__file__),"../") + "config/custom_cloud_config_pb2.py") diff --git a/cli/rmaker_tools/rmaker_prov/protocomm/proto/constants.proto b/cli/rmaker_tools/rmaker_prov/protocomm/proto/constants.proto new file mode 100644 index 0000000..c80d250 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/protocomm/proto/constants.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +/* Allowed values for the status + * of a protocomm instance */ +enum Status { + Success = 0; + InvalidSecScheme = 1; + InvalidProto = 2; + TooManySessions = 3; + InvalidArgument = 4; + InternalError = 5; + CryptoError = 6; + InvalidSession = 7; +} diff --git a/cli/rmaker_tools/rmaker_prov/protocomm/proto/sec0.proto b/cli/rmaker_tools/rmaker_prov/protocomm/proto/sec0.proto new file mode 100644 index 0000000..299fce7 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/protocomm/proto/sec0.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +import "constants.proto"; + +/* Data structure of Session command/request packet */ +message S0SessionCmd { + +} + +/* Data structure of Session response packet */ +message S0SessionResp { + Status status = 1; +} + +/* A message must be of type Cmd or Resp */ +enum Sec0MsgType { + S0_Session_Command = 0; + S0_Session_Response = 1; +} + +/* Payload structure of session data */ +message Sec0Payload { + Sec0MsgType msg = 1; /*!< Type of message */ + oneof payload { + S0SessionCmd sc = 20; /*!< Payload data interpreted as Cmd */ + S0SessionResp sr = 21; /*!< Payload data interpreted as Resp */ + } +} diff --git a/cli/rmaker_tools/rmaker_prov/protocomm/proto/sec1.proto b/cli/rmaker_tools/rmaker_prov/protocomm/proto/sec1.proto new file mode 100644 index 0000000..97e05ea --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/protocomm/proto/sec1.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +import "constants.proto"; + +/* Data structure of Session command1 packet */ +message SessionCmd1 { + bytes client_verify_data = 2; +} + +/* Data structure of Session response1 packet */ +message SessionResp1 { + Status status = 1; + bytes device_verify_data = 3; +} + +/* Data structure of Session command0 packet */ +message SessionCmd0 { + bytes client_pubkey = 1; +} + +/* Data structure of Session response0 packet */ +message SessionResp0 { + Status status = 1; + bytes device_pubkey = 2; + bytes device_random = 3; +} + +/* A message must be of type Cmd0 / Cmd1 / Resp0 / Resp1 */ +enum Sec1MsgType { + Session_Command0 = 0; + Session_Response0 = 1; + Session_Command1 = 2; + Session_Response1 = 3; +} + +/* Payload structure of session data */ +message Sec1Payload { + Sec1MsgType msg = 1; /*!< Type of message */ + oneof payload { + SessionCmd0 sc0 = 20; /*!< Payload data interpreted as Cmd0 */ + SessionResp0 sr0 = 21; /*!< Payload data interpreted as Resp0 */ + SessionCmd1 sc1 = 22; /*!< Payload data interpreted as Cmd1 */ + SessionResp1 sr1 = 23; /*!< Payload data interpreted as Resp1 */ + } +} diff --git a/cli/rmaker_tools/rmaker_prov/protocomm/proto/session.proto b/cli/rmaker_tools/rmaker_prov/protocomm/proto/session.proto new file mode 100644 index 0000000..de267b1 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/protocomm/proto/session.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +import "sec0.proto"; +import "sec1.proto"; + +/* Allowed values for the type of security + * being used in a protocomm session */ +enum SecSchemeVersion { + SecScheme0 = 0; /*!< Unsecured - plaintext communication */ + SecScheme1 = 1; /*!< Security scheme 1 - Curve25519 + AES-256-CTR*/ +} + +/* Data structure exchanged when establishing + * secure session between Host and Client */ +message SessionData { + SecSchemeVersion sec_ver = 2; /*!< Type of security */ + oneof proto { + Sec0Payload sec0 = 10; /*!< Payload data in case of security 0 */ + Sec1Payload sec1 = 11; /*!< Payload data in case of security 1 */ + } +} diff --git a/cli/rmaker_tools/rmaker_prov/protocomm/python/constants_pb2.py b/cli/rmaker_tools/rmaker_prov/protocomm/python/constants_pb2.py new file mode 100644 index 0000000..ed5a47f --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/protocomm/python/constants_pb2.py @@ -0,0 +1,87 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: constants.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='constants.proto', + package='', + syntax='proto3', + serialized_pb=_b('\n\x0f\x63onstants.proto*\x9f\x01\n\x06Status\x12\x0b\n\x07Success\x10\x00\x12\x14\n\x10InvalidSecScheme\x10\x01\x12\x10\n\x0cInvalidProto\x10\x02\x12\x13\n\x0fTooManySessions\x10\x03\x12\x13\n\x0fInvalidArgument\x10\x04\x12\x11\n\rInternalError\x10\x05\x12\x0f\n\x0b\x43ryptoError\x10\x06\x12\x12\n\x0eInvalidSession\x10\x07\x62\x06proto3') +) + +_STATUS = _descriptor.EnumDescriptor( + name='Status', + full_name='Status', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='Success', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='InvalidSecScheme', index=1, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='InvalidProto', index=2, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TooManySessions', index=3, number=3, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='InvalidArgument', index=4, number=4, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='InternalError', index=5, number=5, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='CryptoError', index=6, number=6, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='InvalidSession', index=7, number=7, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=20, + serialized_end=179, +) +_sym_db.RegisterEnumDescriptor(_STATUS) + +Status = enum_type_wrapper.EnumTypeWrapper(_STATUS) +Success = 0 +InvalidSecScheme = 1 +InvalidProto = 2 +TooManySessions = 3 +InvalidArgument = 4 +InternalError = 5 +CryptoError = 6 +InvalidSession = 7 + + +DESCRIPTOR.enum_types_by_name['Status'] = _STATUS +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + + +# @@protoc_insertion_point(module_scope) diff --git a/cli/rmaker_tools/rmaker_prov/protocomm/python/sec0_pb2.py b/cli/rmaker_tools/rmaker_prov/protocomm/python/sec0_pb2.py new file mode 100644 index 0000000..123d1dc --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/protocomm/python/sec0_pb2.py @@ -0,0 +1,196 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: sec0.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import constants_pb2 as constants__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='sec0.proto', + package='', + syntax='proto3', + serialized_pb=_b('\n\nsec0.proto\x1a\x0f\x63onstants.proto\"\x0e\n\x0cS0SessionCmd\"(\n\rS0SessionResp\x12\x17\n\x06status\x18\x01 \x01(\x0e\x32\x07.Status\"n\n\x0bSec0Payload\x12\x19\n\x03msg\x18\x01 \x01(\x0e\x32\x0c.Sec0MsgType\x12\x1b\n\x02sc\x18\x14 \x01(\x0b\x32\r.S0SessionCmdH\x00\x12\x1c\n\x02sr\x18\x15 \x01(\x0b\x32\x0e.S0SessionRespH\x00\x42\t\n\x07payload*>\n\x0bSec0MsgType\x12\x16\n\x12S0_Session_Command\x10\x00\x12\x17\n\x13S0_Session_Response\x10\x01\x62\x06proto3') + , + dependencies=[constants__pb2.DESCRIPTOR,]) + +_SEC0MSGTYPE = _descriptor.EnumDescriptor( + name='Sec0MsgType', + full_name='Sec0MsgType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='S0_Session_Command', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='S0_Session_Response', index=1, number=1, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=201, + serialized_end=263, +) +_sym_db.RegisterEnumDescriptor(_SEC0MSGTYPE) + +Sec0MsgType = enum_type_wrapper.EnumTypeWrapper(_SEC0MSGTYPE) +S0_Session_Command = 0 +S0_Session_Response = 1 + + + +_S0SESSIONCMD = _descriptor.Descriptor( + name='S0SessionCmd', + full_name='S0SessionCmd', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=31, + serialized_end=45, +) + + +_S0SESSIONRESP = _descriptor.Descriptor( + name='S0SessionResp', + full_name='S0SessionResp', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='S0SessionResp.status', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=47, + serialized_end=87, +) + + +_SEC0PAYLOAD = _descriptor.Descriptor( + name='Sec0Payload', + full_name='Sec0Payload', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='msg', full_name='Sec0Payload.msg', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sc', full_name='Sec0Payload.sc', index=1, + number=20, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sr', full_name='Sec0Payload.sr', index=2, + number=21, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='payload', full_name='Sec0Payload.payload', + index=0, containing_type=None, fields=[]), + ], + serialized_start=89, + serialized_end=199, +) + +_S0SESSIONRESP.fields_by_name['status'].enum_type = constants__pb2._STATUS +_SEC0PAYLOAD.fields_by_name['msg'].enum_type = _SEC0MSGTYPE +_SEC0PAYLOAD.fields_by_name['sc'].message_type = _S0SESSIONCMD +_SEC0PAYLOAD.fields_by_name['sr'].message_type = _S0SESSIONRESP +_SEC0PAYLOAD.oneofs_by_name['payload'].fields.append( + _SEC0PAYLOAD.fields_by_name['sc']) +_SEC0PAYLOAD.fields_by_name['sc'].containing_oneof = _SEC0PAYLOAD.oneofs_by_name['payload'] +_SEC0PAYLOAD.oneofs_by_name['payload'].fields.append( + _SEC0PAYLOAD.fields_by_name['sr']) +_SEC0PAYLOAD.fields_by_name['sr'].containing_oneof = _SEC0PAYLOAD.oneofs_by_name['payload'] +DESCRIPTOR.message_types_by_name['S0SessionCmd'] = _S0SESSIONCMD +DESCRIPTOR.message_types_by_name['S0SessionResp'] = _S0SESSIONRESP +DESCRIPTOR.message_types_by_name['Sec0Payload'] = _SEC0PAYLOAD +DESCRIPTOR.enum_types_by_name['Sec0MsgType'] = _SEC0MSGTYPE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +S0SessionCmd = _reflection.GeneratedProtocolMessageType('S0SessionCmd', (_message.Message,), dict( + DESCRIPTOR = _S0SESSIONCMD, + __module__ = 'sec0_pb2' + # @@protoc_insertion_point(class_scope:S0SessionCmd) + )) +_sym_db.RegisterMessage(S0SessionCmd) + +S0SessionResp = _reflection.GeneratedProtocolMessageType('S0SessionResp', (_message.Message,), dict( + DESCRIPTOR = _S0SESSIONRESP, + __module__ = 'sec0_pb2' + # @@protoc_insertion_point(class_scope:S0SessionResp) + )) +_sym_db.RegisterMessage(S0SessionResp) + +Sec0Payload = _reflection.GeneratedProtocolMessageType('Sec0Payload', (_message.Message,), dict( + DESCRIPTOR = _SEC0PAYLOAD, + __module__ = 'sec0_pb2' + # @@protoc_insertion_point(class_scope:Sec0Payload) + )) +_sym_db.RegisterMessage(Sec0Payload) + + +# @@protoc_insertion_point(module_scope) diff --git a/cli/rmaker_tools/rmaker_prov/protocomm/python/sec1_pb2.py b/cli/rmaker_tools/rmaker_prov/protocomm/python/sec1_pb2.py new file mode 100644 index 0000000..cc305b8 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/protocomm/python/sec1_pb2.py @@ -0,0 +1,335 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: sec1.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import constants_pb2 as constants__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='sec1.proto', + package='', + syntax='proto3', + serialized_pb=_b('\n\nsec1.proto\x1a\x0f\x63onstants.proto\")\n\x0bSessionCmd1\x12\x1a\n\x12\x63lient_verify_data\x18\x02 \x01(\x0c\"C\n\x0cSessionResp1\x12\x17\n\x06status\x18\x01 \x01(\x0e\x32\x07.Status\x12\x1a\n\x12\x64\x65vice_verify_data\x18\x03 \x01(\x0c\"$\n\x0bSessionCmd0\x12\x15\n\rclient_pubkey\x18\x01 \x01(\x0c\"U\n\x0cSessionResp0\x12\x17\n\x06status\x18\x01 \x01(\x0e\x32\x07.Status\x12\x15\n\rdevice_pubkey\x18\x02 \x01(\x0c\x12\x15\n\rdevice_random\x18\x03 \x01(\x0c\"\xa9\x01\n\x0bSec1Payload\x12\x19\n\x03msg\x18\x01 \x01(\x0e\x32\x0c.Sec1MsgType\x12\x1b\n\x03sc0\x18\x14 \x01(\x0b\x32\x0c.SessionCmd0H\x00\x12\x1c\n\x03sr0\x18\x15 \x01(\x0b\x32\r.SessionResp0H\x00\x12\x1b\n\x03sc1\x18\x16 \x01(\x0b\x32\x0c.SessionCmd1H\x00\x12\x1c\n\x03sr1\x18\x17 \x01(\x0b\x32\r.SessionResp1H\x00\x42\t\n\x07payload*g\n\x0bSec1MsgType\x12\x14\n\x10Session_Command0\x10\x00\x12\x15\n\x11Session_Response0\x10\x01\x12\x14\n\x10Session_Command1\x10\x02\x12\x15\n\x11Session_Response1\x10\x03\x62\x06proto3') + , + dependencies=[constants__pb2.DESCRIPTOR,]) + +_SEC1MSGTYPE = _descriptor.EnumDescriptor( + name='Sec1MsgType', + full_name='Sec1MsgType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='Session_Command0', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='Session_Response0', index=1, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='Session_Command1', index=2, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='Session_Response1', index=3, number=3, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=440, + serialized_end=543, +) +_sym_db.RegisterEnumDescriptor(_SEC1MSGTYPE) + +Sec1MsgType = enum_type_wrapper.EnumTypeWrapper(_SEC1MSGTYPE) +Session_Command0 = 0 +Session_Response0 = 1 +Session_Command1 = 2 +Session_Response1 = 3 + + + +_SESSIONCMD1 = _descriptor.Descriptor( + name='SessionCmd1', + full_name='SessionCmd1', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='client_verify_data', full_name='SessionCmd1.client_verify_data', index=0, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=31, + serialized_end=72, +) + + +_SESSIONRESP1 = _descriptor.Descriptor( + name='SessionResp1', + full_name='SessionResp1', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='SessionResp1.status', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='device_verify_data', full_name='SessionResp1.device_verify_data', index=1, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=74, + serialized_end=141, +) + + +_SESSIONCMD0 = _descriptor.Descriptor( + name='SessionCmd0', + full_name='SessionCmd0', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='client_pubkey', full_name='SessionCmd0.client_pubkey', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=143, + serialized_end=179, +) + + +_SESSIONRESP0 = _descriptor.Descriptor( + name='SessionResp0', + full_name='SessionResp0', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='SessionResp0.status', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='device_pubkey', full_name='SessionResp0.device_pubkey', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='device_random', full_name='SessionResp0.device_random', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=181, + serialized_end=266, +) + + +_SEC1PAYLOAD = _descriptor.Descriptor( + name='Sec1Payload', + full_name='Sec1Payload', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='msg', full_name='Sec1Payload.msg', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sc0', full_name='Sec1Payload.sc0', index=1, + number=20, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sr0', full_name='Sec1Payload.sr0', index=2, + number=21, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sc1', full_name='Sec1Payload.sc1', index=3, + number=22, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sr1', full_name='Sec1Payload.sr1', index=4, + number=23, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='payload', full_name='Sec1Payload.payload', + index=0, containing_type=None, fields=[]), + ], + serialized_start=269, + serialized_end=438, +) + +_SESSIONRESP1.fields_by_name['status'].enum_type = constants__pb2._STATUS +_SESSIONRESP0.fields_by_name['status'].enum_type = constants__pb2._STATUS +_SEC1PAYLOAD.fields_by_name['msg'].enum_type = _SEC1MSGTYPE +_SEC1PAYLOAD.fields_by_name['sc0'].message_type = _SESSIONCMD0 +_SEC1PAYLOAD.fields_by_name['sr0'].message_type = _SESSIONRESP0 +_SEC1PAYLOAD.fields_by_name['sc1'].message_type = _SESSIONCMD1 +_SEC1PAYLOAD.fields_by_name['sr1'].message_type = _SESSIONRESP1 +_SEC1PAYLOAD.oneofs_by_name['payload'].fields.append( + _SEC1PAYLOAD.fields_by_name['sc0']) +_SEC1PAYLOAD.fields_by_name['sc0'].containing_oneof = _SEC1PAYLOAD.oneofs_by_name['payload'] +_SEC1PAYLOAD.oneofs_by_name['payload'].fields.append( + _SEC1PAYLOAD.fields_by_name['sr0']) +_SEC1PAYLOAD.fields_by_name['sr0'].containing_oneof = _SEC1PAYLOAD.oneofs_by_name['payload'] +_SEC1PAYLOAD.oneofs_by_name['payload'].fields.append( + _SEC1PAYLOAD.fields_by_name['sc1']) +_SEC1PAYLOAD.fields_by_name['sc1'].containing_oneof = _SEC1PAYLOAD.oneofs_by_name['payload'] +_SEC1PAYLOAD.oneofs_by_name['payload'].fields.append( + _SEC1PAYLOAD.fields_by_name['sr1']) +_SEC1PAYLOAD.fields_by_name['sr1'].containing_oneof = _SEC1PAYLOAD.oneofs_by_name['payload'] +DESCRIPTOR.message_types_by_name['SessionCmd1'] = _SESSIONCMD1 +DESCRIPTOR.message_types_by_name['SessionResp1'] = _SESSIONRESP1 +DESCRIPTOR.message_types_by_name['SessionCmd0'] = _SESSIONCMD0 +DESCRIPTOR.message_types_by_name['SessionResp0'] = _SESSIONRESP0 +DESCRIPTOR.message_types_by_name['Sec1Payload'] = _SEC1PAYLOAD +DESCRIPTOR.enum_types_by_name['Sec1MsgType'] = _SEC1MSGTYPE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +SessionCmd1 = _reflection.GeneratedProtocolMessageType('SessionCmd1', (_message.Message,), dict( + DESCRIPTOR = _SESSIONCMD1, + __module__ = 'sec1_pb2' + # @@protoc_insertion_point(class_scope:SessionCmd1) + )) +_sym_db.RegisterMessage(SessionCmd1) + +SessionResp1 = _reflection.GeneratedProtocolMessageType('SessionResp1', (_message.Message,), dict( + DESCRIPTOR = _SESSIONRESP1, + __module__ = 'sec1_pb2' + # @@protoc_insertion_point(class_scope:SessionResp1) + )) +_sym_db.RegisterMessage(SessionResp1) + +SessionCmd0 = _reflection.GeneratedProtocolMessageType('SessionCmd0', (_message.Message,), dict( + DESCRIPTOR = _SESSIONCMD0, + __module__ = 'sec1_pb2' + # @@protoc_insertion_point(class_scope:SessionCmd0) + )) +_sym_db.RegisterMessage(SessionCmd0) + +SessionResp0 = _reflection.GeneratedProtocolMessageType('SessionResp0', (_message.Message,), dict( + DESCRIPTOR = _SESSIONRESP0, + __module__ = 'sec1_pb2' + # @@protoc_insertion_point(class_scope:SessionResp0) + )) +_sym_db.RegisterMessage(SessionResp0) + +Sec1Payload = _reflection.GeneratedProtocolMessageType('Sec1Payload', (_message.Message,), dict( + DESCRIPTOR = _SEC1PAYLOAD, + __module__ = 'sec1_pb2' + # @@protoc_insertion_point(class_scope:Sec1Payload) + )) +_sym_db.RegisterMessage(Sec1Payload) + + +# @@protoc_insertion_point(module_scope) diff --git a/cli/rmaker_tools/rmaker_prov/protocomm/python/session_pb2.py b/cli/rmaker_tools/rmaker_prov/protocomm/python/session_pb2.py new file mode 100644 index 0000000..a30e794 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/protocomm/python/session_pb2.py @@ -0,0 +1,125 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: session.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import sec0_pb2 as sec0__pb2 +import sec1_pb2 as sec1__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='session.proto', + package='', + syntax='proto3', + serialized_pb=_b('\n\rsession.proto\x1a\nsec0.proto\x1a\nsec1.proto\"v\n\x0bSessionData\x12\"\n\x07sec_ver\x18\x02 \x01(\x0e\x32\x11.SecSchemeVersion\x12\x1c\n\x04sec0\x18\n \x01(\x0b\x32\x0c.Sec0PayloadH\x00\x12\x1c\n\x04sec1\x18\x0b \x01(\x0b\x32\x0c.Sec1PayloadH\x00\x42\x07\n\x05proto*2\n\x10SecSchemeVersion\x12\x0e\n\nSecScheme0\x10\x00\x12\x0e\n\nSecScheme1\x10\x01\x62\x06proto3') + , + dependencies=[sec0__pb2.DESCRIPTOR,sec1__pb2.DESCRIPTOR,]) + +_SECSCHEMEVERSION = _descriptor.EnumDescriptor( + name='SecSchemeVersion', + full_name='SecSchemeVersion', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='SecScheme0', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SecScheme1', index=1, number=1, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=161, + serialized_end=211, +) +_sym_db.RegisterEnumDescriptor(_SECSCHEMEVERSION) + +SecSchemeVersion = enum_type_wrapper.EnumTypeWrapper(_SECSCHEMEVERSION) +SecScheme0 = 0 +SecScheme1 = 1 + + + +_SESSIONDATA = _descriptor.Descriptor( + name='SessionData', + full_name='SessionData', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='sec_ver', full_name='SessionData.sec_ver', index=0, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sec0', full_name='SessionData.sec0', index=1, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sec1', full_name='SessionData.sec1', index=2, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='proto', full_name='SessionData.proto', + index=0, containing_type=None, fields=[]), + ], + serialized_start=41, + serialized_end=159, +) + +_SESSIONDATA.fields_by_name['sec_ver'].enum_type = _SECSCHEMEVERSION +_SESSIONDATA.fields_by_name['sec0'].message_type = sec0__pb2._SEC0PAYLOAD +_SESSIONDATA.fields_by_name['sec1'].message_type = sec1__pb2._SEC1PAYLOAD +_SESSIONDATA.oneofs_by_name['proto'].fields.append( + _SESSIONDATA.fields_by_name['sec0']) +_SESSIONDATA.fields_by_name['sec0'].containing_oneof = _SESSIONDATA.oneofs_by_name['proto'] +_SESSIONDATA.oneofs_by_name['proto'].fields.append( + _SESSIONDATA.fields_by_name['sec1']) +_SESSIONDATA.fields_by_name['sec1'].containing_oneof = _SESSIONDATA.oneofs_by_name['proto'] +DESCRIPTOR.message_types_by_name['SessionData'] = _SESSIONDATA +DESCRIPTOR.enum_types_by_name['SecSchemeVersion'] = _SECSCHEMEVERSION +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +SessionData = _reflection.GeneratedProtocolMessageType('SessionData', (_message.Message,), dict( + DESCRIPTOR = _SESSIONDATA, + __module__ = 'session_pb2' + # @@protoc_insertion_point(class_scope:SessionData) + )) +_sym_db.RegisterMessage(SessionData) + + +# @@protoc_insertion_point(module_scope) diff --git a/cli/rmaker_tools/rmaker_prov/prov/__init__.py b/cli/rmaker_tools/rmaker_prov/prov/__init__.py new file mode 100644 index 0000000..e95cf46 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/prov/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 .wifi_prov import * # noqa F403 +from .wifi_scan import * # noqa F403 diff --git a/cli/rmaker_tools/rmaker_prov/prov/prov_util.py b/cli/rmaker_tools/rmaker_prov/prov/prov_util.py new file mode 100644 index 0000000..ed83258 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/prov/prov_util.py @@ -0,0 +1,281 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 __future__ import print_function +from builtins import input +import argparse +import textwrap +import time +import os +import sys +import json +from getpass import getpass + +try: + import security + import transport + import prov +except ImportError as err: + raise err + +# Set this to true to allow exceptions to be thrown +config_throw_except = True + +def on_except(err): + if config_throw_except: + raise RuntimeError(err) + else: + print(err) + +def get_security(secver, pop=None, verbose=False): + """ + Get Security based on input parameters + `secver`: Security Version (Security0/Security1) + `pop`: Proof Of Possession + """ + if secver == 1: + return security.Security1(pop, verbose) + elif secver == 0: + return security.Security0(verbose) + return None + +def get_transport(sel_transport, service_name): + """ + Get object of class `Transport` based on input parameters + `sel_transport` - Transport Mode (softap/ble) + `service_name` - Service Name to connect to + """ + try: + tp = None + if (sel_transport == 'softap'): + if service_name is None: + service_name = '192.168.4.1:80' + tp = transport.Transport_HTTP(service_name) + elif (sel_transport == 'ble'): + if service_name is None: + raise RuntimeError('"--service_name" must be specified for ble transport') + # BLE client is now capable of automatically figuring out + # the primary service from the advertisement data and the + # characteristics corresponding to each endpoint. + # Below, the service_uuid field and 16bit UUIDs in the nu_lookup + # table are provided only to support devices running older firmware, + # in which case, the automated discovery will fail and the client + # will fallback to using the provided UUIDs instead + nu_lookup = {'prov-session': 'ff51', 'prov-config': 'ff52', 'proto-ver': 'ff53'} + tp = transport.Transport_BLE(devname=service_name, + service_uuid='0000ffff-0000-1000-8000-00805f9b34fb', + nu_lookup=nu_lookup) + elif (sel_transport == 'console'): + tp = transport.Transport_Console() + return tp + except RuntimeError as e: + on_except(e) + return None + +def version_match(tp, protover, verbose=False): + """ + Check version match + """ + try: + response = tp.send_data('proto-ver', protover) + + if verbose: + print("proto-ver response : ", response) + + # First assume this to be a simple version string + if response.lower() == protover.lower(): + return True + + try: + # Else interpret this as JSON structure containing + # information with versions and capabilities of both + # provisioning service and application + info = json.loads(response) + if info['prov']['ver'].lower() == protover.lower(): + return True + + except ValueError: + # If decoding as JSON fails, it means that capabilities + # are not supported + return False + + except Exception as e: + on_except(e) + return None + +def has_capability(tp, capability='none', verbose=False): + """ + Check if Transport object `tp` has capabilities + as given in input parameter `capability` + """ + # Note : default value of `capability` argument cannot be empty string + # because protocomm_httpd expects non zero content lengths + try: + response = tp.send_data('proto-ver', capability) + + if verbose: + print("proto-ver response : ", response) + + try: + # Interpret this as JSON structure containing + # information with versions and capabilities of both + # provisioning service and application + info = json.loads(response) + supported_capabilities = info['prov']['cap'] + if capability.lower() == 'none': + # No specific capability to check, but capabilities + # feature is present so return True + return True + elif capability in supported_capabilities: + return True + return False + + except ValueError: + # If decoding as JSON fails, it means that capabilities + # are not supported + return False + + except RuntimeError as e: + on_except(e) + + return False + +def get_version(tp): + """ + Get Version based on input parameters + `tp` - Object of class: Transport + """ + response = None + try: + response = tp.send_data('proto-ver', '---') + except RuntimeError as e: + on_except(e) + response = '' + return response + +def establish_session(tp, sec): + """ + Establish Provisioning Session based on input parameters + `tp` - Object of class: Transport + `sec`- Object of class: Security + """ + try: + response = None + while True: + request = sec.security_session(response) + if request is None: + break + response = tp.send_data('prov-session', request) + if (response is None): + return False + return True + except RuntimeError as e: + on_except(e) + return None + +def scan_wifi_APs(sel_transport, tp, sec): + """ + Scans Wi-Fi AP's based on input parameters + `sel_transport` - Transport Mode (softap/ble) + `tp` - Object of class: Transport + `sec`- Object of class: Security + """ + APs = [] + group_channels = 0 + readlen = 100 + if sel_transport == 'softap': + # In case of softAP we must perform the scan on individual channels, one by one, + # so that the Wi-Fi controller gets ample time to send out beacons (necessary to + # maintain connectivity with authenticated stations. As scanning one channel at a + # time will be slow, we can group more than one channels to be scanned in quick + # succession, hence speeding up the scan process. Though if too many channels are + # present in a group, the controller may again miss out on sending beacons. Hence, + # the application must should use an optimum value. The following value usually + # works out in most cases + group_channels = 5 + elif sel_transport == 'ble': + # Read at most 4 entries at a time. This is because if we are using BLE transport + # then the response packet size should not exceed the present limit of 256 bytes of + # characteristic value imposed by protocomm_ble. This limit may be removed in the + # future + readlen = 4 + try: + message = prov.scan_start_request(sec, blocking=True, group_channels=group_channels) + start_time = time.time() + response = tp.send_data('prov-scan', message) + prov.scan_start_response(sec, response) + + message = prov.scan_status_request(sec) + response = tp.send_data('prov-scan', message) + result = prov.scan_status_response(sec, response) + if result["count"] != 0: + index = 0 + remaining = result["count"] + while remaining: + count = [remaining, readlen][remaining > readlen] + message = prov.scan_result_request(sec, index, count) + response = tp.send_data('prov-scan', message) + APs += prov.scan_result_response(sec, response) + remaining -= count + index += count + + except RuntimeError as e: + on_except(e) + return None + + return APs + +def send_wifi_config(tp, sec, ssid, passphrase): + """ + Send Wi-Fi config based on input parameters + `tp` - Object of class: Transport + `sec`- Object of class: Security + `ssid` - ssid of Wi-Fi network to configure + `passphrase` - passphrase of Wi-Fi network to configure + """ + try: + message = prov.config_set_config_request(sec, ssid, passphrase) + response = tp.send_data('prov-config', message) + return (prov.config_set_config_response(sec, response) == 0) + except RuntimeError as e: + on_except(e) + return None + +def apply_wifi_config(tp, sec): + """ + Apply Wi-Fi config based on input parameters + `tp` - Object of class: Transport + `sec`- Object of class: Security + """ + try: + message = prov.config_apply_config_request(sec) + response = tp.send_data('prov-config', message) + return (prov.config_apply_config_response(sec, response) == 0) + except RuntimeError as e: + on_except(e) + return None + +def get_wifi_config(tp, sec): + """ + Get Wi-Fi config based on input parameters + `tp` - Object of class: Transport + `sec`- Object of class: Security + """ + try: + message = prov.config_get_status_request(sec) + response = tp.send_data('prov-config', message) + return prov.config_get_status_response(sec, response) + except RuntimeError as e: + on_except(e) + return None diff --git a/cli/rmaker_tools/rmaker_prov/prov/user_mapping.py b/cli/rmaker_tools/rmaker_prov/prov/user_mapping.py new file mode 100644 index 0000000..8c3bd98 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/prov/user_mapping.py @@ -0,0 +1,55 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +# APIs for interpreting and creating protobuf packets for `custom-config` protocomm endpoint + +from __future__ import print_function +from future.utils import tobytes +import sys +import os +prov_path = os.path.join(os.path.dirname(__file__),"../") +sys.path.insert(0, prov_path) + +import utils + +#import sys +#sys.path.append('../') +import proto + + +def print_verbose(security_ctx, data): + if (security_ctx.verbose): + print("++++ " + data + " ++++") + + +def custom_cloud_config_request(security_ctx, userid, secretkey): + # Form protobuf request packet from custom-config data + cmd = proto.custom_cloud_config_pb2.CloudConfigPayload() + cmd.msg = proto.custom_cloud_config_pb2.TypeCmdGetSetDetails + cmd.cmd_get_set_details.UserID = tobytes(userid) + cmd.cmd_get_set_details.SecretKey = tobytes(secretkey) + + enc_cmd = security_ctx.encrypt_data(cmd.SerializeToString()).decode('latin-1') + print_verbose(security_ctx, "Client -> Device (CustomConfig cmd) " + utils.str_to_hexstr(enc_cmd)) + return enc_cmd + +def custom_cloud_config_response(security_ctx, response_data): + # Interpret protobuf response packet + decrypt = security_ctx.decrypt_data(tobytes(response_data)) + cmd_resp = proto.custom_cloud_config_pb2.CloudConfigPayload() + cmd_resp.ParseFromString(decrypt) + print_verbose(security_ctx, "CustomConfig msg value " + str(cmd_resp.msg)) + print_verbose(security_ctx, "CustomConfig Status " + str(cmd_resp.resp_get_set_details.Status)) + print_verbose(security_ctx, "CustomConfig Device Secret " + str(cmd_resp.resp_get_set_details.DeviceSecret)) + return cmd_resp.resp_get_set_details.Status, cmd_resp.resp_get_set_details.DeviceSecret diff --git a/cli/rmaker_tools/rmaker_prov/prov/wifi_prov.py b/cli/rmaker_tools/rmaker_prov/prov/wifi_prov.py new file mode 100644 index 0000000..eea45a5 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/prov/wifi_prov.py @@ -0,0 +1,97 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +# APIs for interpreting and creating protobuf packets for Wi-Fi provisioning + +from __future__ import print_function +from future.utils import tobytes + +import utils +import proto + + +def print_verbose(security_ctx, data): + if (security_ctx.verbose): + print("++++ " + data + " ++++") + + +def config_get_status_request(security_ctx): + # Form protobuf request packet for GetStatus command + cfg1 = proto.wifi_config_pb2.WiFiConfigPayload() + cfg1.msg = proto.wifi_config_pb2.TypeCmdGetStatus + cmd_get_status = proto.wifi_config_pb2.CmdGetStatus() + cfg1.cmd_get_status.MergeFrom(cmd_get_status) + encrypted_cfg = security_ctx.encrypt_data(cfg1.SerializeToString()).decode('latin-1') + print_verbose(security_ctx, "Client -> Device (Encrypted CmdGetStatus) " + utils.str_to_hexstr(encrypted_cfg)) + return encrypted_cfg + + +def config_get_status_response(security_ctx, response_data): + # Interpret protobuf response packet from GetStatus command + decrypted_message = security_ctx.decrypt_data(tobytes(response_data)) + cmd_resp1 = proto.wifi_config_pb2.WiFiConfigPayload() + cmd_resp1.ParseFromString(decrypted_message) + print_verbose(security_ctx, "Response type " + str(cmd_resp1.msg)) + print_verbose(security_ctx, "Response status " + str(cmd_resp1.resp_get_status.status)) + if cmd_resp1.resp_get_status.sta_state == 0: + print("Checking Wi-Fi connection - Connected") + elif cmd_resp1.resp_get_status.sta_state == 1: + print("Checking Wi-Fi connection - Connecting") + elif cmd_resp1.resp_get_status.sta_state == 2: + print("Checking Wi-Fi connection - Disconnected") + elif cmd_resp1.resp_get_status.sta_state == 3: + print("Checking Wi-Fi connection - Failed") + if cmd_resp1.resp_get_status.fail_reason == 0: + print("Failure reason - Incorrect Password") + elif cmd_resp1.resp_get_status.fail_reason == 1: + print("Failure reason - Incorrect SSID") + return cmd_resp1.resp_get_status.sta_state + + +def config_set_config_request(security_ctx, ssid, passphrase): + # Form protobuf request packet for SetConfig command + cmd = proto.wifi_config_pb2.WiFiConfigPayload() + cmd.msg = proto.wifi_config_pb2.TypeCmdSetConfig + cmd.cmd_set_config.ssid = tobytes(ssid) + cmd.cmd_set_config.passphrase = tobytes(passphrase) + enc_cmd = security_ctx.encrypt_data(cmd.SerializeToString()).decode('latin-1') + print_verbose(security_ctx, "Client -> Device (SetConfig cmd) " + utils.str_to_hexstr(enc_cmd)) + return enc_cmd + + +def config_set_config_response(security_ctx, response_data): + # Interpret protobuf response packet from SetConfig command + decrypt = security_ctx.decrypt_data(tobytes(response_data)) + cmd_resp4 = proto.wifi_config_pb2.WiFiConfigPayload() + cmd_resp4.ParseFromString(decrypt) + print_verbose(security_ctx, "SetConfig status " + str(cmd_resp4.resp_set_config.status)) + return cmd_resp4.resp_set_config.status + + +def config_apply_config_request(security_ctx): + # Form protobuf request packet for ApplyConfig command + cmd = proto.wifi_config_pb2.WiFiConfigPayload() + cmd.msg = proto.wifi_config_pb2.TypeCmdApplyConfig + enc_cmd = security_ctx.encrypt_data(cmd.SerializeToString()).decode('latin-1') + print_verbose(security_ctx, "Client -> Device (ApplyConfig cmd) " + utils.str_to_hexstr(enc_cmd)) + return enc_cmd + + +def config_apply_config_response(security_ctx, response_data): + # Interpret protobuf response packet from ApplyConfig command + decrypt = security_ctx.decrypt_data(tobytes(response_data)) + cmd_resp5 = proto.wifi_config_pb2.WiFiConfigPayload() + cmd_resp5.ParseFromString(decrypt) + print_verbose(security_ctx, "ApplyConfig status " + str(cmd_resp5.resp_apply_config.status)) + return cmd_resp5.resp_apply_config.status diff --git a/cli/rmaker_tools/rmaker_prov/prov/wifi_scan.py b/cli/rmaker_tools/rmaker_prov/prov/wifi_scan.py new file mode 100644 index 0000000..472f0a7 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/prov/wifi_scan.py @@ -0,0 +1,104 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +# APIs for interpreting and creating protobuf packets for Wi-Fi Scanning + +from __future__ import print_function +from future.utils import tobytes + +import utils +import proto + + +def print_verbose(security_ctx, data): + if (security_ctx.verbose): + print("++++ " + data + " ++++") + + +def scan_start_request(security_ctx, blocking=True, passive=False, group_channels=5, period_ms=120): + # Form protobuf request packet for ScanStart command + cmd = proto.wifi_scan_pb2.WiFiScanPayload() + cmd.msg = proto.wifi_scan_pb2.TypeCmdScanStart + cmd.cmd_scan_start.blocking = blocking + cmd.cmd_scan_start.passive = passive + cmd.cmd_scan_start.group_channels = group_channels + cmd.cmd_scan_start.period_ms = period_ms + enc_cmd = security_ctx.encrypt_data(cmd.SerializeToString()).decode('latin-1') + print_verbose(security_ctx, "Client -> Device (Encrypted CmdScanStart) " + utils.str_to_hexstr(enc_cmd)) + return enc_cmd + + +def scan_start_response(security_ctx, response_data): + # Interpret protobuf response packet from ScanStart command + dec_resp = security_ctx.decrypt_data(tobytes(response_data)) + resp = proto.wifi_scan_pb2.WiFiScanPayload() + resp.ParseFromString(dec_resp) + print_verbose(security_ctx, "ScanStart status " + str(resp.status)) + if resp.status != 0: + raise RuntimeError + + +def scan_status_request(security_ctx): + # Form protobuf request packet for ScanStatus command + cmd = proto.wifi_scan_pb2.WiFiScanPayload() + cmd.msg = proto.wifi_scan_pb2.TypeCmdScanStatus + enc_cmd = security_ctx.encrypt_data(cmd.SerializeToString()).decode('latin-1') + print_verbose(security_ctx, "Client -> Device (Encrypted CmdScanStatus) " + utils.str_to_hexstr(enc_cmd)) + return enc_cmd + + +def scan_status_response(security_ctx, response_data): + # Interpret protobuf response packet from ScanStatus command + dec_resp = security_ctx.decrypt_data(tobytes(response_data)) + resp = proto.wifi_scan_pb2.WiFiScanPayload() + resp.ParseFromString(dec_resp) + print_verbose(security_ctx, "ScanStatus status " + str(resp.status)) + if resp.status != 0: + raise RuntimeError + return {"finished": resp.resp_scan_status.scan_finished, "count": resp.resp_scan_status.result_count} + + +def scan_result_request(security_ctx, index, count): + # Form protobuf request packet for ScanResult command + cmd = proto.wifi_scan_pb2.WiFiScanPayload() + cmd.msg = proto.wifi_scan_pb2.TypeCmdScanResult + cmd.cmd_scan_result.start_index = index + cmd.cmd_scan_result.count = count + enc_cmd = security_ctx.encrypt_data(cmd.SerializeToString()).decode('latin-1') + print_verbose(security_ctx, "Client -> Device (Encrypted CmdScanResult) " + utils.str_to_hexstr(enc_cmd)) + return enc_cmd + + +def scan_result_response(security_ctx, response_data): + # Interpret protobuf response packet from ScanResult command + dec_resp = security_ctx.decrypt_data(tobytes(response_data)) + resp = proto.wifi_scan_pb2.WiFiScanPayload() + resp.ParseFromString(dec_resp) + print_verbose(security_ctx, "ScanResult status " + str(resp.status)) + if resp.status != 0: + raise RuntimeError + authmode_str = ["Open", "WEP", "WPA_PSK", "WPA2_PSK", "WPA_WPA2_PSK", "WPA2_ENTERPRISE"] + results = [] + for entry in resp.resp_scan_result.entries: + results += [{"ssid": entry.ssid.decode('latin-1').rstrip('\x00'), + "bssid": utils.str_to_hexstr(entry.bssid.decode('latin-1')), + "channel": entry.channel, + "rssi": entry.rssi, + "auth": authmode_str[entry.auth]}] + print_verbose(security_ctx, "ScanResult SSID : " + str(results[-1]["ssid"])) + print_verbose(security_ctx, "ScanResult BSSID : " + str(results[-1]["bssid"])) + print_verbose(security_ctx, "ScanResult Channel : " + str(results[-1]["channel"])) + print_verbose(security_ctx, "ScanResult RSSI : " + str(results[-1]["rssi"])) + print_verbose(security_ctx, "ScanResult AUTH : " + str(results[-1]["auth"])) + return results diff --git a/cli/rmaker_tools/rmaker_prov/security/__init__.py b/cli/rmaker_tools/rmaker_prov/security/__init__.py new file mode 100644 index 0000000..1f34260 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/security/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 .security0 import * # noqa: F403, F401 +from .security1 import * # noqa: F403, F401 diff --git a/cli/rmaker_tools/rmaker_prov/security/security.py b/cli/rmaker_tools/rmaker_prov/security/security.py new file mode 100644 index 0000000..acf8a35 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/security/security.py @@ -0,0 +1,20 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +# Base class for protocomm security + + +class Security: + def __init__(self, security_session): + self.security_session = security_session diff --git a/cli/rmaker_tools/rmaker_prov/security/security0.py b/cli/rmaker_tools/rmaker_prov/security/security0.py new file mode 100644 index 0000000..c19bbfe --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/security/security0.py @@ -0,0 +1,64 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +# APIs for interpreting and creating protobuf packets for +# protocomm endpoint with security type protocomm_security0 + +from __future__ import print_function +from future.utils import tobytes + +import proto +from .security import Security + + +class Security0(Security): + def __init__(self, verbose): + # Initialize state of the security1 FSM + self.session_state = 0 + self.verbose = verbose + Security.__init__(self, self.security0_session) + + def security0_session(self, response_data): + # protocomm security0 FSM which interprets/forms + # protobuf packets according to present state of session + if (self.session_state == 0): + self.session_state = 1 + return self.setup0_request() + if (self.session_state == 1): + self.setup0_response(response_data) + return None + + def setup0_request(self): + # Form protocomm security0 request packet + setup_req = proto.session_pb2.SessionData() + setup_req.sec_ver = 0 + session_cmd = proto.sec0_pb2.S0SessionCmd() + setup_req.sec0.sc.MergeFrom(session_cmd) + return setup_req.SerializeToString().decode('latin-1') + + def setup0_response(self, response_data): + # Interpret protocomm security0 response packet + setup_resp = proto.session_pb2.SessionData() + setup_resp.ParseFromString(tobytes(response_data)) + # Check if security scheme matches + if setup_resp.sec_ver != proto.session_pb2.SecScheme0: + print("Incorrect sec scheme") + + def encrypt_data(self, data): + # Passive. No encryption when security0 used + return data + + def decrypt_data(self, data): + # Passive. No encryption when security0 used + return data diff --git a/cli/rmaker_tools/rmaker_prov/security/security1.py b/cli/rmaker_tools/rmaker_prov/security/security1.py new file mode 100644 index 0000000..d6c72bb --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/security/security1.py @@ -0,0 +1,170 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +# APIs for interpreting and creating protobuf packets for +# protocomm endpoint with security type protocomm_security1 + +from __future__ import print_function + +try: + from future.utils import tobytes + + import sys + import utils + import proto + from .security import Security + + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + + import session_pb2 +except ImportError as err: + raise err + +# Enum for state of protocomm_security1 FSM +class security_state: + REQUEST1 = 0 + RESPONSE1_REQUEST2 = 1 + RESPONSE2 = 2 + FINISHED = 3 + + +def xor(a, b): + # XOR two inputs of type `bytes` + ret = bytearray() + # Decode the input bytes to strings + a = a.decode('latin-1') + b = b.decode('latin-1') + for i in range(max(len(a), len(b))): + # Convert the characters to corresponding 8-bit ASCII codes + # then XOR them and store in bytearray + ret.append(([0, ord(a[i])][i < len(a)]) ^ ([0, ord(b[i])][i < len(b)])) + # Convert bytearray to bytes + return bytes(ret) + + +class Security1(Security): + def __init__(self, pop, verbose): + # Initialize state of the security1 FSM + self.session_state = security_state.REQUEST1 + self.pop = tobytes(pop) + self.verbose = verbose + Security.__init__(self, self.security1_session) + + def security1_session(self, response_data): + # protocomm security1 FSM which interprets/forms + # protobuf packets according to present state of session + if (self.session_state == security_state.REQUEST1): + self.session_state = security_state.RESPONSE1_REQUEST2 + return self.setup0_request() + if (self.session_state == security_state.RESPONSE1_REQUEST2): + self.session_state = security_state.RESPONSE2 + self.setup0_response(response_data) + return self.setup1_request() + if (self.session_state == security_state.RESPONSE2): + self.session_state = security_state.FINISHED + self.setup1_response(response_data) + return None + else: + print("Unexpected state") + return None + + def __generate_key(self): + # Generate private and public key pair for client + self.client_private_key = X25519PrivateKey.generate() + self.client_public_key = self.client_private_key.public_key().public_bytes() + + def _print_verbose(self, data): + if (self.verbose): + print("++++ " + data + " ++++") + + def setup0_request(self): + # Form SessionCmd0 request packet using client public key + setup_req = session_pb2.SessionData() + setup_req.sec_ver = session_pb2.SecScheme1 + self.__generate_key() + setup_req.sec1.sc0.client_pubkey = self.client_public_key + self._print_verbose("Client Public Key:\t" + utils.str_to_hexstr(self.client_public_key.decode('latin-1'))) + return setup_req.SerializeToString().decode('latin-1') + + def setup0_response(self, response_data): + # Interpret SessionResp0 response packet + setup_resp = proto.session_pb2.SessionData() + setup_resp.ParseFromString(tobytes(response_data)) + self._print_verbose("Security version:\t" + str(setup_resp.sec_ver)) + if setup_resp.sec_ver != session_pb2.SecScheme1: + print("Incorrect sec scheme") + exit(1) + self.device_public_key = setup_resp.sec1.sr0.device_pubkey + # Device random is the initialization vector + device_random = setup_resp.sec1.sr0.device_random + self._print_verbose("Device Public Key:\t" + utils.str_to_hexstr(self.device_public_key.decode('latin-1'))) + self._print_verbose("Device Random:\t" + utils.str_to_hexstr(device_random.decode('latin-1'))) + + # Calculate Curve25519 shared key using Client private key and Device public key + sharedK = self.client_private_key.exchange(X25519PublicKey.from_public_bytes(self.device_public_key)) + self._print_verbose("Shared Key:\t" + utils.str_to_hexstr(sharedK.decode('latin-1'))) + + # If PoP is provided, XOR SHA256 of PoP with the previously + # calculated Shared Key to form the actual Shared Key + if len(self.pop) > 0: + # Calculate SHA256 of PoP + h = hashes.Hash(hashes.SHA256(), backend=default_backend()) + h.update(self.pop) + digest = h.finalize() + # XOR with and update Shared Key + sharedK = xor(sharedK, digest) + self._print_verbose("New Shared Key XORed with PoP:\t" + utils.str_to_hexstr(sharedK.decode('latin-1'))) + # Initialize the encryption engine with Shared Key and initialization vector + cipher = Cipher(algorithms.AES(sharedK), modes.CTR(device_random), backend=default_backend()) + self.cipher = cipher.encryptor() + + def setup1_request(self): + # Form SessionCmd1 request packet using encrypted device public key + setup_req = proto.session_pb2.SessionData() + setup_req.sec_ver = session_pb2.SecScheme1 + setup_req.sec1.msg = proto.sec1_pb2.Session_Command1 + # Encrypt device public key and attach to the request packet + client_verify = self.cipher.update(self.device_public_key) + self._print_verbose("Client Verify:\t" + utils.str_to_hexstr(client_verify.decode('latin-1'))) + setup_req.sec1.sc1.client_verify_data = client_verify + return setup_req.SerializeToString().decode('latin-1') + + def setup1_response(self, response_data): + # Interpret SessionResp1 response packet + setup_resp = proto.session_pb2.SessionData() + setup_resp.ParseFromString(tobytes(response_data)) + # Ensure security scheme matches + if setup_resp.sec_ver == session_pb2.SecScheme1: + # Read encrypyed device verify string + device_verify = setup_resp.sec1.sr1.device_verify_data + self._print_verbose("Device verify:\t" + utils.str_to_hexstr(device_verify.decode('latin-1'))) + # Decrypt the device verify string + enc_client_pubkey = self.cipher.update(setup_resp.sec1.sr1.device_verify_data) + self._print_verbose("Enc client pubkey:\t " + utils.str_to_hexstr(enc_client_pubkey.decode('latin-1'))) + # Match decryped string with client public key + if enc_client_pubkey != self.client_public_key: + print("Mismatch in device verify") + return -2 + else: + print("Unsupported security protocol") + return -1 + + def encrypt_data(self, data): + return self.cipher.update(data) + + def decrypt_data(self, data): + return self.cipher.update(data) diff --git a/cli/rmaker_tools/rmaker_prov/transport/__init__.py b/cli/rmaker_tools/rmaker_prov/transport/__init__.py new file mode 100644 index 0000000..d54614f --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/transport/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 .transport_console import * # noqa: F403, F401 +from .transport_http import * # noqa: F403, F401 +from .transport_ble import * # noqa: F403, F401 diff --git a/cli/rmaker_tools/rmaker_prov/transport/ble_cli.py b/cli/rmaker_tools/rmaker_prov/transport/ble_cli.py new file mode 100644 index 0000000..92c080e --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/transport/ble_cli.py @@ -0,0 +1,292 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 __future__ import print_function +from builtins import input +from future.utils import iteritems + +import platform + +import utils + +fallback = True + + +# Check if platform is Linux and required packages are installed +# else fallback to console mode +if platform.system() == 'Linux': + try: + import dbus + import dbus.mainloop.glib + import time + fallback = False + except ImportError: + pass + + +# -------------------------------------------------------------------- + + +# BLE client (Linux Only) using Bluez and DBus +class BLE_Bluez_Client: + def connect(self, devname, iface, chrc_names, fallback_srv_uuid): + self.devname = devname + self.srv_uuid_fallback = fallback_srv_uuid + self.chrc_names = [name.lower() for name in chrc_names] + self.device = None + self.adapter = None + self.adapter_props = None + self.services = None + self.nu_lookup = None + self.characteristics = dict() + self.srv_uuid_adv = None + + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + bus = dbus.SystemBus() + manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager") + objects = manager.GetManagedObjects() + + for path, interfaces in iteritems(objects): + adapter = interfaces.get("org.bluez.Adapter1") + if adapter is not None: + if path.endswith(iface): + self.adapter = dbus.Interface(bus.get_object("org.bluez", path), "org.bluez.Adapter1") + self.adapter_props = dbus.Interface(bus.get_object("org.bluez", path), "org.freedesktop.DBus.Properties") + break + + if self.adapter is None: + raise RuntimeError("Bluetooth adapter not found") + + self.adapter_props.Set("org.bluez.Adapter1", "Powered", dbus.Boolean(1)) + self.adapter.StartDiscovery() + + retry = 10 + while (retry > 0): + try: + if self.device is None: + print("Connecting...") + # Wait for device to be discovered + time.sleep(5) + self._connect_() + print("Connected") + print("Getting Services...") + # Wait for services to be discovered + time.sleep(5) + self._get_services_() + return True + except Exception as e: + print(e) + retry -= 1 + print("Retries left", retry) + continue + self.adapter.StopDiscovery() + return False + + def _connect_(self): + bus = dbus.SystemBus() + manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager") + objects = manager.GetManagedObjects() + dev_path = None + for path, interfaces in iteritems(objects): + if "org.bluez.Device1" not in interfaces: + continue + if interfaces["org.bluez.Device1"].get("Name") == self.devname: + dev_path = path + break + + if dev_path is None: + raise RuntimeError("BLE device not found") + + try: + self.device = bus.get_object("org.bluez", dev_path) + try: + uuids = self.device.Get('org.bluez.Device1', 'UUIDs', + dbus_interface='org.freedesktop.DBus.Properties') + # There should be 1 service UUID in advertising data + # If bluez had cached an old version of the advertisement data + # the list of uuids may be incorrect, in which case connection + # or service discovery may fail the first time. If that happens + # the cache will be refreshed before next retry + if len(uuids) == 1: + self.srv_uuid_adv = uuids[0] + except dbus.exceptions.DBusException as e: + print(e) + + self.device.Connect(dbus_interface='org.bluez.Device1') + except Exception as e: + print(e) + self.device = None + raise RuntimeError("BLE device could not connect") + + def _get_services_(self): + bus = dbus.SystemBus() + manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager") + objects = manager.GetManagedObjects() + service_found = False + for srv_path, srv_interfaces in iteritems(objects): + if "org.bluez.GattService1" not in srv_interfaces: + continue + if not srv_path.startswith(self.device.object_path): + continue + service = bus.get_object("org.bluez", srv_path) + srv_uuid = service.Get('org.bluez.GattService1', 'UUID', + dbus_interface='org.freedesktop.DBus.Properties') + + # If service UUID doesn't match the one found in advertisement data + # then also check if it matches the fallback UUID + if srv_uuid not in [self.srv_uuid_adv, self.srv_uuid_fallback]: + continue + + nu_lookup = dict() + characteristics = dict() + for chrc_path, chrc_interfaces in iteritems(objects): + if "org.bluez.GattCharacteristic1" not in chrc_interfaces: + continue + if not chrc_path.startswith(service.object_path): + continue + chrc = bus.get_object("org.bluez", chrc_path) + uuid = chrc.Get('org.bluez.GattCharacteristic1', 'UUID', + dbus_interface='org.freedesktop.DBus.Properties') + characteristics[uuid] = chrc + for desc_path, desc_interfaces in iteritems(objects): + if "org.bluez.GattDescriptor1" not in desc_interfaces: + continue + if not desc_path.startswith(chrc.object_path): + continue + desc = bus.get_object("org.bluez", desc_path) + desc_uuid = desc.Get('org.bluez.GattDescriptor1', 'UUID', + dbus_interface='org.freedesktop.DBus.Properties') + if desc_uuid[4:8] != '2901': + continue + try: + readval = desc.ReadValue({}, dbus_interface='org.bluez.GattDescriptor1') + except dbus.exceptions.DBusException: + break + found_name = ''.join(chr(b) for b in readval).lower() + nu_lookup[found_name] = uuid + break + + match_found = True + for name in self.chrc_names: + if name not in nu_lookup: + # Endpoint name not present + match_found = False + break + + # Create lookup table only if all endpoint names found + self.nu_lookup = [None, nu_lookup][match_found] + self.characteristics = characteristics + service_found = True + + # If the service UUID matches that in the advertisement + # we can stop the search now. If it doesn't match, we + # have found the service corresponding to the fallback + # UUID, in which case don't break and keep searching + # for the advertised service + if srv_uuid == self.srv_uuid_adv: + break + + if not service_found: + self.device.Disconnect(dbus_interface='org.bluez.Device1') + if self.adapter: + self.adapter.RemoveDevice(self.device) + self.device = None + self.nu_lookup = None + self.characteristics = dict() + raise RuntimeError("Provisioning service not found") + + def get_nu_lookup(self): + return self.nu_lookup + + def has_characteristic(self, uuid): + if uuid in self.characteristics: + return True + return False + + def disconnect(self): + if self.device: + self.device.Disconnect(dbus_interface='org.bluez.Device1') + if self.adapter: + self.adapter.RemoveDevice(self.device) + self.device = None + self.nu_lookup = None + self.characteristics = dict() + if self.adapter_props: + self.adapter_props.Set("org.bluez.Adapter1", "Powered", dbus.Boolean(0)) + + def send_data(self, characteristic_uuid, data): + try: + path = self.characteristics[characteristic_uuid] + except KeyError: + raise RuntimeError("Invalid characteristic : " + characteristic_uuid) + + try: + path.WriteValue([ord(c) for c in data], {}, dbus_interface='org.bluez.GattCharacteristic1') + except dbus.exceptions.DBusException as e: + raise RuntimeError("Failed to write value to characteristic " + characteristic_uuid + ": " + str(e)) + + try: + readval = path.ReadValue({}, dbus_interface='org.bluez.GattCharacteristic1') + except dbus.exceptions.DBusException as e: + raise RuntimeError("Failed to read value from characteristic " + characteristic_uuid + ": " + str(e)) + return ''.join(chr(b) for b in readval) + + +# -------------------------------------------------------------------- + + +# Console based BLE client for Cross Platform support +class BLE_Console_Client: + def connect(self, devname, iface, chrc_names, fallback_srv_uuid): + print("BLE client is running in console mode") + print("\tThis could be due to your platform not being supported or dependencies not being met") + print("\tPlease ensure all pre-requisites are met to run the full fledged client") + print("BLECLI >> Please connect to BLE device `" + devname + "` manually using your tool of choice") + resp = input("BLECLI >> Was the device connected successfully? [y/n] ") + if resp != 'Y' and resp != 'y': + return False + print("BLECLI >> List available attributes of the connected device") + resp = input("BLECLI >> Is the service UUID '" + fallback_srv_uuid + "' listed among available attributes? [y/n] ") + if resp != 'Y' and resp != 'y': + return False + return True + + def get_nu_lookup(self): + return None + + def has_characteristic(self, uuid): + resp = input("BLECLI >> Is the characteristic UUID '" + uuid + "' listed among available attributes? [y/n] ") + if resp != 'Y' and resp != 'y': + return False + return True + + def disconnect(self): + pass + + def send_data(self, characteristic_uuid, data): + print("BLECLI >> Write following data to characteristic with UUID '" + characteristic_uuid + "' :") + print("\t>> " + utils.str_to_hexstr(data)) + print("BLECLI >> Enter data read from characteristic (in hex) :") + resp = input("\t<< ") + return utils.hexstr_to_str(resp) + + +# -------------------------------------------------------------------- + + +# Function to get client instance depending upon platform +def get_client(): + if fallback: + return BLE_Console_Client() + return BLE_Bluez_Client() diff --git a/cli/rmaker_tools/rmaker_prov/transport/transport.py b/cli/rmaker_tools/rmaker_prov/transport/transport.py new file mode 100644 index 0000000..4525eeb --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/transport/transport.py @@ -0,0 +1,28 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +# Base class for protocomm transport + +import abc + + +class Transport(): + + @abc.abstractmethod + def send_session_data(self, data): + pass + + @abc.abstractmethod + def send_config_data(self, data): + pass diff --git a/cli/rmaker_tools/rmaker_prov/transport/transport_ble.py b/cli/rmaker_tools/rmaker_prov/transport/transport_ble.py new file mode 100644 index 0000000..fa64416 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/transport/transport_ble.py @@ -0,0 +1,66 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 __future__ import print_function + +from .transport import Transport + +from . import ble_cli + + +class Transport_BLE(Transport): + def __init__(self, devname, service_uuid, nu_lookup): + # Expect service UUID like '0000ffff-0000-1000-8000-00805f9b34fb' + for name in nu_lookup.keys(): + # Calculate characteristic UUID for each endpoint + nu_lookup[name] = service_uuid[:4] + '{:02x}'.format( + int(nu_lookup[name], 16) & int(service_uuid[4:8], 16)) + service_uuid[8:] + + # Get BLE client module + self.cli = ble_cli.get_client() + + # Use client to connect to BLE device and bind to service + if not self.cli.connect(devname=devname, iface='hci0', + chrc_names=nu_lookup.keys(), + fallback_srv_uuid=service_uuid): + raise RuntimeError("Failed to initialize transport") + + # Irrespective of provided parameters, let the client + # generate a lookup table by reading advertisement data + # and characteristic user descriptors + self.name_uuid_lookup = self.cli.get_nu_lookup() + + # If that doesn't work, use the lookup table provided as parameter + if self.name_uuid_lookup is None: + self.name_uuid_lookup = nu_lookup + # Check if expected characteristics are provided by the service + for name in self.name_uuid_lookup.keys(): + if not self.cli.has_characteristic(self.name_uuid_lookup[name]): + raise RuntimeError("'" + name + "' endpoint not found") + + def __del__(self): + # Make sure device is disconnected before application gets closed + try: + self.disconnect() + except Exception: + pass + + def disconnect(self): + self.cli.disconnect() + + def send_data(self, ep_name, data): + # Write (and read) data to characteristic corresponding to the endpoint + if ep_name not in self.name_uuid_lookup.keys(): + raise RuntimeError("Invalid endpoint : " + ep_name) + return self.cli.send_data(self.name_uuid_lookup[ep_name], data) diff --git a/cli/rmaker_tools/rmaker_prov/transport/transport_console.py b/cli/rmaker_tools/rmaker_prov/transport/transport_console.py new file mode 100644 index 0000000..20220e1 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/transport/transport_console.py @@ -0,0 +1,32 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 __future__ import print_function +from builtins import input + +import utils + +from .transport import Transport + + +class Transport_Console(Transport): + + def send_data(self, path, data, session_id=0): + print("Client->Device msg :", path, session_id, utils.str_to_hexstr(data)) + try: + resp = input("Enter device->client msg : ") + except Exception as err: + print("error:", err) + return None + return utils.hexstr_to_str(resp) diff --git a/cli/rmaker_tools/rmaker_prov/transport/transport_http.py b/cli/rmaker_tools/rmaker_prov/transport/transport_http.py new file mode 100644 index 0000000..066975a --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/transport/transport_http.py @@ -0,0 +1,55 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 __future__ import print_function +from future.utils import tobytes + +import socket +import http.client +import ssl + +from .transport import Transport + + +class Transport_HTTP(Transport): + def __init__(self, hostname, certfile=None): + try: + socket.gethostbyname(hostname.split(':')[0]) + except socket.gaierror: + raise RuntimeError("Unable to resolve hostname :" + hostname) + + if certfile is None: + self.conn = http.client.HTTPConnection(hostname, timeout=30) + else: + ssl_ctx = ssl.create_default_context(cafile=certfile) + self.conn = http.client.HTTPSConnection(hostname, context=ssl_ctx, timeout=30) + try: + print("Connecting to " + hostname) + self.conn.connect() + except Exception as err: + raise RuntimeError("Connection Failure : " + str(err)) + self.headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"} + + def _send_post_request(self, path, data): + try: + self.conn.request("POST", path, tobytes(data), self.headers) + response = self.conn.getresponse() + if response.status == 200: + return response.read().decode('latin-1') + except Exception as err: + raise RuntimeError("Connection Failure : " + str(err)) + raise RuntimeError("Server responded with error code " + str(response.status)) + + def send_data(self, ep_name, data): + return self._send_post_request('/' + ep_name, data) diff --git a/cli/rmaker_tools/rmaker_prov/utils/__init__.py b/cli/rmaker_tools/rmaker_prov/utils/__init__.py new file mode 100644 index 0000000..be22f6e --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/utils/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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 .convenience import * # noqa: F403, F401 diff --git a/cli/rmaker_tools/rmaker_prov/utils/convenience.py b/cli/rmaker_tools/rmaker_prov/utils/convenience.py new file mode 100644 index 0000000..953236c --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/utils/convenience.py @@ -0,0 +1,30 @@ +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +# Convenience functions for commonly used data type conversions + + +def str_to_hexstr(string): + # Form hexstr by appending ASCII codes (in hex) corresponding to + # each character in the input string + return ''.join('{:02x}'.format(ord(c)) for c in string) + + +def hexstr_to_str(hexstr): + # Prepend 0 (if needed) to make the hexstr length an even number + if len(hexstr) % 2 == 1: + hexstr = '0' + hexstr + # Interpret consecutive pairs of hex characters as 8 bit ASCII codes + # and append characters corresponding to each code to form the string + return ''.join(chr(int(hexstr[2 * i: 2 * i + 2], 16)) for i in range(len(hexstr) // 2)) diff --git a/cli/rmaker_tools/rmaker_prov/wifi_provisioning/proto/wifi_config.proto b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/proto/wifi_config.proto new file mode 100644 index 0000000..e273dc8 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/proto/wifi_config.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +import "constants.proto"; +import "wifi_constants.proto"; + +message CmdGetStatus { + +} + +message RespGetStatus { + Status status = 1; + WifiStationState sta_state = 2; + oneof state { + WifiConnectFailedReason fail_reason = 10; + WifiConnectedState connected = 11; + } +} + +message CmdSetConfig { + bytes ssid = 1; + bytes passphrase = 2; + bytes bssid = 3; + int32 channel = 4; +} + +message RespSetConfig { + Status status = 1; +} + +message CmdApplyConfig { + +} + +message RespApplyConfig { + Status status = 1; +} + +enum WiFiConfigMsgType { + TypeCmdGetStatus = 0; + TypeRespGetStatus = 1; + TypeCmdSetConfig = 2; + TypeRespSetConfig = 3; + TypeCmdApplyConfig = 4; + TypeRespApplyConfig = 5; +} + +message WiFiConfigPayload { + WiFiConfigMsgType msg = 1; + oneof payload { + CmdGetStatus cmd_get_status = 10; + RespGetStatus resp_get_status = 11; + CmdSetConfig cmd_set_config = 12; + RespSetConfig resp_set_config = 13; + CmdApplyConfig cmd_apply_config = 14; + RespApplyConfig resp_apply_config = 15; + } +} diff --git a/cli/rmaker_tools/rmaker_prov/wifi_provisioning/proto/wifi_constants.proto b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/proto/wifi_constants.proto new file mode 100644 index 0000000..95c7612 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/proto/wifi_constants.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +enum WifiStationState { + Connected = 0; + Connecting = 1; + Disconnected = 2; + ConnectionFailed = 3; +} + +enum WifiConnectFailedReason { + AuthError = 0; + NetworkNotFound = 1; +} + +enum WifiAuthMode { + Open = 0; + WEP = 1; + WPA_PSK = 2; + WPA2_PSK = 3; + WPA_WPA2_PSK = 4; + WPA2_ENTERPRISE = 5; +} + +message WifiConnectedState { + string ip4_addr = 1; + WifiAuthMode auth_mode = 2; + bytes ssid = 3; + bytes bssid = 4; + int32 channel = 5; +} + diff --git a/cli/rmaker_tools/rmaker_prov/wifi_provisioning/proto/wifi_scan.proto b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/proto/wifi_scan.proto new file mode 100644 index 0000000..ea240f2 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/proto/wifi_scan.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; + +import "constants.proto"; +import "wifi_constants.proto"; + +message CmdScanStart { + bool blocking = 1; + bool passive = 2; + uint32 group_channels = 3; + uint32 period_ms = 4; +} + +message RespScanStart { + +} + +message CmdScanStatus { + +} + +message RespScanStatus { + bool scan_finished = 1; + uint32 result_count = 2; +} + +message CmdScanResult { + uint32 start_index = 1; + uint32 count = 2; +} + +message WiFiScanResult { + bytes ssid = 1; + uint32 channel = 2; + int32 rssi = 3; + bytes bssid = 4; + WifiAuthMode auth = 5; +} + +message RespScanResult { + repeated WiFiScanResult entries = 1; +} + +enum WiFiScanMsgType { + TypeCmdScanStart = 0; + TypeRespScanStart = 1; + TypeCmdScanStatus = 2; + TypeRespScanStatus = 3; + TypeCmdScanResult = 4; + TypeRespScanResult = 5; +} + +message WiFiScanPayload { + WiFiScanMsgType msg = 1; + Status status = 2; + oneof payload { + CmdScanStart cmd_scan_start = 10; + RespScanStart resp_scan_start = 11; + CmdScanStatus cmd_scan_status = 12; + RespScanStatus resp_scan_status = 13; + CmdScanResult cmd_scan_result = 14; + RespScanResult resp_scan_result = 15; + } +} diff --git a/cli/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_config_pb2.py b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_config_pb2.py new file mode 100644 index 0000000..0dd6d43 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_config_pb2.py @@ -0,0 +1,466 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: wifi_config.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import constants_pb2 as constants__pb2 +import wifi_constants_pb2 as wifi__constants__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='wifi_config.proto', + package='', + syntax='proto3', + serialized_pb=_b('\n\x11wifi_config.proto\x1a\x0f\x63onstants.proto\x1a\x14wifi_constants.proto\"\x0e\n\x0c\x43mdGetStatus\"\xb2\x01\n\rRespGetStatus\x12\x17\n\x06status\x18\x01 \x01(\x0e\x32\x07.Status\x12$\n\tsta_state\x18\x02 \x01(\x0e\x32\x11.WifiStationState\x12/\n\x0b\x66\x61il_reason\x18\n \x01(\x0e\x32\x18.WifiConnectFailedReasonH\x00\x12(\n\tconnected\x18\x0b \x01(\x0b\x32\x13.WifiConnectedStateH\x00\x42\x07\n\x05state\"P\n\x0c\x43mdSetConfig\x12\x0c\n\x04ssid\x18\x01 \x01(\x0c\x12\x12\n\npassphrase\x18\x02 \x01(\x0c\x12\r\n\x05\x62ssid\x18\x03 \x01(\x0c\x12\x0f\n\x07\x63hannel\x18\x04 \x01(\x05\"(\n\rRespSetConfig\x12\x17\n\x06status\x18\x01 \x01(\x0e\x32\x07.Status\"\x10\n\x0e\x43mdApplyConfig\"*\n\x0fRespApplyConfig\x12\x17\n\x06status\x18\x01 \x01(\x0e\x32\x07.Status\"\xc3\x02\n\x11WiFiConfigPayload\x12\x1f\n\x03msg\x18\x01 \x01(\x0e\x32\x12.WiFiConfigMsgType\x12\'\n\x0e\x63md_get_status\x18\n \x01(\x0b\x32\r.CmdGetStatusH\x00\x12)\n\x0fresp_get_status\x18\x0b \x01(\x0b\x32\x0e.RespGetStatusH\x00\x12\'\n\x0e\x63md_set_config\x18\x0c \x01(\x0b\x32\r.CmdSetConfigH\x00\x12)\n\x0fresp_set_config\x18\r \x01(\x0b\x32\x0e.RespSetConfigH\x00\x12+\n\x10\x63md_apply_config\x18\x0e \x01(\x0b\x32\x0f.CmdApplyConfigH\x00\x12-\n\x11resp_apply_config\x18\x0f \x01(\x0b\x32\x10.RespApplyConfigH\x00\x42\t\n\x07payload*\x9e\x01\n\x11WiFiConfigMsgType\x12\x14\n\x10TypeCmdGetStatus\x10\x00\x12\x15\n\x11TypeRespGetStatus\x10\x01\x12\x14\n\x10TypeCmdSetConfig\x10\x02\x12\x15\n\x11TypeRespSetConfig\x10\x03\x12\x16\n\x12TypeCmdApplyConfig\x10\x04\x12\x17\n\x13TypeRespApplyConfig\x10\x05\x62\x06proto3') + , + dependencies=[constants__pb2.DESCRIPTOR,wifi__constants__pb2.DESCRIPTOR,]) + +_WIFICONFIGMSGTYPE = _descriptor.EnumDescriptor( + name='WiFiConfigMsgType', + full_name='WiFiConfigMsgType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='TypeCmdGetStatus', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TypeRespGetStatus', index=1, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TypeCmdSetConfig', index=2, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TypeRespSetConfig', index=3, number=3, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TypeCmdApplyConfig', index=4, number=4, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TypeRespApplyConfig', index=5, number=5, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=770, + serialized_end=928, +) +_sym_db.RegisterEnumDescriptor(_WIFICONFIGMSGTYPE) + +WiFiConfigMsgType = enum_type_wrapper.EnumTypeWrapper(_WIFICONFIGMSGTYPE) +TypeCmdGetStatus = 0 +TypeRespGetStatus = 1 +TypeCmdSetConfig = 2 +TypeRespSetConfig = 3 +TypeCmdApplyConfig = 4 +TypeRespApplyConfig = 5 + + + +_CMDGETSTATUS = _descriptor.Descriptor( + name='CmdGetStatus', + full_name='CmdGetStatus', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=60, + serialized_end=74, +) + + +_RESPGETSTATUS = _descriptor.Descriptor( + name='RespGetStatus', + full_name='RespGetStatus', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='RespGetStatus.status', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sta_state', full_name='RespGetStatus.sta_state', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='fail_reason', full_name='RespGetStatus.fail_reason', index=2, + number=10, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='connected', full_name='RespGetStatus.connected', index=3, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='state', full_name='RespGetStatus.state', + index=0, containing_type=None, fields=[]), + ], + serialized_start=77, + serialized_end=255, +) + + +_CMDSETCONFIG = _descriptor.Descriptor( + name='CmdSetConfig', + full_name='CmdSetConfig', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='ssid', full_name='CmdSetConfig.ssid', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='passphrase', full_name='CmdSetConfig.passphrase', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='bssid', full_name='CmdSetConfig.bssid', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='channel', full_name='CmdSetConfig.channel', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=257, + serialized_end=337, +) + + +_RESPSETCONFIG = _descriptor.Descriptor( + name='RespSetConfig', + full_name='RespSetConfig', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='RespSetConfig.status', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=339, + serialized_end=379, +) + + +_CMDAPPLYCONFIG = _descriptor.Descriptor( + name='CmdApplyConfig', + full_name='CmdApplyConfig', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=381, + serialized_end=397, +) + + +_RESPAPPLYCONFIG = _descriptor.Descriptor( + name='RespApplyConfig', + full_name='RespApplyConfig', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='RespApplyConfig.status', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=399, + serialized_end=441, +) + + +_WIFICONFIGPAYLOAD = _descriptor.Descriptor( + name='WiFiConfigPayload', + full_name='WiFiConfigPayload', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='msg', full_name='WiFiConfigPayload.msg', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='cmd_get_status', full_name='WiFiConfigPayload.cmd_get_status', index=1, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='resp_get_status', full_name='WiFiConfigPayload.resp_get_status', index=2, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='cmd_set_config', full_name='WiFiConfigPayload.cmd_set_config', index=3, + number=12, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='resp_set_config', full_name='WiFiConfigPayload.resp_set_config', index=4, + number=13, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='cmd_apply_config', full_name='WiFiConfigPayload.cmd_apply_config', index=5, + number=14, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='resp_apply_config', full_name='WiFiConfigPayload.resp_apply_config', index=6, + number=15, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='payload', full_name='WiFiConfigPayload.payload', + index=0, containing_type=None, fields=[]), + ], + serialized_start=444, + serialized_end=767, +) + +_RESPGETSTATUS.fields_by_name['status'].enum_type = constants__pb2._STATUS +_RESPGETSTATUS.fields_by_name['sta_state'].enum_type = wifi__constants__pb2._WIFISTATIONSTATE +_RESPGETSTATUS.fields_by_name['fail_reason'].enum_type = wifi__constants__pb2._WIFICONNECTFAILEDREASON +_RESPGETSTATUS.fields_by_name['connected'].message_type = wifi__constants__pb2._WIFICONNECTEDSTATE +_RESPGETSTATUS.oneofs_by_name['state'].fields.append( + _RESPGETSTATUS.fields_by_name['fail_reason']) +_RESPGETSTATUS.fields_by_name['fail_reason'].containing_oneof = _RESPGETSTATUS.oneofs_by_name['state'] +_RESPGETSTATUS.oneofs_by_name['state'].fields.append( + _RESPGETSTATUS.fields_by_name['connected']) +_RESPGETSTATUS.fields_by_name['connected'].containing_oneof = _RESPGETSTATUS.oneofs_by_name['state'] +_RESPSETCONFIG.fields_by_name['status'].enum_type = constants__pb2._STATUS +_RESPAPPLYCONFIG.fields_by_name['status'].enum_type = constants__pb2._STATUS +_WIFICONFIGPAYLOAD.fields_by_name['msg'].enum_type = _WIFICONFIGMSGTYPE +_WIFICONFIGPAYLOAD.fields_by_name['cmd_get_status'].message_type = _CMDGETSTATUS +_WIFICONFIGPAYLOAD.fields_by_name['resp_get_status'].message_type = _RESPGETSTATUS +_WIFICONFIGPAYLOAD.fields_by_name['cmd_set_config'].message_type = _CMDSETCONFIG +_WIFICONFIGPAYLOAD.fields_by_name['resp_set_config'].message_type = _RESPSETCONFIG +_WIFICONFIGPAYLOAD.fields_by_name['cmd_apply_config'].message_type = _CMDAPPLYCONFIG +_WIFICONFIGPAYLOAD.fields_by_name['resp_apply_config'].message_type = _RESPAPPLYCONFIG +_WIFICONFIGPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFICONFIGPAYLOAD.fields_by_name['cmd_get_status']) +_WIFICONFIGPAYLOAD.fields_by_name['cmd_get_status'].containing_oneof = _WIFICONFIGPAYLOAD.oneofs_by_name['payload'] +_WIFICONFIGPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFICONFIGPAYLOAD.fields_by_name['resp_get_status']) +_WIFICONFIGPAYLOAD.fields_by_name['resp_get_status'].containing_oneof = _WIFICONFIGPAYLOAD.oneofs_by_name['payload'] +_WIFICONFIGPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFICONFIGPAYLOAD.fields_by_name['cmd_set_config']) +_WIFICONFIGPAYLOAD.fields_by_name['cmd_set_config'].containing_oneof = _WIFICONFIGPAYLOAD.oneofs_by_name['payload'] +_WIFICONFIGPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFICONFIGPAYLOAD.fields_by_name['resp_set_config']) +_WIFICONFIGPAYLOAD.fields_by_name['resp_set_config'].containing_oneof = _WIFICONFIGPAYLOAD.oneofs_by_name['payload'] +_WIFICONFIGPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFICONFIGPAYLOAD.fields_by_name['cmd_apply_config']) +_WIFICONFIGPAYLOAD.fields_by_name['cmd_apply_config'].containing_oneof = _WIFICONFIGPAYLOAD.oneofs_by_name['payload'] +_WIFICONFIGPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFICONFIGPAYLOAD.fields_by_name['resp_apply_config']) +_WIFICONFIGPAYLOAD.fields_by_name['resp_apply_config'].containing_oneof = _WIFICONFIGPAYLOAD.oneofs_by_name['payload'] +DESCRIPTOR.message_types_by_name['CmdGetStatus'] = _CMDGETSTATUS +DESCRIPTOR.message_types_by_name['RespGetStatus'] = _RESPGETSTATUS +DESCRIPTOR.message_types_by_name['CmdSetConfig'] = _CMDSETCONFIG +DESCRIPTOR.message_types_by_name['RespSetConfig'] = _RESPSETCONFIG +DESCRIPTOR.message_types_by_name['CmdApplyConfig'] = _CMDAPPLYCONFIG +DESCRIPTOR.message_types_by_name['RespApplyConfig'] = _RESPAPPLYCONFIG +DESCRIPTOR.message_types_by_name['WiFiConfigPayload'] = _WIFICONFIGPAYLOAD +DESCRIPTOR.enum_types_by_name['WiFiConfigMsgType'] = _WIFICONFIGMSGTYPE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +CmdGetStatus = _reflection.GeneratedProtocolMessageType('CmdGetStatus', (_message.Message,), dict( + DESCRIPTOR = _CMDGETSTATUS, + __module__ = 'wifi_config_pb2' + # @@protoc_insertion_point(class_scope:CmdGetStatus) + )) +_sym_db.RegisterMessage(CmdGetStatus) + +RespGetStatus = _reflection.GeneratedProtocolMessageType('RespGetStatus', (_message.Message,), dict( + DESCRIPTOR = _RESPGETSTATUS, + __module__ = 'wifi_config_pb2' + # @@protoc_insertion_point(class_scope:RespGetStatus) + )) +_sym_db.RegisterMessage(RespGetStatus) + +CmdSetConfig = _reflection.GeneratedProtocolMessageType('CmdSetConfig', (_message.Message,), dict( + DESCRIPTOR = _CMDSETCONFIG, + __module__ = 'wifi_config_pb2' + # @@protoc_insertion_point(class_scope:CmdSetConfig) + )) +_sym_db.RegisterMessage(CmdSetConfig) + +RespSetConfig = _reflection.GeneratedProtocolMessageType('RespSetConfig', (_message.Message,), dict( + DESCRIPTOR = _RESPSETCONFIG, + __module__ = 'wifi_config_pb2' + # @@protoc_insertion_point(class_scope:RespSetConfig) + )) +_sym_db.RegisterMessage(RespSetConfig) + +CmdApplyConfig = _reflection.GeneratedProtocolMessageType('CmdApplyConfig', (_message.Message,), dict( + DESCRIPTOR = _CMDAPPLYCONFIG, + __module__ = 'wifi_config_pb2' + # @@protoc_insertion_point(class_scope:CmdApplyConfig) + )) +_sym_db.RegisterMessage(CmdApplyConfig) + +RespApplyConfig = _reflection.GeneratedProtocolMessageType('RespApplyConfig', (_message.Message,), dict( + DESCRIPTOR = _RESPAPPLYCONFIG, + __module__ = 'wifi_config_pb2' + # @@protoc_insertion_point(class_scope:RespApplyConfig) + )) +_sym_db.RegisterMessage(RespApplyConfig) + +WiFiConfigPayload = _reflection.GeneratedProtocolMessageType('WiFiConfigPayload', (_message.Message,), dict( + DESCRIPTOR = _WIFICONFIGPAYLOAD, + __module__ = 'wifi_config_pb2' + # @@protoc_insertion_point(class_scope:WiFiConfigPayload) + )) +_sym_db.RegisterMessage(WiFiConfigPayload) + + +# @@protoc_insertion_point(module_scope) diff --git a/cli/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_constants_pb2.py b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_constants_pb2.py new file mode 100644 index 0000000..8090568 --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_constants_pb2.py @@ -0,0 +1,207 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: wifi_constants.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='wifi_constants.proto', + package='', + syntax='proto3', + serialized_pb=_b('\n\x14wifi_constants.proto\"v\n\x12WifiConnectedState\x12\x10\n\x08ip4_addr\x18\x01 \x01(\t\x12 \n\tauth_mode\x18\x02 \x01(\x0e\x32\r.WifiAuthMode\x12\x0c\n\x04ssid\x18\x03 \x01(\x0c\x12\r\n\x05\x62ssid\x18\x04 \x01(\x0c\x12\x0f\n\x07\x63hannel\x18\x05 \x01(\x05*Y\n\x10WifiStationState\x12\r\n\tConnected\x10\x00\x12\x0e\n\nConnecting\x10\x01\x12\x10\n\x0c\x44isconnected\x10\x02\x12\x14\n\x10\x43onnectionFailed\x10\x03*=\n\x17WifiConnectFailedReason\x12\r\n\tAuthError\x10\x00\x12\x13\n\x0fNetworkNotFound\x10\x01*c\n\x0cWifiAuthMode\x12\x08\n\x04Open\x10\x00\x12\x07\n\x03WEP\x10\x01\x12\x0b\n\x07WPA_PSK\x10\x02\x12\x0c\n\x08WPA2_PSK\x10\x03\x12\x10\n\x0cWPA_WPA2_PSK\x10\x04\x12\x13\n\x0fWPA2_ENTERPRISE\x10\x05\x62\x06proto3') +) + +_WIFISTATIONSTATE = _descriptor.EnumDescriptor( + name='WifiStationState', + full_name='WifiStationState', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='Connected', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='Connecting', index=1, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='Disconnected', index=2, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ConnectionFailed', index=3, number=3, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=144, + serialized_end=233, +) +_sym_db.RegisterEnumDescriptor(_WIFISTATIONSTATE) + +WifiStationState = enum_type_wrapper.EnumTypeWrapper(_WIFISTATIONSTATE) +_WIFICONNECTFAILEDREASON = _descriptor.EnumDescriptor( + name='WifiConnectFailedReason', + full_name='WifiConnectFailedReason', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='AuthError', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='NetworkNotFound', index=1, number=1, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=235, + serialized_end=296, +) +_sym_db.RegisterEnumDescriptor(_WIFICONNECTFAILEDREASON) + +WifiConnectFailedReason = enum_type_wrapper.EnumTypeWrapper(_WIFICONNECTFAILEDREASON) +_WIFIAUTHMODE = _descriptor.EnumDescriptor( + name='WifiAuthMode', + full_name='WifiAuthMode', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='Open', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='WEP', index=1, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='WPA_PSK', index=2, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='WPA2_PSK', index=3, number=3, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='WPA_WPA2_PSK', index=4, number=4, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='WPA2_ENTERPRISE', index=5, number=5, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=298, + serialized_end=397, +) +_sym_db.RegisterEnumDescriptor(_WIFIAUTHMODE) + +WifiAuthMode = enum_type_wrapper.EnumTypeWrapper(_WIFIAUTHMODE) +Connected = 0 +Connecting = 1 +Disconnected = 2 +ConnectionFailed = 3 +AuthError = 0 +NetworkNotFound = 1 +Open = 0 +WEP = 1 +WPA_PSK = 2 +WPA2_PSK = 3 +WPA_WPA2_PSK = 4 +WPA2_ENTERPRISE = 5 + + + +_WIFICONNECTEDSTATE = _descriptor.Descriptor( + name='WifiConnectedState', + full_name='WifiConnectedState', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='ip4_addr', full_name='WifiConnectedState.ip4_addr', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='auth_mode', full_name='WifiConnectedState.auth_mode', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ssid', full_name='WifiConnectedState.ssid', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='bssid', full_name='WifiConnectedState.bssid', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='channel', full_name='WifiConnectedState.channel', index=4, + number=5, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=24, + serialized_end=142, +) + +_WIFICONNECTEDSTATE.fields_by_name['auth_mode'].enum_type = _WIFIAUTHMODE +DESCRIPTOR.message_types_by_name['WifiConnectedState'] = _WIFICONNECTEDSTATE +DESCRIPTOR.enum_types_by_name['WifiStationState'] = _WIFISTATIONSTATE +DESCRIPTOR.enum_types_by_name['WifiConnectFailedReason'] = _WIFICONNECTFAILEDREASON +DESCRIPTOR.enum_types_by_name['WifiAuthMode'] = _WIFIAUTHMODE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +WifiConnectedState = _reflection.GeneratedProtocolMessageType('WifiConnectedState', (_message.Message,), dict( + DESCRIPTOR = _WIFICONNECTEDSTATE, + __module__ = 'wifi_constants_pb2' + # @@protoc_insertion_point(class_scope:WifiConnectedState) + )) +_sym_db.RegisterMessage(WifiConnectedState) + + +# @@protoc_insertion_point(module_scope) diff --git a/cli/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_scan_pb2.py b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_scan_pb2.py new file mode 100644 index 0000000..2e95d8f --- /dev/null +++ b/cli/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_scan_pb2.py @@ -0,0 +1,522 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: wifi_scan.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import constants_pb2 as constants__pb2 +import wifi_constants_pb2 as wifi__constants__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='wifi_scan.proto', + package='', + syntax='proto3', + serialized_options=None, + serialized_pb=_b('\n\x0fwifi_scan.proto\x1a\x0f\x63onstants.proto\x1a\x14wifi_constants.proto\"\\\n\x0c\x43mdScanStart\x12\x10\n\x08\x62locking\x18\x01 \x01(\x08\x12\x0f\n\x07passive\x18\x02 \x01(\x08\x12\x16\n\x0egroup_channels\x18\x03 \x01(\r\x12\x11\n\tperiod_ms\x18\x04 \x01(\r\"\x0f\n\rRespScanStart\"\x0f\n\rCmdScanStatus\"=\n\x0eRespScanStatus\x12\x15\n\rscan_finished\x18\x01 \x01(\x08\x12\x14\n\x0cresult_count\x18\x02 \x01(\r\"3\n\rCmdScanResult\x12\x13\n\x0bstart_index\x18\x01 \x01(\r\x12\r\n\x05\x63ount\x18\x02 \x01(\r\"i\n\x0eWiFiScanResult\x12\x0c\n\x04ssid\x18\x01 \x01(\x0c\x12\x0f\n\x07\x63hannel\x18\x02 \x01(\r\x12\x0c\n\x04rssi\x18\x03 \x01(\x05\x12\r\n\x05\x62ssid\x18\x04 \x01(\x0c\x12\x1b\n\x04\x61uth\x18\x05 \x01(\x0e\x32\r.WifiAuthMode\"2\n\x0eRespScanResult\x12 \n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x0f.WiFiScanResult\"\xd8\x02\n\x0fWiFiScanPayload\x12\x1d\n\x03msg\x18\x01 \x01(\x0e\x32\x10.WiFiScanMsgType\x12\x17\n\x06status\x18\x02 \x01(\x0e\x32\x07.Status\x12\'\n\x0e\x63md_scan_start\x18\n \x01(\x0b\x32\r.CmdScanStartH\x00\x12)\n\x0fresp_scan_start\x18\x0b \x01(\x0b\x32\x0e.RespScanStartH\x00\x12)\n\x0f\x63md_scan_status\x18\x0c \x01(\x0b\x32\x0e.CmdScanStatusH\x00\x12+\n\x10resp_scan_status\x18\r \x01(\x0b\x32\x0f.RespScanStatusH\x00\x12)\n\x0f\x63md_scan_result\x18\x0e \x01(\x0b\x32\x0e.CmdScanResultH\x00\x12+\n\x10resp_scan_result\x18\x0f \x01(\x0b\x32\x0f.RespScanResultH\x00\x42\t\n\x07payload*\x9c\x01\n\x0fWiFiScanMsgType\x12\x14\n\x10TypeCmdScanStart\x10\x00\x12\x15\n\x11TypeRespScanStart\x10\x01\x12\x15\n\x11TypeCmdScanStatus\x10\x02\x12\x16\n\x12TypeRespScanStatus\x10\x03\x12\x15\n\x11TypeCmdScanResult\x10\x04\x12\x16\n\x12TypeRespScanResult\x10\x05\x62\x06proto3') + , + dependencies=[constants__pb2.DESCRIPTOR,wifi__constants__pb2.DESCRIPTOR,]) + +_WIFISCANMSGTYPE = _descriptor.EnumDescriptor( + name='WiFiScanMsgType', + full_name='WiFiScanMsgType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='TypeCmdScanStart', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TypeRespScanStart', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TypeCmdScanStatus', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TypeRespScanStatus', index=3, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TypeCmdScanResult', index=4, number=4, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TypeRespScanResult', index=5, number=5, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=809, + serialized_end=965, +) +_sym_db.RegisterEnumDescriptor(_WIFISCANMSGTYPE) + +WiFiScanMsgType = enum_type_wrapper.EnumTypeWrapper(_WIFISCANMSGTYPE) +TypeCmdScanStart = 0 +TypeRespScanStart = 1 +TypeCmdScanStatus = 2 +TypeRespScanStatus = 3 +TypeCmdScanResult = 4 +TypeRespScanResult = 5 + + + +_CMDSCANSTART = _descriptor.Descriptor( + name='CmdScanStart', + full_name='CmdScanStart', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='blocking', full_name='CmdScanStart.blocking', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='passive', full_name='CmdScanStart.passive', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='group_channels', full_name='CmdScanStart.group_channels', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='period_ms', full_name='CmdScanStart.period_ms', index=3, + number=4, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=58, + serialized_end=150, +) + + +_RESPSCANSTART = _descriptor.Descriptor( + name='RespScanStart', + full_name='RespScanStart', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=152, + serialized_end=167, +) + + +_CMDSCANSTATUS = _descriptor.Descriptor( + name='CmdScanStatus', + full_name='CmdScanStatus', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=169, + serialized_end=184, +) + + +_RESPSCANSTATUS = _descriptor.Descriptor( + name='RespScanStatus', + full_name='RespScanStatus', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='scan_finished', full_name='RespScanStatus.scan_finished', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='result_count', full_name='RespScanStatus.result_count', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=186, + serialized_end=247, +) + + +_CMDSCANRESULT = _descriptor.Descriptor( + name='CmdScanResult', + full_name='CmdScanResult', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='start_index', full_name='CmdScanResult.start_index', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='count', full_name='CmdScanResult.count', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=249, + serialized_end=300, +) + + +_WIFISCANRESULT = _descriptor.Descriptor( + name='WiFiScanResult', + full_name='WiFiScanResult', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='ssid', full_name='WiFiScanResult.ssid', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='channel', full_name='WiFiScanResult.channel', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='rssi', full_name='WiFiScanResult.rssi', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='bssid', full_name='WiFiScanResult.bssid', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='auth', full_name='WiFiScanResult.auth', index=4, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=302, + serialized_end=407, +) + + +_RESPSCANRESULT = _descriptor.Descriptor( + name='RespScanResult', + full_name='RespScanResult', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='entries', full_name='RespScanResult.entries', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=409, + serialized_end=459, +) + + +_WIFISCANPAYLOAD = _descriptor.Descriptor( + name='WiFiScanPayload', + full_name='WiFiScanPayload', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='msg', full_name='WiFiScanPayload.msg', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='status', full_name='WiFiScanPayload.status', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='cmd_scan_start', full_name='WiFiScanPayload.cmd_scan_start', index=2, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='resp_scan_start', full_name='WiFiScanPayload.resp_scan_start', index=3, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='cmd_scan_status', full_name='WiFiScanPayload.cmd_scan_status', index=4, + number=12, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='resp_scan_status', full_name='WiFiScanPayload.resp_scan_status', index=5, + number=13, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='cmd_scan_result', full_name='WiFiScanPayload.cmd_scan_result', index=6, + number=14, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='resp_scan_result', full_name='WiFiScanPayload.resp_scan_result', index=7, + number=15, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='payload', full_name='WiFiScanPayload.payload', + index=0, containing_type=None, fields=[]), + ], + serialized_start=462, + serialized_end=806, +) + +_WIFISCANRESULT.fields_by_name['auth'].enum_type = wifi__constants__pb2._WIFIAUTHMODE +_RESPSCANRESULT.fields_by_name['entries'].message_type = _WIFISCANRESULT +_WIFISCANPAYLOAD.fields_by_name['msg'].enum_type = _WIFISCANMSGTYPE +_WIFISCANPAYLOAD.fields_by_name['status'].enum_type = constants__pb2._STATUS +_WIFISCANPAYLOAD.fields_by_name['cmd_scan_start'].message_type = _CMDSCANSTART +_WIFISCANPAYLOAD.fields_by_name['resp_scan_start'].message_type = _RESPSCANSTART +_WIFISCANPAYLOAD.fields_by_name['cmd_scan_status'].message_type = _CMDSCANSTATUS +_WIFISCANPAYLOAD.fields_by_name['resp_scan_status'].message_type = _RESPSCANSTATUS +_WIFISCANPAYLOAD.fields_by_name['cmd_scan_result'].message_type = _CMDSCANRESULT +_WIFISCANPAYLOAD.fields_by_name['resp_scan_result'].message_type = _RESPSCANRESULT +_WIFISCANPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFISCANPAYLOAD.fields_by_name['cmd_scan_start']) +_WIFISCANPAYLOAD.fields_by_name['cmd_scan_start'].containing_oneof = _WIFISCANPAYLOAD.oneofs_by_name['payload'] +_WIFISCANPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFISCANPAYLOAD.fields_by_name['resp_scan_start']) +_WIFISCANPAYLOAD.fields_by_name['resp_scan_start'].containing_oneof = _WIFISCANPAYLOAD.oneofs_by_name['payload'] +_WIFISCANPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFISCANPAYLOAD.fields_by_name['cmd_scan_status']) +_WIFISCANPAYLOAD.fields_by_name['cmd_scan_status'].containing_oneof = _WIFISCANPAYLOAD.oneofs_by_name['payload'] +_WIFISCANPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFISCANPAYLOAD.fields_by_name['resp_scan_status']) +_WIFISCANPAYLOAD.fields_by_name['resp_scan_status'].containing_oneof = _WIFISCANPAYLOAD.oneofs_by_name['payload'] +_WIFISCANPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFISCANPAYLOAD.fields_by_name['cmd_scan_result']) +_WIFISCANPAYLOAD.fields_by_name['cmd_scan_result'].containing_oneof = _WIFISCANPAYLOAD.oneofs_by_name['payload'] +_WIFISCANPAYLOAD.oneofs_by_name['payload'].fields.append( + _WIFISCANPAYLOAD.fields_by_name['resp_scan_result']) +_WIFISCANPAYLOAD.fields_by_name['resp_scan_result'].containing_oneof = _WIFISCANPAYLOAD.oneofs_by_name['payload'] +DESCRIPTOR.message_types_by_name['CmdScanStart'] = _CMDSCANSTART +DESCRIPTOR.message_types_by_name['RespScanStart'] = _RESPSCANSTART +DESCRIPTOR.message_types_by_name['CmdScanStatus'] = _CMDSCANSTATUS +DESCRIPTOR.message_types_by_name['RespScanStatus'] = _RESPSCANSTATUS +DESCRIPTOR.message_types_by_name['CmdScanResult'] = _CMDSCANRESULT +DESCRIPTOR.message_types_by_name['WiFiScanResult'] = _WIFISCANRESULT +DESCRIPTOR.message_types_by_name['RespScanResult'] = _RESPSCANRESULT +DESCRIPTOR.message_types_by_name['WiFiScanPayload'] = _WIFISCANPAYLOAD +DESCRIPTOR.enum_types_by_name['WiFiScanMsgType'] = _WIFISCANMSGTYPE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +CmdScanStart = _reflection.GeneratedProtocolMessageType('CmdScanStart', (_message.Message,), dict( + DESCRIPTOR = _CMDSCANSTART, + __module__ = 'wifi_scan_pb2' + # @@protoc_insertion_point(class_scope:CmdScanStart) + )) +_sym_db.RegisterMessage(CmdScanStart) + +RespScanStart = _reflection.GeneratedProtocolMessageType('RespScanStart', (_message.Message,), dict( + DESCRIPTOR = _RESPSCANSTART, + __module__ = 'wifi_scan_pb2' + # @@protoc_insertion_point(class_scope:RespScanStart) + )) +_sym_db.RegisterMessage(RespScanStart) + +CmdScanStatus = _reflection.GeneratedProtocolMessageType('CmdScanStatus', (_message.Message,), dict( + DESCRIPTOR = _CMDSCANSTATUS, + __module__ = 'wifi_scan_pb2' + # @@protoc_insertion_point(class_scope:CmdScanStatus) + )) +_sym_db.RegisterMessage(CmdScanStatus) + +RespScanStatus = _reflection.GeneratedProtocolMessageType('RespScanStatus', (_message.Message,), dict( + DESCRIPTOR = _RESPSCANSTATUS, + __module__ = 'wifi_scan_pb2' + # @@protoc_insertion_point(class_scope:RespScanStatus) + )) +_sym_db.RegisterMessage(RespScanStatus) + +CmdScanResult = _reflection.GeneratedProtocolMessageType('CmdScanResult', (_message.Message,), dict( + DESCRIPTOR = _CMDSCANRESULT, + __module__ = 'wifi_scan_pb2' + # @@protoc_insertion_point(class_scope:CmdScanResult) + )) +_sym_db.RegisterMessage(CmdScanResult) + +WiFiScanResult = _reflection.GeneratedProtocolMessageType('WiFiScanResult', (_message.Message,), dict( + DESCRIPTOR = _WIFISCANRESULT, + __module__ = 'wifi_scan_pb2' + # @@protoc_insertion_point(class_scope:WiFiScanResult) + )) +_sym_db.RegisterMessage(WiFiScanResult) + +RespScanResult = _reflection.GeneratedProtocolMessageType('RespScanResult', (_message.Message,), dict( + DESCRIPTOR = _RESPSCANRESULT, + __module__ = 'wifi_scan_pb2' + # @@protoc_insertion_point(class_scope:RespScanResult) + )) +_sym_db.RegisterMessage(RespScanResult) + +WiFiScanPayload = _reflection.GeneratedProtocolMessageType('WiFiScanPayload', (_message.Message,), dict( + DESCRIPTOR = _WIFISCANPAYLOAD, + __module__ = 'wifi_scan_pb2' + # @@protoc_insertion_point(class_scope:WiFiScanPayload) + )) +_sym_db.RegisterMessage(WiFiScanPayload) + + +# @@protoc_insertion_point(module_scope) diff --git a/cli/server_cert/server_cert.pem b/cli/server_cert/server_cert.pem new file mode 100644 index 0000000..61ae256 --- /dev/null +++ b/cli/server_cert/server_cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/components/button/CMakeLists.txt b/components/button/CMakeLists.txt new file mode 100644 index 0000000..0f94704 --- /dev/null +++ b/components/button/CMakeLists.txt @@ -0,0 +1,5 @@ +set(COMPONENT_ADD_INCLUDEDIRS button/include) +set(COMPONENT_SRCS "button/button.c" + "button/button_obj.cpp") + +register_component() diff --git a/components/button/Kconfig b/components/button/Kconfig new file mode 100644 index 0000000..8125118 --- /dev/null +++ b/components/button/Kconfig @@ -0,0 +1,6 @@ +menu "Button" + config IO_GLITCH_FILTER_TIME_MS + int "IO glitch filter timer ms (10~100)" + range 10 100 + default 50 +endmenu \ No newline at end of file diff --git a/components/button/button/button.c b/components/button/button/button.c new file mode 100644 index 0000000..7adb0da --- /dev/null +++ b/components/button/button/button.c @@ -0,0 +1,363 @@ +/* + * ESPRESSIF MIT License + * + * Copyright (c) 2018 + * + * Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case, + * it is free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished + * to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define IOT_CHECK(tag, a, ret) if(!(a)) { \ + ESP_LOGE(tag,"%s:%d (%s)", __FILE__, __LINE__, __FUNCTION__); \ + return (ret); \ + } +#define ERR_ASSERT(tag, param) IOT_CHECK(tag, (param) == ESP_OK, ESP_FAIL) +#define POINT_ASSERT(tag, param, ret) IOT_CHECK(tag, (param) != NULL, (ret)) + +typedef enum { + BUTTON_STATE_IDLE = 0, + BUTTON_STATE_PUSH, + BUTTON_STATE_PRESSED, +} button_status_t; + +typedef struct button_dev button_dev_t; +typedef struct btn_cb button_cb_t; + +struct btn_cb{ + TickType_t interval; + button_cb cb; + void* arg; + uint8_t on_press; + TimerHandle_t tmr; + button_dev_t *pbtn; + button_cb_t *next_cb; +}; + +struct button_dev{ + uint8_t io_num; + uint8_t active_level; + uint32_t serial_thres_sec; + uint8_t taskq_on; + QueueHandle_t taskq; + QueueHandle_t argq; + button_status_t state; + button_cb_t tap_short_cb; + button_cb_t tap_psh_cb; + button_cb_t tap_rls_cb; + button_cb_t press_serial_cb; + button_cb_t* cb_head; +}; + +#define BUTTON_GLITCH_FILTER_TIME_MS CONFIG_IO_GLITCH_FILTER_TIME_MS +static const char* TAG = "button"; + +static void button_press_cb(xTimerHandle tmr) +{ + button_cb_t* btn_cb = (button_cb_t*) pvTimerGetTimerID(tmr); + button_dev_t* btn = btn_cb->pbtn; + // low, then restart + if (btn->active_level == gpio_get_level(btn->io_num)) { + btn->state = BUTTON_STATE_PRESSED; + if (btn->taskq != NULL && btn->argq != NULL && btn->taskq_on && !btn_cb->on_press) { + void *tmp = btn_cb->cb; + xQueueOverwrite(btn->taskq, &tmp); + xQueueOverwrite(btn->argq, &btn_cb->arg); + } else if (btn_cb->cb) { + btn_cb->cb(btn_cb->arg); + } + } +} + +static void button_tap_psh_cb(xTimerHandle tmr) +{ + button_cb_t* btn_cb = (button_cb_t*) pvTimerGetTimerID(tmr); + button_dev_t* btn = btn_cb->pbtn; + xTimerStop(btn->tap_rls_cb.tmr, portMAX_DELAY); + int lv = gpio_get_level(btn->io_num); + + if (btn->active_level == lv) { + // True implies key is pressed + btn->state = BUTTON_STATE_PUSH; + if (btn->press_serial_cb.tmr) { + xTimerChangePeriod(btn->press_serial_cb.tmr, btn->serial_thres_sec*1000 / portTICK_PERIOD_MS, portMAX_DELAY); + xTimerReset(btn->press_serial_cb.tmr, portMAX_DELAY); + } + if (btn->tap_psh_cb.cb) { + btn->tap_psh_cb.cb(btn->tap_psh_cb.arg); + } + } else { + // 50ms, check if this is a real key up + if (btn->tap_rls_cb.tmr) { + xTimerStop(btn->tap_rls_cb.tmr, portMAX_DELAY); + xTimerReset(btn->tap_rls_cb.tmr, portMAX_DELAY); + } + } +} + +static void button_tap_rls_cb(xTimerHandle tmr) +{ + button_cb_t* btn_cb = (button_cb_t*) pvTimerGetTimerID(tmr); + button_dev_t* btn = btn_cb->pbtn; + xTimerStop(btn->tap_rls_cb.tmr, portMAX_DELAY); + if (btn->active_level == gpio_get_level(btn->io_num)) { + + } else { + // high, then key is up + button_cb_t *pcb = btn->cb_head; + while (pcb != NULL) { + if (pcb->tmr != NULL) { + xTimerStop(pcb->tmr, portMAX_DELAY); + } + pcb = pcb->next_cb; + } + if (btn->taskq != NULL && btn->argq != NULL && btn->taskq_on && uxQueueMessagesWaiting(btn->taskq) != 0 && btn->state != BUTTON_STATE_IDLE) { + void (*task)(void*); + void *arg; + xQueueReceive(btn->taskq, &task, 0); + xQueueReceive(btn->argq, &arg, 0); + task(arg); + } + if (btn->press_serial_cb.tmr && btn->press_serial_cb.tmr != NULL) { + xTimerStop(btn->press_serial_cb.tmr, portMAX_DELAY); + } + if (btn->tap_short_cb.cb && btn->state == BUTTON_STATE_PUSH) { + btn->tap_short_cb.cb(btn->tap_short_cb.arg); + } + if(btn->tap_rls_cb.cb && btn->state != BUTTON_STATE_IDLE) { + btn->tap_rls_cb.cb(btn->tap_rls_cb.arg); + } + btn->state = BUTTON_STATE_IDLE; + } +} + +static void button_press_serial_cb(xTimerHandle tmr) +{ + button_dev_t* btn = (button_dev_t*) pvTimerGetTimerID(tmr); + if (btn->press_serial_cb.cb) { + btn->press_serial_cb.cb(btn->press_serial_cb.arg); + } + xTimerChangePeriod(btn->press_serial_cb.tmr, btn->press_serial_cb.interval, portMAX_DELAY); + xTimerReset(btn->press_serial_cb.tmr, portMAX_DELAY); +} + +static void button_gpio_isr_handler(void* arg) +{ + button_dev_t* btn = (button_dev_t*) arg; + portBASE_TYPE HPTaskAwoken = pdFALSE; + int level = gpio_get_level(btn->io_num); + if (level == btn->active_level) { + if (btn->tap_psh_cb.tmr) { + xTimerStopFromISR(btn->tap_psh_cb.tmr, &HPTaskAwoken); + xTimerResetFromISR(btn->tap_psh_cb.tmr, &HPTaskAwoken); + } + + button_cb_t *pcb = btn->cb_head; + while (pcb != NULL) { + if (pcb->tmr != NULL) { + xTimerStopFromISR(pcb->tmr, &HPTaskAwoken); + xTimerResetFromISR(pcb->tmr, &HPTaskAwoken); + } + pcb = pcb->next_cb; + } + } else { + // 50ms, check if this is a real key up + if (btn->tap_rls_cb.tmr) { + xTimerStopFromISR(btn->tap_rls_cb.tmr, &HPTaskAwoken); + xTimerResetFromISR(btn->tap_rls_cb.tmr, &HPTaskAwoken); + } + } + if(HPTaskAwoken == pdTRUE) { + portYIELD_FROM_ISR(); + } +} + +static void button_free_tmr(xTimerHandle* tmr) +{ + if (tmr && *tmr) { + xTimerStop(*tmr, portMAX_DELAY); + xTimerDelete(*tmr, portMAX_DELAY); + *tmr = NULL; + } +} + +esp_err_t iot_button_delete(button_handle_t btn_handle) +{ + POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG); + button_dev_t* btn = (button_dev_t*) btn_handle; + gpio_set_intr_type(btn->io_num, GPIO_INTR_DISABLE); + gpio_isr_handler_remove(btn->io_num); + + button_free_tmr(&btn->tap_rls_cb.tmr); + button_free_tmr(&btn->tap_psh_cb.tmr); + button_free_tmr(&btn->tap_short_cb.tmr); + button_free_tmr(&btn->press_serial_cb.tmr); + + button_cb_t *pcb = btn->cb_head; + while (pcb != NULL) { + button_cb_t *cb_next = pcb->next_cb; + button_free_tmr(&pcb->tmr); + free(pcb); + pcb = cb_next; + } + free(btn); + return ESP_OK; +} + +button_handle_t iot_button_create(gpio_num_t gpio_num, button_active_t active_level) +{ + IOT_CHECK(TAG, gpio_num < GPIO_NUM_MAX, NULL); + button_dev_t* btn = (button_dev_t*) calloc(1, sizeof(button_dev_t)); + POINT_ASSERT(TAG, btn, NULL); + btn->active_level = active_level; + btn->io_num = gpio_num; + btn->state = BUTTON_STATE_IDLE; + btn->taskq_on = 0; + btn->taskq = xQueueCreate(1, sizeof(void*)); + btn->argq = xQueueCreate(1, sizeof(void *)); + btn->tap_rls_cb.arg = NULL; + btn->tap_rls_cb.cb = NULL; + btn->tap_rls_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS; + btn->tap_rls_cb.pbtn = btn; + btn->tap_rls_cb.tmr = xTimerCreate("btn_rls_tmr", btn->tap_rls_cb.interval, pdFALSE, + &btn->tap_rls_cb, button_tap_rls_cb); + btn->tap_psh_cb.arg = NULL; + btn->tap_psh_cb.cb = NULL; + btn->tap_psh_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS; + btn->tap_psh_cb.pbtn = btn; + btn->tap_psh_cb.tmr = xTimerCreate("btn_psh_tmr", btn->tap_psh_cb.interval, pdFALSE, + &btn->tap_psh_cb, button_tap_psh_cb); + gpio_install_isr_service(0); + gpio_config_t gpio_conf; + gpio_conf.intr_type = GPIO_INTR_ANYEDGE; + gpio_conf.mode = GPIO_MODE_INPUT; + gpio_conf.pin_bit_mask = (1 << gpio_num); + gpio_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + gpio_conf.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&gpio_conf); + gpio_isr_handler_add(gpio_num, button_gpio_isr_handler, btn); + return (button_handle_t) btn; +} + +esp_err_t iot_button_rm_cb(button_handle_t btn_handle, button_cb_type_t type) +{ + button_dev_t* btn = (button_dev_t*) btn_handle; + button_cb_t* btn_cb = NULL; + if (type == BUTTON_CB_PUSH) { + btn_cb = &btn->tap_psh_cb; + } else if (type == BUTTON_CB_RELEASE) { + btn_cb = &btn->tap_rls_cb; + } else if (type == BUTTON_CB_TAP) { + btn_cb = &btn->tap_short_cb; + } else if (type == BUTTON_CB_SERIAL) { + btn_cb = &btn->press_serial_cb; + } + btn_cb->cb = NULL; + btn_cb->arg = NULL; + btn_cb->pbtn = btn; + button_free_tmr(&btn_cb->tmr); + return ESP_OK; +} + +esp_err_t iot_button_set_serial_cb(button_handle_t btn_handle, uint32_t start_after_sec, TickType_t interval_tick, button_cb cb, void* arg) +{ + button_dev_t* btn = (button_dev_t*) btn_handle; + btn->serial_thres_sec = start_after_sec; + if (btn->press_serial_cb.tmr == NULL) { + btn->press_serial_cb.tmr = xTimerCreate("btn_serial_tmr", btn->serial_thres_sec*1000 / portTICK_PERIOD_MS, + pdFALSE, btn, button_press_serial_cb); + } + btn->press_serial_cb.arg = arg; + btn->press_serial_cb.cb = cb; + btn->press_serial_cb.interval = interval_tick; + btn->press_serial_cb.pbtn = btn; + xTimerChangePeriod(btn->press_serial_cb.tmr, btn->serial_thres_sec*1000 / portTICK_PERIOD_MS, portMAX_DELAY); + return ESP_OK; +} + +esp_err_t iot_button_set_evt_cb(button_handle_t btn_handle, button_cb_type_t type, button_cb cb, void* arg) +{ + POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG); + button_dev_t* btn = (button_dev_t*) btn_handle; + if (type == BUTTON_CB_PUSH) { + btn->tap_psh_cb.arg = arg; + btn->tap_psh_cb.cb = cb; + btn->tap_psh_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_RATE_MS; + btn->tap_psh_cb.pbtn = btn; + xTimerChangePeriod(btn->tap_psh_cb.tmr, btn->tap_psh_cb.interval, portMAX_DELAY); + } else if (type == BUTTON_CB_RELEASE) { + btn->tap_rls_cb.arg = arg; + btn->tap_rls_cb.cb = cb; + btn->tap_rls_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_RATE_MS; + btn->tap_rls_cb.pbtn = btn; + xTimerChangePeriod(btn->tap_rls_cb.tmr, btn->tap_psh_cb.interval, portMAX_DELAY); + } else if (type == BUTTON_CB_TAP) { + btn->tap_short_cb.arg = arg; + btn->tap_short_cb.cb = cb; + btn->tap_short_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_RATE_MS; + btn->tap_short_cb.pbtn = btn; + } else if (type == BUTTON_CB_SERIAL) { + iot_button_set_serial_cb(btn_handle, 1, 1000 / portTICK_RATE_MS, cb, arg); + } + return ESP_OK; +} + +esp_err_t iot_button_add_on_press_cb(button_handle_t btn_handle, uint32_t press_sec, button_cb cb, void* arg) +{ + POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG); + IOT_CHECK(TAG, press_sec != 0, ESP_ERR_INVALID_ARG); + button_dev_t* btn = (button_dev_t*) btn_handle; + button_cb_t* cb_new = (button_cb_t*) calloc(1, sizeof(button_cb_t)); + POINT_ASSERT(TAG, cb_new, ESP_FAIL); + cb_new->on_press = 1; + cb_new->arg = arg; + cb_new->cb = cb; + cb_new->interval = press_sec * 1000 / portTICK_PERIOD_MS; + cb_new->pbtn = btn; + cb_new->tmr = xTimerCreate("btn_press_tmr", cb_new->interval, pdFALSE, cb_new, button_press_cb); + cb_new->next_cb = btn->cb_head; + btn->cb_head = cb_new; + return ESP_OK; +} + +esp_err_t iot_button_add_on_release_cb(button_handle_t btn_handle, uint32_t press_sec, button_cb cb, void* arg) +{ + POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG); + IOT_CHECK(TAG, press_sec != 0, ESP_ERR_INVALID_ARG); + button_dev_t* btn = (button_dev_t*) btn_handle; + button_cb_t* cb_new = (button_cb_t*) calloc(1, sizeof(button_cb_t)); + POINT_ASSERT(TAG, cb_new, ESP_FAIL); + btn->taskq_on = 1; + cb_new->arg = arg; + cb_new->cb = cb; + cb_new->interval = press_sec * 1000 / portTICK_PERIOD_MS; + cb_new->pbtn = btn; + cb_new->tmr = xTimerCreate("btn_press_tmr", cb_new->interval, pdFALSE, cb_new, button_press_cb); + cb_new->next_cb = btn->cb_head; + btn->cb_head = cb_new; + return ESP_OK; +} + diff --git a/components/button/button/button_obj.cpp b/components/button/button/button_obj.cpp new file mode 100644 index 0000000..8a1f287 --- /dev/null +++ b/components/button/button/button_obj.cpp @@ -0,0 +1,64 @@ +/* + * ESPRESSIF MIT License + * + * Copyright (c) 2017 + * + * Permission is hereby granted for use on ESPRESSIF SYSTEMS products only, in which case, + * it is free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished + * to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include +#include +#include +#include + +CButton::CButton(gpio_num_t gpio_num, button_active_t active_level) +{ + m_btn_handle = iot_button_create(gpio_num, active_level); +} + +CButton::~CButton() +{ + iot_button_delete(m_btn_handle); + m_btn_handle = NULL; +} + +esp_err_t CButton::set_evt_cb(button_cb_type_t type, button_cb cb, void* arg) +{ + return iot_button_set_evt_cb(m_btn_handle, type, cb, arg); +} + +esp_err_t CButton::set_serial_cb(button_cb cb, void* arg, TickType_t interval_tick, uint32_t start_after_sec) +{ + return iot_button_set_serial_cb(m_btn_handle, start_after_sec, interval_tick, cb, arg); +} + +esp_err_t CButton::add_on_press_cb(uint32_t press_sec, button_cb cb, void* arg) +{ + return iot_button_add_on_press_cb(m_btn_handle, press_sec, cb, arg); +} + +esp_err_t CButton::add_on_release_cb(uint32_t press_sec, button_cb cb, void* arg) +{ + return iot_button_add_on_release_cb(m_btn_handle, press_sec, cb, arg); +} + +esp_err_t CButton::rm_cb(button_cb_type_t type) +{ + return iot_button_rm_cb(m_btn_handle, type); +} diff --git a/components/button/button/include/iot_button.h b/components/button/button/include/iot_button.h new file mode 100644 index 0000000..287959b --- /dev/null +++ b/components/button/button/include/iot_button.h @@ -0,0 +1,281 @@ +/* + * ESPRESSIF MIT License + * + * Copyright (c) 2018 + * + * Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case, + * it is free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished + * to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + + +#ifndef _IOT_BUTTON_H_ +#define _IOT_BUTTON_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +typedef void (* button_cb)(void*); +typedef void* button_handle_t; + +typedef enum { + BUTTON_ACTIVE_HIGH = 1, /*!