CircuitJS1をElectronアプリ化する

電子回路のシミュレーションツールを探していたところ、ブラウザ上で動的にシミュレートするサービス CircuitJS1 を見つけました。

Circuit Simulator Applet

ちょうどQucsという他の電子回路シミュレータを触り始めたところだったのですが、Qucsでは回路に変更を加えるたびにシミュレーションコマンドを意図的に実行しなくてはならないため、少し煩わしく感じていました。
以前に、iCircuitという動的に電子回路のシミュレーションができるiOSアプリを使っていたこともあり、似たような操作感のシミュレータは無いかと探していたところ、このCircuitJS1を見つけました。

CircuitJS1の操作感については、リンク先で実際に触ってみてもらうのがわかりやすいです。 CircuitJS1では、回路に追加した抵抗などによる変化を即座にシミュレーションに反映してくれるだけではなく、作成した回路図のURLリンクなども生成できるため、Markdownなどのテキストメモとの相性もいいと思います。

そんな素晴らしいCircuitJS1なのですが、使っているうちにデスクトップアプリとしてオフラインでも使用したい欲求が湧き上がってきました。

そこで今回、CircuitJS1をElectronアプリ化してみましたので、その手順をまとめてみました。

まず、CircuitJS1のGitHubリポジトリはこちらになります。
pfalstad/circuitjs1: Electronic Circuit Simulator in the Browser

READMEを読むと、Electronアプリとしてビルドするための方法が記載してありますので、それに沿って進めていきます。 おおまかには以下の手順になります。

  1. 必要なツールのインストール
  2. EclipseからGWTコンパイル(JavaのコードをJavaScriptに変換)
  3. Electron用のディレクトリ構成を構築
  4. Electronアプリとして起動

それでは進めていきましょう。

必要なツールのインストール

以下のツールをそれぞれインストールします。

手元の環境ではArchLinuxとパッケージマネージャにyayを使っているので、以下のコマンドでインストールしました。

$ yay -S jdk8-openjdk eclipse-java electron

合わせて、CircuitJS1のコードも適当な場所にgit cloneしておきましょう。

$ git clone https://github.com/pfalstad/circuitjs1.git

EclipseからGWTコンパイル(JavaのコードをJavaScriptに変換)

Electronアプリとして動かすためには、Javaで書かれているコードをJavaScriptに変換しなくてはなりません。(GWT/Electronに詳しくないため、間違った理解をしているかもしれません)
そのためにGWTGoogle Web Toolkit )を使って変換を試みます。

GWTプラグインのインストール

  1. インストールしたEclipseを起動して、適当なワークスペースを設定します。
  2. Help > Eclipse Marketplace から"GWT"で検索して、GWT Eclipse Pluginをインストールします。

Javaのバージョン確認

GWTJava 8を使ってコンパイルするのがいいようです。 (僕は最初にJava 11を使ってコンパイルしたのですが、以下のエラーが出てGWTコンパイルができませんでした。)

Compiling module com.lushprojects.circuitjs1.circuitjs1 [ERROR] Hint: Check that your module inherits 'com.google.gwt.core.Core' either directly or indirectly (most often by inheriting module 'com.google.gwt.user.User')

Eclipseが使用するJavaのバージョンは、 Window > Preferences > Java > Installed JREs で確認できます。 ここでJava 8が設定されていないようなら、設定し直す必要があります。

参考までに、手元の環境で設定されている値は以下になります。

JRE home: /usr/lib/jvm/java-8-openjdk
JRE name: java-8-openjdk

CircuitJS1プロジェクトのインポートとGWTでのコンパイル

  1. Welcome画面から、"Import existing projects"をクリックします。
  2. Select root directory > Browse... でさきほどcloneしたcircuitjs1/ディレクトリを指定します。
  3. ツールバーにあるGWTボタンから GWT Compile Project... をクリックし、circuitjs1プロジェクトを選択してコンパイルを実行します。
  4. コンパイルに成功すると、 war/ディレクトリ下にWEB-INF/とcircuitjs1/が生成されます。 
