h_nari @ 熊本市のブログ。電子工作、プログラミング、ゲーム、TV、 政治、インターネットなどに日々の思い付きを、 うだうだ~と書いていきたい。
このブログにはコメント欄を設けておりません。 記事への御意見、ご質問はtwitter @h_nari宛に お願い致します。


アーカイブ


アマゾン・ベストセラー

メタ情報
RSS
Login

タグ:『プログラミング』の付いた記事

地図遊び

国土地理院の地図データなどを使うプログラムを書いて遊んでいる。 キッカケは 湊あくあの マイクラ配信での発言。 ENサーバとの接続前後で、お互いへの観光が盛んになった時期。 ENメンバーに見てもらうような施設をつくりたい。 日本的なものって何? 秋葉原? という発言があった。 秋葉原全体をマインクラフト上に再現するのは大変だが、 用地確保と土地の割当だけし、 あとは作りたい人が作りたいものを作るということにすれば 面白いのではなかろうか。 マイクラのブロック単位(1m)の地図を用意すれば作れるのでは? 地図は公開されている地図データから生成可能ではないかと考えた。

地図データを使う

地図データは 国土地理院のものを使用する。 提供される地図データは様々だが 基本はタイルと呼ばれる256x256のメルカトル図法の地図画像データ。 拡大率に相当するズームレベルというものがあり、 ズームレベル0では地球全体を1枚のタイルで表し、 ズームレベルが1増える毎にタイルの枚数が4倍になる。 最大ズームレベル18までの地図画像が提供される。

この画像は以下のURLでアクセス可能。

https://cyberjapandata.gsi.go.jp/xyz/{t}/{z}/{x}/{y}.{ext}

ここで{t}は地図の種類、 {z}はズームレベル、 {x}と{y}はタイルの座標となっている。 例えば以下のurlで日本周辺の地図が表示される。

タイルを利用するプログラムを作るには 目的の場所のタイルの座標を知る必要がある。 緯度経度から変換可能だが、 目的の場所の緯度経度がわからない。 結局GoogleMapのような地図ソフトが必要になる。

地図ソフトは Leafletのような地図ライブラリを 使用すると簡単に作ることができる。 地理院タイルを用いたサイト構築サンプル集に紹介されているが 31行のソースファイルで このような 地図ソフトが動く。 地図上に様々なマーク、図形等も表示可能なので、 そういうことをやりたい場合、GoogleMap APIを使うより やりやすいかもしれない。

プログラムを作る

メルカトル図法の地図は緯度が違うとスケールが違う。 正確に平面のスケール一定の地図に変換できないので妥協する。 基準点を決め、そこでのスケールを全体に適用する。 基準点の設定と領域のサイズの見当をつけるための ツールをleafletで作成した。 画面を以下に示す。

この例では中央通りと神田明神通りの交差点に 基準点を設定、512m x 512m のマーカーを表示している。 これだけの領域があれば秋葉原を充分カバーできそうである。 人によっては 256m x 256mでもカバーできるかもしれない。

基準点の座標を基準にタイルの画像を取得、 canvasに描画し、ブロック単位のグリッド(1m,4m,16m,128m)も 描画するプログラムを作成した。マイクラ座標とのオフセットも 設定できるのでマイクラのx,z座標も表示している。

これでマイクラ側での作業が可能になったので 自分のワールドでバーチャル・秋葉原の作成を開始したが、 地図が見にくい。画像を拡大しているので 線がにじむのだ。にじんでいる、どの部分を線として採用するかに 悩んでしまう。 この時点で ベクトルタイルの提供実験に気がつく。 ベクトルデータを描画してやればにじまない。

ベクトル地図

ベクトルタイルで提供されているデータについては 丁寧に説明 してあるのだが、知らないことが多くて戸惑う。 ファイルの形式は Vector tile specification 準拠。 この仕様は Google Protocol Buffers を符号化フォーマットとして使用している。 いろいろ調べてわかったのは Google Protocol Buffersを使用していると protocというプログラムでパーザ(構文解析プログラム)を 自動で生成できるらしい。 protocにVector tile specificationの vector_tile.protoを食わせてやる。 protocでjavascriptのプログラムを生成できるが、 typescriptのプログラムが欲しい。 探すと Protocol Buffers から TypeScript の型定義を作るというページがあり無事生成できた。 これで無事、にじまないブロックを描画できるようになった。

Vector Tileでは、データがLayerで分かれているが 道路(road)と建物境界(building)と川(river)だけ ブロックとして描画している。 マイクラ側では道路境界を 玄武岩、 建物境界を ネザーラックで描いている。

マイクラにバーチャル秋葉原

まずは、秋葉原のある程度の領域の 道路と建物境界の線を配置することを目標に作業している。 クリエーティブモードだと作業感が出て飽きそうな 気がしたのでサバイバルモード。 整地から大変だが、目的があれば整地も楽しい。 秋葉原周辺の高さを調べると、だいたい標高4mぐらいなので マイクラ側ではY=68としている。御茶ノ水駅方面に拡張する場合には 高くなっていくので注意が必要。

低い部分を埋め立てると大変なので、蓋をする形にしたが 蓋など作らず、いきなり空中に線を描いていったほうが 楽だったと反省している。

湧き潰しを完璧にするため Light Overlayを導入。 すごく楽になった。

最後に

プログラムは、まだ試作レベルだが いずれ整理して公開したい。 その時までにはマイクラのバーチャル秋葉原も もう少し見れるものにしたい。

マイクラは好きだが、 建築勢というわけでもないので、 ある程度施設が整うとやることが無くなってしまう。 このプログラムで現実の地形を再現するという目標が できたわけで、これは私の性に合っているようだ。 これでマイクラの整地を楽しめる。

国土地理院はデータをたくさん公開していて偉い。 湊あくあは、もっとマイクラ配信をして欲しい。



FANコントローラ作成中

PCファン用4Pコネクタを手に入れたので、 これを使った機器を作ろうと思う。 機能としては温度計測と、それに伴うFANの制御。 温度とFANの状況の報告など。 となるとCPUはESP32、 LCDは沢山もっている2.4インチLCDモジュール・タッチスクリーン付きとした。

ESP32は、当初 ESP32-WROOM32モジュールを直接使用するつもりだったが、 設計を進めるうち、意外と基板のスペースに余裕がないことに気が付き、 ESP32 Devkitを使用するとUSBシリアル、ファーム書き込み、電源レギュレータ等を 省けるし、使い回しも可能なので、こちらを使うことにし、基板を注文。 秋月にも部品を注文するが(コロナのせいで?)発送が滞っているらしく 基板と同時に到着した。

基板を組み立てると色々と設計ミスが判明。 大きなところではLCDモジュールのコネクタを180度間違えた。 でもまぁ機能の確認はできるので、 この基板でソフトの開発を行うことにする。

ソフトの環境はVSCode + PlatformIO。 使い方に慣れて最近は、こればかり使用している。 特にgitのログが見易い。

まずはLCDを使えるようにしようと、 どのライブラリを使うか考え、 自作の Humblesoft_ILI9341 を使用することにする。 Adafruit_ILI9341と比べると、 fontxの漢字が使用可能、部分スクロール機能、 高速化などが違うのだが、 今回は漢字は必須ではないし部分スクロールも使わない、 高速化も5年前の話なので今となっては速いかどうかわからないが、 自作のものの方が使いやすい、たまには更新したなどの理由で こちらにした。 使用するSPIを指定する機能などを追加して 表示できるようになった。

さて次はファンや温度センサーを動かしたいのだが タッチスクリーンでファンを制御できるようにしようか、 ライブラリは昔、作ったよね、どこだっけ? いきなりタッチスクリーン操作は面倒なので 最初はシリアルモニターからコマンドを入力して操作する CLI(Command Line Interface)の方が楽よね。 Arduino用のCLIライブラリも昔、作ったよね。 どこだ? gitにも置いていない。

既存のものを探してみると"arduino cli"で検索すると Arduino CLIというArduinoのCLIでの開発環境のページばかり 表示される。 "arduino cli library"で SimpleCLIを知るが、 あまり使い易そうではない。 はやり自作が一番ということで CLI_Libraryという名前で 改めて作成した。 使用例は、こんな感じ。

#include <Arduino.h>
#include <cli.h>
CLI cli;
bool cmd_sum(CLI *cli, const char *arg) {
  const char *p = arg;
  int sum = 0, iVal;
  while (get_int(p, &iVal, &p)) sum += iVal;
  return cli->printf("sum = %d\n", sum);
}
bool cmd_factorize(CLI *cli, const char *arg) {
  int n, i, c = 0;
  if (!get_int(arg, &n, NULL))
    return cli->error("integer argment required");
  if (n < 2) return cli->error("bad interger %d, must be >1", n);
  cli->printf("%d =", n);
  for (i = 2; i <= n; i++) {
    while (n % i == 0) {
      cli->printf(" %s%d", c++ ? "* ": "", i);
      n /= i;
    }
  }
  return cli->printf("\n");
}
void setup(void) {
  Serial.begin(115200);
  cli.init(&Serial);
  cli.cmd("sum", "Calculate the sum of the arguments", cmd_sum);
  cli.cmd("f", "factorize the argument", cmd_factorize);
}
void loop(void) {
  cli.update();
}

結構、気に入っているがどうだろう。 ほぼ、自分専用のライブラリであっても githubに置いておいたほうが使い易い。



ROS2勉強中

軽トラのラジコンを買ったので いろいろ遊びたいと思っているのだが、 だらだらやっても面白くないので この機会にROS2などを学び、 それらの機能を使って遊んで行こうと考えた。

ROSとはRobot Operating Systemのことで、 ロボットの制御を行う複数のプログラム間の通信など の機能を提供してくれるものらしい。 ROS2は、そのversion2。 あまりよくわかっていないが、 とりあえずインストールしてみる。

Raspbery PiにROS2をインストールする話は この辺の記事を参考にしてできた。 調べてわかったのは、ROS2をインストールするには, まず ubuntu linuxをインストールする必要があるらしい。 公式からubuntu用のパッケージが提供されているので ubuntuだと簡単にインストールできるが、他の環境だとかなり大変らしい。

Raspberry PiにはROS2をインストールできたが、 手許のパソコンにもROS2をインストールして通信させたいのだが、 手許のパソコンはWindows10とdebianなので、これにかなり苦労した。

いろいろ調べたら、 docker上でROS1/2を動かすという記事 を見つけた。dockerというのはバーチャル・マシンだろうという程度の理解しか無いのだが なんとか動かすことができた。 動いてしまうと Web経由でX-Windowも使えてとても便利。 しかし、この上のROS2をRaspberry-Pi上のROS2と通信させることができない。

ROS2の通信方法を理解していないことが問題なのだが、 dockerのネットワーク構成もややこしい。 multicastでなんとかならんかとおもうが、 multicastを使ったことが無いので、その勉強から始める。 結局、multicastのルーティング(?)がうまくいかず dockerは諦めた。

使っていないPCにubuntuをインストールしてやろうと考え、 古いノート ThinkPad X-200を引っ張り出すが 電源すら入らない。電池を脱いたりして試すが ピクリとも動かない。

次に、Windowsで使用していたPCを起動してみる。 電源を入れるとBIOSがピーピー鳴き出す。 cmosをリセット、ボタン電池も交換し、一度起動するが DVDドライブがうまく動かない。そのうち、またBIOSがピーピー 鳴き出す。メモリーカードなどを一度脱いて掃除してみるが 改善されなかったので諦めた。

