M5Paper の FactoryTest に HelloWorld アプリを追加する

M5PaperM5Stack より発売されたスマホ型 e-Ink 搭載 IoT デバイスである。

M5Paper には非常にリッチなサンプルプログラムである FactoryTest が存在する。

FactoryTest の GUI は EPDGUI, Frame というフレームワークからなる。これらは簡単に拡張できる構造となっており、実際に他のサンプルプログラムである Todo, Calculator でも利用されているが、非常に残念なことに Documentation が存在せず、Web で検索してもこれらを活用している例は少なく、各人が独自の UI を制作して利用している状態である。

この記事の目的は、そのような状態に一石を投じるべく、FactoryTest に HelloWorld アプリを実装する方法を示すことで、EPDGUI, Frame の使用方法を解説することである。

実際に動いているところの写真を載せる。

f:id:KainokiKaede:20210417175519j:plain f:id:KainokiKaede:20210417175527j:plain

実装後のプロジェクトは GitHub に掲載したFactoryTest からの diff を見ることで、新規機能の実装の際に必要な要素を概観することができる。ある程度プログラミングに慣れている人であれば記事を読むまでもなくこの diff を見ればよいだろう。

フレームワークの構造

EPDGUI フレームワークにおいて一つ一つの画面は Frame という名で管理される。新たな画面はこの Frame_Base クラスを継承して作成する。

EPDGUI フレームワークにおいて、画面に表示されるボタンや画像などの要素は EPDGUI_Base を継承して作成される。このクラスには位置・サイズ・タッチの判定などの機能が含まれている。たとえばこれを継承して作成された EPDGUI_Button クラスには、タッチされたときの挙動などが定義されている。

フレームの起動から終了までの流れ

このフレームワークがどのように用いられるかを、FactoryTest のコードを追うことによって見ていく。

起動

起動後、まず setup() 内から SysInit_start() が呼ばれる。

ここでは、はじめに表示する Frame である Frame_Main のインスタンスを作成し、それを EPDGUI_PushFrame を用いて frame_stack へ push している。

    // Frame_Main の新規インスタンスを作成。
    Frame_Main *frame_main = new Frame_Main();
    // EPDGUI_PushFrame でそのインスタンスを frame_stack へ Push。
    EPDGUI_PushFrame(frame_main);

このように、EPDGUI では Frame を frame_stack に push することで実行することができる。

続いて実行予定のフレームの全てを EPDGUI_AddFrame を用いて frame_map へ追加していく。これにより、名称を指定することで EPDGUI_GetFrame を用いて Frame を得ることができるようになる。ぶっちゃけこの構造はいらないんじゃないかと思う(実行したい Frame をその都度 frame_stack に push するのではだめなのか?)が、FactoryTest で用いられている方法なのでそれに従っておく。

    // 実行予定のフレームのインスタンスを作成。
    Frame_FactoryTest *frame_factorytest = new Frame_FactoryTest();
    // EPDGUI_AddFrame でそのインスタンスを名前付きで frame_map へ Add。
    EPDGUI_AddFrame("Frame_FactoryTest", frame_factorytest);
    // ...
    // ほかにも実行予定のフレームを frame_map へ登録していく。
    /// ...

実行

setup() が終了すると、loop() 内の EPDGUI_MainLoop() が実行される。

この中では、frame_stack の一番上にある Frame を frame_stack.top() で取ってきたのちに、そのフレームを初期化(frame->init(args) を実行)し、EPDGUI_Run(frame) を実行している。この EPDGUI_Run は frame が終了する際に終了する。つまり各 Frame が実行〜終了のプロセスを終えるごとに EPDGUI_MainLoop() は1回実行されるということになる。

EPDGUI_MainLoop() から実行された EPDGUI_Run の中にも while loop がある。

ここでは、まずその Frame が実行状態にあることを確認したのちに、frame->fun() を1回実行する。ゆえに各 Frame でループ実行したい処理がある場合は1ループぶんを frame->run() 内に実装すればよい。run() の戻り値が 0 のときその Frame は終了させられるので、終了したいとき以外は 0 以外を返さなければならない。

frame->run() が実行されたのち、タッチ状態が判定され、変化があれば EPDGUI_Process(x,y) が実行される。

EPDGUI_Process(x,y) では、epdgui_object_list (表示されているボタンなどの要素が全て入っている list)内の全てに対して UpdateState(x,y) を実行する。ゆえに、タッチイベントに反応させたい要素を作るには、UpdateState を実装したのちに要素のインスタンスを EPDGUI_AddObject を用いて epdgui_object_list 内に登録すればよい。

ここまでが EPDGUI_Run のループである。ループが終了するとまたループの頭に戻る。

終了

EPDGUI_Run のループ内で、PopFrame() を実行したのちに _is_run を 0 にすると、次の EPDGUI_Run 内のループ中に frame->exit() が呼ばれてフレームは終了される。ゆえにフレーム終了時に行いたい処理は frame->exit() 内に実装すればよい。

