2026年9月1日火曜日

EXCELの表をKaTeX形式に変換するマクロ

生成AI時代にプログラミング言語を学習する必要はあるのか?

最近では生成AIがプログラムを作成する時代になってきましたが、そんな時代にプログラミング言語を学習する必要はあるのでしょうか?

ということで早速 Google さんに聞いてみました。

生成AI時代にプログラミング言語を学習する必要はあるのか?

 答えは

生成AIがコードを自動生成する時代においても、プログラミング言語の学習は不要になるどころか、AIの出力を正しく理解して使いこなすために重要です。
  • コードを「読む力(読解力)」が不可欠になる
  • 適切な指示(プロンプト)を出すため
  • エラーやトラブルへの対応

 ということで学習の仕方も変わるようです。

  • 「書く」から「理解する・指示する」へ:
    文法を細かく暗記してゼロからコードを打ち込む作業はAIが担うため、全体像を組み立てるプログラミング的思考や構造の理解が重視されるようになっています。
  • 作りながら学ぶ:
    あらかじめ言語を完璧にマスターするのではなく、まずは作りたいものをAIと一緒に形にし、その過程で必要な知識を補うアプローチが効果的です。

とは言え巷にあふれるプログラミング言語の学習書ではやはり文法が優先しているように思います。確かに本としてまとめるには一番書きやすい形式ではないかと思います。

実際にPythonを教えている身としては、教えやすいので文法から入るという流れで教えているのが現状です。ただ、最近はデータ形式を中心に教えるようにして、文法はその流れの中で教えるという方向にしています。Pythonは静的型付け言語ではありませんが、プログラムを作成する上でやはりデータ型を考慮する場面が出てきます。

例えば、inputで入力したデータは数値変換しないと計算には使えない。

小学校でも2020年度から「プログラミング的思考」を教えるようになり、その後、順次中学校、高校へと進み2025年1月の大学共通テストからは科目「情報」が追加され、国公立大学を受験する際は、原則として「情報Ⅰ」の受験が必要となりました。
内容的にはプログラミングを全面に出すというよりも、情報に関する覚えておくべき知識が網羅されているように感じます。

そこで、最近始めたのですがnoteでPython初心者向けの記事を投稿し始めました。
システム開発のV字モデルをベースに、アルゴロジックというゲームを攻略するアプリの開発を行うというストーリーで話を進めています。
まずは、要求仕様から考えて、必要なプログラムの詳細仕様書を作成する流れで、その中でPythonの知識を学習していくという流れで進めています。

note(ノート)には表の挿入機能がない

noteはシンプルなデザインで簡単に文章や画像、音声などを誰でも簡単につくって公開できるメディアプラットフォームです。HTMLも一部使用できるものもありますがtableタグについては使用できないようです。

いろいろと調べるとKaTeX(カテフ)を使って数式や擬似的な表などを表示できるようになっています。ただ、KaTeXの表記方法は面倒なのでEXCELで表を作成してそれをKaTeX形式に変換してクリップボードへコピーするVBAを作成しました。

noteがシンプルなデザインを基本としているので表も単純でシンプルな形式のみを対象として、セルの連結や複雑な罫線なども対象外としています。
EXCELの表をKaTeX形式に変換してくれるサイト(Tables Generatorなど)もあるようですが、note専用の生成ツールとして生成AIに作ってもらいました。


'=== 文字列をクリップボードへコピー ===
Private Sub CopyToClipboard(ByVal s As String)
' DataObjectの作成(遅延バインディング)
    Set myData = CreateObject("New:{1C3B4210-F441-11CE-B9EA-00AA006B1A69}")
    
    ' クリップボードにテキストを設定
    myData.SetText s
    myData.PutInClipboard
    
    MsgBox "LaTeX形式の表をクリップボードにコピーしました。", vbInformation
End Sub

'=== Excel範囲 → KaTeX表変換 ===
Sub RangeToKaTeXtop()
    Dim rng As Range
    Dim r As Range, c As Range
    Dim latex As String
    Dim cols As Long
    Dim rowText As String
    
    Set rng = Selection
    cols = rng.Columns.Count
    
    latex = "$$" & vbCrLf
    latex = latex & "\newcommand{\arraystretch}{1.3} %" & vbCrLf
    latex = latex & "\begin{array}{" & WorksheetFunction.Rept("|l", cols) & "|}" & " \\\\[-1.4em]\hline" & vbCrLf
    
    For Each r In rng.Rows
        rowText = ""
        For Each c In r.Cells
        If c.HorizontalAlignment = xlCenter Then
            rowText = rowText & "\hspace{2px} "
        End If
        rowText = rowText & "\text{" & c.Value & "} & "
        Next c
        rowText = Left(rowText, Len(rowText) - 3)
        latex = latex & rowText & " \\\\" & " \hline"
        If r.Row = 1 Then
            latex = latex & " \\[-1.4em]\hline"
        End If
        latex = latex & vbCrLf
    Next r
    
    latex = latex & "\end{array}" & vbCrLf
    latex = latex & "$$"
    
    CopyToClipboard latex
    
End Sub

2026年3月1日日曜日

Google Blogerにコピーボタン(execCommand不使用)を追加しました。

 日本ではBlogerが不人気なようです。

完全無料、広告表示なし、https標準対応、25年以上継続的に運用されてきたサービスなんですが、日本では使っている人が少ないようです。
とりあえず始めるのは簡単で、カスタマイズしようと思えば知識は必要ですが可能という強力なサービスだと思うのですが利用者は増えていないようです。

カスタマイズも最近でしたら生成AIがあるのでHTMLやCSS、Javascriptもあまり知識がない私のような者でもそこそ出来るようになってきました。

という事で、今回コードブロックの右側にコピーボタンを配置し、クリックするとコードをコピーできる機能を実装してみました。ただし、execCommand については非推奨になりましたので代替案であるHTML5のClipboard API を使用した形で実装したいと思います。
まずはGoogleさんで検索をして幾つかのサイトを参考に実装してみました。
私のサイトではソースの表示にはprettyprintを使っているのですが、linenumsを指定するとうまくコピーできない(1行目だけコピーされる)ようです。

原因は、linenumsを指定するとol、liダグを使って行番号を表示させているのですがこれが原因のようです。

という事でCopilotに指導を仰ぎながら修正したのが以下のコードとなります。

CSS

Blogerのテーマ > マイテーマの「HTML編集」で「]]></b:skin>」の前に以下のCSSを追加して下さい。

pre.prettyprint {
  overflow: auto;
  word-wrap: normal;
  text-align: left;
  margin: 0;
}
pre.prettyprint > ol.linenums > li
{
  list-style-type: decimal;
  margin: 0;
  padding: 0;
}
.code-container {
  text-align: right;
  margin: 0;
}
.copy-button{
  font-size: 12px;
  padding: 2px 10px;
  color: #fff;
  background-color: #444;
  border: none;
  cursor: pointer;
  transition: background-color 0.3s ease;
}
.copy-button:hover{
  background: #999
}
pre code{
  display: block;
  margin: 0;
  padding: 2px 5px;
  line-height: 1;
  overflow: auto;
  color: #f8f8f8;
}

Javascript

同じく、「/body」の直前に以下のコードを追加して下さい。

  <script>//<![CDATA[
    // コピーボタン作成用コード
    (function(d){
        const pres = d.querySelectorAll('pre.prettyprint');
        if(pres.length == 0){
          return;
        }
        pres.forEach(function(pre) {
          // コード(pre)とCopyボタンを収容するdiv作成(Copyボタンにprettyprintの影響が及ばないようにする為)
          const div = d.createElement("div");
          div.className = "code-container";
          // Copyボタンを生成してdivに追加(子の先頭)
          const btn = d.createElement('button');
          btn.className = 'copy-button';
          btn.textContent = 'copy';
          div.appendChild(btn);
          btn.addEventListener('click', copy_code, false);
          // コード(pre)の親にdivを追加
          pre.parentNode.insertBefore(div, pre);
          // コード(pre)を現在の親からdivの子の末尾へ移動)
          div.appendChild(pre);
        });
    })(document);

    // コピー機能用コード
    function copy_code(){
      const pre = this.nextElementSibling;
      let code;
      if(pre.classList.contains('linenums')){
        code = [ ... pre.querySelectorAll('li')]
          .map(li => li.textContent)
          .join('\n');	  
      } else{
        code = pre.textContent;
      }
      if(navigator.clipboard){
        navigator.clipboard.writeText(code).then(() => {
          this.textContent = 'Copied';
          setTimeout(() => {
            this.textContent = 'Copy';
          }, 500);
        }).catch(e => {
          alert('コピーできませんでした\nお手数ですが手動でコピーしてください\n\n' + e.message);
        });
      } else{
        alert('このブラウザは Clipboard API 非対応です\nお手数ですが手動でコピーしてください');
      }
    }
  //]]></script>

あとはprettyprintを使ってコードを埋め込めば、自動的にCopyボタンが追加されます。

今回参考にさせていただいたサイト
コードブロックにコピーボタンを実装する(Clipboard API 対応版)
Bloggerでコードをきれいに表示する方法-copy機能の追加

2026年2月26日木曜日

Raspberry Pi 500+ 日本語配列版はほぼパソコン

Raspberry Pi 500+ 日本語配列版をポチってしまいました。 

Raspberry Pi 500+は2025年9月25日に発表されましたが、そのキーボードを日本語版にして16GBメモリに256GB SSD持つキーボード一体型のRaspberry Pi 5 になります。
その詳細はこちらに詳しく紹介されているので見て下さい。

自宅に来た、Raspberry Pi 500+を早速試してみました。
数値的なものよりも体感的にどう感じるかで評価したいと思いますのでブラウザを使って試したいと思います。標準でChromiumとFirefoxが入っていて初回起動時に選択ができるようになっていました。
Raspberry Pi 4はmicroSD、Raspberry Pi 5はSSDからの起動となります。

2回目以降起動時ChromiumFirefox
Raspberry Pi 4/8GB4.238.89
Raspberry Pi 500+/16GB1.484.29