最後に試したのが, Macbook Air。 最近ほとんど使っていないので ubuntuを入れてもかまわない。 Macbook Airのbios的な操作方法など知らなかったが、 ネットで 参考となる記事を見つけ、 順調にインストールできた。 その後のROS2のインストールもできた。 Macbook Air上のubuntuは、なかなか見た目が良く MacOSと比べて遜色がない。 MacOSに不慣れな私には ubuntuでも問題を感じない。

これでRaspberry PiとMacbook Air上で ROS2同士で通信できるようになった。 Raspberry PiにUSBカメラを繋ぎ、 v4l2_camera を起動。 Mac側で rqt_image_viewを起動し 無事、画像を転送できた。 これで最初の目標はクリアすることができたので、 次は何をすべきが考えるが、 ROS2の理解が圧倒的に足りないので、 ROS2の勉強をすることにする。 幸い、 ROSの公式ドキュメント Tutorialがよくできているので、 これを少しずつやっている。 最初から、これを読むべきだった気がする。

このTutorialをやるのに dockerで動くROS2環境が,とても便利だ。 PCで沢山のウィンドウを開いてtuturialをやることができる。



ESP32のタイマー割り込み

esp32でタイマー割り込みを使用するプログラムを書いた。 LEDを色んなパターンで点滅させ、状態を表示するプログラム。 割り込みを使えばプログラムが長い処理を行っている最中でも 状態を表示できる。

"esp32 timer 割り込み"で検索すると いろいろ情報が出てくるが多くのサンプルプログラムは 同じ。こんな感じ。


volatile int timeCounter1;
hw_timer_t *timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
void IRAM_ATTR onTimer(){
  portENTER_CRITICAL_ISR(&timerMux);
  timeCounter1++;
  portEXIT_CRITICAL_ISR(&timerMux);
}
void setup() {
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 1000000, true);
  timerAlarmEnable(timer);
}
void loop() {
  if (timeCounter > 0) {
    portENTER_CRITICAL(&timerMux);
    timeCounter1--;
    portEXIT_CRITICAL(&timerMux);
  }
}

このプログラムを参考にタイマー割り込み処理を組み込み 動作させると以下のErrorが発生し、再起動してしまう。

Guru Meditation Error: Core  1 panic'ed (Cache disabled but cached memory region accessed)

このメッセージで検索すると、どうやら割り込みハンドラに IRAM_ATTRを指定していない時に発生する ものらしい。 しかしIRAM_ATTRは付けている。 割り込みハンドラをサンプルプログラムと同等の小さなものに 変更してみるとErrorは発生しない。 自分のプログラムでErrorが発生する場合も、かならず発生する わけではなくタイマー割り込みの周期を短くすると発生しやすく 長くするとしにくくなる傾向がみられる。

いろいろ試しているうちに、あるアイデアを思いつく。 自分の割り込みハンドラではswitch文を使用している。 switch文はテーブル参照のような形にコンパイルされる場合も ある。 switch文を if .. else if .. 形式に書き直せば Errorが起きないのではなかろうか....

で、試すとErrorは起きない。解決したのだろうか? もともとErrorも発生したりしなかったりするので たまたま起きていないだけかもしれない。 徹底的に調べれば良いのかもしれないが、 そこまで気力が起きない。

自分が使いたかった動作ではErrorが起きていないので 自分としては問題解決したことにする。 この件に関し何かご存じの方がいらっしゃったら 教えていただければ幸いである。



vtuberチャンネル登録者数取得プログラム

昨日 サメちゃんこと Hololive-ENのGawr Guraが チャンネル登録者数200万人を達成した。 V-tuberではKizuna Aiに次ぐ2番目だそうだ。 Hololiveの100万人超えメンバーは 戌神ころね126万、 白上フブキ119万、 兎田ペコラ117万で つい先日、湊あくあが100万を達成、 宝生マリンも99.4万人なので 数日中に100万人を突破するだろう。 チャンネル登録者100万人を越える日 の予想グラフと比べてみると興味深い。

ということで チャンネル登録者数の動向が気になるので 登録者数自動取得のプログラムを作成した。 1日1回起動し、データベースにデータを記録、 あとでグラフ化して眺める感じ。

ScialBladeのデータ

データの取得先だが、 まずは おあさんの記事を参考に SocialBlade を調べる。トップページで検索すると 各チャンネル等のデータを見ることができる。 例えば サメちゃんのデータとか。 大して面白くは無い。 むしろチャンネル登録者が200万いるのに subscriber rankが10,824位であることに驚く。

データは Bussiness APIで取得できるようだ。 但し使用するにはcreditを購入する必要がある。 creditの値段は最低単位だと100creditで$50。 但し今は新年割引で$26.55で購入可能。 user statisticsは1creditで30日間参照可能。 top listは24時間可能だそうだ。 最初に$50払うのは痛いし、 データを取得し続け、お金を払い続けるのも 嫌なので他を当たることにする。

Youtube Data API

youtubeというかgoogle自体も データをAPIで提供している。 YouTube Data API の概要 という文書がある。 Googleにアプリケーションを登録すれば データが取得できるらしい。 過剰なアクセスを避けるため クォータの制限はあるようだが とりあえず無料で使用できるようだ。

欲しい情報は channelsのlistにある。 下のurlにアクセスするとyoutubeのチャンネルの 統計情報(動画数、再生回数、登録者数)を取得できる。

https://www.googleapis.com/youtube/v3/videos?id=channelのID
  &key=ApplicationのKey&part=id,statistics

登録者数取得プログラム

あとはチャンネルIDのリストがあれば 各チャンネルの登録者数のデータを取得できる。 これもデータベース上に置くことを考えたが 管理プログラムを作るのが面倒なので Excelシートで管理することにした。 登録者数取得プログラムで読み込んで処理する。

プログラムはpythonで書いた。 ライブラリは webの読み書きに requests, Excelの読み出しに openpyxlfを使用した。 データベースへの書き込みは fluentd(td-agent)を使用しているので requestsで行っている。

プログラムを示す。

#!/usr/local/bin/python3
import openpyxl
import pprint
import requests
import json
def read_member_list( file ):
    wb = openpyxl.load_workbook(file)
    sheet = wb['List']
    for row in range(1,10):
        for col in range(1,5):
            cell = sheet.cell(row=row, column=col)
            if cell.value == 'member_id':
                break
        else:
            continue
        break
    else:
        raise Error('"member_id" not found')
    cTitle = row
    cId = col
    cGroup = cName = cChannel = None
    for col in range(col+1, col+10):
        cell = sheet.cell(row=row, column = col)
        if cell.value == 'group':
            cGroup= col
        elif cell.value == 'name':
            cName = col
        elif cell.value == 'channel_id':
            cChannel = col
    if cGroup == None:
        raise Error('"group" not found')
    if cName == None:
        raise Error('"name" not found')
    if cChannel == None:
        raise Error('"channel_id" not found')
    row = cTitle + 1
    data = {}
    while True:
        id = sheet.cell(row=row, column = cId).value
        if not id:
            break
        name = sheet.cell(row=row, column = cName).value
        group = sheet.cell(row=row, column = cGroup).value
        channel = sheet.cell(row=row, column = cChannel).value
        data[id] = {'name' : name, 'group': group, 'channel_id': channel}
        row = row + 1
    return data
def get_subs_data( app_key, channel_list ):
    url = 'https://www.googleapis.com/youtube/v3/channels'
    r = requests.get(url, params = {'key': app_key,
                                    'id': ','.join(channel_list),
                                    'part': 'id,statistics'})
    return r.json()
def put_data( members, data_list):
    url = 'http://fuent_dのホスト/データのid'
    channel2id = {}
    for id in members:
        ch = members[id]['channel_id']
        channel2id[ch] = id
    send_data = []
    for d in data_list:
        channel_id = d['id']
        id = channel2id[channel_id]
        if id:
            s = d['statistics']
            cSubs = s['subscriberCount']
            cVideo = s['videoCount']
            cView = s['viewCount']
            send_data.append({ 'member_id' : id,
                               'view_count': cView,
                               'subscriber_count': cSubs,
                               'video_count' : cVideo})
        else:
            print('channel %s not defined' % channel_id)
    if len(send_data) > 0:
        s = json.dumps(send_data)
        requests.post(url, data = s)
        print('%d data sent' % len(send_data))
    else:
        print('no data sent')
if __name__ == '__main__':
    app_key = 'アプリケーションのID'
    m = read_member_list('エクセルファイルのパス')
    channels = []
    for id in m:
        channels.append(m[id]['channel_id'])
    json_data = get_subs_data(app_key, channels)
    put_data(m, json_data['items'])

このプログラムをcronで1日1回動かし、 データの取得が始まった。

グラフ化

集めたデータをグラフで見たい。 とりあえず grafanaでグラフ化してみるが、 たくさんのチャンネルを表示する設定を 1つづつ手作業で行うのは大変だ。 何か方法はないかと調べたところ、 grafanaではグラフ表示の設定を json形式でexportしたりimportしたり することができる。 手作業で1つのチャンネルを表示する設定を作り、 json形式でexport。 それをベースにプログラムで 全てのチャンネルのグラフ設定を生成し、 grafanaでimportしてやれば良い。

以下のプログラムを作成した。 ちなみにimportしている read_member_list.pyは 前の登録者数取得プログラム。 この中のexcel読込み関数を使用している。

#!/usr/local/bin/python3
import json
from pprint import pprint
from get_data import read_member_list
member_list = 'チャンネルのリストのエクセルファイルのパス';
template_file = 'grafanaでexportしたjsonファイルのパス'
output_file = '出力するjsonファイルのパス'
sql_template = ("SELECT\n"
                + "  UNIX_TIMESTAMP(date) as time_sec,\n"
                + "  subscriber_count as value,\n"
                + "  \"{1}\" as metric\n"
                + "FROM data\n"
                + "WHERE $__timeFilter(date) and member_id = \"{0}\"\n"
                + "ORDER BY date ASC\n")
m = read_member_list(member_list)
targets = []
for id in m:
    name = m[id]['name']
    t = { "alias" : "",
          "format" : "time_series",
          "rawSql" : sql_template.format(id,name),
          "refId"  : name}
    targets.append(t)
f = open(template_file, 'r')
t = json.load(f)
for p in t["panels"]:
    p["targets"] = targets
with open(output_file, 'w') as f:
    json.dump(t, f, indent=2, ensure_ascii=False)

grafanaの表示例を下に示す。

今後

データを取り始めて、まだ2週間ほどだから仕方ないのだが グラフ化したものの、ほとんど水平の線ばかりで面白くない。 1年も経てば面白くなるかもしれない。 過去のデータも欲しいので、SocialBladeにお金を払って 取得しようかとも思うが1人あたり1credit, 25円とかかかるのは ちょっと考えてしまう。

カーソルを合わせれば数値は表示されるので 登録者数を調べるのは早くなった。 微妙なところを拡大とかはgrafanaで出来ないので そういう表示プログラムを自作しても良いかも知れない。

それにしても jsonは便利だ。 任意のデータ構造を表現できる man readableなテキスト形式で 様々な言語のライブラリが提供されている。 昔こういうものを夢想していたことを思い出した。 こういうものを楽に扱えるマシン環境、 スクリプト言語等が揃ったおかげで 普及したわけだが、 なんかソフトウェアの進歩を実感する。