フレーム終了後は EPDGUI_MainLoop() が再度頭から実行され、frame_stack.top() にある次のフレームが EPDGUI_Run(frame) される。

HelloWorld アプリを実装する

いよいよアプリの実装に入る。

まずは座標などを決めるために、何らかのお絵かきアプリを用いて目標とする画面を作ってみる。私は下のような画面を作成した。

f:id:KainokiKaede:20210417183601j:plain

続いて frame_helloworld.cpp, frame_helloworld.h を作る。ヘッダファイルでは変数定義のみを行っている。frame_helloworld.cpp について解説する。

前述したように、新しい Frame を作成するにあたって実装する必要があるのは、コンストラクタ・デストラクタ・init・run・exit である。ただし run, exit の際に何もしなくていいなら、空の関数が基底クラスで定義されているので再定義は不要である。

#include "frame_helloworld.h"

Frame_HelloWorld::Frame_HelloWorld(void)
{
    // コンストラクタ。

    // _frame_name を定義する。
    _frame_name = "Frame_HelloWorld";

    // Frame(アプリ)名を上段に表示する。
    _canvas_title->drawString("HelloWorld", 270, 34);

    // 左上に表示される、ホーム画面に戻るボタンを作成する。
    // exitbtn 関数 は Frame_Base に定義されている。実行すると _key_exit 変数に戻るボタンのインスタンスが作成される。
    exitbtn("Home");
    // ボタンから指が離れたときに、コールバック関数に渡す 0 番目の変数を定義する。ここでは _is_run を渡している。
    _key_exit->AddArgs(EPDGUI_Button::EVENT_RELEASED, 0, (void*)(&_is_run));
    // ボタンから指が離れたときに実行されるコールバック関数 exit_cb(Frame_Base で定義されている)をバインドしている。
    _key_exit->Bind(EPDGUI_Button::EVENT_RELEASED, &Frame_Base::exit_cb);

    // Hello World と表示するボタンのインスタンスを作成している。位置は前述のお絵かきアプリで作成した画面から算出する。
    _key_helloworld = new EPDGUI_Button("Hello M5Paper World!", 30, 137, 480, 190, EPDGUI_Button::STYLE_DEFAULT);
}

Frame_HelloWorld::~Frame_HelloWorld(void)
{
    // デストラクタ。とくに何もしない。
}

int Frame_HelloWorld::init(epdgui_args_vector_t &args)
{
    // init.

    // まずこの Frame が実行中であるフラグを立てる。_is_run = 0 の状態で次のループが始まると Frame は終了される。
    _is_run = 1;
    // 画面を全消しする。
    M5.EPD.Clear();
    // アプリ名を上段に表示する。
    _canvas_title->pushCanvas(0, 8, UPDATE_MODE_NONE);
    // epdgui_object_list に Hello World を表示しているボタンを登録している。こうしなければボタンは表示されない。
    EPDGUI_AddObject(_key_helloworld);
    // 同様に、epdgui_object_list に戻るボタンを登録している。
    EPDGUI_AddObject(_key_exit);
    // FactoryTest の Frame は 3 とか 6 とか 9 とかを返している。この値を参照するコードはないのでテキトーでよさそう。
    return 3;
}

// run, exit のときには何もしなくてよいので定義していない。

ここまでで HelloWorld フレームは作成できた。

続いて FactoryTest からこのフレームを実行できるようにする。変更点は GitHub の diff のほうが見やすいのでそちらを見ていただいてもよい。

frame/frame.h に frame_helloworld.h を追加。これはもしかしたら不要かもしれない。

#include "frame_helloworld.h"

systeminit.cpp に次を追加。これで frame_map に "Frame_HelloWorld" のキーで HelloWorld フレームのインスタンスを作成できた。

        Frame_HelloWorld *frame_helloworld = new Frame_HelloWorld();
        EPDGUI_AddFrame("Frame_HelloWorld", frame_helloworld);

92x92 のアイコンを作成して、

f:id:KainokiKaede:20210417183532p:plain

M5Paper のリポジトリに付属しているツールである image2gray.py でアイコンを  ImageResource_HelloWorld.h に変換。変換先のファイルを一度開いて、ifndef, define の行を書き換えないとファイルが読み込まれないので注意。

#ifndef IMAGERESOURCE_HELLOWORLD_H
#define IMAGERESOURCE_HELLOWORLD_H
// ...

frame_main.cpp に新しいボタンを追加して、それを押すと HelloWorld フレーム(アプリ)が開くように変更。

#include "frame_helloworld.h"
#include "../resources/ImageResource_HelloWorld.h"

enum{
    kKeyHelloWorld
}

// HelloWorld アプリのボタンを押したときに呼ばれるコールバック関数。
// Frame_HelloWorld のインスタンスを frame_stack へ push している。
void key_helloworld_cb(epdgui_args_vector_t &args)
{
    Frame_Base *frame = EPDGUI_GetFrame("Frame_HelloWorld");
    if(frame == NULL)
    {
        frame = new Frame_HelloWorld();
        EPDGUI_AddFrame("Frame_HelloWorld", frame);
    }
    EPDGUI_PushFrame(frame);
    *((int*)(args[0])) = 0;
}

