HTTP2 化に伴なって、サイト全体の最適化を行ないました

依存の整理

もはや jQuery なしでも簡単に書けそうなスクリプト部分から jQuery 依存を抜きました。また、JSDeferred を Promise で置き換えました。

script 要素の async / defer

script 要素については必要に応じて async や defer をつけるようにし、基本的に外部スクリプトでブロックする可能性を排除しました。

async は script 要素同士で独立している場合無条件につけられます (非シーケンシャル)。defer はページのDOMが構築されたあとに実行されるように遅延されます (シーケンシャル)

defer は DOMContentLoaded 直前にまとめて呼ばれるようです。

外部ライブラリを自分でホスト

外部ライブラリをCDN経由でロードしている部分がありましたが、TLS セッションの無駄遣いな気がするので、自分でホストするように変えました。自分のホストであれば HTTP2 のセッションが生きているので無駄な処理が減りそうです。

動的圧縮から静的圧縮に

h2o で compress: ON とすると accept-encoding に応じて gzip か br で動的に圧縮してクライアントに返却してくれます。

しかし殆ど変更されないライブラリみたいなものは動的にやるのがもったいないので、静的に圧縮するようにしました。特に brotli は圧縮処理が遅い感じなので意味がありそうです。

h2o 的には file.send-compressed: ON にしって、filename.js.gz と filename.js.brを置くようにします。

.gz と .br をつくるスクリプト

以下のようなスクリプトを作っておくと、指定したファイルの .gz と .br を作れて便利です。

静的に作っておけば、特に VPS のスペックが非力だと相対的に効果が高いはずです。

#!/bin/sh

for f in "$@"
do
        echo "$f"
        gzip --best < "$f"  > "$f".gz
        bro --quality 10 --input "$f" --output "$f".br
done

brotli について

h2o は brotli に対応していて、accept-encoding: br なときはオンデマンドも br を返すみたいです。

ただ、brotli は chrome に実装済みといっても flags で有効にしないとまだ使えないみたいです。

Firefox 45 は標準で accept-encoding: br を吐くようです。

ブログシステムでのキャッシュ

今まで表示のたびに DB からひいてきてテンプレート処理を行っていましたが、キャッシュするようにしました。もともとそこまで遅いわけではないのですが 、一応これも効果がありました。

ただ、キャッシュを入れるとキャッシュ無効化の処理のために考えることが大変増えます。今のところあまり筋の良い実装ではないのですが、一応動いています。

MathJax のサーバサイド化

[tech] サーバーサイド MathJax で数式表示を高速化する | Fri, Apr 8. 2016 - 氾濫原 別エントリにしました。

いろいろやった結果

キャッシュなしの状態でロードして、DOMContentLoaded まで 300ms を切るぐらいにできました。ただし onload までは2秒〜3秒かかっています。onload まで遅いのは画像のサイズが大きいというのと、あとは主にアドセンスのせいです。

同じ環境で www.google.co.jp のDOMContentLoaded を測ると 170ms ぐらいでしたので、要素数などを比較するといい線まできている気がします。

さらにできることは?

いわゆる minify をやっていないので、これをやる余地がまだあります。しかし結構面倒です。そしてたとえ削れても数kBなので、意味があるのか疑問を持っています。

  1. トップ
  2. tech
  3. サイトの最適化

このサイトでは数式を本文中に TeX 形式で書いて MathJax で処理させています。↓ こういうやつです。ベクターなので昨今の高解像度事情でもいい感じに綺麗に表示できます。

これはクライアントサイドで本文中の TeX フォーマットを探して全部 SVG とかに置き換えていくのですが、これがなかなか重い処理です。

特にスマートフォンの場合、数式がまともに表示されるまで5秒〜10秒ぐらいかかるので、それまでモヤモヤした感じになります。

さらにいえば、これは非同期で変換をかけているのでロード直後にリフローが頻発することになります。

ということで、これらの問題をなんとかしたいので、標題のようにサーバサイドで MathJax を使ってみるようにしてみました。

このサイトははてな記法で書いたエントリを保存時にHTMLに変換し、変換済みをDBにいれて、表示のときはそれを出しているだけです。なので、保存時に MathJax を通して本文中の数式を埋め込み SVG に変換するようにします。

HTML の断片をうけとって MathJax にかける HTTP サーバ

幸い MathJax-node という node.js 上で動かせる MathJax があったので、これをそのまま使い、http.Server でラップして、ブログシステムとは別に API サーバを立ち上げました。

#!/usr/bin/env node

var mjAPI = require("mathjax-node/lib/mj-page.js");