Serial.begin()を2回やるとダメな話

arduiono-esp32でlog_i()が使えるようになった と喜んでいたら、ある環境で使えない。

使える環境と比べつつ色々試した結果 Serial.begin()を2回呼び出すとlog_i()が 何も出力しなくなることがわかった。 この状態でも Serial.print()系は出力されるので わかりにくい。

Serial.begin()を2回呼び出すなんてしないだろうと 思うかも知らないが、例えば次の例では 2回呼び出されている。

void setup(void) {
    Serial.begin(115200);
    M5.init();
}

M5Stackを使っているとやりがち。 M5.init()でSerial.begin()が呼びだれている。 ちなみに第3引数にfalseを指定するとSerial.begin()の呼び出しは 抑制できる。

Serial.begin()を2回呼び出すとlog_i()というか それから呼び出されるets_printf()が出力されなくなる 理由だが、mutexがらみの問題かとは思うが 詳しくは調べていない。



arduino-esp32のCore Debug Level

arduino-esp32のライブラリのソースを見ると log_e()やlog_v()で 各種メッセージが出力されている ()。 これらのメッセージは Arduino IDEだと メニューのツール→Core Debug Levelで設定で 出したり出さなかったりできる。

platformioだと platformio.iniでbuild_flagsを指定してやればいい。

platformio.iniの例

[env:m5stack-core-esp32]
platform = espressif32
board = m5stack-core-esp32
framework = arduino
upload_port = COM39
monitor_port = COM39
monitor_speed = 115200
build_flags =
  -DCORE_DEBUG_LEVEL=3  ; 0:None, 1:Error, 2:WARN, 3:Info, 4:Debug, 5:Verbose

log_iの出力はシリアルポートに出力される。 例を下に示す。

[I][BLEDevice.cpp:593] addPeerDevice(): add conn_id: 0, GATT role: client
Connected
[I][main.cpp:91] loop(): connection succeeded.
Notify callback for characteristic ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6
temp = 20.5 : humidity = 25.0
Disconnected
[I][BLEDevice.cpp:604] removePeerDevice(): remove: 0, GATT role client
[I][main.cpp:88] loop():

[I]で始まる行がlog_iの出力で、 最初の行は 下の行 で出力されたものである。

log_i("add conn_id: %d, GATT role: %s", conn_id, _client? "client":"server");

つまり[I][BLEDevice.cpp:593] addPeerDevice():の部分は 自動で出力されている。 この辺の定義は esp32-hal-log.hにあるので興味がある人は 見て欲しい。

ポイントはソースファイル名がフルパスではなくファイル名だけ表示されているところ。 ソース中に __FILE__と書けば、コンパイル時にソースファイル名の文字列に 置換されるので printfデバッグでは下の行のテンプレートを 用意しソース各所に挿入してやると便利。

fprintf(stderr, "%s(%s:%d)\n",__FUNCTION__,__FILE__,__LINE__);

しかし、Arduionoでこれをやると、ソースファイルがかなり深いディレクトリーにあるので __FILE__がとんでもなく長くなりメッセージが読み難い。 仕方がないのでArduinoではファイル名は諦めて以下の行を挿入していた。

Serial.printf("%s:%d\n", __FUNCTION__,__LINE__);

ちなみに esp8266やesp32のarduinoでは Serial(Printクラス)でprintf()が使える。

log_iでは、どうしているかと調べたら __FILE__を直接使うのではなく pathToFileName()という関数を通して使用していた。 分かってしまえば簡単だが、なかなか自分でやる気は起きなった。

今後はprintfデバッグでlog_iあたりを使うことにしようと思う。



Electron + typescript で serialportを使う

PC用のプログラムを作成する場合、 Electron + typescript を使うのが最近の好みだ。 まだまだ理解していない部分も多いが vscodeとの一体感とか 見た目の綺麗さとか developer toolの使いやすさとか intelisenseとかが気に入っている。

自作モータドライバ基板の 特性等を調べるプログラムに 前回はpython + TkInterを使ったのだが 今回はElectron + typescript を使ってみた。 シリアル通信を使うので serialportライブラリを使用したのだが 使えるまでに色々とトラブったので記録しておく。

serialportが動かない

以前作ったプログラムを参考に Electron + typescriptのプログラムを 作っていく。 sassも使用しwebpackで まとめている。

npm install serialport し、 動かしてみるとエラーが出る。 bindingが見つからないというような エラーが出ている。 electron最新バージョンとの相性を疑い electron単体の雛形 にserialportを組み込んでみたが動いた。 エラーメッセージで検索した結果、 webpack.config.js に以下の記述を追加して 動くようになった。

    externals: {
        serialport: 'commonjs serialport'
    },

指定したモジュールをバンドル対象から外し 外部依存のままにするという意味らしい。

rendererプロセスでは動かない

serialportをrendererプロセスで使うと mainプロセスで使え というようなエラーが出る。 electronのrendererプロセスは htmlファイルの描画を行うプロセスで、 ここで ローカルのPCのファイルシステムやハードウェア(シリアルポート等)に アクセスできてしまうと、htmlファイル中に悪意のあるjavascriptプログラムが ある場合、いろいろと悪いこと、例えばファイルを全部消すとか、意図せぬファイルを 読み出されるとかされる可能性がある。 だからserialportがrendererプロセスで動かない というのは理解できる。

自分のプログラムでは外部のhtmlファイルを読むことは無いので 無頓着に全ての処理をrendererプロセス側で行っていて electronの出す警告も、なんか面倒くさいと思いつつ 場当たり的に処理してきた。 しかし、electronのversionが進む毎に rendererプロセスへの 制限は厳しくなるようだし、自分も外部HTMLファイルを読むような プログラムを将来書くかもしれないので、ここらでちゃんと対応することにした。

次の記事 : ElectronでcontextBridgeによる安全なIPC通信 - Qiita を参考に BrowserWindowのwebPreferencesオプションに 以下の指定を行った。

nodeIntegraton は false : rendererプロセスでnodeの機能は使えない。 javascriptの機能のみ使用可能。

contextIsolation は true : mainプロセスと rendererプロセスの windowオブジェクトが別のものになる。 そのかわり contextBridge が使用可能になる。

preloadで通信用オブジェクトを設置: preloadで指定したjavascripファイルで contextBridgd.exposeInMainWorldを使用し rendererプロセスのwindowに 通信用のオブジェクトを設定できる。 ここに ipcRenderer等を使用した関数を置き、 通信に使用する。 rendererプロセスの javascriptが使用できる追加のAPIは ここで作成したものだけに限定されることになる。

typescriptの型宣言

contextBridgeで追加したオブジェクトに redererプロセス側からアクセスするコードを 書くとtypescriptで windowsオブジェクトにそんなpropertyは無いと エラーになる。 対策を調べた結果、宣言をtypescriptで書き importしてやればいいらしい。 以下の宣言を書いた。

IApi.ts

export default interface IApi {
    onError: (func: (type: 'error' | 'log', ...args: any[]) => void) => void;
    getPortList: () => Promise;
    getComPort: () => Promise;
    ... 中略 ...
    onPortData: (func: (data: string) => void) => void;
}
declare global {
    interface Window {
        api: IApi;
    }
}

このIApiの定義は preload.tsからもImportしているので 食い違いがあればtypescriptがエラーを出してくれる。

preload.ts

const { contextBridge, ipcRenderer } = require('electron');
import IApi from './IApi';
import SerialPort from 'serialport';
const api: IApi = {
    onError(func: (type: 'error' | 'log', ...args: any[]) => void) {
        ipcRenderer.on('error', (ev, ...args) => { func('error', ...args); });
        ipcRenderer.on('log', (ev, ...args) => {func('log', ...args); });
    },
    getPortList: () => { return ipcRenderer.invoke('get-port-list'); },
    getComPort: () => { return ipcRenderer.invoke('get-com-port'); },
     ... 中略 ...
    onPortData: func => { ipcRenderer.on('port-data', (ev, data: string) => { func(data); }); },
};
contextBridge.exposeInMainWorld("api", api);

IApiの型情報はtypescriptがpreload.tsから自動で抽出し webpackが自動で上手く処理して手で書かなくても 良さそうなものだと思い調べてみたのだが 方法が見つからなかった。

jquery-confirmのエラーを消す

javascriptで使用するダイアログのライブラリでは jquery-confirmがお気に入り。 しかしjquery-confirmをtypescriptで使用すると

$.alert({
    title:'Alert!',
    content:'Testだよ'}
    );

などと書くことになるので、 typescriptがJQueryStaticには alertなどというpropertyは無いと エラーが出るのが悩みだった。 jquery-confirmのtypescript用の型宣言を探すが見つからない。

しかしもう宣言を追加する方法を学んだので対処できる。 以下のファイルを作成しimportすることで対応できた。

jconfirm.ts

interface JQueryStatic {
    alert: (any) => void;
    confirm: (any) => void;
    dialog: (any) => void;
}

rendererプロセスでエラー

これまでの修正を施したプログラムを実行すると redererプロセスで 'require not defined'というような エラーがでる。 requireは nodeの機能であり、 nodeの機能は使えなくしたので当然だ。 つまり webpackがnode用のコードを出力している わけだが修正の仕方がわからない。 babelとか導入する必要があるのだろうかと悩んだが、 結局 webpack.config.jsの rederer用の設定の targetを 'electron-renderer'から 'web'に変更することで対応できた。

メインプロセスのconsole.logの出力が見れない

これでプログラムは動くようになった。 vscodeのデバッガでプログラムを起動すると デバッグ用に入れたメインプロセスの console.logの出力が見れない。 これまではメインプロセス側では、ほとんど仕事をしていなかったので console.logの出力を見る必要がなかったが、 今後はUI以外の仕事は基本メインプロセス側でやることになるので、 見れないとかなり困る。

調べた結果、 .vscode/launch.jsonに以下の指定を追加することで vscodeのデバッグコンソールに出力されるようになった。

           "outputCapture": "std"

vscodeのデバッガも使えるはずなのだが、まだ使い方を憶えていない。

最後に

vscode + electron + typescript のプログラミング環境は かなり気に入っている。 まだ分からないことも多いが徐々に憶えて この環境でのknow how を集めていきたい。



XIAOMI湿度計その後

フィラメント・ケースの湿度を監視している XIAOMIの湿度計だがトラブルが発生した。 Linuxサーバーが遅くなる。 調べると bluepy-helperというプロセスが 幾つか全速で走っている。 どうも pythonのbluepyを使用しているプログラムで Peripheralとのconnectionを切断した時に 時々bluepy-helperのプロセスが走りっぱなしに なるようだ。

LYWSD03MMC.pyには bluepy-helperをkillする処理が何箇所か入っているが それでも充分では無いようだ。

いろいろプログラムを試して Threadをセンサー毎に走らせ connectionを繋ぎっぱなしで 切断しないプログラムにしたら bluepy-helper問題は発生しなくなった。 システムのload averageも監視しているが 特に重くなることも無いようだ。

ただし、 pythonでbluepyを使用し scanするプログラムが停止するようになった。 何かエラーが発生しているのだろう。 こちらのプログラムは停止していても さほど問題は無いので、このまま運用することにする。

プログラムを以下に示す。