// ...

_key[8] = new EPDGUI_Button("HelloWorld", 20, 390, KEY_W, KEY_H);

// ...
    _key[kKeyHelloWorld]->CanvasNormal()->pushImage(0, 0, 92, 92, ImageResource_helloworld_icon_92x92);
    *(_key[kKeyHelloWorld]->CanvasPressed()) = *(_key[kKeyHelloWorld]->CanvasNormal());
    _key[kKeyHelloWorld]->CanvasPressed()->ReverseColor();
    _key[kKeyHelloWorld]->AddArgs(EPDGUI_Button::EVENT_RELEASED, 0, (void*)(&_is_run));
    _key[kKeyHelloWorld]->Bind(EPDGUI_Button::EVENT_RELEASED, key_helloworld_cb);

    _names->fillCanvas(0);
    _names->drawString("HelloWorld", 20 + 46, 16);
    _names->pushCanvas(0, 337+151, mode);

int Frame_Main::init(epdgui_args_vector_t &args)
    for(int i = 0; i < 9; i++)
    {
        EPDGUI_AddObject(_key[i]);
    }

これにて終了。コンパイルして M5Paper に送ると HelloWorld アプリが出現しているはずである。

結語

この記事によって M5Paper の GUI 開発がさかんになれば幸いである。

MacBook Pro (Retina, 13-inch, Late 2013) を macOS Big Sur にアップデートできない

自分が持っている古い MacBook Pro (Retina, 13-inch, Late 2013) を Big Sur にアップデートしようとしたら、システム環境設定のソフトウェアアップデート画面で「必要なバージョンの macOS がありません」(英語だと "The requested version of macOS is not available." )と表示されてアップデートできなかった。この型式のコンピュータは macOS Big Sur と互換性のあるコンピュータ のリストに入っているので本来ならばアップデートできるはずである。

少し調べてみたところ、このモデルの MacBook Pro に Big Sur を導入すると文鎮化する問題 があるようで、一時的にアップデート対象から外された ようだ。

同時に Apple も文鎮化した場合の対応について公表している。アップデート対象に戻るかどうかは現段階では不明なようである。

Twitter でもこの現象は複数の人が経験していた (link1, link2 )。日本語で情報が見つからなかったので取り急ぎブログを更新する。

2021-02-06 追記:再度システム環境設定を確認したところ Big Sur へのアップデートが表示され、正常にアップデートできた。問題は解消したものと思われる。

M5Stack の Proto Module に収まる Real Time Clock を制作する

M5Stack は多機能なマイコンモジュールである。スピーカーとか microSD スロットとかがはじめからついているが、リアルタイムクロック (RTC) がついておらず、時計として使うには 毎回 Wi-Fi 経由で NTP を用いて時刻情報を取得する とか、iPhone などと BLE で接続し Current Time Service を用いて時刻情報を取得する とかのテクニックが必要である。これでは不便なので Proto モジュール 上に DS1302 という IC を使用して RTC を構築した。

ポイントは数点ある。

  1. GPIO のうちどのポートを使うか。
  2. ボタン電池をどうやって Proto モジュールに収めるか。

1つ目については M5 Stack ハマりどころ に詳しい。要は:

  • 使うべきではないポート
    • GPIO 25, Audioと共有
    • GPIO 23, LCDと共有
    • GPIO 19, SDと共有
    • GPIO 18, LCDと共有
    • GPIO 3, TXDと共有
    • GPIO 1, RXDと共有
    • GPIO 0, PULLUP BOOTと共有
  • 注意して使うポート
    • GPIO 34-39, 入力オンリー
    • GPIO 2, [GPIO 0]が0の時、これも0でリセットされるとダウンロードモードになる。
    • GPIO 12, 起動時に1だと LDOが1.8V, 0だと3.3Vになる。
    • GPIO 15 , GPIO 5 標準でPULLUP 起動時に設定すると SDIOスレーブタイミングの設定。

ひとまず今回は SCLK = 13, IO = 5, CE = 17 とすることに決めた。

2つ目についてはコイン電池ホルダーをいくつか試し、タカチ電機工業 表面実装型 コイン電池ホルダー SMTUシリーズ の CR2032 用 ならば入ったのでそれを利用した。 Proto モジュール内部で許容される高さはだいたい 5.5 mm ぐらいっぽい(上のホルダーは 5.4 mm)。

下に配線図と実際の写真を載せる。めちゃめちゃ汚い配線だが許容した。

f:id:KainokiKaede:20200828150239p:plain
M5Stack Proto Module RTC 配線図

f:id:KainokiKaede:20200827230116j:plain
M5Stack Proto Module RTC 写真

いろいろなところからコードをお借りしてガッチャンしてとりあえず動くコードを書いた。

#include <M5Stack.h>
#include <DS1302.h>
// DS1302 用のモジュールを https://github.com/msparks/arduino-ds1302 からダウンロードして使用可能にしておく。

