Kindle Paperwhite (11世代) 購入

Kindle Voyage 買ったのはなんと7年前。

blog.kondoumh.com

文庫本的サイズで軽いので今も SF とか読むのにけっこう使ってます。

11世代目の Kindle Paperwhite はフォームファクタがリニューアルされ、画面が大きくベゼルも細くなりました。なんといっても USB-C 対応。これは素晴らしい。ということで広告なし版をポチりました*1

ベゼルが細くなってかなりモダンな感じになりました。

WiFi の情報は Amazon が知ってるのでスイッチを入れたらもうログインしてました。

Voyage と比べると画面描画やスワイプ操作への反応も良くなったし、画面色の暖かさも調整できます。漫画の早送り操作などもサポートされてかなり UI は進歩してきたなと思いました。

左 Paperwhite 右 Voyage やはり画面サイズが大きいので情報量が増えてますね。漫画は Kindle デバイスではほぼ読みません*2が Voyage よりは実用的です。

f:id:kondoumh:20211127154745j:plain

Voyage より約20グラム重いですが、大きさのわりに軽い(密度が低い?)せいか Voyage より重いって感じはないです。

USB-C で充電も手軽になったので Kindle の稼働率が上がりそうです。

*1:ブラックフライデー前だったので3000円ぐらい高かったですが。

*2:macOS の Kindle アプリで大きい画面で読みます。

1Clipboard で Chrome の稼働を1つのマシンに集約する

1Clipboard は 複数の PC / Mac のクリップボード履歴を共有するツールです。

1clipboard.io

Google Drive を仲介してクリップボードデータを共有するため、インストールして各端末で Google ログイン・パーミッション設定をする必要があります。一度設定すれば、各 OS でのクリップボード履歴が同期され、他のマシンからも利用可能になります。

macOS のメニューバーアイコン。

f:id:kondoumh:20211117220013p:plain

クリップボードにコピーするたびに同期中ステータス表示になります。Dropbox によく似ています。

f:id:kondoumh:20211117215915p:plain

クリップボード履歴の UI。スターをつけて絞り込んだりもできます。

f:id:kondoumh:20211115215128p:plain

このツール、複数マシンでキーボードとマウスを共有してるのでクリップボードも共有したくて見つけました。コピーの主たるソースはブラウザ (Chrome) です。

以前から複数のマシンで Chrome 起動して URL やコードスニペットをコピペしているのが非効率だと感じていました。あるマシンではコピー済みなのに別マシンでは未コピーなのでわざわざ検索して同じページを開いてコピーする・・という操作はかなりのストレスです。Chrome を開くのは1台のマシンにして、他のマシンは作業に必要なウィンドウだけを開いた状態にしたい。

1Clipboard をしばらく使用してみて、最新データ反映にややラグがありますが、それでもかなりストレスが軽減されました。マシンを意識しなくていいというのは想像以上に便利だと感じました。

自分の Google アカウントに閉じた共有とはいえ、パスワードなど機微な情報の扱いについては履歴から消すなど気をつけたいと思いました*1

*1:コピーを抑止する操作や、フィルター機能があればよいと思いました。

REALFORCE R3 (BT / All 30g) を導入

2008年に購入した REALFORCE 91UBK を長らく使ってきました。

blog.kondoumh.com

まだまだ使える感じですが、レーザー刻印の文字はかなり薄くなり、たまにチャタリングも起こるようになりました。数年前新しいの買おうかと思ったら R2 になってスペースキーが長くなってしまい見送っていたのでした。macOS のようにスペースキー両側の変換、無変換に日本語入力のオン・オフを割り当てているのでスペースキーが長いのは耐えられないのです。HHKB も所有してますが、手が大きいので長時間作業ではフルピッチのキーキャップが使える REALFORCE がメインになっていました。

先日 REALFORCE R3 がリリースされ、スペースキーが R2 世代よりぐっと短くなりました。しかも Bluetooth 対応。早速ポチりました。

www.realforce.co.jp

翌日には配送されてきたので 91UBK と取り替えてみました。まずはUSB ケーブルで接続。

f:id:kondoumh:20211109225421j:plain

変荷重か All 30g か迷いましたが、腱鞘炎気味なので指先に負荷の少なそうな All 30g を選んで正解でした。とても軽いタッチでタイプ音も静か。スペースキーは 91UBK よりやや長いですが無理なく使えます。