#!/usr/local/bin/python3 -u
from bluepy import btle
import time
import sys
import os
import requests
import json
import threading
class XiaomiDelegate(btle.DefaultDelegate):
    def __init__(self, addr, sensor_id):
        btle.DefaultDelegate.__init__(self)
        self.addr = addr
        self.sensor_id = sensor_id
        self.tSend = None
    def handleNotification(self, chandle, data):
        if not self.tSend or time.time() - self.tSend > 60*10:
            self.sendData(data)
            self.tSend = time.time()
    def sendData(self, data):
        t = int.from_bytes(data[0:2],byteorder='little',signed=True)/100
        h = int.from_bytes(data[2:3],byteorder='little')
        v = int.from_bytes(data[3:5],byteorder='little')/1000
        s = self.sensor_id
        json_str = json.dumps([{'sensor_id' : s, 'value': t},
                               {'sensor_id' : s+1, 'value' : h},
                               {'sensor_id' : s+2, 'value' : v}])
        r = requests.post('http://my_server_addr/fluentd_port',
                      data=json_str)
def thread_getData(**kwargs):
    addr = kwargs['addr']
    sensor_id = kwargs['sensor_id']
    while True:
        try:
            p = btle.Peripheral(addr)
            p.writeCharacteristic(0x0038, b'\x01\x00', True);
            p.writeCharacteristic(0x0046, b'\xf4\0x01\0x00', True);
            p.withDelegate(XiaomiDelegate(addr, sensor_id))
            while True:
                p.waitForNotifications(10)
        except btle.BTLEException as e:
            print('Error:',e)
if __name__ == '__main__':
    sensors = {
        "A4:C1:38:91:79:3F": 138,
        "A4:C1:38:BC:11:D9": 132,
        "A4:C1:38:1F:EB:AB": 135,
        "A4:C1:38:79:5E:67": 141
    }
    threads = {}
    for addr in sensors:
        print('thread start for',addr)
        threads[addr] = t = threading.Thread(target=thread_getData,
                                             kwargs={'addr': addr,
                                                     'sensor_id':
                                                     sensors[addr]})
        t.start()
        time.sleep(20)

しかし、防湿フィラメント・ケースが完成し 湿度の監視ができるようになったら すっかり秋になり湿度が下がってしまった。 湿度50%ぐらいでも防湿は必要なのだろうか?



XIAOMI 湿度計到着

AliExpressに注文していた XIAOMIの湿度計がやっと届いた。 8月16日に注文していたので5.5週ぐらいで 届いたことになる。 価格は4個で1,229円。1個あたり306円と激安。

コンパクトで可愛いデザイン。 電池の絶縁テープを抜くと すぐに温度と湿度が表示された。 裏蓋を外し内部を確認。 電池はCR2032。 MOYASHI氏のフィラメントケースの ホルダーにバッチリはまって気持ちが良い。

データを取得するプログラム

アプリでアクセスできるらしいのだが、 どうせアプリでは使わないので MOYASHI氏のブログで紹介されている JsBergbau/MiTemperature2: Read the values of the Xiaomi Mi Bluetooth Temperature sensor 2を試す。

Python3.7以上が必要ということで、 自分のLinux機はPython3.5.3だったので ソースをダウンロードし 3.7.9をインストール。 LYWSD03MMC.pyをダウンロードし 動かすと usageが表示された。 これを読んで以下のことがわかった。

  • デバイスのMACアドレスを指定する必要がある
  • -cで回数を指定しないと永遠にデータを表示し続ける
  • -callでデータ取得時に外部プログラムを起動可能
  • データ補正機能が充実。湿度データがあまり信用されていない?

MACアドレスの取得

bluetoothについて詳しくないのだが、 湿度計のMACアドレスは以下のbluetoothctlコマンドを実行し、 scan onと入力することで取得できた。 LYWSD03MMC がXIAOMIの湿度計らしい。

$ bluetoothctl
[NEW] Controller 00:1B:DC:03:9F:EC hslpc24 [default]
[NEW] Device B0:99:28:A4:55:D2 F-PLUG_001BDC039FEC
[bluetooth]# scan on
Discovery started
[NEW] Device 51:64:82:65:03:9D 51-64-82-65-03-9D
[NEW] Device 57:FC:16:D4:63:DB 57-FC-16-D4-63-DB
[NEW] Device A4:C1:38:91:79:3F LYWSD03MMC
[NEW] Device A4:C1:38:79:5E:67 LYWSD03MMC
[NEW] Device A4:C1:38:1F:EB:AB LYWSD03MMC
[NEW] Device A4:C1:38:BC:11:D9 LYWSD03MMC
[CHG] Device 51:64:82:65:03:9D RSSI: -59
[CHG] Device A4:C1:38:79:5E:67 RSSI: -62
[CHG] Device 51:64:82:65:03:9D RSSI: -76
[CHG] Device A4:C1:38:1F:EB:AB RSSI: -71
[bluetooth]# quit
[DEL] Controller 00:1B:DC:03:9F:EC hslpc24 [default]
$

callbackプログラムの引数

-callで与えるプログラムに 渡される引数を確認するため、引数を 表示するだけのプログラムで動かしてみる。

$ cat callback.py
#!/usr/local/bin/python3
import sys
print(sys.argv)
$ python3 ./LYWSD03MMC.py -d A4:C1:38:91:79:3F -c 1 -call callback.py
Trying to connect to A4:C1:38:91:79:3F
Temperature: 28.78
Humidity: 65
Battery voltage: 2.726
1 measurements collected. Exiting in a moment.
/somewhere/callback.py sensorname,temperature,humidity,voltage,timestamp A4:C1:38:91:79:3F 28.78 65 2.726 1601098469
['/somewhere/callback.py', 'sensorname,temperature,humidity,voltage,timestamp', 'A4:C1:38:91:79:3F', '28.78', '65', '2.726', '1601098469']
$

sys.argv[1]にパラメータ名のリストが渡され sys.argv[2]以降に値が渡されている。 パラメータはLYWSD03MMC.pyのオプションで変化する場合が あるらしい。

データ記録プログラム

必要な情報が揃ったので、 湿度データを記録するプログラムを作成した。 データの記録はwebサーバのapiにアクセスすることで センサーIDと値が時刻とともにMysqlに保存している。

まず、callback用のプログラム。

$ cat xiaomi_callback.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys, os, urllib.request
sensors = {
    "A4:C1:38:BC:11:D9": 132,    # MAC addr: sensor_id
    "A4:C1:38:1F:EB:AB": 135,
    "A4:C1:38:91:79:3F": 138,
    "A4:C1:38:79:5E:67": 141
}
if len(sys.argv) > 1:
    params = sys.argv[1]
    i = 2
    data = {}
    for param in params.split(','):
        data[param] = sys.argv[i]
        i += 1
    id = data['sensorname']
    sid = sensors[id]
    keys = ['temperature','humidity','voltage'];
    for i in range(3):
        key = keys[i]
        if key in data:
            v = data[key]
            url  = 'http://my_server_addr/dms/api.put?'
            url += 'sensor=%d&value=%s' % (sid+i, v)
            urllib.request.urlopen(url)
else:
    for k in sensors.keys():
        print(k)
$

cronから呼び出すプログラム

nari@hslpc24$ cat xiaomi.sh
#!/bin/bash
DIR="/home/my_program_dir"
for id in  `$DIR/xiaomi_callback.py`; do
    /usr/local/bin/python3 -u $DIR/LYWSD03MMC.py -d $id -c 1 -call xiaomi_callback.py
done
$

このプログラムをcronで10分毎に起動している。

$ crontab -l
...
*/10 *    *   *   *   /home/my_program_dir/xiaomi.sh > /dev/null
...
$

実装

調べたMACアドレスと割り当てたセンサーID(番号)を印刷し、 両面テープでXIAOMI湿度計に貼り付けた。

フィラメント・ケース-3Dプリンタ間は PTFEチューブにいれてフィラメントを送っている。 このチューブには湿気の侵入を防ぐことと 3Dプリンタがフィラメントを引いても ケースが動かないという利点がある。 X-Smartのヘッドはチューブを受け止める 形状になっているが、ENDER3V2はそうなっていないので、 自作の部品を追加し、チューブを受け止めれるようにした。

計測結果

前にも書いたが Mysqlに書き込んだデータは grafanaでグラフ化し ブラウザで見る。 昨晩、湿度計を設置したあとのグラフを以下に示す。

湿度計は今回設置したXIAOMIの4個以外に2個ある。 1つは netatomの室内モジュールについているもので、 もう一つは, sht31モジュールで自作したもの。 XIAOMIの湿度計が、なかなか来ないので 待ちきれず作ってしまった。 sht31は湿度の精度は±2%で高精度ということになっている。 これと比べるとXIAOMIの奴は10%程度高い湿度を示すようである。

XIAOMIの湿度計の設置状況は以下の通り。

  • X-Smart用フィラメントBox(1F設置)
  • Ender3V2用フィラメントBox
  • フィラメント保管Box
  • Ender3V2近く

上3つは密閉ケースで乾燥機が入っている。 ケースに入れた時点で湿度が下がり始め、 蓋を開けたタイミングで湿度が戻る様子が 観測できる。

今後

乾燥剤がどれくらいの期間持つかとか、 本当に湿度絡みのトラブルがなくなるのかとか、 様子を見ていきたい。

余ったXIAOMI湿度計1個は フィラメントBOXをもう1個作って入れる予定。

2020/10/09(金)追記

このプログラムでは問題が発生してしまった。 詳しくは XIAOMI湿度計その後参照。


Arduino で XIAOプログラミング

HDDサンダーで 回転数をXIAOで読み取るまでできた。 その際に取得できた ArduinoでXIAOのプログラム、 ライブラリでは解決できず、 自分でチップのレジスタを操作するような プログラムを書くために 必要な知識をいくつか紹介する。


ソースの在り処

seeeduino XIAOのarduionoのソースは web上では https://github.com/Seeed-Studio/ArduinoCore-samd にある。割と見やすいので、よく利用する。 ただし tools以下のファイルは無い。 PC上では、arduinoのboardマネージャで導入した場合, C:\Users\ユーザ名\AppData\Local\Arduino15\packages\Seeeduino ディレクトリ以下に配置される。

Exploreで開けるのも面倒な場所なので C:\Users\ユーザ名\AppData\Local\Arduino15\packages フォルダを exploreで「クイックアクセスにピン留め」しておくと 各種CPUのarduinoのソースに簡単にアクセスでき 便利になる。

ついでに cygwinのchereコマンドで context menuに「Cygwin here」を追加しておくと、 cygwinを開き

$ find . -type f | xargs grep SOME_TEXT
$ find . -name '*.h' | xargs grep SOME_TEXT

などとして、任意の文字列の出現箇所を調べられるので便利。

プログラミングの雰囲気を掴む

チップのレジスタを操作するようなプログラムを いきなり書くのは敷居が高いので、 まずは簡単な機能のソースを見て 雰囲気を掴もう。

GPIOの操作の仕方は digitalWrite()のソースを見る。 wiring_digital.c にある。 PWMの使い方は wiring_analog.c のanalogWrite()を見る。

プログラムを詳しく読む

雰囲気だけではプログラムは書けないので、 真面目に XIAOで扱われているマイコン、 ATSAMD21G18に取り組もう。 データシートにざっと目を通す。

マイコンの各ブロックの制御レジスタは 型定義が

C:\Users\ユーザ名\AppData\Local\Arduino15\packages\Seeeduino\tools\CMSIS-Atmel\1.2.1\CMSIS-Atmel\CMSIS\Device\ATMEL\samd21\include\component
にブロックごとに有る。