追記:2026/02/27
初回起動時の測定も行なってみました。
SSDの威力が発揮されている結果となりました。

初回起動時ChromiumFirefox
Raspberry Pi 4/8GB12.7118.25
Raspberry Pi 500+/16GB2.766.5

初回起動時はライブラリの読み込みなどで時間がかかるので、数回起動してから測定を行なっています。電源投入後の初回は2,3秒microSDでは8秒〜10秒、SSDでは1、2秒時間が多くかかるようです。
注目すべきはRaspberry Pi 500+でのChromiumでほぼ瞬間で起動される印象です。

そのお値段ですが、本体+公式マウス+公式ACアダプターで約53,000円(秋月電子通商)でした。これに公式ディスプレーを付けると約73,000円ですが、すでにディスプレーを持っていたり、リビングのTVを使うなら5万円ちょっとです。

しばらく使ってみてちょっと不満なのは、電源ボタンがDeleteキーのすぐ横にあるので、Deleteキーを多用する身としては誤って電源ボタンを押して、ログアウトダイアログボックスが表示されるのが面倒なのと、キー音がちょと耳障りな事でしょうか。

Chromiumを快適に使うちょっとした作業

Raspberry PiといえばChromiumですが、実はRaspberry Pi OSがBookwormになってからChromiumで日本語入力ができないという現象が出ていました。そのあたりの詳細はこちらに詳しいのですが、ここではとりあえずどうすれば解消できるかだけ纏めておきたいと思います。

STEP1: fcitxをfcitx5に変更する。

標準では fcitx-mozc がインストールされていますが、これを fcitx5-mozc にインストールし直すと共に、日本語フォントもインストールしておきます。
Terminalを開いて以下のコマンドを順次実行して下さい。

sudo apt update
sudo apt install fcitx5-mozc fcitx5-config-qt -y
sudo apt install fonts-noto-cjk fonts-ipafont -y

STEP2: Wayland用にChromiumを設定する。

これでChromiumで日本語入力は可能ですが、変換ポップアップがおかしな場所に表示される問題があるのでこの対策を行います。

具体的には /etc/chromium.d/10-gtk-version ファイルを作成して以下の内容を追加します。

<実行コマンド>

sudo nano /etc/chromium.d/10-gtk-version

<追加内容>

if [ ! "$XDG_SESSION_TYPE" = "x11" ]; then
    export CHROMIUM_FLAGS="$CHROMIUM_FLAGS --gtk-version=4"
fi

編集を保存して終了するには、Ctrl+o、Enter、Ctrl+x の順にキー入力します。

STEP3: Fcitx5 をデフォルトに設定

以下のコマンドを実行して、再起動すれば Chromium で日本語入力が問題なく出来る様になります。

im-config -n fcitx5


2026年2月21日土曜日

Windows11でALT+Shift+PRT SCでウインドウのスクショが取れなかった件

久々の投稿です

2年程ご無沙汰しておりました。
元々、筆不精者が仕事が忙しくて物書きが全くできませんでした。ひと段落着いたのでまた書きたいと思います。
今回は投稿の為にWindows11で画面のスクショを撮ろうとしたら、ハイコントラストのショートカットとバッティングしていてスクショが取れなかったというお話です。

Windows11でALT+PRT SCはアクティブウインドウのみをスクショする為のショートカット何ですが、私のノートパソコンでは当該キーがSCR LK(スクロールロック)と共同となっていてALT+PRT SCの為にはALT+Shift+PRT SCとする必要があることからハイコントラストのショートカットと重複してしまったという事です。

解決策

ハイコントラストのショートカットを無効にする事で対応しました。

  1. 「設定」アプリを開きます(Windowsキー + I)。
  2. 左側のメニューから「アクセシビリティ」をクリックします。
  3. 「コントラスト テーマ」を選択します。
  4. 「コントラスト テーマのキーボード ショートカット」という項目を探し、スイッチを「オフ」にします。


2023年8月11日金曜日

Edisonが故障した一件

 最近またEdisonで遊んでいます。


教育用のデバイスとしてよく出来ているなと思っているEdisonですが、現在3台を所有しています。
最近このうちの1台が調子が悪くなってしましました。具体的にはBeep用のデバイスが壊れたようです。購入時にチェックをしたつもりですが今回久々に音関係のプログラムを作っていたところ音が出なくなっていました。
通常は電源を入れるとBeep音が鳴るのですがこれが出ないのでおかしいなと思って、とりあえずプログラムを作って動かした所やはり音が出ませんでした。

あれ、Blogerのプレビューが動かない!


これはMacBook Airで書いて居るのですが、今までプレビューも問題なく使えていましたが今日は「読み込んでいます」でループして居るようです。
とりあえずiMacに移って続きを書き始めましたが、こちらでは問題無くプレビューできるようです。
私のMacBook AirもEarly 2015と既に8年前の物、最近ではシステムアップデートもできていませんし、バッテリーもお亡くなり寸前の状態です。そろそろ買い替えたいなと考えていますが、今のところ動いているので延び延びになっています。
来年あたりは変えかられるかなぁ〜

閑話休題


早速、サポートに連絡を入れましたがレスポンスも翌日にはあり、その後も丁寧に対応してくれました。非常に好感が持てましたので嬉しくなってアップした次第です。5、6回のやりとりをしましたが、概ね翌日の午前中にはレスポンスがあリました。アーストラリアとは2時間の時差なので、向こうで朝一メールをチェックして対応しているのかなと言う感じでした。

やはり、ブザーデバイスの故障ということになり、保証期間内なので代替品を送ってもらえることになりました。

保証期間は1年間あるのですが、これもしっかりとした物作りができていると言う自信なのでしょうか? Edisonそのものは非常にコンパクトでしっかりとした作りになっています。小さいお子さんも使う前提で小さな部品もほぼ無く(電池カバーのみ)鋭角な部分も少なく安全にも配慮された作りになっていると思います。 LEGOと組み合わせることができるので、レゴを使った場合には小さなお子さんがLEGOブロックを飲み込む危険があるので注意が必要にはなります。
製造は中国で行っているようですが、今まで問題になったのは電池ボックスの蓋のバネが弱くて電池がうまく接触しなかったケースぐらいです。これもバネを少し伸ばしてあげることで問題なく使えるようになりました。

マイナス点としては、電源スイッチが非常に小さいので小さなお子さんでは電源を入れられないかもしれないと言う点。
あとは、電池ボックスの蓋がちょっと癖があるので開け閉めにコツがいることでしょうか。これはホームページのFAQにも対策が書いてありました。

Edisonドキュメントの日本語化を進めています


EdisonはEdBlock(Scratch Jr)、EdScratch(Scratch)、EdPy(Python)と理解に応じた言語が用意されているので年長さんあたりから中学生あたりまで広くプログラミングの学習に使えるなと考えています。
ただ、問題は日本語ドキュメントが無いのがネックになっていると思います。上記3つの言語ツールも全て英語版だけなのでそこが利用する上でのネックになっているのかと思います。
本体は6千円程度で入手できるのでLEGOと比べると購入しやすいと思います。

と言うことで、現在プログラミング学習用ドキュメントの日本語化を進めています。
本家が非常に良質なプログラミング学習用ドキュメントを用意してくれていて、翻訳したいと連絡したら翻訳用キットを送ってもらえました。
現在、鋭意翻訳中ですので終了しましたら公開していきたいと考えています。
1人でやっているので時間がかかりますが、まずは生徒用ドキュメントから翻訳を始めています。

今しばらくお待ちくださいね。

2023年4月7日金曜日

ESP32をW5500で有線LAN接続してmDNSしてみた -2-

前回投稿より1年近くが経ってしまいました。途中、投稿しとこうかと思った事も幾つかあったのですが、ついついコードを書く事が優先して気がつくと何を書こうとしたのかも忘れてしまう有様でした。
そろそろボケが入って来たのかな?

W5500搭載 ATOM PoEキット


現在IoT 関連のプログラムを趣味のように書いていますが、その中でこんな物を見つけたのでWi-Fiが使えない&電源を取るのが難しいという状況でセンサーが使えそうだなということで早速購入して遊んでみました。

W5500については以前ブログにアップしていたので簡単に動くかと思ったのですが・・・

当時のplatformio.iniは
[env:esp32doit-devkit-v1]
platform = espressif32
board = esp32doit-devkit-v1
framework = arduino
lib_deps = 
	adafruit/Ethernet2@^1.0.4
	khoih.prog/MDNS_Generic@^1.0.1
こんな感じだったのですが、既にこのバージョンは古くなって使えなくなっていました。
ならばと最新版でコンパイルするとコンパイルエラーが多発してうまくいきません。
GitHubのサンプルResolvingHostNames.inoを見ると

// To be included only in main(), .ino with setup() to avoid `Multiple Definitions` Linker Error
#include <MDNS_Generic.h>

のようなコメントもありましたが、依存関係を見るとWiFiを参照していたりと本来ネットワーク媒体には非依存で作成すべきなところがだいぶ複雑になっていそうです。
そこでもっと軽いライブラリはないかなと探してみたところ、やはりいました。

ArduinoMDNS


PlatformIOにもありましたので早速使うことにしました。
依存関係も無くすっきりとしたライブラリのようです。

今回のplatformio.iniは
[env:m5stack-atom]
platform = espressif32
board = m5stack-atom
framework = arduino
monitor_speed = 115200
lib_deps = 
	m5stack/M5Atom@^0.1.0
	arduino-libraries/Ethernet@^2.0.2
	arduino-libraries/ArduinoMDNS@^0.0.0
こんな感じです。

早速テストして見ると、パニックを起こしてしまいます。
どうやらアドレスエラーのようです・・・
前回同様またソースを見ることになるとは(( _ _ ))..zzzZZ

_finishedResolvingNameが原因でした