Fn キーと1〜4 キーの組み合わせで Bluetooth 接続先を切り替え可能です。Bluetooth と USB 切り替えは Fn + 5 で可能です。Fn キーは元々 Windows のコンテキストメニューを出すための特殊キーがあった位置に配置されています。Fn キーは右 Alt / 右 Ctrl よりも幅が広く取られており、切り替えで多用するための配慮がされています。

MacBook Pro と2台の Windows ラップトップ、そして iPad Pro と繋ぎました。MacBook Pro と1台のラップトップはマウスまで共有したいので有線 & USB 切替器で、あとは BT 接続。無線オンリーで使う場合は単三電池駆動ですが、有線だと給電状態になるため電池レスで BT も使えます。

MacBook Pro 有線
ThinkPad 有線
iPad Pro BT1
Let's note BT2

iPadOS は Magic Keyboard 以外の JIS キーボードは英語キーボードと認識するのが伝統でしたが、REALFORCE に関しては JIS 配列として認識してくれました*1。日本語の切り替えは Caps Lock キーを割り当てられるのでたまに使う用途としては十分です。何より置き場所に困っていた Magic Keyboad を片付けられるのがいいです。

ということで新 REALFORCE にはとても満足しています。BT による接続性もさることながら、軽く静かにタイピングできることに価値があると思いました。

キーマップやキースイッチの深さを設定するソフトウェアも提供されています。今のところカスタマイズは要らない感じなのでそのうち試してみようかと思っています。

www.realforce.co.jp

追記) キーボードだけマルチペアリングで切り替えられても、マウスもセットで切り替えできないとつらい問題があります。ロジクールの MX Master とか導入してそれぞれ切り替えるという方法も考えられますが、Ctrl キー2連打でスイッチできる USB 切替器と比べるとイマイチそうです。今のところ BT がオフられているマシンも使ってるので、有線メインで BT は補助的と割り切ることにしています。

*1:iPadOS 側の仕様変更によるものかもしれません。

Duet Display で iPad を Windows ラップトップの外部ディスプレイにする

最近会社支給の Let's note がリニューアルして性能的に2015年の私物 MacBook Pro を上回りました (M1 Mac は未発注)。

scrapbox.io

これまで iPad Pro をメインディスプレイの左に浮かべて、Magic Trackpad と Magic Keyboard をペアリングして使ってたのですが、この位置に Let's note を置きたくなりました。メインのディスプレイとキーボードの左サイドはサブマシンの一等地なので。

てことで、Magic Trackpad とかを脇へ退けて空いた場所に Let's note を置きました。そうするとその上に浮かんでいる iPad Pro の画面を外部ディスプレイとして使いたくなります。iPad Pro は最近の MacBook だと普通に外部ディスプレイとして使えるのですが Windows だとそうはいきません。が、サードパーティのソフトウェアを使えば実現できます。

ja.duetdisplay.com

Duet 買ったのは旧型の MacBook Air を使ってた頃なのでもう6年も前ですが、iPad も PC も当時よりスペックアップしてるし実用度も上がってるかも・・とうっすらと期待して USB-C ケーブルで iPad Pro を接続しました。

Windows と iPad で Duet Display を起動。ソフトウェアで描画してるのでダイレクトにディスプレイに投射するよりかなりオーバーヘッドが大きいと思いますが、有線接続なのもあってか許容範囲の描画速度だと思いました。PC の CPU 負荷もほとんどありません。

ブラウザで static な画面を映したり Windows Terminal でコマンドを打つなどの操作を iPad 側で、本体ディスプレイを VS Code 専用にするという使い方ができます。

f:id:kondoumh:20211028201216j:plain

Let's note のホイールパッドですが Duet だと Trackpad と同じように2本指のスクロールも効きます*1し、くるくるスクロールもできます。残念ながら Trackpad では操作できません。画面直接タッチしてスクロールもいけてこれが意外と使えます。

ところで iPad Pro 11inch は高解像度なのに画面が小さいのでフォントが小さくなってしまい老眼にはキビシイため 125% に拡大すると見やすくなりました。

f:id:kondoumh:20211028195643p:plain

PC から iPad が常に給電されている状態なので充電の心配もいらなくなり (それはそれでバッテリーにとってどうなのか気になりますが) トータルで快適になった感じがします。メインのディスプレイは MacBook 専用になったのでコンテキストスイッチがなくなり楽になったのも大きいです。