実態の定義は以下に有る。

C:\Users\ユーザ名\AppData\Local\Arduino15\packages\Seeeduino\tools\CMSIS-Atmel\1.2.1\CMSIS-Atmel\CMSIS\Device\ATMEL\samd21\include\samd21g18a.h

これらを参照しながら読むとプログラムの詳細な動きが 理解できる。

PWMの周期を変更

前回の報告で述べた通り、 HDDモータを制御するPWM信号の周波数は DRV11873の仕様で 7~100kHzの範囲内である 必要がある。 Seeeduino XIAOの analogWrite()のPWMの周波数は 実測で730Hzぐらいなので 仕様に合わない。

これを修正すべく wiring_analog.c を読んだところ以下のことが判明した。

  • カウンタは16bitモードで動作
  • 48MHzのクロックを216=65536で分周し、 48,000,000Hz ÷ 65536 = 732.421875 Hzが出力されている。
  • カウントの最大値 0xffff を設定している箇所がある。

カウントの設定値を4800に変更することで、 PWMの周波数を 48MHz ÷ 4800 = 10KHz にすることができた。

信号の周期を計測

BLDCモータドライバDRV11873は モータ駆動の1サイクルに同期した パルスがFG端子から出力される。 この何サイクルかがモータの1回転に 対応するので、この信号の周期を計測することで モータの回転数を取得することができる。 ATSAMD21G18のカウンタでFG信号の周期を 計測したので、その方法を説明する。

カウンタで信号の周期を計測するための 構成を図に示す。 外部ポートPA4から入力された信号を EIC(外部割込)に接続しイベントを発生させ、 EVSYS(イベントシステム)でタイマーに接続し 周期を計測する。タイマーはTC4とTC5を合わせて 32bitタイマーとして計測する。 クロックはシステムクロックの48Mhzを使用するので 最大 232[count]÷ 48M[count/秒] = 89.47[秒]周期まで計測可能 となる。

特徴的なのがEVSYS(イベントシステム)モジュール。 これは各周辺ブロック間で信号をやり取りするバスで

  • 12チャンネル
  • イベント・ジェネレータ74個
  • イベント・ユーザ29個
となっている。 他のマイコン、例えばSTM32だと タイマーやADやDAなどの制御レジスタに これを1にすると入力がTimer-nのX信号が 接続されます、というようなbitがいろいろある。 謎のブロック間ネットワークである。 EVSYSはそれを一般化しているわけで 好意がもてる。 しかし、残念なことに制御レジスタのアドレス空間を ケチっているので使いにくい。

EICの使い方は WInterrupt.cにある attachInterrupt()のソースを見ると 大体わかる。

EVSYSは使い方が分かりづらい。 arduinoでは全く使われていない。 ハマったのは以下の2点。

  • クロックを供給する必要がある
  • レジスタを部分アクセスすると失敗する

クロックの供給というのは、コードで言うと以下の部分。

PM->APBCMASK.bit.EVSYS_ = 1;

STM32とかでプログラムしていると当たり前の部分だが、 Arduinoのソースを読んでいてもあまり出てこないので 気が付かなかった。 調べると wiring.cで結構初期化されている。

Datasheetの 11章 Peripherals Configuration Summaryが参考になった。

レジスタを部分アクセスすると失敗する というのは次の部分。 まず、正常に動かないコード。

EVSYS->USER.bit.CHANNEL = 2;
EVSYS->USER.bit.USER  = 0x13;

正常に動くコード。

EVSYS->USER.reg = EVSYS_USER_USER(0x13) | EVSYS_USER_CHANNEL(2);

このコードはイベント・チャンネル1 (CHANNEL-1の値)の出力を TC(USER 0x13)に接続しているが、 上側のコードではDMAC CH0(USER 0x00)にも接続されてしまい、 DMAC側でイベントを受け入れる設定になっていないので エラーとなり、イベント・チャンネル1が動かなる模様。 正常に動くコードにたどり着くまで、かなり時間がかかった。

割込処理

これでタイマーに信号の周期が記録されるようになるが、 プログラムの処理上、周期計測後に割込を発生させる。 割込の処理も WInterrupt.cが参考になる。

タイマーのレジスタで割込を有効化し、 以下のコードでNVIC(割り込みコントローラを設定する)

IRQn_Type v = TC4_IRQn;
NVIC_DisableIRQ(v);
NVIC_ClearPendingIRQ(v);
NVIC_SetPriority(v, 0);
NVIC_EnableIRQ(v);

cortex_handlers.c にデフォルトの割込ハンドラが定義されている。 TC4関連だけ抜き出すと以下のようになる。

/* Default empty handler */
void Dummy_Handler(void)
{
#if defined DEBUG
  __BKPT(3);
#endif
  for (;;) { }
}
...
void TC4_Handler ( void ) __attribute__ ((weak, alias "Dummy_Handler")));
...

デフォルトのTC4割込ハンドラは Dummpy_Handlerという無限ループのハンドラが設定されている。 weak属性が付いているので、 TC4_Handlerという 関数を定義すれば呼び出される。

void TC4_Handler(void){
  uint8_t flags = TCx->COUNT32.INTFLAG.reg;
  if (flags & TC_INTFLAG_MC0) {
    ... 割込処理 ...
  }
  // 割込フラグクリア
  TCx->COUNT32.INTFLAG.reg = 0xff;
}

VSCodeのエラーを消す

VSCodeでArduinoのソースを編集していると intellisenseで大量のエラーが表示される。 今までは無視してきたのだが、 今回 エラーを消すべく設定を調べてみた。

intellisenseの設定は プロジェクトのフォルダーの .vscode/c_cpp_properties.json を用意すれば良いのだが、 自分で最初から書くのは大変なので VSCodeにArduino拡張を利用する。 拡張機能タブでArduinoを検索すると 3つぐらい見つかるので、その中の Microsoft製のものをインストールする。

インストール後、コマンドパレットで Arduino:initializeを実行。 Boardで Seeduino XIAOを選択すると .vscode/c_cpp_properties.json が生成されるが、 まだ膨大にエラーが表示される。

修正が必要なのは defines と includePath。 definesの参考になるのが Seeeduino Arduinoの boards.txtファイル。 これに

seeed_XIAO_m0.build.extra_flags= -DARDUINO_SAMD_ZERO -D__SAMD21__ -D__SAMD21G18A__ -DARM_MATH_CM0PLUS -DSEEED_XIAO_M0 {build.usb_flags}
という行がある。 あと、足りないincludePathを追加し 以下の設定でエラーが無くなった。

{
    "env": {
        "users": "C:/Users/ユーザ名",
        "ulib": "${users}/Documents/Arduino/libraries",
        "seeeduinoDir": "${users}/AppData/Local/Arduino15/packages/Seeeduino",
        "samd": "${seeeduinoDir}/hardware/samd/1.7.6"
    },
    "configurations": [
        {
            "name": "Win32",
            "includePath": [
                "${ulib}/Adafruit_SSD1306",
                "${ulib}/Adafruit_GFX_Library",
                "${samd}/cores/arduino",
                "${samd}/libraries/SPI",
                "${samd}/libraries/WIRE",
                "${samd}/libraries/Adafruit_ZeroDMA",
                "${samd}/libraries/TimerTCC0",
                "${samd}/variants/XIAO_m0",
                "${samd}/**",
                "${seeeduinoDir}/tools/**"
            ],
            "forcedInclude": [
                "${samd}/cores/arduino/Arduino.h"
            ],
            "intelliSenseMode": "gcc-x64",
            "compilerPath": "${seeeduinoDir}/tools/arm-none-eabi-gcc/7-2017q4/bin/arm-none-eabi-gcc.exe",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "defines": [
                "ARDUINO=10813",
                "ARDUINO_SAMD_ZERO",
                "__SAMD21__",
                "__SAMD21G18A__",
                "ARM_MATH_CM0_PLUS",
                "SEEED_XIAO_M0"
            ]
        }
    ],
    "version": 4
}

includePathで、 ** でサブディレクトリとして 指定してあるのに個別のディレクトリを指定している 部分がある。これは個別指定の方を削除すると エラーになってしまうので仕方なく入れている。

intelliSenseModeとcompilerpathも デフォルトから変えている。 これはデフォルトのままだと pinMode()が未定義というようなエラーが 出るためで、原因はわからない。 intellisenseは謎が多い。

次は

HDDの出力をPWMで制御し、 回転数も検出できるようになった。 次は、フィードバックをかけて 定速制御を行えばいいのだが、 フィードバック・ゲインをどう決めるかが問題。

いつもなら、実験しながら適当に決めてしまうのだが、 ちゃんと特性を計測し、極配置とかで設計してやるのも 面白いかもとか思っている。 どうなることか。



「ESP8266のntpの設定は1行で」更新

「ESP8266のntpの設定は1行で」 というのは 3年前にQiitaに投稿した記事なのだが、 地味にLGTM(Look Good to Me,旧like)を 貰い続けている記事の1つ。

その記事に引数の値がおかしい、と コメントがついた。 サンプルプログラム通りだと 時間が18時間遅れるらしい。

調べると esp8266/arduinoの versionのせいらしい。 2.6.3までは正常に動くが 2.7.0以降はおかしくなる。 configTime()関数の 第1引数の符号の解釈が 逆になってしまったらしい。

これが一時的なバグなのか、 恒久的な仕様変更なのかわからないと configTime()関数は使用しづらい。 幸い 2.7.0以降 configTzTime()という 関数が追加されており、TimeZoneを 文字列("JST-9"とか)で指定する。 これなら、将来 仕様が変わることも ないだろうということで、 Qiitaの記事を更新した。

Arudioでプログラムしていると ソースが全部 githubで見れるのは いいのだが、みんな容赦なく 更新してくるので、公開したプログラム でコンパイルエラーが出る、などという目に 割とよく合う。 プログラム公開時には、動作確認した ライブラリのversionを明示しておいた方が良い。 問題発生時、ライブラリのversionを指定のものに 戻して下さいと言える。



USB-I2C基板 (ソフトウェア編)

USB-I2C基板はpythonから使いたい。 pythonでI2C経由で機器を操作する プログラムを作りたい。 MCP2221 Pythonで検索すると いろいろ情報が見つかる。


PyMCP2221Aを試す

PyMCP2221Aというライブラリを試す。 MCP2221Aの機能(ADC,DAC,GPIO,CLK,I2C)を全て サポートしているようだ。

I2C接続のセンサーを試すと、簡単に値の読み取りなど できるようになった。もう少し派手なデモも欲しいので、 I2C接続のOLEDディスプレイ(128x64モノクロ)も試すが、 うまくいかない。 調べると PyMCP2221Aは 60byteまでの書き込みにしか対応していないことが判明。 そこで 60byte超のデータにも対応できるよう改造してみるが うまくいかない。

Circuit Python Libraryを試す

Adafruitも MCP2221AのUSB-I2Cボードを出しているので、 それ用のPythonライブラリを試してみる。 すると描画はできるが、繰り返すとエラーになる。 試したプログラムを以下に示す。

import os
os.environ['BLINKA_MCP2221'] = '1'
from board import SCL, SDA
import busio
import adafruit_ssd1306
from random import randint
i2c = busio.I2C(SCL, SDA)
oled = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c)
oled.fill(0)
for i in range(1000):
    print(i)
    x0 = randint(0,128)
    y0 = randint(0,64)
    x1 = randint(0,128)
    y1 = randint(0,64)
    oled.line(x0,y0,x1,y1,1)
    oled.show()