#define incPin 39                   // (+) Inc. button
#define decPin 38                   // (−) Dec. button
#define entPin 37                   // Enter button


// Timer function: https://lang-ship.com/blog/work/esp32-timer/
hw_timer_t * timer = NULL;
void IRAM_ATTR onTimer() {
  // M5 LCD の左上のカーソル位置は (0, 0) で、右下のカーソル位置は (312, 232) である。
  // 1文字の大きさが (6,8) であることと液晶サイズが (320,240) であることとに依拠していると思われる。
  // これをはみ出していたら clear する。
  if (M5.Lcd.getCursorX() >= 320 || M5.Lcd.getCursorY() >= 240) {
    M5.Lcd.clear();
    M5.Lcd.setCursor(0,0);  // https://lang-ship.com/reference/unofficial/M5StickC/Tips/M5Display/
  }
}



// DS1302
// https://github.com/msparks/arduino-ds1302/blob/master/examples/set_clock/set_clock.ino を改変。

namespace {

// Set the appropriate digital I/O pin connections. These are the pin
// assignments for the Arduino as well for as the DS1302 chip. See the DS1302
// datasheet:
//
//   http://datasheets.maximintegrated.com/en/ds/DS1302.pdf
const int kCePin   = 17;  // Chip Enable
const int kIoPin   = 5;  // Input/Output
const int kSclkPin = 13;  // Serial Clock

// Create a DS1302 object.
DS1302 rtc(kCePin, kIoPin, kSclkPin);

String dayAsString(const Time::Day day) {
  switch (day) {
    case Time::kSunday: return "Sunday";
    case Time::kMonday: return "Monday";
    case Time::kTuesday: return "Tuesday";
    case Time::kWednesday: return "Wednesday";
    case Time::kThursday: return "Thursday";
    case Time::kFriday: return "Friday";
    case Time::kSaturday: return "Saturday";
  }
  return "(unknown day)";
}

void printTime() {
  // Get the current time and date from the chip.
  Time t = rtc.time();

  // Name the day of the week.
  const String day = dayAsString(t.day);

  // Format the time and date and insert into the temporary buffer.
  char buf[50];
  snprintf(buf, sizeof(buf), "%s %04d-%02d-%02d %02d:%02d:%02d",
           day.c_str(),
           t.yr, t.mon, t.date,
           t.hr, t.min, t.sec);

  // Print the formatted string to serial so we can see the time.
  M5.Lcd.println(buf);
}

}  // namespace



//曜日を求める。 https://edu.clipper.co.jp/pg-2-47.html
// 0 = 日曜日
int subZeller( int y, int m, int d )
{
    if( m < 3 ) {
        y--; m += 12;
    }
    return ( y + y/4 - y/100 + y/400 + ( 13*m + 8 )/5 + d )%7;
}

Time::Day subZellerForDS1302Library( int y, int m, int d)
{
    switch (subZeller(y, m, d)) {
      case 0:
        return Time::kSunday;
      case 1:
        return Time::kMonday;
      case 2:
        return Time::kTuesday;
      case 3:
        return Time::kWednesday;
      case 4:
        return Time::kThursday;
      case 5:
        return Time::kFriday;
      case 6:
        return Time::kSaturday;
    }
}



// 時計の設定。 http://radiopench.blog96.fc2.com/blog-entry-923.html を改変。

char buff[10];                     // 文字列操作バッファ
String yymmdd = "yyyy/mm/dd";      // 年月日文字列
String hhmmss = "hh:mm/ss";        // 時分秒文字列
int yy, mo, dd, hh, mi, ss;        // 時刻の要素

void getDateTime(){                // RTCに値を読む、日時の文字列を作成する
  int x;
  yymmdd = "";
  hhmmss = "";
  Time t = rtc.time();

  x =  t.yr%100;           // 年
  yy = x;
  sprintf(buff, "20%02d", x);      // 20に続いて右詰め2桁、1桁なら先頭にゼロ
  yymmdd += buff;

  x = t.mon;            // 月
  mo = x;
  sprintf(buff, "/%02d", x);       // / に続けて右詰め2桁
  yymmdd += buff;
  x = t.date;            // 日
  dd = x;
  sprintf(buff, "/%02d", x);       // / に続けて右詰め2桁
  yymmdd += buff;                  // 年月日の文字列完成(ex:2019/02/28)

  x =  t.hr;           // 時
  hh = x;
  sprintf(buff, "%02d", x);        // 右詰め2桁
  hhmmss += buff;
  x = t.min;            // 分
  mi = x;
  sprintf(buff, ":%02d", x);       // : に続けて右詰め2桁、
  hhmmss += buff;
  x = t.sec;            // 秒
  sprintf(buff, ":%02d", x);       // :に続けて右詰め2桁
  hhmmss += buff;                  // 時分秒の文字列完成(ex:01:02:03)
}