$ cd circuitjs1
$ ls war
circuitjs1/     # <- generated
circuitjs.html
customfunction.html
customlogic.html
iframe.html
shortrelay.php
WEB-INF/        # <- generated

Electron用のディレクトリ構成を構築

Electronアプリとして起動させるために、Electron用のディレクトリ構成を作ります。
Application Distribution | Electron

(ここでは例としてWindows/Linux用のディレクトリ構成にします。macOSではディレクトリ構成が異なってきますのでご注意ください。)

$ cd circuitjs1
$ mkdir -p electron/resources
$ cp -r app electron/resources/
$ cp -r war electron/resources/app/
$ tree -F -L 2 electron/resources/app/
electron/resources/app/
├── index.html
├── main.js
├── package.json
├── preload.js
├── renderer.js
└── war/
    ├── circuitjs1/
    ├── circuitjs.html
    ├── customfunction.html
    ├── customlogic.html
    ├── iframe.html
    ├── shortrelay.php
    └── WEB-INF/

Electronアプリとして起動

全ての準備が整いました。さっそくCircuitJS1をElectronアプリとして起動してみます。

$ cd circuitjs1
$ electron electron/resources/app/

f:id:ryochack:20190104012015p:plain

これでCircuitJS1がElectronアプリとして起動するようになり、オフラインでも気軽に電子回路シミュレータが使えるようになりました。

Rustでプラットフォーム依存の処理を書く

#[cfg(...)] Attribute

Rustでは #[cfg(...)] アトリビュートを使うことによって、OSやCPUに応じた条件コンパイルを行うことができる。

例えば、次のように書くことでコンパイルターゲットのOSに応じたhello()をビルドし、呼び出すことができる。

#[cfg(target_os = "windows")]
fn hello() { println!("Hello, I'm Windows!"); }

#[cfg(target_os = "linux")]
fn hello() { println!("Hello, I'm Linux!"); }

#[cfg(target_os = "macos")]
fn hello() { println!("hello, I'm MacOS."); }

fn main() {
    hello();
}

モジュールを分ける

各複数プラットフォーム向けの処理が小さい時には、前述のように1つのファイルの中にベタ書きでも問題ないが、コードが膨らんできた場合は各プラットフォーム毎にファイルを分けた方がいいだろう。

そこで、次のようなモジュール構成を考てみる。 nameモジュールを用意し、OS依存部分はnameモジュールの中に隠蔽することを目的とする。

Cargo.toml
src/
├── lib.rs
└── name
    ├── linux.rs
    ├── macos.rs
    ├── mod.rs
    └── windows.rs

name/${os_dependent}.rs

OS依存の処理を name/以下の linux.rs, macos.rs, windows.rsにそれぞれ書いていく。 各モジュールはOSの名前をStringで返すだけのname()関数を実装する。

name/linux.rs

pub fn name() -> String {
    "Linux".to_owned()
}

name/macos.rs

pub fn name() -> String {
    "MacOS".to_owned()
}

name/windows.rs

pub fn name() -> String {
    "Windows".to_owned()
}

name/mod.rs

各OS依存の処理を他のモジュールから扱えるようにするために name/mod.rs を記述する。

name/mod.rs

#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "linux")]
pub use self::linux::*;

#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "macos")]
pub use self::macos::*;

#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(target_os = "windows")]
pub use self::windows::*;

ここでのmod.rsの役割は、コンパイル対象とするモジュールの切り替えと、他のモジュールへの再エキスポートだ。

コンパイル対象とするモジュールの切り替え

これは前述のように #[cfg(target_os = "linux")] を使ってコンパイル対象のモジュールを指定する。

#[cfg(target_os = "linux")]
pub mod linux;

他のモジュールへの再エクスポート

pub useによって再エクスポートすることによって、他のモジュールからnameのOS依存モジュールを意識せずに使えるようにしている。

#[cfg(target_os = "linux")]
pub use self::linux::*;

この記述がないと、nameモジュールのOS依存処理を使う時は常にモジュール名を明確に指定しなくてはならない。 これでは以下のように他のモジュールが常にOS依存を気にしたコードを書かなくてはならず、非常によろしくない。