実行結果も下に示す。

$ C:/Users/nari/AppData/Local/Programs/Python/Python36-32/python.exe c:/01proj/v385_USB_I2C/python/blinka_test.py
0
1
 ... 中略 ...
86
87
Traceback (most recent call last):
  File "c:/01proj/v385_USB_I2C/python/blinka_test.py", line 19, in 
    oled.show()
  File "C:\Users\nari\AppData\Local\Programs\Python\Python36-32\lib\site-packages\adafruit_ssd1306.py", line 185, in show
    self.write_framebuf()
  File "C:\Users\nari\AppData\Local\Programs\Python\Python36-32\lib\site-packages\adafruit_ssd1306.py", line 232, in write_framebuf
    self.i2c_device.write(self.buffer)
  File "C:\Users\nari\AppData\Local\Programs\Python\Python36-32\lib\site-packages\adafruit_bus_device\i2c_device.py", line 104, in write
    self.i2c.writeto(self.device_address, buf, start=start, end=end, stop=stop)
  File "C:\Users\nari\AppData\Local\Programs\Python\Python36-32\lib\site-packages\busio.py", line 94, in writeto
    address, memoryview(buffer)[start:end], stop=stop
  File "C:\Users\nari\AppData\Local\Programs\Python\Python36-32\lib\site-packages\adafruit_blinka\microcontroller\mcp2221\i2c.py", line 19, in writeto
    self._mcp2221.i2c_writeto(address, buffer, start=start, end=end)
  File "C:\Users\nari\AppData\Local\Programs\Python\Python36-32\lib\site-packages\adafruit_blinka\microcontroller\mcp2221\mcp2221.py", line 298, in i2c_writeto
    self._i2c_write(0x90, address, buffer, start, end)
  File "C:\Users\nari\AppData\Local\Programs\Python\Python36-32\lib\site-packages\adafruit_blinka\microcontroller\mcp2221\mcp2221.py", line 202, in _i2c_write
    raise RuntimeError("Unrecoverable I2C state failure")
RuntimeError: Unrecoverable I2C state failure
$

その他の試み

MicroChip社提供の MCP2111A用DLLを Pythonから使ってみようとするが、 うまくいかず、 Linux用DriverをRaspberry Piで試すが OLEDへの表示はできない。

現象としては、MCP2221Aへ書き込み(USBパケットの送出)を 繰り返していると、コマンドが受け入れられませんでした という状態になり、リトライを繰り返すうち、 復旧不可能な状態になってしまう感じ。

結論

MCP2221のI2Cは、短いパケットであれば 使えているようであるが、 (現状のソフトウェアでは?) 長いデータの書き込み などを安定して行えないようである。

自分の用途では、短いデータのやりとりで 充分な気もするが、動作のわけのわからない 感じが気持ち悪いし USB-I2Cの転送速度の遅さも気に入らないので MCP2221Aの使用は諦めることにする。

次は FT232Hを試そうかと思う。 これだと I2Cに加えてSPIも使用可能。 Adafruitの奴はカッコいいのだが、 カッコ良すぎて実験用にピンヘッダをハンダ付け するのもためらわれてしまう。 秋月の奴だと気軽に実験できそう。



LED版RssDispも更新

昨日更新した OLED版のRSS表示プログラムの調子は良いが、 LED版の方は調子が悪い。 調子が悪くて、しばらく消していたのだが、 RSS表示はともかく、時計が見易く慣れてしまっていたので LED表示も復活させたい。 というわけでLED版も更新することにした。 ソースを調べるとLED版はOLED版のソースを流用したものだったので、 変更箇所が同じで、簡単に更新できた。

これでhttpsのRSSが読み込めるようになったので、 読み込み先も更新するのだが、 なんかYahoo!ニュース - RSSにあるものから適当に選べば十分ではないか、 という気がしてきた。そうすれば、このサイトのfingerprintだけ あれば十分なので更新も年1回すれば済む。

ということで、 このページから、ネットやTVであまり見かけなさそうな ニュースソースを意識しながら、海外ニュース(BBC,CNN)とか、 ローカルニュース(熊本日日新聞)とか、お笑いナタリーとかを選ぶ。

しかし このページは、何のために存在しているのだろうか? RSSでニュースを読む人のために存在しているの? よくわからない。

更新したプログラムを githubにアップロードした。 これは LCDマトリックスコントローラ用の Arduino Library, Humblesoft_LedMatの サンプルプログラムの一つで esp8266(ESP-WROOM-02)上で動作する。 この コントローラ LEDマトリックス・モジュール 5V1Aの電源 があれば動作させることができる。



RssDispを更新

OLEDの寿命で取り上げた RSSを表示するプログラム: RssDispの表示が 調子悪くなってしまった。 原因の1つは、httpでRSSを提供するサイトが減ってしまったことで、 もう一つは Make:の RSSの反応が遅くなってしまったことらしい。 リクエストを出して10秒ぐらい返事が帰ってこない。

OLEDに表示されたRSSを真剣に読んでいるわけではないが、 正常に表示されないと気持ち悪いので プログラムを修正することにした。

改良点は、以下の通り。

  • httpsサイトへの対応
  • サイトごとのRSS最大表示数の設定
  • その他、表示が不自然なところを減らす

esp8266 Arduinoだと、 httpsサイトへのアクセスも可能だが、 サイトの指紋/fingerprintも指定してやらなければならない。 このfingerprintはchrome等でアクセスし証明書を表示させれば わかるのだが、証明書が変わるたび更新しないといけない。 ということで、これまでhttpsへの対応は行っていなかった。

以前は、httpで提供されているRSSも多かったのだが、 殆どのサイトでhttpsでしかRSSが提供されなくなってきた。 Yahoo ニュースのRSS も httpsでしか提供されない。

ということで、とりあえず fingerprintで httpsへの 接続を行うことにした。

サイトごとの最大表示記事数は、 Make:のRSSが50記事あり、 かなり古い記事まで含んでいるので、その対策。

その他の表示の改良は、RSSの取得に時間がかかる場合、 表示が止まっていたのを、それなりの表示が継続するするよう 改造した。

今回の改良で、以前のように 多くのサイトのRSSを表示できるようになり満足している。 次は LEDに表示している奴 にも同様の改良を施したい。 その次に、fingerprintを使わない httpsへの接続の実験か。



2GTプーリー形状のベジェ曲線化

タイミングベルト2GT用のプーリーを3Dプリンタで 出力できるようになったが、 頂点多すぎる問題が気になって 他の作業が進まない。 仕方が無いので、 プーリー形状のベジェ曲線化に取り組むことにする。

点列をベジェ曲線に変換する方法を ネットで検索するが、簡単には見つからない。 TensorFlowを使う方法 最小二乗法を使う方法 (媒介変数tの値が解れば最小2乗法で解けるらしい) などに翻弄されつつ、 stack overflow 経由で 1990年に出版された本 Graphic Gems のプログラム FitCurves.c に辿り着く。 ソースも github で公開されている。 コンパイルすると、ちゃんと動いた。

そのままでは使いづらいので、自分でpython化した。 できあがったのが こちら。 numpyを使ったので、割とコンパクトになったのでは なかろうか。 点列をベジェ曲線の制御点列に変換する。 なかなか便利な気がする。他の用途にも使えそうだ。

出来上がったプログラムは githubに公開している。 今回のプログラムでは、 ベジェ曲線化するため、 なるべく単純な曲線を扱いたかったので、 プーリー形状が歯ごとにおなじ形の繰り返しであり、 その形も左右対称であるので、歯の半分の形状を生成、 それをベジェ曲線化、さらに倍にしたのち歯数分コピー という手順で作成した。 このため曲線の数は歯数の2倍の数の倍数となる。 例えば20歯のプーリーの曲線数は40の倍数になる。

以下にベジェ曲線化した歯型と元の歯型を比較した図を示す。 ベジェ曲線化の程度は、許容誤差(-eオプションで指定)で変化する。 許容誤差を大きくしても曲線の数は40より小さくはならない。 見た感じでは、許容誤差0.05mm, 曲線数80ぐらいあれば充分のような 気がする。 今回のプログラムでは、処理が複雑になるので、オフセットの設定は できなくなっている。

生成したsvgファイルをfusion360に挿入すると、 以前のものより処理が軽い。ほとんど問題にならない感じである。 20歯、40歯、60歯のプーリーを試作し、タイミングベルトとの噛み合わせ を確認したが、問題は無かった。

プログラムの残りの課題は、前述のオフセット処理と高速化である。 そのうち、気が向いたらやってみたい。



タイミング・ベルト用プーリーの製作

前回シンクロベルトを購入したと書いたが、 タイミング・ベルトというのが正しいのだろうか? ミスミの分類ではタイミングペルトで、 シンクロベルトはバンドー化学の商品名か?という気もする。 今後はタイミングペルトと書くことにする。

タイミングペルトがあれば、ギアというか歯付きプーリーも必要になる。 とりあえず手持ちで20歯のものが4個あるが、3Dプリンタで自由に作れると大変嬉しい。

ギアのように、生成プラグインや歯型生成の方法があれば いいのだが見つからない。ベルトの形状はわかっているので、 多角形で近似して、多角形同士の論理演算のライブラリがあれば、 多角形でプーリーの形状が得られるのではないか?と フリーの多角形論理演算ライブラリを探すとあった。

複数見つかった中 Angus jhonson's Clipper library には pythonバインディングがある。 サンプルプログラムを試したら動いたので、 これを使う。

まず、タイミングベルト2GTの形状を作成する。 2GT 図面で検索すると いろいろみつかるが、 ミスミのページの図面がわかりやすかったので、 これを元に製図し、必要な座標、角度等を求め、 形状生成のプログラムを作成した。 次にピッチ円状に沿ってベルトが回転しながら、 ピッチ円からベルト形状を切り取ることで プーリーの歯型を生成するプログラムを 作成した。 SVGファイルが出力される。 プログラムは github で公開している。

生成したSVGファイルをFusion360で読み込み、 赤い棒が10mmであることを確認。 直径5mmの穴を追加し、押出でプーリーを試作。 歯型を生成する際、オフセットを指定し、 指定分だけ内側に縮めて作成できる。 オフセットなし、0.1mm, 0.2mm, 0.3mmの4種類で 20歯のプーリーを印刷してみた。 ベルトと噛み合せた結果 オフセットなしが最も良好。 0.1mmも噛み合うが、0.2mm, 0.3mmは隙間が大きくて 噛み合わないことがわかった。

次に、どれくらい小さいプーリーも使えるか 調べるため、オフセット無しで 16歯、14歯、12歯、10歯 のプーリーを印刷。 実際に使えるかどうかは、わからないが 10歯までベルトと噛み合わせることができた。

ここまでの試作品は、形状や噛み合わせをみるために ツバが無いものだが、実用上はベルトが外れないよう ツバが必要だ。ツバ有りのものも試作した。 サポート有りで1体で印刷できた。サポート材が 歯面を乱すこともない。

次はベルトとプーリーを使って動くものを作りたい。 あと、現状のプーリー形状はなかなか重い。 20歯で1000頂点を超える。Fusion360での押出処理も重い。 歯数が多くなると、もっと重くなるので、 頂点を減らす処理も検討したい。



srcPrint