ソースを見るとどうやら_finishedResolvingNameメソッドでアドレス例外を出しているようです。
void MDNS::_finishedResolvingName(char* name, const byte ipAddr[4])
{   
   if (NULL != this->_nameFoundCallback) {
      if (NULL != name) {
         uint8_t* n = this->_findFirstDotFromRight((const uint8_t*)name);
         *(n-1) = '\0';
      }
      this->_nameFoundCallback((const char*)name, IPAddress(ipAddr));
   }

   my_free(this->_resolveNames[0]);
   this->_resolveNames[0] = NULL;
}

このメソッドは名前解決が出来た時に呼び出されてコールバック関数を呼び出すのですが、ソースを検索すると、名前解決できなかった時も呼び出されているます。但しipAddrがNULLで呼び出されていました!(前回調査時と同じ不具合)ということでその部分を以下のように変更して問題は解決しました。
this->_nameFoundCallback((const char*)name, 
                 (ipAddr == NULL ? INADDR_NONE : IPAddress(ipAddr)));

resolveNameサンプル


ヘッダファイル:
    DNSClient dns;
    EthernetUDP udp;
    MDNS mdns = MDNS(udp);
ソースファイル:
/* ****************************************************************************
 *      DNS
 ******************************************************************************/
IPAddress resolved_ip_address;
void nameFound(const char* name, IPAddress ip) {
    resolved_ip_address = ip;
    Serial.println(ip == INADDR_NONE ? " failed !!!" : " successed !");
}

IPAddress NetworkClientW5500Class::resolveName(const char *name) {
    IPAddress ipAddress;
    boolean rc = ipAddress.fromString(name);
    if(!rc) { // IP指定だった場合は名前解決不要
        if(this->useDNS) {
            int ret = this->dns.getHostByName(name, ipAddress);
            if (ret != 1) {
                // mDNS初期化
                if(!this->beginMDNS) {
                    if (!this->mdns.begin(Ethernet.localIP(), this->client_name)) {
                        // mDNS初期化エラー
                        Serial.println("Error setting up MDNS(W5500) responder!!!");
                        while(true) { 
                            delay(1000);
                            Serial.print(".");
                        }
                    }
                    this->beginMDNS = true;
                    this->mdns.setNameResolvedCallback(nameFound);
                    Serial.println("mdns.begin is success !");
                }
                Serial.print("host(");
                Serial.print(name);
                Serial.print(") IP address resolving ");
                int count = 0;
                resolved_ip_address = INADDR_NONE;
                while(true) {
                    if (!this->mdns.isResolvingName()) {
                        this->mdns.resolveName(name, 5000);
                        count++;
                        if(count > DNS_RETRY_COUNT)
                            break;
                        else if(count > 1)
                            Serial.print("retry resolving ");
                    }
                    Serial.print(".");
                    this->mdns.run();
                    if(resolved_ip_address != INADDR_NONE) {
                        ipAddress = resolved_ip_address;
                        count = 0;
                        break;
                    }
                    delay(100);
                }
                if(count > 0) {
                    // 名前解決できず
                    Serial.println(" was ended in failure!!!");
                    while(true) { 
                        delay(1000);
                        Serial.print(".");
                    }
                }
            }
            Serial.print("host(");
            Serial.print(name);
            Serial.print(") IP address : ");
            Serial.println(ipAddress);
        } else {
            Serial.print("host(");
            Serial.print(name);
            Serial.print(") name resolution failed !!!");
            while(true) {     
                delay(1000);
                Serial.print(".");
            }
        }     
    }
    return ipAddress;
}

2022年5月24日火曜日

bullseyeで初期設定後画面が真っ黒になって困った

あれ、画面が真っ黒に 

Raspberry Pi OSとして最近はbullseyeを主に使っています。
今まではIoT用途が多かったのでヘッドレスでのセットアップがばかりでしたが久々にディスプレイ+キーボード+マウスの構成でインストールをしたところ初期設定後の再起動で画面が真っ黒になって困ってしまいました。

どうやら言語設定を日本語にすると画面が真っ黒になってしまうようです。

最新のImagerでは色々と初期設定が可能ですが何もしないで今まで通りのインストールを行うと英語の初期設定画面は問題なく表示されるのですが、ロケールなどを日本語環境用に設定した後のリブート後に画面が真っ黒になってしまいます。

OSは問題なく起動されているのでsshでログインすることは可能です。

インストール環境

Raspberry Pi 4B 8MB
MicroSDカード:GIGASTONE 32MB
Raspberry OS:2022-04-04-raspios-bullseye-arm64.img
ディスプレイ:I-O DATA EX-LD2071TB
キーボード、マウス:自宅にあったもの

boot.txtの修正で問題解決

他のディスプレイに接続すると問題なく画面が表示されるディスプレイもあります。
どうやらディスプレイとの相性のようです。

昔もディスプレイとの相性があってconfig.txtをいじったことを思い出しました。

と言うことで今回はSDカードを取り出してWindowsで/boot/config.txtの以下の部分を修正して問題なく画面が表示されるようになりました。
値についてはそれぞれの環境によって違いますが参考としてはこちらと各自のディスプレイの取説を見て下さい。

ちなみにI-O DATA EX-LD2071TBは1920×1080のプログレッシブモードでHDMI接続なので1080pの60Hzの設定(hdmi_mode=5)を使用しました。

framebuffer_width=1920
framebuffer_height=1080

hdmi_group=1
hdmi_mode=5


2022年5月3日火曜日

bullseyeでGrove Base Hat for Raspberry Piが動かない!

 Raspberry Pi OS bullseyeがリリース

2021年10月30日にbullseyeがリリースされましたが、その後Busterも引き続きサポートされていく事がアナウンスされました。本家のNewは2021年12月2日に出されましたが、2021年12月10日にはZD Net Japanが日本語の記事をアップしています。

これに伴ってGrove Base Hat for Raspberry Piの一部センサー等についてbullseyeでは提供されるライブラリで動かなくなったものが出てきた様です。

いずれライブラリもアップデートされるでしょうが今回仕事でGrove Base Hat for Raspberry Pi(以下Hat)を使う必要があったのであえてLegacyを使わずにbullseyeにチャレンジしてみました。

Online one-click installationは使えません

まずはseeed studioのHatのページを見てみるとbullseye以上の場合は「Online one-click installation」が使えないと記載されています。これはbullseyeからPython2が標準インストールされてなくなったのが原因です。但し「Step by step installation」の手順でPython3でインストールすればbullseyeでもHatの利用は可能です。

ほとんどのセンサー類は問題なく動く様です。

では何が問題なのかと言う事ですが、色々と調べてみるとHatライブラリでupmmraaを使っていると問題になるようです。
bullseyeにはupmとmraaはインストールされていないので手動でインストールしようとしてもパッケージが見つからないのでインストール出来ませんでした。
seeed studioのGithubでもupmとmraaについては既にメンテナンスされていない様なのでGroveモジュールでupmとmraaを使用している物は使えないとなっています。

Node-REDで動かしてみる

取り敢えずはbullseyeの環境でインストールしてNode-REDでこちらの温度センサーを使ってみると以下の様なエラ〜メッセージが表示されます。

Error: Command failed: python -u /home/pi/.node-red/node_modules/node-red-contrib-grove-base-hat/grove-temperature-sensor/grove-temperature-sensor.py 2 1651393678682
Traceback (most recent call last):
  File "/home/pi/.node-red/node_modules/node-red-contrib-grove-base-hat/grove-temperature-sensor/grove-temperature-sensor.py", line 26, in <module>
    from grove.factory import Factory
  File "/usr/local/lib/python3.9/dist-packages/grove/factory/__init__.py", line 2, in <module>
    from .factory import *
  File "/usr/local/lib/python3.9/dist-packages/grove/factory/factory.py", line 38, in <module>
    from grove.temperature import *
  File "/usr/local/lib/python3.9/dist-packages/grove/temperature/__init__.py", line 2, in <module>
    from .mcp9808 import TemperMCP9808
  File "/usr/local/lib/python3.9/dist-packages/grove/temperature/mcp9808.py", line 38, in <module>
    from upm.pyupm_mcp9808 import MCP9808
ModuleNotFoundError: No module named 'upm'
    at ChildProcess.exithandler (child_process.js:383:12)
    at ChildProcess.emit (events.js:400:28)
    at maybeClose (internal/child_process.js:1058:16)
    at Socket.<anonymous> (internal/child_process.js:443:11)
    at Socket.emit (events.js:400:28)
    at Pipe.<anonymous> (net.js:686:12) {
  killed: false,
  code: 1,
  signal: null,
  cmd: 'python -u /home/pi/.node-red/node_modules/node-red-contrib-grove-base-hat/grove-temperature-sensor/grove-temperature-sensor.py 2 1651393678682'
}

この温度センサーではNTC Thermistorを使っているのでMCP9808は使っていないのですがエラーになっています。「grove/temperature/__init__.py」が有無を言わさずに「mcp9808.py」をimportしている様です。

そこでupmとmraaを使用しているモジュールを調べてみると

$ sudo grep -rl upm /usr/local/lib/python3.9/dist-packages/grove
/usr/local/lib/python3.9/dist-packages/grove/display/jhd1802.py
/usr/local/lib/python3.9/dist-packages/grove/display/__init__.py
/usr/local/lib/python3.9/dist-packages/grove/display/sh1107g.py
/usr/local/lib/python3.9/dist-packages/grove/display/__pycache__/jhd1802.cpython-39.pyc
/usr/local/lib/python3.9/dist-packages/grove/display/__pycache__/__init__.cpython-39.pyc
/usr/local/lib/python3.9/dist-packages/grove/display/__pycache__/sh1107g.cpython-39.pyc
/usr/local/lib/python3.9/dist-packages/grove/temperature/mcp9808.py
/usr/local/lib/python3.9/dist-packages/grove/temperature/__pycache__/mcp9808.cpython-39.pyc
/usr/local/lib/python3.9/dist-packages/grove/grove_optical_rotary_encoder.py
$
$sudo grep -rl mraa /usr/local/lib/python3.9/dist-packages/grove
/usr/local/lib/python3.9/dist-packages/grove/display/sh1107g.py
/usr/local/lib/python3.9/dist-packages/grove/display/jhd1802.py
/usr/local/lib/python3.9/dist-packages/grove/display/__pycache__/sh1107g.cpython-39.pyc
/usr/local/lib/python3.9/dist-packages/grove/display/__pycache__/jhd1802.cpython-39.pyc
/usr/local/lib/python3.9/dist-packages/grove/gpio/gpio_mraa.py
/usr/local/lib/python3.9/dist-packages/grove/gpio/__init__.py
/usr/local/lib/python3.9/dist-packages/grove/gpio/__pycache__/gpio_mraa.cpython-39.pyc
/usr/local/lib/python3.9/dist-packages/grove/gpio/__pycache__/__init__.cpython-39.pyc
/usr/local/lib/python3.9/dist-packages/grove/grove_optical_rotary_encoder.py

