├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── codelab_adapter_client ├── .gitignore ├── __init__.py ├── _version.py ├── base.py ├── base_aio.py ├── cloud_message.py ├── config.py ├── data │ └── user_settings.toml ├── hass.py ├── message.py ├── microbit.py ├── mqtt_node.py ├── session.py ├── settings.toml ├── simple_node.py ├── thing.py ├── tools │ ├── adapter_helper.py │ ├── linda.py │ ├── mdns_browser.py │ ├── mdns_registration.py │ ├── message.json │ ├── monitor.py │ ├── pub.py │ └── trigger.py ├── topic.py └── utils.py ├── docs └── readme.md ├── examples ├── HANode_simple_example.ipynb ├── cube_symphony_pygame.py ├── cube_symphony_sonicpi.py ├── eim_node.py ├── eim_node_aio.py ├── face_recognition_open_door.py ├── helloworld.hy ├── microbit_display.py ├── microbit_event.py ├── microbit_link_camera.py ├── neverland_control_door.py ├── neverland_door_open_capture.py ├── neverland_handle_message.ipynb ├── neverland_i_am_reading.py ├── neverland_irobot.py ├── neverland_list_all_lights.py ├── neverland_toggle_light.py ├── neverland_wechat.py ├── pgzero_makeymakey.py ├── play_neverland.py ├── readme.md └── src │ ├── Hand Clap.wav │ ├── High Hat.wav │ ├── Large Cowbell.wav │ └── Snare Drum.wav ├── readme.md ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests ├── test_codelab_adapter_client.py ├── test_linda_client.py ├── test_node.py └── test_subscriber.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * codelab_adapter_client version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | env 104 | venv3.6/ 105 | venv3.7 106 | 107 | .DS_Store 108 | .vscode/ 109 | 110 | env3.8/ 111 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | python: 5 | - 3.6 6 | - 3.5 7 | - 3.4 8 | - 2.7 9 | 10 | # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 11 | install: pip install -U tox-travis 12 | 13 | # Command to run tests, e.g. python setup.py test 14 | script: tox 15 | 16 | # Assuming you have installed the travis-ci CLI tool, after you 17 | # create the Github repo and add it to Travis, run the 18 | # following command to finish PyPI deployment setup: 19 | # $ travis encrypt --add deploy.password 20 | deploy: 21 | provider: pypi 22 | distributions: sdist bdist_wheel 23 | user: wwj718 24 | password: 25 | secure: PLEASE_REPLACE_ME 26 | on: 27 | tags: true 28 | repo: wwj718/codelab_adapter_client 29 | python: 3.6 30 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Wenjie Wu 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/wwj718/codelab_adapter_client/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | codelab_adapter_client could always use more documentation, whether as part of the 42 | official codelab_adapter_client docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/wwj718/codelab_adapter_client/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `codelab_adapter_client` for local development. 61 | 62 | 1. Fork the `codelab_adapter_client` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/codelab_adapter_client.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv codelab_adapter_client 70 | $ cd codelab_adapter_client/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the 80 | tests, including testing other Python versions with tox:: 81 | 82 | $ flake8 codelab_adapter_client tests 83 | $ python setup.py test or py.test 84 | $ tox 85 | 86 | To get flake8 and tox, just pip install them into your virtualenv. 87 | 88 | 6. Commit your changes and push your branch to GitHub:: 89 | 90 | $ git add . 91 | $ git commit -m "Your detailed description of your changes." 92 | $ git push origin name-of-your-bugfix-or-feature 93 | 94 | 7. Submit a pull request through the GitHub website. 95 | 96 | Pull Request Guidelines 97 | ----------------------- 98 | 99 | Before you submit a pull request, check that it meets these guidelines: 100 | 101 | 1. The pull request should include tests. 102 | 2. If the pull request adds functionality, the docs should be updated. Put 103 | your new functionality into a function with a docstring, and add the 104 | feature to the list in README.rst. 105 | 3. The pull request should work for Python 2.7, 3.4, 3.5 and 3.6, and for PyPy. Check 106 | https://travis-ci.org/wwj718/codelab_adapter_client/pull_requests 107 | and make sure that the tests pass for all supported Python versions. 108 | 109 | Tips 110 | ---- 111 | 112 | To run a subset of tests:: 113 | 114 | $ py.test tests.test_codelab_adapter_client 115 | 116 | 117 | Deploying 118 | --------- 119 | 120 | A reminder for the maintainers on how to deploy. 121 | Make sure all your changes are committed (including an entry in HISTORY.rst). 122 | Then run:: 123 | 124 | $ bumpversion patch # possible: major / minor / patch 125 | $ git push 126 | $ git push --tags 127 | 128 | Travis will then deploy to PyPI if tests pass. 129 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | * 4.1.7 (2021-03-16) 5 | * 2.1.3 (2020-11-25) 6 | * 2.1.2 (2020-10-29) 7 | * 2.1.1 (2020-10-18) 8 | * 2.1.0 (2020-10-15) 9 | * 2.0.3 (2020-10-15) 10 | * 2.0.2 (2020-09-30) 11 | * 2.0.1 (2020-09-26) 12 | * 2.0.0 (2020-09-24) 13 | * 1.9.1 (2020-08-29) 14 | * 1.9.1 (2020-08-14) 15 | * 1.9.0 (2020-08-14) 16 | * 1.8.1 (2020-07-11) 17 | * 1.8.0 (2020-07-05) 18 | * 1.7.4 (2020-05-10) 19 | * 1.7.3 (2020-05-10) 20 | * 1.7.2 (2020-05-10) 21 | * 1.7.1 (2020-05-10) 22 | * 1.7.0 (2020-05-10) 23 | * 1.6.2 (2020-05-06) 24 | * 1.6.1 (2020-05-06) 25 | * 1.6.0 (2020-04-16) 26 | * 1.5.0 (2020-04-09) 27 | * 1.4.0 (2020-04-07) 28 | * 1.3.0 (2020-04-07) 29 | * 1.2.0 (2020-04-06) 30 | * 1.1.0 (2020-04-03) 31 | * 1.0.1 (2020-03-25) 32 | * 1.0.0 (2019-10-01) 33 | * 0.9.8 (2019-09-11) 34 | * 0.9.4 (2019-08-28) 35 | * 0.9.3 (2019-08-21) 36 | * 0.9.2 (2019-08-20) 37 | * 0.9.1 (2019-08-18) 38 | * 0.9.0 (2019-08-17) 39 | * 0.8.2 (2019-08-16) 40 | * 0.8.1 (2019-08-16) 41 | * 0.8.0 (2019-08-16) 42 | * 0.7.1 (2019-08-09) 43 | * 0.7.0 (2019-08-08) 44 | * 0.6.0 (2019-07-30) 45 | * 0.5.0 (2019-07-27) 46 | * 0.4.1 (2019-07-24) 47 | * 0.4.0 (2019-07-24) 48 | * 0.3.1 (2019-07-21) 49 | * 0.3.0 (2019-07-21) 50 | * 0.2.0 (2019-07-21) 51 | * 0.1.0 (2019-07-03) 52 | * First release on PyPI. 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Python Boilerplate contains all the boilerplate you need to create a Python package. 5 | Copyright (C) 2019 Wenjie Wu 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | Also add information on how to contact you by electronic and paper mail. 21 | 22 | You should also get your employer (if you work as a programmer) or school, 23 | if any, to sign a "copyright disclaimer" for the program, if necessary. 24 | For more information on this, and how to apply and follow the GNU GPL, see 25 | . 26 | 27 | The GNU General Public License does not permit incorporating your program 28 | into proprietary programs. If your program is a subroutine library, you 29 | may consider it more useful to permit linking proprietary applications with 30 | the library. If this is what you want to do, use the GNU Lesser General 31 | Public License instead of this License. But first, please read 32 | . 33 | 34 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | include readme.md 7 | recursive-include codelab_adapter_client/data * 8 | 9 | recursive-include tests * 10 | recursive-exclude * __pycache__ 11 | recursive-exclude * *.py[co] 12 | 13 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 33 | 34 | clean-build: ## remove build artifacts 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-test: ## remove test and coverage artifacts 48 | rm -fr .tox/ 49 | rm -f .coverage 50 | rm -fr htmlcov/ 51 | rm -fr .pytest_cache 52 | 53 | lint: ## check style with flake8 54 | flake8 codelab_adapter_client tests 55 | 56 | test: ## run tests quickly with the default Python 57 | py.test 58 | 59 | test-all: ## run tests on every Python version with tox 60 | tox 61 | 62 | coverage: ## check code coverage quickly with the default Python 63 | coverage run --source codelab_adapter_client -m pytest 64 | coverage report -m 65 | coverage html 66 | $(BROWSER) htmlcov/index.html 67 | 68 | docs: ## generate Sphinx HTML documentation, including API docs 69 | rm -f docs/codelab_adapter_client.rst 70 | rm -f docs/modules.rst 71 | sphinx-apidoc -o docs/ codelab_adapter_client 72 | $(MAKE) -C docs clean 73 | $(MAKE) -C docs html 74 | $(BROWSER) docs/_build/html/index.html 75 | 76 | servedocs: docs ## compile the docs watching for changes 77 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 78 | 79 | release: dist ## package and upload a release 80 | twine upload dist/* 81 | 82 | dist: clean ## builds source and wheel package 83 | python setup.py sdist 84 | python setup.py bdist_wheel 85 | ls -l dist 86 | 87 | install: clean ## install the package to the active Python's site-packages 88 | python setup.py install 89 | 90 | merge_from_upstream: 91 | git remote add upstream https://github.com/wwj718/codelab_adapter_client; 92 | git fetch upstream; 93 | git merge upstream/master; 94 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | codelab_adapter_client 3 | ====================== 4 | 5 | 6 | .. image:: https://img.shields.io/pypi/v/codelab_adapter_client.svg 7 | :target: https://pypi.python.org/pypi/codelab_adapter_client 8 | 9 | .. image:: https://img.shields.io/travis/wwj718/codelab_adapter_client.svg 10 | :target: https://travis-ci.org/wwj718/codelab_adapter_client 11 | 12 | .. image:: https://readthedocs.org/projects/codelab-adapter-client/badge/?version=latest 13 | :target: https://codelab-adapter-client.readthedocs.io/en/latest/?badge=latest 14 | :alt: Documentation Status 15 | 16 | 17 | Python Client of CodeLab Adapter(https://adapter.codelab.club/) v2. 18 | 19 | 20 | Install 21 | ------- 22 | 23 | pip install codelab_adapter_client 24 | 25 | 26 | Usage 27 | ----- 28 | 29 | from codelab_adapter_client import AdapterNode 30 | 31 | example 32 | ------- 33 | 34 | https://github.com/wwj718/codelab_adapter_client/blob/master/examples/extension_eim.py 35 | 36 | 37 | Features 38 | -------- 39 | 40 | * TODO 41 | 42 | Credits 43 | ------- 44 | 45 | This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. 46 | 47 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 48 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage 49 | -------------------------------------------------------------------------------- /codelab_adapter_client/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Ignore dynaconf secret files 3 | .secrets.* 4 | -------------------------------------------------------------------------------- /codelab_adapter_client/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for codelab_adapter_client.""" 2 | 3 | __author__ = """Wenjie Wu""" 4 | __email__ = 'wuwenjie718@gmail.com' 5 | __version__ = '4.4.2' 6 | 7 | from .base import MessageNode, AdapterNode, JupyterNode, SimpleNode 8 | from .hass import HANode 9 | from .base_aio import MessageNodeAio, AdapterNodeAio 10 | from .utils import send_message, send_simple_message, run_monitor -------------------------------------------------------------------------------- /codelab_adapter_client/_version.py: -------------------------------------------------------------------------------- 1 | protocol_version = "3.0" -------------------------------------------------------------------------------- /codelab_adapter_client/base.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | import sys 4 | import uuid 5 | import os 6 | from abc import ABCMeta, abstractmethod 7 | from pathlib import Path 8 | import argparse 9 | import concurrent.futures 10 | 11 | import msgpack 12 | import zmq 13 | # import psutil 14 | from codelab_adapter_client.config import settings 15 | from codelab_adapter_client.topic import * 16 | from codelab_adapter_client.utils import threaded, TokenBucket, LindaTimeoutError, NodeTerminateError, LindaOperate 17 | from codelab_adapter_client._version import protocol_version 18 | from codelab_adapter_client.session import _message_template 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | SPEED_DEBUG = False 23 | 24 | class MessageNode(metaclass=ABCMeta): 25 | # jupyter client Session: https://github.com/jupyter/jupyter_client/blob/master/jupyter_client/session.py#L249 26 | def __init__( 27 | self, 28 | name='', 29 | logger=logger, 30 | codelab_adapter_ip_address=None, 31 | subscriber_port='16103', 32 | publisher_port='16130', #write to conf file(jupyter) 33 | subscriber_list=[SCRATCH_TOPIC, NODES_OPERATE_TOPIC, LINDA_CLIENT], 34 | loop_time=settings.ZMQ_LOOP_TIME, 35 | connect_time=0.1, 36 | external_message_processor=None, 37 | receive_loop_idle_addition=None, 38 | token=None, 39 | bucket_token=100, 40 | bucket_fill_rate=100, 41 | recv_mode = "noblock", 42 | ): 43 | ''' 44 | :param codelab_adapter_ip_address: Adapter IP Address - 45 | default: 127.0.0.1 46 | :param subscriber_port: codelab_adapter subscriber port. 47 | :param publisher_port: codelab_adapter publisher port. 48 | :param loop_time: Receive loop sleep time. 49 | :param connect_time: Allow the node to connect to adapter 50 | :param token: for safety 51 | :param bucket_token/bucket_fill_rate: rate limit 52 | ''' 53 | self.last_pub_time = time.time 54 | self.bucket_token = bucket_token 55 | self.bucket_fill_rate = bucket_fill_rate 56 | self.recv_mode = recv_mode 57 | self.bucket = TokenBucket(bucket_token, bucket_fill_rate) 58 | self.logger = logger 59 | self._running = True # use it to control Python thread, work with self.terminate() 60 | if name: 61 | self.name = name 62 | else: 63 | self.name = type(self).__name__ # instance name(self is instance) 64 | self.token = token 65 | self.subscriber_port = subscriber_port 66 | self.publisher_port = publisher_port 67 | self.subscriber_list = subscriber_list 68 | self.subscribed_topics = set( 69 | ) # genetate sub topics self.subscribed_topics.add 70 | self.receive_loop_idle_addition = receive_loop_idle_addition 71 | self.external_message_processor = external_message_processor 72 | self.connect_time = connect_time 73 | if codelab_adapter_ip_address: 74 | self.codelab_adapter_ip_address = codelab_adapter_ip_address 75 | else: 76 | # check for a running CodeLab Adapter 77 | # self.check_adapter_is_running() 78 | # determine this computer's IP address 79 | self.codelab_adapter_ip_address = '127.0.0.1' 80 | self.loop_time = loop_time 81 | 82 | self.logger.info( 83 | '\n************************************************************') 84 | self.logger.info('CodeLab Adapter IP address: ' + 85 | self.codelab_adapter_ip_address) 86 | self.logger.info('Subscriber Port = ' + self.subscriber_port) 87 | self.logger.info('Publisher Port = ' + self.publisher_port) 88 | self.logger.info('Loop Time = ' + str(loop_time) + ' seconds') 89 | self.logger.info( 90 | '************************************************************') 91 | 92 | # establish the zeromq sub and pub sockets and connect to the adapter 93 | self.context = zmq.Context() 94 | # 以便于一开始就发送消息,尽管连接还未建立 95 | self.publisher = self.context.socket(zmq.PUB) 96 | pub_connect_string = f'tcp://{self.codelab_adapter_ip_address}:{self.publisher_port}' 97 | self.publisher.connect(pub_connect_string) 98 | # Allow enough time for the TCP connection to the adapter complete. 99 | time.sleep(self.connect_time / 100 | 2) # block 0.3 -> 0.1, to support init pub 101 | 102 | def __str__(self): 103 | return self.name 104 | 105 | def is_running(self): 106 | return self._running 107 | 108 | ''' 109 | def check_adapter_is_running(self): 110 | adapter_exists = False 111 | for pid in psutil.pids(): 112 | p = psutil.Process(pid) 113 | try: 114 | p_command = p.cmdline() 115 | except psutil.AccessDenied: 116 | # occurs in Windows - ignore 117 | continue 118 | try: 119 | if any('codelab' in s.lower() for s in p_command): 120 | adapter_exists = True 121 | else: 122 | continue 123 | except UnicodeDecodeError: 124 | continue 125 | 126 | if not adapter_exists: 127 | raise RuntimeError( 128 | 'CodeLab Adapter is not running - please start it.') 129 | ''' 130 | 131 | def set_subscriber_topic(self, topic): 132 | if not type(topic) is str: 133 | raise TypeError('Subscriber topic must be string') 134 | self.subscriber_list.append(topic) 135 | 136 | def publish_payload(self, payload, topic=''): 137 | if not type(topic) is str: 138 | raise TypeError('Publish topic must be string', 'topic') 139 | 140 | if self.bucket.consume(1): 141 | # pack 142 | message = msgpack.packb(payload, use_bin_type=True) 143 | 144 | pub_envelope = topic.encode() 145 | if SPEED_DEBUG: 146 | self.logger.debug(f"SPEED_DEBUG-publish_payload: {time.time()}") 147 | self.publisher.send_multipart([pub_envelope, message]) 148 | else: 149 | now = time.time() 150 | if (now - self.last_pub_time > 1): 151 | error_text = f"发送消息过于频繁!({self.bucket_token}, {self.bucket_fill_rate})" # 1 /s or ui 152 | self.logger.error(error_text) 153 | self.pub_notification(error_text, type="ERROR") 154 | self.last_pub_time = time.time() 155 | 156 | def receive_loop(self): 157 | """ 158 | This is the receive loop for receiving sub messages. 159 | """ 160 | self.subscriber = self.context.socket(zmq.SUB) 161 | sub_connect_string = f'tcp://{self.codelab_adapter_ip_address}:{self.subscriber_port}' 162 | self.subscriber.connect(sub_connect_string) 163 | 164 | if self.subscriber_list: 165 | for topic in self.subscriber_list: 166 | self.subscriber.setsockopt(zmq.SUBSCRIBE, topic.encode()) 167 | self.subscribed_topics.add(topic) 168 | 169 | while self._running: 170 | try: 171 | # https://github.com/jupyter/jupyter_client/blob/master/jupyter_client/session.py#L814 172 | if self.recv_mode == "noblock": 173 | data = self.subscriber.recv_multipart(zmq.NOBLOCK) # NOBLOCK 174 | else: 175 | data = self.subscriber.recv_multipart() 176 | # unpackb 177 | try: 178 | # some data is invalid 179 | topic = data[0].decode() 180 | payload = msgpack.unpackb(data[1], 181 | raw=False) # replace unpackb 182 | self.message_handle(topic, payload) 183 | except Exception as e: 184 | self.logger.error(str(e)) 185 | # 这里很慢 186 | # self.logger.debug(f"extension.receive_loop -> {time.time()}") 187 | # if no messages are available, zmq throws this exception 188 | except zmq.error.Again: 189 | try: 190 | if self.receive_loop_idle_addition: 191 | self.receive_loop_idle_addition() 192 | time.sleep(self.loop_time) 193 | except KeyboardInterrupt: 194 | self.clean_up() 195 | raise KeyboardInterrupt 196 | ''' 197 | except msgpack.exceptions.ExtraData as e: 198 | self.logger.error(str(e)) 199 | ''' 200 | 201 | def receive_loop_as_thread(self): 202 | # warn: zmq socket is not threadsafe 203 | threaded(self.receive_loop)() 204 | 205 | def message_handle(self, topic, payload): 206 | """ 207 | Override this method with a custom adapter message processor for subscribed messages. 208 | """ 209 | print( 210 | 'message_handle method should provide implementation in subclass.') 211 | 212 | def clean_up(self): 213 | """ 214 | Clean up before exiting. 215 | """ 216 | self._running = False 217 | time.sleep(0.1) 218 | # todo 等待线程退出后再回收否则可能出错 219 | self.publisher.close() 220 | self.subscriber.close() 221 | self.context.term() 222 | 223 | 224 | class AdapterNode(MessageNode): 225 | ''' 226 | CodeLab Adapter Node 227 | 228 | Adapter Extension is subclass of AdapterNode 229 | 230 | message_types = [ 231 | "notification", "from_scratch", "from_adapter", "current_extension" 232 | ] 233 | ''' 234 | def __init__(self, 235 | start_cmd_message_id=None, 236 | is_started_now=True, 237 | *args, 238 | **kwargs): 239 | ''' 240 | :param codelab_adapter_ip_address: Adapter IP Address - 241 | default: 127.0.0.1 242 | :param subscriber_port: codelab_adapter subscriber port. 243 | :param publisher_port: codelab_adapter publisher port. 244 | :param loop_time: Receive loop sleep time. 245 | :param connect_time: Allow the node to connect to adapter 246 | ''' 247 | super().__init__(*args, **kwargs) 248 | if not hasattr(self, 'TOPIC'): 249 | self.TOPIC = ADAPTER_TOPIC # message topic: the message from adapter 250 | if not hasattr(self, 'NODE_ID'): 251 | self.NODE_ID = "eim" 252 | if not hasattr(self, 'HELP_URL'): 253 | self.HELP_URL = "http://adapter.codelab.club/extension_guide/introduction/" 254 | if not hasattr(self, 'WEIGHT'): 255 | self.WEIGHT = 0 256 | # todo handler: https://github.com/offu/WeRoBot/blob/master/werobot/robot.py#L590 257 | # self._handlers = {k: [] for k in self.message_types} 258 | # self._handlers['all'] = [] 259 | 260 | if not start_cmd_message_id: 261 | ''' 262 | 1 node from cmd(start_cmd_message_id in args) 以脚本运行 263 | 1.1 if __name__ == '__main__': 264 | 1.2 采用命令行参数判断,数量内容 更精准,因为ide也是使用脚本启动 265 | 2 extension from param(with start_cmd_message_id) 266 | 3 work with jupyter/mu 267 | ''' 268 | if "--start-cmd-message-id" in sys.argv: 269 | parser = argparse.ArgumentParser() 270 | parser.add_argument("--start-cmd-message-id", dest="message_id", default=None, 271 | help="start cmd message id, a number or uuid(string)") 272 | args = parser.parse_args() 273 | start_cmd_message_id = args.message_id 274 | else: 275 | pass # extensions 276 | 277 | self.start_cmd_message_id = start_cmd_message_id 278 | self.logger.debug(f"start_cmd_message_id -> {self.start_cmd_message_id}") 279 | if is_started_now and self.start_cmd_message_id: 280 | self.started() 281 | 282 | # linda 283 | self.linda_wait_futures = [] 284 | 285 | def started(self): 286 | ''' 287 | started notify 288 | ''' 289 | self.pub_notification(f"启动 {self.NODE_ID}") 290 | # request++ and uuid, Compatible with them. 291 | try: 292 | int_message = int(self.start_cmd_message_id) 293 | self.send_reply(int_message) 294 | except ValueError: 295 | self.send_reply(self.start_cmd_message_id) 296 | 297 | def send_reply(self, message_id, content="ok"): 298 | response_message = self.message_template() 299 | response_message["payload"]["message_id"] = message_id 300 | response_message["payload"]["content"] = content 301 | self.publish(response_message) 302 | 303 | ''' 304 | def add_handler(self, func, type='all'): 305 | # add message handler to Extension instance。 306 | # :param func: handler method 307 | # :param type: handler type 308 | 309 | # :return: None 310 | 311 | if not callable(func): 312 | raise ValueError("{} is not callable".format(func)) 313 | 314 | self._handlers[type].append(func) 315 | 316 | def get_handlers(self, type): 317 | return self._handlers.get(type, []) + self._handlers['all'] 318 | 319 | def handler(self, f): 320 | # add handler to every message. 321 | 322 | self.add_handler(f, type='all') 323 | return f 324 | ''' 325 | 326 | def generate_node_id(self, filename): 327 | ''' 328 | extension_eim.py -> extension_eim 329 | ''' 330 | node_name = Path(filename).stem 331 | return self._node_name_to_node_id(node_name) 332 | 333 | def _node_name_to_node_id(self, node_name): 334 | return f'eim/{node_name}' 335 | 336 | # def extension_message_handle(self, f): 337 | def extension_message_handle(self, topic, payload): 338 | """ 339 | the decorator for adding current_extension handler 340 | 341 | self.add_handler(f, type='current_extension') 342 | return f 343 | """ 344 | self.logger.info("please set the method to your handle method") 345 | 346 | def exit_message_handle(self, topic, payload): 347 | self.pub_extension_statu_change(self.NODE_ID, "stop") 348 | if self._running: 349 | stop_cmd_message_id = payload.get("message_id", None) 350 | self.terminate(stop_cmd_message_id=stop_cmd_message_id) 351 | 352 | def message_template(self): 353 | # _message_template(sender,node_id,token) 354 | template = _message_template(self.name, self.NODE_ID, self.token) 355 | return template 356 | 357 | def publish(self, message): 358 | assert isinstance(message, dict) 359 | topic = message.get('topic') 360 | payload = message.get("payload") 361 | if not topic: 362 | topic = self.TOPIC 363 | if not payload.get("node_id"): 364 | payload["node_id"] = self.NODE_ID 365 | self.logger.debug( 366 | f"{self.name} publish: topic: {topic} payload:{payload}") 367 | 368 | self.publish_payload(payload, topic) 369 | 370 | ######################## 371 | # todo linda mixin 372 | def _send_to_linda_server(self, operate, _tuple): 373 | ''' 374 | send to linda server and wait it (client block / future) 375 | return: 376 | message_id 377 | ''' 378 | assert isinstance(operate, LindaOperate) 379 | assert isinstance(_tuple, list) 380 | if not self._running: 381 | # loop 382 | Exception(f"_running: {self._running}") 383 | 384 | topic = LINDA_SERVER # to 385 | 386 | payload = self.message_template()["payload"] 387 | payload["message_id"] = uuid.uuid4().hex 388 | payload["operate"] = operate.value # 将枚举数据变成序列化 389 | payload["tuple"] = _tuple 390 | payload["content"] = _tuple # 是否必要 391 | 392 | self.logger.debug( 393 | f"{self.name} publish: topic: {topic} payload:{payload}") 394 | 395 | self.publish_payload(payload, topic) 396 | return payload["message_id"] 397 | 398 | 399 | def _send_and_wait_origin(self, operate, _tuple, timeout): 400 | ''' 401 | 失败之后out东西 402 | ''' 403 | # 确保 running, 接收到消息 404 | message_id = self._send_to_linda_server(operate, _tuple) 405 | ''' 406 | return future timeout 407 | futurn 被消息循环队列释放 408 | ''' 409 | f = concurrent.futures.Future() 410 | self.linda_wait_futures.append((message_id, f)) 411 | # todo 加入到队列里: (message_id, f) f.set_result(tuple) 412 | try: 413 | result = f.result(timeout=timeout) 414 | return result 415 | except concurrent.futures.TimeoutError: 416 | # result = f"timeout: {timeout}" 417 | # todo 结构化 418 | raise LindaTimeoutError(f'timeout: {timeout}; message_id: {message_id}') 419 | # todo exit exception 420 | # return result 421 | 422 | def _send_and_wait(self, operate, _tuple, timeout): 423 | try: 424 | return self._send_and_wait_origin(operate, _tuple, timeout) 425 | except: 426 | if operate in [LindaOperate.IN]: 427 | self.logger.warning(f'cancel: {operate} {_tuple}') 428 | self._send_to_linda_server(LindaOperate.OUT, _tuple) # 不等回复 429 | time.sleep(0.01) 430 | 431 | def linda_in(self, _tuple: list, timeout=None): 432 | ''' 433 | timeout 心理模型不好,尽量不用,timeout之后,linda server还在维护 in_queue 434 | 尽量使用inp 435 | # https://docs.python.org/zh-cn/3/library/typing.html 436 | params: 437 | _tuple: list 438 | linda in 439 | block , 不要timeout? 440 | todo 441 | 返回future,由用户自己决定是否阻塞? callback 442 | 参数 return_future = False 443 | 444 | todo 445 | 如果中断取消呢? 446 | 粗糙的做法 out 出去 447 | ''' 448 | return self._send_and_wait(LindaOperate.IN, _tuple, timeout) 449 | 450 | def linda_inp(self, _tuple: list): 451 | return self._send_and_wait(LindaOperate.INP, _tuple, None) 452 | 453 | 454 | # 阻塞吗? 455 | def linda_rd(self, _tuple: list, timeout=None): 456 | ''' 457 | rd 要能够匹配才有意思, 采用特殊字符串,匹配 458 | 459 | 如果当前没有 client 要等待吗(服务端如果看到相同的会再次发送,不等待的服务端返回 [], 先做阻塞的),行为在client决定,已经收到通知了 460 | ''' 461 | return self._send_and_wait(LindaOperate.RD, _tuple, timeout) 462 | 463 | def linda_rdp(self, _tuple: list): 464 | return self._send_and_wait(LindaOperate.RDP, _tuple, None) 465 | 466 | 467 | def linda_out(self, _tuple, wait=True): 468 | # 限制速率, 每秒30帧 469 | # out 是否也确认,以此限制速率,确保收到 470 | # self._send_to_linda_server(LindaOperate.OUT, _tuple) 471 | if wait: 472 | return self._send_and_wait(LindaOperate.OUT, _tuple, None) 473 | else: 474 | return self._send_to_linda_server(LindaOperate.OUT, _tuple) # message id 回执 475 | 476 | 477 | 478 | # helper , 立刻返回 479 | def linda_dump(self): 480 | timeout=None 481 | return self._send_and_wait(LindaOperate.DUMP, ["dump"], timeout) 482 | 483 | def linda_status(self): 484 | timeout=None 485 | return self._send_and_wait(LindaOperate.STATUS, ["status"], timeout) 486 | 487 | def linda_reboot(self): 488 | timeout=None 489 | return self._send_and_wait(LindaOperate.REBOOT, ["reboot"], timeout) 490 | 491 | ######################## 492 | 493 | def get_node_id(self): 494 | return self.NODE_ID 495 | 496 | def pub_notification(self, content, topic=NOTIFICATION_TOPIC, type="INFO"): 497 | ''' 498 | type 499 | ERROR 500 | INFO 501 | { 502 | topic: 503 | payload: { 504 | content: 505 | } 506 | } 507 | ''' 508 | node_id = self.NODE_ID 509 | payload = self.message_template()["payload"] 510 | payload["type"] = type 511 | payload["content"] = content 512 | self.publish_payload(payload, topic) 513 | 514 | def pub_html_notification(self, 515 | html_content, 516 | topic=NOTIFICATION_TOPIC, 517 | type="INFO"): 518 | ''' 519 | type 520 | ERROR 521 | INFO 522 | { 523 | topic: 524 | payload: { 525 | content: 526 | } 527 | } 528 | ''' 529 | node_id = self.NODE_ID 530 | payload = self.message_template()["payload"] 531 | payload["type"] = type 532 | payload["html"] = True 533 | # json 描述 534 | payload["content"] = html_content # html 535 | self.publish_payload(payload, topic) 536 | 537 | def pub_device_connect_status(self): 538 | ''' 539 | msg_type?or topic? 540 | different content 541 | device name 542 | node_id 543 | status: connect/disconnect 544 | ''' 545 | pass 546 | 547 | def stdin_ask(self): 548 | ''' 549 | https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-stdin-router-dealer-channel 550 | use future(set future)? or sync 551 | pub/sub channel 552 | a special topic or msg_type 553 | build in 554 | ''' 555 | pass 556 | 557 | def pub_status(self, extension_statu_map): 558 | ''' 559 | pub node status 560 | ''' 561 | topic = NODES_STATUS_TOPIC 562 | payload = self.message_template()["payload"] 563 | payload["content"] = extension_statu_map 564 | self.publish_payload(payload, topic) 565 | 566 | def pub_extension_statu_change(self, node_name, statu): 567 | topic = NODE_STATU_CHANGE_TOPIC 568 | node_id = self.NODE_ID 569 | payload = self.message_template()["payload"] 570 | payload["node_name"] = node_name 571 | payload["content"] = statu 572 | self.publish_payload(payload, topic) 573 | 574 | def receive_loop_as_thread(self): 575 | threaded(self.receive_loop)() 576 | 577 | def message_handle(self, topic, payload): 578 | """ 579 | Override this method with a custom adapter message processor for subscribed messages. 580 | :param topic: Message Topic string. 581 | :param payload: Message Data. 582 | 583 | all the sub message 584 | process handler 585 | 586 | default sub: [SCRATCH_TOPIC, NODES_OPERATE_TOPIC] 587 | """ 588 | if SPEED_DEBUG: 589 | self.logger.debug(f"SPEED_DEBUG-message_handle: {time.time()}, topic:{topic}") 590 | if self.external_message_processor: 591 | # handle all sub messages 592 | # to handle websocket message 593 | self.external_message_processor(topic, payload) 594 | 595 | if topic == NODES_OPERATE_TOPIC: 596 | ''' 597 | 分布式: 主动停止 使用node_id 598 | extension也是在此关闭,因为extension也是一种node 599 | UI触发关闭命令 600 | ''' 601 | command = payload.get('content') 602 | if command == 'stop': 603 | ''' 604 | to stop node/extension 605 | ''' 606 | # 暂不处理extension 607 | # payload.get("node_id") == self.NODE_ID to stop extension 608 | # f'eim/{payload.get("node_name")}' == self.NODE_ID to stop node (generate extension id) 609 | if payload.get("node_id") == self.NODE_ID or payload.get( 610 | "node_id") == "all" or self._node_name_to_node_id( 611 | payload.get("node_name")) == self.NODE_ID: 612 | # self.logger.debug(f"node stop message: {payload}") 613 | # self.logger.debug(f"node self.name: {self.name}") 614 | self.logger.info(f"stop {self}") 615 | self.exit_message_handle(topic, payload) 616 | return 617 | 618 | if topic in [SCRATCH_TOPIC]: 619 | ''' 620 | x 接受来自scratch的消息 621 | v 接受所有订阅主题的消息 622 | 插件业务类 623 | ''' 624 | if payload.get("node_id") == self.NODE_ID: 625 | self.extension_message_handle(topic, payload) 626 | ''' 627 | handlers = self.get_handlers(type="current_extension") 628 | for handler in handlers: 629 | handler(topic, payload) 630 | ''' 631 | 632 | if topic == LINDA_CLIENT: 633 | for (message_id, future) in self.linda_wait_futures: 634 | if message_id == payload.get("message_id"): 635 | future.set_result(payload["tuple"]) 636 | break 637 | 638 | if topic in [LINDA_CLIENT, LINDA_SERVER]: 639 | if hasattr(self, "_linda_message_handle"): 640 | getattr(self, "_linda_message_handle")(topic, payload) 641 | 642 | # todo : 如果存在 _linda_message_handle,则调用,所有的都收 643 | 644 | 645 | 646 | def terminate(self, stop_cmd_message_id=None): 647 | if self._running: 648 | self.logger.info(f"stopped {self.NODE_ID}") 649 | self.pub_notification(f"停止 {self.NODE_ID}") # 会通知给 UI 650 | if stop_cmd_message_id: 651 | self.send_reply(stop_cmd_message_id) 652 | # super().terminate() 653 | # 释放 future 654 | # for (message_id, future) in self.linda_wait_futures: 655 | for (message_id, f) in self.linda_wait_futures: 656 | if not f.done(): 657 | f.set_exception(NodeTerminateError("terminate")) 658 | # f.set_result(Exception("terminate")) 659 | # f.set_result("terminate") 660 | # time.sleep(0.1) 661 | 662 | self.clean_up() 663 | 664 | def is_connected(self, timeout=0.1): 665 | # ping set timeout 666 | _tuple = ["%%ping", "ping"] 667 | try: 668 | res = self._send_and_wait(LindaOperate.OUT, _tuple, timeout=timeout) 669 | return True 670 | except LindaTimeoutError as e: 671 | return False 672 | 673 | class JupyterNode(AdapterNode): 674 | def __init__(self, *args, **kwargs): 675 | super().__init__(*args, **kwargs) 676 | 677 | 678 | class SimpleNode(JupyterNode): 679 | def __init__(self, *args, **kwargs): 680 | super().__init__(*args, **kwargs) 681 | 682 | def simple_publish(self, content): 683 | message = {"payload": {"content": ""}} 684 | message["payload"]["content"] = content 685 | self.publish(message) 686 | -------------------------------------------------------------------------------- /codelab_adapter_client/base_aio.py: -------------------------------------------------------------------------------- 1 | import zmq 2 | import zmq.asyncio 3 | import time 4 | import logging 5 | import uuid 6 | import sys 7 | import msgpack 8 | from abc import ABCMeta, abstractmethod 9 | from pathlib import Path 10 | import asyncio 11 | import argparse 12 | 13 | from codelab_adapter_client.config import settings 14 | from codelab_adapter_client.topic import * 15 | # from codelab_adapter_client.topic import ADAPTER_TOPIC, SCRATCH_TOPIC, NOTIFICATION_TOPIC, EXTS_OPERATE_TOPIC 16 | from codelab_adapter_client.utils import threaded, TokenBucket, NodeTerminateError, LindaOperate 17 | from codelab_adapter_client.session import _message_template 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class MessageNodeAio(metaclass=ABCMeta): 23 | def __init__( 24 | self, 25 | name='', 26 | logger=logger, 27 | codelab_adapter_ip_address=None, 28 | subscriber_port='16103', 29 | publisher_port='16130', 30 | subscriber_list=[SCRATCH_TOPIC, NODES_OPERATE_TOPIC, LINDA_CLIENT], 31 | loop_time=settings.ZMQ_LOOP_TIME, # todo config by user 32 | connect_time=0.3, 33 | external_message_processor=None, 34 | receive_loop_idle_addition=None, 35 | event_loop=None, 36 | token=None, 37 | bucket_token=100, 38 | bucket_fill_rate=100, 39 | recv_mode = "noblock", 40 | ): 41 | ''' 42 | :param codelab_adapter_ip_address: Adapter IP Address - 43 | default: 127.0.0.1 44 | :param subscriber_port: codelab_adapter subscriber port. 45 | :param publisher_port: codelab_adapter publisher port. 46 | :param loop_time: Receive loop sleep time. 47 | :param connect_time: Allow the node to connect to adapter 48 | :param token: for safety 49 | :param bucket_token/bucket_fill_rate: rate limit 50 | ''' 51 | self.last_pub_time = time.time() 52 | self.bucket_token = bucket_token 53 | self.bucket_fill_rate = bucket_fill_rate 54 | self.recv_mode = recv_mode 55 | self.bucket = TokenBucket(bucket_token, bucket_fill_rate) 56 | self._running = True # use it to receive_loop 57 | self.logger = logger 58 | if name: 59 | self.name = name 60 | else: 61 | self.name = type(self).__name__ 62 | self.token = token 63 | self.subscriber_list = subscriber_list 64 | self.receive_loop_idle_addition = receive_loop_idle_addition 65 | self.external_message_processor = external_message_processor 66 | self.connect_time = connect_time 67 | 68 | if codelab_adapter_ip_address: 69 | self.codelab_adapter_ip_address = codelab_adapter_ip_address 70 | else: 71 | # check for a running CodeLab Adapter 72 | # determine this computer's IP address 73 | self.codelab_adapter_ip_address = '127.0.0.1' 74 | 75 | self.subscriber_port = subscriber_port 76 | self.publisher_port = publisher_port 77 | self.loop_time = loop_time 78 | 79 | self.logger.info( 80 | '\n************************************************************') 81 | self.logger.info('CodeLab Adapter IP address: ' + 82 | self.codelab_adapter_ip_address) 83 | self.logger.info('Subscriber Port = ' + self.subscriber_port) 84 | self.logger.info('Publisher Port = ' + self.publisher_port) 85 | self.logger.info('Loop Time = ' + str(loop_time) + ' seconds') 86 | self.logger.info( 87 | '************************************************************') 88 | 89 | if event_loop: 90 | self.event_loop = event_loop 91 | else: 92 | self.event_loop = asyncio.get_event_loop() 93 | 94 | # 放在init 可能会有线程问题, 但如此依赖允许在消息管道建立之前,发送消息。 95 | # establish the zeromq sub and pub sockets and connect to the adapter 96 | self.context = zmq.asyncio.Context() # zmq.Context() 97 | self.subscriber = self.context.socket(zmq.SUB) 98 | connect_string = "tcp://" + self.codelab_adapter_ip_address + ':' + self.subscriber_port 99 | self.subscriber.connect(connect_string) 100 | 101 | self.publisher = self.context.socket(zmq.PUB) 102 | connect_string = "tcp://" + self.codelab_adapter_ip_address + ':' + self.publisher_port 103 | self.publisher.connect(connect_string) 104 | 105 | def __str__(self): 106 | return self.name 107 | 108 | def get_publisher(self): 109 | return self.publisher 110 | 111 | async def set_subscriber_topic(self, topic): 112 | """ 113 | This method sets a subscriber topic. 114 | You can subscribe to multiple topics by calling this method for 115 | each topic. 116 | :param topic: A topic string 117 | """ 118 | 119 | if not type(topic) is str: 120 | raise TypeError('Subscriber topic must be string') 121 | # todo: base.py 122 | self.subscriber.setsockopt(zmq.SUBSCRIBE, topic.encode()) 123 | 124 | async def pack(self, data): 125 | return msgpack.packb(data, use_bin_type=True) 126 | 127 | async def unpack(self, data): 128 | return msgpack.unpackb(data, raw=False) 129 | 130 | async def publish_payload(self, payload, topic=''): 131 | """ 132 | This method will publish a payload and its associated topic 133 | :param payload: Protocol message to be published 134 | :param topic: A string value 135 | """ 136 | # self.logger.debug(f"publish_payload begin-> {time.time()}") 137 | # make sure the topic is a string 138 | if not type(topic) is str: 139 | raise TypeError('Publish topic must be string', 'topic') 140 | 141 | if self.bucket.consume(1): 142 | message = await self.pack(payload) 143 | 144 | pub_envelope = topic.encode() 145 | await self.publisher.send_multipart([pub_envelope, message]) 146 | else: 147 | now = time.time() 148 | if (now - self.last_pub_time > 1): 149 | error_text = f"发送消息过于频繁!({self.bucket_token}, {self.bucket_fill_rate})" # 1 /s or ui 150 | self.logger.error(error_text) 151 | await self.pub_notification(error_text, type="ERROR") 152 | self.last_pub_time = time.time() 153 | 154 | # self.logger.debug(f"publish_payload end-> {time.time()}") # fast! 155 | 156 | async def receive_loop(self): 157 | """ 158 | This is the receive loop for adapter messages. 159 | This method may be overwritten to meet the needs 160 | of the application before handling received messages. 161 | """ 162 | if self.subscriber_list: 163 | for topic in self.subscriber_list: 164 | await self.set_subscriber_topic(topic) 165 | # await asyncio.sleep(0.3) 166 | 167 | while self._running: 168 | # NOBLOCK 169 | # todo create_task 170 | try: 171 | if self.recv_mode == "noblock": 172 | data = await self.subscriber.recv_multipart(zmq.NOBLOCK) 173 | else: 174 | data = await self.subscriber.recv_multipart() 175 | # self.logger.debug(f'{data}') 176 | #data = await asyncio.wait_for(data, timeout=0.001) 177 | # data = await self.subscriber.recv_multipart() # await future 178 | try: 179 | # some data is invalid 180 | topic = data[0].decode() 181 | payload = await self.unpack(data[1]) 182 | except Exception as e: 183 | self.logger.error(str(e)) 184 | # todo 185 | continue # 丢弃一帧数据 186 | await self.message_handle(topic, payload) 187 | except zmq.error.Again: 188 | await asyncio.sleep(self.loop_time) 189 | except Exception as e: 190 | # recv_multipart() timeout 191 | self.logger.error(e) 192 | 193 | async def start_the_receive_loop(self): 194 | self.receive_loop_task = self.event_loop.create_task( 195 | self.receive_loop()) 196 | 197 | async def message_handle(self, topic, payload): 198 | """ 199 | Override this method with a custom adapter message processor for subscribed messages. 200 | :param topic: Message Topic string. 201 | :param payload: Message Data. 202 | """ 203 | print('this method should be overwritten in the child class') 204 | 205 | # noinspection PyUnresolvedReferences 206 | 207 | async def clean_up(self): 208 | """ 209 | Clean up before exiting. 210 | """ 211 | self._running = False 212 | # print('clean_up!') 213 | await asyncio.sleep(0.1) 214 | # await self.publisher.close() 215 | # await self.subscriber.close() 216 | # await self.context.term() 217 | 218 | 219 | class AdapterNodeAio(MessageNodeAio): 220 | ''' 221 | CodeLab Adapter AdapterNodeAio 222 | ''' 223 | def __init__(self, 224 | start_cmd_message_id=None, 225 | is_started_now=True, 226 | *args, 227 | **kwargs): 228 | ''' 229 | :param codelab_adapter_ip_address: Adapter IP Address - 230 | default: 127.0.0.1 231 | :param subscriber_port: codelab_adapter subscriber port. 232 | :param publisher_port: codelab_adapter publisher port. 233 | :param loop_time: Receive loop sleep time. 234 | :param connect_time: Allow the node to connect to adapter 235 | ''' 236 | super().__init__(*args, **kwargs) 237 | self.ADAPTER_TOPIC = ADAPTER_TOPIC # message topic: the message from adapter 238 | self.SCRATCH_TOPIC = SCRATCH_TOPIC # message topic: the message from scratch 239 | if not hasattr(self, 'TOPIC'): 240 | self.TOPIC = ADAPTER_TOPIC # message topic: the message from adapter 241 | if not hasattr(self, 'NODE_ID'): 242 | self.NODE_ID = "eim" 243 | if not hasattr(self, 'HELP_URL'): 244 | self.HELP_URL = "http://adapter.codelab.club/extension_guide/introduction/" 245 | if not hasattr(self, 'WEIGHT'): 246 | self.WEIGHT = 0 247 | 248 | if not start_cmd_message_id: 249 | if "--start-cmd-message-id" in sys.argv: 250 | # node from cmd, extension from param 251 | parser = argparse.ArgumentParser() 252 | parser.add_argument("--start-cmd-message-id", dest="message_id", default=None, 253 | help="start cmd message id, a number or uuid(string)") 254 | args = parser.parse_args() 255 | start_cmd_message_id = args.message_id 256 | 257 | self.start_cmd_message_id = start_cmd_message_id 258 | self.logger.debug(f"start_cmd_message_id -> {self.start_cmd_message_id}") 259 | if is_started_now and self.start_cmd_message_id: 260 | time.sleep(0.1) # 等待建立连接 261 | self.event_loop.run_until_complete(self.started()) 262 | # asyncio.create_task(self.started()) 263 | self.linda_wait_futures = [] 264 | 265 | 266 | async def started(self): 267 | ''' 268 | started notify 269 | todo await 270 | ''' 271 | # request++ and uuid, Compatible with them. 272 | await self.pub_notification(f"启动 {self.NODE_ID}") 273 | try: 274 | int_message = int(self.start_cmd_message_id) 275 | await self.send_reply(int_message) 276 | except ValueError: 277 | # task 278 | await self.send_reply(self.start_cmd_message_id) 279 | 280 | 281 | async def send_reply(self, message_id, content="ok"): 282 | response_message = self.message_template() 283 | response_message["payload"]["message_id"] = message_id 284 | response_message["payload"]["content"] = content 285 | await self.publish(response_message) 286 | 287 | def generate_node_id(self, filename): 288 | ''' 289 | extension_eim.py -> extension_eim 290 | ''' 291 | node_name = Path(filename).stem 292 | return self._node_name_to_node_id(node_name) 293 | 294 | def _node_name_to_node_id(self, node_name): 295 | return f'eim/{node_name}' 296 | 297 | # def extension_message_handle(self, f): 298 | async def extension_message_handle(self, topic, payload): 299 | """ 300 | the decorator for adding current_extension handler 301 | 302 | self.add_handler(f, type='current_extension') 303 | return f 304 | """ 305 | self.logger.info("please set the method to your handle method") 306 | 307 | async def exit_message_handle(self, topic, payload): 308 | await self.pub_extension_statu_change(self.NODE_ID, "stop") 309 | if self._running: 310 | stop_cmd_message_id = payload.get("message_id", None) 311 | await self.terminate(stop_cmd_message_id=stop_cmd_message_id) 312 | 313 | def message_template(self): 314 | # _message_template(sender,username,node_id,token) dict 315 | template = _message_template(self.name, self.NODE_ID, self.token) 316 | return template 317 | 318 | async def publish(self, message): 319 | assert isinstance(message, dict) 320 | topic = message.get('topic') 321 | payload = message.get("payload") 322 | if not topic: 323 | topic = self.TOPIC 324 | if not payload.get("node_id"): 325 | payload["node_id"] = self.NODE_ID 326 | # self.logger.debug(f"{self.name} publish: topic: {topic} payload:{payload}") 327 | 328 | await self.publish_payload(payload, topic) 329 | 330 | async def pub_notification(self, 331 | content, 332 | topic=NOTIFICATION_TOPIC, 333 | type="INFO"): 334 | ''' 335 | type 336 | ERROR 337 | INFO 338 | { 339 | topic: 340 | payload: { 341 | content: 342 | } 343 | } 344 | ''' 345 | node_id = self.NODE_ID 346 | payload = self.message_template()["payload"] 347 | payload["type"] = type 348 | payload["content"] = content 349 | await self.publish_payload(payload, topic) 350 | 351 | async def pub_extension_statu_change(self, node_name, statu): 352 | topic = NODE_STATU_CHANGE_TOPIC 353 | node_id = self.NODE_ID 354 | payload = self.message_template()["payload"] 355 | payload["node_name"] = node_name 356 | payload["content"] = statu 357 | await self.publish_payload(payload, topic) 358 | 359 | async def message_handle(self, topic, payload): 360 | """ 361 | Override this method with a custom adapter message processor for subscribed messages. 362 | :param topic: Message Topic string. 363 | :param payload: Message Data. 364 | 365 | all the sub message 366 | process handler 367 | 368 | default sub: [SCRATCH_TOPIC, NODES_OPERATE_TOPIC] 369 | """ 370 | if self.external_message_processor: 371 | # handle all sub messages 372 | # to handle websocket message 373 | await self.external_message_processor(topic, payload) 374 | 375 | if topic == NODES_OPERATE_TOPIC: 376 | ''' 377 | 分布式: 主动停止 使用node_id 378 | extension也是在此关闭,因为extension也是一种node 379 | UI触发关闭命令 380 | ''' 381 | command = payload.get('content') 382 | if command == 'stop': 383 | ''' 384 | to stop node/extension 385 | ''' 386 | # 暂不处理extension 387 | self.logger.debug(f"node stop message: {payload}") 388 | self.logger.debug(f"node self.name: {self.name}") 389 | # payload.get("node_id") == self.NODE_ID to stop extension 390 | # f'eim/{payload.get("node_name")}' == self.NODE_ID to stop node (generate extension id) 391 | if payload.get("node_id") == self.NODE_ID or payload.get( 392 | "node_id") == "all" or self._node_name_to_node_id( 393 | payload.get("node_name")) == self.NODE_ID: 394 | self.logger.info(f"stop {self}") 395 | await self.exit_message_handle(topic, payload) 396 | return 397 | 398 | if topic in [SCRATCH_TOPIC]: 399 | ''' 400 | x 接受来自scratch的消息 401 | v 接受所有订阅主题的消息 402 | 插件业务类 403 | ''' 404 | if payload.get("node_id") == self.NODE_ID: 405 | await self.extension_message_handle(topic, payload) 406 | ''' 407 | handlers = self.get_handlers(type="current_extension") 408 | for handler in handlers: 409 | handler(topic, payload) 410 | ''' 411 | 412 | if topic == LINDA_CLIENT: 413 | # 来自Linda的消息,透明发往 web(使用 socketio 管道) 414 | for (message_id, future) in self.linda_wait_futures: 415 | if message_id == payload.get("message_id"): 416 | future.set_result(payload["tuple"]) 417 | break 418 | 419 | async def terminate(self, stop_cmd_message_id=None): 420 | ''' 421 | stop by thread 422 | await 423 | 424 | # await self.clean_up() # todo 同步中运行异步 425 | print(f"{self} terminate!") 426 | # self.logger.info(f"{self} terminate!") 427 | await self.clean_up() 428 | self.logger.info(f"{self} terminate!") 429 | ''' 430 | 431 | if self._running: 432 | self.logger.info(f"stopped {self.NODE_ID}") 433 | await self.pub_notification(f"停止 {self.NODE_ID}") # 会通知给 UI 434 | if stop_cmd_message_id: 435 | await self.send_reply(stop_cmd_message_id) 436 | await asyncio.sleep(0.1) 437 | # super().terminate() 438 | for (message_id, f) in self.linda_wait_futures: 439 | if not f.done(): 440 | f.set_exception(NodeTerminateError("terminate")) 441 | await self.clean_up() 442 | 443 | ############## 444 | # linda . 和线程future几乎一模一样 445 | async def _send_to_linda_server(self, operate, _tuple): 446 | ''' 447 | send to linda server and wait it (client block / future) 448 | return: 449 | message_id 450 | ''' 451 | assert isinstance(operate, LindaOperate) 452 | # assert isinstance(_tuple, list) 453 | assert isinstance(_tuple, list) 454 | if not self._running: 455 | # loop 456 | Exception(f"_running: {self._running}") 457 | topic = LINDA_SERVER # to 458 | payload = self.message_template()["payload"] 459 | payload["message_id"] = uuid.uuid4().hex 460 | payload["operate"] = operate.value 461 | payload["tuple"] = _tuple 462 | payload["content"] = _tuple # 是否必要 463 | 464 | self.logger.debug( 465 | f"{self.name} publish: topic: {topic} payload:{payload}") 466 | 467 | await self.publish_payload(payload, topic) 468 | return payload["message_id"] 469 | 470 | 471 | async def _send_and_wait_origin(self, operate, _tuple, timeout): 472 | # operate 枚举 473 | message_id = await self._send_to_linda_server(operate, _tuple) 474 | ''' 475 | return future timeout 476 | futurn 被消息循环队列释放 477 | 478 | timeout 479 | https://docs.python.org/3/library/asyncio-task.html#asyncio.wait_for 480 | ''' 481 | f = asyncio.Future() # todo asyncio future 482 | self.linda_wait_futures.append((message_id, f)) 483 | # todo 加入到队列里: (message_id, f) f.set_result(tuple) 484 | try: 485 | return await asyncio.wait_for(f, timeout=timeout) # result() 非阻塞 查询状态 486 | except asyncio.TimeoutError: 487 | # print('timeout!') 488 | raise asyncio.TimeoutError(f'timeout: {timeout}; message_id: {message_id}') 489 | 490 | async def _send_and_wait(self, operate, _tuple, timeout): 491 | try: 492 | return await self._send_and_wait_origin(operate, _tuple, timeout) 493 | except: 494 | if operate in [LindaOperate.IN]: 495 | self.logger.warning(f'cancel: {operate} {_tuple}') 496 | await self._send_to_linda_server(LindaOperate.OUT, _tuple) # 不等回复 497 | time.sleep(0.01) 498 | 499 | async def linda_in(self, _tuple: list, timeout=None): 500 | return await self._send_and_wait(LindaOperate.IN, _tuple, timeout) 501 | 502 | async def linda_inp(self, _tuple: list): 503 | return await self._send_and_wait(LindaOperate.INP, _tuple, None) 504 | 505 | async def linda_rd(self, _tuple: list, timeout=None): 506 | return await self._send_and_wait(LindaOperate.RD, _tuple, timeout) 507 | 508 | async def linda_rdp(self, _tuple: list): 509 | return await self._send_and_wait(LindaOperate.RDP, _tuple, None) 510 | 511 | async def linda_out(self, _tuple, wait=True): 512 | if wait: 513 | return await self._send_and_wait(LindaOperate.OUT, _tuple, None) 514 | else: 515 | return await self._send_to_linda_server(LindaOperate.OUT, _tuple) # message id 回执 516 | # await self._send_to_linda_server(LindaOperate.OUT, _tuple) 517 | 518 | # helper 519 | async def linda_dump(self): 520 | timeout=None 521 | return await self._send_and_wait(LindaOperate.DUMP, ["dump"], timeout) 522 | 523 | # helper 524 | async def linda_status(self): 525 | timeout=None 526 | return await self._send_and_wait(LindaOperate.STATUS, ["status"], timeout) 527 | 528 | async def linda_reboot(self): 529 | timeout=None 530 | return await self._send_and_wait(LindaOperate.REBOOT, ["reboot"], timeout) 531 | 532 | async def is_connected(self, timeout=0.1): 533 | # ping set timeout 534 | _tuple = ["%%ping", "ping"] 535 | try: 536 | res = await self._send_and_wait(LindaOperate.OUT, _tuple, timeout=timeout) 537 | return True 538 | except asyncio.TimeoutError: 539 | return False 540 | 541 | -------------------------------------------------------------------------------- /codelab_adapter_client/cloud_message.py: -------------------------------------------------------------------------------- 1 | import time 2 | import queue 3 | 4 | from loguru import logger 5 | from codelab_adapter_client.mqtt_node import MQTT_Node 6 | 7 | 8 | class HelloCloudNode(MQTT_Node): 9 | ''' 10 | 在 cloud jupyterlab(jupyterhub) 里与Scratch通信 11 | 以交互计算课程为测试场景 12 | ''' 13 | 14 | def __init__(self, **kwargs): 15 | super().__init__(sub_topics=["eim/from_scratch"], **kwargs) 16 | self.message_queue = queue.Queue() 17 | self._latest_send_time = 0 18 | 19 | def on_message(self, client, userdata, msg): 20 | logger.debug(f"topic->{msg.topic} ;payload-> {msg.payload}") 21 | self.message_queue.put(msg.payload.decode()) # 来自 Scratch 22 | # import IPython;IPython.embed() 23 | # todo reply mqtt 24 | # logger.debug((client, userdata, msg)) 25 | 26 | def _send_message(self, content): 27 | ''' 28 | 限制速率 29 | ''' 30 | self.publish("eim/from_python", str(content)) 31 | send_time = time.time() 32 | if send_time - self._latest_send_time < 0.15: 33 | time.sleep(0.15) 34 | self._latest_send_time = send_time 35 | 36 | def _receive_message(self, block=False): 37 | if block: 38 | return self.message_queue.get() 39 | else: 40 | try: 41 | return str(self.message_queue.get_nowait()) 42 | except queue.Empty: 43 | time.sleep(0.01) 44 | return None 45 | 46 | 47 | node = HelloCloudNode() 48 | 49 | send_message = node._send_message 50 | receive_message = node._receive_message -------------------------------------------------------------------------------- /codelab_adapter_client/config.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import sys 3 | import platform 4 | import pkg_resources 5 | 6 | from dynaconf import Dynaconf 7 | 8 | 9 | def is_win(): 10 | if platform.system() == "Windows": 11 | return True 12 | 13 | 14 | def is_mac(): 15 | if platform.system() == "Darwin": 16 | # which python3 17 | # 不如用PATH python 18 | return True 19 | 20 | 21 | def is_linux(): 22 | if platform.system() == "Linux": 23 | return True 24 | 25 | 26 | def look_up_root(): 27 | """ 28 | adapter home root dir 29 | """ 30 | python_path = sys.executable # str 31 | if is_mac(): 32 | # app、 app_packages、 Support(mac)/python(win) 33 | resources_dir = pathlib.Path(python_path).parents[2] 34 | if is_win(): 35 | resources_dir = pathlib.Path(python_path).parents[1] 36 | if is_linux(): 37 | # 作为 root 38 | resources_dir = pathlib.Path.home() 39 | return resources_dir 40 | 41 | def is_in_china(): 42 | from time import gmtime, strftime 43 | # current time zone 44 | c_zone = strftime("%z", gmtime()) # time.strftime('%Z', time.localtime()) # fuck windows 🖕️ 45 | if c_zone == "+0800": 46 | return True 47 | 48 | 49 | ADAPTER_HOME_DIR_NAME = "adapter_home" 50 | ROOT = look_up_root() 51 | CODELAB_ADAPTER_DIR = ROOT / ADAPTER_HOME_DIR_NAME 52 | ADAPTER_HOME = CODELAB_ADAPTER_DIR # 如果不存在, 就不存在 53 | 54 | user_settings_file = ADAPTER_HOME / 'user_settings.toml' 55 | 56 | global_settings_file = ADAPTER_HOME / "global_settings.toml" 57 | 58 | if global_settings_file.is_file(): 59 | settings_files = [str(global_settings_file)] 60 | else: 61 | settings_files = [] 62 | 63 | if user_settings_file.is_file(): 64 | settings_files.append(str(user_settings_file)) 65 | else: 66 | print("未找到 user_settings_file") 67 | path = pathlib.Path(pkg_resources.resource_filename('codelab_adapter_client', f"data/user_settings.toml")) 68 | # 使用内置的 69 | print("使用 codelab_adapter_client 内置的 data/user_settings.toml") 70 | settings_files.append(str(path)) 71 | 72 | # 内置的配置,最弱 73 | 74 | settings = Dynaconf( 75 | envvar_prefix="CODELAB", 76 | # envvar_prefix False 将获取所有环境变量 77 | # envvar_prefix=False, # https://www.dynaconf.com/envvars/#custom-prefix 78 | # 'settings.py', 79 | # 'settings.toml' '.secrets.toml' 80 | settings_files=settings_files, # todo ~/codelab_adapter/user_settings.py 81 | ) # 按顺序加载, .local 82 | 83 | if not settings.get("ZMQ_LOOP_TIME"): 84 | # export CODELAB_ZMQ_LOOP_TIME = 0.01 85 | settings.ZMQ_LOOP_TIME = 0.02 86 | 87 | if not settings.get("ADAPTER_HOME_PATH"): # 环境 88 | settings.ADAPTER_HOME_PATH = str(CODELAB_ADAPTER_DIR) 89 | 90 | sys.path.insert(1, settings.ADAPTER_HOME_PATH) 91 | 92 | # CN_PIP MIRRORS 93 | if not settings.get("USE_CN_PIP_MIRRORS"): 94 | settings.USE_CN_PIP_MIRRORS = False # may be overwriten by user settings 95 | if is_in_china(): 96 | settings.USE_CN_PIP_MIRRORS = True 97 | 98 | if not settings.get("CN_PIP_MIRRORS_HOST"): 99 | settings.CN_PIP_MIRRORS_HOST = "https://pypi.tuna.tsinghua.edu.cn/simple" 100 | 101 | if not settings.get("PYTHON3_PATH"): 102 | settings.PYTHON3_PATH = None 103 | 104 | # `envvar_prefix` = export envvars with `export DYNACONF_FOO=bar`. 105 | # `settings_files` = Load this files in the order. 106 | if not settings.get("NODE_LOG_PATH"): 107 | settings.NODE_LOG_PATH = ADAPTER_HOME / "node_log" 108 | 109 | 110 | # 获取TOKEN 111 | def _get_adapter_home_token(codelab_adapter_dir): 112 | ''' 113 | 不同进程可以共享TOKEN,jupyter对port的操作类似 114 | ''' 115 | token_file = pathlib.Path(codelab_adapter_dir) / '.token' 116 | if token_file.exists(): 117 | with open(token_file) as f: 118 | TOKEN = f.read() 119 | return TOKEN 120 | 121 | if not settings.get("TOKEN"): 122 | adapter_home_token = _get_adapter_home_token(settings.ADAPTER_HOME_PATH) 123 | if adapter_home_token: 124 | settings.TOKEN = adapter_home_token 125 | -------------------------------------------------------------------------------- /codelab_adapter_client/data/user_settings.toml: -------------------------------------------------------------------------------- 1 | # doc: https://adapter.codelab.club/user_guide/settings/ 2 | dynaconf_merge = true 3 | ADAPTER_MODE = 1 # user client 4 | # TOKEN = "df3fjw09w2rf" 5 | PRO_KEY = "" 6 | DEBUG = false 7 | # websocket/HTTP 8 | SOCKET_SERVER_PORT = 12358 9 | USE_SSL = true 10 | IS_WEBSOCKET_SAFE = false # open for wss client 11 | OPEN_EIM_API = true 12 | OPEN_REST_API = false 13 | OPEN_WEBSOCKET_API = true 14 | AUTO_OPEN_WEBUI = true 15 | DEFAULT_ADAPTER_HOST = "codelab-adapter.codelab.club" 16 | WEB_UI_ENDPOINT = "" # default: https://codelab-adapter.codelab.club:12358 17 | KEEP_LAST_CLIENT = false 18 | ALWAYS_KEEP_ADAPTER_RUNNING = true 19 | # OSC 20 | OPEN_OSC_SERVER = true 21 | OSC_PORT = 12361 22 | # zeromq message 23 | ZMQ_LOOP_TIME = 0.02 24 | OPEN_MESSAGE_HUB = false # 0.0.0.0 or 127.0.0.1 25 | PYTHON3_PATH = "" 26 | RC_EXTENSIONS = ["extension_webUI_manager.py"] # like linux rc.local, Run-ControlFiles 27 | RC_NODES = [] 28 | # USE_CN_PIP_MIRRORS = false 29 | # Linda 30 | TUPLE_SPACE_MAX_LENGTH = 1000 31 | OPEN_LINDA_REST_API = true 32 | # Adapter new Version 33 | LATEST_VERSION = "https://adapter.codelab.club/about/latest_version.json" -------------------------------------------------------------------------------- /codelab_adapter_client/hass.py: -------------------------------------------------------------------------------- 1 | # HA 2 | from .base import AdapterNode 3 | from codelab_adapter_client.topic import * 4 | import time 5 | from loguru import logger 6 | import random 7 | 8 | GET_STATES = "get_states" 9 | 10 | 11 | class HANode(AdapterNode): 12 | ''' 13 | 使用继承的目的是让新手获得专家的能力 -- alan kay 14 | real playing 15 | 16 | todo 获取设备状态 17 | ''' 18 | 19 | def __init__(self, *args, mode="rpi", **kwargs): 20 | ''' 21 | mode rpi/local 22 | ''' 23 | if mode: 24 | kwargs["codelab_adapter_ip_address"] = "rpi.codelab.club" 25 | kwargs["logger"] = logger 26 | super().__init__(*args, **kwargs) 27 | self.TOPIC = TO_HA_TOPIC # after super init 28 | self.set_subscriber_topic(FROM_HA_TOPIC) 29 | # self._message_id = 1 # HA server处理id 30 | self.target_entity_ids = ["door", "cube"] 31 | self.states = None 32 | self.lights = None 33 | self.switches = None 34 | self.get_states() 35 | 36 | def list_all_light_entity_ids(self): 37 | return self.lights 38 | 39 | def list_all_switch_entity_ids(self): 40 | return self.switches 41 | 42 | def get_states(self): 43 | get_states_message = { 44 | "type": GET_STATES, 45 | } 46 | message = self.message_template() 47 | message['payload']['content'] = get_states_message 48 | self.publish(message) 49 | 50 | def call_service(self, 51 | domain="light", 52 | service="turn_off", 53 | entity_id="light.yeelight1"): 54 | content = { 55 | "type": "call_service", 56 | "domain": domain, 57 | "service": service, 58 | "service_data": { 59 | "entity_id": entity_id 60 | } 61 | } 62 | message = self.message_template() 63 | message['payload']['content'] = content 64 | self.publish(message) 65 | 66 | def message_handle(self, topic, payload): 67 | ''' 68 | ''' 69 | if topic == FROM_HA_TOPIC: 70 | message_id = payload["content"]["id"] 71 | mytype = payload["content"].get("mytype") 72 | if mytype == GET_STATES: 73 | # 建议 使用IPython交互式探索 74 | self.states = payload["content"] 75 | result = payload["content"]["result"] 76 | for i in result: 77 | if i["entity_id"] == "group.all_lights": 78 | self.lights = i["attributes"]["entity_id"] 79 | if i["entity_id"] == "group.all_switches": 80 | self.switches = i["attributes"]["entity_id"] 81 | 82 | # HA部分只订阅了状态变化事件, get_states单独处理 83 | content = payload.get("content") 84 | event_type = content["type"] 85 | 86 | if event_type == "event": 87 | ''' 88 | with open('/tmp/neverland.json', 'w+') as logfile: 89 | print(payload, file=logfile) 90 | ''' 91 | data = content["event"]["data"] 92 | entity_id = data["entity_id"] 93 | if any([ 94 | target_entity_id in entity_id 95 | for target_entity_id in self.target_entity_ids 96 | ]): 97 | 98 | new_state = data["new_state"]["state"] 99 | old_state = data["old_state"]["state"] 100 | self.logger.debug( 101 | f'old_state:{old_state}, new_state:{new_state}' 102 | ) # 观察,数据 103 | 104 | if "door" in entity_id: 105 | ''' 106 | 开门 107 | old_state:off, new_state:on 108 | 关门 109 | old_state:on, new_state:off 110 | 111 | 模仿gpiozero 112 | ''' 113 | method_name = None 114 | entity = "door" 115 | action = None 116 | if (old_state, new_state) == ("off", "on"): 117 | # print("open door") 118 | action = "open" 119 | if (old_state, new_state) == ("on", "off"): 120 | action = "close" 121 | # print("close door") 122 | method_name = f"when_{action}_{entity}" # when open door 123 | if action: 124 | self.user_event_method(method_name, entity, action, 125 | entity_id) 126 | 127 | if "cube" in entity_id: 128 | ''' 129 | old_state:, new_state:rotate_left 130 | ''' 131 | method_name = None 132 | entity = "cube" 133 | action = None 134 | if (not old_state) and new_state: 135 | action = new_state 136 | method_name = f"when_{action}_{entity}" 137 | if action: 138 | self.user_event_method(method_name, entity, 139 | action, entity_id) 140 | 141 | def user_event_method(self, method_name, entity, action, entity_id): 142 | if hasattr(self, method_name): 143 | getattr(self, method_name)() 144 | 145 | if hasattr(self, "neverland_event"): 146 | getattr(self, "neverland_event")(entity, action, 147 | entity_id) # entity action 148 | -------------------------------------------------------------------------------- /codelab_adapter_client/message.py: -------------------------------------------------------------------------------- 1 | import time 2 | import queue 3 | 4 | from loguru import logger 5 | from codelab_adapter_client import AdapterNode 6 | 7 | 8 | class HelloNode(AdapterNode): 9 | ''' 10 | 为入门者准备(CodeLab 交互计算 课程) 11 | fork from https://github.com/CodeLabClub/codelab_adapter_extensions/blob/master/nodes_v3/node_eim_monitor.py 12 | ''' 13 | 14 | NODE_ID = "eim" 15 | HELP_URL = "http://adapter.codelab.club/extension_guide/HelloNode/" 16 | WEIGHT = 97 17 | DESCRIPTION = "hello node" 18 | 19 | def __init__(self, **kwargs): 20 | super().__init__(**kwargs) 21 | self.message_queue = queue.Queue() 22 | 23 | def extension_message_handle(self, topic, payload): 24 | content = payload["content"] 25 | self.message_queue.put(content) 26 | # response = sys.modules["eim_monitor"].monitor(content, self.logger) 27 | if payload.get("message_type") == "nowait": 28 | return 29 | else: 30 | payload["content"] = "ok" 31 | message = {"payload": payload} 32 | self.publish(message) 33 | 34 | def _send_message(self, content): 35 | message = self.message_template() 36 | message["payload"]["content"] = str(content) 37 | self.publish(message) 38 | time.sleep(0.05) # 避免过快发送消息 20/s 39 | 40 | def _receive_message(self): 41 | try: 42 | return str(self.message_queue.get_nowait()) 43 | except queue.Empty: 44 | time.sleep(0.01) 45 | return None 46 | 47 | 48 | node = HelloNode() 49 | node.receive_loop_as_thread() 50 | time.sleep(0.1) # wait for connecting 51 | 52 | send_message = node._send_message 53 | receive_message = node._receive_message -------------------------------------------------------------------------------- /codelab_adapter_client/microbit.py: -------------------------------------------------------------------------------- 1 | # work with https://adapter.codelab.club/extension_guide/microbit/ 2 | import time 3 | from loguru import logger 4 | from codelab_adapter_client import AdapterNode 5 | from codelab_adapter_client.topic import ADAPTER_TOPIC, SCRATCH_TOPIC 6 | 7 | 8 | class MicrobitNode(AdapterNode): 9 | ''' 10 | send/recv microbit extension message 11 | duck like scratch 12 | ''' 13 | 14 | def __init__(self): 15 | super().__init__( 16 | logger=logger, 17 | external_message_processor=self.external_message_processor) 18 | self.TOPIC = SCRATCH_TOPIC 19 | self.NODE_ID = "eim/usbMicrobit" 20 | self.set_subscriber_topic(ADAPTER_TOPIC) 21 | 22 | def microbit_event(self, data): 23 | pass 24 | 25 | def external_message_processor(self, topic, payload): 26 | # self.logger.info(f'the message payload from extention: {payload}') 27 | if topic == ADAPTER_TOPIC: 28 | node_id = payload["node_id"] 29 | if node_id == self.NODE_ID: 30 | content = payload["content"] 31 | self.microbit_event(content) 32 | 33 | def send_command(self, 34 | content="display.show('hi', wait=True, loop=False)"): 35 | heart = "Image(\"07070:70707:70007:07070:00700\"" # show heart 36 | message = self.message_template() 37 | message['payload']['content'] = content 38 | self.publish(message) 39 | 40 | def run(self): 41 | while self._running: 42 | time.sleep(1) 43 | 44 | 45 | if __name__ == "__main__": 46 | try: 47 | node = MicrobitNode() 48 | node.receive_loop_as_thread() 49 | node.run() 50 | except KeyboardInterrupt: 51 | node.terminate() # Clean up before exiting. -------------------------------------------------------------------------------- /codelab_adapter_client/mqtt_node.py: -------------------------------------------------------------------------------- 1 | # https://github.com/eclipse/paho.mqtt.python 2 | import paho.mqtt.client as mqtt 3 | from loguru import logger 4 | import time 5 | 6 | 7 | class MQTT_Node: 8 | def __init__(self, **kwargs): 9 | # super().__init__(**kwargs) 10 | # mqtt settings 11 | self.address = kwargs.get("address", "mqtt.longan.link") 12 | self.port = kwargs.get("port", 1883) 13 | self.username = kwargs.get("port", "guest") 14 | self.password = kwargs.get("port", "test") 15 | 16 | self.sub_topics = kwargs.get("sub_topics", ["/scratch3"]) 17 | # mqtt client 18 | self.client = mqtt.Client() 19 | self.client.on_connect = kwargs.get("on_connect", self._on_connect) 20 | self.client.on_message = kwargs.get("on_message", self.on_message) 21 | self.client.on_disconnect = kwargs.get("on_message", self._on_disconnect) 22 | self.client.username_pw_set(self.username, self.password) 23 | self.client.connect(self.address, self.port, 60) 24 | self.client.loop_start() # as thread 25 | 26 | def _on_connect(self, client, userdata, flags, rc): 27 | logger.info( 28 | "MQTT Gateway Connected to MQTT {}:{} with result code {}.".format( 29 | self.address, self.port, str(rc))) 30 | # when mqtt is connected to subscribe to mqtt topics 31 | if self.sub_topics: 32 | for topic in self.sub_topics: 33 | client.subscribe(topic) 34 | 35 | def _on_disconnect(self, client, userdata, flags, rc): 36 | logger.warning('disconnect') 37 | 38 | def on_message(self, client, userdata, msg): 39 | ''' 40 | msg.topic string 41 | msg.payload bytes 42 | ''' 43 | logger.debug(f"topic->{msg.topic} ;payload-> {msg.payload}") 44 | # import IPython;IPython.embed() 45 | # todo reply mqtt 46 | # logger.debug((client, userdata, msg)) 47 | 48 | # client.publish(topic, payload=None, qos=0, retain=False, properties=None) 49 | def publish(self, topic, payload, **kwargs): 50 | ''' 51 | 原始结构 raw_payload bytes 52 | ''' 53 | self.client.publish(topic, payload=payload, **kwargs) 54 | 55 | 56 | class EIM_MQTT_Node: 57 | ''' 58 | EIM over MQTT 59 | ''' 60 | pass 61 | 62 | 63 | if __name__ == "__main__": 64 | node = MQTT_Node() 65 | while True: 66 | time.sleep(1) -------------------------------------------------------------------------------- /codelab_adapter_client/session.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Session object for building, serializing, sending, and receiving messages. 3 | The Session object supports serialization, HMAC signatures, 4 | and metadata on messages. 5 | 6 | Also defined here are utilities for working with Sessions: 7 | * A Message object for convenience that allows attribute-access to the msg dict. 8 | 9 | ref: https://github.com/jupyter/jupyter_client/blob/master/jupyter_client/session.py 10 | ''' 11 | import pprint 12 | import os 13 | from datetime import datetime 14 | from datetime import timezone 15 | utc = timezone.utc 16 | 17 | from ._version import protocol_version 18 | 19 | 20 | class Message: 21 | """A simple message object that maps dict keys to attributes. 22 | A Message can be created from a dict and a dict from a Message instance 23 | simply by calling dict(msg_obj).""" 24 | 25 | def __init__(self, msg_dict): 26 | dct = self.__dict__ 27 | for k, v in (dict(msg_dict)).items(): 28 | if isinstance(v, dict): 29 | v = Message(v) 30 | dct[k] = v 31 | 32 | # Having this iterator lets dict(msg_obj) work out of the box. 33 | def __iter__(self): 34 | return iter((self.__dict__).items()) 35 | 36 | def __repr__(self): 37 | return repr(self.__dict__) 38 | 39 | def __str__(self): 40 | return pprint.pformat(self.__dict__) 41 | 42 | def __contains__(self, k): 43 | return k in self.__dict__ 44 | 45 | def __getitem__(self, k): 46 | return self.__dict__[k] 47 | 48 | 49 | def utcnow(): 50 | """Return timezone-aware UTC timestamp""" 51 | return datetime.utcnow().replace(tzinfo=utc) 52 | 53 | 54 | def msg_header(msg_id, msg_type, username, session): 55 | """Create a new message header 56 | 57 | 'header' : { 58 | 'msg_id' : str, # typically UUID, must be unique per message 59 | 'session' : str, # typically UUID, should be unique per session 60 | 'username' : str, # Username for the Session. Default is your system username. 61 | # ISO 8601 timestamp for when the message is created 62 | 'date': str, 63 | # All recognized message type strings are listed below. 64 | 'msg_type' : str, # 枚举 65 | # the message protocol version 66 | 'version' : '3.0', 67 | }, 68 | """ 69 | date = utcnow() 70 | version = protocol_version 71 | return locals() 72 | 73 | 74 | def extract_header(msg_or_header): 75 | """Given a message or header, return the header.""" 76 | if not msg_or_header: 77 | return {} 78 | try: 79 | # See if msg_or_header is the entire message. 80 | h = msg_or_header['header'] 81 | except KeyError: 82 | try: 83 | # See if msg_or_header is just the header 84 | h = msg_or_header['msg_id'] 85 | except KeyError: 86 | raise 87 | else: 88 | h = msg_or_header 89 | if not isinstance(h, dict): 90 | h = dict(h) 91 | return h 92 | 93 | def msg(self, msg_type, content=None, parent=None, header=None, metadata=None): 94 | """Return the nested message dict. 95 | This format is different from what is sent over the wire. The 96 | serialize/deserialize methods converts this nested message dict to the wire 97 | format, which is a list of message parts. 98 | 99 | 'header':{} 100 | 'msg_id' : str, #uuid 101 | 'msg_type' : str, # _reply消息必须具有parent_header 102 | 'parent_header' : dict, 103 | 'content' : dict, 104 | 'metadata' : {}, # 不常使用 105 | """ 106 | msg = {} 107 | header = self.msg_header(msg_type) if header is None else header 108 | msg['header'] = header 109 | msg['msg_id'] = header['msg_id'] 110 | msg['msg_type'] = header['msg_type'] 111 | msg['parent_header'] = {} if parent is None else extract_header(parent) 112 | msg['content'] = {} if content is None else content 113 | msg['metadata'] = metadata 114 | # buffer 115 | return msg 116 | 117 | def sign(self, msg_list): 118 | """ 119 | https://github.com/jupyter/jupyter_client/blob/master/jupyter_client/session.py#L592 120 | 121 | Sign a message with HMAC digest. If no auth, return b''. 122 | Parameters 123 | ---------- 124 | msg_list : list 125 | The [p_header,p_parent,p_content] part of the message list. 126 | """ 127 | pass 128 | 129 | 130 | username = os.environ.get('USER', 'username') , # 3.0, Username for the Session. Default is your system username. 131 | 132 | 133 | def _message_template(sender,node_id,token): 134 | ''' 135 | topic: self.TOPIC: channel 136 | payload: 137 | node_id? //类似jupyter kernel 138 | content 139 | sender 类似 session_id 进程名字 140 | timestamp? date 141 | 142 | msg['msg_id'] = header['msg_id'] 143 | msg['msg_type'] = header['msg_type'] 144 | msg['parent_header'] // 触发的消息 145 | 'username' : str, # Username for the Session. Default is your system username. 146 | ''' 147 | template = { 148 | "payload": { 149 | "parent_header": {}, # 3.0, reply 150 | "version": protocol_version, # 3.0 151 | "content": "content", # string or dict 152 | "sender": sender, # like session (uuid), adapter/nodes/ 153 | "username": username , # 3.0, Username for the Session. Default is your system username. 154 | "node_id": node_id, # like jupyter kernel id/name 155 | "message_id": "", # session + count # uuid 156 | "message_type": "", # 3.0 deside content reply/req/pub 157 | "token": token 158 | } 159 | } 160 | return template -------------------------------------------------------------------------------- /codelab_adapter_client/settings.toml: -------------------------------------------------------------------------------- 1 | # dynaconf -i config.settings list 2 | # https://www.dynaconf.com/#initialize-dynaconf-on-your-project 3 | # ZMQ_LOOP_TIME = 0.01 -------------------------------------------------------------------------------- /codelab_adapter_client/simple_node.py: -------------------------------------------------------------------------------- 1 | import time 2 | from loguru import logger 3 | from codelab_adapter_client import AdapterNode 4 | 5 | class EimMonitorNode(AdapterNode): 6 | ''' 7 | fork from https://github.com/CodeLabClub/codelab_adapter_extensions/blob/master/nodes_v3/node_eim_monitor.py 8 | ''' 9 | 10 | NODE_ID = "eim" 11 | HELP_URL = "http://adapter.codelab.club/extension_guide/eim_monitor/" 12 | WEIGHT = 97 13 | DESCRIPTION = "响应一条eim消息" 14 | 15 | def __init__(self, monitor_func, **kwargs): 16 | super().__init__(**kwargs) 17 | self.monitor_func = monitor_func 18 | 19 | def extension_message_handle(self, topic, payload): 20 | content = payload["content"] 21 | # response = sys.modules["eim_monitor"].monitor(content, self.logger) 22 | payload["content"] = self.monitor_func(content) 23 | message = {"payload": payload} 24 | self.publish(message) 25 | 26 | def run(self): 27 | while self._running: 28 | time.sleep(0.1) 29 | 30 | 31 | if __name__ == "__main__": 32 | def monitor(content): 33 | return content[::-1] 34 | try: 35 | node = EimMonitorNode(monitor) 36 | node.receive_loop_as_thread() 37 | node.run() 38 | except KeyboardInterrupt: 39 | node.terminate() # Clean up before exiting. -------------------------------------------------------------------------------- /codelab_adapter_client/thing.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Adapter Thing 3 | 与具体设备通信,对外提供服务 4 | 5 | 以 6 | node_alphamini 7 | node_yeelight 8 | extension_usb_microbit 9 | 为原型 10 | 11 | 需要实现一些抽象接口(与外部交互) 12 | list 13 | connect 14 | status 15 | disconnect 16 | 17 | ''' 18 | from abc import abstractmethod, ABCMeta 19 | 20 | class AdapterThing(metaclass=ABCMeta): 21 | ''' 22 | class Robot(AdapterThingAio): 23 | def __init__(self, node_instance): 24 | super().__init__(node_instance) 25 | 26 | async def list(self): 27 | print("list robot") 28 | 29 | def connect(self): 30 | print("list robot") 31 | 32 | def disconnect(self): 33 | print("list robot") 34 | ''' 35 | def __init__(self, thing_name, node_instance): 36 | self.node_instance = node_instance 37 | self.is_connected = False 38 | self.thing_name = thing_name 39 | self.thing = None 40 | 41 | 42 | def _ensure_connect(self): 43 | # 确认某件事才往下,否则返回错误信息, 意外时将触发 44 | if not self.is_connected: 45 | raise Exception("{self.thing_name} not connected!") 46 | 47 | @abstractmethod 48 | def list(self, **kwargs): # 可以是 async 49 | # connect things 50 | '''please Implemente in subclass''' 51 | 52 | @abstractmethod 53 | def connect(self, **kwargs) -> bool: 54 | # connect thing 55 | '''please Implemente in subclass''' 56 | 57 | @abstractmethod 58 | def status(self, **kwargs) -> bool: 59 | # query thing status 60 | '''please Implemente in subclass''' 61 | 62 | 63 | @abstractmethod 64 | def disconnect(self, **kwargs) -> bool: 65 | # disconnect things 66 | '''please Implemente in subclass''' 67 | 68 | -------------------------------------------------------------------------------- /codelab_adapter_client/tools/adapter_helper.py: -------------------------------------------------------------------------------- 1 | # adapter helper 2 | from pprint import pprint 3 | # show adapter home path 4 | from codelab_adapter_client.config import settings 5 | from codelab_adapter_client.utils import open_path_in_system_file_manager 6 | 7 | def adapter_helper(): 8 | pprint(f"adapter home: {settings.ADAPTER_HOME_PATH}") # open it 9 | # open_path_in_system_file_manager(ADAPTER_HOME) 10 | 11 | def list_settings(): 12 | pprint(settings.as_dict()) 13 | # print() 14 | # open_path_in_system_file_manager(ADAPTER_HOME) -------------------------------------------------------------------------------- /codelab_adapter_client/tools/linda.py: -------------------------------------------------------------------------------- 1 | ''' 2 | codelab-linda --monitor 3 | codelab-linda --out [1, "hello"] 4 | codelab-linda --rd [1, "*"] 5 | codelab-linda --in [1, "hello"] 6 | codelab-linda --dump 7 | 8 | todo 9 | json 输出,彩色 10 | ping 11 | is_connected 12 | 往返视角 13 | 14 | click 15 | adapter full 已经内置 click 16 | ''' 17 | import time 18 | import queue 19 | import ast 20 | 21 | import click 22 | from codelab_adapter_client import AdapterNode 23 | from codelab_adapter_client.topic import LINDA_SERVER, LINDA_CLIENT 24 | 25 | 26 | class PythonLiteralOption(click.Option): 27 | def type_cast_value(self, ctx, value): 28 | try: 29 | return ast.literal_eval(value) 30 | except: 31 | raise click.BadParameter(value) 32 | 33 | 34 | class CatchAllExceptions(click.Group): 35 | # https://stackoverflow.com/questions/44344940/python-click-subcommand-unified-error-handling 36 | def __call__(self, *args, **kwargs): 37 | try: 38 | return self.main(standalone_mode=False, *args, **kwargs) 39 | except Exception as e: 40 | click.echo(e) 41 | finally: 42 | if globals().get('mynode') and mynode._running: 43 | mynode.terminate() # ok! 44 | 45 | 46 | class MyNode(AdapterNode): 47 | NODE_ID = "linda/linda_cli" # 是否有问题? 48 | 49 | def __init__(self, codelab_adapter_ip_address, recv_mode="noblock"): # todo 发给 Linda 的也订阅 50 | super().__init__(codelab_adapter_ip_address=codelab_adapter_ip_address, recv_mode=recv_mode) 51 | # self.set_subscriber_topic(LINDA_SERVER) # add topic 52 | self.set_subscriber_topic('') 53 | self.q = queue.Queue() 54 | 55 | 56 | def _linda_message_handle(self, topic, payload): 57 | # click.echo(f'{topic}, {payload}') 58 | self.q.put((topic, payload)) 59 | 60 | ''' 61 | def message_handle(self, topic, payload): 62 | if topic in [LINDA_SERVER, LINDA_CLIENT]: 63 | click.echo(f'{topic}, {payload}') 64 | ''' 65 | 66 | # tudo, help不要初始化,需要放到cli中,ctx传递到CatchAllExceptions 67 | 68 | 69 | 70 | @click.group(cls=CatchAllExceptions) 71 | @click.option('-i', 72 | '--ip', 73 | envvar='IP', 74 | help="IP Address of Adapter", 75 | default="127.0.0.1", 76 | show_default=True, 77 | required=False) 78 | @click.pass_context 79 | def cli(ctx, ip): 80 | ''' 81 | talk with linda from cli 82 | ''' 83 | # ctx.obj = mynode # todo ip ,多参数 84 | global mynode # 给退出时候用 85 | 86 | ctx.ensure_object(dict) 87 | mynode = MyNode(ip) 88 | ''' 89 | if ip in ["127.0.0.1", "localhost"] 90 | mynode = MyNode(ip) 91 | else: 92 | mynode = MyNode(ip, recv_mode="block") 93 | ''' 94 | mynode.receive_loop_as_thread() 95 | 96 | time.sleep(0.05) 97 | 98 | ctx.obj['node'] = mynode 99 | ctx.obj['ip'] = ip 100 | 101 | 102 | @cli.command() 103 | @click.option('-d', '--data', cls=PythonLiteralOption, default=[]) 104 | @click.pass_obj 105 | def out(ctx, data): 106 | ''' 107 | out the tuple to Linda tuple space 108 | ''' 109 | # codelab-linda out --data '[1, "hello"]' 110 | # codelab-linda --ip '192.168.31.111' out --data '[1, "hello"]' # 注意参数位置! 111 | assert isinstance(data, list) 112 | # click.echo(f'ip: {ctx["ip"]}') 113 | res = ctx['node'].linda_out(data) 114 | click.echo(res) 115 | return ctx['node'] 116 | 117 | 118 | @click.command("in") 119 | @click.option('-d', '--data', cls=PythonLiteralOption, default=[]) 120 | @click.pass_obj 121 | def in_(ctx, data): # replace 122 | ''' 123 | match and remove a tuple from Linda tuple space 124 | ''' 125 | # codelab-linda in --data '[1, "*"]' 126 | 127 | assert isinstance(data, list) 128 | res = ctx["node"].linda_in(data) 129 | click.echo(res) 130 | return ctx["node"] 131 | 132 | @click.command() 133 | @click.option('-d', '--data', cls=PythonLiteralOption, default=[]) 134 | @click.pass_obj 135 | def rd(ctx, data): # replace 136 | ''' 137 | rd(read only) a tuple from Linda tuple space 138 | ''' 139 | assert isinstance(data, list) 140 | res = ctx["node"].linda_rd(data) 141 | click.echo(res) 142 | return ctx["node"] 143 | 144 | @click.command() 145 | @click.option('-d', '--data', cls=PythonLiteralOption, default=[]) 146 | @click.pass_obj 147 | def rdp(ctx, data): # replace 148 | ''' 149 | rd(rd but Non-blocking) a tuple from Linda tuple space 150 | ''' 151 | assert isinstance(data, list) 152 | res = ctx["node"].linda_rdp(data) 153 | click.echo(res) 154 | return ctx["node"] 155 | 156 | @click.command() 157 | @click.option('-d', '--data', cls=PythonLiteralOption, default=[]) 158 | @click.pass_obj 159 | def inp(ctx, data): # replace 160 | ''' 161 | in(in but Non-blocking) a tuple from Linda tuple space 162 | ''' 163 | # codelab-linda inp --data '[1, "*"]' 164 | assert isinstance(data, list) 165 | res = ctx["node"].linda_inp(data) 166 | click.echo(res) 167 | return ctx["node"] 168 | 169 | 170 | @click.command() 171 | @click.pass_obj 172 | def monitor(ctx): # replace 173 | ''' 174 | linda message monitor 175 | ''' 176 | while ctx["node"]._running: 177 | if not ctx["node"].q.empty(): 178 | click.echo(ctx["node"].q.get()) 179 | else: 180 | time.sleep(0.1) 181 | 182 | @click.command() 183 | @click.pass_obj 184 | def ping(ctx): # replace 185 | ''' 186 | ping linda server. eg: codelab-linda --ip 192.168.31.100 ping 187 | ''' 188 | t1 = time.time() 189 | res = ctx["node"].is_connected() 190 | if res: 191 | t2 = time.time() 192 | click.echo(f"Online!") 193 | else: 194 | click.echo("Offline!") 195 | 196 | 197 | @click.command() 198 | @click.pass_obj 199 | def dump(ctx): 200 | ''' 201 | dump all tuples from Linda tuple space 202 | ''' 203 | res = ctx['node'].linda_dump() 204 | click.echo(res) 205 | return ctx['node'] 206 | 207 | @click.command() 208 | @click.pass_obj 209 | def status(ctx): 210 | ''' 211 | get Linda tuple space status 212 | ''' 213 | res = ctx['node'].linda_status() 214 | click.echo(res) 215 | return ctx['node'] 216 | 217 | @click.command() 218 | @click.pass_obj 219 | def reboot(ctx): 220 | ''' 221 | reboot(clean) Linda tuple space 222 | ''' 223 | res = ctx['node'].linda_reboot() 224 | click.echo(res) 225 | return ctx['node'] 226 | 227 | 228 | @cli.resultcallback() 229 | def process_result(result, **kwargs): 230 | # click.echo(f'After command: {result} {kwargs}') 231 | # result is node 232 | if result and result._running: 233 | result.terminate() 234 | 235 | 236 | # helper 237 | cli.add_command(dump) 238 | cli.add_command(status) 239 | cli.add_command(reboot) 240 | 241 | # core 242 | cli.add_command(out) 243 | cli.add_command(in_) 244 | cli.add_command(inp) 245 | cli.add_command(rd) 246 | cli.add_command(rdp) 247 | 248 | # monitor 249 | cli.add_command(monitor) 250 | cli.add_command(ping) -------------------------------------------------------------------------------- /codelab_adapter_client/tools/mdns_browser.py: -------------------------------------------------------------------------------- 1 | """ 2 | # https://github.com/jstasiak/python-zeroconf/blob/master/examples/browser.py 3 | 4 | Example of browsing for a service. 5 | 6 | The default is HTTP and HAP; use --find to search for all available services in the network 7 | 8 | HAP(homekit accessory protocol(HAP)) 协议 是 apple 发布的用于 ios 手机作为controller, 去控制 accessory 的用于智能家居的一个通讯协议。有两种方式: ble 和IP. 9 | 10 | HAP-python 11 | """ 12 | 13 | import argparse 14 | import logging 15 | import socket 16 | from time import sleep 17 | from typing import cast 18 | 19 | from zeroconf import IPVersion, ServiceBrowser, ServiceStateChange, Zeroconf, ZeroconfServiceTypes 20 | 21 | 22 | def on_service_state_change( 23 | zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange 24 | ) -> None: 25 | print("Service %s of type %s state changed: %s" % (name, service_type, state_change)) 26 | 27 | if state_change is ServiceStateChange.Added: 28 | info = zeroconf.get_service_info(service_type, name) 29 | print("Info from zeroconf.get_service_info: %r" % (info)) 30 | if info: 31 | addresses = ["%s:%d" % (socket.inet_ntoa(addr), cast(int, info.port)) for addr in info.addresses] 32 | print(" Addresses: %s" % ", ".join(addresses)) 33 | print(" Weight: %d, priority: %d" % (info.weight, info.priority)) 34 | print(" Server: %s" % (info.server,)) 35 | if info.properties: 36 | print(" Properties are:") 37 | for key, value in info.properties.items(): 38 | print(" %s: %s" % (key, value)) 39 | else: 40 | print(" No properties") 41 | else: 42 | print(" No info") 43 | print('\n') 44 | 45 | 46 | # if __name__ == '__main__': 47 | def main(): 48 | logging.basicConfig(level=logging.DEBUG) 49 | 50 | parser = argparse.ArgumentParser() 51 | parser.add_argument('--debug', action='store_true') 52 | parser.add_argument('--find', action='store_true', help='Browse all available services') 53 | version_group = parser.add_mutually_exclusive_group() 54 | version_group.add_argument('--v6', action='store_true') 55 | version_group.add_argument('--v6-only', action='store_true') 56 | args = parser.parse_args() 57 | 58 | if args.debug: 59 | logging.getLogger('zeroconf').setLevel(logging.DEBUG) 60 | if args.v6: 61 | ip_version = IPVersion.All 62 | elif args.v6_only: 63 | ip_version = IPVersion.V6Only 64 | else: 65 | ip_version = IPVersion.V4Only 66 | 67 | zeroconf = Zeroconf(ip_version=ip_version) 68 | 69 | services = ["_http._tcp.local.", "_hap._tcp.local."] 70 | if args.find: 71 | services = list(ZeroconfServiceTypes.find(zc=zeroconf)) 72 | 73 | print("\nBrowsing %d service(s), press Ctrl-C to exit...\n" % len(services)) 74 | browser = ServiceBrowser(zeroconf, services, handlers=[on_service_state_change]) 75 | 76 | try: 77 | while True: 78 | sleep(0.1) 79 | except KeyboardInterrupt: 80 | pass 81 | finally: 82 | zeroconf.close() -------------------------------------------------------------------------------- /codelab_adapter_client/tools/mdns_registration.py: -------------------------------------------------------------------------------- 1 | """ Example of announcing a service (in this case, a fake HTTP server) """ 2 | 3 | import argparse 4 | import logging 5 | import socket 6 | from time import sleep 7 | import uuid 8 | 9 | from zeroconf import IPVersion, ServiceInfo, Zeroconf 10 | from codelab_adapter_client.utils import get_local_ip 11 | 12 | # if __name__ == '__main__': 13 | def main(): 14 | logging.basicConfig(level=logging.DEBUG) 15 | 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument('--debug', action='store_true') 18 | parser.add_argument("--name", dest="name", default=None, 19 | help="mdns service name") 20 | version_group = parser.add_mutually_exclusive_group() 21 | version_group.add_argument('--v6', action='store_true') 22 | version_group.add_argument('--v6-only', action='store_true') 23 | args = parser.parse_args() 24 | 25 | if args.debug: 26 | logging.getLogger('zeroconf').setLevel(logging.DEBUG) 27 | if args.v6: 28 | ip_version = IPVersion.All 29 | elif args.v6_only: 30 | ip_version = IPVersion.V6Only 31 | else: 32 | ip_version = IPVersion.V4Only 33 | 34 | properties = {'who': 'codelab'} # 详细信息 35 | 36 | name = args.name if args.name else uuid.uuid4().hex[:8] 37 | 38 | service_type = "_http._tcp.local." 39 | service_name = f"{name}._http._tcp.local." 40 | server = f"{name}.local." 41 | port = 12358 42 | info = ServiceInfo( 43 | service_type, 44 | service_name, 45 | addresses=[socket.inet_aton(get_local_ip())], 46 | port=port, 47 | properties=properties, 48 | server=server, 49 | ) 50 | print(info) 51 | zeroconf = Zeroconf(ip_version=ip_version) 52 | print("Registration of a service, press Ctrl-C to exit...") 53 | zeroconf.register_service(info) 54 | try: 55 | while True: 56 | sleep(0.1) 57 | except KeyboardInterrupt: 58 | pass 59 | finally: 60 | print("Unregistering...") 61 | zeroconf.unregister_service(info) 62 | zeroconf.close() -------------------------------------------------------------------------------- /codelab_adapter_client/tools/message.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "core/exts/operate", 3 | "payload": { "content": "start", "node_name": "extension_eim" } 4 | } -------------------------------------------------------------------------------- /codelab_adapter_client/tools/monitor.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import signal 3 | import sys 4 | import zmq 5 | 6 | from codelab_adapter_client import AdapterNode 7 | from codelab_adapter_client.topic import ADAPTER_TOPIC, SCRATCH_TOPIC, NOTIFICATION_TOPIC, EXTS_OPERATE_TOPIC 8 | 9 | class Monitor(AdapterNode): 10 | """ 11 | This class subscribes to all messages on the hub and prints out both topic and payload. 12 | """ 13 | 14 | def __init__(self, codelab_adapter_ip_address=None, 15 | subscriber_port='16103', publisher_port='16130', name=None): 16 | super().__init__( 17 | name=name, 18 | codelab_adapter_ip_address=codelab_adapter_ip_address, 19 | subscriber_port=subscriber_port, 20 | publisher_port=publisher_port, 21 | start_cmd_message_id = -1 22 | ) 23 | 24 | self.set_subscriber_topic('') 25 | try: 26 | self.receive_loop() 27 | except zmq.error.ZMQError: 28 | sys.exit() 29 | except KeyboardInterrupt: 30 | sys.exit(0) 31 | 32 | def message_handle(self, topic, payload): 33 | print(topic, payload) 34 | 35 | def run(self): 36 | pass 37 | 38 | def monitor(): 39 | parser = argparse.ArgumentParser() 40 | parser.add_argument("-i", dest="codelab_adapter_ip_address", default="None", 41 | help="None or IP address used by CodeLab Adapter") 42 | parser.add_argument("-n", dest="name", default="Monitor", help="Set name in banner") 43 | parser.add_argument("-p", dest="publisher_port", default='16130', 44 | help="Publisher IP port") 45 | parser.add_argument("-s", dest="subscriber_port", default='16103', 46 | help="Subscriber IP port") 47 | 48 | args = parser.parse_args() 49 | kw_options = {} 50 | 51 | if args.codelab_adapter_ip_address != 'None': 52 | kw_options['codelab_adapter_ip_address'] = args.codelab_adapter_ip_address 53 | 54 | kw_options['name'] = args.name 55 | 56 | kw_options['publisher_port'] = args.publisher_port 57 | kw_options['subscriber_port'] = args.subscriber_port 58 | 59 | my_monitor = Monitor(**kw_options) 60 | 61 | # my_monitor.start() 62 | 63 | # signal handler function called when Control-C occurs 64 | # noinspection PyShadowingNames,PyUnusedLocal,PyUnusedLocal 65 | def signal_handler(signal, frame): 66 | print('Control-C detected. See you soon.') 67 | 68 | my_monitor.clean_up() 69 | sys.exit(0) 70 | 71 | # listen for SIGINT 72 | signal.signal(signal.SIGINT, signal_handler) 73 | signal.signal(signal.SIGTERM, signal_handler) 74 | 75 | 76 | if __name__ == '__main__': 77 | monitor() 78 | -------------------------------------------------------------------------------- /codelab_adapter_client/tools/pub.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import signal 3 | import sys 4 | import zmq 5 | import json 6 | 7 | from codelab_adapter_client.topic import ADAPTER_TOPIC, SCRATCH_TOPIC, NOTIFICATION_TOPIC, EXTS_OPERATE_TOPIC 8 | from codelab_adapter_client.utils import threaded 9 | from codelab_adapter_client import AdapterNode 10 | 11 | # todo 交互式输入工具 12 | 13 | 14 | class Pub(AdapterNode): 15 | """ 16 | This class pub messages on the hub. 17 | 18 | help: 19 | codelab-message-pub -h 20 | usage: 21 | codelab-message-pub -t hello_topic 22 | codelab-message-pub -d eim/node_test 23 | codelab-message-pub -c hello_content 24 | codelab-message-pub -j '{"payload":{"content":"test contenst", "node_id": "eim"}}' 25 | """ 26 | 27 | def __init__(self, 28 | codelab_adapter_ip_address=None, 29 | subscriber_port='16103', 30 | publisher_port='16130', 31 | name=None, 32 | ): 33 | super().__init__( 34 | name=name, 35 | codelab_adapter_ip_address=codelab_adapter_ip_address, 36 | subscriber_port=subscriber_port, 37 | publisher_port=publisher_port, 38 | start_cmd_message_id = -1 # 为了防止命令行参数被提前解析,丑陋的补丁 39 | ) 40 | 41 | self.set_subscriber_topic('') 42 | 43 | 44 | def pub(): 45 | parser = argparse.ArgumentParser() 46 | parser.add_argument( 47 | "-i", 48 | dest="codelab_adapter_ip_address", 49 | default="None", 50 | help="None or IP address used by CodeLab Adapter") 51 | parser.add_argument( 52 | "-n", dest="name", default="Pub", help="Set name in banner") 53 | parser.add_argument( 54 | "-p", dest="publisher_port", default='16130', help="Publisher IP port") 55 | parser.add_argument( 56 | "-s", 57 | dest="subscriber_port", 58 | default='16103', 59 | help="Subscriber IP port") 60 | parser.add_argument( 61 | "-t", dest="topic", default=ADAPTER_TOPIC, help="message topic") 62 | parser.add_argument( 63 | "-d", dest="node_id", default='eim', help="node id") 64 | parser.add_argument( 65 | "-c", dest="content", default='hi', help="payload['content']") 66 | parser.add_argument( 67 | "-j", 68 | dest="json_message", 69 | default='', 70 | help="json message(with topic and payload)") 71 | 72 | args = parser.parse_args() 73 | kw_options = {} 74 | 75 | if args.codelab_adapter_ip_address != 'None': 76 | kw_options[ 77 | 'codelab_adapter_ip_address'] = args.codelab_adapter_ip_address 78 | 79 | kw_options['name'] = args.name 80 | kw_options['publisher_port'] = args.publisher_port 81 | kw_options['subscriber_port'] = args.subscriber_port 82 | 83 | my_pub = Pub(**kw_options) 84 | 85 | if args.json_message: 86 | message = json.loads(args.json_message) 87 | topic = message["topic"] 88 | payload = message["payload"] 89 | else: 90 | payload = my_pub.message_template()["payload"] 91 | payload["content"] = args.content 92 | 93 | topic = args.topic 94 | if payload.get("node_id", None): 95 | payload["node_id"] = args.node_id 96 | 97 | my_pub.publish_payload(payload, topic) 98 | 99 | # signal handler function called when Control-C occurs 100 | # noinspection PyShadowingNames,PyUnusedLocal,PyUnusedLocal 101 | ''' 102 | def signal_handler(signal, frame): 103 | print('Control-C detected. See you soon.') 104 | 105 | my_pub.clean_up() 106 | sys.exit(0) 107 | 108 | # listen for SIGINT 109 | signal.signal(signal.SIGINT, signal_handler) 110 | signal.signal(signal.SIGTERM, signal_handler) 111 | ''' 112 | 113 | 114 | if __name__ == '__main__': 115 | pub() 116 | -------------------------------------------------------------------------------- /codelab_adapter_client/tools/trigger.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import signal 3 | import sys 4 | import zmq 5 | import json 6 | 7 | from codelab_adapter_client.topic import ADAPTER_TOPIC, SCRATCH_TOPIC, NOTIFICATION_TOPIC, EXTS_OPERATE_TOPIC 8 | from codelab_adapter_client.utils import threaded 9 | from codelab_adapter_client import AdapterNode 10 | 11 | 12 | # todo 交互式输入工具 13 | 14 | class Trigger(AdapterNode): 15 | """ 16 | This class subscribes to all messages on the hub and prints out both topic and payload. 17 | """ 18 | 19 | def __init__(self, codelab_adapter_ip_address=None, 20 | subscriber_port='16103', publisher_port='16130', name=None): 21 | super().__init__( 22 | name=name, 23 | codelab_adapter_ip_address=codelab_adapter_ip_address, 24 | subscriber_port=subscriber_port, 25 | publisher_port=publisher_port, 26 | start_cmd_message_id = -1 27 | ) 28 | 29 | self.set_subscriber_topic('') 30 | self.run() 31 | try: 32 | self.receive_loop() 33 | except zmq.error.ZMQError: 34 | sys.exit() 35 | except KeyboardInterrupt: 36 | sys.exit() 37 | 38 | def message_handle(self, topic, payload): 39 | pass 40 | # print(topic, payload) 41 | 42 | @threaded 43 | def run(self): 44 | while self._running: 45 | # print(">>>self.publish({'topic':EXTS_OPERATE_TOPIC,'payload':{'content':'start', 'node_id':'extension_eim2'}})") 46 | code = input(">>>read json from /tmp/message.json (enter to run)") 47 | with open("/tmp/message.json") as f: 48 | message = json.loads(f.read()) 49 | self.publish(message) 50 | 51 | def trigger(): 52 | parser = argparse.ArgumentParser() 53 | parser.add_argument("-i", dest="codelab_adapter_ip_address", default="None", 54 | help="None or IP address used by CodeLab Adapter") 55 | parser.add_argument("-n", dest="name", default="Trigger", help="Set name in banner") 56 | parser.add_argument("-p", dest="publisher_port", default='16130', 57 | help="Publisher IP port") 58 | parser.add_argument("-s", dest="subscriber_port", default='16103', 59 | help="Subscriber IP port") 60 | 61 | args = parser.parse_args() 62 | kw_options = {} 63 | 64 | if args.codelab_adapter_ip_address != 'None': 65 | kw_options['codelab_adapter_ip_address'] = args.codelab_adapter_ip_address 66 | 67 | kw_options['name'] = args.name 68 | 69 | kw_options['publisher_port'] = args.publisher_port 70 | kw_options['subscriber_port'] = args.subscriber_port 71 | 72 | my_trigger = Trigger(**kw_options) 73 | # my_monitor.start() 74 | 75 | # signal handler function called when Control-C occurs 76 | # noinspection PyShadowingNames,PyUnusedLocal,PyUnusedLocal 77 | def signal_handler(signal, frame): 78 | print('Control-C detected. See you soon.') 79 | 80 | my_trigger.clean_up() 81 | sys.exit(0) 82 | 83 | # listen for SIGINT 84 | signal.signal(signal.SIGINT, signal_handler) 85 | signal.signal(signal.SIGTERM, signal_handler) 86 | 87 | 88 | if __name__ == '__main__': 89 | trigger() 90 | -------------------------------------------------------------------------------- /codelab_adapter_client/topic.py: -------------------------------------------------------------------------------- 1 | ''' 2 | channel 3 | 4 | 将RPC和 [CQRS](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation) 都放在pubsub里 (ROS) 5 | 6 | 7 | [jupyter client messaging](https://jupyter-client.readthedocs.io/en/stable/messaging.html) 8 | * Shell code execution req-rep 9 | * Jupyter kernel Control channel 10 | * shutdown/restart 11 | * stdin: input from user 12 | * IOPub 13 | * stdout, stderr, debugging events 14 | 15 | 有两种视角看待消息的流向 16 | 目的地 17 | 订阅 18 | 出发点和目的地是相对的! 19 | publish 到目的地 20 | 21 | 分离管道和意义 22 | ''' 23 | 24 | # notification 25 | # adapter data 26 | # from to 27 | ADAPTER_TOPIC = "adapter/nodes/data" # 来自 Adapter 插件的消息,关注方向,Scratch的目的地,Adapter Node的发出地 28 | 29 | # scratch command 30 | SCRATCH_TOPIC = "scratch/extensions/command" 31 | # JUPYTER_TOPIC = "from_jupyter/extensions" 32 | 33 | # core 34 | # EXTS_OPERATE_TOPIC由manage订阅,node自治 35 | EXTS_OPERATE_TOPIC = "core/exts/operate" # 区分extensions和node(server) 36 | NODES_OPERATE_TOPIC = "core/nodes/operate" 37 | NODES_STATUS_TOPIC = "core/nodes/status" 38 | ADAPTER_STATUS_TOPIC = "core/status" # adapter core info 39 | NODES_STATUS_TRIGGER_TOPIC = "core/nodes/status/trigger" 40 | NODE_STATU_CHANGE_TOPIC = "core/node/statu/change" 41 | NOTIFICATION_TOPIC = "core/notification" 42 | GUI_TOPIC = "gui/operate" 43 | 44 | # ble 45 | ADAPTER_BLE_TOPIC = "adapter/ble" 46 | SCRATCH_BLE_TOPIC = "scratch/ble" 47 | 48 | # mqtt gateway(from/to) 49 | TO_MQTT_TOPIC = "to_mqtt" 50 | FROM_MQTT_TOPIC = "from_mqtt" 51 | 52 | # jupyter 53 | 54 | # Home Assistant gateway(from/to) 55 | FROM_HA_TOPIC = "from_HA" 56 | TO_HA_TOPIC = "to_HA" 57 | 58 | # websocket(socketio) 59 | # Home Assistant 60 | FROM_WEBSOCKET_TOPIC = "from_websocket" 61 | TO_WEBSOCKET_TOPIC = "to_websocket" 62 | 63 | # linda 64 | LINDA_SERVER = "linda/server" 65 | LINDA_CLIENT = "linda/client" -------------------------------------------------------------------------------- /codelab_adapter_client/utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import os 3 | import pathlib 4 | import platform 5 | import subprocess 6 | import sys 7 | import threading 8 | import time 9 | import webbrowser 10 | import urllib 11 | import urllib.request 12 | import re 13 | import base64 14 | import socket 15 | from enum import Enum 16 | 17 | from loguru import logger 18 | 19 | import uflash 20 | from codelab_adapter_client.config import settings, CODELAB_ADAPTER_DIR 21 | 22 | 23 | def get_adapter_home_path(): 24 | return CODELAB_ADAPTER_DIR 25 | 26 | def get_or_create_node_logger_dir(): 27 | codelab_adapter_dir = get_adapter_home_path() 28 | dir = codelab_adapter_dir / "node_log" 29 | return dir 30 | 31 | 32 | def setup_loguru_logger(): 33 | # 风险: 可能与adapter logger冲突, 同时读写文件 34 | # 日志由node自行处理 35 | node_logger_dir = get_or_create_node_logger_dir() 36 | debug_log = str(node_logger_dir / "debug.log") 37 | info_log = str(node_logger_dir / "info.log") 38 | error_log = str(node_logger_dir / "error.log") 39 | logger.add(debug_log, rotation="1 MB", retention="30 days", level="DEBUG") 40 | logger.add(info_log, rotation="1 MB", retention="30 days", level="INFO") 41 | logger.add(error_log, rotation="1 MB", retention="30 days", level="ERROR") 42 | 43 | 44 | def get_python3_path(): 45 | # todo 区分作为普通代码运行和作为冻结代码运行 46 | if not getattr(sys, 'frozen', False): 47 | # 普通模式 node 48 | return str(sys.executable) 49 | 50 | if settings.PYTHON3_PATH: 51 | # 允许用户覆盖settings.PYTHON3_PATH 52 | logger.info( 53 | f"local python3_path-> {settings.PYTHON3_PATH}, overwrite by user settings") 54 | return settings.PYTHON3_PATH 55 | # If it is not working, Please replace python3_path with your local python3 path. shell: which python3 56 | if (platform.system() == "Darwin"): 57 | # which python3 58 | # 不如用PATH python 59 | path = "/usr/local/bin/python3" # default 60 | if platform.system() == "Windows": 61 | path = "python" 62 | if platform.system() == "Linux": 63 | path = "/usr/bin/python3" 64 | logger.info(f"local python3_path-> {path}") 65 | return path 66 | 67 | 68 | def threaded(function): 69 | """ 70 | https://github.com/malwaredllc/byob/blob/master/byob/core/util.py#L514 71 | 72 | Decorator for making a function threaded 73 | `Required` 74 | :param function: function/method to run in a thread 75 | """ 76 | @functools.wraps(function) 77 | def _threaded(*args, **kwargs): 78 | t = threading.Thread(target=function, 79 | args=args, 80 | kwargs=kwargs, 81 | name=time.time()) 82 | t.daemon = True # exit with the parent thread 83 | t.start() 84 | return t 85 | 86 | return _threaded 87 | 88 | 89 | class TokenBucket: 90 | """An implementation of the token bucket algorithm. 91 | https://blog.just4fun.site/post/%E5%B0%91%E5%84%BF%E7%BC%96%E7%A8%8B/scratch-extension-token-bucket/#python%E5%AE%9E%E7%8E%B0 92 | 93 | >>> bucket = TokenBucket(80, 0.5) 94 | >>> print bucket.consume(10) 95 | True 96 | >>> print bucket.consume(90) 97 | False 98 | """ 99 | def __init__(self, tokens, fill_rate): 100 | """tokens is the total tokens in the bucket. fill_rate is the 101 | rate in tokens/second that the bucket will be refilled.""" 102 | self.capacity = float(tokens) 103 | self._tokens = float(tokens) 104 | self.fill_rate = float(fill_rate) 105 | self.timestamp = time.time() 106 | 107 | def consume(self, tokens): 108 | """Consume tokens from the bucket. Returns True if there were 109 | sufficient tokens otherwise False.""" 110 | if tokens <= self.tokens: 111 | self._tokens -= tokens 112 | else: 113 | return False 114 | return True 115 | 116 | def get_tokens(self): 117 | if self._tokens < self.capacity: 118 | now = time.time() 119 | delta = self.fill_rate * (now - self.timestamp) 120 | self._tokens = min(self.capacity, self._tokens + delta) 121 | self.timestamp = now 122 | return self._tokens 123 | 124 | tokens = property(get_tokens) 125 | 126 | 127 | def subprocess_args(include_stdout=True): 128 | ''' 129 | only Windows 130 | ''' 131 | if hasattr(subprocess, 'STARTUPINFO'): 132 | si = subprocess.STARTUPINFO() 133 | si.dwFlags |= subprocess.STARTF_USESHOWWINDOW 134 | env = os.environ 135 | else: 136 | si = None 137 | env = None 138 | 139 | ret = {} 140 | ret.update({ 141 | 'stdin': subprocess.PIPE, 142 | 'stderr': subprocess.PIPE, 143 | 'startupinfo': si, 144 | 'env': env 145 | }) 146 | return ret 147 | 148 | 149 | def get_pip_mirrors(): 150 | if settings.USE_CN_PIP_MIRRORS: 151 | return f"-i {settings.CN_PIP_MIRRORS_HOST}" # settings 152 | else: 153 | return "" 154 | 155 | 156 | def install_requirement(requirement, use_cn_mirrors=True): 157 | python_path = get_python3_path() 158 | pip_mirrors = get_pip_mirrors() # maybe blank 159 | install_cmd = f'{python_path} -m pip install {" ".join(requirement)} {pip_mirrors} --upgrade' 160 | logger.debug(f"install_cmd -> {install_cmd}") 161 | output = subprocess.call( 162 | install_cmd, 163 | shell=True, 164 | ) 165 | return output 166 | 167 | 168 | def is_win(): 169 | if platform.system() == "Windows": 170 | return True 171 | 172 | 173 | def is_mac(): 174 | if (platform.system() == "Darwin"): 175 | # which python3 176 | # 不如用PATH python 177 | return True 178 | 179 | 180 | def is_linux(): 181 | if platform.system() == "Linux": 182 | return True 183 | 184 | 185 | # https://github.com/thonny/thonny/blob/master/thonny/ui_utils.py#L1764 186 | def open_path_in_system_file_manager(path): 187 | if platform.system() == "Darwin": 188 | # http://stackoverflow.com/a/3520693/261181 189 | # -R doesn't allow showing hidden folders 190 | cmd = "open" 191 | if platform.system() == "Linux": 192 | cmd = "xdg-open" 193 | if platform.system() == "Windows": 194 | cmd = "explorer" 195 | subprocess.Popen([cmd, str(path)]) 196 | return [cmd, str(path)] 197 | 198 | open_path = open_path_in_system_file_manager 199 | 200 | def run_monitor(monitor_func, codelab_adapter_ip_address=None): 201 | from codelab_adapter_client.simple_node import EimMonitorNode 202 | logger.debug("waiting for a message...") 203 | try: 204 | node = EimMonitorNode(monitor_func, codelab_adapter_ip_address=codelab_adapter_ip_address, start_cmd_message_id=-1) 205 | node.receive_loop_as_thread() 206 | node.run() 207 | except KeyboardInterrupt: 208 | node.terminate() # Clean up before exiting. 209 | finally: 210 | logger.debug("stop monitor.") 211 | 212 | 213 | def send_simple_message(content): 214 | import ssl 215 | # https eim send, python3 216 | # 阻塞问题 频率消息 http连接数? 217 | # 中文有问题 218 | url = f"https://codelab-adapter.codelab.club:12358/api/message/eim" 219 | data = {"message": content} 220 | url_values = urllib.parse.urlencode(data) 221 | full_url = url + '?' + url_values 222 | with urllib.request.urlopen(full_url, context=ssl.SSLContext()) as response: 223 | # html = response.read() 224 | return "success!" 225 | 226 | send_message = send_simple_message 227 | 228 | def save_base64_to_image(src, name): 229 | """ 230 | ref: https://blog.csdn.net/mouday/article/details/93489508 231 | 解码图片 232 | eg: 233 | src="data:image/gif;base64,xxx" # 粘贴到在浏览器地址栏中可以直接显示 234 | :return: str 保存到本地的文件名 235 | """ 236 | 237 | result = re.search("data:image/(?P.*?);base64,(?P.*)", src, 238 | re.DOTALL) 239 | if result: 240 | ext = result.groupdict().get("ext") 241 | data = result.groupdict().get("data") 242 | else: 243 | raise Exception("Do not parse!") 244 | 245 | img = base64.urlsafe_b64decode(data) 246 | 247 | filename = "{}.{}".format(name, ext) 248 | with open(filename, "wb") as f: 249 | f.write(img) 250 | # do something with the image... 251 | return filename 252 | 253 | def get_local_ip(): 254 | try: 255 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 256 | s.connect(("8.8.8.8", 80)) # 114.114.114.114 257 | ip = s.getsockname()[0] 258 | return ip 259 | except Exception as e: 260 | str(e) 261 | 262 | class LindaTimeoutError(Exception): 263 | pass 264 | 265 | class NodeTerminateError(Exception): 266 | pass 267 | 268 | class LindaOperate(Enum): 269 | OUT = "out" 270 | IN = "in" 271 | INP = "inp" 272 | RD = "rd" 273 | RDP = "rdp" 274 | # helper 275 | DUMP = "dump" 276 | STATUS = "status" 277 | REBOOT = "reboot" 278 | 279 | 280 | def _get_adapter_endpoint_with_token(path="/"): 281 | if settings.WEB_UI_ENDPOINT: 282 | return f'{settings.WEB_UI_ENDPOINT}?adapter_token={settings.TOKEN}' 283 | else: 284 | if settings.USE_SSL: 285 | scheme = "https" 286 | else: 287 | scheme = "http" 288 | endpoint = f'{scheme}://{settings.DEFAULT_ADAPTER_HOST}:12358{path}?adapter_token={settings.TOKEN}' 289 | return endpoint 290 | 291 | def open_webui(): 292 | # as http/https 293 | url = _get_adapter_endpoint_with_token() 294 | webbrowser.open(url) 295 | logger.info(f'Open WebUI -> {url}') # 统计从启动到打开webui时间, 在开发环境我的电脑下,1s 296 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # upload to pypi 2 | https://pypi.org/project/twine/ 3 | 4 | * pip install twine 5 | * python -m pip install --upgrade setuptools wheel 6 | * python setup.py sdist bdist_wheel 7 | * twine upload dist/* -------------------------------------------------------------------------------- /examples/HANode_simple_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "'''\n", 10 | "Requirement:\n", 11 | " codelab-adapter v2\n", 12 | " extension_HA.py or Neverland\n", 13 | "'''" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 1, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "from codelab_adapter_client import HANode" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 2, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "class Neverland(HANode):\n", 32 | " def __init__(self):\n", 33 | " super().__init__()" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 3, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "neverland = Neverland()" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 4, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "neverland.call_service(service=\"turn_off\")" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 5, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "# neverland.call_service(service=\"turn_off\",domain=\"switch\", entity_id=\"switch.0x00158d0002ecce03_switch_right\")" 61 | ] 62 | } 63 | ], 64 | "metadata": { 65 | "kernelspec": { 66 | "display_name": "Python 3", 67 | "language": "python", 68 | "name": "python3" 69 | }, 70 | "language_info": { 71 | "codemirror_mode": { 72 | "name": "ipython", 73 | "version": 3 74 | }, 75 | "file_extension": ".py", 76 | "mimetype": "text/x-python", 77 | "name": "python", 78 | "nbconvert_exporter": "python", 79 | "pygments_lexer": "ipython3", 80 | "version": "3.7.2" 81 | } 82 | }, 83 | "nbformat": 4, 84 | "nbformat_minor": 2 85 | } 86 | -------------------------------------------------------------------------------- /examples/cube_symphony_pygame.py: -------------------------------------------------------------------------------- 1 | ''' 2 | pip install pgzero 3 | 4 | pgzrun x.py 5 | 6 | doc: https://pygame-zero.readthedocs.io/en/stable/index.html 7 | ''' 8 | from codelab_adapter_client import HANode 9 | import time 10 | 11 | ''' 12 | hand_clap = pygame.mixer.Sound("src/Hand Clap.wav") 13 | cowbell = pygame.mixer.Sound("src/Large Cowbell.wav") 14 | ''' 15 | 16 | beep = tone.create('A3', 0.5) # pgzero 内建对象 17 | 18 | 19 | class Neverland(HANode): 20 | def __init__(self): 21 | super().__init__() 22 | 23 | def neverland_event(self, entity, action, entity_id): 24 | ''' 25 | entity_id 26 | ''' 27 | print(entity, action, entity_id) 28 | if entity == "cube": 29 | if action == "slide": 30 | beep.play() 31 | 32 | def run(self): 33 | self.receive_loop() 34 | 35 | 36 | neverland = Neverland() 37 | 38 | try: 39 | neverland.run() 40 | except KeyboardInterrupt: 41 | neverland.terminate() 42 | -------------------------------------------------------------------------------- /examples/cube_symphony_sonicpi.py: -------------------------------------------------------------------------------- 1 | ''' 2 | pip install python-sonic 3 | 4 | instal sonic pi 5 | ''' 6 | import pygame 7 | from codelab_adapter_client import HANode 8 | from time import sleep 9 | 10 | from psonic import play, run # work with sonic pi 11 | 12 | 13 | def sonicpi_sample_wav(wav_file): 14 | wav_file_dir = "~/mylab/codelabclub/lab/codelab_adapter_client/examples/src" 15 | return f'sample "{wav_file_dir}/{wav_file}"' 16 | 17 | def test_run(): 18 | return ''' 19 | define :foo do 20 | play 50 21 | sleep 1 22 | play 55 23 | sleep 0.5 24 | end 25 | 26 | foo 27 | 28 | sleep 1 29 | 30 | 2.times do 31 | foo 32 | end 33 | ''' 34 | 35 | cowbell = sonicpi_sample_wav("Large Cowbell.wav") 36 | hand_clap = sonicpi_sample_wav("Large Cowbell.wav") 37 | 38 | # sample "~/mylab/codelabclub/lab/codelab_adapter_client/examples/src/Large Cowbell.wav" 39 | # set midi todo with sonic 40 | 41 | 42 | class Neverland(HANode): 43 | def __init__(self): 44 | super().__init__() 45 | self.beat = 1/4 46 | 47 | def neverland_event(self, entity, action, entity_id): 48 | ''' 49 | entity_id 50 | ''' 51 | print(entity, action, entity_id) 52 | if entity == "cube": 53 | if action == "slide": 54 | run(cowbell) 55 | # run(test_run()) 56 | if action == "rotate_left": 57 | # play (60, attack=0.5, decay=1, sustain_level=0.4, sustain=2, release=0.5) 58 | # attack 淡入时间,中间持续时间,release淡出时间 59 | # play是非阻塞的 60 | play(70,sustain=0.25) # 响度 amp=2/0.5, 方向 pan=-1/1/0 61 | sleep(self.beat) 62 | play(72,sustain=0.25) 63 | 64 | def run(self): 65 | self.receive_loop() 66 | 67 | 68 | neverland = Neverland() 69 | 70 | try: 71 | neverland.run() 72 | except KeyboardInterrupt: 73 | neverland.terminate() 74 | ''' 75 | while True: 76 | for event in pygame.event.get(): 77 | if event.type == pygame.QUIT: 78 | pygame.quit(); #sys.exit() if sys is imported 79 | if event.type == pygame.KEYDOWN: 80 | if event.key == pygame.K_h: 81 | hand_clap.play() 82 | if event.key == pygame.K_cc: 83 | cowbell.play() 84 | ''' -------------------------------------------------------------------------------- /examples/eim_node.py: -------------------------------------------------------------------------------- 1 | import time 2 | from loguru import logger 3 | from codelab_adapter_client import AdapterNode 4 | 5 | 6 | class EIMNode(AdapterNode): 7 | def __init__(self): 8 | super().__init__() 9 | self.NODE_ID = "eim" 10 | 11 | def send_message_to_scratch(self, content): 12 | message = self.message_template() 13 | message["payload"]["content"] = content 14 | self.publish(message) 15 | 16 | def extension_message_handle(self, topic, payload): 17 | self.logger.info(f'the message payload from scratch: {payload}') 18 | content = payload["content"] 19 | if type(content) == str: 20 | content_send_to_scratch = content[::-1] # 反转字符串 21 | self.send_message_to_scratch(content_send_to_scratch) 22 | 23 | def run(self): 24 | while self._running: 25 | time.sleep(1) 26 | 27 | 28 | if __name__ == "__main__": 29 | try: 30 | node = EIMNode() 31 | node.receive_loop_as_thread() 32 | node.run() 33 | except KeyboardInterrupt: 34 | node.terminate() # Clean up before exiting. 35 | -------------------------------------------------------------------------------- /examples/eim_node_aio.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from codelab_adapter_client import MessageNodeAio 3 | 4 | class TestNode(MessageNodeAio): 5 | def __init__(self,event_loop): 6 | super().__init__(event_loop=event_loop) 7 | 8 | loop = asyncio.get_event_loop() 9 | test_node = TestNode(loop) 10 | # loop 11 | async def hello_world(): 12 | while True: 13 | await asyncio.sleep(1) 14 | 15 | loop.run_until_complete(hello_world()) -------------------------------------------------------------------------------- /examples/face_recognition_open_door.py: -------------------------------------------------------------------------------- 1 | ''' 2 | office access control system 3 | https://github.com/ageitgey/face_recognition/blob/master/README_Simplified_Chinese.md 4 | https://github.com/ageitgey/face_recognition/blob/master/examples/facerec_from_webcam_faster.py 5 | https://github.com/wwj718/cv_note/tree/master/face_detect 6 | https://github.com/wwj718/cv_note/blob/master/face_detect/facerec_from_webcam_faster_print.py 7 | ''' 8 | import face_recognition 9 | import cv2 10 | import numpy as np 11 | import subprocess 12 | 13 | # This is a demo of running face recognition on live video from your webcam. It's a little more complicated than the 14 | # other example, but it includes some basic performance tweaks to make things run a lot faster: 15 | # 1. Process each video frame at 1/4 resolution (though still display it at full resolution) 16 | # 2. Only detect faces in every other frame of video. 17 | 18 | # PLEASE NOTE: This example requires OpenCV (the `cv2` library) to be installed only to read from your webcam. 19 | # OpenCV is *not* required to use the face_recognition library. It's only required if you want to run this 20 | # specific demo. If you have trouble installing it, try any of the other demos that don't require it instead. 21 | 22 | # Get a reference to webcam #0 (the default one) 23 | video_capture = cv2.VideoCapture(0) 24 | 25 | # Load a sample picture and learn how to recognize it. 26 | # obama_image = face_recognition.load_image_file("obama.jpg") 27 | # obama_face_encoding = face_recognition.face_encodings(obama_image)[0] 28 | 29 | # Load a second sample picture and learn how to recognize it. 30 | # biden_image = face_recognition.load_image_file("biden.jpg") 31 | # biden_face_encoding = face_recognition.face_encodings(biden_image)[0] 32 | 33 | # python3 ../camera/read_from_webcam_screenshot.py 34 | my_image = face_recognition.load_image_file("test.jpg") 35 | my_face_encoding = face_recognition.face_encodings(my_image)[0] 36 | 37 | # Create arrays of known face encodings and their names 38 | known_face_encodings = [ 39 | # obama_face_encoding, biden_face_encoding, my_face_encoding 40 | my_face_encoding 41 | ] 42 | known_face_names = ["myself"] 43 | 44 | # Initialize some variables 45 | face_locations = [] 46 | face_encodings = [] 47 | face_names = [] 48 | 49 | 50 | def main(): 51 | process_this_frame = True 52 | while True: 53 | # Grab a single frame of video 54 | ret, frame = video_capture.read() 55 | 56 | # Resize frame of video to 1/4 size for faster face recognition processing 57 | small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25) 58 | 59 | # Convert the image from BGR color (which OpenCV uses) to RGB color (which face_recognition uses) 60 | rgb_small_frame = small_frame[:, :, ::-1] 61 | 62 | # Only process every other frame of video to save time 63 | if process_this_frame: 64 | # Find all the faces and face encodings in the current frame of video 65 | face_locations = face_recognition.face_locations(rgb_small_frame) 66 | face_encodings = face_recognition.face_encodings( 67 | rgb_small_frame, face_locations) 68 | 69 | face_names = [] 70 | for face_encoding in face_encodings: 71 | # See if the face is a match for the known face(s) 72 | matches = face_recognition.compare_faces( 73 | known_face_encodings, face_encoding,tolerance=0.3) 74 | name = "Unknown" 75 | 76 | # # If a match was found in known_face_encodings, just use the first one. 77 | # if True in matches: 78 | # first_match_index = matches.index(True) 79 | # name = known_face_names[first_match_index] 80 | 81 | # Or instead, use the known face with the smallest distance to the new face 82 | face_distances = face_recognition.face_distance( 83 | known_face_encodings, face_encoding) 84 | best_match_index = np.argmin(face_distances) 85 | if matches[best_match_index]: 86 | name = known_face_names[best_match_index] 87 | face_names.append(name) 88 | 89 | process_this_frame = not process_this_frame 90 | 91 | # Display the results 92 | for (top, right, bottom, left), name in zip(face_locations, 93 | face_names): 94 | # Scale back up face locations since the frame we detected in was scaled to 1/4 size 95 | top *= 4 96 | right *= 4 97 | bottom *= 4 98 | left *= 4 99 | 100 | # Draw a box around the face 101 | print(name) 102 | # print(name, (left, top), (right, bottom)) 103 | subprocess.call(f"codelab-message-pub -t adapter/nodes/data -c {name}", shell=True) 104 | 105 | # Hit 'q' on the keyboard to quit! 106 | if cv2.waitKey(1) & 0xFF == ord('q'): 107 | break 108 | 109 | 110 | try: 111 | main() 112 | except KeyboardInterrupt: 113 | pass 114 | # Release handle to the webcam 115 | video_capture.release() 116 | cv2.destroyAllWindows() 117 | -------------------------------------------------------------------------------- /examples/helloworld.hy: -------------------------------------------------------------------------------- 1 | (import codelab_adapter_client) 2 | (import time) 3 | 4 | (defclass HelloWorldNode [codelab_adapter_client.AdapterNode] 5 | "LISP hello world node" 6 | 7 | (defn --init-- [self] 8 | (.--init-- (super)) 9 | (setv self.NODE_ID "eim")) 10 | 11 | (defn extension-message-handle [self topic payload] 12 | (print f"the message payload from scratch: {payload}") 13 | (setv content (list (get payload "content"))) 14 | (.reverse content) 15 | (payload.__setitem__ "content" (.join "" content)) 16 | (print payload) 17 | (self.publish {"payload" payload}) 18 | ) 19 | 20 | (defn run [self] 21 | (while self._running (time.sleep 1))) 22 | ) 23 | 24 | (setv node (HelloWorldNode)) 25 | (.receive-loop-as-thread node) 26 | (.run node) -------------------------------------------------------------------------------- /examples/microbit_display.py: -------------------------------------------------------------------------------- 1 | from codelab_adapter_client.microbit import MicrobitNode 2 | 3 | 4 | class MyNode(MicrobitNode): 5 | def __init__(self): 6 | super().__init__() 7 | 8 | def run(self): 9 | # document: https://microbit-micropython.readthedocs.io/en/latest/ 10 | content = "a" 11 | py_code = f"display.scroll('{content}', wait=False, loop=False)" 12 | self.send_command(py_code) 13 | 14 | 15 | if __name__ == "__main__": 16 | try: 17 | node = MyNode() 18 | # node.receive_loop_as_thread() # get microbit data 19 | node.run() 20 | except KeyboardInterrupt: 21 | node.terminate() # Clean up before exiting. -------------------------------------------------------------------------------- /examples/microbit_event.py: -------------------------------------------------------------------------------- 1 | from codelab_adapter_client.microbit import MicrobitNode 2 | import time 3 | 4 | 5 | class MyNode(MicrobitNode): 6 | def __init__(self): 7 | super().__init__() 8 | 9 | def when_button_a_is_pressed(self): 10 | self.logger.info("you press button A!") 11 | 12 | def microbit_event(self, data): 13 | self.logger.debug(data) 14 | if data["button_a"] == True: 15 | self.when_button_a_is_pressed() 16 | 17 | def run(self): 18 | # document: https://microbit-micropython.readthedocs.io/en/latest/ 19 | while self._running: 20 | time.sleep(1) 21 | 22 | 23 | if __name__ == "__main__": 24 | try: 25 | node = MyNode() 26 | node.receive_loop_as_thread() # get microbit data 27 | node.run() 28 | except KeyboardInterrupt: 29 | node.terminate() # Clean up before exiting. -------------------------------------------------------------------------------- /examples/microbit_link_camera.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | 3 | import time 4 | from loguru import logger 5 | from codelab_adapter_client import AdapterNode 6 | 7 | content = 0 8 | 9 | 10 | class EIMNode(AdapterNode): 11 | def __init__(self): 12 | super().__init__() 13 | self.NODE_ID = "eim" 14 | 15 | def extension_message_handle(self, topic, payload): 16 | global content 17 | # self.logger.info(f'the message payload from scratch: {payload}') 18 | content = payload["content"] 19 | # print(content) 20 | 21 | 22 | node = EIMNode() 23 | node.receive_loop_as_thread() 24 | 25 | 26 | def show_webcam(mirror=False): 27 | global content 28 | scale = 10 29 | cam = cv2.VideoCapture(0) 30 | while True: 31 | ret_val, image = cam.read() 32 | if mirror: 33 | image = cv2.flip(image, 1) 34 | 35 | #get the webcam size 36 | height, width, channels = image.shape 37 | 38 | #prepare the crop 39 | centerX, centerY = int(height / 2), int(width / 2) 40 | radiusX, radiusY = int(scale * height / 100), int(scale * width / 100) 41 | 42 | minX, maxX = centerX - radiusX, centerX + radiusX 43 | minY, maxY = centerY - radiusY, centerY + radiusY 44 | 45 | cropped = image[minX:maxX, minY:maxY] 46 | resized_cropped = cv2.resize(cropped, (width, height)) 47 | 48 | cv2.imshow('my webcam', resized_cropped) 49 | 50 | #add + or - 5 % to zoom 51 | key = cv2.waitKey(1) & 0xFF 52 | 53 | if key == ord("q"): 54 | break # esc to quit 55 | if key == ord("a"): 56 | if scale < 48: 57 | scale += 2 # +2 58 | 59 | if key == ord("b"): 60 | if scale >= 10: 61 | scale -= 2 # +2 62 | 63 | print("content:", content) 64 | if scale == "A": 65 | print("A") 66 | else: 67 | scale = 10 + 40/100*int(content) 68 | 69 | cv2.destroyAllWindows() 70 | 71 | 72 | show_webcam() -------------------------------------------------------------------------------- /examples/neverland_control_door.py: -------------------------------------------------------------------------------- 1 | from codelab_adapter_client import HANode 2 | import time 3 | 4 | class Neverland(HANode): 5 | def __init__(self): 6 | super().__init__() 7 | 8 | def open_door(self): 9 | self.call_service( 10 | service="turn_on", 11 | domain="switch", 12 | entity_id="switch.0x00158d0001b774fd_switch_l1") 13 | 14 | def close_door(self): 15 | self.call_service( 16 | service="turn_off", 17 | domain="switch", 18 | entity_id="switch.0x00158d0001b774fd_switch_l1") 19 | 20 | neverland = Neverland() 21 | neverland.open_door() 22 | time.sleep(5) 23 | neverland.close_door() -------------------------------------------------------------------------------- /examples/neverland_door_open_capture.py: -------------------------------------------------------------------------------- 1 | ''' 2 | pip install codelab_adapter_client opencv-python 3 | 4 | # ref: https://gpiozero.readthedocs.io/en/stable/recipes.html#button-controlled-camera 5 | ''' 6 | from codelab_adapter_client import HANode 7 | from datetime import datetime 8 | import cv2 9 | 10 | cap = cv2.VideoCapture(0) 11 | 12 | 13 | class Neverland(HANode): 14 | def __init__(self): 15 | super().__init__() 16 | 17 | def capture(self): 18 | timestamp = datetime.now().isoformat() 19 | ret, frame = cap.read() 20 | cv2.imwrite(f"/tmp/{timestamp}.jpg", frame) 21 | 22 | def when_open_door(self): 23 | print("The door is opened") 24 | self.capture() 25 | 26 | def run(self): 27 | self.receive_loop() 28 | 29 | 30 | neverland = Neverland() 31 | neverland.run() 32 | -------------------------------------------------------------------------------- /examples/neverland_handle_message.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 3, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from codelab_adapter_client import HANode\n", 10 | "from codelab_adapter_client.topic import FROM_HA_TOPIC\n", 11 | "# from pprint import pprint as print\n", 12 | "class Neverland(HANode):\n", 13 | " def __init__(self):\n", 14 | " super().__init__()\n", 15 | "\n", 16 | " def run(self):\n", 17 | " self.receive_loop()" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "neverland = Neverland()\n", 27 | "neverland.run()" 28 | ] 29 | } 30 | ], 31 | "metadata": { 32 | "kernelspec": { 33 | "display_name": "Python 3", 34 | "language": "python", 35 | "name": "python3" 36 | }, 37 | "language_info": { 38 | "codemirror_mode": { 39 | "name": "ipython", 40 | "version": 3 41 | }, 42 | "file_extension": ".py", 43 | "mimetype": "text/x-python", 44 | "name": "python", 45 | "nbconvert_exporter": "python", 46 | "pygments_lexer": "ipython3", 47 | "version": "3.7.2" 48 | } 49 | }, 50 | "nbformat": 4, 51 | "nbformat_minor": 2 52 | } 53 | -------------------------------------------------------------------------------- /examples/neverland_i_am_reading.py: -------------------------------------------------------------------------------- 1 | from codelab_adapter_client import HANode 2 | import subprocess 3 | import time 4 | 5 | class Neverland(HANode): 6 | def __init__(self): 7 | super().__init__() 8 | 9 | def open_my_book(self): 10 | subprocess.call("open /Applications/iBooks.app/", shell=True) 11 | 12 | def when_open_door(self): 13 | print("The door is opened") 14 | self.open_my_book() 15 | 16 | def when_close_door(self): 17 | print("The door is closed") 18 | 19 | def neverland_event(self, entity, action, entity_id): 20 | print(entity, action) 21 | 22 | def run(self): 23 | self.receive_loop() 24 | 25 | 26 | neverland = Neverland() 27 | neverland.run() -------------------------------------------------------------------------------- /examples/neverland_irobot.py: -------------------------------------------------------------------------------- 1 | from codelab_adapter_client import HANode 2 | 3 | 4 | class Neverland(HANode): 5 | def __init__(self): 6 | super().__init__() 7 | 8 | 9 | neverland = Neverland() 10 | 11 | # neverland.call_service(service="toggle") 12 | 13 | neverland.call_service(service="turn_on",domain="vacuum", entity_id="vacuum.roomba") 14 | # service: start_pause stop return_to_base -------------------------------------------------------------------------------- /examples/neverland_list_all_lights.py: -------------------------------------------------------------------------------- 1 | from codelab_adapter_client import HANode 2 | import subprocess 3 | import time 4 | 5 | class Neverland(HANode): 6 | def __init__(self): 7 | super().__init__() 8 | 9 | neverland = Neverland() 10 | neverland.receive_loop_as_thread() 11 | time.sleep(1) 12 | all_light = neverland.list_all_light_entity_ids() 13 | print(all_light) -------------------------------------------------------------------------------- /examples/neverland_toggle_light.py: -------------------------------------------------------------------------------- 1 | from codelab_adapter_client import HANode 2 | 3 | 4 | class Neverland(HANode): 5 | def __init__(self): 6 | super().__init__() 7 | 8 | 9 | neverland = Neverland() 10 | 11 | neverland.call_service(service="toggle") 12 | 13 | # neverland.call_service(service="turn_off",domain="switch", entity_id="switch.0x00158d0002ecce03_switch_right") 14 | -------------------------------------------------------------------------------- /examples/neverland_wechat.py: -------------------------------------------------------------------------------- 1 | ''' 2 | python >= 3.6 3 | pip install itchat codelab_adapter_client 4 | ''' 5 | import itchat 6 | from codelab_adapter_client import HANode 7 | 8 | class Neverland(HANode): 9 | def __init__(self): 10 | super().__init__() 11 | 12 | neverland = Neverland() 13 | 14 | @itchat.msg_register(itchat.content.TEXT) 15 | def text_reply(msg): 16 | if msg.text == "开灯": 17 | neverland.call_service(service="turn_on") 18 | msg.user.send("已开灯") 19 | if msg.text == "关灯": 20 | neverland.call_service(service="turn_off") 21 | msg.user.send("已关灯") 22 | 23 | 24 | # neverland.call_service(service="turn_off",domain="switch", entity_id="switch.0x00158d0002ecce03_switch_right") 25 | itchat.auto_login(hotReload=True) 26 | itchat.run() -------------------------------------------------------------------------------- /examples/pgzero_makeymakey.py: -------------------------------------------------------------------------------- 1 | ''' 2 | makey makey作为输入: 接触、水果 3 | 4 | pgzrun x.py 5 | ''' 6 | 7 | 8 | def on_key_down(): 9 | if keyboard.space: 10 | print("space") 11 | beep = tone.create('E3', 0.5) 12 | beep.play() 13 | 14 | if keyboard.b: 15 | print("b") -------------------------------------------------------------------------------- /examples/play_neverland.py: -------------------------------------------------------------------------------- 1 | ''' 2 | pip install replit-play codelab_adapter_client 3 | ''' 4 | 5 | import play 6 | import time 7 | from codelab_adapter_client import HANode 8 | 9 | # cat = play.new_text('=^.^=', font_size=70) 10 | cat = play.new_text('click to turn off the light!', font_size=70) 11 | 12 | 13 | class Neverland(HANode): 14 | def __init__(self): 15 | super().__init__() 16 | 17 | 18 | neverland = Neverland() 19 | 20 | num = 1 21 | 22 | 23 | @cat.when_clicked 24 | def win_function(): 25 | global num 26 | cat.show() 27 | 28 | if num % 2 == 0: 29 | neverland.call_service(service="turn_on") 30 | cat.words = 'click to turn off the light!' 31 | else: 32 | neverland.call_service(service="turn_off") 33 | cat.words = 'click to turn on the light!' 34 | num = num + 1 # num += 1 35 | 36 | 37 | play.start_program() -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | # readme 2 | > education as life 3 | 4 | 与生活相关的小案例 5 | 6 | ### 快看书! 7 | 如果有人开门则切换屏幕 8 | 9 | subprocess 10 | shell命令 11 | 12 | 解析json 13 | pprint 14 | 15 | ### 声控灯 16 | pygame 17 | 18 | ### 情绪灯光 19 | opencv 20 | 21 | ### GUI开关 22 | guizero 23 | 24 | Pygame/Pygame Zero 25 | 26 | play 27 | 28 | p5py 29 | 30 | [Play: 像学习Scratch那样学习Python](https://blog.just4fun.site/replit-play.html) 31 | 32 | ### 做一个手柄 33 | keyboard HCI 34 | 35 | ### 微信关灯 36 | itchat 37 | 38 | ### jupyter lab 39 | jupyter lab --notebook-dir=~/codelab_adapter -------------------------------------------------------------------------------- /examples/src/Hand Clap.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeLabClub/codelab_adapter_client_python/f15b34396c294aaeacfa9b86814f90094f717933/examples/src/Hand Clap.wav -------------------------------------------------------------------------------- /examples/src/High Hat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeLabClub/codelab_adapter_client_python/f15b34396c294aaeacfa9b86814f90094f717933/examples/src/High Hat.wav -------------------------------------------------------------------------------- /examples/src/Large Cowbell.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeLabClub/codelab_adapter_client_python/f15b34396c294aaeacfa9b86814f90094f717933/examples/src/Large Cowbell.wav -------------------------------------------------------------------------------- /examples/src/Snare Drum.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeLabClub/codelab_adapter_client_python/f15b34396c294aaeacfa9b86814f90094f717933/examples/src/Snare Drum.wav -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # codelab_adapter_client 2 | Python Client of [CodeLab Adapter](https://adapter.codelab.club/) v3. 3 | 4 | # Install 5 | ```bash 6 | # Python >= 3.6 7 | pip install codelab_adapter_client 8 | ``` 9 | 10 | # Usage 11 | ```python 12 | from codelab_adapter_client import AdapterNode 13 | ``` 14 | 15 | # example 16 | [extension_eim.py](https://github.com/wwj718/codelab_adapter_client/blob/master/examples/extension_eim.py) 17 | 18 | # tools(for debugging) 19 | ``` 20 | codelab-message-monitor # subscribes to all messages and print both topic and payload. 21 | codelab-message-trigger # pub the message in json file(`/tmp/message.json`). 22 | codelab-message-pub -j '{"topic":"eim/test","payload":{"content":"test contenst"}}' 23 | ``` 24 | 25 | `/tmp/message.json`: 26 | 27 | ```json 28 | { 29 | "topic": "adapter_core/exts/operate", 30 | "payload": { "content": "start", "node_name": "extension_eim" } 31 | } 32 | ``` 33 | 34 | # FAQ 35 | ## 在 Adapter jupyterlab 中升级 codelab_adapter_client 36 | ```py 37 | import pip 38 | pip.main(['install', 'https://github.com/CodeLabClub/codelab_adapter_client_python/archive/master.zip']) 39 | # 安装完成后在jupyterlab restart 一下 kernel 40 | ``` 41 | 42 | ## EIM message 43 | 与 Scratch EIM 积木配合使用 44 | 45 | ``` 46 | from codelab_adapter_client.message import receive_message, send_message 47 | ``` 48 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip==18.1 2 | bumpversion==0.5.3 3 | wheel==0.32.1 4 | watchdog==0.9.0 5 | flake8==3.5.0 6 | tox==3.5.2 7 | coverage==4.5.1 8 | Sphinx==1.8.1 9 | twine==1.12.1 10 | 11 | pytest==3.8.2 12 | pytest-runner==4.2 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.3.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:codelab_adapter_client/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | 20 | [aliases] 21 | # Define setup.py command aliases here 22 | test = pytest 23 | 24 | [tool:pytest] 25 | collect_ignore = ['setup.py'] 26 | 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """The setup script.""" 4 | 5 | from setuptools import setup, find_packages 6 | 7 | with open('README.rst') as readme_file: 8 | readme = readme_file.read() 9 | 10 | with open('HISTORY.rst') as history_file: 11 | history = history_file.read() 12 | REQUIRES_PYTHON = ">=3.6.0" 13 | # todo verison, pyzmq 18 19 20 ok 14 | requirements = ['pyzmq==20.0.0', 'msgpack-python==0.5.6', 'loguru==0.5.3', 'uflash==1.3.0', "zeroconf==0.28.8", "click==7.1.2", "dynaconf==3.1.2", "paho-mqtt==1.5.1"] 15 | 16 | setup_requirements = [ 17 | 'pytest-runner', 18 | ] 19 | 20 | test_requirements = ['pytest', 'pytest-mock'] 21 | 22 | setup( 23 | python_requires=REQUIRES_PYTHON, 24 | author="Wenjie Wu", 25 | author_email='wuwenjie718@gmail.com', 26 | classifiers=[ 27 | 'Development Status :: 2 - Pre-Alpha', 28 | 'Intended Audience :: Developers', 29 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 30 | 'Natural Language :: English', 31 | "Programming Language :: Python :: 2", 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Programming Language :: Python :: 3.5', 36 | 'Programming Language :: Python :: 3.6', 37 | 'Programming Language :: Python :: 3.7', 38 | ], 39 | description= 40 | "Python Boilerplate contains all the boilerplate you need to create a Python package.", 41 | install_requires=requirements, 42 | license="GNU General Public License v3", 43 | long_description=readme + '\n\n' + history, 44 | include_package_data=True, 45 | keywords='codelab_adapter_client', 46 | name='codelab_adapter_client', 47 | packages=[ 48 | 'codelab_adapter_client', 'codelab_adapter_client.tools' 49 | ], # find_packages(include=['codelab_adapter_client','codelab_adapter_client.tools']), 50 | entry_points={ 51 | 'console_scripts': [ 52 | 'codelab-message-monitor = codelab_adapter_client.tools.monitor:monitor', 53 | 'codelab-message-trigger = codelab_adapter_client.tools.trigger:trigger', 54 | 'codelab-message-pub = codelab_adapter_client.tools.pub:pub', 55 | 'codelab-adapter-helper = codelab_adapter_client.tools.adapter_helper:adapter_helper', 56 | 'codelab-adapter-settings = codelab_adapter_client.tools.adapter_helper:list_settings', 57 | 'codelab-mdns-registration = codelab_adapter_client.tools.mdns_registration:main', 58 | 'codelab-mdns-browser = codelab_adapter_client.tools.mdns_browser:main', 59 | 'codelab-linda = codelab_adapter_client.tools.linda:cli', 60 | ], 61 | }, 62 | setup_requires=setup_requirements, 63 | test_suite='tests', 64 | tests_require=test_requirements, 65 | url='https://github.com/wwj718/codelab_adapter_client', 66 | version='4.4.2', 67 | zip_safe=False, 68 | ) 69 | -------------------------------------------------------------------------------- /tests/test_codelab_adapter_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Tests for `codelab_adapter_client` package.""" 5 | 6 | import pytest 7 | 8 | 9 | @pytest.fixture 10 | def response(): 11 | """Sample pytest fixture. 12 | 13 | See more at: http://doc.pytest.org/en/latest/fixture.html 14 | """ 15 | # import requests 16 | # return requests.get('https://github.com/audreyr/cookiecutter-pypackage') 17 | 18 | 19 | def test_content(response): 20 | """Sample pytest test function with the pytest fixture as an argument.""" 21 | # from bs4 import BeautifulSoup 22 | # assert 'GitHub' in BeautifulSoup(response.content).title.string -------------------------------------------------------------------------------- /tests/test_linda_client.py: -------------------------------------------------------------------------------- 1 | ''' 2 | py.test tests/test_linda_client.py -s 3 | 4 | Adapter运行之后,跑测试 5 | ''' 6 | 7 | import time 8 | import asyncio 9 | # import pytest 10 | from codelab_adapter_client import AdapterNodeAio 11 | from codelab_adapter_client import AdapterNode 12 | from codelab_adapter_client.utils import LindaTimeoutError 13 | 14 | class MyNode(AdapterNodeAio): 15 | NODE_ID = "linda/aio/test" 16 | 17 | def __init__(self): 18 | super().__init__() 19 | 20 | class MySyncNode(AdapterNode): 21 | NODE_ID = "linda/sync/test" 22 | 23 | def __init__(self): 24 | super().__init__() 25 | 26 | async def _async_test_main(node): 27 | task = asyncio.create_task(node.receive_loop()) 28 | await asyncio.sleep(0.1) # 等待管道建立 29 | _tuple = ["test_linda"] 30 | # reboot 31 | res = await node.linda_reboot() 32 | assert res == [] 33 | 34 | # out 35 | tuple_number = 3 36 | for i in range(tuple_number): 37 | _tuple = [i] 38 | res = await node.linda_out(_tuple) 39 | assert res == _tuple 40 | 41 | # dump 42 | res = await node.linda_dump() 43 | print(res) 44 | assert res == [[i] for i in range(tuple_number)] 45 | 46 | # rd 47 | _tuple = [1] 48 | res = await node.linda_rd(_tuple) 49 | assert res == _tuple 50 | 51 | # in 52 | for i in range(tuple_number): 53 | _tuple = [i] 54 | res = await node.linda_in(_tuple) 55 | assert res == _tuple 56 | # 消耗完 57 | 58 | 59 | # 生成新的 60 | _tuple = ["hello", "world"] 61 | await node.linda_out(_tuple) 62 | 63 | # rdp 64 | res = await node.linda_rdp(_tuple) 65 | assert res == _tuple 66 | 67 | # inp 68 | res = await node.linda_inp(_tuple) 69 | assert res == _tuple 70 | 71 | res = await node.linda_dump() 72 | assert res == [] 73 | # in 74 | await node.terminate() 75 | 76 | def test_sync_linda_client(): 77 | node = MySyncNode() 78 | node.receive_loop_as_thread() 79 | time.sleep(0.1) 80 | 81 | res = node.linda_reboot() 82 | assert res == [] 83 | 84 | res = node.linda_out([1, 2, 3]) # out 85 | assert res == [1, 2, 3] 86 | 87 | res = node.linda_out([1, 2, 4]) # out 88 | res = node.linda_dump() 89 | assert res == [[1, 2, 3], [1, 2, 4]] 90 | 91 | res = node.linda_rd([1, 2, 3]) 92 | assert res == [1, 2, 3] 93 | 94 | res = node.linda_rdp([1, 2, "*"]) 95 | assert res == [1, 2, 3] # 先入先出 96 | 97 | res = node.linda_in([1,2,3]) 98 | assert res == [1, 2, 3] 99 | 100 | res = node.linda_dump() 101 | assert res == [[1, 2, 4]] 102 | 103 | res = node.linda_inp(["xxx"]) 104 | assert res == [] 105 | 106 | res = node.linda_inp([1, "*", 4]) 107 | assert res == [1, 2, 4] 108 | 109 | res = node.linda_reboot() 110 | assert res == [] 111 | 112 | def test_async_linda_client(): 113 | node = MyNode() 114 | asyncio.run(_async_test_main(node)) # 不退出? 115 | 116 | 117 | # 创建 118 | # print("find_microbit: ",utils.find_microbit()) 119 | -------------------------------------------------------------------------------- /tests/test_node.py: -------------------------------------------------------------------------------- 1 | from codelab_adapter_client import AdapterNode 2 | 3 | 4 | class TestNode: 5 | @classmethod 6 | def setup_class(cls): 7 | """ 8 | https://www.jianshu.com/p/a57b5967f08b 9 | 整个类只执行一次,如果是测试用例级别的则用setup_method 10 | """ 11 | 12 | class DemoNode(AdapterNode): 13 | def __init__(self): 14 | # super().__init__(name='TestExtension2',codelab_adapter_ip_address="192.168.31.148") 15 | super().__init__(name='demoNode') 16 | # self.set_subscriber_topic('eim') 17 | 18 | cls.node = DemoNode() 19 | 20 | @classmethod 21 | def teardown_class(cls): 22 | """ teardown any state that was previously setup with a call to 23 | setup_class. 24 | """ 25 | cls.node.clean_up() 26 | 27 | def test___str__(self): 28 | # print("ok") 29 | assert str(self.node) == "demoNode" 30 | -------------------------------------------------------------------------------- /tests/test_subscriber.py: -------------------------------------------------------------------------------- 1 | from codelab_adapter_client import AdapterNode 2 | 3 | 4 | class DemoNode(AdapterNode): 5 | def __init__(self): 6 | # super().__init__(name='TestExtension2',codelab_adapter_ip_address="192.168.31.148") 7 | super().__init__(name='demoNode') 8 | # self.set_subscriber_topic('eim') 9 | 10 | def test_subscribed_topics(): 11 | node = DemoNode() 12 | # node.subscriber 13 | assert node.subscribed_topics == set(node.subscriber_list) 14 | node.clean_up() 15 | # import IPython;IPython.embed() 16 | 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34, py35, py36, flake8 3 | 4 | [travis] 5 | python = 6 | 3.6: py36 7 | 3.5: py35 8 | 3.4: py34 9 | 2.7: py27 10 | 11 | [testenv:flake8] 12 | basepython = python 13 | deps = flake8 14 | commands = flake8 codelab_adapter_client 15 | 16 | [testenv] 17 | setenv = 18 | PYTHONPATH = {toxinidir} 19 | deps = 20 | -r{toxinidir}/requirements_dev.txt 21 | ; If you want to make tox run the tests with the same versions, create a 22 | ; requirements.txt with the pinned versions and uncomment the following line: 23 | ; -r{toxinidir}/requirements.txt 24 | commands = 25 | pip install -U pip 26 | py.test --basetemp={envtmpdir} 27 | 28 | 29 | --------------------------------------------------------------------------------