テキストファイルをpdfに変換するプログラムを 作成した。名前はsrcPrint。プログラムのソースファイルを 印刷する時に使うやつ。 昔あった a2psみたいなやつ。 GitHubで公開中

プログラムをしていると、時々欲しくなる。 a2psやa2pdfなどや フリーソフトを探してみたりして、 なんとかしてきたが、 今回 electron と typescript の練習を兼ねて、 作ってみた。

electronを使っているので、テキストファイルは htmlに変換され、 cssでフォーマットを指定、 electronの機能でpdfに変換される。

現状、最低限の機能のみだが、 自分用としては、十分使える。 気が向いたら、機能拡張したい。



Doxygen再び

今、マイコンで動くテキスト・エディタを作っている。 まだ、Linux上で開発している段階だが、 ある程度できたので、 ここらでプログラムの整理をすることにする。

関数をグループごとに整理し、必要ならば名前も付け替えたい。 ここで Doxygenを使ってみたら便利だった。

まず、ファイルごとに関数の一覧を出してくれる。 これだけで便利だ。


次に、Doxygenの生成するhtmlを見ていると、 これを、もっと良いものにしてやろうという気持ちが起こる。 しかし、無駄に説明を追加しがちにもなるので、注意も必要だ。

あと、 graphvizも使うと、関数の呼び出し関係図を表示してくれて 興味深い。


マイコン用のテキスト・エディタは RAMの使用量が数Kbyte程度で、 sdカード上のファイルを編集するもの。 画面は ANSIエスケープ・コードで制御する。

現在プログラムは、1800行程度で、大体の機能は実装できた。 キーバインドはemacs風。C-w (delete-region)とC-y(yank)も 実装した。 undoは未実装。編集可能なファイルのサイズに 制限は無い(はず、 2Gまで?、処理速度は知らない)

使用目的は、マイコン・システムの開発支援。 sdメモリカードは便利で、開発するマイコンシステムで 頻繁に使用するが、メモリーカードの内容を書き換えるのに PCに差し替えるのが面倒。 開発中にデバッグ用のコンソールから書き換えられたら便利だろう、 というのが開発の動機。

マイコン(STM32F2XX)への移植は、これからだが、 うまくいくと良いなぁ。



Raspberry Pi用OLED ライブラリを作った

最終的に作ったライブラリは こちら

ラズパイ・ファン基板で OLEDを取り付け易くしたのだけれど、 i2c接続なので表示速度はどうなのだろうというのが気になっていた。

というのは、i2cは転送速度が速くない。 ラズパイのデフォルトでは100kbps。 OLEDは 128x64 dot なので、128x64 = 8kbit。 これを100kbpsで転送すると、転送時間は 8kbit ÷ 100kbps = 0.08sec 連続表示させると 毎秒 1 ÷ 0.08 = 12.5 フレーム表示できることになる。 スムーズな動画表示には少し遅い。転送以外の処理の時間も必要。

実際のところはどうなのだろうと測ってみることにした。

テストプログラム

使用したライブラリは Adafruit_Python_SSD1306。 グラフィック・ライブラリ Pillowのimageのデータを、そのまま表示できる 使い易いライブラリだ。 以下のプログラムを動かしてみた。

import Adafruit_SSD1306,time
from PIL import Image,ImageDraw,ImageFont
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None,i2c_address=0x3C)
disp.begin()
image = Image.new('1',(disp.width,disp.height))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype(
    font='/usr/share/fonts/truetype/freefont/FreeMono.ttf',
    size=50)
t0 = time.time()
draw.text((0, 0), 'Test', font=font, fill=1)
t1 = time.time()
disp.image(image)
t2 = time.time()
disp.display()
t3 = time.time()
print('draw text : %f' % (t1-t0))
print("disp image: %f" % (t2-t1))
print("display   : %f" % (t3-t2))

実行結果は次のようになった。

pi@raspberrypi$ python speed_test.py
draw text : 0.001905
disp image: 0.033468
display   : 0.116709
pi@raspberrypi$

データ転送に 0.116秒かかっているが、計算の 0.08秒とは さほど違わない。次に /boot/config.txtに以下の行を追加後 rebootし、 i2cを400kbpsにして試してみる。

dtparam=i2c_baudrate=400000

実行結果

pi@raspberrypi$ python speed_test.py
draw text : 0.002004
disp image: 0.033668
display   : 0.036361
pi@raspberrypi$

データ転送は期待通り約4倍高速化されたが、 disp imageの 0.033秒が大きい。 これはPillowのImageのデータをOLEDのバッファの フォーマットに変換する処理で、ソースを見ると Pythonで1bitづつ処理している。 この処理に時間が掛るとデータ転送を 早くしても、表示はあまり速くならない。

ライブラリ作成

ということで、 ラズパイ i2c接続OLED用に Pythonのライブラリを作ってしまった。 githubで公開している。

i2cの操作は直接 /dev/i2c-1とかをオープンして行っている。 ここの資料を見ながらプログラムしたら、 特に問題もなく出来た。

テストプログラムで確認

新しいテストプログラムで処理時間を調る。 テストプログラムを新しいライブラリ用に若干修正した。

import time
from PIL import Image,ImageDraw,ImageFont
from RaspiOled import oled
oled.begin()
image = Image.new('1',oled.size)
draw = ImageDraw.Draw(image)
font = ImageFont.truetype(
    font='/usr/share/fonts/truetype/freefont/FreeMono.ttf',
    size=50)
t0 = time.time()
draw.text((0, 0), 'Test', font=font, fill=1)
t1 = time.time()
oled.image(image)
t2 = time.time()
oled.vsync()
t3 = time.time()
print('draw text : %f' % (t1-t0))
print("disp image: %f" % (t2-t1))
print("sync      : %f" % (t3-t2))

実行結果は次のようになった。 ちなみにi2sの速度は400kbps

pi@raspberrypi$ python3 speed_test2.py
draw text : 0.001897
disp image: 0.000538
sync      : 0.023852
pi@raspberrypi$

OLEDバッファへの書き込み(disp image)は0.0005秒になり、 データ転送も 0.023秒と高速化された。

で、やっぱり Bad Apple

ここまでやると、当然やるのはBad Apple。 Bad Appleの動画をffmpegでフレームごとに bmpのファイルに変換し、 画像表示プログラム( image.py)で 再生している。今回は音もwavファイルに変換したものを ラズパイのaplayコマンドで同時に再生している。 ちなみに 画像表示プログラムとaplayを同時に走らせると、 画像が遅れるので、sleepコマンドで1.6秒後にaplayを起動している。

RssDisp

動画 BadAppleを再生できるのは良いのだが、まだなんか不満。 Oledでグリグリヌメヌメ表示できることを示したい。 HSES-NODE-OLEDで動かした RssDispを動かしたい。

で、実装したのだが、予想外に手間がかかった。 文字のスクロール表示は、当初Pillow側でやろうかと思って いたのだが、あまり効率よくできそうな気がせず、 ライブラリに表示イメージをずらす機能を追加。 更にテキストをスクロール表示させるクラスも ライブラリに追加。 これでやっと RSSの表示プログラム rssDisp.pyが完成した。

HSES-NODE-OLED版とほぼ同じ動作が実現できた。 こちらの方が https サイトにも対応しているので、 表示できるRSSサイトが多い。 ESP8266/Arduinoでも httpsサイトへのアクセスはできるのだが、fingerprintを 入力する必要があり、面倒なので使っていない。

でもまぁ、HSES-NODE-OLEDの方が表示しっぱなしでも 惜しくないので良いかもしれない。 HSES-NODE-OLEDは スイッチサイエンスで発売中



VS Codeでstm32のプログラミング

windows上のemacsでIMEを上手くコントロールできないため、 VisualStudio Code(以下VS Codeと記す)の環境整備を頑張った所、 かなり使えるようになってきた。 これで移行できるかもしれない。

Windows上の上のemacsといえば、 昔はmeadowとか使って不満は無かったのだが、 更新されなくなり、 gnupack の emacs だとIMEパッチが当たっていて 良いのだが、 makeコマンドが無い? 導入方法がわからないため、 導入が楽なcygwinのemasを使用し、 日本語はだましだまし使っていた。

今回、stm32のプログラムを開発するに当たって、 日本語のメモをたくさん書いていきたいと考えたため emacsに我慢できなくなり、vscodeの 環境構築にトライしてみた。 electronのプログラムで VS Codeに慣れてきたというのもある。

makeの実行

まず、vscodeからmakeコマンドの起動。 タスクの構成で、 tasks.jsonファイルを 以下のように設定することで何とかなった。

    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "make",
            "type": "shell",
            "command": "/c/MinGW/msys/1.0/bin/make.exe write ",
            "options": {
                "cwd": "${workspaceFolder}/src"
            },
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": false,
                "panel": "new",
                "showReuseMessage": true
            },
            "problemMatcher": {
                "owner": "gcc",
                "fileLocation": [
                    "relative",
                    "${workspaceRoot}/src"
                ],
                "pattern": {
                    "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*):\\s+(.*)$",
                    "file": 1,
                    "line": 2,
                    "column": 3,
                    "endLine": 2,
                    "endColumn": 3,
                    "severity": 4,
                    "message": 5
                }
            }
        }
    ]
}

problemMatcherも定義しているので、 コンパイル時のエラーも「問題」パネルを 開けば、クリック一発で問題箇所を開くことができる。

emacsのnext-errorコマンドみたいに、 1コマンドで「問題」パネルを開き、最初の問題を選択してくれると 嬉しいのだが、そのようなコマンドを未だ発見できていない。

プログラムの自動フォーマット

keybinds.jsonで ctrl+I を押した時 editor.action.formatが呼び出され インデントの修正等、自動で行われるようにしているのだが、 Cのプログラムを編集しているときctrl+Iを押しても、 Cのフォーマッタが無いというようなエラーが表示されるだけで、 フォーマットされない。

そこで、 拡張機能 Microsoft C/C++ for VS Codeを導入。 無事、フォーマットされるようになった。

includePathの設定

しかし、使っていると includePathの設定がされていない、 というメッセージが出るようになる。 面倒で無視していたのだが、 C/C++拡張機能のお蔭で、高度な機能(intelliSenseとか 定義に移動とか)使えるようなので、 真面目に設定してみた所、 設定ファイルc_cpp_properties.jsonは 次のようになった。

{
    "configurations": [
        {
            "name": "Win32",
            "includePath": [
                "C:\\cygwin64\\usr\\include",
                "C:\\cygwin64\\usr\\include\\w32api",
                "C:\\gnu_tools_arm_embedded\\6-2017-q2-update\\arm-none-eabi\\include",
                "C:\\gnu_tools_arm_embedded\\6-2017-q2-update\\lib\\gcc\\arm-none-eabi\\6.3.1\\include",
                "${workspaceFolder}\\src"
            ],
            "defines": ["__CYGWIN__"],
            "intelliSenseMode": "clang-x64",
            "compilerPath": "/c/gnu_tools_arm_embedded/6-2017-q2-update/bin/arm-none-eabi-gcc.exe",
            "cStandard": "c11",
            "cppStandard": "c++17"
        }
    ],
    "version": 4
}

これで、だいたい、使えるようになったのだが、まだ問題がある。 なんか includePathに定義していないPath C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\includeが含まれているようなのである。 stdint.hで「定義へ移動」とすると、このフォルダーのstdint.hが 表示されてしまう。これはどうしたら良いのだろうか。



Electronの本 読了

Electronの本を読了した。