mod name;
let os_name = name::linux::name();

pub useすることによって、OS依存モジュール名を省略して使用することができるようになり、他のモジュールからOS依存を気にせずに使用できるようになる。

// mod.rsでpub use self::linux::*;
mod name;
let os_name = name::name();

lib.rs

最後に、今まで定義したnameモジュールを使ったlib.rsを次に記述する。

lib.rs

mod name;
pub fn greeting() {
    println!("Hello, I'm {}.", name::name());
}
// -> Hello, I'm ${YOUR_OS}.

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(target_os = "linux")]
    #[test]
    fn test_linux() {
        assert_eq!("Linux", name::name());
    }

    #[cfg(target_os = "macos")]
    #[test]
    fn test_linux() {
        assert_eq!("MacOS", name::name());
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn test_linux() {
        assert_eq!("Windows", name::name());
    }
}

Linux上でテストを実行すると、name()は"Linux"の文字列を返してくれているのが確認できる。

$ cargo test
   Compiling hello v0.1.0 (/home/ryo/code/sandbox/rust/scraps/target_os)
    Finished dev [unoptimized + debuginfo] target(s) in 0.75s
     Running target/debug/deps/hello-69d1a57a65d70c5b

running 1 test
test tests::test_linux ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests hello

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

参考

Linux kernel moduleビルドのメモ

ArchLinux(4.17.10)上でのkernel moduleビルドを試したメモ。

Linux kernelのソースコードを入手する

prepare_kernel_source.sh

#!/usr/bin/env sh
set -eu
KERNEL_VERSION=`uname -r | sed -r 's/([0-9]+\.[0-9]+\.[0-9]+).*$/\1/'`
LINUX_VERSION=linux-${KERNEL_VERSION}

if [ -e $LINUX_VERSION ]; then
    echo "$LINUX_VERSION is already exist."
    exit 1
fi

wget https://www.kernel.org/pub/linux/kernel/v4.x/${LINUX_VERSION}.tar.xz
tar -xvJf ${LINUX_VERSION}.tar.xz
ln -s ${LINUX_VERSION} kernel-source
cd ${LINUX_VERSION}
make clean && make mrproper

# prepare kernel
zcat /proc/config.gz > .config
make oldconfig
make prepare && make scripts
$ cd $HOME
$ sh prepare_kernel_source.sh

kernel moduleのソースコードMakefileを書く

hello.c

#include <linux/module.h>
#include <linux/init.h>

MODULE_LICENSE("MIT");

static int hello_init(void)
{
    printk(KERN_INFO "Hello, World!\n");
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_INFO "Goodbye...\n");
}

module_init(hello_init);
module_exit(hello_exit);

Makefile

obj-m := hello.o

KERNEL_SRC_DIR=${HOME}/kernel-source
PWD:=$(shell pwd)
BUILD_DIR=$(PWD)
VERBOSE=0

all:
    make -C ${KERNEL_SRC_DIR} M=${BUILD_DIR} KBUILD_VERBOSE=${VERBOSE} modules

clean:
    make -C ${KERNEL_SRC_DIR} M=${BUILD_DIR} clean
$ make
-> hello.ko

libelf-dev でのコンパイルエラーが起こったら

コンパイルエラー

$ make
make -C /root/kernel-source M=/root/workspace/hello KBUILD_VERBOSE=0 modules
make[1]: Entering directory '/root/kernel-source'
Makefile:970: *** "Cannot generate ORC metadata for CONFIG_UNWINDER_ORC=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel".  Stop.
make[1]: Leaving directory '/root/kernel-source'
Makefile:8: recipe for target 'all' failed
make: *** [all] Error 2

解決方法

$ apt-get install libelf-dev

Install/Remove kernel module

$ sudo insmod hello.ko
$ lsmod
-> Module                  Size  Used by
   hello                  16384  0
$ dmesg
-> [11869.696026] Hello, World!
$ sudo rmmod hello
$ lsmod
$ dmesg
-> [11900.796428] Goodbye...

