2022年 SPRESENSE™ 活用コンテスト 」開催中!ローパワーでハイパフォーマンスなエッジコンピューティングを、あなたの手で活用してみませんか?

eucalyのアイコン画像
eucaly 2021年12月05日作成 (2021年12月05日更新)
製作品 製作品 閲覧数 1140
eucaly 2021年12月05日作成 (2021年12月05日更新) 製作品 製作品 閲覧数 1140

自作キーボードキットを、iTunesリモコンにしてみる

自作キーボードキットを、iTunesリモコンにしてみる

この記事は、「キーボード #3 Advent Calendar 2021」の、5日目の記事です。
4日目の記事は、おかちまち様の記事でした。

さて、こんにちは、ゆうかりです。
今回は、自作キーボードキットで色々やらかすお話です。
・・・あんまキーボードしてる記事では無い気もしますが、気にしない!。

マクロキーボード余っちゃう事件

回路の研究だのはんだ付けの練習だの自らに言い訳しつつ、遊舎工房さんで安めの自作マクロキーボードキットをいくつか購入したのですが。
まあ不通に使う分には、そう何個もマクロキーボードいらないよね、というわけで。
自作キーボード歴1か月にして、「積みキーボード」が発生してしまっている状況でした。

ターゲットは「25KEYS」さんの「Casette42」です。
https://25keys.com/cassette42/

こんなのね(写真は遊舎工房さんから)。
キャプションを入力できます

iTunesリモコンにしてみる

  • キーがいくつかある
  • OLEDなディスプレイもくっついてる
  • 何よりカセットテープがモチーフ
    なので、今回はコイツを、「iTunesのリモコン」にしてみることにしました。

リモコン化作戦

もちろん、「メディアキーをQMKで割り当ててPCのiTunesをコントロール」でも、立派にリモコンでありますが。
作業場のiTunesは、サーバにインストールしてあって、画面とかなんもついておりません。
普段のコントロールは、Airplay先のアンプだのRetuneだので行っております。
リモートデスクトップ使ってるので、まあキーボードをそのままキーボードとして使うのはちょっとアレです。
また、OLEDに曲名とか表示したいのですが、Casette42に使われているマイコン「ATMEGA32U4」は、ガッツリ日本語フォント載せられるような潤沢なメモリ領域を持っていません。
英語だけなら何とかなりそうですが、それはそれで寂しいので、「外部から画像を突っ込む」仕様を今回はチャレンジの方向で!。

まあ、つーわけで。
以下作戦でいくことにしました。

  • ArduinoでCasette42を制御
  • 母艦はAirplayサーバの「shairport-sync」が入っているRaspberry Pi ZeroWH
  • USBシリアル接続で機材同士を接続
  • 画像はRaspberry Pi側で生成、Casette42に送る
  • キー入力は簡単なコマンドにしてRaspberry Pi側に送る

画面制御 (SSD1306 / Arduino側 )

ライブラリ使わずに制御

何はともあれ、まずはArduino側の画面制御を作りこむことに。
Casette42に使われているOLEDは、「SSD1306」というICを使ったI2C制御のもの。
128x64のものが割とメジャーで、Amazonだの秋月だのにも売られています。
ハイコントラストでカッチリ映ってステキなのですが、こいつは内部にフォントとかを持っていないので、描画情報を全て送ってあげる必要があります。

Adafruitのライブラリを使ってもよかったのですが、なんとなく自前制御を実装してみることに。
このへんの記事を参考にしました。
https://www.mgo-tec.com/blog-entry-31.html
https://garchiving.com/use-oled-without-library/

流し込みテスト用のデータ生成

まずは、テスト用のバイナリデータを作らないと何がどうなってるのやら、です。
なので、まず2値のBMP画像を用意し、コイツを01文字列に変換するスクリプトを作成しました。
単純にヘッダを無視してあとはバイナリ変換してるだけです。

bitmaptrans.pl

open (OUT,">evtest.txt"); open (IN,"evtest.bmp"); binmode (IN); $buffersize = 1; $count = 0; $output = ""; while (){ $count++; read(IN,$readdata,$buffersize); if ($count < 63){next;} if ($count > 20000){exit;} $readdata = unpack("B8",$readdata); print OUT $readdata; }

出力はこんな感じで。
BMP画像の特性上、上下がひっくり返ります。
キャプションを入力できます

こいつをArduinoに食わせるバイト列へ変換。
ひっくり返った画像は、読み込み部でひっくり返して直してます。

transarray.pl