掲載されているコードを律儀に入力し、 動作させながら読んでいったので、 時間はかかったが、 ある程度、知識が身についた気がする。

コードを本の通りに入力しても、 1文字でも間違えれば、動作しない。 コンソールを開ければ、例外のログとかが見れて、 大体理由がわかるのだが、 devToolを開けるのも 大変な場面が多々あり、なかなか大変だった。

あと main process側のメッセージは devToolのコンソールには 表示されないのかな? この辺の話は、まだ十分理解できていない。 こういうデバッグの話も詳しく説明してくれると、 もっといい本になるのではないかと思う。

今後は、独自プログラムの開発を行いつつ、 いろんなライブラリを試し、 Electronの技術を身に着けて行きたい。



Electronの本

Electronを覚えたいと、 正月にも書いているのだが、 一向に使えるようにならない。

ネットでElectron入門的な記事を探し、 試しているのだが、周辺の知識が 足りなすぎて、勉強が進まない。

本でも買うかとAmazonで検索すると、 Electronとタイトルにある本は2冊しかない。

1冊は あなたにとってElectronを学ぶ価値があるかどうか、この本を読んで判断してみてくださいで、kindle本。 41ページで99円。

とりあえず、これを読んでみて 2冊めのElectronではじめるアプリ開発 ~JavaScript/HTML/CSSでデスクトップアプリを作ろうのレビューを見ると、 入門書ではなさそう。なんか良さそうに思えたので中古を購入。

で、届いて読んでいるのだが、結構良さそうだ。 この本に従って、アプリ開発を試してみよう。



電光掲示板プログラムを公開

HSES-LMC1の秋月電子での発売に合わせて、 電光掲示板プログラムを GitHubで公開した。

これは、HSES-LMC1を一般(?)の電光掲示板として動作 させるためのプログラム。

表示内容をSDカードに記憶し、読み出しながら表示する。 Web経由で表示内容の編集ができる。 (使用可能なブラウザは chrome のみ)

インストール方法・使い方は GitHubのWikiに書いたので 一度、見てみて欲しい。

表示できるメッセージは、

  • スタティック・テキスト: 文字列を内蔵Fontで描画、動きなし
  • スクロール・テキスト:文字列をスクロール表示
  • 画像(静止、スクロールとも)
  • 動画(未リリース)

などがあるが、使い方を説明しているのは、上2つまで。 画像はリリースしているが、説明が未だ。 動画は、完成しているが、変換プログラム(FFmpeg)の 説明を検討中で未リリース。

あと、メッセージをzipファイルでアップロード/ダウンロード する機能もある。

今はブラウザ・Javscriptの各種ライブラリが凄いので、 Webインターフェースをつけると、高機能なUIを 簡単に作れてしまう。



esp8266/Arduinoを 2.3.0に戻す

以前いじっていた esp8266/Arduinoのプログラムを make ota で書き込もうとするがエラーになる。 ArduinoOTAのexampleの BasicOTA で試すと書き込めるので、サイズの問題か? OTAが使えないと、とてもプログラムを開発する気にならない。 困った。

2.3.0のころは書けていたので、2.4.1から戻してみることにする。 ボードマネージャで戻す方法はよくわからなかったので、 git で esp8266/Arduinoをインストールし直す。 ちなみにgit clone では -b オプションで バージョン(tag)を 指定できる。

Arduino-IDEからは、すぐに使用できるようになったが makefileからだとエラーになる。 いろいろ調べた結果、git でインストールした際、 テキストファイルの改行コードが変換されていたことが 原因だと判明。 boards.txt platform.txtの改行が CRLFになってしまうと、 makefileでの処理中にCRコードが残り、 エラーになってしまうようだ。

これで makefileから OTAで書き込めるようになった。 2.4.1でうまくいかない理由は調べていない。 ArduinoOTAについても、いつか、調べないとなぁ。



Raspberry Piで動画をLEDに音付きで再生

hzeller/rpi-rgb-led-matrix付属の 動画再生プログラム video-viewerを改造し、 動画ファイルを音つきで再生できるようになった。 前にも書いたが フレームの遅れが蓄積されない ように改造した。 変更点は、pull-requestを投げたいが、 まだ、ちょっと汚いのが悩みどころ。

音の再生は別プログラム(aplay)で 行っている。これも将来的には組込みたい。

いつもの MMD Bad Apple!! Now in 3D with more Color を再生したのが下の動画。

プログラムの改造は簡単だったが、 動作の確認のためRaspberry-Piから 音を出すのに手間取った。

まず hzeller/rpi-rgb-led-matrixを動かすには /boot/config.txtで dtparam=audio=off しなければいけないので、 Raspberry Piのaudio出力は使用できなし、 HDMIへの音声出力も使えなくなる。

USB-speakerやbluetoothへの出力は可能なので、 まず手持ちのbluetoothスピーカで試すと、 音は出たが、厳しい条件の動画(60fpsのやつとか)を 再生させると音が出なくなり、復旧できなくて あきらめた。

USBスピーカを購入し試すと、音は出るが 最大音量で、音量調節が効かない。スピーカ側に ボリュームやミュートのスイッチはあるのだが、 どうもソフトウェアでコントロールするタイプのようで、 Windowsでは機能したがRaspberry Piでは動かない。 amixerコマンドで音量調節を試みるが、 あまり音量が変化しなかったり、無音になったりと 極端でうまく音量調節できない。 無音になる寸前の数値で、ある程度音量調整できたので、 そこで動画を撮影した。 アナログのボリュームつまみがついたUSBスピーカが欲しい。



Raspberry Pi Zero WH 動作テスト

miniHDMI変換アダプタとUSB-microB OTGケーブル が届いたので、 Raspberry Pi Zero WHを動作させてみる。

Zeroは基板は極小だが、ケーブルを刺すと そうでもなくなる。 ケーブルを ケーブル・オーガナイザで固定してやると良い感じである。

Raspbianをダウンロード、 microSDカードに書込み 立ち上げたら、特に問題もなく起動。 キーボード、WiFiの設定を行い、 普通に使えるようになった。

その後、 hzeller/rpi-rgb-led-matrixをインストールし、 LEDマトリックスモジュールへの表示を試す。

表示はできるが、負荷が重い感じ。 動画の再生を行うと、はっきりと遅い。 やはり コア4個のRasberry Pi 3と比べると、 コア1個のZeroだと、余裕が無い感じだ。 表示も、多少、ちらつきが感じられる。



ESP32_bt_speaker 更新

ESP32をBluetoothスピーカーにするプログラム ESP32_bt_speakerを更新した。

作成後、esp-idfが、かなり更新されたので、 いろいろと問題があるだろうなとは思っていたのだが、 手を付けずにいたのだが、 githubにissueついたので、 対応することにした。

最新の esp-idf環境でESP32_bt_speakerをコンパイルしてみるとエラーが沢山発生。 ひとつづつ対応するのも大変なんで、元となったサンプルプログラム bluetooth/a2dp_sinkから再び作り直すつもりで、a2dp_sinkを コンパイルしてみると、I2Sと内蔵DACへの出力機能が 追加されていることが判明した。

これが動くのであれば私のESP32_bt_speakerは、お役御免。 今後、私が面倒を見なくても良くなる、と思って プログラムをESP32開発ボードに書き込むも 音が出る様子は無い。

わたしのプログラムと見比べながら、修正と試行を繰り返し、 音が出るようになった。I2S出力側は、実験できないので わからないが、内蔵DAC出力側は、テストされていないような 感じがする。

修正は pull requestを出して esp-idf側に反映してもらいたい ところだが、やり方がよくわからないので、 とりあえず ESP32_bt_speaker側を更新しておいた。

変更点を以下に示すので、これを見て a2dp_sinkの ソースを変更してもらっても良いと思う。

diff --git a/examples/bluetooth/a2dp_sink/main/bt_app_av.c b/examples/bluetooth/a2dp_sink/main/bt_app_av.c
index 289c1f16..4958e3d9 100644
--- a/examples/bluetooth/a2dp_sink/main/bt_app_av.c
+++ b/examples/bluetooth/a2dp_sink/main/bt_app_av.c
@@ -53,7 +53,27 @@ void bt_app_a2d_cb(esp_a2d_cb_event_t event, esp_a2d_cb_param_t *param)
 void bt_app_a2d_data_cb(const uint8_t *data, uint32_t len)
 {
-    i2s_write_bytes(0, (const char *)data, len, portMAX_DELAY);
+    TickType_t delay = 50 / portTICK_PERIOD_MS;
+    if(len % 8){
+      ESP_LOGE(BT_AV_TAG,"unexpected data len:%u",len);
+      return;
+    }
+
+    for(int i=0; i<len; i+= 4){
+      uint16_t d[2];
+
+      d[0] = data[i+0]|(data[i+1] << 8);
+      d[0] ^= (1 << 11);
+      d[0] <<= 4;
+      d[1] = data[i+2]|(data[i+3] << 8);
+      d[1] ^= (1 << 11);
+      d[1] <<= 4;
+
+      int n = i2s_push_sample(0, (const char *)d, delay);
+      if(n < 0)
+	ESP_LOGE(BT_AV_TAG, "i2s_write_bytes error:%d",n);
+    }
+
     if (++m_pkt_cnt % 100 == 0) {
         ESP_LOGI(BT_AV_TAG, "Audio packet count %u", m_pkt_cnt);
     }
diff --git a/examples/bluetooth/a2dp_sink/main/main.c b/examples/bluetooth/a2dp_sink/main/main.c
index a6093386..30ee28a2 100644
--- a/examples/bluetooth/a2dp_sink/main/main.c
+++ b/examples/bluetooth/a2dp_sink/main/main.c
@@ -54,14 +54,14 @@ void app_main()
     i2s_config_t i2s_config = {
 #ifdef CONFIG_A2DP_SINK_OUTPUT_INTERNAL_DAC
-        .mode = I2S_MODE_DAC_BUILT_IN,
+        .mode = I2S_MODE_DAC_BUILT_IN | I2S_MODE_MASTER | I2S_MODE_TX,
 #else
         .mode = I2S_MODE_MASTER | I2S_MODE_TX,                                  // Only TX
 #endif
         .sample_rate = 44100,
         .bits_per_sample = 16,
         .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,                           //2-channels
-        .communication_format = I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB,
+        .communication_format = I2S_COMM_FORMAT_I2S_MSB,
         .dma_buf_count = 6,
         .dma_buf_len = 60,                                                      //
         .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1                                //Interrupt level 1

このプログラムには、まだ問題が残されている。 1つは音量を上げた時、音割れする問題で、 もう一つは 通信を中断させた場合DMAバッファに 音データが残り、鳴り続けてしまう問題である。

音割れの方は、DACが8bit, Bluetooth上のデータも12bitなので、 ボリュームを上げていけば音割れするのは仕方ないような 気もするが、 iPhone単体や、他のbluetoothスピーカーで試すと ボリュームを上げても音割れしない。

データ操作のバグも疑い、いろいろ試すがわからない。 仕様なのではないかという気もするのだが、 どうなのだろう。

切断後、音が鳴る問題は、音の出力先を切り替えるなどすると、 発生させることができる。通信の切断の検出はできるので、 ここでDMAバッファをクリアする処理を書いてみたのだが うまくいかなかった。やりかたがまずかったのかもしれない。

Bluetoothについての知識が乏しいので、 ちゃんと対策しようとすると、 なかなか苦しい。