mjAPI.start();
mjAPI.config({
	tex2jax: {
		inlineMath: [["\\(","\\)"]],
		displayMath: [ ["$$", "$$"] ]
	},
	extensions: ["tex2jax.js"]
});

var http = require('http');
http.createServer(function (req, res) {
	var html = [];
	req.on('readable', function () {
		var chunk = req.read();
		console.log('readable');
		if (chunk) html.push(chunk.toString('utf8'));
	});
	req.on('end', function() {
		console.log('end');
		console.log('html', html);

		mjAPI.typeset({
			html: html.join(""),
			renderer: "SVG",
			inputs: ["TeX"],
			ex: 6,
			width: 40
		}, function (result) {
			console.log('typeset done');
			console.log(result);

			res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
			res.end(result.html);
		});
	});
}).listen(13370, '127.0.0.1');
console.log('Server running at http://127.0.0.1:13370/');

ブログシステムからは保存時にこのAPIを呼んでHTMLをフィルタしてDBに保存するようにしました。

ハマりどころ

動的な MathJax だと、ページのコンテナサイズから適切な幅を計算してくれるのですが、サーバサイドだとそれができませんので、適切な横幅を自分で指定する必要がありました。

スマートフォンでの閲覧を考えると、width: 40 ぐらいにして、念のため CSS で svg { max-width: 100% } とかを入れておくとよさそうです。width が大きいと、数式ごとのフォントサイズが変わりまくって大変うっとおしいです。

MathJax-node の速度

MathJax-node はなぜかものすごく遅いです。ライブラリロード済みでも10秒ぐらいかかります。そもそも起動も遅いです。オンデマンドでやるのは無理そうなレベルです。

MathJax の JS のロードを抑制

これに伴なって、MathJax を使わなくても良さそうな場合、JSのロードすらやめるようにしました。

所感

MathJax の JS は2段階ぐらいでロードされるので RTT が長いほど不利になります。サーバサイドでやってしまえばJSを転送する必要も実行する必要もなくなるのでエコです。おかげでロードが結構早くなりました。

しかもページロード直後から数式が完全な形で表示されるので、ページに数式がある場合、体感的な速度は数倍に感じます。

  1. トップ
  2. tech
  3. サーバーサイド MathJax で数式表示を高速化する

ログの設定を stdout のままにしていたので rotate するように設定しました。(Ubuntu 12.04.5 LTS)

h2o の設定

sudo mkdir /var/log/h2o
sudo chown www-data:adm /var/log/h2o/

YAML の参照を使うとログフォーマットを使いまわせてよいです。まず global に

access-log: &ACCESSLOG
  path: /dev/null
  format: "..."

と書いて、あとは各ホストで path を上書きする形で設定します。

access-log:
  <<: *ACCESSLOG
  path: /var/log/h2o/host.access.log

なおこのYAML参照は h2o 2.0.0 からのサポートのようです。

logrotate の設定

# /etc/logrotate.d/h2o