それぞれのソースにあたってみると、

  • displayでは__init__.pyで直接upmからimportしています。
  • temperatureでは__init__.pyで直接upmをimportしていませんが、__init__.pyでmcp9808.pyをimportしているので間接的にimportされます。
  • grove_optical_rotary_encoderについては光ロータリーエンコーダを使用しなければ問題ありません。
  • gpioについてはRPi.GPIOがインストールされていない場合にgpio_mraaを使用するようになっていますがbullseyeではRPi.GPIOがインストールされているのでmraaは使用されません。

以上からdisplayとtemperatureについて対応してあげれば使用できるようになりそうです。

Groveにパッチを当てる

以下の修正をする事でNode-REDで問題なく温度の測定をする事ができるようになりました。

$ sudo vi /usr/local/lib/python3.9/dist-packages/grove/temperature/__init__.py

from .temper import Temper,TemperTypedNTC
### from .mcp9808 import TemperMCP9808

### __all__ = ["Temper", "TemperTypedNTC", "TemperMCP9808"]
__all__ = ["Temper", "TemperTypedNTC"]

$ sudo vi /usr/local/lib/python3.9/dist-packages/grove/display/__init__.py

### from upm.pyupm_lcd import SSD1306, SSD1308, SSD1327
### from .jhd1802 import JHD1802
### from .sh1107g import SH1107G_SSD1327

### __all__ = ["JHD1802", "SH1107G_SSD1327"]
__all__ = []

尚、コメントにしたGroveモジュールについて使用する場合は自分で対応する必要があります。

2022年5月2日月曜日

Raspberry Pi Imager update to v1.7

 知らないうちにRaspberry Pi Imagerが便利になっていた


セミナーでハンズオンを行う機会が多いのですが、その場合ヘッドレスインストールする事がほとんどでした。
この時に今まではssh、Wi-Fiの設定については/bootにファイルを設置することで可能でしたがhostnameを変更することは出来ませんでした。この為、複数のRaspberry Piが「raspberrypi」と言うhostnameでインストールされることになりせっかくZeroconfでssh接続しようと思っても同じhostnameが乱立することになり初期設定手順が煩雑になっていました。

Raspberry Pi Imagerなんてイメージが書き込めればいいと思っていたのでアップデートすることもなくほったらかしになっていたのですがいつの間にか便利機能が追加されていた様です。

今回v1.7になることで日本語対応してくれた様ですが、v1.6の時に便利機能が追加されていた様です。2021年3月19日のNewsにちゃんと紹介されていました(><)

この時はまだCtrl-Shift-Xと裏技的な取り扱いでしたが今回v1.7ではわかり易くなっていましたので日本語化されたその画面を紹介したいと思います。基本的な設定内容はv1.6と変更はありませんので詳細は上記のNewsを見てください。とは言っても画面を見れば判る事ばかりですが。

Raspberry Pi Imager v1.7


起動直後の画面が日本語になっています。
まだこの時点では設定ボタンは表示されていません。


此処でインストールするOSを選択すると


設定ボタンが表示されます。
これをクリックするとこんな画面が表示されてhostnameも設定できる様になっています(@_@)


これなら複数台のRaspberry Piがあって一斉にインストールしてもssh接続で困る事は無くなったのでセミナーがよりスムーズに進みそうです。
また、この設定はインストールのデフォルト設定とする事も可能なので自宅で何回もイメージの書き換えを行う時も楽になりそうです。

※1:bullseyeからユーザpiが自動設定されなくなったのでユーザ名/パスワードの設定を忘れるとbullseyeのヘッドレスインストールではMicroSDへの書き込み後初めての起動で立ち上がらなくなるのでご注意ください。

※2:bullseyeからuserconf.txtを/bootに用意する事で初期ユーザの作成が行われます。

2022年1月3日月曜日

MINDSTORMS Python まとめ

 MINDSTORMS Python 全部入り

今まで実装してきた非同期処理、ステートマシン、独自ライブラリについて全てまとめたソースについてGiHubへ公開しておきました。
非同期処理で距離センサーを使用しながら、ステートマシンで直進します。距離センサーが20cm以内に物体を検知した場合にイベントで通知するので停止するという簡単なプログラムです。
メイン処理、ステートマシンでの処理、非同期処理するプログラムと3つのプロジェクトに分けて作成してあります。
MINDSTORMSのモーターやセンサーオブジェクトはそれぞれでimportしても動くとは思いますが、接続ポートの変更などを考慮してメインで全て定義して引数で渡す様にしています。
独自ライブラリの方ではできるだけ汎用性が出る様にプログラムを作成する様に心がけるなどすると将来的にも役立つと思います。

クラスの継承問題

サンプルプログラムではclassを使ってオブジェクト思考風に設計していますが、ここで1つ問題が出てきます。オブジェクト指向で設計していくと継承を使いたくなりますがプロジェクトを分割した場合、親クラスが自分のプロジェクト内に定義されているか、importする必要があるということです。(クラスだけでなく関数でも同じです。)

サンプルではステートマシン用のプロジェクト内にStateクラスとGoStraightクラス(Stateクラスを継承)が定義されていますが、プログラムが大きるなくと各ステート毎にプロジェクトを分割したくなると思います。(私だとステート毎にプロジェクトに分割すると思います)
StateクラスとStateMachineクラス(やその他共通で使用するクラスや関数)を共通ライブラリ化して1つのプロジェクトとして各ステートではStateクラスなどをimportして使うというやり方です。
GoStraightクラスを別プロジェクトにした例が以下です。(Project3.pyとします)

import json

class CopyProject:
    def __init__(self):
        f = open('projects/.slots', 'r')
        data = f.read()
        f.close()
        self.slots = json.loads(data.replace('\'', '"'))

    def getID(self, no):
        return self.slots[no]['id']

    def copy(self, no_from, no_to, name):
        from_path = 'projects/{}/__init__.mpy'.format(self.slots[no_from]['id'])
        to_path = 'projects/{}/{}.mpy'.format(self.slots[no_to]['id'], name)
        f = open(from_path, 'rb')
        data = f.read()
        f.close()
        f = open(to_path, 'wb')
        f.write(data)
        f.close()

cp = CopyProject()
cp.copy(1, 3, 'lib1')
from .lib1 import State

# 直進
class GoStraight(State):
    def __init__(self, name: String, wheels):
        super().__init__(name)
        self.__wheels = wheels

    def entry(self):
        self.__wheels.start(0, -20)

    def exit(self):
        self.__wheels.stop()

if __file__.split('/')[3] == '__init__.mpy':
    print(__file__)

Stateのimportを行わないとプロジェクトをアップロードする時にStateが無いというエラーが出てアップロードすることが出来ません。またStateをimportするのはアップロードする時だけて良さそうですが(ライブラリとしてimportされる場合は、事前にStateをimportしておく)、GoStraightをimportする時もStateをimportしていないとエラーとなります。

CopyProjectクラスだけはimportして使うことが出来ないので、今回の例の様にStateクラスの提供する機能が殆どない様なケースではGoStraightクラスではStateクラスを継承せずに必要なメソッドは自力でコーディングしておく方がソースがスッキリする様です。

共通ライブラリ化の場合は参照関係を整理して助長性とのトレードオフを考える必要がありそうです。


2022年1月2日日曜日

MINDSTORMS Pythonで if __name__ == "__main__"

 if __name__ == "__main__"

Pythonのドキュメントによると

モジュールは、自身の __name__ をチェックすることでメインスコープで実行されているかどうかを確認できます。これはモジュールがスクリプトとして、あるいはインポートでなく python -m で起動されたときに実行するコードの条件として使用できる一般的なイディオムです:

とあります。
ライブラリを作成している時にテストコードを書く場合に良く使用されますが、今回のLEGOでのライブラリ化では使用できませんでした。

色々試した結果以下の方法で実現できました。

LEGOでif __name__ == "__main__"

プロジェクトとして実行された場合にHUBのメモリやディスクの使用状況を表示しています。