Duet を購入した当時は1度試して2度と使わなかった気がしますが、今回はかなり使い続ける予感がしました*2

最初に接続した時の UI のナビゲーションとか Windows の通知を使った状況表示もよくできていてこの6年間たゆまず開発が続いてたということが分かります。iPad と Mac の組み合わせだったら本家のエコシステムに敵わないけど Windows や Android と連携できるところで独自の強みを発揮しています。

iPad がスリープすると PC 側の Duet を再起動する必要があるのが今のところちょっとめんどくさいポイントです。

追記) しばらく快適に使ってたのですが、マウス操作が許容できないぐらい遅延する問題が発生してしまい、使用をやめました。またバージョンアップしたら試してみようと思います。

*1:本体ディスプレイでは効きません。

*2:当時使わなくなったのは本体の CPU 負荷が高かったからという記憶があります。

GitHub Container Registry でプライベートなイメージを GitHub Actions + Kubernetes で使う

かなり前 GitHub Packages でコンテナイメージを扱ってました。

blog.kondoumh.com

その後 Container Registry が登場しました。Packages の時は public なイメージの Pull にも認証が必要でしたが、Container Registry では不要になっています。そして既存の Packages のコンテナイメージは順次 Container Registry に移行されるみたいです。

docs.github.com

OSS じゃない開発では private な Container Registry に開発中のアプリのイメージを push し、CI パイプラインで pull して利用するケースが多いと思います。CI で Kubernetes クラスターにデプロイしてテストするケースもあるでしょう。その時の認証情報の 扱いなどが気になったので調べてみました。

docs.github.com

まず、手元の PC からの push / pull。

GitHub の Settings / Developer settings で PAT (Personal Access Token) を作成します。この時、write:packages をスコープに含める必要があります。この PAT を環境変数に設定して ghcr.io に docker login します*1

export CR_PAT=YOUR_TOKEN
echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin

この状態で docker build / tag / push するとプライベートなパッケージとしてコンテナが登録されます *2

docker push ghcr.io/OWNER/IMAGE_NAME:latest

f:id:kondoumh:20211024163329p:plain

GitHub Actions でイメージをビルドして push するには Container Registry と Actions を実行するリポジトリを接続した上で Container Registry への書き込み権限を付与する必要があります。

docs.github.com

手動で push した直後はリポジトリとの関連付けがないので Registry のページには接続用の UI が表示されています。

f:id:kondoumh:20211025165447p:plain

Actions を実行するリポジトリを追加して、Manage Actions access のセクションでリポジトリの Role を Write に指定します。

f:id:kondoumh:20211025165836p:plain

これで、GitHub Actions で PAT を使わなくても暗黙に設定される GITHUB_TOKEN を使ってプライベートイメージを扱えるようになります。PAT はセキュリティのため比較的短い期間で expire するのが普通で、長期間 expire しない PAT を GitHub Actions では利用しないことが推奨されています。

docs.github.com

イメージをビルドして、push するワークフローの例です。docker コマンドも使えますが、ビルド、タグ付け、push をしてくれる Action を利用しました。

jobs:
  Build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Build and push container image
      uses: elgohr/Publish-Docker-Github-Action@master
      with:
        name: ghcr.io/kondoumh/gh-container-registry/nodejs-server
        username: kondoumh
        password: ${{ secrets.GITHUB_TOKEN }}
        registry: ghcr.io

GitHub Actions で Minikube や Kind などの Kubernetes 環境にプライベートなコンテナイメージを使ってデプロイするには、事前に Secret を適用しておき、イメージの manifest で Secret を指定します。GitHub Actions のステップでは以下のように GITHUB_TOKEN を docker-password に指定して Secret を作成すれば OK です。

  Deploy:
    needs: [Build]
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - run: minikube start
    - name: Create secret for GitHub container registry
      run: |
        kubectl create secret docker-registry regcred \
          --docker-server=https://ghcr.io \
          --docker-username=kondoumh \
          --docker-password=${{ secrets.GITHUB_TOKEN }}
    - name: Deploy to minikube
      run: kubectl apply -f manifest.yml

