Jupyterlab Micropython Extension 是一个用于连接 MicroPython 开发板的 Jupyter Lab 插件. 目前仅仅在 WindowsUbuntu 18.04 系统下测试可用.

button_demo

插件依靠 JupyterLab MicroPython KernelMicroPython 通讯, 使用 JupyterLab MicroPython KernelJupyter Lab 可以通过输入 %serialconnect --port=/dev/ttyUSB0 来连接:

jupyterlab_micropython_kernel

插件的不同按钮对应了不同的 magic command 以此实现通过 UIMicroPython 开发板交互.

项目地址: JupyterLab MicroPython Ext

Requirements

  • JupyterLab >= 1.0
  • jupyterlab_micropython_kernel >= 0.0.1

Install

Install JupyterLab MicroPython Kernel

这个插件是通过 JupyterLab MicroPython KernelMicroPython 交互的. 所以要使用插件需要首先安装 JupyterLab MicroPython Kernel

首先下载 JupyterLab MicroPython Kernel 到本地, 并在 bash 中切换到项目路径:

1
2
git clone https://github.com/zhouzaihang/jupyterlab_micropython_kernel.git
cd jupyterlab_micropython_kernel

然后使用 pip 安装它 作为一个 Python3 的库, 使用 -e 参数表示在可编辑模式(editable mode)下从一个本地的项目路径或 VCS URL 中安装一个项目:

1
pip install -e .

python/../site-packages (Python 默认库的安装位置) 会创建一个文件指向到当前目录, 当你使用 git pull 或者其他操作修改代码后, 会自动调用最新的代码.(如果出错的话请检查你使用 Python 环境下是否应该使用 pip3, sudo pip 等)

然后输入以下命令把 kernelspec 添加到 jupyter 中:

1
python -m jupyterlab_micropython_kernel.install

这会创建文件 .local/share/jupyter/kernels/micropython/kernel.json, 并指向当前安装的 kernelspec.

你可以使用以下命令查看当前所有安装的 kernelspec 极其位置:

1
jupyter kernelspec list

Install Extension

首先下载 JupyterLab MicroPython Ext 到本地, 并在 bash 中切换到项目路径:

1
2
3
4
# Clone the repo to your local environment
git clone https://github.com/zhouzaihang/jupyterlab_micropython_ext.git
# Move to jupyterlab_micropython_ext directory
cd jupyterlab_micropython_ext

jlpm 命令是一个 Jupyter Lab 使用的 yarn 固定版本, yarn会在安装 Jupyter Lab 的时候一起安装. 下面的命令中你也可以使用
yarnnpm 替换 jlpm.

1
2
3
4
5
6
# Install dependencies
jlpm
# Build Typescript source
jlpm build
# Link your development version of the extension with JupyterLab
jupyter labextension link .

当项目有更新时, 你可以运行以下命令更新代码并 rebuild 插件和 Jupyter Lab 程序.

1
2
3
4
5
6
# Update source code
git pull
# Rebuild Typescript source after making changes
jlpm build
# Rebuild JupyterLab after making any changes
jupyter lab build

在开发过程中, 你可以监听文件的变化, 然后在文件修改后自动 rebuild 插件和 Jupyter Lab 程序.

1
2
3
4
# Watch the source directory in another terminal tab
jlpm watch
# Run jupyterlab in watch mode in one terminal tab
jupyter lab --watch

Development

搭建开发环境可以见 Jupyter Lab 官方文档

打开 src/index.ts, 找到 extension: JupyterFrontEndPlugin<void>, 修改其中的 activate:

1
2
3
4
5
6
7
8
/**
* The plugin registration information.
*/
const extension: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab_micropython_ext',
autoStart: true,
activate: activate
};

编写 activate 函数:

1
2
3
4
5
6
7
/**
* Initialization data for the jupyterlab_micropython_ext extension.
*/
const activate = async (app: JupyterFrontEnd) => {
app.docRegistry.addWidgetExtension('Notebook', new MicropythonExtension())
console.log('JupyterLab extension jupyterlab_micropython_ext is activated!');
}

然后编写在 activate 中调用的 MicropythonExtension 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* A notebook widget extension that adds a Extension to the toolbar.
*/
export
class MicropythonExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
/**
* Create a new extension object.
*/
createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {

return new DisposableDelegate(() => {});
}
}

接下来需要在面板的工具栏中添加按钮, 首先在 MicropythonExtensioncreateNew 方法中创建一个 ToolbarButton:

1
2
3
4
5
6
7
8
let serialConnectButton = new ToolbarButton({
className: 'serialConnect',
iconClassName: 'fa fa-cloud-plug',
onClick: () => {
console.log("Click ToolbarButton");
},
tooltip: 'Connect device with serialport'
});

把创建好的按钮插入到 toolbar 中:

1
panel.toolbar.insertItem(8, 'Serial Connect', serialConnectButton)

最后要记得在 DisposableDelegatedispose 按钮:

1
2
3
return new DisposableDelegate(() => {
uploadFolderButton.dispose();
});

最后编写按钮的业务逻辑, 把按钮的点击事件修改为一个异步函数:

1
2
3
4
5
6
7
8
9
10
let serialConnectCallback = async () => {
// TODO
};

let serialConnectButton = new ToolbarButton({
className: 'serialConnect',
iconClassName: 'fa fa-plug',
onClick: serialConnectCallback,
tooltip: 'Connect device with serialport'
});

使用 InputDialog 可以设置一个弹出的输入框, 接收输入数据. 使用 input.button.accept 判断是否收到输入:

1
2
3
4
5
6
7
8
9
const input = await InputDialog.getText({
title: 'Serial Port',
okLabel: 'Connect',
placeholder: '/dev/ttyUSB0'
});

if (input.button.accept) {
// TODO
}

使用 context.session.kernel 可以获取当前运行的 kernel, 然后通过 kernel.requestExecute() 方法给 kernel 发送要执行的语句:

1
2
3
4
5
6
7
8
const port = input.value;
const code: string = `%serialconnect --port=${port}`;
const content: KernelMessage.IExecuteRequestMsg['content'] = {
code,
stop_on_error: true
};
kernelrequestExecute(content);
context.session.kernel.requestExecute(content, false);

kernel.requestExecute 执行后会返回的内容是异步的, 可以使用 done 监听是否执行完成, 使用 onIOPub 监听收到的返回数据:

1
2
3
4
5
6
7
8
9
10
11
12
let future = context.session.kernel.requestExecute(content, false);
let result = "";

future.onIOPub = msg => {
if ('name' in msg.content && msg.content.name === 'stdout' && msg.content.text !== undefined) {
result = `${result}${msg.content.text.replace(/\u001b\[\d+m/g, '')}. `
}
};

future.done.then(() => {
showErrorMessage("Kernel Execute Result", result.toString());
});

因为 kernel 返回数据是 ANSI Escape sequences, 所以使用正则表达式 \u001b\[\d+m 过滤: msg.content.text.replace(/\u001b\[\d+m/g, '')

最终的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* A notebook widget extension that adds a Extension to the toolbar.
*/
export
class MicropythonExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
/**
* Create a new extension object.
*/
createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {

let kernelrequestExecute = (content: KernelMessage.IExecuteRequestMsg['content']) => {
let future = context.session.kernel.requestExecute(content, false);
let result = "";

future.onIOPub = msg => {
if ('name' in msg.content && msg.content.name === 'stdout' && msg.content.text !== undefined) {
result = `${result}${msg.content.text.replace(/\u001b\[\d+m/g, '')}. `
}
};

future.done.then(() => {
showErrorMessage("Kernel Execute Result", result.toString());
});
}

let serialConnectCallback = async () => {
const input = await InputDialog.getText({
title: 'Serial Port',
okLabel: 'Connect',
placeholder: '/dev/ttyUSB0'
});
if (input.button.accept) {
const port = input.value;
const code: string = `%serialconnect --port=${port}`;
const content: KernelMessage.IExecuteRequestMsg['content'] = {
code,
stop_on_error: true
};
kernelrequestExecute(content);
}
};

let serialConnectButton = new ToolbarButton({
className: 'serialConnect',
iconClassName: 'fa fa-plug',
onClick: serialConnectCallback,
tooltip: 'Connect device with serialport'
});

panel.toolbar.insertItem(8, 'Serial Connect', serialConnectButton)

return new DisposableDelegate(() => {
serialConnectButton.dispose();
});
}
}

/**
* Initialization data for the jupyterlab_micropython_ext extension.
*/
const activate = async (app: JupyterFrontEnd) => {
app.docRegistry.addWidgetExtension('Notebook', new MicropythonExtension())
console.log('JupyterLab extension jupyterlab_micropython_ext is activated!');
}

/**
* The plugin registration information.
*/
const extension: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab_micropython_ext',
autoStart: true,
activate: activate
};

export default extension;

Uninstall

运行以下命令卸载插件:

1
jupyter labextension uninstall jupyterlab_micropython_ext