TypeScript+rollup.js+EmotionでReact開発できる環境を作ってみた
お久しぶりです。
前回のブログ投稿から、日が経ってしまいました。
ここ最近は個人的に作ってみたいサービスのイメージが湧いたので、AWS CDK(Python)で遊んでいたんですが、フロントエンドまでイメージが広がりました。
これまではVue.jsを使っていたんですが、折角なんで、きちんと触ったことのないReactでやってみようと思いつきました。
前回のブログ投稿からだいぶ日が経ってしまうとは、この時は思ってもいませんでした。
Motivation
Vue.jsにはVue CLIがあり、ReactにもCreate React Appというとても便利なCLIがあります。
今更ながらゼロからReact開発を学ぶので、CLIを利用しない環境構築をしてきちんと仕組みを理解してみようと思いました。
いっそのこと欲張りに全部盛りにしてやってみよう!!!(←ここが日が経った理由)
ということで
- TypeScriptを使ってみよう
- Babelとwebpackを使わないで実現してみよう
- 公式のライブラリを前提に組み上げてみよう
という前提条件を課して進めることをMotivationとしました。
Babelとwebpack以外にしてみようと考えた理由は以下の通りです。
ツール | 理由 |
---|---|
Babel | TypeScriptを利用するのでコンパイルはTypeScriptに任せたい |
webpack | 多機能なので軽量なバンドルの仕組みにしたい |
今回は勉強も兼ねているので、少しだけセオリーから外れた構成で進めてみました。
開発環境の要素を考える
Reactの公式ドキュメントを読み進めながら構成を考えました。
この時点で決めていたのはTypeScriptを利用することだけだったので、検討する要素は以下の2点でした。
スタイルの定義の仕方
CSS とスタイルの使用をみる限り、クラス属性を利用した制御について書かれているのみで、React はスタイルがどのように定義されているかには関心を持ちません。と記載されていました。
コンポーネント単位でCSSは定義したいと考えていたので、サードパーティのライブラリの選定が必要になりました。
利用するバンドラ・コンパイラを決める
ゼロからツールチェインを作成するに記載されています。
パッケージマネージャはnpmとしたので、バンドラ、コンパイラの選定が必要になりました。
構成を検討する
なお、公式ドキュメントに記載のあったParcelはバンドルした結果のファイル名が制御できないので、今回の想定からは外しました。
CSSのライブラリはいくつか調査しましたが、Styled ComponentsはHTML要素(例えばbuttonなど)にまで影響を与える実装の仕方だったので、今回の想定からは外しました。
これらの要素で開発環境の構成を再検討しました。
実現方法 | 理由 |
---|---|
browserify+CSS Modules | NG CSS Modulesのbrowserify公式プラグインがhighly experimentalのステータスのまま |
rollup.js+CSS Modules | NG CSS Modulesのrollup公式プラグインが存在しない |
rollup.js+Emotion | OK JavaScriptでcssを定義するので、rollup公式プラグインのnode_modulesの解決プラグインで対応できる |
実際に環境構築を行い、前提条件を満たすことができる構成はrollup.js、Emotionの組み合わせかな?と思ったので、一旦こちらの構成で進めることとしました。
環境構築手順
TypeScript、rollup.js、Emotionの構成で開発環境を構築してみたいと思います。
appディレクトリを作成
まずはappディレクトリを作成します。
ソースコードはsrcディレクトリ、ビルドした結果はpublicディレクトリに配置します。
mkdir react-app cd react-app npm init -y mkdir src mkdir -p public/scripts
ディレクトリを作成したら、npm initでpackage.jsonの最低限の設定を行います。
必要なパッケージのインストール
次に、React、TypeScript、rollup.js、Emotionに必要なパッケージのインストールを行います。
npm i -D react react-dom @types/react @types/react-dom npm i -D typescript tslib npm i -D rollup npm i -D @rollup/plugin-typescript @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-replace npm i -D @emotion/react
TypeScriptと一緒にtslibをインストールしています。これはrollup.jsのTypeScriptプラグインに必要なためです。
rollup.jsは4つプラグインをインストールしています。
プラグイン | 理由 |
---|---|
@rollup/plugin-typescript | TypeScriptを利用するため |
@rollup/plugin-commonjs | CommonJSのモジュールをES6に変換するため |
@rollup/plugin-node-resolve | ローカルのnode_modulesにあるサードパーティのライブラリをバンドルするため |
@rollup/plugin-replace | バンドルする際に対象文字列を置換するため |
@rollup/plugin-replaceは追加で入れたプラグインで、バンドル時のエラーで、process.env.NODE_ENVが解釈できないとエラーを解決するために追加しました。
プラグインのREADMEをみる限り、最も多い利用するケースのようです。
tsconfigの設定
次に、tsconfigの設定を行います。
node_modules/.bin/tsc --init
を実行するとrootディレクトリ直下にtsconfig.jsonが生成されます。
下記の内容は動くための最低限の設定です。
{ "compilerOptions": { /* Basic Options */ "target": "es5", "module": "ES2015", "lib": [ "es2015", "dom" ], "allowJs": true, "jsx": "react", "declaration": true, "declarationMap": true, "sourceMap": true, "outDir": "./public/scripts", "rootDir": "./src", "removeComments": true, /* Strict Type-Checking Options */ "strict": true, /* Module Resolution Options */ "moduleResolution": "node", "esModuleInterop": true, /* Advanced Options */ "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": [ "./src/**/*" ] }
rollup.config.jsの設定
次に、rollup.jsの設定を行います。
rootディレクトリ直下にrollup.config.jsを作成し、バンドルするための設定を定義します。
- intput:entryポイントとなるファイルを指定
- output:バンドルした結果の出力先を指定
- plugins:利用するプラグインを指定
import typescript from '@rollup/plugin-typescript'; import resolve from "@rollup/plugin-node-resolve"; import commonjs from "@rollup/plugin-commonjs"; import replace from '@rollup/plugin-replace'; export default { input: 'src/App.tsx', output: [ { file: 'public/scripts/bundle.js', format: "cjs", sourcemap: true }, { file: 'public/scripts/bundle.es.js', format: "esm", sourcemap: true } ], plugins: [ replace({ // alternatively, one could pass process.env.NODE_ENV or 'development` to stringify 'process.env.NODE_ENV': JSON.stringify('development') }), commonjs(), resolve(), typescript() ], };
動作確認
最後に動作確認を行うためのコードを定義します。
ディレクトリ構成と各種ファイルは以下のようにします。
index.html
index.htmlは以下の通りです。
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>test</title> </head> <body> <div id="app"> </div> <script type="text/javascript" src="./scripts/bundle.js"></script> </body> </html>
Welcome.tsx
Welcome.tsxは以下の通りです。
Emotion.jsはcss属性を利用するのですが、公式ドキュメントに2つの方法が記載されています。
- Babel Preset
- JSX Pragma
今回はBabelを利用しないようにしているので、JSX Pragmaを利用しました。
/* @jsx jsx */ import React from "react"; import { css, jsx } from '@emotion/react'; interface WelcomeProps { name: string } const style = css` color: hotpink; `; export default class Welcome extends React.Component<WelcomeProps, {}> { render() { return <div css={style}>Hello, {this.props.name}</div>; } }
App.tsx
entryファイルとなるApp.tsxは以下の通りです。
import React from 'react'; import ReactDOM from 'react-dom'; import Welcome from "./components/atoms/Welcome"; ReactDOM.render( <Welcome name="React" />, document.getElementById('app') );
これらの設定が終わったら、ビルドを実行します。
package.jsonのscriptsにbuildコマンドとして「rollup -c」を追加します。
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "rollup -c" }
追加できたら「npm run build」を実行してください。rollup.config.jsに定義した出力先にバンドルされたファイルが生成されます。
index.htmlを開くと以下の画面が表示されます。
無事、ビルドが成功しました!!!
まとめ
ひとまず、Reactで書いたコンポーネントをビルドしてブラウザで表示することができるようになりました。
設定周りの考慮など、まだ足りていない部分があるかもしれませんが、分かったらUPDATEしていきたいと思います。
当初の想定では去年の12月にはこの記事を投稿しているはずだったんですが、だいぶ手間取りました。。。
私のフロントエンド周りの知識が不足していることが分かってよかったです。
ようやく、本来のReactでのフロントエンド開発を行う下地ができたので、進めていきたいと思います。
ではでは。
PyCon JP 2020に参加してきました。
今回、初めてPyCon JP 2020にてトーク採択という良いチャンスをもらい、
初めてづくしで疲れてしまいました。
オンラインパーティの中締めで失礼して寝ようかなと思ったけど
ふとブログに感想書いてみようと頭をよぎったので、
若干の酔いに任せて、まとまってないけど書いてみました。
Speakerとしての感想 は当日に書いた感想です。
セッションとかの感想は後日、アップデートします。
各セッションの感想
個人的には色んなラインナップのあるセッションだったなあと感じました。
Webアプリ開発やDeepLearningのセッションはベースとしてありながらも、それ以外のセッションがあったので、個人的にはとても楽しかったです。
去年の初参加はWebアプリ開発、 DeepLearning系に参加したので、今回はそれ以外に参加してみようと思いました。
1日目
- 2020年代のコンテナ時代のPythonアーキテクチャ&デプロイ
- PythonからGolangに変更してから再びPythonに戻った理由
- Django + SQLAlchemy: シンプルWay
- GAE/Python2 to Python3 Migration Journey
- Pythonではじめるソフトウェア無線
個人的な本命は、Django + SQLAlchemy: シンプルWayでした。Django-ORMで苦しんだことがあるので、とても良かったです。説明いただいた構成が無理なく進める上では良いんだろうなと感じました。もう一度資料を見直してきちんと理解したいと思います。
予想を裏切られるくらい面白かったのは、Pythonではじめるソフトウェア無線でした。奥深い世界と発表者の方がめちゃくちゃ詳しくて圧倒されました。
2日目
事前準備、個人リハーサルしていて、セッションは見れませんでした。。。
今週の土日に見直そうと思っています。
基調講演の感想
基調講演:芝世弐氏
将棋の話はとても面白かったです。
あそこまで飄々とされて結果が出ていると、脱帽という感じでした。
ただ、ゴールに向かって必要なことを淡々と進めている印象を受けました。
研究職の方と仕事をしたことがありますが、その時と同じ印象で、自分が見ている次元より上の次元でモノゴト捉えているんだよなーと改めて感じました。
Keynote: Mr. Rich Jones
私が15年間エンジニアとして、いくつかのカンファレンスでキーノートを聞いてきましたが、トークの構成や使う言葉を含めてNo1のトークだったと思います。
これをライブで体験できたのは良かった。
シビックテックに対して関わりたいとStay Home中に感じたので、刺さるものが多かったです。
State-lessはそういうことかーーって思いました。
(バランスを取りたがる私としては、この話は惹かれつつ、踏み込みすぎるのは厳しいという"はざま"にいます)
とりあえず、色んな人に見て欲しいので、Facebookやチームの人に共有してきました。
Speakerとしての感想
PyCon JPには去年、初めて参加して色んな気遣いのあるカンファレンスだなーと感じ、普段仕事ではPython使ってないけど、今回のカンファレンスに参加してみようと思いました。
普段、トークを聴く側の参加者でしかなかった私ですが、今回トークする側にまわったことで裏側を見ることができ、さらに色んなことに気遣いがあるなーと感じました。
- リハーサルなどカンファレンスまでの準備
- 当日のトークセッションをスムーズに行うためのオペレーション
- キーノートの同日通訳はとても最高でした!
当然、改善すべきことはあるかもしれませんし、これが全てOKかというとそうではないとは思いますが(私の見えないところには何かあったかもしれない) 技術カンファレンスの裏側を支えるスタッフの方々は今取れる最善をとって進めていただいたと感じました。
PyCon JPだけではなく、いろんな技術カンファレンスがあって、それぞれに裏方のスタッフの方がいると思います。
今回、初めてトークする側に回って、一層、スタッフの方々に対する敬意の気持ちしかないなと思いました。
この辺りはスタッフなどコミュニティを支える活動をしていない私だからの感想かもしれませんが、こういった貢献の仕方もあるんだなと改めて感じました。
なんかもう、コミュニティやカンファレンスを運営している人たちすげーな。
自分のトークに対する感想とかよりも、それが一番の感想になっちゃいました。
PyCon JPを支えているスタッフの方々、全てに感謝いたします。
ではでは。
toioをpythonで動かす:番外編(型で学んだこと)
はじめに
前回は「toioをpythonで動かす:BLE操作」を行いました。
今回はちょっと番外編です。
これまでtoio.jsのコードを読みながら、これってpythonで書くとどうなるんだ?と考えながら書いていました。
私自身pythonのAPIドキュメントを全て読んでいたわけではなかったので、割と新しい発見がありました。
型で学んだこと
Union型/Optional型
toio.jsのコードはtypescriptで書かれています。
その中で以下のようなコードが出てきました。
export type DataType = | { buffer: Uint8Array data: PositionIdInfo dataType: 'id:position-id' } | { buffer: Uint8Array data: StandardIdInfo dataType: 'id:standard-id' } | { buffer: Uint8Array dataType: 'id:position-id-missed' } | { buffer: Uint8Array dataType: 'id:standard-id-missed' }
これは { buffer: Uint8Array … } のそれぞれを型とし、DataTypeにはいずれかの型が入ることを示しています。
これをpythonで実現しようとするとtype hintのUnion型で対応することになります。
typing --- 型ヒントのサポート — Python 3.8.3 ドキュメント
pythonでも匿名クラスはありますが、ここで匿名クラスにしてコードが読みづらかったので、別途クラスを定義しました。
from typing import Union from toiopy.data import ( PositionIdType, StandardIdType, IdMissedType ) class Hoge: DataType: Union[ PositionIdType, StandardIdType, IdMissedType ]
これでPositionIdType、StandardIdType、IdMissedTypeのいずれかの型が入るDataTypeとなります。
また、似たケースでNullを許容する場合もあります。
private timer: NodeJS.Timer | null = null
この場合、Union型ではなく、Optional型を利用することで同様のことが実現できます。(pythonなのでNullではなく、Noneですが)
typing --- 型ヒントのサポート — Python 3.8.3 ドキュメント
from typing import Optional from toiopy.data import TImer class Fuga: timer: Optional[TImer] = None
typescriptにはinterfaceという概念があり、完全に同じ振る舞いに書き換えることはできないですが、代替することができました。
Enumの機能API
export enum Note { C0 = 0, CS0, D0, DS0, E0, F0, FS0, G0, GS0, A0, AS0, B0, C1, CS1, D1, DS1, E1, F1, FS1, G1, GS1, A1, AS1, B1, C2, CS2, D2, DS2, E2, F2, FS2, G2, GS2, A2, AS2, B2, C3, CS3, D3, DS3, E3, F3, FS3, G3, GS3, A3, AS3, B3, C4, CS4, D4, DS4, E4, F4, FS4, G4, GS4, A4, AS4, B4, C5, CS5, D5, DS5, E5, F5, FS5, G5, GS5, A5, AS5, B5, C6, CS6, D6, DS6, E6, F6, FS6, G6, GS6, A6, AS6, B6, C7, CS7, D7, DS7, E7, F7, FS7, G7, GS7, A7, AS7, B7, C8, CS8, D8, DS8, E8, F8, FS8, G8, GS8, A8, AS8, B8, C9, CS9, D9, DS9, E9, F9, FS9, G9, GS9, A9, AS9, B9, C10, CS10, D10, DS10, E10, F10, FS10, G10, NO_SOUND }
このコードをみて、OH...ってなりました。
pythonだとインデントが必要になるので、これ全部変換するのか。。。。
ありました。解決する方法が。
Enumに機能APIがあり、文字列からEnum型を定義することが可能です。
enum --- 列挙型のサポート — Python 3.8.3 ドキュメント
Animal = Enum('Animal', 'ANT BEE CAT DOG')
と
class Animal(Enum): ANT BEE CAT DOG
は同義になります。
また、C0=0となっているので、同様に開始する要素の値を設定することもできます。(defaultは1になります)
これらを踏まえてtoio.jsで定義されているEnumをpythonで実現した内容は以下の通りです。
names = """ C0,CS0,D0,DS0,E0,F0,FS0,G0,GS0,A0,AS0,B0, C1,CS1,D1,DS1,E1,F1,FS1,G1,GS1,A1,AS1,B1, C2,CS2,D2,DS2,E2,F2,FS2,G2,GS2,A2,AS2,B2, C3,CS3,D3,DS3,E3,F3,FS3,G3,GS3,A3,AS3,B3, C4,CS4,D4,DS4,E4,F4,FS4,G4,GS4,A4,AS4,B4, C5,CS5,D5,DS5,E5,F5,FS5,G5,GS5,A5,AS5,B5, C6,CS6,D6,DS6,E6,F6,FS6,G6,GS6,A6,AS6,B6, C7,CS7,D7,DS7,E7,F7,FS7,G7,GS7,A7,AS7,B7, C8,CS8,D8,DS8,E8,F8,FS8,G8,GS8,A8,AS8,B8, C9,CS9,D9,DS9,E9,F9,FS9,G9,GS9,A9,AS9,B9, C10,CS10,D10,DS10,E10,F10,FS10,G10, NO_SOUND """ Note = Enum( "Note", names, module=__name__, qualname="toiopy.data.Note", start=0, )
Enum( value='NewEnumName', names=<...>, *, module='...', qualname='...', type=<mixed-in class>, start=1 )
namesの指定の仕方はいくつかあり、空白またはカンマで区切った文字列でも構いません。
'RED GREEN BLUE' | 'RED,GREEN,BLUE' | 'RED, GREEN, BLUE'
または名前のイテレータで指定もできます:
['RED', 'GREEN', 'BLUE']
または (名前, 値) のペアのイテレータでも指定できます:
[('CYAN', 4), ('MAGENTA', 5), ('YELLOW', 6)]
またはマッピングでも指定できます:
{'CHARTREUSE': 7, 'SEA_GREEN': 11, 'ROSEMARY': 42}
利用シーンは限定的かもしれませんが、Enumには機能APIがあると頭の片隅に置いておいて良さそうです。
mypy
ここまで、typescriptの型の表現をpythonで実現するにはということで話してきましたが、type hintは、あくまでヒントであって実行時には影響を与えません。(文法エラーで失敗するわけではない)
ここで、型のチェックはmypyを使って型の状態を静的にチェックします。
インストール
導入するのは簡単です。 こちらのコマンドを実行してください。
これまでと同様にvenvをactivateしてから実行してください。
pip install mypy
型チェックする
これで型チェックが可能となります。
mypy <ディレクトリ名>
実際に私自身が作成したクラスなどは問題なく型チェックができたのですが、pipなどでinstallしたモジュールでエラーが出る場合があります。
それは型定義ファイルがないから発生するのですが、その場合はmypy.iniを作成することで対応可能です。
[mypy-Adafruit_BluefruitLE.*] ignore_missing_imports = True
この場合は、BLE操作でinstallしたAdafruit_BluefruitLEに対する型チェックを行わないようにしています。
また、一部だけ型チェックを無効化にする方法もあります。
無視したいコードの行末尾に#type: ignoreのコメントをつけます。
from typing import Optional from toiopy.data import TImer class Fuga: timer = None #type: ignore
本来はこういった使い方はあまり良くないとは思いますが、必要になったケースもありました。
この投稿にも書いたEnumの機能APIです。
私が使っていたバージョンでは機能APIを用いるとうまく対応されておらず、エラーになるので、このような対応をしました。
# mypyの不具合で機能APIのEnumはtype ignoreとする Note = Enum( # type: ignore "Note", names, module=__name__, qualname="toiopy.data.Note", start=0, )
ちなみに、まだ試していないですが、mypyのEnum機能APIの対応を行うIssueはclosedに変わりました。
Support functional API for Enum creation · Issue #2306 · python/mypy · GitHub
mypyのバージョンをあげたら解決するかもしれません。
まとめ
typescriptからpythonに置き換えることで型について学ぶことが多かったです。
エンジニアとしての始まりがJavaなどの静的型言語から始めた人間としては、型がある方が安心してしまうのは習性のようなものかなと思いました。
toioをpythonで動かす:BLE操作
はじめに
前回は「toioをpythonで動かす:バイナリデータ操作」を行いました。
今回から本丸のBLE操作を行って、toioを制御したいと思います。
BLE入門
BLEはBluetooth Low Energyの略で、Bluetoothを用いて通信を行う規格のことをです。
toioではキューブとの通信は Bluetooth® 標準規格 Ver. 4.2で行っているとのことです。
通信概要 · toio™コア キューブ 技術仕様
BLEにはセントラル(Central)とペリフェラル(Peripheral)の2種類の役割があります。
通信のホストとなるデバイスをセントラル、何かしらの情報や機能を提供するデバイスをペリフェラルと呼びます。
今回の実装は、toioのキューブがペリフェラルで、pythonで実装しようとしているのがセントラルの部分です。
BLEは規格なので、やり取りするための手順やデータ構造は定まっていて、GATT(Generic attribute profile:汎用アトリビュートプロファイル)と呼ばれています。
このGATTはサービス(Service)とキャラクタリスティック(Characteristic)から構成されています。
toioのキューブが提供するサービスには8つのキャラクタリスティックが存在します。
サービス、キャラクタリスティックはUUIDで識別して操作することとなります。
- ID Information / 読み取りセンサー
- Sensor Information / モーションセンサー
- Button Information / ボタン
- Battery Information / バッテリー
- Motor Control / モーター
- Light Control / ランプ
- Sound Control / サウンド
- Configuration / 設定
BLE操作ライブラリの導入
pythonでBLEを操作するライブラリは存在しています。少し調べた中では以下の2つが検索して見つかりました。
- Adafruit_BluefruitLE
- Mac、Linuxで動かすことができる
- 参考スライド:Mac から Python で BLE ペリフェラルを操作する
- bleak
リンクも張りましたが、参考にしたスライドが最初分かりやすかったので、Adafruitを用いることにしました。
windows環境の方はbleakを用いて試してみてください。番外編として後ほど記事を投稿します。
環境構築
まずは必要なライブラリをpipでインストールします。前の記事で書いた通り、venvを有効にして(activate)おいてください。
Macを利用する場合の前提条件にpyobjcをインストールすることと書いてあるので、まずそちらを実行します。
pip install pyobjc
次にAdafruitをインストールしますが、github上のコードをcloneしてからsetup.pyを実行します。
git clone https://github.com/adafruit/Adafruit_Python_BluefruitLE.git
cd Adafruit_Python_BluefruitLE
sudo python setup.py install
サンプルコードを配置するディレクトリを作成して環境構築は完了です。
cd .. mkdir bleapp cd bleapp touch main.py
toioに接続する下準備を行う
Adafruit_BluefruitLEを利用する上で下準備となる実装を行います。
import Adafruit_BluefruitLE def main(): # ここにメインの処理を実装します if __name__ == '__main__': provider = Adafruit_BluefruitLE.get_provider() provider.initialize() provider.run_mainloop_with(main)
まずは、Adafruit_BluefruitLEからproviderを取得します。
取得したproviderを初期化して、run_mainloop_withを呼び出すところまではこの順番の通りに実施して下さい。
Adafruit_BluefruitLEのライブラリがBLEを操作するために必要な準備を行ってくれています。
- BLEはOSのネイティブライブラリを呼び出すため、OS毎に異なります。Macはcorebluetooth、linuxはbluez_dbusです。
- get_providerで取得したproviderはOS毎のライブラリの差分を吸収してBLE操作を行います。
toioをスキャンする
動かすための下準備のコードを実装したら、次はメインの実装を行います。
def main(): provider.clear_cached_data() adapter = provider.get_default_adapter() adapter.power_on() # adapterがスキャンを始める adapter.start_scan() # adapterがスキャンしている間にペリフェラルを探す # (deviceという名前になってます) device = provider.find_device() # 取得できたら、スキャンを止める adapter.stop_scan()
provider経由でadapterを取得します。
Adafruit_BluefruitLEのadapterはペリフェラルをスキャンする機能を提供しています。
これはあくまでAdafruit_BluefruitLEでの役割なので、BLE操作に必ずadapterという名前のモジュールがあるわけではないです。
adapterでスキャンを開始している間、provider経由でペリフェラル(メソッド名はdeviceですが)を探します。
ペリフェラルが見つかったら、不要なのでスキャンを止めます。
toioを動かす
ペリフェラルを取得できたら、接続して実際に動かす実装を行います。
行数が多いので順番に解説します。
まず、取得したペリフェラル(=device)に接続します。
接続した後、sleepで3秒程待ちます。
device.connect()
time.sleep(3)
Macの場合、内部ではcorebluetoothを利用して操作しているのですが、接続した後に必要な情報は同期で返却されないので、3秒待っています。 (3秒が妥当な数値ではないので適宜直してみて下さい)
次に、ペリフェラルから実際に動かすために必要なキャラクタリスティックを取得します。
device.discover([service_uuid], [motor_uuid]) service = device.find_service(service_uuid) chara = service.find_characteristic(motor_uuid)
uuidはtoioの技術仕様に書かれています。
最後に、toioを動かすためにペリフェラルに動かすために必要情報を書き込みます。
書き込む内容はtoioの技術仕様を参考にしました。
l_direction = 1 r_direction = 1 l_power = 100 r_power = 100 duration = 100 bdl = [ 2, 1, l_direction, l_power, 2, r_direction, r_power, duration ] size = len(bdl) byte_data = pack('B'*size, *bdl) chara.write_value(byte_data) time.sleep(5) device.disconnect()
前回の記事でも書きましたバイナリデータの書き込みも、早くもここで活用します。
書き込んだあと、実際に動作させるため5秒待ちます。
(すぐdisconnectを呼び出すとtoioが動く前に処理が終わってしまいます)
一番最後にdisconnectを呼び出して、ペリフェラルとの接続を閉じます。
実際に動かしてみる
これまでに書いたコードを実際に動作させてみます。
当たり前ですが、MacのBluetoothは有効にしておいて下さい。(私は最初、有効するの忘れてて、なんで動かないのか10分くらい戸惑いました)
python main.py
実際に動くので、toioは床などの広い場所においてから実行して下さい。
まとめ
ひとまず、BLE操作をpythonで行うことができました。
技術仕様が公開されているので、この仕様とtoio.jsのコードを読めば、ある程度、理解することは可能でした。
試しに動かすためのコードはこちらに書いておきます。
from uuid import UUID import time from struct import pack import Adafruit_BluefruitLE def main(): service_uuid = UUID('10b201005b3b45719508cf3efcd7bbae') motor_uuid = UUID('10b201025b3b45719508cf3efcd7bbae') provider.clear_cached_data() adapter = provider.get_default_adapter() adapter.power_on() adapter.start_scan() device = provider.find_device() adapter.stop_scan() device.connect() time.sleep(3) device.discover([service_uuid], [motor_uuid]) service = device.find_service(service_uuid) chara = service.find_characteristic(motor_uuid) l_direction = 1 r_direction = 1 l_power = 100 r_power = 100 duration = 100 bdl = [2, 1, l_direction, l_power, 2, r_direction, r_power, duration] size = len(bdl) byte_data = pack('B'*size, *bdl) chara.write_value(byte_data) time.sleep(5) device.disconnect() if __name__ == '__main__': provider = Adafruit_BluefruitLE.get_provider() provider.initialize() provider.run_mainloop_with(main)
最初はBLE操作の実装の仕方が分からなかったので、Adafruit_BluefruitLEのコードも読んで何となく構造は分かったのですが、その辺りは余裕があったらブログに書きたいと思います。
次回は、少し寄り道で、mypy、flake8、blackを導入して試した話を書きます。
今回のそもそもの目的はtoio.jsのコードをpythonに置き換えることです。
toio.jsはtypescriptで書かれていて、型安全になっているのでmypyを入れてみました。
ついでではありますが、flake8とblackも入れて試してみたので、その辺りを書ければと思います。
toioをpythonで動かす:バイナリデータ操作
はじめに
前回から「toioをpythonで動かす」を始めました。
toio.jsのソースコードを見ると、BLE操作を行うため、バイナリデータの読み書きを行っています。
今回はjavascriptで書かれているコードをpythonで実現したいと思います。
pythonでバイナリデータを扱う
pythonでバイナリデータを扱う場合はbytesかbytearrayを用います。
bytes オブジェクトは不変なオブジェクトで、bytearray オブジェクトは可変なオブジェクトです。
不変と可変を理解するために下のコードを試してみました。
>>> from struct import pack_into >>> >>> data = bytes(b'\x01\x02') >>> data b'\x01\x02' >>> pack_into("B", data, 1, 5) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: argument must be read-write bytes-like object, not bytes >>> >>> data = bytearray(b'\x01\02') >>> data bytearray(b'\x01\x02') >>> >>> pack_into("B", data, 1, 5) >>> data bytearray(b'\x01\x05')
pythonでバイナリデータを操作する場合はstructを用います。
struct --- バイト列をパックされたバイナリデータとして解釈する — Python 3.8.3 ドキュメント
bytesオブジェクトをstruct.pack_intoを用いて書き換えようとするとTypeErrorが発生します。
bytearrayオブジェクトの場合は書き換えが可能で、bytearray(b'\x01\x02')がbytearray(b'\x01\x05')に書き換わっています。
このことからbytesが不変なオブジェクトで、bytearrayが可変なオブジェクトであることがわかると思います。
バイナリデータを読み込む
一例ですが、toio.jsを見てみるとこのような形で実装されています。
const type = buffer.readUInt8(0) const standardId = buffer.readUInt32LE(1) const angle = buffer.readUInt16LE(5)
readUInt8メソッドは、bufferに対して指定されたオフセットの位置から符号なし8ビット整数を読み取っています。
pythonでバイナリデータを読み込む場合は以下のメソッドで対応できます。
- unpack(format, buffer)
- unpack_from(format, buffer, offset=0)
formatは書式指定文字を指定します。
8bit符号なし整数はunsigned charの整数なので"B"を指定します。
上記の例ではoffsetを指定しているのでunpack_fromを用います。
from struct import unpack_from data = bytes(b'\x01\x02\x03\x04\x05\x06\x07') type_data = unpack_from("B", data, 0)[0]
2バイト、4バイトの読み込みは書式指定文字を変更することで行います。
また、javascriptでのソースコードはリトルエンディアンで読み込みを行っているので、バイトオーダーを指定する文字(<)を指定します。
standardId = unpack_from("<I", data, 1)[0] angle = unpack_from("<H", data, 5)[0]
バイナリデータを書き込む
書き込みも同じような形で実装されています。
const buffer = Buffer.alloc(3)
buffer.writeUInt8(4, 0)
buffer.writeUInt16LE(5, 1)
ただし、書き込む前に書き込む領域を割り当てる必要があります。
allocメソッドは書き込み領域を割り当て、writeUInt8メソッドは、bufferに対して指定されたオフセットの位置から符号なし8ビット整数、16ビット整数を書き込んでいます。
pythonでバイナリデータを書き込む場合は以下のメソッドで対応できます。
- pack(format, v1, v2, ...)
- pack_into(format, buffer, offset, v1, v2, ...)
python用の
formatは読み込みと同じように書式指定文字を指定します。
2バイトの書き込みは書式指定文字を指定し、バイトオーダーを指定する文字(<)を指定します。
from struct import pack_into class Buffer: def __init__(self, byte_data: bytearray): self._byte_data = byte_data self.bytelength = len(byte_data) @classmethod def alloc(cls, size: int): return cls(bytearray(size)) def write_uint8(self, value: int, offset: int): return pack_into("B", self._byte_data, offset, value) def write_uint16le(self, value: int, offset: int): return pack_into("<H", self._byte_data, offset, value buffer = Buffer.alloc(3) buffer.write_uint8(4, 0) buffer.write_uint16le(12, 1)
バイナリデータを操作するためのBufferクラスを定義し、 alloc、writeメソッドを定義しました。
まとめ
今回、バイナリデータの割り当て、読み込み、書き込みを行うためBufferクラスを定義しました。
toioに対するBLE操作はこのクラスを用いて行います。
class Buffer: def __init__(self, byte_data: bytearray): self._byte_data = byte_data # 8bit符号なし整数(unsigned char) self.bytelength = len(byte_data) @property def byte_data(self): return bytearray(self._byte_data) @classmethod def from_data(cls, data_array: List): size = len(data_array) byte_data = pack("B" * size, *data_array) return cls(bytearray(byte_data)) def read_uint8(self, offset: int): # unpackした結果はtupleになっている return unpack_from("B", self._byte_data, offset)[0] def read_uint16le(self, offset: int): # unpackした結果はtupleになっている return unpack_from("<H", self._byte_data, offset)[0] def read_uint32le(self, offset: int): # unpackした結果はtupleになっている return unpack_from("<I", self._byte_data, offset)[0] def write_uint8(self, value: int, offset: int): return pack_into("B", self._byte_data, offset, value) def write_uint16le(self, value: int, offset: int): return pack_into("<H", self._byte_data, offset, value) def to_str( self, encoding: str = "utf-8", start: int = 0, end: Optional[int] = None ): if end is None: end = self.bytelength return self._byte_data[start:end].decode(encoding) @classmethod def alloc(cls, size: int): return cls(bytearray(size))
次回はtoioのBLE操作を行いたいと思います。
toioをpythonで動かす
toio とは
コロナの影響で外出を控えているので家にいる時間がだいぶ増えました。
手元にtoioがあったんですが、開発用ライブラリを公開するまで待とうと思ってだいぶ塩漬けになってました。
今回、ちょうど良かったのでtoioを使っていろいろ遊んでみようと思います。
toioはSony Interactive Entertainmentが開発した教育用ビデオゲームコンソールです。
キューブ型のロボットとモノクロディスプレイ付きのドッキングステーション、コントローラーがあり、ソフトウェア(カートリッジ)を切り替えることで様々な遊びを行うことができます。
詳しくは公式サイトをみてください。
toioの技術仕様
githubを見てみるとjavascriptのライブラリは公開されています。
技術仕様も公開されているので、仕組みを理解するためにpythonに書き換えてみることにしました。
モチベーション
toio.jsで公開されているライブラリと限りなく同じ振る舞いを行うpythonのライブラリを実装します。 ブログ記事は複数回に分かれます。各記事は以下のリンクから見てください。
環境
今回は以下の環境で試しました。
それでは、開発環境を構築したいと思います。
pythonをインストールする
人によってはpythonのバージョンを分けたい方がいればpyenvをインストールしてください。
Catalinaだと問題があった気がするのですが、だいぶ前なので忘れてしまいました。。。。
Home · pyenv/pyenv Wiki · GitHub
特に問題がない方はそのまま、pythonをインストールしてください。
仮想環境を作る
各種ライブラリは他の開発に影響を与えたくないので、venvを用います。
cd workspace # 好きな場所で良いです。 mkdir toiopy # 仮想環境のルートディレクトリです cd toiopy python -m venv venv
仮想環境を有効にする
仮想環境を作ったら、有効にします。 toiopyデイレクトリ直下にvenvというディレクトリがいるはずです。
source venv/bin/activate which python python -V
pythonのパスを確認したら、venvの下にあるpythonのパスに切り替わっているはずです。
github
絶賛開発中ですが、少し動くようになったコードはこちらです。
それでは、次回の記事から実際にコードを書いて理解したことを投稿していきます。
Pythonで動的にクラスロードして実行してみた
今、個人的な開発でオープンデータを取集して解析するコード書いています。
オープンデータで公開しているファイルの書式がバラバラでparserを多く書くことになりそうなんで、pythonで動的なクラスロードの仕組みを調べました。
(parserを追加するごとに呼び出しもとをコードを修正するのは大変なので)
実現したいこと
- parserは所定のディレクトリに配置されたら、動的に読み込まれる対象となる
- 読み込まれるモジュールはparserの基底クラスを継承している
- 基底クラスの特定メソッドを呼び出して処理を実行する
Pythonで動的にクラスロードして実行
前提条件
以下の環境で試しました。
抽象クラスとabstract method
検索してみると抽象基底クラス(Abstract Base Class:ABC)というものが準備されているとのことです。
abc --- 抽象基底クラス — Python 3.7.6 ドキュメント
以下の構成でファイルを作りました。
- main.py:実行モジュールです
- parserディレクトリ
抽象基底クラス
# base_parser.py from abc import ABCMeta, abstractmethod class BaseParser(metaclass=ABCMeta): @abstractmethod def parse(self): pass
まず、抽象基底クラスはABCMetaを継承します。
具象クラスに実装させたいメソッドにはabstractmethodデコレータを追加します。このメソッドに実装を定義できないので、passを定義します。
これでBaseParserクラスを継承したクラスは必ずparseメソッドを実装することになります。
具象クラス
試しにbunkyoku_parser.pyにabstractmethodを実装しない状態でインスタンス化するとエラーになります。
# bunkyoku_parser.py from parser.base_parser import BaseParser class BunkyokuParser(BaseParser): def echo(self): print("echo bunkyoku")
echoメソッドではなく、parseメソッドに変えてからBunkyokuParserクラスをインスタンス化するとエラーにならず、正常に実行できました。
# bunkyoku_parser.py from parser.base_parser import BaseParser class BunkyokuParser(BaseParser): def parse(self): print("echo bunkyoku")
importlibで動的にモジュールを読み込む
main.pyに動的にモジュールを呼び出すコードを定義します。
# main.py import os import importlib def parse(): package_name = 'parser' parser_dir = os.listdir(package_name) skip_class_name = ['__init__.py', 'base_parser.py'] for file_name in parser_dir: if file_name.endswith('py') and \ file_name not in skip_class_name: module_name = os.path.splitext(file_name)[0] classpath = package_name + '.' + module_name parser = importlib.import_module(classpath) if __name__ == '__main__': parse()
parserパッケージ配下の一覧をos.listdir(usrsrc) で取得します。
skip_class_nameにはクラスロード対象外のファイル名を定義しておきます。
package_name = 'parser' parser_dir = os.listdir(package_name) skip_class_name = ['__init__.py', 'base_parser.py']
上記で取得した一覧から「拡張子がpy」かつ「skip_class_nameに含まれていないファイル」を処理対象とします。
for file_name in parser_dir: if file_name.endswith('py') and \ file_name not in skip_class_name:
処理対象のファイル名から拡張子を除いたものをモジュール名としてclasspathを生成します。(例:bunkyoku_parser.py → parser.bunkyoku_parser)
生成したclasspathをもとにimportlib.import_moduleでモジュールを読み込みます。
module_name = os.path.splitext(file_name)[0] classpath = package_name + '.' + module_name parser = importlib.import_module(classpath)
これでモジュールを読み込むことができました。
動的に読み込んだモジュールから特定のメソッドを呼び出す
モジュールを読み込むことが出来たので、モジュールの中に含まれているクラスを呼び出して実行します。
読み込んだモジュールの中に含まれるクラス名は読み込んだ時点で分からないので、inspect経由でオブジェクトの情報を取得します。
inspect --- 活動中のオブジェクトの情報を取得する — Python 3.7.6 ドキュメント
parser = importlib.import_module(classpath) clazz_list = inspect.getmembers(parser, inspect.isclass) for clazz in clazz_list: if clazz[0] != 'BaseParser': instance = clazz[1]() instance.parse()
import_moduleで読み込んだモジュールからクラス一覧を取得します。
parser = importlib.import_module(classpath) clazz_list = inspect.getmembers(parser, inspect.isclass)
取得したクラス一覧から抽象基底クラスであるBaseParserは除外して、それ以外のクラスのインスタンスを生成します。
ここで登場するクラスはBaseParserクラスを継承しているので、必ずparseメソッドが存在します。
最後にinstance.parse()を実行することで動的にロードしたクラスの特定メソッドを実行することが出来ます。
for clazz in clazz_list: if clazz[0] != 'BaseParser': instance = clazz[1]() instance.parse()
実行した結果はこちらです。
各モジュールに含まれるBaseParserクラスを継承したクラスが実装したparseメソッドが実行されています。
まとめ
ひとまず、Pythonで動的にクラスロードすることができました。
本来であれば、BaseParserクラスを継承しているか、継承していない場合のエラーハンドリングのチェックが必要ですが、今回の実装では行っていません。
ソースコードはこちらのRepositoryに配置しておきました。