open (IN,"evtest.txt"); binmode (IN); $data = (<IN>); close (IN); (@dataarray) = split(//,$data); our (%datap); $count = 0; for ($i=31;$i>=0;$i--){ for ($j=0;$j<=127;$j++){ $pointer = "$i $j"; $datap{$pointer} .= $dataarray[$count]; $count++; } } $arraycount = 0; $bcount = 0; $boutput = ""; for ($i=0;$i<32;$i++){ for ($j=0;$j<128;$j++){ $pointer = "$i $j"; $boutput .= $datap{$pointer}; $bcount++; if ($bcount > 7){ $boutput = unpack("H2", pack("B8",$boutput)); print "oledbuffer[$arraycount]=0x$boutput;"; $boutput = ""; $bcount = 0; $arraycount++; $arraymod = $arraycount % 8; if ($arraymod == 0){ print "\n"; } } } }

実行結果はこんな感じで、まあ可読性も何もあったもんじゃありませんが。
キャプションを入力できます

Arduino実装

さて、SSD1306の制御ですが。
ざっくり以下な感じです。

  • 縦8ドットごとに、「ページ」に分かれている
  • ページ内8ドットごとに、「縦」に1ドットづつ送り、「横」に並べていく
    ・・・まあぶっちゃけ訳が分からないですが、詳しくは上記URLだの仕様書だのを見て下さい。
    とりあえず試行錯誤し、絵が出るようになりました。
    キャプションを入力できます

送受信周り ( Arduino側 )

キー入力

Casette42の回路は比較的単純で、マトリックスなどは組まれておらず、GPIOにそれぞれキーがぶら下がっている構造です。
https://www.sho-k.co.uk/tech/wp-content/uploads/2021/07/Cassette42SMD.pdf
回路図を追えば、キーがどこに繋がっているのかまあ、分かるかと思います。
ArduinoでGPIOとして使いづらいピンにぶら下がっているのは、背面に実装されているアドレサブルLED、だけでした。
サクっと無視することで対応!。

シリアル周り

以下仕様でデザインしました。

  • 速度は115200bps
  • 受信側はバイナリ受信、単純に512bytesづつ表示用バッファに放り込む
  • 送信側は「keyn」「knobn」を送信、nには数字が入る感じ
  • シリアルタイムアウトは200ms

Arduinoプログラミング

いやあ、一部失敗しとるのですが。
まあいいやってこのまま使ってます。

えぇ、、、ロータリーエンコーダーが上手く動きませんとも。
表示部の処理が重すぎて、上手く位相が取れません。

sketch_kbd_casette42.ino

#include <Wire.h> #define OLED_ADDR (0x3C) //OLED address 製品に記載の数値は7bitなので、8bitに変換して1bit右へずらした値(78>>3c) byte oledbuffer[512]; unsigned long timedata,timedelta; int kbd_pins[] = {8,19,20,21,9,18,10,16,15,14}; int keystatus[10]; unsigned long keytime[10]; unsigned long nowtime,keypushtime; int keyvalue,keycount; void setup_i2c(){ Wire.beginTransmission(OLED_ADDR); Wire.write(0xAE); //display off Wire.write(0xA4); //RAM reset Wire.write(0xA5); //Entire display ON Wire.write(0x00); //set lower column address Wire.write(0x10); //set higher column address Wire.write(0x00); //set display start line Wire.write(0x2E); //Deactivate scrollスクロール表示解除 Wire.write(0xDA); //SETCOMPINS Wire.write(0x02); //32:02 62:12 Wire.write(0x21); //set Column Address Wire.write(0x00 | 0); //Column Start Address←水平開始位置はここで決める(0~126) Wire.write(B01111111); //Column Stop Address 画面をフルに使う Wire.write(0x22); //Set Page Address Wire.write(0x00); //start page address Wire.write(0x07); //stop page address Wire.write(0xB0 | 0); //set page start address←垂直開始位置はここで決める8bitで1ページ(B0~B7) Wire.write(0x81); //contrast control コントラスト設定オン Wire.write(0x7f); //127 コントラスト0-127 Wire.write(0xA6); //normal / reverse A7ならば白黒反転 Wire.write(0xC0); //Com scan direction←開始位置を右下から始める Wire.write(0x8d); //set charge pump enableチャージポンプを入れないと表示されない Wire.write(0x14); //charge pump ON Wire.write(0xAF); //display ON Wire.endTransmission(); } void Display_Black() { int i,j,k; for(i=0; i<4; i++){ Wire.beginTransmission(OLED_ADDR); Wire.write(0x00); //set display start line Wire.write(0xB0 | i); //set page start address←垂直開始位置はここで決める(B0~B7) Wire.write(0x21);//set column addres Wire.write(0x00 | 0);//start column addres←水平開始位置はここで決める(0~126) Wire.write(B01111111);//Column Stop Address 画面をフルに使う Wire.endTransmission(); for(j=0; j<16; j++){//column = 8bit X 16 ←8バイト毎に水平に連続で16個表示 Wire.beginTransmission(OLED_ADDR); // might be different for your display Wire.write(0x40); //Set Display Start Line ←このコマンドが実際のビットを書き込むコマンドらしい for(k=7; k>-1; k--){ Wire.write(0x00); } Wire.endTransmission(); } } } void Display_Output() { int i,j,k,l,m,targetpointer,temppointer; byte outputbyte,tempbit; for(i=0; i<4; i++){ Wire.beginTransmission(OLED_ADDR); Wire.write(0x00); //set display start line Wire.write(0xB0 | i); //set page start address←垂直開始位置はここで決める(B0~B7) Wire.write(0x21);//set column addres Wire.write(0x00 | 0);//start column addres←水平開始位置はここで決める(0~126) Wire.write(B01111111);//Column Stop Address 画面をフルに使う Wire.endTransmission(); for(j=0; j<16; j++){//column = 8bit X 16 ←8バイト毎に水平に連続で16個表示 Wire.beginTransmission(OLED_ADDR); // might be different for your display Wire.write(0x40); //Set Display Start Line ←このコマンドが実際のビットを書き込むコマンドらしい targetpointer = i * 128; targetpointer += j; for(k=7; k>-1; k--){ outputbyte = 0x00; for (l=0; l<8; l++){ temppointer = targetpointer + ( l * 16); tempbit = bitRead(oledbuffer[temppointer],k); bitWrite(outputbyte,l,tempbit); } Wire.write(outputbyte); } Wire.endTransmission(); } } } void kbd_init() { int i; for (i = 0; i < (sizeof(kbd_pins)/sizeof(int)); i++) { pinMode(kbd_pins[i], INPUT_PULLUP); } } void kbd_read() { keycount = 0; int i; for (i = 0; i < (sizeof(kbd_pins)/sizeof(int)); i++) { keyvalue = digitalRead(kbd_pins[i]); key_send(keyvalue); keycount++; } } void key_send(int keyvalue) { if ((keyvalue == 0)&&(keystatus[keycount] == 1)) { keytime[keycount] = millis(); } if ((keyvalue == 1)&&((keystatus[keycount] == 0)||(keystatus[keycount] == 3))) { nowtime = millis(); keypushtime = nowtime - keytime[keycount]; if (keypushtime < 2 ) { // anti chattering return; } if (keypushtime > 100000 ) { // abnormal reset keystatus[keycount] = 1; return; } keystatus[keycount] = 1; return; } if ((keyvalue == 0)&&(keystatus[keycount] != 3)) { keystatus[keycount] = 0; nowtime = millis(); keypushtime = nowtime - keytime[keycount]; if (keypushtime < 2 ) { // anti chattering return; } if (keycount < 6) { Serial.print("key"); Serial.println(keycount); keystatus[keycount] = 3; } if (keycount == 6) { keyvalue = digitalRead(kbd_pins[7]); if (keyvalue == 0) { Serial.println("knob0"); } else { Serial.println("knob1"); } keystatus[keycount] = 3; } if (keycount == 8) { keyvalue = digitalRead(kbd_pins[9]); if (keyvalue == 0) { Serial.println("knob2"); } else { Serial.println("knob3"); } keystatus[keycount] = 3; } } } void setup() { Wire.begin(); kbd_init(); Serial.begin(115200); Serial.setTimeout(200); setup_i2c(); delay(1000); Display_Black(); delay(1000); setuparray(); Display_Output(); } void setuparray() { oledbuffer[0]=0x00;oledbuffer[1]=0x00;oledbuffer[2]=0x00;oledbuffer[3]=0x00;oledbuffer[4]=0x00;oledbuffer[5]=0x00;oledbuffer[6]=0x00;oledbuffer[7]=0x00; oledbuffer[8]=0x00;oledbuffer[9]=0x00;oledbuffer[10]=0x00;oledbuffer[11]=0x00;oledbuffer[12]=0x40;oledbuffer[13]=0x00;oledbuffer[14]=0x00;oledbuffer[15]=0x00; oledbuffer[16]=0x00;oledbuffer[17]=0x00;oledbuffer[18]=0x00;oledbuffer[19]=0x00;oledbuffer[20]=0x00;oledbuffer[21]=0x13;oledbuffer[22]=0x00;oledbuffer[23]=0x00; oledbuffer[24]=0x00;oledbuffer[25]=0x00;oledbuffer[26]=0x20;oledbuffer[27]=0x00;oledbuffer[28]=0xe0;oledbuffer[29]=0x00;oledbuffer[30]=0x3c;oledbuffer[31]=0x00; oledbuffer[32]=0x00;oledbuffer[33]=0x07;oledbuffer[34]=0xf8;oledbuffer[35]=0x01;oledbuffer[36]=0xc0;oledbuffer[37]=0x19;oledbuffer[38]=0x1f;oledbuffer[39]=0xfc; oledbuffer[40]=0x01;oledbuffer[41]=0xfd;oledbuffer[42]=0x30;oledbuffer[43]=0x01;oledbuffer[44]=0xb0;oledbuffer[45]=0x00;oledbuffer[46]=0x66;oledbuffer[47]=0x00; oledbuffer[48]=0x00;oledbuffer[49]=0x07;oledbuffer[50]=0xff;oledbuffer[51]=0x01;oledbuffer[52]=0xdf;oledbuffer[53]=0xfd;oledbuffer[54]=0x9f;oledbuffer[55]=0xfe; oledbuffer[56]=0x01;oledbuffer[57]=0xff;oledbuffer[58]=0x98;oledbuffer[59]=0x01;oledbuffer[60]=0x38;oledbuffer[61]=0x00;oledbuffer[62]=0x82;oledbuffer[63]=0x00; oledbuffer[64]=0x00;oledbuffer[65]=0x00;oledbuffer[66]=0x0f;oledbuffer[67]=0x01;oledbuffer[68]=0xdf;oledbuffer[69]=0xfc;oledbuffer[70]=0x00;oledbuffer[71]=0x38; oledbuffer[72]=0x00;oledbuffer[73]=0x07;oledbuffer[74]=0xc0;oledbuffer[75]=0x01;oledbuffer[76]=0x1c;oledbuffer[77]=0x01;oledbuffer[78]=0x82;oledbuffer[79]=0x00; oledbuffer[80]=0x00;oledbuffer[81]=0x00;oledbuffer[82]=0x00;oledbuffer[83]=0x01;oledbuffer[84]=0x80;oledbuffer[85]=0xe0;oledbuffer[86]=0x00;oledbuffer[87]=0xf0; oledbuffer[88]=0x00;oledbuffer[89]=0x18;oledbuffer[90]=0x00;oledbuffer[91]=0x01;oledbuffer[92]=0x0e;oledbuffer[93]=0x03;oledbuffer[94]=0x03;oledbuffer[95]=0x00; oledbuffer[96]=0x00;oledbuffer[97]=0x0f;oledbuffer[98]=0xff;oledbuffer[99]=0x03;oledbuffer[100]=0x80;oledbuffer[101]=0xe0;oledbuffer[102]=0x01;oledbuffer[103]=0xc0; oledbuffer[104]=0x00;oledbuffer[105]=0x1c;oledbuffer[106]=0x80;oledbuffer[107]=0x01;oledbuffer[108]=0x1f;oledbuffer[109]=0x06;oledbuffer[110]=0x03;oledbuffer[111]=0x00; oledbuffer[112]=0x00;oledbuffer[113]=0x0f;oledbuffer[114]=0xff;oledbuffer[115]=0x83;oledbuffer[116]=0x80;oledbuffer[117]=0xe0;oledbuffer[118]=0x03;oledbuffer[119]=0xfc; oledbuffer[120]=0x03;oledbuffer[121]=0xb9;oledbuffer[122]=0xc0;oledbuffer[123]=0x03;oledbuffer[124]=0x3f;oledbuffer[125]=0x9f;oledbuffer[126]=0x03;oledbuffer[127]=0x00; oledbuffer[128]=0x00;oledbuffer[129]=0x00;oledbuffer[130]=0x0f;oledbuffer[131]=0x03;oledbuffer[132]=0x9f;oledbuffer[133]=0xfe;oledbuffer[134]=0x07;oledbuffer[135]=0xff; oledbuffer[136]=0x03;oledbuffer[137]=0x38;oledbuffer[138]=0xc0;oledbuffer[139]=0x03;oledbuffer[140]=0x7f;oledbuffer[141]=0xff;oledbuffer[142]=0x83;oledbuffer[143]=0x00; oledbuffer[144]=0x00;oledbuffer[145]=0x00;oledbuffer[146]=0x1c;oledbuffer[147]=0x03;oledbuffer[148]=0x9f;oledbuffer[149]=0xfe;oledbuffer[150]=0x1f;oledbuffer[151]=0x0f; oledbuffer[152]=0x03;oledbuffer[153]=0x3c;oledbuffer[154]=0xe0;oledbuffer[155]=0x03;oledbuffer[156]=0x7f;oledbuffer[157]=0xff;oledbuffer[158]=0xc7;oledbuffer[159]=0x00; oledbuffer[160]=0x00;oledbuffer[161]=0x00;oledbuffer[162]=0x78;oledbuffer[163]=0x03;oledbuffer[164]=0x80;oledbuffer[165]=0xe0;oledbuffer[166]=0x3c;oledbuffer[167]=0x03; oledbuffer[168]=0x87;oledbuffer[169]=0x1c;oledbuffer[170]=0xe0;oledbuffer[171]=0x03;oledbuffer[172]=0xff;oledbuffer[173]=0xff;oledbuffer[174]=0xfe;oledbuffer[175]=0x00; oledbuffer[176]=0x00;oledbuffer[177]=0x00;oledbuffer[178]=0xfc;oledbuffer[179]=0x03;oledbuffer[180]=0x80;oledbuffer[181]=0xe0;oledbuffer[182]=0x70;oledbuffer[183]=0x03; oledbuffer[184]=0x87;oledbuffer[185]=0x1e;oledbuffer[186]=0x70;oledbuffer[187]=0x01;oledbuffer[188]=0xff;oledbuffer[189]=0xff;oledbuffer[190]=0xfe;oledbuffer[191]=0x00; oledbuffer[192]=0x00;oledbuffer[193]=0x01;oledbuffer[194]=0xcc;oledbuffer[195]=0x03;oledbuffer[196]=0x87;oledbuffer[197]=0xe0;oledbuffer[198]=0x23;oledbuffer[199]=0xc3; oledbuffer[200]=0x86;oledbuffer[201]=0x0e;oledbuffer[202]=0x70;oledbuffer[203]=0x03;oledbuffer[204]=0xff;oledbuffer[205]=0xff;oledbuffer[206]=0xfe;oledbuffer[207]=0x00; oledbuffer[208]=0x00;oledbuffer[209]=0x07;oledbuffer[210]=0x8c;oledbuffer[211]=0x03;oledbuffer[212]=0x9f;oledbuffer[213]=0xf8;oledbuffer[214]=0x0f;oledbuffer[215]=0xe3; oledbuffer[216]=0x8e;oledbuffer[217]=0x0e;oledbuffer[218]=0x70;oledbuffer[219]=0x03;oledbuffer[220]=0xff;oledbuffer[221]=0xff;oledbuffer[222]=0xfc;oledbuffer[223]=0x00; oledbuffer[224]=0x00;oledbuffer[225]=0x0f;oledbuffer[226]=0x0c;oledbuffer[227]=0x03;oledbuffer[228]=0x98;oledbuffer[229]=0xfe;oledbuffer[230]=0x0c;oledbuffer[231]=0x77; oledbuffer[232]=0x8c;oledbuffer[233]=0x0e;oledbuffer[234]=0x30;oledbuffer[235]=0x06;oledbuffer[236]=0x7f;oledbuffer[237]=0xcf;oledbuffer[238]=0xfe;oledbuffer[239]=0x00; oledbuffer[240]=0x00;oledbuffer[241]=0x1e;oledbuffer[242]=0x0c;oledbuffer[243]=0x01;oledbuffer[244]=0xd8;oledbuffer[245]=0xef;oledbuffer[246]=0x0c;oledbuffer[247]=0x3f; oledbuffer[248]=0x1c;oledbuffer[249]=0x8e;oledbuffer[250]=0x38;oledbuffer[251]=0x04;oledbuffer[252]=0x1c;oledbuffer[253]=0x01;oledbuffer[254]=0xff;oledbuffer[255]=0x00; oledbuffer[256]=0x00;oledbuffer[257]=0x3c;oledbuffer[258]=0x0f;oledbuffer[259]=0xf1;oledbuffer[260]=0xdf;oledbuffer[261]=0xe2;oledbuffer[262]=0x07;oledbuffer[263]=0xfe; oledbuffer[264]=0x09;oledbuffer[265]=0xfc;oledbuffer[266]=0x30;oledbuffer[267]=0x04;oledbuffer[268]=0x08;oledbuffer[269]=0x00;oledbuffer[270]=0x7f;oledbuffer[271]=0x80; oledbuffer[272]=0x00;oledbuffer[273]=0x10;oledbuffer[274]=0x07;oledbuffer[275]=0xf1;oledbuffer[276]=0xc7;oledbuffer[277]=0x80;oledbuffer[278]=0x03;oledbuffer[279]=0xf8; oledbuffer[280]=0x00;oledbuffer[281]=0xf8;oledbuffer[282]=0x00;oledbuffer[283]=0x0c;oledbuffer[284]=0x00;oledbuffer[285]=0x00;oledbuffer[286]=0x3f;oledbuffer[287]=0x80; oledbuffer[288]=0x00;oledbuffer[289]=0x00;oledbuffer[290]=0x00;oledbuffer[291]=0x00;oledbuffer[292]=0x00;oledbuffer[293]=0x00;oledbuffer[294]=0x00;oledbuffer[295]=0x00; oledbuffer[296]=0x00;oledbuffer[297]=0x00;oledbuffer[298]=0x00;oledbuffer[299]=0x0c;oledbuffer[300]=0x00;oledbuffer[301]=0x00;oledbuffer[302]=0x3f;oledbuffer[303]=0x80; oledbuffer[304]=0x7f;oledbuffer[305]=0x03;oledbuffer[306]=0x83;oledbuffer[307]=0x03;oledbuffer[308]=0xc0;oledbuffer[309]=0x60;oledbuffer[310]=0x00;oledbuffer[311]=0x30; oledbuffer[312]=0x18;oledbuffer[313]=0x00;oledbuffer[314]=0xc2;oledbuffer[315]=0x08;oledbuffer[316]=0x00;oledbuffer[317]=0x00;oledbuffer[318]=0x3f;oledbuffer[319]=0xc0; oledbuffer[320]=0x7f;oledbuffer[321]=0x83;oledbuffer[322]=0xc3;oledbuffer[323]=0x03;oledbuffer[324]=0xc0;oledbuffer[325]=0x61;oledbuffer[326]=0x00;oledbuffer[327]=0x30; oledbuffer[328]=0x18;oledbuffer[329]=0x00;oledbuffer[330]=0xc7;oledbuffer[331]=0x08;oledbuffer[332]=0x00;oledbuffer[333]=0x00;oledbuffer[334]=0x3f;oledbuffer[335]=0xc0; oledbuffer[336]=0x71;oledbuffer[337]=0xc3;oledbuffer[338]=0xe3;oledbuffer[339]=0x03;oledbuffer[340]=0xc0;oledbuffer[341]=0x63;oledbuffer[342]=0x0f;oledbuffer[343]=0xff; oledbuffer[344]=0x7f;oledbuffer[345]=0xc7;oledbuffer[346]=0xfb;oledbuffer[347]=0x09;oledbuffer[348]=0x80;oledbuffer[349]=0xc0;oledbuffer[350]=0x3f;oledbuffer[351]=0xc0; oledbuffer[352]=0x70;oledbuffer[353]=0xe3;oledbuffer[354]=0xe3;oledbuffer[355]=0x07;oledbuffer[356]=0xe1;oledbuffer[357]=0xfd;oledbuffer[358]=0x80;oledbuffer[359]=0x30; oledbuffer[360]=0x18;oledbuffer[361]=0x00;oledbuffer[362]=0xc1;oledbuffer[363]=0x89;oledbuffer[364]=0x80;oledbuffer[365]=0xc0;oledbuffer[366]=0x1f;oledbuffer[367]=0x80; oledbuffer[368]=0x70;oledbuffer[369]=0x63;oledbuffer[370]=0x73;oledbuffer[371]=0x06;oledbuffer[372]=0x60;oledbuffer[373]=0x67;oledbuffer[374]=0x81;oledbuffer[375]=0xf0; oledbuffer[376]=0x18;oledbuffer[377]=0x03;oledbuffer[378]=0xc0;oledbuffer[379]=0xc8;oledbuffer[380]=0x00;oledbuffer[381]=0x00;oledbuffer[382]=0x1f;oledbuffer[383]=0x80; oledbuffer[384]=0x70;oledbuffer[385]=0x63;oledbuffer[386]=0x3b;oledbuffer[387]=0x0e;oledbuffer[388]=0x70;oledbuffer[389]=0x66;oledbuffer[390]=0xc3;oledbuffer[391]=0x30; oledbuffer[392]=0x1b;oledbuffer[393]=0xfe;oledbuffer[394]=0xc0;oledbuffer[395]=0x88;oledbuffer[396]=0x00;oledbuffer[397]=0x00;oledbuffer[398]=0x3f;oledbuffer[399]=0x00; oledbuffer[400]=0x70;oledbuffer[401]=0x63;oledbuffer[402]=0x3b;oledbuffer[403]=0x0e;oledbuffer[404]=0x70;oledbuffer[405]=0x66;oledbuffer[406]=0xc3;oledbuffer[407]=0x30; oledbuffer[408]=0x30;oledbuffer[409]=0x06;oledbuffer[410]=0xc6;oledbuffer[411]=0x08;oledbuffer[412]=0x00;oledbuffer[413]=0x00;oledbuffer[414]=0x3e;oledbuffer[415]=0x00; oledbuffer[416]=0x70;oledbuffer[417]=0x63;oledbuffer[418]=0x1f;oledbuffer[419]=0x0c;oledbuffer[420]=0x30;oledbuffer[421]=0x46;oledbuffer[422]=0x03;oledbuffer[423]=0x30; oledbuffer[424]=0x30;oledbuffer[425]=0x06;oledbuffer[426]=0xc6;oledbuffer[427]=0x0c;oledbuffer[428]=0xc3;oledbuffer[429]=0x00;oledbuffer[430]=0x3c;oledbuffer[431]=0x00; oledbuffer[432]=0x70;oledbuffer[433]=0xe3;oledbuffer[434]=0x0f;oledbuffer[435]=0x1f;oledbuffer[436]=0xf8;oledbuffer[437]=0xc6;oledbuffer[438]=0x01;oledbuffer[439]=0xf0; oledbuffer[440]=0x33;oledbuffer[441]=0x03;oledbuffer[442]=0x83;oledbuffer[443]=0x06;oledbuffer[444]=0x7e;oledbuffer[445]=0x00;oledbuffer[446]=0x38;oledbuffer[447]=0x00; oledbuffer[448]=0x71;oledbuffer[449]=0xc3;oledbuffer[450]=0x0f;oledbuffer[451]=0x1f;oledbuffer[452]=0xf8;oledbuffer[453]=0xc6;oledbuffer[454]=0x00;oledbuffer[455]=0x30; oledbuffer[456]=0x36;oledbuffer[457]=0x01;oledbuffer[458]=0x83;oledbuffer[459]=0x02;oledbuffer[460]=0x00;oledbuffer[461]=0x00;oledbuffer[462]=0x70;oledbuffer[463]=0x00; oledbuffer[464]=0x7f;oledbuffer[465]=0x83;oledbuffer[466]=0x07;oledbuffer[467]=0x18;oledbuffer[468]=0x19;oledbuffer[469]=0xac;oledbuffer[470]=0x00;oledbuffer[471]=0x60; oledbuffer[472]=0x66;oledbuffer[473]=0x01;oledbuffer[474]=0x83;oledbuffer[475]=0x03;oledbuffer[476]=0x80;oledbuffer[477]=0x03;oledbuffer[478]=0xc0;oledbuffer[479]=0x00; oledbuffer[480]=0x7f;oledbuffer[481]=0x03;oledbuffer[482]=0x03;oledbuffer[483]=0x38;oledbuffer[484]=0x1c;oledbuffer[485]=0x9c;oledbuffer[486]=0x01;oledbuffer[487]=0xc0; oledbuffer[488]=0x23;oledbuffer[489]=0xf8;oledbuffer[490]=0xfe;oledbuffer[491]=0x00;oledbuffer[492]=0xf9;oledbuffer[493]=0xfe;oledbuffer[494]=0x00;oledbuffer[495]=0x00; oledbuffer[496]=0x00;oledbuffer[497]=0x00;oledbuffer[498]=0x00;oledbuffer[499]=0x00;oledbuffer[500]=0x00;oledbuffer[501]=0x00;oledbuffer[502]=0x00;oledbuffer[503]=0x00; oledbuffer[504]=0x00;oledbuffer[505]=0x00;oledbuffer[506]=0x00;oledbuffer[507]=0x00;oledbuffer[508]=0x0f;oledbuffer[509]=0x00;oledbuffer[510]=0x00;oledbuffer[511]=0x00; } void loop() { kbd_read(); timedata = millis(); Serial.readBytes(oledbuffer, 512); timedelta = millis() - timedata; Display_Output(); }

Airplayサーバ側 ( raspberry pi )

概要

「shairport-sync」というソフトを使うことで、Airplayサーバとして動作します。
んで、コイツを使って、曲名だの貰ったり、リモコン制御用の鍵を貰ったりします。
この辺昔やらかしてるので、以下記事あたりが参考になるかと思います。
https://qiita.com/eucalyhome/items/b2e2374db6e580186987
https://qiita.com/eucalyhome/items/731d38655fdb3f3cad91
https://qiita.com/eucalyhome/items/9efc371737834b79df20

まあ、ポイントは以下な感じ

  • shairport-syncは、設定すればパイプにメタデータとして曲名とかを吐いてくれる
  • iTunesはWebhookを持つ、ポート3689
  • もちろんちゃんと設定すれば、raspberry piからiTunesの音が出せるよ!

セットアップ

Casette42周りの制御は、python3とpyserialでやろうかね、メタデータの変換はperlって感じで。
まずサクッとapt。

apt install libjson-perl
apt install python3-serial python3-usb
apt install python3-pillow

あと、「/ramdisk」に、tmpfsで8MBのRAMディスクを作っています。

shairport-syncの設定

/usr/local/etc/shairport-sync.conf の中身ね。
メタデータ出力設定部

metadata =
{
        enabled = "yes"; // set this to yes to get Shairport Sync to solicit metadata from the source and to pass it on via a pipe
        pipe_name = "/ramdisk/shairport-sync-metadata";
};

起動終了時フラグファイル制御

sessioncontrol =
{
        run_this_before_play_begins = "/usr/bin/touch /ramdisk/airplayflag"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line
        run_this_after_play_ends = "/bin/rm /ramdisk/airplayflag"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line
};

メタデータ受信

メタデータはbase64でエンコードされてます。
まあ昔作ったスクリプトを小改造で。

リモコン用のキーを別に保存することで、再起動してもリモコンが効くようにしてあります。

getmetadata.pl

#!/usr/bin/perl use MIME::Base64 (); use Encode; use JSON; $metajesonfile = "/ramdisk/airplay_metadata"; $keyfile = "/data/actkey.data"; $metadatafile = "/ramdisk/shairport-sync-metadata"; $| = 1; $keydatacount = 0; $titledatacount = 0; $albumdatacount = 0; $artistdatacount = 0; if (-e $keyfile) { open (FILE,$keyfile); $keydata = (<FILE>); close (FILE); } open (PIPEFILE,$metadatafile); while (<PIPEFILE>){ if ($_ =~ /<type>73736e63<\/type><code>61637265<\/code>/){ $keydatacount = 1; next;} if ($keydatacount == 1){ $keydatacount = 2; next;} if ($keydatacount == 2){ if ($_ =~ /^(.+?)<\/data>/){ $outputdata{'key'} = MIME::Base64::decode($1); &outputdata; $keydatacount = 0; }} if ($_ =~ /<type>636f7265<\/type><code>6d696e6d<\/code>/){ $titledatacount = 1; next;} if ($titledatacount == 1){ $titledatacount = 2; next;} if ($titledatacount == 2){ if ($_ =~ /^(.+?)<\/data>/){ $outputdata{'title'} = MIME::Base64::decode($1); &outputdata; $titledatacount = 0; }} if ($_ =~ /<type>636f7265<\/type><code>6173616c<\/code>/){ $albumdatacount = 1; next;} if ($albumdatacount == 1){ $albumdatacount = 2; next;} if ($albumdatacount == 2){ if ($_ =~ /^(.+?)<\/data>/){ $outputdata{'album'} = MIME::Base64::decode($1); &outputdata; $albumdatacount = 0; }} if ($_ =~ /<type>636f7265<\/type><code>61736172<\/code>/){ $artistdatacount = 1; next;} if ($artistdatacount == 1){ $artistdatacount = 2; next;} if ($artistdatacount == 2){ if ($_ =~ /^(.+?)<\/data>/){ $outputdata{'artist'} = MIME::Base64::decode($1); &outputdata; $artistdatacount = 0; }} } exit; sub outputdata { if ($outputdata{'key'} eq "") { $outputdata{'key'} = $keydata; } else { $keydata = $outputdata{'key'}; open (OUTPUTFILE,">$keyfile"); print OUTPUTFILE $keydata; close (OUTPUTFILE); } $jsonbasedata = { key => $outputdata{'key'}, title => $outputdata{'title'}, album => $outputdata{'album'}, artist => $outputdata{'artist'} }; $json_text = to_json( $jsonbasedata ); open (OUTPUTFILE,">$metajesonfile"); print OUTPUTFILE $json_text; close (OUTPUTFILE); }

Casette42 I/F

  • python3ベース
  • pillowを使って画像生成
  • imageオブジェクトをそのままbyte変換し、シリアル経由でCasette42に送信

な感じで作りましたとさ。
フォントは「JF-Dot-milkjf16.ttf」、「JF-Dot-milkjf16B.ttf」を使用、以下URLから入手できます。
http://jikasei.me/font/jf-dotfont/

casetteif.py

#!/usr/bin/env python3 # -*- coding: utf-8 -*- from PIL import Image, ImageFont, ImageDraw import serial import json,os import requests import time metadatafile = '/ramdisk/airplay_metadata' playflagfile = '/ramdisk/airplayflag' font = ImageFont.truetype("/data/JF-Dot-milkjf16.ttf", size=16) fontb = ImageFont.truetype("/data/JF-Dot-milkjf16B.ttf", size=16) scrollstataicwaittime = 16 loadtime = 0 filetime = 0 title = "" album = "" activeremotekey = '' titlelength = 0 albumlength = 0 scrollcount = 1 - scrollstataicwaittime scrollmax = 0 titlepos = 0 albumpos = 0 timenow = 0 timelegacy = 0 ser = serial.Serial('/dev/ttyACM0', 115200, timeout=1.0) while (True): if os.path.isfile(metadatafile) and os.path.isfile(playflagfile): filetime = str(os.path.getmtime(metadatafile)) if loadtime != filetime: timelegacy = int(time.time()) loadtime = filetime try: json_open = open(metadatafile, 'r') json_load = json.load(json_open) except: time.sleep(1) loadtime = 0 continue try: ser.close() except: pass try: ser = serial.Serial('/dev/ttyACM0', 115200, timeout=1.0) except: time.sleep(5) continue image = Image.new("1", (128,32), color=0) draw = ImageDraw.Draw(image) title = str(json_load['title']) album = str(json_load['album']) activeremotekey = str(json_load['key']) titlelengtharray = draw.textsize(title,font) albumlengtharray = draw.textsize(album,font) titlelength = titlelengtharray[0] albumlength = albumlengtharray[0] scrollmax = titlelength if albumlength > titlelength: scrollmax = albumlength scrollmax = scrollmax + scrollstataicwaittime - 128 if scrollmax < 0: scrollmax = 0 scrollcount = 1 - scrollstataicwaittime timenow = int(time.time()) - 600 if timenow > timelegacy: image = Image.new("1", (128,32), color=0) data = image.tobytes() ser.write(data) time.sleep(5) else: if scrollcount < 0: titlepos = 0 albumpos = 0 else: if titlelength < 128: titlepos = 0 else: maxstartpos = titlelength - 128 if maxstartpos < scrollcount: titlepos = maxstartpos else: titlepos = scrollcount if albumlength < 128: albumpos = 0 else: maxstartpos = albumlength - 128 if maxstartpos < scrollcount: albumpos = maxstartpos else: albumpos = scrollcount if scrollcount > scrollmax: scrollcount = 1 - scrollstataicwaittime scrollcount = scrollcount + 1 image = Image.new("1", (128,32), color=0) draw = ImageDraw.Draw(image) draw.text((-titlepos, 0), title, fill=(1), font=fontb) draw.text((-albumpos, 16), album, fill=(1), font=font) image = image.rotate(180) data = image.tobytes() ser.write(data) else: try: ser.close() except: pass try: ser = serial.Serial('/dev/ttyACM0', 115200, timeout=1.0) except: time.sleep(5) continue image = Image.new("1", (128,32), color=0) data = image.tobytes() ser.write(data) time.sleep(1) line = ser.read_all().decode() line.replace('\n','') line.replace('\r','') command = '' if 'key0' in line: command = 'previtem' if 'key1' in line: command = 'playpause' if 'key2' in line: command = 'nextitem' if 'key3' in line: command = 'restartitem' if 'key4' in line: command = 'prevgroup' if 'key5' in line: command = 'nextgroup' if command != '': url = "http://iTunesPCのIPアドレス:3689/ctrl-int/1/" + command headers = {'active-Remote': activeremotekey} try: res = requests.get(url, headers=headers) except: pass ser.close()

変なことが起きても何とかなるように、except祭り気味にしてあります。

自動起動設定 ( systemd )

まあ、上記2つのスクリプトを。
動かしっぱなしに設定。

/etc/systemd/system/getactiveremote.service

[Unit] Description = getactiveremote After = shairport-sync.service [Service] ExecStart=/usr/bin/perl /data/getmetadata.pl Restart=always Type=simple [Install] WantedBy=multi-user.target

/etc/systemd/system/casetteif.service

[Unit] Description=casetteif After=network.target [Service] Type=simple ExecStart=/usr/bin/python3 /data/casetteif.py Restart=always [Install] WantedBy=multi-user.target

あとは有効化と起動で

systemctl daemon-reload
systemctl start getactiveremote
systemctl enable getactiveremote
systemctl start casetteif
systemctl enable casetteif

できました!

曲名は太字、アルバム名は細字で!。
漢字もバッチリ対応!。
キャプションを入力できます

曲名とアルバム名は、それなりにちゃんとスクロールします。
キャプションを入力できます

半角英字もいい感じで、満足!。
キャプションを入力できます

リモコンとしてはまあこんな感じです。
ちゃんと画面も切り替わります、素敵!。

ここに動画が表示されます

さいごに

今回はリモコンとして仕立てましたが、これ

  • USB繋いだら動作するディスプレイとキー入力
    なので、debianやubuntuで動いているサーバの簡易設定表示や変更にも応用できそうな感じ。
    いわゆる「カスタマイズされたフロントパネル」みたいな使い方ができるのかなーと。

まあ、もしキーボードが余り気味なら、楽しい応用手法を考えてみるのも一興かな、と。
みなさまも、ぜひに。

なお、この記事は、Mint60とexmpを使って書きました、もちろん音楽を流しながら・・・。
キャプションを入力できます

以上です。

eucalyのアイコン画像
いつも、てきとうです
ログインしてコメントを投稿する