参考

Rust: Xargoとarm-binutils無しでCortex-Mターゲットのファームウェアをビルドする

2018年4月、Rustでの組み込みプログラミング環境に2つの大きなニュースが舞い込んできた。

Xargo と arm-none-eabi-lld 無しでCortex-M向けのビルドが可能に

1つ目は、rustupのターゲットに thumbv*m-none-eabi* (Cortex-MのTarget Triple)が追加されたというニュース。

今まではrustupでは、thumbv*m-none-eabi* ターゲットがサポートされておらず、Xargo というツールを使ってlibcoreを thumbv*m-none-eabi* 用にビルドしなくてはならなかった。 しかし、nightly (nightly-2018-04-08) 以降のRustでは thumbv*m-none-eabi* が追加されたため、Xargoを使って自前でlibcoreをビルドする必要はなくなる。

nightly 2018-04-09で確認したところ、確かにターゲットリストに thumbv*m-none-eabi* が追加されていた。

$ rustup show
Default host: x86_64-unknown-linux-gnu
...
active toolchain
----------------
nightly-x86_64-unknown-linux-gnu (default)
rustc 1.27.0-nightly (4b9b70c39 2018-04-09)

$ rustup target list | pr -w 90 --columns=2
aarch64-apple-ios                            mips64el-unknown-linux-gnuabi64
aarch64-linux-android                        mipsel-unknown-linux-gnu
aarch64-unknown-fuchsia                      mipsel-unknown-linux-musl
aarch64-unknown-linux-gnu                    powerpc-unknown-linux-gnu
aarch64-unknown-linux-musl                   powerpc64-unknown-linux-gnu
arm-linux-androideabi                        powerpc64le-unknown-linux-gnu
arm-unknown-linux-gnueabi                    s390x-unknown-linux-gnu
arm-unknown-linux-gnueabihf                  sparc64-unknown-linux-gnu
arm-unknown-linux-musleabi                   sparcv9-sun-solaris
arm-unknown-linux-musleabihf                 thumbv6m-none-eabi
armv5te-unknown-linux-gnueabi                thumbv7em-none-eabi
armv7-apple-ios                              thumbv7em-none-eabihf
armv7-linux-androideabi                      thumbv7m-none-eabi
armv7-unknown-linux-gnueabihf                wasm32-unknown-emscripten
armv7-unknown-linux-musleabihf               wasm32-unknown-unknown
armv7s-apple-ios                             x86_64-apple-darwin
asmjs-unknown-emscripten                     x86_64-apple-ios
i386-apple-ios                               x86_64-linux-android
i586-pc-windows-msvc                         x86_64-pc-windows-gnu
i586-unknown-linux-gnu                       x86_64-pc-windows-msvc
i586-unknown-linux-musl                      x86_64-rumprun-netbsd
i686-apple-darwin                            x86_64-sun-solaris
i686-linux-android                           x86_64-unknown-cloudabi
i686-pc-windows-gnu                          x86_64-unknown-freebsd
i686-pc-windows-msvc                         x86_64-unknown-fuchsia
i686-unknown-freebsd                         x86_64-unknown-linux-gnu (default)
i686-unknown-linux-gnu                       x86_64-unknown-linux-gnux32
i686-unknown-linux-musl                      x86_64-unknown-linux-musl
mips-unknown-linux-gnu                       x86_64-unknown-netbsd
mips-unknown-linux-musl                      x86_64-unknown-redox
mips64-unknown-linux-gnuabi64

2つ目は、Cortex-Mバイナリ用のリンクにlldを使えるようになったというニュース。

今までは最後のリンクにはarm-binutilsのarm-none-eabi-ldを使ってリンクを行なっていたが、それをlldに置き換えられるようになった。 これはcoretex-m-rtクレートに含まれているリンカスクリプトの記述をlldに対応させたことで可能になったようだ。

この2つの変更によって、RustでのCortex-Mターゲット向けの開発のために必要なツールが減り、開発環境構築のハードルが大きく下がった印象だ。 (あとはgdbをlldbに置き換えることができれば、arm-binutilsは一切不要になる)