適用している manifest では以下のように imagePullSecrets で Secret を指定します。

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: nodejs-api
  name: nodejs-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nodejs-api
  template:
    metadata:
      labels:
        app: nodejs-api
    spec:
      containers:
      - image: ghcr.io/kondoumh/gh-container-registry/nodejs-server:latest
        name: nodejs-server
        imagePullPolicy: Always
      imagePullSecrets:
      - name: regcred

パブリックなイメージを使用する場合は Secret は不要で使えました。やはりこれが Package registry に比べた改善点ですかね。 プライベートなイメージはデータ転送やストレージへの課金が発生するので注意が必要です。GitHub Actions によってトリガーされるデータ転送は無料ですが、GITHUB_TOKEN 使用しているかどうかで判定しているようですので料金の面からも PAT は使わない方がよさそうです。

docs.github.com

*1:Packages の時は docker.pkg.github.com でした

*2:デフォルトの公開設定はプライベートのようです

野良 Scrapbox アプリ - 動的なタブ幅のリサイズ

blog.kondoumh.com

Electron 14 がリリースされたので野良 Scrapbox に適用しました。

13の時は発生してなかったんですが、タブを作成するときにページ内リンク以外のメニューとか、ツールバーの favs などからページを開くとアプリごとサクッと落ちるようになってしまいました。

タブの UI 部品である electron-tabs に問題がありそうとあたりを付けて試しに最新バージョン (v0.15.0) を適用してみました。

www.npmjs.com

最新版では問題が出ませんでした。適用してたのは、v0.10.0 という2年ぐらい前のバージョンです。2年前といえば、Electron 6か7の頃。かなり古いのでやはりアップデートしないといけない感じです。

ずっとバージョンを固定してたのは、electron-tabs v0.11.0 以降 WebView を埋め込んだタブは折り返しされず隠れてしまうという現象があり、ずっと直っていないからでした。公式のサンプルですらそうなのでもうそういう仕様だと思うしかないみたいです。いちおう issue 登録したので様子見はしますが。

ということで、ブラウザのようにタブをたくさん開いたときにタブ幅をぎゅっと詰めて沢山開くことができるようにするかーってことで、リサイズを実装することにしました。

f:id:kondoumh:20210913152852p:plain

f:id:kondoumh:20210913152909p:plain

これは雑な計算でスタイルシートの幅調整しているだけですが、Chrome などのタブ幅調整がいかに視覚的に違和感ないよう高度に実装されてるかうっすら想像できました*1

github.com

*1:ブラウザのタブは DOM ではなくは各 OS のウィジェットで実装されてると思いますが

Node.js で Google Fit に体重データを登録する

先日 Node.js で Google Fit から歩数や体重データを取得するのをやりました。

blog.kondoumh.com

体重データはスマホアプリから登録できるけど Fit を使う前の過去データは手入力やってられてられないので、API による登録方法を調べました。

公式ドキュメントには記述が見つけられなくて API のヘッダーコメントのサンプルを頼りに試行錯誤が必要でした。

https://github.com/googleapis/google-api-nodejs-client/blob/master/src/apis/fitness/v1.ts

Python 実装ですがこのリポジトリが参考になりました。

github.com

GCP のプロジェクトで Fitness API を有効にしたり OAuth 2.0 クライアント ID を作成したり googleapis の NPM パッケージを使用したりするのは前回と同様です。

www.npmjs.com

まず体重データを登録するための DataSource を登録する必要があります。これには、 fitness.users.dataSources.create メソッドを使用します。dataType の定義が重要で、name 属性に com.google.weight を指定し、field 属性の配列にに体重データを格納するための name 属性 weightformat 属性 floatPoint を指定します。 devicemodelmanifacturer などはダミーデータで OK です。*1

async function createDataSource() {
  const res = await fitness.users.dataSources.create({
    userId: "me",
    requestBody: {
      "application": {
        name: "patch_weight",
        detailsUrl: 'https://example.com',
        version: "1"
      },
      "dataType": {
        name: "com.google.weight",
        field: [
          {
            name: "weight",
            format: "floatPoint"
          }
        ]
      },
      "dataStreamName": "patch_weight",
      "type": "raw",
      "device": {
        manufacturer: "mh",
        model: "hoge",
        type: "scale",
        uid: "pw-01",
        version: "1.0"
      }
    }
  });
  console.log(res.data);
}

この関数を、認証関数に続けて実行します。

authenticate(scopes)
  .then(client => createDataSource())
  .catch(console.error);