if __file__.split('/')[3] == '__init__.mpy':
    import os
    import gc
    import micropython
    print('-----------------------------')
    stat = os.statvfs('/')
    total_size = stat[0] * stat[2]
    free_size = stat[0] * stat[4]
    print('each block is {} bytes big'.format(stat[0]))
    print("in bytes, that's {} bytes free out of a total of {}".format(free_size, total_size))
    print('-----------------------------')
    gc.collect()
    micropython.mem_info()
    print('-----------------------------')
    print('Initial free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
    print('-----------------------------')    
実行すると
-----------------------------
each block is 4096 bytes big
in bytes, that's 29052928 bytes free out of a total of 32505856
-----------------------------
mem: total=595907, current=402437, peak=404485
stack: 1052 out of 7168
GC: total: 242368, used: 193024, free: 49344
 No. of 1-blocks: 3072, 2-blocks: 892, max blk sz: 321, max free sz: 869
-----------------------------
Initial free: 49344 allocated: 193024
-----------------------------
ストレージとしては32Mバイトの殆どが空いていることがわかります。
また、MicroPythonの場合、実行中のプログラムがオブジェクトをインスタンス化すると、必要なメモリがヒープから割り当てられますが、その空き容量が49Kバイト程あることもわかります。

2022年1月1日土曜日

LEGO MINDSTORMS Pythonで独自ライブラリ

 MINDSTORMSのPythonでは独自ライブりの利用ができないのである程度の規模のプログラムではソースが大きくなって見通しが悪くなるというお話をしました。ただ、MINDSTORMSのHUBでは0〜19までの20種類のプログラムをアップロードできて、LEGO本体(以下HUB)のキー操作で選択して実行することができるようになっています。この機能を使えばLEGOには20種類のPythonプログラム(LEGOアプリではプロジェクト:以下プロジェクト)をアップロードできることになります。

ということでアップロードしたプロジェクトはHUBに保存されているはずなのでライブラリとして作成したものをアップロードしてそれをimportすれば、なんちゃってライブラリとして使えるのではないかなと考えHUBの方を少し調べてみました。

MINDSTORMS HUBの調査


注意:以下の内容はMINDSTORMSのHUBを直接操作しますので、自己責任でお願いいたします。

先日MINDSTORMSのMicroPythonのバージョン調査した時は特に説明をしませんでしたがHUBとUSBないしはBluetoothで接続するとPCとシリアル接続ができます。このシリアルポートを使ってMicroPythonをキーボードから操作することができます。
シリアル通信が可能なソフトならなんでも良いのですが私はTeraTermを利用していますが、TeraTermを利用してHUBと接続する方法はこちらの説明が分かり易いと思います。尚、記事の「インストール」については作業する必要はなく「REPL(コンソール/コマンドライン)による実行」の部分だけ参考にすればHUBとの接続は可能です。

HUBとの接続ができると後はMicroPythonのインタプリタに接続した状態になりますので、適時プログラムを入力することでHUBの中身を調査していきます。

>>> import os
>>> os.listdir()
['boot.py', 'bt-lk1.dat', 'bt-lk2.dat', 'main.py', 'util', 'projects', 'runtime', 'system', '_api',
 'commands', 'event_loop', 'mindstorms', 'programrunner', 'protocol', 'sounds', 'spike', 'ui',
 'hub_runtime.mpy', 'version.py', '.runtime_hash', 'etc', 'extra_files', '.extra_files_hash',
 'runtime.log']
 注)見やすくする為、行を折り返しています。
いろいろなファイルやディレクトリがありますね。ファイル拡張子が.pyはPythonのソースファイル、.mpyはソースをコンパイルしMicroPythonのバイトコードに変換されたコードオブジェクトになります。
今後ファイルの中身を見る必要も何回かあると思うので、まずはテキストファイルの中身を表示する関数catを作成しておきます。
では早速main.pyの中を覗いてみます。
>>> def cat(name):
...        f = open(name, 'r')
...        src = f.read()
...        f.close()
...        print(src)
...
...
...
>>> cat('main.py')
import gc
import micropython

import hub_runtime

micropython.alloc_emergency_exception_buf(256)

hub_runtime.start()
hub_runtimeについてhelpを見てみます。
>>> help(hub_runtime)
object <module 'hub_runtime' from 'hub_runtime.mpy'> is of type module
  HubUI -- <class 'HubUI'>
  LinegraphMonitorMethods -- <class 'LinegraphMonitorMethods'>
  SoundMethods -- <class 'SoundMethods'>
  BT_VCP -- BT_VCP(0)
  ProgramRunner -- <class 'ProgramRunner'>
  __connection_changed -- <function __connection_changed at 0x20038b70>
  error_handler -- <ErrorHandler object at 20020e00>
  init -- <function init at 0x20038b40>
  TIMER_PACE_HIGH -- 16
  __file__ -- hub_runtime.mpy
  start -- <function start at 0x20038b60>
  LightMethods -- <class 'LightMethods'>
  ProgramMethods -- <class 'ProgramMethods'>
  initialize_ports -- <function initialize_ports at 0x200197b0>
  pop_force_reset -- <function pop_force_reset at 0x2001d200>
  cleanup_slot_list -- <function cleanup_slot_list at 0x2001d070>
  notify_orientation_event -- <function notify_orientation_event at 0x2001d390>
  USB_VCP -- USB_VCP(0)
  __name__ -- hub_runtime
  HubMethods -- <class 'HubMethods'>
  MotorMethods -- <class 'MotorMethods'>
  hub -- <module 'hub'>
  RPCProtocol -- <class 'RPCProtocol'>
  system -- <System object at 200296b0>
  notify_button_event -- <function notify_button_event at 0x2001d290>
  get_event_loop -- <function get_event_loop at 0x200155c0>
  notify_gesture_event -- <function notify_gesture_event at 0x2001d380>
  WaitMethods -- <class 'WaitMethods'>
  RTTimer -- <class 'RTTimer'>
  RadioBroadcastMethods -- <class 'RadioBroadcastMethods'>
  Timer -- <class 'Timer'>
  DeviceMethods -- <class 'DeviceMethods'>
  runtime -- <module 'runtime' from 'runtime/__init__.mpy'>
  MoveMethods -- <class 'MoveMethods'>
>>>
いろいろな関数やクラスが定義されいます。ProgramRunnerクラスが実際に作成したプロジェクトを実行する為のクラスかなと想像できます。時間ができたらそれぞれについてもう少し調べてみたいと思います。
次にフォルダの中身を見ていきたいと思いますがprojectsフォルダはその名前から作成したプロジェクトを保存していると思われます。
>>> os.listdir('projects')
['standalone_', 'standalone.mpy', '.slots', '15015', '13191', '21493', '7000']
>>>
現在4つのプロジェクトをアップロードしてあるので数字の部分がプロジェクトのようです。LEGOアプリからアップロードを繰り返すと同じプロジェクトをアップロードしてもこの数字は変更されるようです。プロジェクトを修正してアップロードする度に名前が変わってしまうのではimportする時にその都度importする名前を変更する必要があるので使い勝手が悪くなってしまいます。
そこで「.slots」の中身を覗いてみましょう。
>>> cat('projects/.slots')
{
 0: {'name': 'UHJvamVjdCAw', 'id': 13191, 'modified': 1641021587911, 'type': 'python', 'project_id': 'E7DDg6MLrjmB', 'created': 1640675556147},
 1: {'name': 'UHJvamVjdCAx', 'id': 15015, 'modified': 1641019129550, 'type': 'python', 'project_id': '53cym18n7Gxv', 'created': 1640932342297},
 2: {'name': 'UHJvamVjdCAy', 'project_id': 'PTGPVlM9kK14', 'modified': 1641026738726, 'created': 1640934681576, 'id': 21493, 'type': 'python'},
 3: {'name': 'UHJvamVjdCAz', 'type': 'scratch', 'modified': 1641028117920, 'id': 7000, 'project_id': 'objY1Z0dw-Ep', 'created': 1641028058971}}
>>>
注)見やすくする為、一部改行しています。
このファイルでアップロードしたプロジェクトを管理しているようです。プロジェクトをアップロードする0〜19の番号のことはスロットと呼んでいるんですね。またprojectsフォルダ内に作成されている数字のidは調べたところファイルではなくフォルダでした。
そこでこのフォルダの中を見てみると
>>> os.listdir('projects/15015')
['__init__.mpy']
>>>
となっていて、作成したプロジェクトはコンパイルされて「__init__.mpy」というファイル名でHUBへアップロードされていました。
この「__init__.mpy」または「__init__.py」というファイル名はPythonを少し勉強した方ならご存知のように特別なファイル名です。(知りたい方はこちらを参照すると纏まっています)
また、LEGOのMicroPythonの制限なのか色々やってみましたがimportすることが出来ませんでした。(私の力不足なのかもしれませんが・・・)

結論として以下の方法でimportすることができるようになりました。

  1. 別のスロットの「__init__.mpy」を自スロットへ「xxx.mpy」(xxxは任意)としてコピーする。
  2. 「from .xxx import <識別子名>」で必要なクラスや関数をimportする。

ということで前回のステートマシンのサンプルを元にState, StateMachineクラスをライブラリとして分離したものを以下に示します。

別スロットのプロジェクトをimportする


スロット1にState, StateMachineクラスを定義したプロジェクトを、スロット0に以下のプログラムをアップロードしてテストしています。

注)今後LEGOによる仕様変更で以下の方法が利用できなる可能性もあります。

from mindstorms import MSHub, Motor, MotorPair, ColorSensor, DistanceSensor, App
from mindstorms.control import wait_for_seconds, wait_until, Timer
from mindstorms.operator import greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, equal_to, not_equal_to
import math
import json

# 別スロットからのimport用コピー
class CopyProject:
    def __init__(self):
        f = open('projects/.slots', 'r')
        data = f.read()
        f.close()
        self.slots = json.loads(data.replace('\'', '"'))

    def getID(self, no):
        return self.slots[no]['id']

    def copy(self, no_from, no_to, name):
        from_path = 'projects/{}/__init__.mpy'.format(self.slots[no_from]['id'])
        to_path = 'projects/{}/{}.mpy'.format(self.slots[no_to]['id'], name)
        f = open(from_path, 'rb')
        data = f.read()
        f.close()
        f = open(to_path, 'wb')
        f.write(data)
        f.close()

cp = CopyProject()
cp.copy(1, 0, 'lib1')

from .lib1 import State, StateMachine

# オブジェクトの定義
app = App()
hub = MSHub()
wheels = MotorPair(MSHub.PORT_B, MSHub.PORT_A)
arm = Motor(MSHub.PORT_C)
distance_sensor = DistanceSensor(MSHub.PORT_D)
color_sensor = ColorSensor(MSHub.PORT_E)

# ステートの定義
STATE_EXIT = "exit"
STATE_GO_STRAIGHT = "GoStraight"

# 直進
class GoStraight(State):
    def __init__(self, name: String = STATE_GO_STRAIGHT):
        super().__init__(name)

    def entry(self):
        wheels.start(0, -20)

    def do(self) -> String:
        distance = distance_sensor.get_distance_cm()
        if distance and distance < 20: # 測定不能の場合None
            return STATE_EXIT
        return None

    def exit(self):
        wheels.stop()

