Mac の PlatformIO IDE で #include エラーが出る問題を解決する

MacVSCode で PlatformIO を使って M5Stack の開発をしていると、#include <M5EPD.h> の行に

#include エラーが検出されました。includePath を更新してください。...
ソース ファイルを開けません "AvailabilityMacros.h" (dependency of "M5EPD.h")

というエラーが出現する。英語版では

#include errors detected. Please update your includePath. ...
cannot open source file "AvailabilityMacros.h"

であろう。

この状態でもプロジェクトのコンパイルは可能だが、VSCode が常にエラーの存在を訴えてきて annoying である。

このエラーが発生する原因は、最近の macOS には /usr/include が存在しないのにもかかわらず VSCode (もしくは PlatformIO もしくは C/C++ extension)が /usr/include を読みに行っているからだと思われる。

% xcrun --show-sdk-path
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk

で正しい path を確認してやり、platformio.inibuild_flags 内に次の行を追加すればエラーは消える。

build_flags = 
    ; ...
    -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/**

M5Paper で BLE Current Time Service を利用して Real Time Clock を設定する

M5Stack シリーズの一つ M5Paper には real time clock (RTC) BM8563 が搭載されており、M5.RTC ライブラリを利用して時刻の書き込み・読み込みを行うことができる。

この RTC の時刻を合わせる方法として、Wi-Fi を用いて適当な NTP サーバーへ接続し現在時刻を取得する例が Web 上に多く存在するが、Web 接続可能な Wi-Fi が常に使用可能とは限らない。

M5Paper には Bluetooth 機能も付属している。Bluetooth 規格の一種である Bluetooth Low Energy (BLE) には Current Time Service (CTS, リンク先はPDF) という現在時刻を取得できるサービスが存在する。Accessory Design Guidelines for Apple Devices (PDF) には、iOS 7.0 以降は Current Time Service が利用可能であると記載されている。すなわち、iPhone の時計が携帯電話回線経由で常にほぼ正確な時刻に合わせられていることを前提とするならば、M5Paper を BLE で iPhone に接続し、Current Time Service を用いて時刻を取得することで、M5Paper を Wi-Fi に接続することなしにほぼ正確な時刻を取得することが可能となる。

M5StickC を用いて Current Time Service より現在時刻を取得する試みはすでに存在する が、この手法では BLE デバイス検索アプリを別途用意する必要があり、また接続時に iPhone からのペアリング操作が毎回必要であり手間が大きかった。

今回、接続のための専用アプリが不要で、かつ一度ペアリングした以降は再接続時も iPhone 側での操作が不要である ESP32 用 Current Time Service クライアントライブラリ ddlsmurf/ESP32-ANCS-AMS-Notifications を見つけたので、これを紹介するとともに、M5Paper での利用テストプログラムを制作したので公開する。

早速であるが以下がそのプログラムである。公式の RTC 利用例前述のライブラリ を合体させた。初回起動時にはペアリング待ち状態となり、その間に iPhone 側の設定アプリからペアリングを行う。一度ペアリングされた以降は再度同一のプログラムを実行すれば自動で M5Paper が iPhone へペアリングされ時刻情報が取得される。ペアリング情報はおそらく内部的に利用されている ESP32 のライブラリがうまく管理してくれており、スケッチを書き換えても維持されるようだ。

#include <M5EPD.h>

//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//+++++ Get Current Time from BLE Current Time Service +++++
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#include <esp32notifications.h>

void setupTime(int year, int month, int date, int hour, int minute, int second);

void checkCTSAndUpdateRTC() {
  BLENotifications notifications;
  notifications.begin("CTS_test");  // BLE begin.
  while (1) {
    if (notifications.clientCTS->ready()) break;
    delay(100);
  }
  ble_cts_current_time_char_t *time = notifications.clientCTS->readTime();
  if (time->exact_time_256.seconds < 59) {  // Inclimentating time is complicated ... so forget about seconds = 59.
    delay(int(1000. - float(time->exact_time_256.fractions256) * 1000. / 256.));  // wait until next second.
    time->exact_time_256.seconds += 1;  // incliment.
  }
  setupTime(time->exact_time_256.year, time->exact_time_256.month, time->exact_time_256.day,
            time->exact_time_256.hours, time->exact_time_256.minutes, time->exact_time_256.seconds);
  notifications.stop();  // BLE stop.
}
//-----------------------------------------------------------


//++++++++++++++++++++++++
//+++++ RTC settings +++++
//++++++++++++++++++++++++
M5EPD_Canvas canvas(&M5.EPD);

rtc_time_t RTCtime;
rtc_date_t RTCDate;

char timeStrbuff[64];

void flushTime(){
    M5.RTC.getTime(&RTCtime);
    M5.RTC.getDate(&RTCDate);

    sprintf(timeStrbuff,"%d/%02d/%02d %02d:%02d:%02d",
                        RTCDate.year,RTCDate.mon,RTCDate.day,
                        RTCtime.hour,RTCtime.min,RTCtime.sec);

    canvas.drawString(timeStrbuff, 0, 0);
    canvas.pushCanvas(100,200,UPDATE_MODE_DU4);
}

void setupTime(int year, int month, int date, int hour, int minute, int second){
  RTCtime.hour = hour;
  RTCtime.min = minute;
  RTCtime.sec = second;
  M5.RTC.setTime(&RTCtime);

  RTCDate.year = year;
  RTCDate.mon = month;
  RTCDate.day = date;
  M5.RTC.setDate(&RTCDate);
}
//---------------------------


void setup(void) {
  M5.begin();
  M5.EPD.SetRotation(90);
  M5.EPD.Clear(true);
  M5.RTC.begin();
  canvas.createCanvas(400, 300);
  canvas.setTextSize(3);

  checkCTSAndUpdateRTC();
}

void loop() {
    M5.update();
    if( M5.BtnL.pressedFor(2000))  // shutdown.
    {
        canvas.drawString("Shutdown...", 0, 0);
        canvas.pushCanvas(100,200,UPDATE_MODE_DU4);
        delay(600);
        M5.disableEPDPower();
        M5.disableEXTPower();
        M5.disableMainPower();
        esp_deep_sleep_start();
        while(1);
    }
    flushTime();
    delay(500);
}

ぜひこのコードをご活用いただき、NTP にアクセスできない場所でもほぼ正確な時刻を M5Paper に教えてあげてほしい。

追記: M5Paper_FactoryTest にこの機能を実装した。GitHub の diff を載せる。Setting - Sync Time を押すと同期が実行される。

参考

コードの一部などを以下のサイトから拝借した。

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

make

$ 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 を送ってもよさそうだが……。