次のようなレスポンスが得られます。PROJECT_NO には Fitness API を有効化している GCP のプロジェクト番号が入ります。

{
  dataStreamId: 'raw:com.google.weight:PROJECT_NO:mh:hoge:pw-01:patch_weight',
  dataStreamName: 'patch_weight',
  type: 'raw',
  dataType: { name: 'com.google.weight', field: [ [Object] ] },
  device: {
    uid: 'pw-01',
    type: 'scale',
    version: '1.0',
    model: 'hoge',
    manufacturer: 'mh'
  },
  application: {
    version: '1',
    detailsUrl: 'https://example.com',
    name: 'patch_weight'
  },
  dataQualityStandard: []
}

この dataStreamId を指定して、体重データを登録していくことになります。

fitness.users.dataSources.datasets.patch メソッドを使用して体重データを登録する関数を定義します。

async function patch(dataSourceId, datasetId, start, end, time, val) {
  const res = await fitness.users.dataSources.datasets.patch({
    datasetId: datasetId,
    dataSourceId: dataSourceId,
    userId: "me",
    requestBody: {
      "dataSourceId": dataSourceId,
      "minStartTimeNs": start,
      "maxEndTimeNs": end,
      "point": [
        {
          dataTypeName: "com.google.weight",
          startTimeNanos: time,
          endTimeNanos: time,
          value: [
            {
              fpVal: val
            }
          ]
        }
      ]
    },
  });
  console.log(res.data);
}

時刻についてはなぜかナノ秒単位での指定が必要なので変換関数を用意し、特定日付で dataset を刻んで1件登録。測定時刻は午前7時としました。

function getNano(day) {
  const dt = new Date(day);
  return dt.getTime() * 1000000;
}

const dataSourceId = "raw:com.google.weight:PROJECT_NO:mh:hoge:pw-01:patch_weight";
const start = getNano("2020-12-31T00:00:00");
const end = getNano("2021-12-31T23:59:59");
const time = getNano("2020-12-31T07:00:00");
const datasetId = `${start}-${end}`;
const val = 69.3;

authenticate(scopes)
  .then(client => patch(dataSourceId, datasetId, start, end, time, val))
  .catch(console.error);

実行結果。エラーにはなりませんでした。

{
  minStartTimeNs: '1609340400000000000',
  maxEndTimeNs: '1640962799000000000',
  dataSourceId: 'raw:com.google.weight:PROJECT_NO:mh:hoge:pw-01:patch_weight',
  point: [
    {
      startTimeNanos: '1609365600000000000',
      endTimeNanos: '1609365600000000000',
      dataTypeName: 'com.google.weight',
      originDataSourceId: '',
      value: [Array]
    }
  ]
}

ちゃんとデータが入ったか確認するため、前回の歩数と体重データ取得関数で日付を指定して取得してみます。

const from = new Date("2020-12-31T00:00:00");
const to = new Date("2020-12-31T23:59:59");

authenticate(scopes)
  .then(client => aggregate(client, from, to))
  .catch(console.error);

実行結果。歩数は Pixel から、体重は API で登録した DataSource から取得され、他のデバイスと同等に集計されているようです。

raw:com.google.step_count.cumulative:Google:Pixel 3 XL:caa195e4620531ba:Step Counter
2020/12/31 15:05:19
2020/12/31 16:39:24
com.google.step_count.delta
3648

raw:com.google.weight:PROJECT_NO:mh:hoge:pw-01:patch_weight
2020/12/31 7:00:00
2020/12/31 7:00:00
com.google.weight.summary
69.3

ちなみに、Fit アプリで登録したデータはこのようにアプリでユーザの手入力とわかるようになっています。*2

raw:com.google.weight:com.google.android.apps.fitness:user_input
2021/7/23 8:58:26
2021/7/23 8:58:26
com.google.weight.summary
67.80000305175781

Pixel の Fit アプリでも反映まで少し時間がかかりましたが、API で登録したデータが確認できました。

f:id:kondoumh:20210726104907p:plain:w400

ということで、過去データも Fit に登録できるようになりました。

*1:電話や時計を持っているとそのデバイス専用の DataSource が登録されますが、更新はそのデバイスにしか許可されておらず、REST API で更新しようとすると認証エラーになります。

*2:Google Fit と連携可能な体重計の場合はここにデバイス名が入るはず。