void oledDisp2Chr(int x, int y, int val) {  // OLEDの指定場所に2桁の値を表示
  sprintf(buff, "%02d", val);               // データーを10進2桁0フィル文字列に変換
  M5.Lcd.setCursor(x, y);                     // カーソルを指定位置に合わせて
  M5.Lcd.print(buff);                         // 数値を書き込み
}

int oledRW(int x, int y, int d, int stepD, int minD, int maxD) { // OLEから値を入力
  // OLEDの指定位置に2桁右詰めで変数の値を表示。ボタン操作で値を増減し、
  // Ent入力で値を確定し戻り値として返す。表示位置の左上をx, y 座標で指定
  // 操作位置は下線で表示。値は上下限の範囲でサーキュレート。文字サイズは2倍角(12x16画素)
  // 引数:x座標、y座標、変更したい変数、変更ステップ量、下限値、上限値

  oledDisp2Chr(x, y, d);                    // 画面の指定位置に数値を2桁表示(下線付き)
  while (digitalRead(entPin) == LOW) {      // enterボタンが押されていたら離されるまで待つ
  }
  delay(30);
  while (digitalRead(entPin) == HIGH) {     // enterボタンが押されるまで以下を実行

    if (digitalRead(incPin) == 0) {         // + ボタンが押されていたら
      d = d + stepD;                        // x を指定ステップ増加
      if (d > maxD) {                       // 上限超えたら下限へサキュレート
        d = minD;
      }
      oledDisp2Chr(x, y, d);                // 画面の指定位置に数値を2桁表示(下線付き)
      while (digitalRead(incPin) == 0) {    // + ボタンが離されるまで待つ
      }
      delay(30);
    }

    if (digitalRead(decPin) == 0) {         // - ボタンが押されていたら
      d = d - stepD;                        // x を指定ステップ減らす
      if (d < minD) {                       // 下限以下なら上限へサーキュレート
        d = maxD;
      }
      oledDisp2Chr(x, y, d);                // 画面の指定位置に数値を2桁表示(下線付き)
      while (digitalRead(decPin) == 0) {    // - ボタンが離されるまで待つ
      }
      delay(30);
    }
  }
  delay(30);
  return d;                                 // 戻り値
}

void clockAdjust() {                        // OLEDとボタンスイッチで時刻を合わせる
  M5.Lcd.println("Clock adj.");               // 時刻合わせ開始表示

  while (digitalRead(entPin) == LOW) {      // entボタンが離されるまで待つ
  }
  getDateTime();                            // 現在時刻を取得

  M5.Lcd.clear();                           // 画面を消して
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.println(yymmdd);                     // 現在の年月日を表示

  hhmmss[6] = '-';                          // 秒の桁に--を表示
  hhmmss[7] = '-';
  M5.Lcd.setCursor(13, 16);                   //  (表示位置要調整)
  M5.Lcd.println(hhmmss);                     // 時刻表示

  // x, y座標, 値, ステップ, 下限, 上限を指定して時計の設定値を入力
  yy = oledRW(12,  0, yy, 1, 0, 40);        // 年の値を入力
  mo = oledRW(30,  0, mo, 1, 1, 12);        // 月の入力
  dd = oledRW(48,  0, dd, 1, 1, 31);        // 日の入力。存在しない日(ex:2/31)も入力可能だが動作は不定
  hh = oledRW(13, 16, hh, 1, 0, 23);        // 時 (表示位置要調整)
  mi = oledRW(31, 16, mi, 1, 0, 59);        // 分 (表示位置要調整)

  // Initialize a new chip by turning off write protection and clearing the
  // clock halt flag. These methods needn't always be called. See the DS1302
  // datasheet for details.
  rtc.writeProtect(false);
  rtc.halt(false);

  // Make a new time object to set the date and time.
  // Sunday, September 22, 2013 at 01:38:50.
  Time t(2000+yy, int(mo), int(dd), int(hh), int(mi), 0, subZellerForDS1302Library(2000+yy, mo, dd));

  // Set the time and date on the chip.
  rtc.time(t);
}







void setup() {
  // put your setup code here, to run once:
  M5.begin();

  // Mute Speaker Noise: https://asukiaaa.blogspot.com/2020/03/m5stack-disable-speaker.html
  M5.Speaker.begin();
  M5.Speaker.mute();

  // Set ESP32 Timer: https://lang-ship.com/blog/work/esp32-timer/
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 1000000, true);  // us
  timerAlarmEnable(timer);



  if (digitalRead(entPin) == LOW){ // 起動時にEnt.ボタンが押されていたら
    clockAdjust();                 // OLED画面と押しボタンを使って時刻合わせ。
  }
  
}

void loop() {
  // put your main code here, to run repeatedly:
  printTime();
  delay(1000);
}

ふつうに起動するとずっと現在時刻を表示しつづける。ボタンC(一番右のボタン)を押しながら電源を入れると時計設定モードに入ることができる。

Misc

Proto モジュール、GND を引き出すことが想定されてないとしか思えない配置でつらい。