# メイン処理
class Main:
    def __init__(self):
        self.__stateMachine = StateMachine()
        self.__stateMachine.add(State(STATE_EXIT))
        self.setup()
        self.loop()

    def setup(self):
        # ステートの登録
        self.__stateMachine.add(GoStraight())
        # 開始ステートへ切り替える
        self.__stateMachine.changeTo(STATE_GO_STRAIGHT)
        
    def loop(self):
        while self.__stateMachine.currentState != STATE_EXIT:
            nextState = self.__stateMachine.run()
            if nextState:
                self.__stateMachine.changeTo(nextState)
            # 外部よりのステートマシン終了
            if hub.right_button.is_pressed():
                print("right button is pressed !")
                self.__stateMachine.changeTo(STATE_EXIT)

# プログラム開始指示を待つ
hub.left_button.wait_until_pressed()
hub.speaker.beep()
# プログラム開始
Main()
# プログラム終了(メニューに戻る)
raise SystemExit

2021年12月31日金曜日

LEGO MINDSTORMS でステートマシン

なんちゃってスレッドができるようになりましたが、プログラムを勉強していくとまずはモジュール化という考え方が必要になってきます。

Scratchではスプライトというオブジェクト毎にプログラムを作成するということで自然とモジュール化されるようになっていますし、ブロック化やメッセージ送信・受信ということで意識的にモジュール化することも可能です。

私がプログラミングを始めた頃は構造化プログラミングという言葉が流行っていましたがその後、オブジェクト指向、イベント駆動、データ駆動、ドメイン駆動にテスト駆動とプログラミング界隈でもいろいろな言葉が流行りましたが、基本は構造化プログラミングかなと個人的には思っています。

おそらくなんでもそうなんでしょうがある程度のところまでは才能だけで行くことが出来ますが、その上に行くにはやはり基本が大切なのかなと思います。

以下にMINDSTORMSのMicroPythonでのステートマシンのプログラム例を示しますが、ステートマシンは複雑な振る舞いの制御によく使われていて、ROS(Robot Operating System)ではsmachパッケージとしてPythonで利用できるようになっています。

MINDSTORMS用ステートマシン

必要に応じてuasyncioを利用した非同期処理を組み込めば複雑な処理も可能になりますが、その場合はイベント(uasyncuo.Event)によるタスク同期なども考える必要が出てきます。

from mindstorms import MSHub, Motor, MotorPair, ColorSensor, DistanceSensor, App
from mindstorms.control import wait_for_seconds, wait_until, Timer
from mindstorms.operator import greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, equal_to, not_equal_to
import math

# オブジェクトの定義
app = App()
hub = MSHub()
wheels = MotorPair(MSHub.PORT_B, MSHub.PORT_A)
arm = Motor(MSHub.PORT_C)
distance_sensor = DistanceSensor(MSHub.PORT_D)
color_sensor = ColorSensor(MSHub.PORT_E)

# ステートの定義
STATE_EXIT = "exit"
STATE_GO_STRAIGHT = "GoStraight"

# ステートクラス(基底クラス)
class State(object):
    def __init__(self, name: String):
        self.__name = name
        self.__action = None

    @property
    def name(self):
        return self.__name

    def entry(self):
        pass

    def do(self) -> String:
        return None

    def exit(self):
        pass

# ステートマシンクラス
class StateMachine(object):
    def __init__(self):
        self.__stateList = {}
        self.__now = None

    @property
    def currentState(self):
        return self.__now.name if self.__now else ""

    def add(self, state: State):
        self.__stateList[state.name] = state

    def changeTo(self, tag: String):
        if self.__now:
            self.__now.exit()
        self.__now = self.__stateList[tag]
        if self.__now:
            self.__now.entry()

    def run(self) -> String:
        if self.__now:
            return self.__now.do()
        return None

# 直進
class GoStraight(State):
    def __init__(self, name: String = STATE_GO_STRAIGHT):
        super().__init__(name)

    def entry(self):
        wheels.start(0, -20)

    def do(self) -> String:
        distance = distance_sensor.get_distance_cm()
        if distance and distance < 20: # 測定不能の場合None
            return STATE_EXIT
        return None

    def exit(self):
        wheels.stop()

# メイン処理
class Main:
    def __init__(self):
        self.__stateMachine = StateMachine()
        self.__stateMachine.add(State(STATE_EXIT))
        self.setup()
        self.loop()

    def setup(self):
        # ステートの登録
        self.__stateMachine.add(GoStraight())
        # 開始ステートへ切り替える
        self.__stateMachine.changeTo(STATE_GO_STRAIGHT)
        
    def loop(self):
        while self.__stateMachine.currentState != STATE_EXIT:
            nextState = self.__stateMachine.run()
            if nextState:
                self.__stateMachine.changeTo(nextState)
            # 外部よりのステートマシン終了
            if hub.right_button.is_pressed():
                print("right button is pressed !")
                self.__stateMachine.changeTo(STATE_EXIT)

# プログラム開始指示を待つ
hub.left_button.wait_until_pressed()
hub.speaker.beep()
# プログラム開始
Main()
# プログラム終了(メニューに戻る)
raise SystemExit

2021年12月30日木曜日

LEGO MINDSTORMS Pythonでなんちゃってスレッド2

 先日はジェネレータベースのコールチンを実装してみましたが、Python 3.10からはこのジェネレータベースのコールチンは廃止が予定されているようです。
MINDSTORMSのMicroPythonは3.4.0をベースとしたもののようです。

MicroPython v1.14-893-gbae6ff2ee on 2021-11-04; LEGO Technic Large Hub with STM32F413xx
Type "help()" for more information.
>>> import sys
>>> sys.version
'3.4.0'
>>>
現在のPythonの最新バージョンが3.10なのでMINDSTORMSのMicroPythonが3.10になることはまずないとは思いますが、プログラマを目指す子供たちはasyncioベースのコールチンに慣れておくべきかなと思います。(あまり使用することはないとは思いますがw)

コールチンをしっかりと理解しておくとスレッドやタスクといった概念についても理解しやすくなるでしょう。最近ではAndroidアプリの開発でもHTTPアクセス処理などでコールチンを利用した非同期処理が推奨されているようです。

uasyncioと一緒に使うコールチン


基本的なプログラムの作り方はジェネレータベースのコールチンとほぼ同じです。
async defで定義した関数の中でawaitでコールチンの処理を待つようにプログラミングするのが基本になります。実例を見た方が早いですね。
「yield True」を「await uasyncio.sleep_ms(1)」に書き換えた感じです。尚awaitするのはコールチンであれば良いので「uasyncio.sleep_ms(1)」でなく独自のメソッドを指定することも可能です。

コールチンの処理を終了させる方法はジェネレータベースのコールチンと同じ方法(コメントにしてあります)とuasyncio.Taskクラスが提供するcancel()メソッドを利用する方法があります。

uasyncioと一緒に使う方法ではメイン処理もコールチンになっているので適時awaitで処理を切り替えることが可能です。

from mindstorms import MSHub, Motor, MotorPair, ColorSensor, DistanceSensor, App
from mindstorms.control import wait_for_seconds, wait_until, Timer
from mindstorms.operator import greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, equal_to, not_equal_to
import math
import uasyncio

# オブジェクトの定義
app = App()
hub = MSHub()
wheels = MotorPair(MSHub.PORT_B, MSHub.PORT_A)
arm = Motor(MSHub.PORT_C)
distance_sensor = DistanceSensor(MSHub.PORT_D)
color_sensor = ColorSensor(MSHub.PORT_E)

# ステートの定義
STATE_START = 1
STATE_EXIT = -1

# コールチンクラス
class Coroutine1:
    def __init__(self):
        self.__distance = -1
        self.__loop = True

    @property
    def distance(self):
        return self.__distance

    def stop(self):
        self.__loop = False

    async def run(self):
        try:
            while self.__loop:
                await self.__done()
        finally:
            print("Coroutine1 end!")

    async def __done(self):
        self.__distance = distance_sensor.get_distance_cm()
        print(self.__distance)
        await uasyncio.sleep_ms(1)
        if self.__distance and self.__distance < 15:
            print("The wall is near!")
            await uasyncio.sleep_ms(1)

# メイン処理
class Main:
    def __init__(self):
        self.state = STATE_START
        self.setup()
        uasyncio.run(self.run())

    def setup(self):
        self.__coroutines = []
        self.__coroutines.append(Coroutine1())

    async def run(self):
        self.__tasks = []
        for c in self.__coroutines:
            self.__tasks.append(uasyncio.create_task(c.run()))
        while self.state != STATE_EXIT:
            if hub.right_button.is_pressed():
                print("right button is pressed !")
                #for task in self.__coroutines:
                #    task.stop()
                for task in self.__tasks:
                    task.cancel()
                await uasyncio.sleep_ms(30)
                self.state = STATE_EXIT
            else:
                self.__done()
                await uasyncio.sleep_ms(1) # コールチンに処理を譲る

    def __done(self):
        print("Main keep alive!")

# プログラム開始指示を待つ
hub.left_button.wait_until_pressed()
hub.speaker.beep()
# プログラム開始
Main()
# プログラム終了(メニューに戻る)
raise SystemExit

2021年12月29日水曜日

LEGO MINDSTORMS Pythonでなんちゃってスレッド処理

LEGO MINDSTORMS Robot Inventor


こちらに紹介ページはあるのですが「今すぐ購入」ボタンを押すとページは存在せず公式サイトで購入することはできません。日本では教育用のSPIKEプライムセットしか購入できません。SPIKEは教育用なのでパーツを入れるプラスチックケース入りなのに対してInventorはホビー向けなのでケースは紙製となっています。ただ、標準でモータが4つついていたりLEGOパーツの数が多かったりと個人で遊ぶのにはInventorの方が良いので、米国Amazonで購入することにしました。

本体の色がSPIKEは黄色なの対してInventorは青系の色となっています。またプログラム作成用ツールもSPIKE用とInventor用は別になっています。SPIKE用は日本語化されているのですがInventor用は日本で発売されていないこともあってか日本語化はされていません。
最近は小学校でも英語教育が必須化されたりしていますが、遊びの中で英語に触れる(LEGOなので殆ど言葉を利用することはありませんがw)ことが自然と英語を学ぶ良い方法なのかと思ったりします。