/var/log/h2o/*.log {
    daily
    missingok
    rotate 90
    compress
    delaycompress
    notifempty
    create 0640 www-data adm 
    sharedscripts
    postrotate
        svc -h /service/h2o
    endscript
}

h2o は daemontools で管理しているので、postrotate では svc を呼んで SIGHUP を送っています。

一旦状態を確認します

sudo logrotate -dv /etc/logrotate.d/h2o

全ログファイルが log does not need rotating になってるはずです。実際に一度実行します

 sudo logrotate -v /etc/logrotate.d/h2o 

/var/lib/logrotate/status を見ると該当するログファイルが記録されているはずです。日付を一日戻して再度実行するとローテートされることが確認できます。

  1. トップ
  2. tech
  3. h2o のログ設定 (logrotate.d)

以下のようなスクリプトを置いて月イチの cron で更新するようにしました。証明書を更新したあと h2o を restart しています。

#!/bin/sh

# sudo crontab -e
# MAILTO = cho45
# PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
# 25 14 6 * * /srv/www/renew-cert.sh

BIN=/home/cho45/project/letsencrypt/letsencrypt-auto

$BIN certonly --webroot -w /srv/www/www.lowreal.net --renew-by-default -d www.lowreal.net
$BIN certonly --webroot -w /srv/www/lowreal.net --renew-by-default -d lowreal.net
$BIN certonly --webroot -w /srv/www/cho45.stfuawsc.com  --renew-by-default -d cho45.stfuawsc.com

svc -h /service/h2o
  1. トップ
  2. tech
  3. Let's encrypt の自動更新

今のこのサイトの h2o.conf.yaml です。HTTPS (443) のみを処理しています。HTTP (80) は nginx で受けていて、HTTPS 対応ホストに関しては nginx からはリダイレクトしています。

  • アクセスログフォーマットを LTSV に
    • ログフォーマットを YAML の参照で全ホストで共有
  • rewrite rule
  • Strict-Transport-Security (HSTS)
    • 一旦 https でアクセスしてきたクライアントに対して以後 http でのアクセスをさせない
    • 本来はセキュリティのためだが、リダイレクトを一回減らせるのでパフォーマンス的にも一応得
  • "/.well-known": をバインド
    • letsencrypt のホスト検証に使われる

設定の際参考になれば幸いです。

user: www-data
access-log: &ACCESSLOG
  path: /var/log/h2o/access.log
  format: "time:%t\thost:%h\treq:%r\tstatus:%s\tsize:%b\treferer:%{Referer}i\tua:%{User-Agent}i\tcache:%{X-Cache}o\truntime:%{X-Runtime}o\tvhost:%{Host}i\tconnect-time:%{connect-time}x\trequest-header-time:%{request-header-time}x\trequest-body-time:%{request-body-time}x\tprocess-time:%{process-time}x\tresponse-time:%{response-time}x\tduration:%{duration}x\thttp2.stream-id:%{http2.stream-id}x\thttp2.priority:%{http2.priority.received}x"
error-log: /dev/stdout
http2-reprioritize-blocking-assets: ON
ssl-session-resumption:
  mode: all
hosts:
  "lowreal.net:443":
    access-log:
      <<: *ACCESSLOG
      path: /var/log/h2o/lowreal.net.access.log
    http2-casper: ON
    compress: ON
    listen:
      port: 443
      ssl:
        certificate-file: /etc/letsencrypt/live/lowreal.net/fullchain.pem
        key-file:         /etc/letsencrypt/live/lowreal.net/privkey.pem
    header.add: "Strict-Transport-Security: max-age=31536000"
    header.add: "X-Content-Type-Options: nosniff"
    header.add: "X-UA-Compatible: IE=Edge"
    paths:
      "/":
        reproxy: ON
        mruby.handler: |
          require "/srv/www/rewrite_rules.rb"
          lambda do |env|
            RewriteRules.rewrite(env) do
              rewrite '/favicon.ico', '/images/favicon.ico', :break
              rewrite '/apple-touch-icon.png', '/images/apple-touch-icon.png', :break

              rewrite %r{^/2005/colors-canvas\.xhtml$}, '/2005/colors-canvas.html', :permanent
              rewrite %r{^/2005/colors-canvas$}, '/2005/colors-canvas.html', :permanent

              rewrite %r{^/logs/latest$}, '/', :permanent
              rewrite %r{^/logs/latest.rdf$}, '/feed', :permanent
              rewrite %r{^/logs/latest.atom$}, '/feed', :permanent
              rewrite %r{^/latest\.rdf$}, '/feed', :permanent
              rewrite %r{^/blog/index\.(rdf|atom)$}, '/feed', :permanent
              rewrite %r{^/logs(/.+?)(\.(rdf|atom))$}, '/feed', :permanent

              rewrite %r{^/logs(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
              rewrite %r{^/blog(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
              rewrite %r{^/photo$}, '/photo/', :permanent

              rewrite %r{^/(\d\d\d\d/\d\d(/\d\d)?)$}, '/\1/', :permanent
              rewrite %r{^/\d\d\d\d/$}, '/', :redirect

              rewrite %r{^/view-img(/.+?)$}, '\1', :permanent

              rewrite %r{^/(\d\d\d\d/([^\d]|\d\d[^/]).*)}, '/files/\1', :break
            end
          end
        proxy.reverse.url: http://localhost:5001
        proxy.preserve-host: ON
      "/files":
        file.dir: /srv/www/lowreal.net/files
      "/images":
        file.dir: /srv/www/lowreal.net/Nogag/static/images
      "/css":
        file.dir: /srv/www/lowreal.net/Nogag/static/css
      "/js":
        file.dir: /srv/www/lowreal.net/Nogag/static/js
      "/lib":
        file.dir: /srv/www/lowreal.net/Nogag/static/lib
      "/.well-known":
        file.dir: /srv/www/lowreal.net/.well-known

  "www.lowreal.net:443":
    access-log:
      <<: *ACCESSLOG
      path: /var/log/h2o/www.lowreal.net.access.log
    http2-casper: ON
    compress: ON
    listen:
      port: 443
      ssl:
        certificate-file: /etc/letsencrypt/live/www.lowreal.net/fullchain.pem
        key-file:         /etc/letsencrypt/live/www.lowreal.net/privkey.pem
    header.add: "Strict-Transport-Security: max-age=31536000"
    header.add: "X-Content-Type-Options: nosniff"
    header.add: "X-UA-Compatible: IE=Edge"
    paths:
      "/":
        reproxy: ON
        mruby.handler: |
          lambda do |env|
            link = [
              '/styles/201002/201002.css',
              '/js/site-script.js',
            ].map{|p| "<#{p}>; rel=preload"}.join("\n")
            case env['PATH_INFO']
            when "/"
              if (env['HTTP_ACCEPT_LANGUAGE'] || '') =~ /ja/
                return [307, {"x-reproxy-url" => "/index.ja.html", "link" => link }, []]
              else
                return [307, {"x-reproxy-url" => "/index.en.html", "link" => link }, []]
              end
            when "/index.ja.html", "/index.en.html"
              return [399, {"link" => link }, []]
            end
            return [399, {}, []]
          end
        file.dir: /srv/www/www.lowreal.net
#        file.index: [ index.en.html ]

  "cho45.stfuawsc.com:443":
    access-log:
      <<: *ACCESSLOG
      path: /var/log/h2o/cho45.stfuawsc.com.access.log
    listen:
      port: 443
      ssl:
        certificate-file: /etc/letsencrypt/live/cho45.stfuawsc.com/fullchain.pem
        key-file:         /etc/letsencrypt/live/cho45.stfuawsc.com/privkey.pem
    header.add: "Strict-Transport-Security: max-age=31536000"
    header.add: "X-Content-Type-Options: nosniff"
    header.add: "X-UA-Compatible: IE=Edge"
    paths:
      "/":
        file.dir: /srv/www/cho45.stfuawsc.com
        redirect:
          status: 301
          url:    "/niro/"
      "/niro/":
        proxy.reverse.url: http://localhost:5001/niro/
        proxy.preserve-host: ON
      "/tmp":
        mruby.handler: |
          require "htpasswd.rb"
          Htpasswd.new("/srv/www/.htpasswd", "Restricted")
        file.dir: /srv/www/cho45.stfuawsc.com/tmp
  1. トップ
  2. tech
  3. 現在の h2o.conf.yaml

全部 HTTPS 対応にしてリダイレクトかけるようにしました。

cho45.stfuawsc.com は既に HTTPS にしてありました。こちらは nginx に letsencrypt の証明書を入れた構成だったのですが、これを機に HTTPS のフロントを h2o にして、nginx は HTTP だけを配信するようにしました。これで HTTPS は HTTP2 に対応になりました。

このサイトは割と複雑な URL の rewrite ルールをしいているので、バックエンドのアプリケーションに直接ディスパッチせず、一旦 HTTPS でも nginx を経由するようにして設定し、徐々に h2o で全リクエストを処理するように置き換えていきました。www.lowreal.net も同時に HTTPS 対応しましたがそれぞれのドメインの構成は以下の通りです

  • cho45.stfuawsc.com
    • h2o → static file
    • h2o → backend
  • lowreal.net
    • h2o → static file
    • h2o → backend
  • www.lowreal.net
    • h2o → static file (accept-language を mruby で見てる)

証明書以外にやったこと

一応 mixed content を回避したり、push してみたりしたくていろいろやりました

  • はてなスターのスキーム変更
  • スター画像のホスト元 (フォトライフ) のドメイン変更 (cdn-akナントカに)
  • tumblr のスキーム変更
  • facebook ボタン廃止
  • twitter ボタンのスキーム変更
  • Amazon 画像のドメイン変更 (過去に遡ってエントリ内の画像URLを変更)
  • バックエンドアプリケーション (ブログシステム) へ Link: rel=preload を簡単に吐ける機能を追加

h2o での server push の確認方法

h2o は server push したコンテンツのヘッダに x-http2-push: pushed を含めてくれるので、ちゃんと push されてるか確認する一番簡単な方法はこれを見ることっぽいです。

  1. トップ
  2. tech
  3. lowreal.net のHTTP2/HTTPS 化を実施

このサイトのHTTPS化にあたって nginx で書いていた rewrite のルールを h2o の mruby で処理するように変える必要がありました。

しかしベタで書くのも面倒なので、そこそこ機械的な置換ですむような書きかたができるようなライブラリを書いてしのぎました。

mruby.handler

    paths:
      "/":
        proxy.reverse.url: http://localhost:5001
        proxy.preserve-host: ON
        mruby.handler: |
          require "/srv/www/rewrite_rules.rb"
          lambda do |env|
            RewriteRules.rewrite(env) do
              rewrite '/favicon.ico', '/images/favicon.ico', :break
              rewrite '/apple-touch-icon.png', '/images/apple-touch-icon.png', :break

              rewrite %r{^/2005/colors-canvas\.xhtml$}, '/2005/colors-canvas.html', :permanent
              rewrite %r{^/2005/colors-canvas$}, '/2005/colors-canvas.html', :permanent

              rewrite %r{^/logs/latest$}, '/', :permanent
              rewrite %r{^/logs/latest.rdf$}, '/feed', :permanent
              rewrite %r{^/logs/latest.atom$}, '/feed', :permanent
              rewrite %r{^/latest\.rdf$}, '/feed', :permanent
              rewrite %r{^/blog/index\.(rdf|atom)$}, '/feed', :permanent
              rewrite %r{^/logs(/.+?)(\.(rdf|atom))$}, '/feed', :permanent

              rewrite %r{^/logs(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
              rewrite %r{^/blog(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
              rewrite %r{^/photo$}, '/photo/', :permanent

              rewrite %r{^/(\d\d\d\d/\d\d(/\d\d)?)$}, '/\1/', :permanent
              rewrite %r{^/\d\d\d\d/$}, '/', :redirect

              rewrite %r{^/view-img(/.+?)$}, '\1', :permanent
            end
          end

h2o の path はディレクトリの指定しかできないみたいなので (自動的に末尾スラッシュがついたりする)、rewrite で同時にパス決め打ちのディスパッチも行っています。

rewrite_rules.rb

class RewriteRules
	class RewriteRulesException < Exception
		attr_reader :response
		def initialize(response)
			@response = response
		end
	end

	attr_reader :env
	attr_reader :path

	def self.rewrite(env, &block)
		self.new(env).run(&block)
	end

	def initialize(env, &block)
		@env = env
		@path_orig = "#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
		@path = @path_orig.dup
	end

	def run(&block)
		begin
			instance_eval(&block)
			if @path != @path_orig
				return [ 307, { 'x-reproxy-url' => path }, [ ] ]
			else
				return [ 399, {}, [ ] ]
			end
		rescue RewriteRulesException => e
			return e.response
		end
	end

	def rewrite(regexp, replace, flag=:continue)
		if @path.gsub!(regexp, replace)
			case flag
			when :redirect
				raise RewriteRulesException.new([ 302, { 'Location' => @path }, [ ] ])
			when :permanent
				raise RewriteRulesException.new([ 301, { 'Location' => @path }, [ ] ])
			when :break
				raise RewriteRulesException.new([ 307, { 'x-reproxy-url' => @path }, [ ] ])
			when :continue
				# nothing
			else
				raise "unsupported flag: #{flag}"
			end
		end
	end
end
require 'rspec'

describe RewriteRules do
	it "should treat :permanent as 302 redirect" do
		env = {
			'PATH_INFO' => '/logs/latest'
		}
		expect(RewriteRules.rewrite(env) {
			rewrite %r{^/logs/latest$}, '/', :permanent
			rewrite %r{^/logs(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
			rewrite %r{^/foobar/}, '/bazbaz/'
		}).to eq( [301, {"Location"=>"/"}, []])
	end

	it "should treat :redirect as 301 redirect" do
		env = {
			'PATH_INFO' => '/logs/piyo.html'
		}
		expect(RewriteRules.rewrite(env) {
			rewrite %r{^/logs/latest$}, '/', :permanent
			rewrite %r{^/logs(/.+?)(\.(x?html|xml|txt))?$}, '\1', :redirect
			rewrite %r{^/foobar/}, '/bazbaz/'
		}).to eq([302, {"Location"=>"/piyo"}, []])
	end

	it "should treat as internal proxy by default" do
		env = {
			'PATH_INFO' => '/foobar/baz'
		}
		expect(RewriteRules.rewrite(env) {
			rewrite %r{^/logs/latest$}, '/', :permanent
			rewrite %r{^/logs(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
			rewrite %r{^/foobar/}, '/bazbaz/'
		}).to eq([307, {"x-reproxy-url"=>"/bazbaz/baz"}, []])
	end

	it "can write logic in dsl" do
		env = {
			'PATH_INFO' => '/foobar/baz',
			'XXX' => true,
		}
		expect(RewriteRules.rewrite(env) {
			if env['XXX']
				rewrite %r{^/foobar/}, '/bazbaz/'
			end
		}).to eq([307, {"x-reproxy-url"=>"/bazbaz/baz"}, []])

		env = {
			'PATH_INFO' => '/foobar/baz',
			'XXX' => false
		}
		expect(RewriteRules.rewrite(env) {
			if env['XXX']
				rewrite %r{^/foobar/}, '/bazbaz/'
			end
		}).to eq([399, {}, []])
	end
end

ところで

rewrite ルールが ruby で書けるということは、すなわち自由にテスト可能であることを意味します。Apache の RewriteRule や nginx の rewrite をテストするのはかなり面倒なので、かなり強力で嬉しい感じがします。

  1. トップ
  2. tech
  3. nginx の rewrite ルールっぽく h2o の mruby でリクエストの rewrite を行う

NHK Eテレの番組はそれぞれ対象年齢が設定されていて、「いないいないばぁ」は0〜2歳、「おかあさんといっしょ」は2〜4歳、「みいつけた」は4〜5歳あたりらしい。

うちの子供はまだ2歳になっていないので「みいつけた」はまだ早いことになるが、見せてみると特に嫌がらずに見ていたりする (オフロスキーは嫌いみたいだけど)。「いしゅ、いしゅ」と言うぐらいには見る。

「いないいないばぁ」と「おかあさんといっしょ」は大人が見ても面白いとはいえない感じだけど (というか無駄に元気がよくて、疲れる)、「みいつけた」は大人が見てもゆるくて面白い。

  • 害になるような人物が出てこない
  • 不愉快ができことが起こらない
  • 現実にはありえない日常を過ごす
  • 説教臭い内容ではない

あたりを考えてみると、日常系アニメと似たようなジャンルに思える。

コッシー

椅子のキャラクターでコッシーというのがいて、スタジオ内で結構自由に動くんだけど、どうやって動いているのかさっぱりわからない。謎の技術。ひっくり返るシーンとかもあって、足の裏が見えたりするけど、車輪がついていたりするようにも見えない。謎

びっくりしたのが声をあててる声優で、高橋茂雄 (サバンナ) とクレジットされている。ブラジルの皆さんじゃないほうの人・ザッカーバーグに似てるほうの人だけど、アメトークとかにしか出てないイメージだったのでびっくりした。エンディングの作詞とかもやってて余計びっくりする。全然違和感がない (喋る椅子に対して違和感もクソもない気もするけど) し聞きやすい声質だし、謎の才能。

歌の作曲が星野源だったり、この番組に限らないけどEテレの子供向け番組は凝ったキャスティングされていることがあって面白い。「おかあさんといっしょ」でもつんく作曲の歌がちょいちょいある。

「伝送線路トランス」という言葉が一般的な用語かどうかよくわからないのですが、トロイダル・コア活用百科ではこのような用語になっていました。一応 transmission-line transformer で検索すると使われており、Amidon のドキュメントでも出てきますが……

それはともかく、伝送線路トランスは一見奇妙な感じがして面白いです。とりあえず LTSpice で等価回路を書いて試してみました。

位相反転回路

GND のとりかたが入力側と出力側で逆なので位相が反転します。

この回路、L1 と L2 が結合しているため、これらで1つのコモンモードチョークとして働いてアイソレーションされるために位相反転ができているのですが、なんとかく不思議な感じがします。

自分の中では「コモンモードチョーク」はノーマルモードに影響を与えないイメージなのですが、「コモンモードチョーク」があるおかげでノーマルモードの位相反転ができているのです。ぱっと見だと結線されていない GND 経由 の電流 (すなわちコモンモード) が阻止されることを強くイメージする必要があります。

ところで、結合係数を減らすとどうなるか見てみます。

これはつまりコモンモードチョークとしての機能が失われている場合です。高い結合が得られていない限り機能しなくなくなることがわかりました。

インピーダンス変換器

1:4 のインピーダンス変換器もシミュレーションしてみました。

純伝送線路トランス

伝送線路トランスを2つ使い、入力を並列、出力を直列にすることで、出力電圧を倍にできる (出力電流は半分) という回路です。単純に、それぞれの入出力がアイソレーションされていると考えると動きそうだなというイメージはできます。

が、やはり一見奇妙に見えます。

伝送線路的トランス

入力信号にトランスの出力を重合する形で出力電圧を倍にする回路です。広域が犠牲になる代わりに純伝送線路トランスよりコアの数を減らせるメリットがあります。実際の回路だとこちらのほうが良く見ます。

ぱっと見だと完全に意味不明ですが「重ねあわせる」ことを意識すると理解できるようなできないような感じがします。

通常のトランスとの違い

通常のトランスは、一旦電気エネルギーを磁気エネルギーに変換して再度電気エネルギーに変換するという動作をします。なので、高い結合係数と低いコア損失を同時に実現できなければいけません。

伝送線路トランスはコモンモードのアイソレーションによって実現されており、磁気エネルギーはメインのエネルギー伝達に使われていないので、結合係数が高ければコア損失が多少あっても問題になりません。

  1. トップ
  2. tech
  3. LTSpice で伝送線路トランス

トロイダル・コア活用百科の応用の章にある10MHz帯用2ポールバンドパスフィルタのうち、挿入損失1dBのもので T50-6 を使ったものを作ってみました。

改訂新版 定本 トロイダル・コア活用百科 —トロイダル・コイルの理論・製作と応用回路 (定本シリーズ) - 山村 英穂

山村 英穂

5.0 / 5.0

回路と実装

4pF が手元にないので3.9pF で代用しました。また、AWG #22 のワイヤーがなかったため、φ0.4mm で代用しています(なのでコイルのQは多少下っているはずです)


特性

書籍内でのこのグラフだと(2)に相当します。

実際にスペアナで同じ範囲を表示すると以下のようになりました。(多少トリマで調整した状態で、完全に追いこめてはいません)

挿入損失が 2dB になってますが、おおむね近い特性が再現できました。

ついでに 50MHz までのグラフです。綺麗に下降せずに盛りあがっています。

ここからさらに広げていっても、ほとんど減衰量は増えていませんでした (スクリーンショットを撮り忘れましたが)

  1. トップ
  2. tech
  3. 10MHz バンドパスフィルタ

10MHz バンドパスフィルタ | tech - 氾濫原 の続きです。挿入損失 6dB バージョンの定数を変更してみました。コイルはそのままです。

定数変更

実装の関係で、書籍内の定数とすこしずれています。

評価

書籍内のグラフと範囲をあわせたものです。やってて気付いたのですが、書籍のグラフは中心周波数が10MHzではないんですね。狭帯域だからかアマチュアバンドの中心?にあわせてあるのかな。手元の調整は10MHzを中心としてやっています (かなり繊細な調整が必要でした)。

帯域内は約-9dBの挿入損失になっています。

300MHz までの範囲です。100MHz ぐらいまでは -70dB ぐらいまでの減衰があります。

  1. トップ
  2. tech
  3. 10MHz バンドパスフィルタ2

あたりの続きです。

ちょっと飽きてきたというか疲れてきたので、とりあえず使えるレベルを目指すという意味でケースに収めることを優先することにしました。

発注した基板そのままでは収めることができなかったので、いろいろ試行錯誤して収めました。本来基板を設計する段階でケースまで決めてしまえればいいんですが、今回そこまで気をまわすことができませんでした。おかげでだいぶ苦労しましたがなんとか入りました。

Bluetooth モジュール用基板

Bluetooth モジュールは別途基板を自分で掘って載せるようにしました。場所がないので2階建です。

切削はそこそこ上手くいったのですが、ほとんど肉眼で確認できないのにGNDと導通があったり、実際実装してみると見えない部分で半田がショートしたりと、なかなか厳しい状態になりました。3回ぐらいモジュールの半田づけをやりなおしたのでパターンが剥れたり、ついでに勘違いからRX/TXが逆配線になっていると思ってリワークまでしたけど元々の配線であってたり、、、みたいないろんな思い出があります。

ケースの加工

CNC でやりましたが、プラスチック(ABS) の加工はものすごく苦手意識があります。アルミと違ってプラスチックは溶けるので早く動かしすぎてもダメだし遅く動かしすぎてもダメだしで「安全方向にふる」という運用が通用しないのです。

今回は以下ぐらいでやるとすくなくともエンドミルに溶けたプラスチックが固着して折れるということはなくなりました…… (2本折りました)

  • φ1mm 2枚刃 エンドミル
  • F500 (割と早い) Z送り50 (かなり遅い)
  • 回転数 50% 約5000rpm
    • S500 指定だが、最大1000を最大の電圧として500だけ電圧をかけるという意味で、500は回転数ではない (最大の50%の回転)

タコメータをつけてないので回転数が正確にわかってません。

インターフェイス

見ての通りですがインターフェイス的には

  • 電源スイッチ
  • LED (Bluetooth の接続状況: 接続待ちは点滅・接続済みは点灯)
  • BNC コネクタ

しかありません。これは最初からこうするつもりで作っていて、Bluetooth 経由で接続したスマートフォンから全ての操作を行うという設計です。

というのも、アナライザを作るなら、まず絶対に Bluetooth などで無線化したいという思いがありました。なぜかといえば、実際にフィールドでアンテナの調整を行う場合、給電点と調整点というのは必ずしも一致しておらず、既存のアンテナアナライザーだと給電点と調整点をいったりきたりする必要があって面倒だからです。例えば7MHz帯の半波長逆Vダイポールをフィールドで一時的に立てようと思うと、給電点と調整点では10mぐらいの距離があってしんどいのです。

今後

これでとりあえず使えるぐらいにはなりました。しかし、かなり問題があります。

  1. Android 側のアプリケーションの完成度がいまいちなのをなんとかする
    • ソフトウェアだけなのでおいおい
  2. インピーダンスのグラフが波うつ
    • どっかで反射の影響が起きていると思うけどどこかわかってない
  3. RFアンプの設計からやりなおす
    • というか面倒なのでMMIC使うとか
  4. 低消費電力化
    • RFアンプに常時電源供給してしまっているのをやめる
    • RFアンプの再設計
    • AD9851 と Bluetooth デバイスの消費電力がデカいのでできることは限られる
  5. ケースに組みこんだら計測にノイズが乗るようになった
    • NiMH 2本を電源にしたいと拘ったため、DC/DC で 5V まで昇圧しているが間違いなくノイズ源になっている
    • Bluetooth モジュールとADC部が近い
  6. もっと小さく作りたい
    • MCU が無駄にでかい

単三電池2本の電源内蔵するのも今回なぜかこだわっていたのですが、どうせDC/DCで昇圧するならモバイルバッテリ使えるようにして、電池は内蔵しないほうがコンパクトかつ総合的には荷物が減って良さそうです。

  1. トップ
  2. tech
  3. アンテナアナライザーをケースに収めた

:processDebugResources FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':processDebugResources'.
> com.android.ide.common.process.ProcessException: org.gradle.process.internal.ExecException: Process 'command '/Users/cho45/Library/Android/sdk/build-tools/23.0.1/aapt'' finished with non-zero exit value 1

こんなエラーで死んでしまいました。gradle 経由で実行されてるんですが、gradle へのコマンドラインオプションの渡しかたがわからず……

cordova clean したり rm -rf platforms/android しても解決しなかったのですが、Dropbox に入れておいたせいで、ソースツリー内に「www/src/app (xxxx 競合コピー 2016-03-30).js」のようなファイル(特に心あたりなし)ができており、これを削除したら解決しました。稀な例だと思いますが…

  1. トップ
  2. tech
  3. cordova run android --device でエラー

朝に悪心で眼が覚め、すこしすると連続した水下痢に。断続的に下痢が出続ける。

悪心もあるので寝ていようとしたが、寝ると夢の中でも下痢をしており、ケツから漏れるという夢を見て「ガタッ!!」と起きること数回、「夢は見るものではなく実現するもの」と言わんばかりに夢じゃなくなったので履いていたパンツは捨てて新しいのし、ズボンは漂白に。

一通り落ちついてきたので一旦会社に行ってしょぼいタスクをやって、やっぱキツいので帰宅して寝ていたが、不用意に寝ると漏れる夢を見るので落ちつかず。

最近子どもが胃腸炎だったので伝染ってきたようだ。妻も胃腸炎で死んでおり、家庭が崩壊している。なお子どもはもう割と元気。

驚異の防臭袋 BOS(ボス)Sサイズ大容量200枚入り 赤ちゃん用 おむつ 処理袋 (袋カラー:ピンク) - BOS

BOS

5.0 / 5.0

パンツ捨てるときはこれに入れた。ほんとこの袋は死ぬほど便利だ

あたりの続きです。

シリアル接続を Bluetooth にして、スマートフォンから接続して測定して結果を表示するようにしています。

Bluetooth モジュールは RN42 を使っています。基板上に載せることもできるようにしたのですが、コントロール側の基板が無駄に大きく、別途作りなおそうかとも思っているので、ひとまずブレッドボードで接続しています。

アプリケーション側

ネイティブで書く気はさらさら起きないため Apache Cordova を使っています。HTML + CSS + JS でアプリを書くやつですがいろんなところで実績があるので割と安心して使えそうです。何度か触ってますが Hello, World までしかやったことがなかったので、Cordova でまともなアプリケーションを作るのは初めてです。

Bluetooth SPP を使いたいので、cordova-plugin-bluetooth-serial を使っています。

全体的なフレームワークとして Polymer を使っています。とはいえレンダリングは canvas なので今のところあんまり Polymer 的機能はつかっていません。

割と普通に書いたら動いたので、それほどハマりどころはありませんでした。

その他

コードはそのうち公開します。

これでとりあえず、

  1. いくつかの設計候補からの設計方針の決定
  2. 実際の回路図起こし (Eagle)
  3. 基板のアートワーク・基板発注 (Eagle, PCB Way)
  4. 手実装
  5. ファームウェアの開発 (mbed)
  6. ソフトウェア開発 (Cordova, Polymer)

までを一通り一つのプロジェクトとして、ほぼ独りでできる見通しになりました。

自分で作ったハードウェアを自分で書いたソフトウェア、特にインターフェイス部分は HTML/CSS/JS という高レイヤーまで一通りできたぞという実感がようやく沸いてきたので嬉しいです。

  1. トップ
  2. tech
  3. 自作アンテナアナライザーのBluetooth化とアプリケーション