はじめは PCF8523 という IC を使用しようと思ったが、この IC の i2c address 0x68 と M5Stack Gray に標準搭載されている MPU9250 の i2c address 0x68 がぶつかってしまい使用不可であった。

ちなみに M5Stick-C ならば RTC がビルトインされているようだ。M5Stack にも載せてくれればよかったのに。

2021-08-16 追記

M5Stack 用 RTC モジュール基盤が Switch Science で販売されている。IC は PCF8563 で、I2C アドレスは 0x51 のようであり MPU9250 とのアドレス衝突もなさそう。

MacVim で日本語入力中に Shift+Space が効かない問題を修正する

問題

  • 私は日本語入力中でも Shift+Spaceで常に半角スペースを入力できるようにしている。
  • この機能はほぼすべてのアプリケーションで問題なく利用可能だが、唯一 MacVim のみで利用不可能であった。MacVim は私のメインエディタなので非常に困っていた。
  • MacVim-Kaoriya ではこの問題が修正されているが、更新が途絶えており可能であれば最新版の MacVim を使用したい。
  • macOS 標準の日本語入力ではなく Google 日本語入力に変更すればこの問題は解決するとする記事もあるが、私の環境では Google 日本語入力でも Shift+Space で半角スペース入力不可であった。

既存の報告

既存の報告もいくつかある。

MacVim-Kaoriya には下のパッチが取り込まれている。このパッチを取り込む形で最新版の MacVim をビルドすればよいと考えた。

commit 6b36374325cbcfe58172edfe95b6bbeff42b3bae
Author: Masayuki Yamaya <yamaya@cyberdom.co.jp>
Date:   Wed Oct 22 23:59:30 2014 +0900

    Fix dont input space with shift on yosemite

diff --git a/src/MacVim/MMTextViewHelper.m b/src/MacVim/MMTextViewHelper.m
index fdc7aaf..3da0c29 100644
--- a/src/MacVim/MMTextViewHelper.m
+++ b/src/MacVim/MMTextViewHelper.m
@@ -187,6 +187,11 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
         // with Ctrl-6 or Ctrl-^ when IM is active.
         [self doKeyDown:@"\x1e"];
         string = nil;
+#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_10)
+    } else if ((flags & NSShiftKeyMask) && [string isEqualToString:@" "]) {
+        // HACK! for Yosemite - Fix for Shift+Space inputing
+        // do nothing
+#endif
     } else {
         // HACK!  interpretKeyEvents: may call insertText: or
         // doCommandBySelector:, or it may swallow the key (most likely the

挿入ファイルと位置: https://github.com/macvim-dev/macvim/blob/35dc1a84c170d9945cd4cda69cafe60999f86824/src/MacVim/MMTextViewHelper.m#L176

ビルド

./configure

ビルドオプションは MacVim の Release 版をビルドしていると思われる Travis CI で指定されているものを利用した。 https://travis-ci.com/github/macvim-dev/macvim/jobs/372444524/config

$ ./configure LANGOPT="--enable-perlinterp=dynamic --enable-pythoninterp=dynamic --enable-python3interp=dynamic --enable-rubyinterp=dynamic --enable-luainterp=dynamic --with-lua-prefix=/usr/local" HAS_GETTEXT=1

2022-12-28 追記: ビルドは GitHub CI に移行したようだ。MacVim GitHub CI における MacVim Release 174 のビルド方法ワークフロー)を参考にビルドオプションを見直した。

configure 前に brew install lua などで lua などをインストールしておく。--enable-pythoninterp=dynamic はうまくいかなかったので除外した。arm64 の設定は ld: symbol(s) not found for architecture arm64 のエラーが出たため除外した。

$ ./configure --with-features=huge --enable-netbeans --with-tlib=ncurses --enable-cscope --enable-gui=macvim --enable-perlinterp=dynamic --enable-python3interp=dynamic --enable-rubyinterp=dynamic --enable-luainterp=dynamic --with-lua-prefix=/usr/local --with-macarchs=x86_64 --enable-fail-if-missing

make

Anaconda を使っている場合は make 前に conda deactivate しておく。 また Xcode をアップデートしてある場合は xcode-select --install しておく。

$ make -C src Vim
$ make

make 中にいくつかのエラーに遭遇したので対処した。

msgfmt: "ISO-8859-1" から "UTF-8" に変換できません. msgfmt は iconv() に依存しています.このバージョンは iconv() なしで作られています.

which gettext したところ Anaconda でインストールしたものが参照されていた。Deactivate して再度 make した。

$ conda deactivate

試行錯誤中に brew install gettext もしたが、おそらく conda deactivate だけでOKで、brew install gettext は不要だったと考える(∵ 公式のビルド説明書に Xcode Tools 以外には依存していないと書かれているので)。

tool 'xcodebuild' requires Xcode, but active developer directory '/Library/Developer/CommandLineTools' is a command line tools instance

Command Line Tools を使用可能にしていなかったために起きたと思われる。 下のURLを参考に使用可能とした。 https://qiita.com/eytyet/items/59c5bad1c167d5addc68