プログラミング用言語としてはScratchベースのブロックプログラミングとPythonが利用できます。PythonはMicroPythonなのでthreadingは入っていませんし、ローレベルのAPIである_threadも利用することはできません。利用できるのは非同期 I/O スケジューラuasyncio(非同期処理を扱うための標準ライブラリasyncioのサブセット)となるのでコールチンを利用した非同期処理のコードを書くことは可能となります。

ただ、ここで大切なのはマルチタスクやマルチスレッド、非同期処理といった概念ではなく複数の処理があたかも同時に処理されているように見えることが子供達にとっては大切だということだと思います。

MINDSTORMSでPythonは必要か?


Scratchでプログラムを作成する場合、まずはゲーム的なプログラムを作ることが多いと思いますが、その場合それぞれのスプライト用にプログラムを作成する考え方は直感的ですし、それを動かす場合に非同期処理(ScratchはNode.jsをベースに開発されているのでシングルスレッド・シングルプロセスが基本)を意識せずにマルチスレッド的な動作が自然と実践できるScratchの考え方は非常に優れていると思います。

さてそれではMINDSTORMSをPythonでプログラムする場合マルチスレッドは必要になるのかな?という疑問が出てきます。
ロボット工学は畑違いですが、MINDSTORMSのプログラムを作っていて感じるのはマルチスレッドよりもステートマシンでプログラムした方がすっきりとできるかなという感じです。

MINDSTORMS用のScratchは必要なブロックもきめ細かく用意されているのであえてPythonでプログラムを作成する必要はないかなと思います。Pythonを使ってプログラムしたい方はプログラミング言語を勉強したい方が取り組むべきかなと感じました。

Pythonジェネレータベースのコールチン


ということでまずはジェネレータベースの非同期処理を作ってみました。

Scratchの場合はスプライトという単位でプログラムが分けられますが、MINDSTORMS用のPythonではファイルの分割は出来なそうです。ある程度の規模のプログラムになるとできればファイル分割できた方が管理がやりやすいのですがね。

ノンプリエンティブなマルチタスクになりますので適時「yield True」でCPUを解放するのがポイントになります。またMainの__done()は非同期処理の対象外になっているので、ここで長時間CPUを占有すると非同期処理に影響を与えるので極力小さい処理にする必要があります。

LEGOでの注意事項としてはモータ制御はstart、stopで行うようにしてmoveは(同期処理になるので、短い距離では問題ないと思いますが)極力使用しないように、またセンサーのwait系も使用しないようにします。
from mindstorms import MSHub, Motor, MotorPair, ColorSensor, DistanceSensor, App
from mindstorms.control import wait_for_seconds, wait_until, Timer
from mindstorms.operator import greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, equal_to, not_equal_to
import math

# オブジェクトの定義
app = App()
hub = MSHub()
wheels = MotorPair(MSHub.PORT_B, MSHub.PORT_A)
arm = Motor(MSHub.PORT_C)
distance_sensor = DistanceSensor(MSHub.PORT_D)
color_sensor = ColorSensor(MSHub.PORT_E)

# ステートの定義
STATE_START = 1
STATE_EXIT = -1

# ジェネレータクラス
class Generator1:
    def __init__(self):
        self.__distance = -1
        self.__loop = True

    @property
    def distance(self):
        return self.__distance

    def stop(self):
        self.__loop = False

    # ジェネレータ関数本体
    def loop(self):
        while self.__loop:
            yield from self.__done()
        yield False

    def __done(self):
        self.__distance = distance_sensor.get_distance_cm()
        print(self.__distance)
        yield True
        if self.__distance and self.__distance < 15:
            print("The wall is near!")
            yield True

# メイン処理
class Main:
    def __init__(self):
        self.setup()
        self.loop()

    def setup(self):
        # ジェネレータの登録
        self.generators = []
        self.distance_sensor = Generator1()
        self.generators.append(self.distance_sensor)
        # ジェネレータ関数の登録
        self.generatorLoops = []
        for g in self.generators:
            self.generatorLoops.append(g.loop())

    def loop(self):
        while self.state != STATE_EXIT:
            if hub.right_button.is_pressed():
                for i, g in enumerate(self.generators):
                    g.stop()
                    while (next(self.generatorLoops[i])):
                        pass
                self.state = STATE_EXIT
            else:
                # ジェネレータの次の要素を取得(処理の再開)
                for loop in self.generatorLoops:
                    next(loop)
                # 自身の処理
                self.__done()
        
    def __done(self):
        print("Main keep alive!")

# プログラム開始指示を待つ
hub.left_button.wait_until_pressed()
hub.speaker.beep()
# プログラム開始
Main()
# プログラム終了(メニューに戻る)
raise SystemExit

2021年9月23日木曜日

Raspberry Pi ZeroでHeadphonesが無くなった

最近こんなの

Adeept Robotic Arm Kit for Arduino

こんなの

「Petoi Bittle」(ペトイ・ビトル)

を購入して遊んでいました。








両方共にArduino互換ボードでサーボモーターを駆動する物ですが基本的なプログラムは提供されるのでとりあえず組み立てるだけで動かすことができます。

Arm Kitの方は仕組みは簡単なので工夫すればいろいろとプログラムをして遊べそうです。ただバッテリーが18650を2本とちょっと特殊ですが、バッテリーがなくてもUSBからの給電でも動くので無理してバッテリーを購入する必要はないとおもいます。なをバッテリーを購入するときは充電器も専用なものが必要となるのでご注意下さいね。

Bittleの方は赤外線リモコンもついていて組み立てればすぐに遊ぶことができます。
提供されるプログラムですぐに動かすことができますが、スムーズに歩くようにさせるには調整が必要になるようです。ビデオ等ではスムーズに動いているようですが、この写真のように顔をつけていない物がほとんどです。
ちょっと調整をしてみましたが思ったようにうまく動かすことができません。想像ですが提供されるプログラムは顔の部分がついていない状態で調整されているのではないかなと思われます。顔がつくことで重量バランスが違ってくるので、単純なキャリブレーションで調整できずにプログラム側の調整も必要になるのではないかなと。

ということでBittleのプログラムをざっと眺めてみましたが、各サーボモータの角度を持つフレームを複数用意してループさせることで動作をさせるという形式でした。
うまく歩かない場合はこの角度を微調整していくことでよりスムーズに動作させることが可能になります。とはいえ、なかなか時間がかかる作業になりますので今回は見送り、よたよたと動くことで満足することにしました。

といったことをやっていて、そういえばRAPIROをしばらく動かしていないということに気が付きました。

引っ張り出して来て1年ぶり程に動かしたら、どうも調子が悪いようです。

MicroSDカードでエラーが発生


原因を調べてみると組み込んだRaspberry Pi ZeroのMicroSDカードでエラーが発生しているようです。あちこちのデモで使用した時にきちんとShutdownせずに電源を切っていたのが原因かもしれません。せっかくShutdownスイッチを付けたのですが撤収時にバタバタしてついつい電源をプチンと切ることが多かったのかもしれません。
fsckで修復してみましたがやはりファイルシステムが破損しているようです、ログインがパスワードエラーで弾かれてしまいます。

出来る範囲で必要なファイルをサルベージしてからMicroSDカードをフォーマットしなおしてRaspberry Pi OSからインストールです。

bcm2835 Headphonesがなくなった


Raspberry Pi Zeroにはヘッドフォン用ジャックは付いていませんがHDMIで音声出力も可能にはなっています。また、RAPIROに組み込んで使うときはPWM Audioを利用してスピーカーから音声を出力していました。
ということでOSインストール後音声の出力先を3.5mm Jackに変更しようとraspi-configを実行してみたのですがHDMIしか表示されません。どうやらOSがStretchからBusterに変わったのが原因のようです。ハードウエア的についていない物は表示しないということでしょうか?

config.txtの修正で解決


ネットを検索すると同じような現象でこまっていた方もいるようで、こちらのサイトに解決策が上がっていました。
以下の2行を/boot/config.txtの最後に追加することでGPIO 12と13でPWM Audioが利用できます。(一般的にはGPIO 13と18のペアが利用されますがGPIO 18がRAPIROとの接続ケーブルで使用しているのでペアを変更して利用しています)
dtoverlay=audremap,enable_jack=on
dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4

2021年6月3日木曜日

QNAPでPostgreSQLを動かす

 PostgreSQL公式では起動できない

QNAPでPostgreSQLを動かす場合はContainerSenterを導入してDockerで動かすようになっています。
標準でサポートされているデータベースはMariaDBなのですがバージョンが5.5.57(最新版は10.5.10)と古くこちらも最新版を動かす場合はDockerで動かすことになります。
今回は仕事の関係もあってPostgreSQLの11.12を動かす必要があったのでインストールを行ったのですがここでちょっとハマったのでアップしておきます。

ContainerSenterを導入して作成でPostgreSQLを検索するれば「公式」も表示されるのでこちらをインストールすれば簡単かなと思ったのですが・・・

/usr/lib/postgresql/11/bin/postgres: error while loading shared libraries: libsystemd.so.0: ELF load command alignment not page-aligned        
no data was returned by command ""/usr/lib/postgresql/11/bin/postgres" -V"                                                                     
The program "postgres" is needed by initdb but was not found in the                                                                            
same directory as "/usr/lib/postgresql/11/bin/initdb".                                                                                         
Check your installation.    

ライブラリのエラーで起動できないようです。

QNAPのCPUには2種類ある

私が購入したのはTS-231P3ですが使っているCPUは


とあるようにArmを使ってますが、Arm以外にもIntel CPUを使用している機種も存在します。QNAPのサポートでもPostgreSQLのインストール方法は記載されていますが、情報が古く現在のバージョンでは「Odoo」として表示されます。

またサポートでは注意書きとして「x86ベースのデバイスのみに対応しています。」とありますが現在のサンプルで使用しているイメージは「postgresql_armhf」となっているのでちょっとチグハグになっていました。

