├── rpyc_ikernel
├── __init__.py
├── __main__.py
├── compat.py
├── scheduler.py
├── install.py
├── adb.py
└── kernel.py
├── MANIFEST.in
├── tests
├── test-output
│ └── .scripttest-test-dir.txt
├── images
│ ├── index.png
│ └── kernels.png
├── test_extract_uuid.py
├── test_rpyc.py
├── usage_connect.ipynb
├── test_cli.py
├── test_command_fix.py
├── test_ugly-code.ipynb
├── test_all.ipynb
└── usage_display.ipynb
├── .gitignore
├── tox.ini
├── setup.py
├── README_cn.md
└── README.md
/rpyc_ikernel/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 |
--------------------------------------------------------------------------------
/tests/test-output/.scripttest-test-dir.txt:
--------------------------------------------------------------------------------
1 | placeholder
--------------------------------------------------------------------------------
/tests/images/index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipeed/rpyc_ikernel/HEAD/tests/images/index.png
--------------------------------------------------------------------------------
/tests/images/kernels.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sipeed/rpyc_ikernel/HEAD/tests/images/kernels.png
--------------------------------------------------------------------------------
/rpyc_ikernel/__main__.py:
--------------------------------------------------------------------------------
1 | """
2 | rpyc ikernel entry point.
3 |
4 | From here you can get to 'manage', otherwise it is assumed
5 | that a kernel is required instead and instance one instead.
6 | """
7 |
8 | from ipykernel.kernelapp import IPKernelApp
9 | from .kernel import RPycKernel
10 |
11 | IPKernelApp.launch_instance(kernel_class=RPycKernel)
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python cruft
2 | __pycache__/
3 | *.py[cod]
4 | *.egg-info/
5 | *.egg
6 | pip-wheel-metadata
7 | build/
8 | dist/
9 |
10 | # IDE cruft
11 | .idea
12 | .vscode
13 | *.swp
14 | *.orig
15 | .venv/
16 | venv
17 | *.ps1
18 |
19 | # Testing
20 | .pytest_cache/
21 | .tox
22 | *.db
23 | # pytest -> junit
24 | *.xml
25 | .cache
26 | .ipynb_checkpoints
27 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # Tox (http://tox.testrun.org/) is a tool for running tests
2 | # in multiple virtualenvs. This configuration file will run the
3 | # test suite on all supported python versions. To use it, "pip install tox"
4 | # and then run "tox" from this directory.
5 |
6 | [tox]
7 | envlist = py27, py38
8 |
9 | [testenv]
10 | passenv = LC_ALL, LANG
11 | commands = py.test
12 | deps =
13 | pytest
14 | scripttest
15 |
--------------------------------------------------------------------------------
/rpyc_ikernel/compat.py:
--------------------------------------------------------------------------------
1 | """
2 | compat.py
3 |
4 | Compatibility layer for transitions from IPython 3.0 to
5 | Jupyter structure. Will attempt to import Jupyter versions
6 | of modules, but will fall back to IPython if it is not
7 | available.
8 |
9 | Provides the following modules:
10 | kernelspec -> {jupyter_client,IPython}.kernel.kernelspec
11 | tempdir -> {tempfile,IPython.utils.tempdir}
12 |
13 | """
14 | # This is a module copied from Python 3.2, so will exist
15 | # in 3.2 onwards
16 | import tempfile as tempdir
17 |
18 | # kernelspec is moved in jupyter
19 | try:
20 | from jupyter_client import kernelspec
21 | except ImportError:
22 | from IPython.kernel import kernelspec
23 |
24 | # Otherwise it might be in genutils, but that will be dissolved
25 | if not hasattr(tempdir, "TemporaryDirectory"):
26 | try:
27 | from ipython_genutils import tempdir
28 | except ImportError:
29 | from IPython.utils import tempdir
30 |
31 | __all__ = ("kernelspec", "tempdir")
32 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # default to setuptools so that 'setup.py develop' is available,
2 | # but fall back to standard modules that do the same
3 | try:
4 | from setuptools import setup
5 |
6 | scripts = []
7 | except ImportError:
8 | from distutils.core import setup
9 |
10 | scripts = ["bin/rpyc_ikernel"]
11 |
12 | setup(
13 | name="rpyc_ikernel",
14 | version="0.5.1",
15 | description="rpyc for jupyter kernel",
16 | long_description=open('README.md', 'r', encoding='UTF-8').read(),
17 | long_description_content_type='text/markdown',
18 | author="Juwan",
19 | author_email="juwan@sipeed.com",
20 | license="BSD",
21 | url="https://github.com/sipeed/rpyc_ikernel",
22 | packages=["rpyc_ikernel"],
23 | # scripts=scripts,
24 | # entry_points={"console_scripts": ["rpyc_ikernel = rpyc_ikernel.__main__:main"]},
25 | install_requires=["notebook", "pexpect", "rpyc", "pillow"],
26 | tests_requires=["pytest", "scripttest"],
27 | include_package_data=True,
28 | classifiers=[
29 | "Programming Language :: Python :: 3",
30 | "Framework :: IPython",
31 | "License :: OSI Approved :: BSD License",
32 | ],
33 | )
34 |
--------------------------------------------------------------------------------
/tests/test_extract_uuid.py:
--------------------------------------------------------------------------------
1 | """
2 | test_extract_uuid.py
3 |
4 | Check that the uuid can be extracted from filenames when
5 | available.
6 |
7 | """
8 |
9 | from uuid import UUID
10 |
11 | from rpyc_ikernel.kernel import extract_uuid, RemoteIKernel
12 |
13 |
14 | def test_extract_uuid():
15 | """Check that it gets the right one."""
16 | this_uuid = UUID("fbbdec0f-1403-48c9-b2ae-b5b5e1572068")
17 |
18 | full_path = (
19 | "/home/user/.local/share/jupyter/runtime/"
20 | "kernel-fbbdec0f-1403-48c9-b2ae-b5b5e1572068.json"
21 | )
22 | relative_path = (
23 | "../../.local/share/jupyter/runtime/"
24 | "kernel-fbbdec0f-1403-48c9-b2ae-b5b5e1572068.json"
25 | )
26 | no_path = "kernel-fbbdec0f-1403-48c9-b2ae-b5b5e1572068.json"
27 |
28 | assert extract_uuid(full_path) == this_uuid
29 | assert extract_uuid(relative_path) == this_uuid
30 | assert extract_uuid(no_path) == this_uuid
31 |
32 |
33 | def test_no_uuid():
34 | """Check that it gets nothing from these."""
35 | full_path = "/home/user/.local/share/jupyter/runtime/" "kernel-not-a-uuid.json"
36 | relative_path = "../../.local/share/jupyter/runtime/" "kernel-not-a-uuid.json"
37 | no_path = "kernel-not-a-uuid.json"
38 |
39 | assert extract_uuid(full_path) is None
40 | assert extract_uuid(relative_path) is None
41 | assert extract_uuid(no_path) is None
42 |
43 |
44 | def test_kernel_uuid():
45 | """Check that any kernel gets a uuid."""
46 | this_uuid = UUID("fbbdec0f-1403-48c9-b2ae-b5b5e1572068")
47 | no_path = "kernel-fbbdec0f-1403-48c9-b2ae-b5b5e1572068.json"
48 |
49 | test_kernel = RemoteIKernel(connection_info=no_path, interface="test")
50 | assert test_kernel.uuid == this_uuid
51 |
52 | test_kernel = RemoteIKernel(connection_info="not-a-uuid.json", interface="test")
53 | assert test_kernel.uuid is not None
54 | assert isinstance(test_kernel.uuid, UUID)
55 |
--------------------------------------------------------------------------------
/tests/test_rpyc.py:
--------------------------------------------------------------------------------
1 | import os
2 | import rpyc
3 | import tempfile
4 | from rpyc.utils.server import ThreadedServer, ThreadPoolServer
5 | from rpyc import SlaveService
6 | import unittest
7 |
8 |
9 | class Test_ThreadedServer(unittest.TestCase):
10 |
11 | def setUp(self):
12 | self.server = ThreadedServer(SlaveService, port=18878, auto_register=False)
13 | self.server.logger.quiet = False
14 | self.server._start_in_thread()
15 |
16 | def tearDown(self):
17 | while self.server.clients:
18 | pass
19 | self.server.close()
20 |
21 | def test_connection(self):
22 | conn = rpyc.classic.connect("localhost", port=18878)
23 | print(conn.modules.sys)
24 | print(conn.modules["xml.dom.minidom"].parseString(""))
25 | conn.execute("x = 5")
26 | self.assertEqual(conn.namespace["x"], 5)
27 | self.assertEqual(conn.eval("1+x"), 6)
28 | conn.close()
29 |
30 |
31 | class Test_ThreadedServerOverUnixSocket(unittest.TestCase):
32 |
33 | def setUp(self):
34 | self.socket_path = tempfile.mktemp()
35 | self.server = ThreadedServer(SlaveService, socket_path=self.socket_path, auto_register=False)
36 | self.server.logger.quiet = False
37 | self.server._start_in_thread()
38 |
39 | def tearDown(self):
40 | self.server.close()
41 | os.remove(self.socket_path)
42 |
43 | def test_connection(self):
44 | c = rpyc.classic.unix_connect(self.socket_path)
45 | print(c.modules.sys)
46 | print(c.modules["xml.dom.minidom"].parseString(""))
47 | c.execute("x = 5")
48 | self.assertEqual(c.namespace["x"], 5)
49 | self.assertEqual(c.eval("1+x"), 6)
50 | c.close()
51 |
52 |
53 | class Test_ThreadPoolServer(Test_ThreadedServer):
54 |
55 | def setUp(self):
56 | self.server = ThreadPoolServer(SlaveService, port=18878, auto_register=False)
57 | self.server.logger.quiet = False
58 | self.server._start_in_thread()
59 |
60 |
61 | if __name__ == "__main__":
62 | unittest.main()
63 |
--------------------------------------------------------------------------------
/rpyc_ikernel/scheduler.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | TIMEFORMAT = '%Y-%m-%d %H:%M:%S %Z'
5 |
6 | import threading
7 | import time
8 |
9 |
10 | class Scheduler(threading.Thread):
11 |
12 | def __init__(self, trigger, interval, fn, args=(), kwargs={}):
13 | super().__init__()
14 | if trigger not in ['delay', 'recur']:
15 | raise ValueError('trigger must be "delay" or "recur"')
16 | self.trigger = trigger
17 | self.interval = interval
18 | self.fn = fn
19 | self.args = args
20 | self.kwargs = kwargs
21 | self._event = threading.Event()
22 | self._stopped = True
23 | self._result = []
24 |
25 | # Scheduler constructor
26 | @classmethod
27 | def create(cls, **scheduler_kwargs):
28 | return cls(**scheduler_kwargs)
29 |
30 | @property
31 | def stopped(self):
32 | return self._stopped
33 |
34 | @property
35 | def result(self):
36 | return dict(self._result)
37 |
38 | def run(self):
39 | self._stopped = self._event.is_set() # False
40 | if self.trigger == 'delay':
41 | self._delay()
42 | elif self.trigger == 'recur':
43 | self._recur()
44 |
45 | def _delay(self):
46 | if not self._stopped:
47 | self._event.wait(timeout=self.interval)
48 | self._execute()
49 |
50 | def _recur(self):
51 | while not self._stopped:
52 | self._execute()
53 | self._event.wait(timeout=self.interval)
54 |
55 | def _execute(self):
56 | self._fn(self.args, self.kwargs)
57 |
58 | def _fn(self, args, kwargs):
59 | return_value = self.fn(*args, **kwargs)
60 | if return_value:
61 | timestamp = time.strftime(TIMEFORMAT, time.localtime())
62 | self._result.append( (timestamp, return_value) )
63 |
64 | # threads can only be started once
65 | # i.e., cannot start again once cancelled
66 | def cancel(self):
67 | self._event.set()
68 | self._stopped = self._event.is_set() # True
69 |
--------------------------------------------------------------------------------
/rpyc_ikernel/install.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import sys
4 | import argparse
5 |
6 | from jupyter_client.kernelspec import KernelSpecManager
7 | from IPython.utils.tempdir import TemporaryDirectory
8 |
9 | # copied out from https://github.com/takluyver/bash_kernel/blob/master/bash_kernel/install.py
10 |
11 | # sys.executable should be "python3"
12 | kernel_json = {
13 | "argv": [sys.executable, "-m", "rpyc_ikernel", "-f", "{connection_file}"],
14 | "display_name": "RPyc-Python",
15 | "language": "Python"
16 | }
17 |
18 |
19 | def install_my_kernel_spec(user=True, prefix=None):
20 | if "python2" in sys.executable:
21 | print("I think this needs python3")
22 | with TemporaryDirectory() as td:
23 | os.chmod(td, 0o755) # Starts off as 700, not user readable
24 | with open(os.path.join(td, 'kernel.json'), 'w') as f:
25 | json.dump(kernel_json, f, sort_keys=True, indent=2)
26 | # TODO: Copy resources once they're specified
27 |
28 | print('Installing IPython kernel spec of RPyc')
29 | k = KernelSpecManager()
30 | k.install_kernel_spec(td, 'RPyc', user=user,
31 | replace=True, prefix=prefix)
32 |
33 | h = k.get_kernel_spec("RPyc")
34 | print("...into", h.resource_dir)
35 |
36 |
37 | def _is_root():
38 | try:
39 | return os.geteuid() == 0
40 | except AttributeError:
41 | return False # assume not an admin on non-Unix platforms
42 |
43 |
44 | def main(argv=None):
45 | parser = argparse.ArgumentParser(
46 | description='Install KernelSpec for RPyc Kernel'
47 | )
48 | prefix_locations = parser.add_mutually_exclusive_group()
49 |
50 | prefix_locations.add_argument(
51 | '--user',
52 | help='Install KernelSpec in user homedirectory',
53 | action='store_true'
54 | )
55 | prefix_locations.add_argument(
56 | '--sys-prefix',
57 | help='Install KernelSpec in sys.prefix. Useful in conda / virtualenv',
58 | action='store_true',
59 | dest='sys_prefix'
60 | )
61 | prefix_locations.add_argument(
62 | '--prefix',
63 | help='Install KernelSpec in this prefix',
64 | default=None
65 | )
66 |
67 | args = parser.parse_args(argv)
68 |
69 | user = False
70 | prefix = None
71 | if args.sys_prefix:
72 | prefix = sys.prefix
73 | elif args.prefix:
74 | prefix = args.prefix
75 | elif args.user or not _is_root():
76 | user = True
77 |
78 | install_my_kernel_spec(user=user, prefix=prefix)
79 |
80 |
81 | if __name__ == '__main__':
82 | main()
83 |
--------------------------------------------------------------------------------
/tests/usage_connect.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "source": [
7 | "# 请将 \"192.168.0.156\" 修改成设备的 IP 地址,即可连接你的硬件。\r\n",
8 | "$connect(\"192.168.0.156\")\r\n",
9 | "# 推荐使用 adb forward tcp:18812 tcp:18812 && adb forward tcp:18812 tcp:18812 配置到本机自动连接。\r\n",
10 | "\r\n",
11 | "# 连接成功会显示远端机器的硬件信息。\r\n",
12 | "import platform\r\n",
13 | "print(platform.uname())"
14 | ],
15 | "outputs": [
16 | {
17 | "output_type": "stream",
18 | "name": "stdout",
19 | "text": [
20 | "uname_result(system='Linux', node='sipeed', release='4.9.118', version='#110 PREEMPT Fri Dec 25 08:16:04 UTC 2020', machine='armv7l', processor='')\n"
21 | ]
22 | }
23 | ],
24 | "metadata": {}
25 | },
26 | {
27 | "cell_type": "code",
28 | "execution_count": 1,
29 | "source": [
30 | "import platform"
31 | ],
32 | "outputs": [],
33 | "metadata": {}
34 | },
35 | {
36 | "cell_type": "code",
37 | "execution_count": 1,
38 | "source": [
39 | "print(platform.uname())"
40 | ],
41 | "outputs": [
42 | {
43 | "output_type": "stream",
44 | "name": "stdout",
45 | "text": [
46 | "uname_result(system='Linux', node='sipeed', release='4.9.118', version='#110 PREEMPT Fri Dec 25 08:16:04 UTC 2020', machine='armv7l', processor='')\n"
47 | ]
48 | }
49 | ],
50 | "metadata": {}
51 | },
52 | {
53 | "cell_type": "code",
54 | "execution_count": 1,
55 | "source": [
56 | "$connect(\"192.168.0.156\")\r\n",
57 | "# 当它重新连接以后环境是干净的,所以会报 name 'platform' is not defined 。\r\n",
58 | "print(platform.uname())"
59 | ],
60 | "outputs": [
61 | {
62 | "output_type": "stream",
63 | "name": "stderr",
64 | "text": [
65 | "2021-01-21 14:27:38,508 - rpyc_ikernel - ERROR - name 'platform' is not defined\n",
66 | "\n",
67 | "========= Remote Traceback (1) =========\n",
68 | "Traceback (most recent call last):\n",
69 | " File \"/usr/lib/python3.8/site-packages/rpyc/core/protocol.py\", line 324, in _dispatch_request\n",
70 | " res = self._HANDLERS[handler](self, *args)\n",
71 | " File \"/usr/lib/python3.8/site-packages/rpyc/core/protocol.py\", line 592, in _handle_call\n",
72 | " return obj(*args, **dict(kwargs))\n",
73 | " File \"/usr/lib/python3.8/site-packages/rpyc/core/service.py\", line 152, in execute\n",
74 | " execute(text, self.namespace)\n",
75 | " File \"\", line 3, in \n",
76 | "NameError: name 'platform' is not defined\n",
77 | "\n"
78 | ]
79 | }
80 | ],
81 | "metadata": {}
82 | },
83 | {
84 | "cell_type": "code",
85 | "execution_count": 1,
86 | "source": [
87 | "import platform"
88 | ],
89 | "outputs": [],
90 | "metadata": {}
91 | },
92 | {
93 | "cell_type": "code",
94 | "execution_count": 1,
95 | "source": [
96 | "print(platform.uname())"
97 | ],
98 | "outputs": [
99 | {
100 | "output_type": "stream",
101 | "name": "stdout",
102 | "text": [
103 | "uname_result(system='Linux', node='sipeed', release='4.9.118', version='#110 PREEMPT Fri Dec 25 08:16:04 UTC 2020', machine='armv7l', processor='')\n"
104 | ]
105 | }
106 | ],
107 | "metadata": {}
108 | },
109 | {
110 | "cell_type": "code",
111 | "execution_count": null,
112 | "source": [],
113 | "outputs": [],
114 | "metadata": {}
115 | }
116 | ],
117 | "metadata": {
118 | "celltoolbar": "编辑元数据",
119 | "kernelspec": {
120 | "display_name": "RPyc-Python",
121 | "language": "Python",
122 | "name": "rpyc"
123 | },
124 | "language_info": {
125 | "codemirror_mode": "python",
126 | "file_extension": ".py",
127 | "mimetype": "text/python",
128 | "name": "Python"
129 | }
130 | },
131 | "nbformat": 4,
132 | "nbformat_minor": 4
133 | }
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | test_cli.py
5 |
6 | Check that the output of rpyc_ikernel is as expected when run as a
7 | commandline script.
8 |
9 | """
10 |
11 | import sys
12 |
13 | import pytest
14 |
15 | # pytest tries to include this if Test is kept in the name
16 | from scripttest import TestFileEnvironment as Env
17 |
18 | # Generic just for testing
19 | env = Env()
20 |
21 |
22 | def test_launcher_doing_nothing():
23 | """Run without arguments."""
24 | # This says error because there are no arguments -- that's fine.
25 | result = env.run("rpyc_ikernel", expect_error=True)
26 | assert result.returncode != 0
27 | # Help is useless, really
28 | result = env.run("rpyc_ikernel", "--help")
29 | assert "usage: rpyc_ikernel" in result.stdout
30 |
31 |
32 | def test_launcher_version():
33 | """Launcher not manager."""
34 | # version goes to stderr in PY2, stdout in PY3
35 | result = env.run("rpyc_ikernel", "-V", expect_stderr=True)
36 | assert "Remote Jupyter kernel launcher" in result.stdout + result.stderr
37 |
38 |
39 | def test_manage_doing_nothing():
40 | """Manage is different to the kernel itself."""
41 | # Does not error. Gives help.
42 | result = env.run("rpyc_ikernel", "manage")
43 | assert "usage" in result.stdout
44 | result = env.run("rpyc_ikernel", "manage", "--help")
45 | assert "usage" in result.stdout
46 |
47 |
48 | def test_manage_basics():
49 | result = env.run("rpyc_ikernel", "manage", "-V", expect_stderr=True)
50 | assert "Remote Jupyter kernel manager" in result.stdout + result.stderr
51 | # Show will not error, even with no kernels
52 | result = env.run("rpyc_ikernel", "manage", "--show")
53 |
54 |
55 | def test_minimum_args():
56 | # interface, kernel_cmd and name are required!
57 | result = env.run(
58 | "rpyc_ikernel",
59 | "manage",
60 | "--add",
61 | "--kernel_cmd=command",
62 | "--name=name",
63 | expect_error=True,
64 | )
65 | assert "interface must be specified" in result.stderr
66 | result = env.run(
67 | "rpyc_ikernel",
68 | "manage",
69 | "--add",
70 | "--interface=local",
71 | "--name=name",
72 | expect_error=True,
73 | )
74 | assert "kernel_cmd is required" in result.stderr
75 | result = env.run(
76 | "rpyc_ikernel",
77 | "manage",
78 | "--add",
79 | "--interface=local",
80 | "--kernel_cmd=command",
81 | expect_error=True,
82 | )
83 | assert "name is required for kernel" in result.stderr
84 | result = env.run(
85 | "rpyc_ikernel",
86 | "manage",
87 | "--add",
88 | "--interface=local",
89 | "--kernel_cmd=command",
90 | "--name=name",
91 | expect_error=False,
92 | )
93 | assert "Added kernel" in result.stdout
94 | result = env.run("rpyc_ikernel", "manage", "--delete", "rpyc_local_name")
95 | assert "Removed kernel" in result.stdout
96 |
97 |
98 | @pytest.mark.skipif(
99 | sys.platform == "win32", reason="Unicode not working in windows terminal."
100 | )
101 | def test_unicode():
102 | created = []
103 | # Create a unicode containing kernel with unicode in every field possible
104 | result = env.run(
105 | "rpyc_ikernel",
106 | "manage",
107 | "--add",
108 | "--interface=local",
109 | "--kernel_cmd=command ☂",
110 | "--name=name ☂",
111 | "--language=lang ☂",
112 | "--pe=pe ☂",
113 | "--host=host ☂",
114 | "--workdir=dir ☂",
115 | "--remote-precmd=pre ☂",
116 | "--launch-cmd=launch ☂",
117 | "--remote-launch-args=rla ☂",
118 | "--tunnel-hosts=th ☂",
119 | )
120 | assert u"\u2602" in result.stdout
121 | result = env.run("rpyc_ikernel", "manage", "--show")
122 | assert u"\u2602" in result.stdout
123 | result = env.run(
124 | "rpyc_ikernel", "manage", "--delete", "rpyc_local_name_launch_pe___via_th__"
125 | )
126 | assert "Removed kernel" in result.stdout
127 |
--------------------------------------------------------------------------------
/tests/test_command_fix.py:
--------------------------------------------------------------------------------
1 | """
2 | test_command_fix.py
3 |
4 | Check the special cases where the kernel command needs fixing up.
5 |
6 | """
7 |
8 | from rpyc_ikernel.manage import command_fix
9 |
10 |
11 | def test_irkernel_unescaped(capsys):
12 | """Check that it gets escaped."""
13 | kernel_command = (
14 | "/usr/lib64/R/bin/R --slave -e IRkernel::main() --args {connection_file}"
15 | )
16 | fixed_kernel_command = (
17 | "/usr/lib64/R/bin/R --slave -e 'IRkernel::main()' --args {connection_file}"
18 | )
19 | assert command_fix(kernel_command) != kernel_command
20 | out, _err = capsys.readouterr()
21 | assert "Escaping" in out
22 | assert command_fix(kernel_command) == fixed_kernel_command
23 |
24 |
25 | def test_irkernel_already_escaped():
26 | """Don't escape if it should already work."""
27 | # single quotes
28 | kernel_command = (
29 | "/usr/lib64/R/bin/R --slave -e 'IRkernel::main()' --args {connection_file}"
30 | )
31 | assert command_fix(kernel_command) == kernel_command
32 | # double quotes
33 | kernel_command = (
34 | '/usr/lib64/R/bin/R --slave -e "IRkernel::main()" --args {connection_file}'
35 | )
36 | assert command_fix(kernel_command) == kernel_command
37 | # escaped
38 | kernel_command = (
39 | r"/usr/lib64/R/bin/R --slave -e IRkernel::main\(\) --args {connection_file}"
40 | )
41 | assert command_fix(kernel_command) == kernel_command
42 |
43 |
44 | def test_unescaped_brackets(capsys):
45 | """Warn if there are brackets """
46 | # unescaped cases
47 | command_fix("one two three ( four five six")
48 | out, _err = capsys.readouterr()
49 | assert "unescaped brackets" in out
50 |
51 | command_fix("one two three ) four five six")
52 | out, _err = capsys.readouterr()
53 | assert "unescaped brackets" in out
54 |
55 | command_fix("one two three (\) four five six")
56 | out, _err = capsys.readouterr()
57 | assert "unescaped brackets" in out
58 |
59 |
60 | def test_escaped_brackets(capsys):
61 | """Don't warn if it seems fine."""
62 | # escaped cases
63 | command_fix("one two three \(\) four five six")
64 | out, _err = capsys.readouterr()
65 | assert "unescaped brackets" not in out
66 |
67 | command_fix("one two three '()' four five six")
68 | out, _err = capsys.readouterr()
69 | assert "unescaped brackets" not in out
70 |
71 | command_fix('one two three "()" four five six')
72 | out, _err = capsys.readouterr()
73 | assert "unescaped brackets" not in out
74 |
75 |
76 | def test_missing_quotations(capsys):
77 | """Warn if command seems broken."""
78 | # Broken
79 | command_fix("one two three ' four five six")
80 | out, _err = capsys.readouterr()
81 | assert "missing quotation marks" in out
82 | command_fix('one two three " four five six')
83 | out, _err = capsys.readouterr()
84 | assert "missing quotation marks" in out
85 | # Fine
86 | command_fix("one two three ' four ' five six")
87 | out, _err = capsys.readouterr()
88 | assert "missing quotation marks" not in out
89 | command_fix('one two three " four " five six')
90 | out, _err = capsys.readouterr()
91 | assert "missing quotation marks" not in out
92 |
93 |
94 | # Test when running from the commandline
95 | # pytest tries to include this if Test is kept in the name
96 | from scripttest import TestFileEnvironment as Env
97 |
98 | # Generic just for testing
99 | env = Env()
100 |
101 |
102 | def test_fix_in_manage():
103 | # interface, kernel_cmd and name are required...
104 | # Break kernel_cmd
105 | result = env.run(
106 | "rpyc_ikernel",
107 | "manage",
108 | "--add",
109 | "--interface=local",
110 | "--kernel_cmd=something IRkernel::main()",
111 | "--name=name",
112 | )
113 | assert "Escaping IRkernel" in result.stdout
114 |
115 | result = env.run(
116 | "rpyc_ikernel",
117 | "manage",
118 | "--add",
119 | "--interface=local",
120 | "--kernel_cmd=something unescaped()",
121 | "--name=name",
122 | )
123 | assert "unescaped brackets" in result.stdout
124 |
125 | result = env.run(
126 | "rpyc_ikernel",
127 | "manage",
128 | "--add",
129 | "--interface=local",
130 | "--kernel_cmd=\"something badly quoted'",
131 | "--name=name",
132 | )
133 | assert "missing quotation marks" in result.stdout
134 |
135 | result = env.run("rpyc_ikernel", "manage", "--delete", "rpyc_local_name")
136 | assert "Removed kernel" in result.stdout
137 |
--------------------------------------------------------------------------------
/README_cn.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
RPyC の IPykernel
4 |
5 |
6 | [](./LICENSE)
7 | [](https://badge.fury.io/py/rpyc-ikernel)
8 |
9 | [English Readme](README.md)
10 |
11 | ## 内核安装方式
12 |
13 | - pip install rpyc_ikernel
14 | - python -m rpyc_ikernel.install
15 |
16 | 
17 |
18 | ## 内核简介
19 |
20 | 本内核继承了IPythonKernel(iPython)类并且支持了低端的硬件(armv7l),能在占用不超过16MB下运行Python编程、实时的图像和视频流
21 |
22 | - 通过[rpyc](https://github.com/tomerfiliba-org/rpyc)实现了远程访问(RPC)内核,详见此[文档](http://rpyc.readthedocs.org/)
23 |
24 | - 为运行[MaixPy3](https://github.com/sipeed/MaixPy3)的远端机器建立了一个RPC服务,来在本地和远程设备之间传输代码并显示图像和结果
25 |
26 | ### 特殊的函数
27 |
28 | | 命令格式 | 命令用途 | 命令用法 |
29 | | ---- | ---- | ---- |
30 | | $connect("localhost") | 连接到远程的IP (例如: "192.168.44.171:18812") | [usage_connect.ipynb](./tests/usage_connect.ipynb) |
31 |
32 | ## 安装方法
33 |
34 | 按以下顺序介绍
35 |
36 | - 为[远端 Python]配置rpyc服务
37 | - 为[本地 Python]配置jupyter环境
38 |
39 | ## 为[远端 Python]配置rpyc服务.
40 |
41 | 在远端设备上使用**ifconfig** 或 **ipconfig**命令获取它的IP地址,并且确保这个地址能被**ping**到
42 |
43 | ### MaixPy3 系列
44 |
45 | 确保远端设备已经被配置为了**Python3**环境,输入`pip3 install maixpy3`来安装**rpyc**服务,拷贝以下命令并且运行来启动服务
46 |
47 | ```shell
48 | python -c "import maix.mjpg;maix.mjpg.start()"
49 | ```
50 |
51 | ### 其他环境
52 |
53 | 以下代码可以提供一个远程访问环境
54 |
55 | ```python
56 | try:
57 | from rpyc.utils.server import ThreadedServer
58 | from rpyc.core.service import SlaveService
59 | rpyc_server = ThreadedServer(
60 | SlaveService, hostname=HostName, port=RpycPort, reuse_addr=True)
61 | rpyc_server.start()
62 | except OSError as e:
63 | # logging.debug('[%s] OSError: %s' % (__file__, str(e))) # [Errno 98] Address already in use
64 | exit(0)
65 | ```
66 |
67 | 现在你的rpyc服务已经配置好了
68 |
69 | ## 为[本地 Python]配置jupyter环境
70 |
71 | 以Python3为例,请确保安装了基本的python3和pip3环境和命令行。只需要运行下面这一行代码。
72 |
73 | ```shell
74 | pip3 install rpyc_ikernel && python3 -m rpyc_ikernel.install
75 | ```
76 |
77 | 大陆的用户可以使用清华源来加速
78 |
79 | ```shell
80 | pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple rpyc_ikernel && python3 -m rpyc_ikernel.install
81 | ```
82 |
83 | 装完以上的包之后,可以通过输入`jupyter notebook`来启动服务。启动之后,系统默认的浏览器会自动打开(建议使用Chrome和360浏览器极速版)。请选择rpyc内核并新建一个使用该内核的文件
84 |
85 | 
86 |
87 | 如果你没有看见它,你可以输入`python3 -m rpyc_ikernel.install`来完成内核的安装,然后就可以看到了。
88 |
89 | ### 对于 Windows
90 |
91 | 输入`python -m rpyc_ikernel.install`来完成内核的安装
92 |
93 | 如果有如下常见的找不到模块的错误,它们常见于py2和py3混装的环境,请检查用的python和pip的版本和环境变量。
94 |
95 | - `/usr/bin/python3: Error while finding module specification for'rpyc-ikernel.install' (ModuleNotFoundError: No module named'rpyc-ikernel')`
96 | - `/usr/bin/python: No module named rpyc-ikernel`
97 |
98 | > 对于有些机器的环境,python3被命名为python;或者存在多个版本的python和pip,那么此时需要正确选择使用的python命令。
99 |
100 | 你可以输入`jupyter kernelspec list`来检查正确安装的jupyter内核。如果这里没有rpyc,那么这个内核就还没被安装。
101 |
102 | ```shell
103 | Available kernels:
104 | bash /home/juwan/.local/share/jupyter/kernels/bash
105 | micropython /home/juwan/.local/share/jupyter/kernels/micropython
106 | python3 /home/juwan/.local/share/jupyter/kernels/python3
107 | rpyc /home/juwan/.local/share/jupyter/kernels/rpyc
108 | ```
109 |
110 | ## 在Jupyter NoteBook中运行Python
111 |
112 | 在运行代码之前,清配置ip地址以连接,否则,它会默认地连接到"localhost"地址请求服务。
113 |
114 | ```python
115 | $connect("192.168.43.44")
116 | import platform
117 | print(platform.uname())
118 | ```
119 |
120 | 然后就可看到如下的返回结果
121 |
122 | ```shell
123 | uname_result(system='Linux', node='linux-lab', release='5.4.0-56-generic', version='#62-Ubuntu SMP Mon Nov 23 19:20:19 UTC 2020', machine= 'x86_64', processor='x86_64')
124 | ```
125 |
126 | ## 常见问题
127 |
128 | 你可以按以下顺序排除问题:
129 |
130 | ### 环境问题
131 |
132 | 当您发现执行Python代码后没有响应时,可以按照以下步骤进行错误诊断。
133 |
134 | - 检查远程设备的rpyc服务是否存在/运行。 (使用ps -a命令)
135 | - 如果在代码仍在运行时按中断按钮无法停止,请刷新代码页或重新启动内核并再次尝试执行代码。
136 | - 重新启动jupyter服务并重新连接到远程设备以执行代码。
137 |
138 | 如果仍不工作,可能是网络问题,请继续进行故障排除。
139 |
140 | ### 网络问题
141 |
142 | 确保本地设备可以连接到远程设备,并能使用Ping或套接字(socket)进行连接。
143 |
144 | - 确定设备所属的网络,并尝试ping从机的IP地址。
145 | - 确定远程端所属的网络,并尝试ping主机IP地址。
146 | - 确保上层路由器的转发规则不限制服务端口18811、18812、18813。
147 |
148 | ### 其他问题
149 |
150 | 拔下网络电缆或重新启动机器,重置硬件和其他重置操作。
151 |
152 | ## 设计灵感
153 |
154 | 内核设计来自以下Python存储库
155 |
156 | - [maixpy3](https://github.com/sipeed/maixpy3)
157 | - [ipykernel](https://github.com/ipython/ipykernel)
158 | - [rpyc](https://github.com/tomerfiliba-org/rpyc)
159 |
160 | 参考内核如下
161 |
162 | - [bash_kernel](https://github.com/takluyver/bash_kernel)
163 | - [ubit_kernel](https://github.com/takluyver/ubit_kernel)
164 | - [remote_ikernel](https://github.com/tdaff/remote_ikernel)
165 | - [jupyter_micropython_kernel](https://github.com/goatchurchprime/jupyter_micropython_kernel)
166 |
--------------------------------------------------------------------------------
/tests/test_ugly-code.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {
7 | "scrolled": true
8 | },
9 | "outputs": [
10 | {
11 | "name": "stdout",
12 | "output_type": "stream",
13 | "text": [
14 | "[ rpyc-kernel ]( running at Wed Dec 1 16:21:01 2021 )\n",
15 | "uname_result(system='Linux', node='sipeed', release='4.9.118', version='#2502 PREEMPT Wed Dec 1 03:11:00 UTC 2021', machine='armv7l', processor='')\n",
16 | "Thu Jan 1 01:10:14 1970\n"
17 | ]
18 | }
19 | ],
20 | "source": [
21 | "import platform\n",
22 | "print(platform.uname())\n",
23 | "import time\n",
24 | "print(time.asctime())"
25 | ]
26 | },
27 | {
28 | "cell_type": "code",
29 | "execution_count": 1,
30 | "metadata": {},
31 | "outputs": [
32 | {
33 | "name": "stdout",
34 | "output_type": "stream",
35 | "text": [
36 | "[ rpyc-kernel ]( running at Wed Dec 1 16:21:05 2021 )\n",
37 | "test\n"
38 | ]
39 | },
40 | {
41 | "data": {
42 | "text/plain": [
43 | "_get_exception_class..Derived: HELLO\n",
44 | "\n",
45 | "========= Remote Traceback (1) =========\n",
46 | "Traceback (most recent call last):\n",
47 | " File \"/usr/lib/python3.8/site-packages/rpyc/core/protocol.py\", line 324, in _dispatch_request\n",
48 | " File \"/usr/lib/python3.8/site-packages/rpyc/core/protocol.py\", line 592, in _handle_call\n",
49 | " File \"\", line 2, in \n",
50 | "Exception: HELLO\n",
51 | "\n"
52 | ]
53 | },
54 | "execution_count": 1,
55 | "metadata": {},
56 | "output_type": "execute_result"
57 | }
58 | ],
59 | "source": [
60 | "print('test')\n",
61 | "raise Exception('HELLO')"
62 | ]
63 | },
64 | {
65 | "cell_type": "code",
66 | "execution_count": 1,
67 | "metadata": {},
68 | "outputs": [
69 | {
70 | "name": "stdout",
71 | "output_type": "stream",
72 | "text": [
73 | "[ rpyc-kernel ]( running at Wed Dec 1 16:21:07 2021 )\n"
74 | ]
75 | },
76 | {
77 | "data": {
78 | "text/plain": [
79 | "_get_exception_class..Derived: division by zero\n",
80 | "\n",
81 | "========= Remote Traceback (1) =========\n",
82 | "Traceback (most recent call last):\n",
83 | " File \"/usr/lib/python3.8/site-packages/rpyc/core/protocol.py\", line 324, in _dispatch_request\n",
84 | " File \"/usr/lib/python3.8/site-packages/rpyc/core/protocol.py\", line 592, in _handle_call\n",
85 | " File \"\", line 1, in \n",
86 | "ZeroDivisionError: division by zero\n",
87 | "\n"
88 | ]
89 | },
90 | "execution_count": 1,
91 | "metadata": {},
92 | "output_type": "execute_result"
93 | }
94 | ],
95 | "source": [
96 | "255 / 0"
97 | ]
98 | },
99 | {
100 | "cell_type": "code",
101 | "execution_count": 1,
102 | "metadata": {},
103 | "outputs": [
104 | {
105 | "name": "stdout",
106 | "output_type": "stream",
107 | "text": [
108 | "[ rpyc-kernel ]( running at Wed Dec 1 16:24:54 2021 )\n",
109 | "4448.158232867\n",
110 | "4449.184878576\n",
111 | "4450.213568327\n",
112 | "4451.241270619\n",
113 | "4452.269931078\n"
114 | ]
115 | }
116 | ],
117 | "source": [
118 | "\n",
119 | "import time\n",
120 | "for i in range(5):\n",
121 | " time.sleep(1)\n",
122 | " print(time.time())\n"
123 | ]
124 | },
125 | {
126 | "cell_type": "code",
127 | "execution_count": 1,
128 | "metadata": {},
129 | "outputs": [
130 | {
131 | "name": "stdout",
132 | "output_type": "stream",
133 | "text": [
134 | "[ rpyc-kernel ]( running at Wed Dec 1 16:25:07 2021 )\n"
135 | ]
136 | },
137 | {
138 | "data": {
139 | "text/plain": [
140 | "\r\n",
141 | "Traceback (most recent call last):\r\n",
142 | " File \"\", line unknown, in \r\n",
143 | "Remote.KeyboardInterrupt\r\n"
144 | ]
145 | },
146 | "execution_count": 1,
147 | "metadata": {},
148 | "output_type": "execute_result"
149 | }
150 | ],
151 | "source": [
152 | "import time\n",
153 | "while True:\n",
154 | " time.sleep(1)"
155 | ]
156 | },
157 | {
158 | "cell_type": "code",
159 | "execution_count": 1,
160 | "metadata": {},
161 | "outputs": [
162 | {
163 | "name": "stdout",
164 | "output_type": "stream",
165 | "text": [
166 | "[ rpyc-kernel ]( running at Wed Dec 1 16:25:14 2021 )\n"
167 | ]
168 | },
169 | {
170 | "data": {
171 | "text/plain": [
172 | "\r\n",
173 | "Traceback (most recent call last):\r\n",
174 | " File \"\", line unknown, in \r\n",
175 | "Remote.KeyboardInterrupt\r\n"
176 | ]
177 | },
178 | "execution_count": 1,
179 | "metadata": {},
180 | "output_type": "execute_result"
181 | }
182 | ],
183 | "source": [
184 | "while True:\n",
185 | " pass"
186 | ]
187 | },
188 | {
189 | "cell_type": "code",
190 | "execution_count": null,
191 | "metadata": {},
192 | "outputs": [],
193 | "source": []
194 | }
195 | ],
196 | "metadata": {
197 | "kernelspec": {
198 | "display_name": "RPyc-Python",
199 | "language": "Python",
200 | "name": "rpyc"
201 | },
202 | "language_info": {
203 | "codemirror_mode": "python",
204 | "file_extension": ".py",
205 | "mimetype": "text/python",
206 | "name": "Python"
207 | }
208 | },
209 | "nbformat": 4,
210 | "nbformat_minor": 4
211 | }
212 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
RPyC の IPykernel
4 |
5 |
6 | [](./LICENSE)
7 | [](https://badge.fury.io/py/rpyc-ikernel)
8 |
9 | [中文文档](README_cn.md)
10 |
11 | ## Kernel installation
12 |
13 | - pip install rpyc_ikernel
14 | - python -m rpyc_ikernel.install
15 |
16 | 
17 |
18 | ## Kernel introduction
19 |
20 | Inherit the IPythonKernel (iPython) class, and support low-end hardware (armv7l) for Python programming and real-time image and video streaming with less occupation 16M.
21 |
22 | - Implement remote call (RPC) core through [rpyc](https://github.com/tomerfiliba-org/rpyc), see this [docs](http://rpyc.readthedocs.org/).
23 |
24 | - Set up an RPC service for the remote machine with [MaixPy3](https://github.com/sipeed/MaixPy3) and transfer the local code to the remote (remote) to run & display images & get results.
25 |
26 | ### Special functions
27 |
28 | | Command format | Command function | How to use |
29 | | ---- | ---- | ---- |
30 | | $connect("localhost") | Connect to the remote IP address (for example: "192.168.44.171:18812") | [usage_connect.ipynb](./tests/usage_connect.ipynb) |
31 |
32 | ## installation method
33 |
34 | Explain in the following order:
35 |
36 | - Configure rpyc service for [Remote Python].
37 | - Install jupyter environment for [Local Python].
38 |
39 | ## Configure rpyc service for [Remote Python].
40 |
41 | Use **ifconfig** or **ipconfig** on your remote device to get your IP address, please make sure that the address can be **ping**.
42 |
43 | ### MaixPy3 series
44 |
45 | Make sure that the remote device is configured in the **Python3** environment, enter `pip3 install maixpy3` to install the **rpyc** service, copy the following command and run it to start the service.
46 |
47 | ```shell
48 | python -c "import maix.mjpg;maix.mjpg.start()"
49 | ```
50 |
51 | ### Other environmental
52 |
53 | The following code provides the remote call environment.
54 |
55 | ```python
56 | try:
57 | from rpyc.utils.server import ThreadedServer
58 | from rpyc.core.service import SlaveService
59 | rpyc_server = ThreadedServer(
60 | SlaveService, hostname=HostName, port=RpycPort, reuse_addr=True)
61 | rpyc_server.start()
62 | except OSError as e:
63 | # logging.debug('[%s] OSError: %s' % (__file__, str(e))) # [Errno 98] Address already in use
64 | exit(0)
65 | ```
66 |
67 | At this time your rpyc service has been up.
68 |
69 | ## Install jupyter environment for [local Python].
70 |
71 | Take Python3 as an example, please make sure that the basic environment/commands of python3 and pip3 have been installed, just call the code under the command line.
72 |
73 | ```shell
74 | pip3 install rpyc_ikernel && python3 -m rpyc_ikernel.install
75 | ```
76 |
77 | Tsinghua source can be used for domestic download acceleration.
78 |
79 | ```shell
80 | pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple rpyc_ikernel && python3 -m rpyc_ikernel.install
81 | ```
82 |
83 | After the above package is installed, enter `jupyter notebook` to start the service. After startup, the system default browser will be opened automatically (recommended foreign Google browser or domestic 360 speed browser), please select the kernel of rpyc and create a new (new) designation The code file of the kernel.
84 |
85 | 
86 |
87 | If you don't see it, you can enter `python3 -m rpyc_ikernel.install` to complete the kernel installation, and you can see it now.
88 |
89 | ### On Windows
90 |
91 | enter `python -m rpyc_ikernel.install` to complete the kernel installation.
92 |
93 | If there are the following common errors that cannot find modules, they are common in py2 and py3 environments. Please confirm whether the system environment variable is python / pip command.
94 |
95 | - `/usr/bin/python3: Error while finding module specification for'rpyc-ikernel.install' (ModuleNotFoundError: No module named'rpyc-ikernel')`
96 | - `/usr/bin/python: No module named rpyc-ikernel`
97 |
98 | > For some machine environment variables, python3 is python, or multiple versions of python and pip coexist, then the python command is required at this time.
99 |
100 | You can enter `jupyter kernelspec list` to view the currently installed jupyter kernel. If there is no rpyc, the kernel is not installed.
101 |
102 | ```shell
103 | Available kernels:
104 | bash /home/juwan/.local/share/jupyter/kernels/bash
105 | micropython /home/juwan/.local/share/jupyter/kernels/micropython
106 | python3 /home/juwan/.local/share/jupyter/kernels/python3
107 | rpyc /home/juwan/.local/share/jupyter/kernels/rpyc
108 | ```
109 |
110 | ## Run Python code in Notebook
111 |
112 | Before running the code, please configure the IP address to connect, otherwise it will connect to the "localhost" address to request the service by default.
113 |
114 | ```python
115 | $connect("192.168.43.44")
116 | import platform
117 | print(platform.uname())
118 | ```
119 |
120 | It can be seen that the returned results are as follows:
121 |
122 | ```shell
123 | uname_result(system='Linux', node='linux-lab', release='5.4.0-56-generic', version='#62-Ubuntu SMP Mon Nov 23 19:20:19 UTC 2020', machine= 'x86_64', processor='x86_64')
124 | ```
125 |
126 | ## common problem
127 |
128 | You can troubleshoot problems in the following order:
129 |
130 | ### Environmental issues
131 |
132 | When you find that there is no response after the Python code is executed, you can follow the steps below to troubleshoot the error.
133 |
134 | - Check if the rpyc service of the remote device exists/runs. (Ps -a)
135 | - If pressing the interrupt button fails to stop while the code is still running, please refresh the code page or restart the kernel and try to execute the code again.
136 | - Restart the jupyter service and reconnect to the remote device to execute code.
137 |
138 | If it still does not work, it may be a network problem, so continue to troubleshoot.
139 |
140 | ### Internet problem
141 |
142 | Make sure that the local machine can connect to the remote machine, and use Ping or socket to connect.
143 |
144 | - Determine the network to which the machine belongs, and try to ping the IP address of the slave machine.
145 | - Determine the network to which the remote end belongs, and try to ping the host IP address.
146 | - Make sure that the forwarding rules of the upper router do not restrict the service ports 18811, 18812, 18813.
147 |
148 | ### other problems
149 |
150 | Unplug the network cable or restart the machine, reset the hardware and other reset operations.
151 |
152 | ## Design inspiration
153 |
154 | The kernel design is taken from the following Python repository.
155 |
156 | - [maixpy3](https://github.com/sipeed/maixpy3)
157 | - [ipykernel](https://github.com/ipython/ipykernel)
158 | - [rpyc](https://github.com/tomerfiliba-org/rpyc)
159 |
160 | The reference kernel is as follows.
161 |
162 | - [bash_kernel](https://github.com/takluyver/bash_kernel)
163 | - [ubit_kernel](https://github.com/takluyver/ubit_kernel)
164 | - [remote_ikernel](https://github.com/tdaff/remote_ikernel)
165 | - [jupyter_micropython_kernel](https://github.com/goatchurchprime/jupyter_micropython_kernel)
166 |
--------------------------------------------------------------------------------
/tests/test_all.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {
7 | "scrolled": true
8 | },
9 | "outputs": [
10 | {
11 | "name": "stdout",
12 | "output_type": "stream",
13 | "text": [
14 | "[ rpyc-kernel ]( running at Wed Dec 8 17:39:11 2021 )\n"
15 | ]
16 | }
17 | ],
18 | "source": [
19 | "while True:\n",
20 | " pass"
21 | ]
22 | },
23 | {
24 | "cell_type": "code",
25 | "execution_count": null,
26 | "metadata": {},
27 | "outputs": [
28 | {
29 | "name": "stdout",
30 | "output_type": "stream",
31 | "text": [
32 | "[ rpyc-kernel ]( running at Fri Dec 10 05:14:22 2021 )\n"
33 | ]
34 | }
35 | ],
36 | "source": [
37 | "from maix import camera, image, display\n",
38 | "\n",
39 | "while True:\n",
40 | " tmp = camera.capture()\n",
41 | " ma = tmp.find_blobs([(28,-36,-14,68,-5,15)])\n",
42 | " if ma:\n",
43 | " for i in ma:\n",
44 | " tmp.draw_rectangle(i[\"x\"], i[\"y\"], i[\"x\"] + i[\"w\"], i[\"y\"] + i[\"h\"], (255, 0, 0), 1)\n",
45 | " display.show(tmp)"
46 | ]
47 | },
48 | {
49 | "cell_type": "code",
50 | "execution_count": null,
51 | "metadata": {},
52 | "outputs": [],
53 | "source": []
54 | },
55 | {
56 | "cell_type": "code",
57 | "execution_count": null,
58 | "metadata": {
59 | "scrolled": false
60 | },
61 | "outputs": [],
62 | "source": [
63 | "from maix import display, camera\n",
64 | "while True:\n",
65 | " display.show(camera.capture())"
66 | ]
67 | },
68 | {
69 | "cell_type": "code",
70 | "execution_count": null,
71 | "metadata": {},
72 | "outputs": [],
73 | "source": [
74 | "\n",
75 | "from PIL import Image\n",
76 | "from PIL import ImageDraw\n",
77 | "from PIL import ImageFont\n",
78 | "\n",
79 | "import time, random\n",
80 | "\n",
81 | "from maix import display\n",
82 | "\n",
83 | "image = Image.new(\"RGB\", (120, 120), (0, 255, 0))# (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))\n",
84 | "draw = ImageDraw.Draw(image)\n",
85 | "\n",
86 | "draw.line((20, 20, 100, 100), 'cyan')\n",
87 | "\n",
88 | "display.show(image)\n"
89 | ]
90 | },
91 | {
92 | "cell_type": "code",
93 | "execution_count": null,
94 | "metadata": {},
95 | "outputs": [],
96 | "source": [
97 | "\n",
98 | "from PIL import Image\n",
99 | "from PIL import ImageDraw\n",
100 | "from PIL import ImageFont\n",
101 | "\n",
102 | "import time, random\n",
103 | "\n",
104 | "from maix import display\n",
105 | "\n",
106 | "image = Image.new(\"RGB\", (120, 120), (255, 0, 0))# (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))\n",
107 | "draw = ImageDraw.Draw(image)\n",
108 | "\n",
109 | "draw.line((20, 20, 100, 100), 'cyan')\n",
110 | "\n",
111 | "display.show(image)\n"
112 | ]
113 | },
114 | {
115 | "cell_type": "code",
116 | "execution_count": null,
117 | "metadata": {},
118 | "outputs": [],
119 | "source": [
120 | "\n",
121 | "from PIL import Image\n",
122 | "from PIL import ImageDraw\n",
123 | "from PIL import ImageFont\n",
124 | "\n",
125 | "import time, random\n",
126 | "\n",
127 | "from maix import display\n",
128 | "\n",
129 | "image = Image.new(\"RGB\", (120, 120), (0, 255, 0))# (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))\n",
130 | "draw = ImageDraw.Draw(image)\n",
131 | "\n",
132 | "draw.line((20, 20, 100, 100), 'cyan')\n",
133 | "\n",
134 | "display.show(image)\n"
135 | ]
136 | },
137 | {
138 | "cell_type": "code",
139 | "execution_count": null,
140 | "metadata": {},
141 | "outputs": [],
142 | "source": [
143 | "\n",
144 | "from PIL import Image\n",
145 | "from PIL import ImageDraw\n",
146 | "from PIL import ImageFont\n",
147 | "\n",
148 | "import time, random\n",
149 | "\n",
150 | "from maix import display\n",
151 | "\n",
152 | "image = Image.new(\"RGB\", (120, 120), (255, 0, 0))# (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))\n",
153 | "draw = ImageDraw.Draw(image)\n",
154 | "\n",
155 | "draw.line((20, 20, 100, 100), 'cyan')\n",
156 | "\n",
157 | "display.show(image)\n"
158 | ]
159 | },
160 | {
161 | "cell_type": "code",
162 | "execution_count": null,
163 | "metadata": {},
164 | "outputs": [],
165 | "source": [
166 | "from maix import display, camera\n",
167 | "display.show(camera.capture().resize(100, 100))"
168 | ]
169 | },
170 | {
171 | "cell_type": "code",
172 | "execution_count": null,
173 | "metadata": {
174 | "scrolled": true
175 | },
176 | "outputs": [],
177 | "source": [
178 | "from maix import display, camera\n",
179 | "while True:\n",
180 | " display.show(camera.capture().resize(100, 100))"
181 | ]
182 | },
183 | {
184 | "cell_type": "code",
185 | "execution_count": null,
186 | "metadata": {},
187 | "outputs": [],
188 | "source": [
189 | "from maix import display, camera\n",
190 | "while True:\n",
191 | " display.show(camera.capture().resize(200, 200))"
192 | ]
193 | },
194 | {
195 | "cell_type": "code",
196 | "execution_count": null,
197 | "metadata": {},
198 | "outputs": [],
199 | "source": [
200 | "while True:\n",
201 | " pass"
202 | ]
203 | },
204 | {
205 | "cell_type": "code",
206 | "execution_count": null,
207 | "metadata": {
208 | "scrolled": false
209 | },
210 | "outputs": [],
211 | "source": [
212 | "from maix import display, camera\n",
213 | "while True:\n",
214 | " display.show(camera.capture())"
215 | ]
216 | },
217 | {
218 | "cell_type": "code",
219 | "execution_count": null,
220 | "metadata": {},
221 | "outputs": [],
222 | "source": [
223 | "from maix import display, camera\n",
224 | "camera.config((320, 240))\n",
225 | "while True:\n",
226 | " display.show(camera.capture())"
227 | ]
228 | },
229 | {
230 | "cell_type": "code",
231 | "execution_count": null,
232 | "metadata": {},
233 | "outputs": [],
234 | "source": []
235 | },
236 | {
237 | "cell_type": "code",
238 | "execution_count": null,
239 | "metadata": {},
240 | "outputs": [],
241 | "source": []
242 | },
243 | {
244 | "cell_type": "code",
245 | "execution_count": null,
246 | "metadata": {},
247 | "outputs": [],
248 | "source": []
249 | },
250 | {
251 | "cell_type": "code",
252 | "execution_count": null,
253 | "metadata": {},
254 | "outputs": [],
255 | "source": []
256 | },
257 | {
258 | "cell_type": "code",
259 | "execution_count": null,
260 | "metadata": {},
261 | "outputs": [],
262 | "source": []
263 | },
264 | {
265 | "cell_type": "code",
266 | "execution_count": null,
267 | "metadata": {},
268 | "outputs": [],
269 | "source": []
270 | },
271 | {
272 | "cell_type": "code",
273 | "execution_count": null,
274 | "metadata": {},
275 | "outputs": [],
276 | "source": []
277 | },
278 | {
279 | "cell_type": "code",
280 | "execution_count": null,
281 | "metadata": {},
282 | "outputs": [],
283 | "source": []
284 | },
285 | {
286 | "cell_type": "code",
287 | "execution_count": null,
288 | "metadata": {},
289 | "outputs": [],
290 | "source": []
291 | },
292 | {
293 | "cell_type": "code",
294 | "execution_count": null,
295 | "metadata": {},
296 | "outputs": [],
297 | "source": []
298 | }
299 | ],
300 | "metadata": {
301 | "kernelspec": {
302 | "display_name": "RPyc-Python",
303 | "language": "Python",
304 | "name": "rpyc"
305 | },
306 | "language_info": {
307 | "codemirror_mode": "python",
308 | "file_extension": ".py",
309 | "mimetype": "text/python",
310 | "name": "Python"
311 | }
312 | },
313 | "nbformat": 4,
314 | "nbformat_minor": 4
315 | }
316 |
--------------------------------------------------------------------------------
/rpyc_ikernel/adb.py:
--------------------------------------------------------------------------------
1 |
2 | # Author: Chema Garcia (aka sch3m4)
3 | # Contact: chema@safetybits.net | http://safetybits.net/contact
4 | # Homepage: http://safetybits.net
5 | # Project Site: http://github.com/sch3m4/pyadb
6 |
7 | import sys
8 | import os
9 | import re
10 | import subprocess
11 |
12 |
13 | class ADB():
14 | __adb_path = None
15 | __output = None
16 | __error = None
17 | __return = 0
18 | __device = None
19 | # use device with given serial number (overrides $ANDROID_SERIAL)
20 | __target = None
21 | # -s SERIAL
22 | # use device with given serial number (overrides $ANDROID_SERIAL)
23 | devices = []
24 | try_times = 0
25 |
26 | # reboot modes
27 | REBOOT_RECOVERY = 1
28 | REBOOT_BOOTLOADER = 2
29 |
30 | # default TCP/IP port
31 | DEFAULT_TCP_PORT = 5555
32 | # default TCP/IP host
33 | DEFAULT_TCP_HOST = "localhost"
34 |
35 | def __init__(self, adb_path='adb', device=None):
36 | self.__adb_path = adb_path
37 | if sys.platform.startswith('win'):
38 | exe_path = os.getcwd() + "\\adb\\adb.exe"
39 | # print(self.__adb_path, exe_path)
40 | if os.path.isfile(exe_path):
41 | self.__adb_path = exe_path
42 |
43 | if device:
44 | self.set_target_device(device)
45 | self.connect_check()
46 | return
47 |
48 | if self.devices == None:
49 | self.init_devices()
50 |
51 | if self.devices:
52 | # default use the 1st device [adb -s serial_number]
53 | self.set_target_device(self.devices[0][0])
54 | self.connect_check()
55 |
56 | def connect_check(self):
57 | '''
58 | After we initialied an instance of Adb_Wrapper, we should check if it is
59 | working well. If not, we must initial it again. Considering with the
60 | case that we do not need to restart the adb while we re-initial it, so
61 | we set the global flag 'NEED_RESTART_ADB' to False.
62 | '''
63 | adb_shell_args_test = ['uname']
64 | ret = self.run_shell_cmd(adb_shell_args_test)
65 | if ret is None or len(ret) == 0:
66 | self.try_times += 1
67 | if self.try_times > 3:
68 | # print("It has tried 3 times, please check your devices.")
69 | return False
70 | # print('[W] Init Android_native_debug falied, try again.')
71 | self.__init__()
72 | return False
73 | return True
74 |
75 | def is_emulator(self):
76 | target_dev = self.get_target_device()
77 | if target_dev.find('emulator') > -1:
78 | return True
79 |
80 | return False
81 |
82 | def __clean__(self):
83 | self.__output = None
84 | self.__error = None
85 | self.__return = 0
86 |
87 | def get_output(self):
88 | return self.__output
89 |
90 | def get_error(self):
91 | return self.__error
92 |
93 | def get_return_code(self):
94 | return self.__return
95 |
96 | def last_failed(self):
97 | '''
98 | Did the last command fail?
99 | '''
100 | if self.__output is None and self.__error is not None and self.__return:
101 | return True
102 | return False
103 |
104 | def __build_command__(self, cmd):
105 | ret = None
106 |
107 | if self.__device is not None and self.__target is None:
108 | self.__error = "Must set target device first"
109 | self.__return = 1
110 | return ret
111 |
112 | if sys.platform.startswith('win'):
113 | exe_path = os.getcwd() + "\\adb\\adb.exe"
114 | if os.path.isfile(exe_path):
115 | self.__adb_path = exe_path
116 | ret = self.__adb_path + " "
117 | if self.__target is not None:
118 | ret += "-s " + self.__target + " "
119 | if isinstance(cmd, list):
120 | ret += ' '.join(cmd)
121 | else:
122 | ret += cmd
123 | else:
124 | ret = [self.__adb_path]
125 | if self.__target is not None:
126 | ret += ["-s", self.__target]
127 | for i in cmd:
128 | ret.append(i)
129 |
130 | return ret
131 |
132 | def run_cmd(self, cmd):
133 | '''
134 | Runs a command by using adb tool ($ adb )
135 | cmd have to be a list.
136 | '''
137 | self.__clean__()
138 |
139 | if self.__adb_path is None:
140 | self.__error = "ADB path not set"
141 | self.__return = 1
142 | return
143 |
144 | if not isinstance(cmd, list):
145 | cmd = cmd.split()
146 |
147 | # For compat of windows
148 | cmd_list = self.__build_command__(cmd)
149 |
150 | adb_proc = subprocess.Popen(cmd_list, stdin=subprocess.PIPE,
151 | stdout=subprocess.PIPE,
152 | stderr=subprocess.PIPE,
153 | shell=False)
154 | (self.__output, self.__error) = adb_proc.communicate()
155 | self.__return = adb_proc.returncode
156 |
157 | def run_shell_cmd(self, cmd):
158 | '''
159 | Executes a shell command
160 | adb shell
161 | '''
162 | self.__clean__()
163 | if not isinstance(cmd, list):
164 | cmd = cmd.split()
165 | sh_cmd = cmd.copy()
166 | sh_cmd.insert(0, 'shell')
167 | self.run_cmd(sh_cmd)
168 | return self.__output
169 |
170 | def get_version(self):
171 | '''
172 | Returns ADB tool version
173 | adb version
174 | '''
175 | self.run_cmd("version")
176 | ret = self.__output.split()[-1:][0]
177 | return ret
178 |
179 | def check_path(self):
180 | '''
181 | Intuitive way to verify the ADB path
182 | '''
183 | if self.get_version() is None:
184 | return False
185 | return True
186 |
187 | def set_adb_path(self, adb_path):
188 | '''
189 | Sets ADB tool absolute path
190 | '''
191 | if os.path.isfile(adb_path) is False:
192 | return False
193 | self.__adb_path = adb_path
194 | return True
195 |
196 | def get_adb_path(self):
197 | '''
198 | Returns ADB tool path
199 | '''
200 | return self.__adb_path
201 |
202 | def start_server(self):
203 | '''
204 | Starts ADB server
205 | adb start-server
206 | '''
207 | self.__clean__()
208 | self.run_cmd('start-server')
209 | return self.__output
210 |
211 | def kill_server(self):
212 | '''
213 | Kills ADB server
214 | adb kill-server
215 | '''
216 | self.__clean__()
217 | self.run_cmd('kill-server')
218 |
219 | def restart_server(self):
220 | '''
221 | Restarts ADB server
222 | '''
223 | self.kill_server()
224 | return self.start_server()
225 |
226 | def restore_file(self, file_name):
227 | '''
228 | Restore device contents from the backup archive
229 | adb restore
230 | '''
231 | self.__clean__()
232 | self.run_cmd(['restore', file_name])
233 | return self.__output
234 |
235 | def wait_for_device(self):
236 | '''
237 | Blocks until device is online
238 | adb wait-for-device
239 | '''
240 | self.__clean__()
241 | self.run_cmd('wait-for-device')
242 | return self.__output
243 |
244 | def get_help(self):
245 | '''
246 | Returns ADB help
247 | adb help
248 | '''
249 | self.__clean__()
250 | self.run_cmd('help')
251 | return self.__output
252 |
253 | def init_devices(self):
254 | '''
255 | Returns a list of connected devices
256 | adb devices
257 | '''
258 | self.run_cmd(['devices', '-l'])
259 | lines = re.split(r'\n', self.__output.decode(encoding='utf-8'))
260 | # print('init_devices', self.__output.decode(encoding='utf-8'))
261 | for line in lines[1:]: # [1:-1]:
262 | dev = line.split()
263 | if len(dev):
264 | self.devices.append(dev)
265 |
266 | def set_target_device(self, device):
267 | '''
268 | Select the device to work with
269 | '''
270 | self.__clean__()
271 | self.__target = device
272 | return True
273 |
274 | def get_target_device(self):
275 | '''
276 | Returns the selected device to work with
277 | '''
278 | return self.__target
279 |
280 | def get_state(self):
281 | '''
282 | Get ADB state
283 | adb get-state
284 | '''
285 | self.__clean__()
286 | self.run_cmd('get-state')
287 | return self.__output
288 |
289 | def get_serialno(self):
290 | '''
291 | Get serialno from target device
292 | adb get-serialno
293 | '''
294 | self.__clean__()
295 | self.run_cmd('get-serialno')
296 | return self.__output
297 |
298 | def reboot_device(self, mode):
299 | '''
300 | Reboot the target device
301 | adb reboot recovery/bootloader
302 | '''
303 | self.__clean__()
304 | if mode not in (self.REBOOT_RECOVERY, self.REBOOT_BOOTLOADER):
305 | self.__error = "mode must be REBOOT_RECOVERY/REBOOT_BOOTLOADER"
306 | self.__return = 1
307 | return self.__output
308 | self.run_cmd(["reboot", "%s" % "recovery"
309 | if mode == self.REBOOT_RECOVERY else "bootloader"])
310 | return self.__output
311 |
312 | def check_root(self):
313 | self.run_shell_cmd(['whoami'])
314 | return 'root' in self.get_output().decode()
315 |
316 | def set_system_rw(self):
317 | '''
318 | Mounts /system as rw
319 | adb remount
320 | '''
321 | self.__clean__()
322 | self.run_cmd("remount")
323 | return self.__output
324 |
325 | def get_remote_file(self, remote, local):
326 | '''
327 | Pulls a remote file
328 | adb pull remote local
329 | '''
330 | self.__clean__()
331 | self.run_cmd(['pull', remote, local])
332 |
333 | if self.__error is not None and "bytes in" in self.__error:
334 | self.__output = self.__error
335 | self.__error = None
336 |
337 | return self.__output
338 |
339 | def push_local_file(self, local, remote):
340 | '''
341 | Push a local file
342 | adb push local remote
343 | '''
344 | self.__clean__()
345 | self.run_cmd(['push', local, remote])
346 | return self.__output
347 |
348 | def listen_usb(self):
349 | '''
350 | Restarts the adbd daemon listening on USB
351 | adb usb
352 | '''
353 | self.__clean__()
354 | self.run_cmd("usb")
355 | return self.__output
356 |
357 | def listen_tcp(self, port=DEFAULT_TCP_PORT):
358 | '''
359 | Restarts the adbd daemon listening on the specified port
360 | adb tcpip
361 | '''
362 | self.__clean__()
363 | self.run_cmd(['tcpip', port])
364 | return self.__output
365 |
366 | def get_bugreport(self):
367 | '''
368 | Return all information from the device that should be included in a bug report
369 | adb bugreport
370 | '''
371 | self.__clean__()
372 | self.run_cmd("bugreport")
373 | return self.__output
374 |
375 | def get_jdwp(self):
376 | '''
377 | List PIDs of processes hosting a JDWP transport
378 | adb jdwp
379 | '''
380 | self.__clean__()
381 | self.run_cmd("jdwp")
382 | return self.__output
383 |
384 | def get_logcat(self, lcfilter=""):
385 | '''
386 | View device log
387 | adb logcat
388 | '''
389 | self.__clean__()
390 | self.run_cmd(['logcat', lcfilter])
391 | return self.__output
392 |
393 | def run_emulator(self, cmd=""):
394 | '''
395 | Run emulator console command
396 | '''
397 | self.__clean__()
398 | self.run_cmd(['emu', cmd])
399 | return self.__output
400 |
401 | def connect_remote(self, host=DEFAULT_TCP_HOST, port=DEFAULT_TCP_PORT):
402 | '''
403 | Connect to a device via TCP/IP
404 | adb connect host:port
405 | '''
406 | self.__clean__()
407 | self.run_cmd(['connect', "%s:%s" % (host, port)])
408 | return self.__output
409 |
410 | def disconnect_remote(self, host=DEFAULT_TCP_HOST, port=DEFAULT_TCP_PORT):
411 | '''
412 | Disconnect from a TCP/IP device
413 | adb disconnect host:port
414 | '''
415 | self.__clean__()
416 | self.run_cmd(['disconnect', "%s:%s" % (host, port)])
417 | return self.__output
418 |
419 | def ppp_over_usb(self, tty=None, params=""):
420 | '''
421 | Run PPP over USB
422 | adb ppp
423 | '''
424 | self.__clean__()
425 | if tty is None:
426 | return self.__output
427 |
428 | cmd = ["ppp", tty]
429 | if params != "":
430 | cmd += params
431 |
432 | self.run_cmd(cmd)
433 | return self.__output
434 |
435 | def sync_directory(self, directory=""):
436 | '''
437 | Copy host->device only if changed (-l means list but don't copy)
438 | adb sync
439 | '''
440 | self.__clean__()
441 | self.run_cmd(['sync', directory])
442 | return self.__output
443 |
444 | def forward_socket(self, local=None, remote=None):
445 | '''
446 | Forward socket connections
447 | adb forward
448 | '''
449 | self.__clean__()
450 | if local is None or remote is None:
451 | return self.__output
452 | self.run_cmd(['forward', local, remote])
453 | return self.__output
454 |
455 | def uninstall(self, package=None, keepdata=False):
456 | '''
457 | Remove this app package from the device
458 | adb uninstall [-k] package
459 | '''
460 | self.__clean__()
461 | if package is None:
462 | return self.__output
463 |
464 | cmd = 'uninstall '
465 | if keepdata:
466 | cmd += '-k '
467 | cmd += package
468 | self.run_cmd(cmd.split())
469 | return self.__output
470 |
471 | def install(self, fwdlock=False, reinstall=False, sdcard=False, pkgapp=None):
472 | '''
473 | Push this package file to the device and install it
474 | adb install [-l] [-r] [-s]
475 | -l -> forward-lock the app
476 | -r -> reinstall the app, keeping its data
477 | -s -> install on sdcard instead of internal storage
478 | '''
479 |
480 | self.__clean__()
481 | if pkgapp is None:
482 | return self.__output
483 |
484 | cmd = "install "
485 | if fwdlock is True:
486 | cmd += "-l "
487 | if reinstall is True:
488 | cmd += "-r "
489 | if sdcard is True:
490 | cmd += "-s "
491 |
492 | cmd += pkgapp
493 | self.run_cmd(cmd.split())
494 | return self.__output
495 |
496 | def find_binary(self, name=None):
497 | '''
498 | Look for a binary file on the device
499 | '''
500 | self.run_shell_cmd(['which', name])
501 |
502 | if self.__output is None: # not found
503 | self.__error = "'%s' was not found" % name
504 | elif self.__output.strip() == "which: not found": # which binary not available
505 | self.__output = None
506 | self.__error = "which binary not found"
507 | else:
508 | self.__output = self.__output.strip()
509 |
510 | return self.__output
511 |
512 | adb = ADB()
513 |
514 | def bind_rpycs():
515 | # return
516 | global adb
517 | # for item in adb.devices:
518 | # print(item)
519 | # if adb.check_root():
520 | # print("I'm root.")
521 | if(adb.connect_check()):
522 | import time
523 | for i in range(3):
524 | adb.run_shell_cmd("ps | grep 'from maix import mjpg;mjpg.start();' | awk '{print $1}'")
525 | res = adb.get_output()
526 | if res:
527 | tmp = (res.decode())
528 | tmp = tmp.replace('\r', '')
529 | res = tmp.split('\n')
530 | # filter(None, res)
531 | res = [x for x in res if x != '']
532 | # print(i, len(res), res) # exist server
533 | if (len(res) > 1):
534 | return True
535 |
536 | # ----
537 | adb.run_shell_cmd('/etc/init.d/S52ntpd stop')
538 | adb.run_shell_cmd("ps | grep python | awk '{print $1}' | xargs kill -9")
539 | # ----
540 | adb.forward_socket('tcp:18811', 'tcp:18811')
541 | adb.forward_socket('tcp:18811', 'tcp:18811')
542 | adb.forward_socket('tcp:18811', 'tcp:18811')
543 | adb.forward_socket('tcp:18812', 'tcp:18812')
544 | adb.forward_socket('tcp:18812', 'tcp:18812')
545 | adb.forward_socket('tcp:18812', 'tcp:18812')
546 | adb.forward_socket('tcp:22', 'tcp:22')
547 | adb.forward_socket('tcp:22', 'tcp:22')
548 | adb.forward_socket('tcp:22', 'tcp:22')
549 | adb.run_shell_cmd("python -c 'from maix import mjpg;mjpg.start();'")
550 | # adb.run_shell_cmd('/etc/init.d/S40network stop')
551 | # print(adb.get_output().decode())
552 | # adb.run_shell_cmd('killall tcpsvd')
553 | # print(adb.get_output().decode())
554 | # adb.run_shell_cmd("ps | grep python | awk '{print $1}' | xargs kill -9")
555 | # # adb.run_shell_cmd('/etc/init.d/S51dropbear stop')
556 | # # print(adb.get_output().decode())
557 | # adb.run_shell_cmd('/etc/init.d/S52ntpd stop')
558 | # # print(adb.get_output().decode())
559 | # adb.forward_socket('tcp:18811', 'tcp:18811')
560 | # adb.forward_socket('tcp:18812', 'tcp:18812')
561 | # adb.run_shell_cmd('python -c "import maix.mjpg;maix.mjpg.start()"')
562 | # # print(adb.get_output().decode())
563 | # # import sys
564 | # sys.exit(666)
565 | return False
566 |
567 | if __name__ == "__main__":
568 | # adb.run_shell_cmd('/etc/init.d/S52ntpd stop')
569 | # adb.run_shell_cmd("ps | grep python | awk '{print $1}' | xargs kill -9")
570 | # # ----
571 | adb.forward_socket('tcp:18811', 'tcp:18811')
572 | adb.forward_socket('tcp:18812', 'tcp:18812')
573 | # while True:
574 | # print('bind_rpycs()', bind_rpycs())
575 |
--------------------------------------------------------------------------------
/rpyc_ikernel/kernel.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | kernel.py
5 |
6 | Run standard IPython/Jupyter kernels on remote machines using
7 | job schedulers.
8 |
9 | """
10 |
11 | import imghdr
12 | import base64
13 | import os
14 | import logging
15 | import time
16 | import traceback
17 | import urllib.request
18 | import re
19 | import _thread
20 | import socket
21 |
22 | from PIL import Image # , UnidentifiedImageError
23 | import rpyc
24 |
25 | try:
26 | from pexpect import spawn as pexpect_spawn
27 | except ImportError:
28 | from pexpect.popen_spawn import PopenSpawn
29 |
30 | class pexpect_spawn(PopenSpawn):
31 | def isalive(self):
32 | return self.proc.poll() is None
33 |
34 | # from .scheduler import Scheduler
35 |
36 | from .adb import bind_rpycs, adb
37 |
38 | def config_maixpy3():
39 | from threading import Thread
40 | import time
41 | def tmp():
42 | while True:
43 | bind_rpycs()
44 | time.sleep(1)
45 | t = Thread(target=tmp,args=())
46 | t.setDaemon(True)
47 | t.start()
48 |
49 | # from ipykernel.kernelbase import Kernel
50 | from ipykernel.ipkernel import IPythonKernel
51 |
52 | ########################################################################################################################################
53 |
54 | class ProtoError(Exception):
55 | pass
56 |
57 | class MjpgSocket():
58 |
59 | def read_header_line(self, stream):
60 | '''Read one header line within the stream.
61 | The headers come right after the boundary marker and usually contain
62 | headers like Content-Type and Content-Length which determine the type and
63 | length of the data portion.
64 | '''
65 | return stream.readline().decode('iso8859-1').strip()
66 |
67 |
68 | def read_headers(self, stream, boundary):
69 | '''Read and return stream headers.
70 | Each stream data packet starts with an empty line, followed by a boundary
71 | marker, followed by zero or more headers, followed by an empty line,
72 | followed by actual data. This function reads and parses the entire header
73 | section. It returns a dictionary with all the headers. Header names are
74 | converted to lower case. Each value in the dictionary is a list of header
75 | fields values.
76 | '''
77 | l = self.read_header_line(stream)
78 | if l == '':
79 | l = self.read_header_line(stream)
80 | # print("read_headers", l, boundary)
81 | if l != boundary:
82 | raise ProtoError('Boundary string expected, but not found')
83 |
84 | headers = {}
85 | while True:
86 | l = self.read_header_line(stream)
87 | # An empty line indicates the end of the header section
88 | if l == '':
89 | break
90 |
91 | # Parse the header into lower case header name and header body
92 | i = l.find(':')
93 | if i == -1:
94 | raise ProtoError('Invalid header line: ' + l)
95 | name = l[:i].lower()
96 | body = l[i+1:].strip()
97 |
98 | lst = headers.get(name, list())
99 | lst.append(body)
100 | headers[name] = lst
101 |
102 | return headers
103 |
104 |
105 | def skip_data(self, stream, left):
106 | while left:
107 | rv = stream.read(left)
108 | if len(rv) == 0 and left:
109 | raise ProtoError('Not enough data in chunk')
110 | left -= len(rv)
111 |
112 |
113 | def read_data(self, buf, stream, length):
114 | v = memoryview(buf)[:length]
115 | while len(v):
116 | n = stream.readinto(v)
117 | if n == 0 and len(v):
118 | raise ProtoError('Not enough data in chunk')
119 | v = v[n:]
120 | return buf
121 |
122 |
123 | def parse_content_length(self, headers):
124 | # Parse and check Content-Length. The header must be present in
125 | # each chunk, otherwise we wouldn't know how much data to read.
126 | clen = headers.get('content-length', None)
127 | try:
128 | return int(clen[0])
129 | except (ValueError, TypeError):
130 | raise ProtoError('Invalid or missing Content-Length')
131 |
132 |
133 | def check_content_type(self, headers, type_):
134 | ctype = headers.get('content-type', None)
135 | if ctype is None:
136 | raise ProtoError('Missing Content-Type header')
137 | ctype = ctype[0]
138 |
139 | i = ctype.find(';')
140 | if i != -1:
141 | ctype = ctype[:i]
142 |
143 | if ctype != type_:
144 | raise ProtoError('Wrong Content-Type: %s' % ctype)
145 |
146 | return True
147 |
148 |
149 | def open_mjpeg_stream(self, stream):
150 | if stream.status != 200:
151 | raise ProtoError('Invalid response from server: %d' % stream.status)
152 | h = stream.info()
153 |
154 | boundary = h.get_param('boundary', header='content-type', unquote=True)
155 | if boundary is None:
156 | raise ProtoError('Content-Type header does not provide boundary string')
157 | # boundary = '--' + boundary
158 |
159 | return boundary
160 |
161 |
162 | def read_mjpeg_frame(self, stream, boundary):
163 | hdr = self.read_headers(stream, boundary)
164 | clen = self.parse_content_length(hdr)
165 | if clen == 0:
166 | raise EOFError('End of stream reached')
167 | self.check_content_type(hdr, 'image/jpeg')
168 | buf = bytearray(clen)
169 | self.read_data(buf, stream, clen)
170 | return buf
171 |
172 | def unit_test(self):
173 | try:
174 | url = "http://127.0.0.1:18811"
175 | with urllib.request.urlopen(url, timeout = 3) as stream:
176 | boundary = self.open_mjpeg_stream(stream)
177 | while True:
178 | tmp = self.read_mjpeg_frame(stream, boundary)
179 | print(len(tmp), tmp)
180 | except EOFError:
181 | pass
182 | except Exception as e:
183 | traceback.print_exc()
184 |
185 | def __init__(self, url: str):
186 | self._url = url
187 | self.stream = urllib.request.urlopen(url, timeout = 9)
188 | self.boundary = self.open_mjpeg_stream(self.stream)
189 |
190 | def iter_content(self):
191 | while True:
192 | yield self.read_mjpeg_frame(self.stream, self.boundary)
193 |
194 |
195 | # import requests
196 | # class MjpgReader():
197 | # """
198 | # MJPEG format
199 |
200 | # Content-Type: multipart/x-mixed-replace; boundary=--BoundaryString
201 | # --BoundaryString
202 | # Content-type: image/jpg
203 | # Content-Length: 12390
204 |
205 | # ... image-data here ...
206 |
207 |
208 | # --BoundaryString
209 | # Content-type: image/jpg
210 | # Content-Length: 12390
211 |
212 | # ... image-data here ...
213 | # """
214 |
215 | # def __init__(self, url: str):
216 | # self._url = url
217 | # self.session = requests.Session()
218 |
219 | # def iter_content(self):
220 | # """
221 | # Raises:
222 | # RuntimeError
223 | # """
224 |
225 | # r = self.session.get(self._url, stream=True, timeout=3)
226 | # # r = requests.get(self._url, stream=True, timeout=3)
227 |
228 | # # parse boundary
229 | # content_type = r.headers['content-type']
230 | # index = content_type.rfind("boundary=")
231 | # assert index != 1
232 | # boundary = content_type[index+len("boundary="):] + "\r\n"
233 | # boundary = boundary.encode('utf-8')
234 |
235 | # rd = io.BufferedReader(r.raw)
236 | # while True:
237 | # self._skip_to_boundary(rd, boundary)
238 | # length = self._parse_length(rd)
239 | # yield rd.read(length)
240 |
241 | # def _parse_length(self, rd) -> int:
242 | # length = 0
243 | # while True:
244 | # line = rd.readline()
245 | # if line == b'\r\n':
246 | # return length
247 | # if line.startswith(b"Content-Length"):
248 | # length = int(line.decode('utf-8').split(": ")[1])
249 | # assert length > 0
250 |
251 | # def _skip_to_boundary(self, rd, boundary: bytes):
252 | # for _ in range(10):
253 | # if boundary in rd.readline():
254 | # break
255 | # else:
256 | # raise RuntimeError("Boundary not detected:", boundary)
257 |
258 | ########################################################################################################################################
259 |
260 | def _setup_logging(verbose=logging.INFO):
261 |
262 | log = logging.getLogger("rpyc_ikernel")
263 | log.setLevel(verbose)
264 |
265 | console = logging.StreamHandler()
266 | formatter = logging.Formatter('%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
267 | console.setFormatter(formatter)
268 |
269 | log.handlers = []
270 | log.addHandler(console)
271 |
272 | # So that we can attach these to pexpect for debugging purposes
273 | # we need to make them look like files
274 | def _write(*args, **_):
275 | """
276 | Method to attach to a logger to allow it to act like a file object.
277 |
278 | """
279 | message = args[0]
280 | # convert bytes from pexpect to something that prints better
281 | if hasattr(message, "decode"):
282 | message = message.decode("utf-8")
283 |
284 | for line in message.splitlines():
285 | if line.strip():
286 | log.debug(line)
287 |
288 | def _pass():
289 | """pass"""
290 | pass
291 |
292 | log.write = _write
293 | log.flush = _pass
294 |
295 | return log
296 |
297 | # _async_raise(ident, SystemExit)
298 |
299 |
300 | def _async_raise(tid):
301 | import inspect
302 | import ctypes
303 | exctype = KeyboardInterrupt
304 | """raises the exception, performs cleanup if needed"""
305 | tid = ctypes.c_long(tid)
306 | if not inspect.isclass(exctype):
307 | exctype = type(exctype)
308 | res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
309 | tid, ctypes.py_object(exctype))
310 | if res == 0:
311 | return # maybe thread killed
312 | # raise ValueError("invalid thread id")
313 | if res != 1:
314 | # """if it returns a number greater than one, you're in trouble,
315 | # and you should call it again with exc=NULL to revert the effect"""
316 | ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
317 | raise SystemError("PyThreadState_SetAsyncExc failed")
318 |
319 |
320 | class RPycKernel(IPythonKernel):
321 | implementation = 'rpyc_kernel'
322 |
323 | language_info = {'name': 'Python',
324 | 'codemirror_mode': 'python',
325 | 'mimetype': 'text/python',
326 | 'file_extension': '.py'}
327 |
328 | def __init__(self, **kwargs):
329 | IPythonKernel.__init__(self, **kwargs)
330 | self.log = _setup_logging()
331 | self.remote = None
332 | self.address = "localhost"
333 | self.clear_output = True
334 | self.last_result = ""
335 | # for do_handle
336 | self.pattern = re.compile("[$](.*?)[(](.*)[)]")
337 | self.commands = {
338 | 'exec': '%s',
339 | 'connect': 'self.connect_remote(%s)',
340 | }
341 | config_maixpy3()
342 | # self.do_reconnect()
343 | # bind_rpycs()
344 |
345 |
346 | def do_reconnect(self):
347 | global adb
348 | import sys
349 | for i in range(5):
350 | try:
351 | self.remote = rpyc.classic.connect(self.address)
352 | self.remote.modules.sys.stdin = sys.stdin
353 | self.remote.modules.sys.stdout = sys.stdout
354 | self.remote.modules.sys.stderr = sys.stderr
355 | self.remote._config['sync_request_timeout'] = None
356 | self.remote_exec = rpyc.async_(self.remote.modules.builtins.exec) # Independent namespace
357 | # self.remote_exec = rpyc.async_(self.remote.execute) # Common namespace
358 | return True
359 | # ConnectionRefusedError: [Errno 111] Connection refused
360 | except Exception as e:
361 | self.remote = None
362 | # self.log.debug('%s on Remote IP: %s' % (repr(e), self.address))
363 | print("[ rpyc-kernel ]( Connect IP: %s at %s)" % (self.address, time.asctime()))
364 | time.sleep(2)
365 | print("[ rpyc-kernel ]( Connect IP: %s fail! )" % (self.address))
366 | if sys.platform.startswith('win'):
367 | try:
368 | if adb.connect_check():
369 | adb.kill_server()
370 | except Exception as e:
371 | self.log.info("[ rpyc-kernel ]( adb %s )" % (str(e)))
372 | # import sys
373 | # sys.exit()
374 |
375 | def check_connect(self):
376 | if self.remote:
377 | try:
378 | if self.remote.closed:
379 | raise Exception('remote %s closed' % self.address)
380 | self.log.debug('checking... (%s)' % self.remote.closed)
381 | self.remote.ping() # fail raise PingError
382 | return True
383 | except Exception as e: # PingError
384 | # self.log.error(repr(e))
385 | if self.remote != None:
386 | self.remote.close()
387 | self.remote = None
388 | return self.do_reconnect()
389 |
390 | def connect_remote(self, address="localhost"):
391 | self.address = address
392 | self.do_reconnect()
393 |
394 | def _ready_display(self, port=18811):
395 | self._media_client, self._media_port, self._media_work = None, port, True
396 | self._clear_display(self.remote)
397 | self._media_work = True
398 | _thread.start_new_thread(self._update_display, ())
399 |
400 | def _clear_display(self, remote):
401 | self.log.debug('[%s] _clear_display %s %s' % (self.remote, self._media_work, self._media_client))
402 | try:
403 | # if self._media_work == True or self._media_client != None:
404 | # self.log.info('[%s] _media_work %s _media_client %s' % (self.remote, self._media_work, self._media_client))
405 | # time.sleep(1) # many while True maybe ouput last result
406 | self._media_work = False
407 | remote.modules['maix.mjpg'].clear_mjpg()
408 | except Exception as e:
409 | self.log.debug(e)
410 |
411 | def _update_display(self):
412 | # return
413 | while self._media_work:
414 | try:
415 | self.log.debug('[%s] _update_display_ ' % (self._media_client))
416 | if self._media_client == None:
417 | try:
418 | self._media_client = MjpgSocket("http://%s:%d" % (self.address, self._media_port))
419 | self.log.debug('[%s] connect... (%s)' % (self._media_client, os.getpid()))
420 | except socket.timeout as e:
421 | self.log.debug(e)
422 | break
423 | if self._media_client != None:
424 | # for content in self._media_client.iter_content():
425 | # self.log.info('iter_content... (%s)' % len(content))
426 | try:
427 | content = next(self._media_client.iter_content())
428 | image_type = imghdr.what(None, content)
429 | # import io
430 | # self.log.info(image_type)
431 | # img = io.BytesIO(content)
432 | # Image.open(img)
433 | # tmp = Image.open()
434 | # buf = io.BytesIO()
435 | # tmp.resize((tmp.size[0], tmp.size[1])).save(buf, format = "JPEG")
436 | # buf.flush()
437 | # content = buf.getvalue()
438 | if self.clear_output: # used when updating lines printed
439 | self.send_response(self.iopub_socket,
440 | 'clear_output', {"wait": True})
441 | # self.log.info(content)
442 | # image_type = imghdr.what(None, content)
443 | # self.log.info(image_type)
444 | image_data = base64.b64encode(content).decode('iso8859-1')
445 | # self.log.info(len(image_data))
446 | self.send_response(self.iopub_socket, 'display_data', {
447 | 'data': {
448 | 'image/' + image_type: image_data
449 | },
450 | 'metadata': {}
451 | })
452 | except Exception as e:
453 | self.log.debug('[%s] Exception %s' % (self._media_client, e))
454 | # except UnicodeDecodeError as e:
455 | # self.log.debug('[%s] UnicodeDecodeError %s ' % (self._media_client, e))
456 | except Image.UnidentifiedImageError as e:
457 | self.log.debug('[%s] UnidentifiedImageError %s' % (self._media_client, e))
458 | # time.sleep(0.02)
459 | time.sleep(0.01)
460 | # except OSError as e:
461 | # pass
462 | # except ConnectionResetError as e:
463 | except Exception as e:
464 | self.log.debug(e)
465 | # import traceback
466 | # traceback.print_exc()
467 | if (self._media_client):
468 | try:
469 | self._media_client.stream.close()
470 | except Exception as e:
471 | self.log.debug('[%s] Exception ' % (e))
472 | self._media_client = None
473 |
474 | def kill_task(self):
475 | try:
476 | master = rpyc.classic.connect(self.address)
477 | thread = master.modules.threading
478 | # print(thread.enumerate()) # kill remote's thread
479 | lists = [i for i in thread.enumerate() if i.__class__.__name__ not in [
480 | 'MjpgServerThread', '_MainThread']]
481 | kills = [i.ident for i in lists if i.ident not in [
482 | thread.main_thread().ident, thread.get_ident()]]
483 | # print(kills)
484 | for id in kills:
485 | try:
486 | master.teleport(_async_raise)(id)
487 | except Exception as e:
488 | self.log.debug('teleport Exception (%s)' % repr(e))
489 | # print(master.modules.threading.enumerate())
490 | # master.modules['traceback'].print_exc()
491 | self._clear_display(master)
492 | master.close()
493 | except Exception as e:
494 | self.log.debug(e)
495 |
496 | def do_handle(self, code):
497 | # self.log.debug(code)
498 | # code = re.sub(r'([#](.*)[\n])', '', code) # clear '# etc...' but bug have "#"
499 | # self.log.info(code)
500 |
501 | cmds = self.pattern.findall(code)
502 | # self.log.info(cmds)
503 | for cmd in cmds:
504 | _format = self.commands.get(cmd[0], None)
505 | if _format:
506 | # print(_format % cmd[1])
507 | exec(_format % cmd[1])
508 | code = self.pattern.sub('', code)
509 |
510 | # self.log.debug(code)
511 | return code
512 |
513 | def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False):
514 | if not code.strip():
515 | return {'status': 'ok', 'execution_count': self.execution_count,
516 | 'payload': [], 'user_expressions': {}}
517 | self.log.debug(code)
518 |
519 | # Handle the host call code
520 | code = self.do_handle(code)
521 |
522 | interrupted = False
523 | self.last_result = ""
524 | if self.check_connect():
525 | try:
526 | try:
527 | print("[ rpyc-kernel ]( running at %s )" % (time.asctime()))
528 | self._ready_display()
529 |
530 | # self.remote.modules.builtins.exec(code)
531 |
532 | self.result = self.remote_exec(code, self.remote.modules.builtins.globals())
533 | # self.result.wait()
534 | def get_result(result):
535 | if result.error:
536 | pass # is error
537 | self.log.debug(result.value)
538 | # print('get_result', result, result.value, result.error)
539 | self.result.add_callback(get_result)
540 | # self.log.info('self.result.ready (%s)' % repr(self.result.ready))
541 | while self.result.ready == False:
542 | # self.log.info('self.result.ready (%s)' % repr(self.result.ready))
543 | time.sleep(0.001) # print(end='')
544 | # time.sleep(0.2)
545 | # with rpyc.classic.redirected_stdio(self.remote):
546 | # self.remote_exec(code)
547 |
548 | # self.remote.execute(code)
549 | # self.log.info(self.result)
550 | except KeyboardInterrupt as e:
551 | # self.remote.execute("raise KeyboardInterrupt") # maybe raise main_thread Exception
552 | interrupted = True
553 | # self.kill_task()
554 | self.last_result = '\r\nTraceback (most recent call last):\r\n File "", line unknown, in \r\nRemote.KeyboardInterrupt\r\n'
555 | self.log.debug(self.last_result)
556 | # raise e
557 | # remote stream has been closed(cant return info)
558 | except EOFError as e:
559 | self.log.debug(e)
560 | # self.remote.close() # not close self
561 | try:
562 | self.remote.modules.os._exit(233) # should close remote
563 | except Exception as e:
564 | pass
565 | except Exception as e:
566 | import traceback, sys
567 | # traceback.print_exc()
568 | exc_type, exc_value, exc_traceback = sys.exc_info()
569 | for s in traceback.format_exception(exc_type, exc_value, exc_traceback):
570 | if "Remote Traceback" in s:
571 | self.last_result = ""
572 | self.last_result += s
573 | # self.log.error(e)
574 | # raise e
575 | pass
576 | finally:
577 | self.kill_task()
578 |
579 | if len(self.last_result) > 0:
580 | self.send_response(self.iopub_socket, 'execute_result', {
581 | 'execution_count': self.execution_count,
582 | 'status': 'ok',
583 | 'payload': [],
584 | 'user_expressions': {},
585 | 'data': {
586 | # 'text/' + image_type: image_data
587 | "text/plain" : str(self.last_result)
588 | },
589 | 'metadata': {},
590 | })
591 |
592 | if interrupted:
593 | return {'status': 'abort', 'execution_count': self.execution_count}
594 |
595 | return {
596 | 'status': 'ok',
597 | 'execution_count': self.execution_count,
598 | 'payload': [],
599 | 'user_expressions': {}
600 | }
601 |
--------------------------------------------------------------------------------
/tests/usage_display.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [
8 | {
9 | "name": "stdout",
10 | "output_type": "stream",
11 | "text": [
12 | "[ rpyc-kernel ]( running at Thu Oct 28 16:46:43 2021 )\n",
13 | "uname_result(system='Linux', node='sipeed', release='4.9.118', version='#2369 PREEMPT Tue Oct 26 08:46:44 UTC 2021', machine='armv7l', processor='')\n"
14 | ]
15 | }
16 | ],
17 | "source": [
18 | "import platform\n",
19 | "print(platform.uname())"
20 | ]
21 | },
22 | {
23 | "cell_type": "code",
24 | "execution_count": 1,
25 | "metadata": {},
26 | "outputs": [
27 | {
28 | "data": {
29 | "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAHgAeADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCxRRRX52flIUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUwHfT79Bz/F96gHaQf4qsALdJheJBWsIe19QKtFLjBwaMVlJcujASiiikAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUoJByDikpRTUrO6As8XQ7CYf8Aj9V8Hp2oHXjpVji6HYTD/wAfrq0rr+9+YtyrRSkEHBFJXK1bRjCiiikAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUoJByDSUU07O6AtcXQ7CYf8Aj9ViDnB6UAnORwKs8XQ9Jh/4/XVpXX978wKtLSkYODTa5GmtGG4UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFOBwcikpKE2tUG5a4uh6TD/x+q2DnB6UAnPHSrPF0OwmH/j9deldf3vzAq0UpBBwRSVytWdmAUUUUgCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClBIORSUU07aoC1xdDsJh/4/VYg5welAJzkcCrPF0PSYf+P11aV1/e/MCrS0pGDg02uRprRhuFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABTgcHIpKShNrVBuWuLoekw/8fqtg5welAJzx0qzxdDsJh/4/XXpXX978wKtFKQQcEUlcrVnZgFFFFIAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApQSDkUlFNO2qAtcXQ7CYf+P1WIOcHpQCc5HAqzxdD0mH/AI/XVpXX978wKtLSkYODTa5GmtGG4UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFOBwcikpKE2tUG5a4uh6TD/wAfqtg5welAJzx0qzxdDsJh/wCP116V1/e/MCrRSkEHBFJXK1Z2YBRRRSAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKUEg5FJRTTtqgLXF0OwmH/j9ViDnB6UAnORwKs8XQ9Jh/4/XVpXX978wKtLSkYODTa5GmtGG4UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFOBwcikpKE2tUG5a4uh6TD/AMfqtg5welAJzx0qzxdDsJh/4/XXpXX978wKtFKQQcEUlcrVnZgFFFFIAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApQSDkUlFNO2qAtcXQ7CYf8Aj9ViDnB6UAnORwKs8XQ9Jh/4/XVpXX978wKtLSkYODTa5GmtGG4UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFOBwcikpKE2tUG5a4uh6TD/x+q2DnB6UAnPHSrPF0OwmH/j9deldf3vzAq0UpBBwRSVytWdmAUUUUgCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClBIORSUU07aoC1xdDsJh/4/VYg5welAJzkcCrPF0PSYf+P11aV1/e/MCrS0pGDg02uRprRhuFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABTgcHIpKShNrVBuWuLoekw/8fqtg5welAJzx0qzxdDsJh/4/XXpXX978wKtFKQQcEUlcrVnZgFFFFIAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApQSDkUlFNO2qAtcXQ7CYf+P1WIOcHpQCc5HAqzxdD0mH/AI/XVpXX978wKtLSkYODTa5GmtGG4UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFOBwcikpKE2tUG5a4uh6TD/wAfqtg5welAJzx0qzxdDsJh/wCP116V1/e/MCrRSkEHBFJXK1Z2YBRRRSAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKUEg5FJRTTtqgLXF0OwmH/j9ViDnB6UAnORwKs8XQ9Jh/4/XVpXX978wKtLSkYODTa5GmtGG4UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFOBwcikpKE2tUG5a4uh6TD/AMfqtg5welAJzx0qzxdDsJh/4/XXpXX978wKtFKQQcEUlcrVnZgFFFFIAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApQSDkUlFNO2qAtcXQ7CYf8Aj9ViDnB6UAnORwKs8XQ9Jh/4/XVpXX978wKtLSkYODTa5GmtGG4UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFOBwcikpKE2tUG5a4uh6TD/x+q2DnB6UAnPHSrPF0OwmH/j9deldf3vzAq0UpBBwRSVytWdmAUUUUgCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClBIORSUU07aoC1xdDsJh/4/VYg5welAJzkcCrPF0PSYf+P11aV1/e/MCrS0pGDg02uRprRhuFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABTgcHIpKShNrVBuWuLoekw/8fqtg5welAJzx0qzxdDsJh/4/XXpXX978wKtFKQQcEUlcrTWjAKKKKQBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSgkHINJRTTs7oC1xdDsJh/wCP1XPXnpR3z2qfi6HpMP8Ax+upyVVf3vzDcrUUpHY8GkrkBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUoPcdaSijULD5JHlfJpDzSZO2lxtGTWkpc/qA2iiiswCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/2Q=="
30 | },
31 | "metadata": {},
32 | "output_type": "display_data"
33 | },
34 | {
35 | "name": "stdout",
36 | "output_type": "stream",
37 | "text": [
38 | "[ rpyc-kernel ]( running at Wed Dec 1 16:27:52 2021 )\n"
39 | ]
40 | }
41 | ],
42 | "source": [
43 | "\n",
44 | "from PIL import Image\n",
45 | "from PIL import ImageDraw\n",
46 | "from PIL import ImageFont\n",
47 | "\n",
48 | "import time, random\n",
49 | "\n",
50 | "from maix import display\n",
51 | "\n",
52 | "image = Image.new(\"RGB\", (240, 240), (0, 231, 0))# (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))\n",
53 | "draw = ImageDraw.Draw(image)\n",
54 | "\n",
55 | "draw.line((20, 20, 200, 200), 'cyan')\n",
56 | "\n",
57 | "display.show(image)\n"
58 | ]
59 | },
60 | {
61 | "cell_type": "code",
62 | "execution_count": 1,
63 | "metadata": {
64 | "scrolled": false
65 | },
66 | "outputs": [
67 | {
68 | "data": {
69 | "image/jpeg": "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"
70 | },
71 | "metadata": {},
72 | "output_type": "display_data"
73 | },
74 | {
75 | "data": {
76 | "text/plain": [
77 | "\r\n",
78 | "Traceback (most recent call last):\r\n",
79 | " File \"\", line unknown, in \r\n",
80 | "Remote.KeyboardInterrupt\r\n"
81 | ]
82 | },
83 | "execution_count": 1,
84 | "metadata": {},
85 | "output_type": "execute_result"
86 | }
87 | ],
88 | "source": [
89 | "from maix import display, camera\n",
90 | "from PIL import ImageDraw\n",
91 | "import time\n",
92 | "while True:\n",
93 | " im = camera.capture()\n",
94 | " display.show(im)"
95 | ]
96 | },
97 | {
98 | "cell_type": "code",
99 | "execution_count": 1,
100 | "metadata": {},
101 | "outputs": [
102 | {
103 | "data": {
104 | "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAHgAeADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDhKKKK+cP2YKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAHZCgEjkdfelbcjBGUrIf4TSEgleNxXkCtjK+IkGcJq4HPYTgfoMCtoQVReZ5+KxU8PNJfD18jHApDQQUYqwIYHBBo61m731O+MoyimhKKKKkYUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQA4CkBKMGUkMDkEUUdad3e4nGMo2aNn5dfTsmqIPoLgf0wKx2BVirKQwOCD2oDFWDKSGByCO1bPy6+n8Kaog+guB/TArdtVV/e/M8eUpYOVlrTf4GJRSsCrFWBDA4INJWDVj14yUldBRRRSKCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClDFWDKSGByCKSimnbYUoqSsza+TX4/4U1NB9BcD+mBWMQUYqwIYHBB7UAlGDKSGByCO1bPya/H/CmpqPoLgf0wK6NKq/vfmeSubAy7wf4GLRSkFWKsCGBwQaSuc9aE1JXQUUUUhhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSqSrBlJDA5BFJRTFOCkrM2vk1+PsmpqPoLgf0wKxiCjFWBDA4IPagEowZSQwOQR2rZ+TX4/wCFNTQfQXA/pgV0aVV/e/M8luWBl3g/wMWilKlWKsCGBwQaSudqx60ZKSugooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClDFWDKSGByCKSimnbYUoqSsza+TX4/4U1NB9BcD+mBWMQUYqwIYHBB7UAlGDKSGByCO1bPya/H/CmpqPoLgf0wK6NKq/vfmeSubAy7wf4GLRSkFWKsCGBwQaSuc9aE1JXQUUUUhhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSqSrBlJDA5BFJRTFOCkrM2vk1+PsmpqPoLgf0wKxiCjFWBDA4IPagEowZSQwOQR2rZ+TX4/wCFNTQfQXA/pgV0aVV/e/M8luWBl3g/wMWilKlWKsCGBwQaSudqx60ZKSugooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClDFWDKSGByCKSimnbYUoqSsza+TX4/4U1NB9BcD+mBWMQUYqwIYHBB7UAlGDKSGByCO1bPya/H/CmpqPoLgf0wK6NKq/vfmeSubAy7wf4GLRSkFWKsCGBwQaSuc9aE1JXQUUUUhhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSqSrBlJDA5BFJRTFOCkrM2vk1+PsmpqPoLgf0wKxiCjFWBDA4IPagEowZSQwOQR2rZ+TX4/wCFNTQfQXA/pgV0aVV/e/M8luWBl3g/wMWilKlWKsCGBwQaSudqx60ZKSugooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClDFWDKSGByCKSimnbYUoqSsza+TX4/4U1NB9BcD+mBWMQUYqwIYHBB7UAlGDKSGByCO1bPya/H/CmpqPoLgf0wK6NKq/vfmeSubAy7wf4GLRSkFWKsCGBwQaSuc9aE1JXQUUUUhhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSqSrBlJDA5BFJRTFOCkrM2vk1+PsmpqPoLgf0wKxiCjFWBDA4IPagEowZSQwOQR2rZ+TX4/wCFNTQfQXA/pgV0aVV/e/M8luWBl3g/wMWilKlWKsCGBwQaSudqx60ZKSugooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClDFWDKSGByCKSimnbYUoqSsza+TX4/4U1NB9BcD+mBWMQUYqwIYHBB7UAlGDKSGByCO1bPya/H/CmpqPoLgf0wK6NKq/vfmeSubAy7wf4GLRSkFWKsCGBwQaSuc9aE1JXQUUUUhhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSqSrBlJDA5BFJRTFOCkrM2vk1+PsmpqPoLgf0wKxiCjFWBDA4IPagEowZSQwOQR2rZ+TX4/wCFNTQfQXA/pgV0aVV/e/M8luWBl3g/wMWilKlWKsCGBwQaSudqx60ZKSugooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClDFWDKSGByCKSimnbYUoqSsza+TX4/4U1NB9BcD+mBWMQUYqwIYHBB7UAlGDKSGByCO1bPya/H/CmpqPoLgf0wK6NKq/vfmeSubAy7wf4GLRSkFWKsCGBwQaSuc9aE1JXQUUUUhhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSqSrBlJDA5BFJRTFOCkrM2vk1+PsmpqPoLgf0wKxiCjFWBDA4IPagEowZSQwOQR2rZ+TX4/wCFNTQfQXA/pgV0aVV/e/M8luWBl3g/wMWilKlWKsCGBwQaSudqx60ZKSugooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClDFWDKSGByCKSimnbYUoqSsza+TX4/4U1NB9BcD+mBWMQUYqwIYHBB7UAlGDKSGByCO1bPya/H/CmpqPoLgf0wK6NKq/vfmeSubAy7wf4GLRSkFWKsCGBwQaSuc9aE1JXQUUUUhhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSqSrBlJDA5BFJRTFOCkrM2vk1+PsmpqPoLgf0wKxiCjFWBDA4IPagEowZSQwOQR2rZ+TX4/wCFNTQfQXA/pgV0aVV/e/M8luWBl3g/wMWilKlWKsCGBwQaSudqx60ZKSugooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClDFWDKSGByCKSimnbYUoqSsza+TX4/4U1NB9BcD+mBWMQUYqwIYHBB7UAlGDKSGByCO1bPya/H/CmpqPoLgf0wK6NKq/vfmeSubAy7wf4GLRSkFWKsCGBwQaSuc9aE1JXQUUUUhhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSqSrBlJDA5BFJRTFOCkrM2vk1+PsmpqPoLgf0wKxiCjFWBDA4IPagEowZSQwOQR2rZ+TX4/wCFNTQfQXA/pgV0aVV/e/M8luWBl3g/wMWilKlWKsCGBwQaSudqx60ZKSugooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClDFWDKSGByCKSimnbYUoqSsza+TX4/4U1NB9BcD+mBWMQUYqwIYHBB7UAlGDKSGByCO1bPya/H/CmpqPoLgf0wK6NKq/vfmeSubAy7wf4GLRSkFWKsCGBwQaSuc9aE1JXQUUUUhhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSqSrBlJDA5BFJRTFOCkrM2vk1+PsmpqPoLgf0wKxiCjFWBDA4IPagEowZSQwOQR2rZ+TX4/wCFNTQfQXA/pgV0aVV/e/M8luWBl3g/wMWilKlWKsCGBwQaSudqx60ZKSugooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClDFWDKSGByCKSimnbYUoqSsza+TX4/4U1NB9BcD+mBWMQUYqwIYHBB7UAlGDKSGByCO1bPya/H/CmpqPoLgf0wK6NKq/vfmeSubAy7wf4GLRSkFWKsCGBwQaSuc9aE1JXQUUUUhhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSqSrBlJDA5BFJRTFOCkrM2vk1+PsmpqPoLgf0wKxiCjFWBDA4IPagEowZSQwOQR2rZ+TX4/wCFNUQfQXA/pgV0aVV/e/M8luWBl3g/wMWilKlWKsCGBwQaSudpo9aMlJXQUUUUhhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABSqSrBlJDA5BFJRTTsxSipKzNv5deT+FNUQfQXA/pgVjNlWKsCGBwQe1G4qwdSQw5BHatj5Nfj/AIU1NR9BcD+mBW0pKaPI9/AT5Y603+BjZpDSkMrFWBBBwQaTFZNWPXhPnVwoooqRhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFACmgEqwZSQQcgikzQKpOwpwU9yxc3M15cme5IzjAAGKhH3GNIflwc5pcEn5+FobuSkoLliNoooqSwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9k="
105 | },
106 | "metadata": {},
107 | "output_type": "display_data"
108 | },
109 | {
110 | "name": "stdout",
111 | "output_type": "stream",
112 | "text": [
113 | "[ rpyc-kernel ]( running at Wed Dec 1 16:27:34 2021 )\n"
114 | ]
115 | }
116 | ],
117 | "source": [
118 | "\n",
119 | "from PIL import Image\n",
120 | "from PIL import ImageDraw\n",
121 | "from PIL import ImageFont\n",
122 | "\n",
123 | "import time, random\n",
124 | "\n",
125 | "from maix import display\n",
126 | "\n",
127 | "image = Image.new(\"RGB\", (240, 240), (231, 0, 0))# (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))\n",
128 | "draw = ImageDraw.Draw(image)\n",
129 | "\n",
130 | "draw.line((20, 20, 200, 200), 'cyan')\n",
131 | "\n",
132 | "display.show(image)\n"
133 | ]
134 | },
135 | {
136 | "cell_type": "code",
137 | "execution_count": null,
138 | "metadata": {},
139 | "outputs": [],
140 | "source": []
141 | }
142 | ],
143 | "metadata": {
144 | "kernelspec": {
145 | "display_name": "RPyc-Python",
146 | "language": "Python",
147 | "name": "rpyc"
148 | },
149 | "language_info": {
150 | "codemirror_mode": "python",
151 | "file_extension": ".py",
152 | "mimetype": "text/python",
153 | "name": "Python"
154 | }
155 | },
156 | "nbformat": 4,
157 | "nbformat_minor": 4
158 | }
159 |
--------------------------------------------------------------------------------