Xcode を起動→Preferences→Locations→Command Line Tools のリストボックスから Xcode 10.x(現在のバージョンと同一の名称)を選択した。

ちなみにXcodeをアップデートした際は毎回 $ xcode-select --install すべきとのこと。

結果

Shift+Space で半角入力ができる MacVim をビルドすることができた。

2014年頃から存在する問題のようで、Pull Request を送ってもよさそうだが……。

追記

MacVim r179 をインストールしてみたらこの問題が修正されているように見える。しかしどのコミットで修正されたのかは不明。

オフラインの状態で iOS に APN プロファイル設定ファイルを適用する

格安 SIM の使用には APN プロファイルの適用が必要であるものがある。 この設定ファイルはウェブサイトからダウンロード可能となっていることが多いが、たとえば海外に行って海外の SIM を使用したのちに日本に帰ってきた場合、着陸から空港の Wi-Fi が使えるようになるまでの間は日本の SIM が使えないということになる。 特に成田空港ではタキシングに15分程度かかることがあるため、オフラインの状態で APN プロファイルの再設定をすることができるようにしておけば15分早く家族に無事を伝えることができて親孝行である。

ウェブを検索すると、設定ファイルをメールで自分自身に送っておく手法が複数ヒットするが(下に参考URLを掲載する)、メールアプリのキャッシュからいつ消えるかわからないので不安が残る。 https://iwatomo3.blogspot.com/2017/09/iphoneapnsim.html https://www.iphone-kakuyasu-sim.jp/news/no-wifi-iphone-apn/

Pythonista を使えば iOS 内で HTML サーバーを立ち上げることができるので、Safari を開いてそこから設定ファイルをダウンロード可能である。 下のスクリプトと同じディレクトリ内に、事前にダウンロードしておいた設定ファイル(拡張子 .mobileconfig)を配置しておく。実行すると localhost:8080 でディレクトリ内のファイル一覧が見られるので、設定ファイルをタップするといつもの設定適用画面になる。複数の SIM を使い分けている場合は複数の mobileconfig ファイルを置いておけば場合によって使い分けられるだろう。

Spotify API を利用して、ライブラリ内の曲の情報などを保存する

Spotify で曲を保存したりしていても、いつか解約したときにすべて消えてしまうような気がしてつらい。 せめてどんな曲を保存していたかの情報があれば、乗り換え先の音楽サービスで同じものを探すことができるかもしれない。 幸い Spotify は善良なサービスなので、API を公開している。これを利用すれば情報を取得できる。

事前に Spotify for Developers に登録してクライアントIDなどを取得する必要がある。 https://developer.spotify.com/dashboard/login

また、Python から Spotify の情報にアクセスするために、Spotipy というライブラリを利用する。 https://github.com/plamere/spotipy

import sys
import spotipy
import spotipy.util as util

scope = 'user-library-read user-follow-read playlist-read-collaborative playlist-read-private'
username = 'your_username'
token = util.prompt_for_user_token(username, scope, client_id='your_client_id',client_secret='your_client_secret',redirect_uri='http://localhost/')

if not token:
    print("Can't get token for", username)
    sys.exit()

sp = spotipy.Spotify(auth=token)
# Get all saved tracks:

# The result is paginated, so use spotipy.Spotify().next() to retrive all items.
results = sp.current_user_saved_tracks(limit=50)
tracks = results['items']
while results['next']:
    results = sp.next(results)
    tracks.extend(results['items'])

for item in tracks:
    track = item['track']
    print(track['name'] + ' - ' + track['artists'][0]['name'])

# Save: tracks
San Diego - South Park
Once Upon a Time - Toby Fox
Start Menu - Toby Fox
...
Ribcage - Kettel
Bootmens - Kettel
Alacasa (Rolando Simmons Remix) - Kettel
# Get all saved albums:

results = sp.current_user_saved_albums(limit=50)
albums = results['items']
while results['next']:
    results = sp.next(results)
    albums.extend(results['items'])

album_tracks = {}
for album in albums:
    album = album['album']
    print(album['name'] + ' - ' + album['artists'][0]['name'])
    results = sp.album_tracks(album['id'])
    this_album_tracks = results['items']
    while results['next']:
        results = sp.next(results)
        this_album_tracks.extend(results['items'])
    album_tracks[album['id']] = this_album_tracks
    for item in this_album_tracks:
        print(item['name'])

# Save: albums, album_tracks
UNDERTALE Soundtrack - Toby Fox
Once Upon a Time
Start Menu
...
Ribcage
Bootmens
Alacasa (Rolando Simmons Remix)
# Get all followed artists:

results = sp.current_user_followed_artists(limit=50)['artists']
artists = results['items']
while results['next']:
    results = sp.next(results)
    artists.extend(results['items'])

for item in artists:
    artist = item
    print(artist['name'])

# Save: artists
Kettel
Domotic
Casey Abrams
...
Aphex Twin
Juana Molina
I Am Robot And Proud
# Get all playlists:

results = sp.current_user_playlists(limit=50)
playlists = results['items']
while results['next']:
    results = sp.next(results)
    playlists.extend(results['items'])

playlist_tracks = {}
for playlist in playlists:
    print(playlist['name'])
    results = sp.user_playlist_tracks(user=sp.me()['id'], playlist_id=playlist['id'])  # playlist の中身を表示
    this_playlist_tracks = results['items']
    while results['next']:
        results = sp.next(results)
        this_playlist_tracks.extend(results['items'])
    playlist_tracks[playlist['id']] = this_playlist_tracks
    for item in this_playlist_tracks:
        print(item['track']['name'])

# Save: playlists, playlist_tracks
Jazz Favorite
Del Sasser
IDM
...
Words (feat. Bri Tolani) [kr1sh Remix]
Angery
Without Your Love (feat. Ralph Larenzo)
data = {'tracks': tracks, 'albums': albums, 'album_tracks': album_tracks, 'artists': artists, 'playlists': playlists, 'playlist_tracks': playlist_tracks}

import pickle
with open('spotify_data.pickle', mode='wb') as f:
    pickle.dump(data, f)

家事の自動化に必要な投資額とオススメ機種紹介

最近 Twitter で「食洗機買え」「ルンバ買え」の声をよく目にする。たしかにこれらの自動化家電は家事の負担を軽減させ、QoL の向上に寄与する。しかしそれらの声に対して「でもお高いんでしょう」「いろいろあってどれがいいかわからない」と尋ねる声も目にする。そこで、金額やオススメ機種の情報を探しているかた向けに、実際に食洗機・ルンバ・ドラム型洗濯乾燥機を導入してみた自分がそれぞれのオススメを紹介しようと思う。結論から書くと、すべて購入すると工事費をあわせて約 22 万円弱かかるが、それでも購入して損はない。

ロボット掃除機

ロボット掃除機は、1回の掃除で床を完璧に綺麗にするものではない。むしろ毎日動かして綺麗を維持するためのものである。ゆえに、1回の掃除力は高くなくてよく、金額としては安いものでも十分にその役割を果たしてくれる。

そうは言っても、他社製の安いものより Roomba はやはり安定なので、Roomba の最安品がおすすめである。

Roomba 641(2018-07-24 現在、約32000円)

カーペットを使用しない・床に物を置かないなど、生活をロボット掃除機に適したものに変更する必要はある。

食器洗い乾燥機 追記あり

ビルトインでない家庭用食洗機は Panasonic しか製造していないので、選択肢は少ない。 その少ない選択肢の中で、毎日使用する・昼食は外で取る前提を置けば、1-2人で生活するならばプチ食洗で間に合う。 乾燥機能は必須なので、必ず乾燥機能つきのものを選択する必要がある(洗濯機能のみのものは、洗濯完了後に食洗機の蓋を開ける必要があるが、それは自動化としては不十分である。選択完了後に蓋を開けられるような人は家事を自動化しようとは思い立たないはずである)。特殊な洗浄機能は不要である。以上の条件を満たすのは下の1機種のみである。

NP-TCM4(2018-07-24 現在、約40000円)

食洗機購入に際しての注意点としては工事費が別途かかることが挙げられる。本体以外に分岐水栓パーツ代(約1万円強;蛇口の形式によって異なる)+分岐水栓工事代(約1万円弱;業者によって異なる)が別途必要で、本体と合わせて計6万円程度の出費となる。

また、食器についても、この食洗機に入り、かつ食洗機対応の食器を選んで購入・使用する必要がある。

加えて、調理器具(フライパン等)も、T-fal の取っ手が取れるタイプのうち、食洗機対応で、この食洗機に入るものを使用すれば手洗いの手間が省ける。

2019-03-15 追記

2018年9月、水道工事の不要な食洗機が発売された。SKジャパン の SDW-J5L である。

2019年3月現在の価格が約40000円弱と、NP-TCM4と同等かそれ以下の価格であるのに加え、洗浄水の供給がタンク式なので水道工事が不要であるという大きな利点がある。 この利点によって、入居・退去時の水道工事代を削減することができるのに加え、設置自由度が上がるため一人暮らしの狭い台所でも設置場所を見つけることができる(排水はバケツなどを別途購入してそこへすればよい)。

価格・工事不要・設置自由度の点から、前述の NP-TCM4 よりおすすめできる品である。

ドラム型洗濯乾燥機

各社各機種存在するが、基本的な機能はどれも同じである。要は自動で洗濯から乾燥までやってくれればよいので、最安のもので問題ない。

たとえば、BD-SG100BL などで十分用をなすだろう(2018-07-24 現在、約120000円)

これについても食洗機と同様に、ノーアイロンシャツなど、乾燥後に放置してもシワにならない衣服を選ぶ必要はある。

まとめ

上記3機種で、計22万円弱。ロボット掃除機をより低価格なものに抑えれば20万円以内も夢ではない。それでもなかなか高額だが、これらによって家事の時間的・肉体的・精神的負担が激減する。初任給が出たら買って損はないと考える。