このニュースを報告しているJaparicさんはXargoの作者でもあり、Rustでの組み込み開発環境をずっとリードし続けている。 この方がいなかったら、今のRustはここまで組み込み分野を視野に入れたものになっていただろうか…。

nightly-2018-04-08 以降でのCortex-Mターゲットのファームウェアビルド方法

Xargo無し、arm-binutils無しでのビルド手順についてメモしておく。
動作確認には、STM32F401 Nucleo-64ボード(Cortex-M4F)を使用する。

大まかな手順は次のとおり。

  1. rustupを最新にする
  2. rustupから対象となるarm MCUthumbv*m-none-eabi* ターゲットを追加する
  3. Rustプロジェクトを作成する。
  4. .cargo/config にリンカの設定を記述する
  5. 必要なクレートを Cargo.toml追記する
  6. コードを書いてビルドする

rustupを最新にする

$ rustup update

また、ビルドにはnightlyが要求されるので、デフォルトをnightlyに変更しておく。

$ rustup default nightly

thumbv*m-none-eabi* をターゲットに追加する

今回動作確認を行うMCUはCortex-M4Fなので、rustupから thumbv7em-none-eabihf をターゲットとして追加する。

$ rustup target add thumbv7em-none-eabihf
info: downloading component 'rust-std' for 'thumbv7em-none-eabihf'
info: installing component 'rust-std' for 'thumbv7em-none-eabihf'

自分が使うCortex-M MCUがどのTarget Tripleを選べばよいかについては次のとおり。

Target Triple Target MCUs
thumbv6m-none-eabi Targets the Cortex-M0, Cortex-M0+ and Cortex-M1 processors (ARMv6-M architecture)
thumbv7em-none-eabi Targets the Cortex-M4 and Cortex-M7 processors (ARMv7E-M)
thumbv7em-none-eabihf Targets the Cortex-M4F and Cortex-M7F processors (ARMv7E-M)
thumbv7m-none-eabi Targets the Cortex-M3 processor (ARMv7-M)

Rustプロジェクトを作成する

LEDを点滅させる簡易プログラムのリポジトリを作成したため、この手順ではこちらを使う。
ryochack/Rust_NUCLEO-F401RE_Led_Blink

$ git clone https://github.com/ryochack/Rust_NUCLEO-F401RE_Led_Blink.git

一から作る場合には、 cargo new でプロジェクトを作成する。

.cargo/config にリンカの設定を記述する

.cargo/config にターゲット毎にリンカの設定を記述する。 ここではリンカにlldを指定している。

.cargo/config

[target.thumbv7em-none-eabihf]
runner = 'arm-none-eabi-gdb'
rustflags = [
  "-C", "link-arg=-Tlink.x",
  "-C", "linker=lld",
  "-Z", "linker-flavor=ld.lld",
  "-Z", "thinlto=no",
]

必要なクレートを Cargo.toml追記する

thumbv*m-none-eabi* のビルドに必要なクレートを Cargo.toml にそれぞれ追加する。

Cargo.toml

[package]
name = "nucleo-f401re_led_blink"
authors = ["ryochack"]
version = "0.1.0"

[dependencies]
cortex-m = "0.4.0"
cortex-m-rt = "0.4.0"
panic-abort = "0.1.1"
volatile-register = "0.2.0"

コードを書いてビルドする

コードが書けたら(ここではRust_NUCLEO-F401RE_Led_Blinkリポジトリのコードを使う想定)、次のコマンドでターゲットを指定してビルドする。

$ cargo build --target thumbv7em-none-eabihf

これでSTM32F401用のELFファイルが target/thumbv7em-none-eabihf/debug/nucleo-f401re_led_blink として出力される。

$ file nucleo-f401re_led_blink
nucleo-f401re_led_blink: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped

このELFファイルを次の記事の内容に従ってSTM32F401 Nucleo-64ボードに書き込むことで動作が確認できる。
OpenOCD + ST-LinkでFirmware書き込み - ryochack.blog