インストールバージョンでX.X.X-alpineを選択

結論としてはベースOSとしてalpineを選択することで問題なく起動することができました。
「ELF load command alignment not page-aligned」問題はQNAPだけでなく発生しているようですが参考になれば幸いです。


2021年4月22日木曜日

Vega-liteで温度測定グラフを描いてみた

ここ数日はVegaにハマっています。
IoTシステムでよく使う稼働状況グラフは表示できたので次に測定結果をグラフにすることにチャレンジしました。Node-REDのDashboardを使えば簡単に表示できますが、同じことをVega-liteで出来るようにしてみました。

mark: lineのTips


折れ線グラフはVega-liteのサンプルにもあるので簡単に表示できましたが、以下2点がちょっと不満なのであれこれと格闘してみました。
  • 表示範囲にちょっと余白を持ちたい
    • Vegaではデータからグラフの表示範囲(domain)を自動的に計算してくれるのですが、数値軸の場合は0から最大値までとなり、時間軸の場合はデータの時間範囲になります。
      domainを指定することでその範囲を指定することが可能ですが、データの値を元に指定したい。さらに、domainを最小値と最大値に設定すると上下(あるいは左右)に余白がなくなってグラフとしてちょっと見辛いという問題がありました。
  • 時間軸の表示方法をHH:MMとしたい
    • 時間軸の場合timeUnitをhoursminutesに設定すると軸ラベルはHH:MMで表示してくれるようになりますが、データも分単位に集約されてジャギーなグラフになってしまいます。測定が1秒毎のデータを表示しつつラベル表示はHH:MMとしたい時にどうしたら良いかでちょっとハマリました。

Dashboardでは何気に配慮してくれているので見やすいグラフに仕上がっています。

結果は以下のようになりました、Vega-Editorで確認してください。
ポイントはtransformのcalculateを使って軸データから上下(あるいは左右)の余白分を加減算した列を追加してdomainで指定することです。

{
    "description": "Line Chart Sample",
    "width": 600,
    "height": 600, 
    "data": {
		"values": [
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.35,"P":980.094,"H":33.53},"sensed_at":"2021-04-21T01:35:38.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.35,"P":980.096,"H":33.53},"sensed_at":"2021-04-21T01:35:43.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.35,"P":980.105,"H":33.53},"sensed_at":"2021-04-21T01:35:48.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.35,"P":980.116,"H":33.531},"sensed_at":"2021-04-21T01:35:53.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.36,"P":980.099,"H":33.531},"sensed_at":"2021-04-21T01:35:58.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.36,"P":980.138,"H":33.52},"sensed_at":"2021-04-21T01:36:03.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.36,"P":980.133,"H":33.52},"sensed_at":"2021-04-21T01:36:08.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.36,"P":980.152,"H":33.52},"sensed_at":"2021-04-21T01:36:13.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.36,"P":980.138,"H":33.52},"sensed_at":"2021-04-21T01:36:18.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.37,"P":980.143,"H":33.509},"sensed_at":"2021-04-21T01:36:23.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.37,"P":980.118,"H":33.509},"sensed_at":"2021-04-21T01:36:28.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.124,"H":33.51},"sensed_at":"2021-04-21T01:36:33.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.14,"H":33.509},"sensed_at":"2021-04-21T01:36:38.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.126,"H":33.51},"sensed_at":"2021-04-21T01:36:43.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.16,"H":33.51},"sensed_at":"2021-04-21T01:36:48.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.126,"H":33.51},"sensed_at":"2021-04-21T01:36:53.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.116,"H":33.51},"sensed_at":"2021-04-21T01:36:58.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.144,"H":33.51},"sensed_at":"2021-04-21T01:37:03.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.39,"P":980.128,"H":33.486},"sensed_at":"2021-04-21T01:37:08.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.11,"H":33.51},"sensed_at":"2021-04-21T01:37:13.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.11,"H":33.51},"sensed_at":"2021-04-21T01:37:18.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.094,"H":33.497},"sensed_at":"2021-04-21T01:37:23.000Z"}
		]
    },
    "transform": [
        {"calculate": "datum.data.T-0.01", "as": "minY"},
        {"calculate": "datum.data.T+0.01", "as": "maxY"}
    ],
    "mark": {"type":"line", "clip": true},
    "encoding": {
        "x": {
            "field": "sensed_at",
            "timeUnit": "hoursminutesseconds",
            "axis": { "format": "%H:%M" }
        },
        "y": {
            "field": "data.T",
            "type": "quantitative",
            "scale": {
                "domain": {"data": "data_0", "fields": ["minY", "maxY"]}
            }
        }
    }
  }

2021年4月17日土曜日

Vega-liteで稼働状況グラフを作成してみた

現在ESP32とRaspberry Piを使ってNC工作機械の稼働監視を行おうとしています。
稼働監視その物はESP32のNC工作機械主軸の電流を計測することで可能なのですが、問題はその稼働状況をグラフ化するところになります。

データはRaspberry Pi上でNode-redを動かしているのでDashboardを使えば可視化はある程度できましすが、よく見かける稼働監視グラフ(横方向の積み上げグラフあるいはガントチャートの応用)を表示しようとしてつまづきました。
Dashboardそのものはchartでグラフ化は可能ですが、積み上げグラフには対応していないようです。

node-red-node-ui-vega

ライブラリを検索しているとnode-red-node-ui-vegaというノードを発見しました。
Vega(「可視化を記述する言語」を目指した物で描画ライブラリとしてはD3を使用しているようです)及びVega-lite(Vegaの文法を簡略化した物でVega用の定義ファイルを出力して描画はVegaで行います)をNode-redから使えるようにしてくれるノードのようです。非常に高機能なグラフ化ツールのようです。積み上げグラフを表示する程度ならVega-liteで十分なようです。

サンプルも豊富に用意されていますが、残念ながらx軸を時間でとった横方向の積み上げグラフのサンプルはありませんでした。(横方向の積み上げ棒グラフのサンプルはありました)
これを修正して横軸を時間にできれば出来そうです。

ところがこれでハマってしまいました。Vegaに関する日本語の解説本もなく情報が少ない上にVegaの概念がちょっと複雑なのが原因です。基本的にはデータをチャネルという概念でデータ変換(データドメインから表示ドメインへ変換)して表示用の修飾を行って可視化(グラフ表示)するという考え方のようです。

Vega-liteによる稼働状況グラフ

結局数日かけた結果が以下のソースになります。Vega-Editorをブラウザで表示してコピペをするとどのように表示されるか見ることが出来ます。
このサイトを使えば自分で表示したいグラフのテストが簡単に出来るので開発もスムーズに出来ます。

{
  "config": {
      "legend": { "disable": true }
  },
  "width": 600,
  "height": 100,
  "data": {
    "values": [
      {"machine": "machine01", "start": "2021-04-01 08:30:00", "end": "2021-04-01 10:00:00", "color": "#ff0000"},
      {"machine": "machine01", "start": "2021-04-01 10:00:00", "end": "2021-04-01 11:15:00", "color": "#00ff00"},
      {"machine": "machine01", "start": "2021-04-01 12:00:00", "end": "2021-04-01 13:00:00", "color": "#ff00ff"},
      {"machine": "machine01", "start": "2021-04-01 14:00:00", "end": "2021-04-01 16:42:00", "color": "#0000ff"}
    ]
  },
  "mark": "bar",
  "encoding": {
    "y": {"field": "machine", "title": null},
    "x": {
      "field": "start",
      "timeUnit": "hoursminutes",
      "scale": {
        "domain": [{"hours": 8}, {"hours": 19}]
      },      
      "title": null
    },
    "x2": {
      "field": "end",
      "timeUnit": "hoursminutes"
    },
    "color": {
      "legend": {"disable": true, "title": null, "values": [ "" ]},
      "field": "color",
      "scale": {
          "range": {"field": "color"}
      }
    }
  }
}

2021年4月8日木曜日

PowerBI 32bitでMariaDBに接続できなかった件

Microsoft.PowerBI.OleDb is not registered

 最近IoT関係で計測データの分析をする必要があったのでPowerBIを使ってみました。
とりあえず自分のPCで分析できたので、顧客先のPCで同じことをやろうとしたところ上記のようなエラーになって分析できませんでした。
違いといえば自分のWindows10が64bitのProバージョンなのに対して、顧客のWindows10は32bitのHomeであることでした。

PowerBI及びMariaDB Connector/ODBCについては32bit版をインストールしたので問題ないと思ったのですが・・・
テーブルの選択は可能なのでMariaDBへの接続まではできているようです、その後内部的にデータ処理用にOleDBのオブジェクトに変換しているようで、そこでクラスが登録されていないというエラーのようです。

いつものようにGoogleさんにお伺いを立てるとこちらのサイトに行き当たりました。

内容的には64bitのPowerBIから32bitのAccessに接続しようとして発生したエラーの話のようですが、結論としてはMicrosoft Access Database Engineをインストールすれば解決するよという事のようです。OLE DBがAccess Database Engineに含まれているのかな?

一件落着


早速こちらから32bitのAccess Database Engineをダウンロードしてインストールして再度テストしましたが・・・

う・ご・か・な・い・・・

同じくOleDBが無いというエラーです。
PCを再起動してテストしても同じです。

そこで再度PowerBIも含めてインストールをやり直してみました。
  1. Microsoft Access Database Engineのインストール
  2. MariaDB Connector/ODBCのインストール
  3. PowerBIのインストール
今度はちゃんとアクセスすることができました。

とすると、インストールされているエンジンに合わせてPowerBIが必要なモジュールをインストールしているのかもしれませんね。

いずれにしろ一件落着でした。

EXCELの表をKaTeX形式に変換するマクロ

生成AI時代にプログラミング言語を学習する必要はあるのか? 最近では生成AIがプログラムを作成する時代になってきましたが、そんな時代にプログラミング言語を学習する必要はあるのでしょうか? ということで早速 Google さんに聞いてみました。 生成AI時代にプログラミング言語を学...