いずれ消え行く無駄な情報を、密やかに発信する装置。つまり日記。
Raspberry Pi 4を使ってRTSPで配信される動画をRTMPに変換してyoutube liveで配信するシェルスクリプトです。
Webカメラには、IO-Dataの屋外WebカメラのTS-NA200Wを使用しています。
AC100Vが50Hzの地域では25fpsで1920x1080の動画を撮影する事が可能です。ビットレートはCBRで2Mbpsになります。これを無線LANで転送する事が可能です。
Raspberry Pi 4にRaspbianをインストールして使用しています。インストールしたパッケージは一番ミニマルなパッケージになります。
pi@raspberrypi:~ $ uname -a Linux raspberrypi 4.19.97-v7l+ #1294 SMP Thu Jan 30 13:21:14 GMT 2020 armv7l GNU/Linux
ffmpegでTS-NA200WからRTSPでパケットを受け、ffmpegで現在時刻等の字幕を合成して、RTMPでyoutube liveに送信をします。
ffmpegにはハードウェアエンコーダが搭載されており、Raspbianでも使用することが可能です。ffmpegでハードウェアエンコーダを指定するにはエンコーダーにh264_omxを指定することで可能となります。
ハードウェアエンコーダを使用して変換をしましたが、CPU使用率は抑えることができましたが、fpsを稼ぐことができずリアルタイムエンコードが難しい状況でした。そのため、4コアあるCPUを活用する方針としてCPUエンコーダーであるlibx264を使用しました。
また、youtube liveの仕様で音声ストリームが無い動画は受け付けないようだったので、無音の音声を追加して配信をしています。
#!/bin/sh #=====CONFIG===== # youtubeで指定されたlivekeyを設定する LIVEKEY="xxxx-xxxx-xxxx-xxxx" # ライブ配信を開始した時刻を設定する STREAMING_START_TIME=`date +%s -d '2020-4-21 19:38:0'` #=====URL_SETTING===== RTSP_URL="rtsp://userid:password@192.168.1.x:16272/ipcam_h264.sdp" RTMP_URL="rtmp://a.rtmp.youtube.com/live2/${LIVEKEY}" #=====MAIN===== while true do echo "==========Start[`date +%Y%m%d_%H%M%S`]==========" ELAPSED_TIME=$((`date +%s` - ${STREAMING_START_TIME})) TEXT="drawtext=fontfile=./ipagp.ttf: text='gaso / Current Time %{localtime\:%F %T} JST (UTC+9) / Elapsed Time %{pts\:hms\:$ELAPSED _TIME}': fontcolor=white@0.7: fontsize=32: bordercolor=black: borderw=3: x=8: y=8'" #FFMPEG OPTION # -thread 0 # ffmpegのスレッド数を0(最適)に設定する # -re # 入力ストリームのフレームレート速度で読み込む # (TS-NA220Wの出力フレームレートは25pfsに設定) # -rtsp_transport tcp # RTSP transport protcolにTCPを使用する # デフォルト値のUDPを使用するとパケットロスが発生しブロックノイズが発生する # -i $RTSP_URL # 入力ストリームの指定 # IDとpasswordはIPアドレスの前に指定する # -filter_complex "$TEXT" # 動画に表示する現在時刻や経過時間などを合成するためのフィルター # filter_complexは複数入力ストリームを扱えるが今回はRTPSの入力ソース1つだけを使用 # -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 # 仮想デバイスから無音の音声を生成する # -c:a aac # 音声はaacコーデックで変換する # -f flv # 出力フォーマットはyoutubeが指定するFLVとする # -loglevel info # ログレベルを指定する # 動作状況を確認する時はinfo、本番稼働時はfatalとする # -b:v 5000k # 出力ビットレートを5Mbpsに指定する # youtubeの推奨値は1920x1080で3500kbpsとされているがyoutube側で再変換する際にビットレートが低いと画質劣化が生じる # -c:v libx264 # エンコーダーをlibx264に指定する # raspberry pi 4はハードウェアエンコーダh264_omxが使用できるが4coreのCPUで計算する方がパフォーマンスが良かった # -preset ultrafast # 圧縮率の指定を最も圧縮率の悪いultrafastに指定する # 帯域に余裕があるのであれば圧縮率を上げる必要はない # ultrafast以外のsuperfast, veryfast等ではリアルタイム処理が間に合わない # -force_key_frames expr:gte\(t,n_forced*4\) # キーフレームを4秒に1回入れるように指定する # youtubeが2から4秒毎にキーフレームを入れるよう推奨している # $RTMP_URL # 出力ストリームを指定する ffmpeg -threads 0 -re -rtsp_transport tcp -i $RTSP_URL -filter_complex "$TEXT" -f lavfi -i anullsrc=channel_layout=stereo:sample_r ate=44100 -c:a aac -f flv -loglevel info -b:v 5000k -c:v libx264 -preset ultrafast -force_key_frames expr:gte\(t,n_forced*4\) $RTMP_ URL #=====WAITING TIME===== echo "==========Finished[`date +%Y%m%d_%H%M%S`]==========" sleep 2 done
スクリプトの動作にあたっては、スクリプトのあるパスと同じ場所にフォントファイルが必要になります。このスクリプトではIPAフォントを使用しました。
ロードアベレージは4を超える事もあります。概ねCPU使用率は300%から350%程度でした。
pi@raspberrypi:~ $ more hoge top - 19:01:47 up 15:30, 2 users, load average: 4.08, 3.25, 1.71 Tasks: 107 total, 1 running, 106 sleeping, 0 stopped, 0 zombie %Cpu(s): 28.1 us, 4.7 sy, 57.8 ni, 9.4 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st MiB Mem : 3728.0 total, 3339.5 free, 221.3 used, 167.2 buff/cache MiB Swap: 0.0 total, 0.0 free, 0.0 used. 3384.4 avail Mem
CPUを酷使するためCPU温度はかなり高温になります。4月の気温でもCPUにヒートシンクを付け小型のファンを付けた状態で温度は60度を越えました。
pi@raspberrypi:~ $ vcgencmd measure_temp temp=61.0'C
Raspberry Piで秋月電子で販売されているロータリーエンコーダー(EC12PLRGBSDVBF-D-25K-24-24C-61)の動作確認を行いました。
ロータリーエンコーダーとは以下の通り接続を行いました。GPIOはプログラムでプルアップしています。
[RE] - [RaspberryPi] Cpin - 14pin(GND) Apin - 11pin(GPIO17) Bpin - 12pin(GPIO18)
プログラムではwiringPiのwiringPiISR関数を使用しました。この関数はGPIOのレベルが変化した際に指定した関数を呼び出す事ができ、ロータリーエンコーダーの回転に合わせたイベント処理が可能となります。
また、使用したロータリーエンコーダーはクリック機能が付いています。GPIOをプルアップすることにより、何もしていない時はA端子・B端子共にLレベルとなります。回転させるとA端子とB端子がそれぞれHレベルになり、次のクリック位置になると両方ともにLレベルに戻ります。右回転と左回転を識別するには、A端子とB端子のイベントが起こる順番を識別することで可能となります。
#include <wiringPi.h> #include <stdio.h> #include <stdlib.h> int a; int b; int p_a; int p_b; int num; void click_a(void){ int _a; _a = digitalRead(0);//GPIOの値を取得。 if(_a != a){ //同じ値が連続した場合にスキップする。 if(a == 1){ //一つ前のA端子の値を格。 p_a = 1; }else{ p_a = 0; } a = _a; //a端子とb端子の直前の値が1であり、今の値が0である場合にTRUE。 if(a == 0 && b == 0 && p_a == 1 && p_b == 1){ printf("right %d\n", ++num); } } } void click_b(void){ int _b; _b = digitalRead(1); if(_b != b){ if(b == 1){ p_b = 1; }else{ p_b = 0; } b = _b; if(a == 0 && b == 0 && p_a == 1 && p_b == 1){ printf("left %d\n", --num); } } } int main(void){ int setup = 0; a=1; b=1; p_a=0; p_b=0; num=0; setup = wiringPiSetup(); pullUpDnControl(0, PUD_UP); pullUpDnControl(1, PUD_UP); wiringPiISR(0, INT_EDGE_BOTH, click_a); wiringPiISR(1, INT_EDGE_BOTH, click_b); while(setup != -1){ sleep(1); } }
Raspberry Piと秋月電子で販売されている大気圧センサーのSCP1000を使用して大気圧計を作成しました。
使用したLinuxはdebianで、以下のバージョンです。
pi@raspberrypi ~/work/lcd $ uname -a Linux raspberrypi 3.12.22+ #691 PREEMPT Wed Jun 18 18:29:58 BST 2014 armv6l GNU/Linux
SPIを有効化するにはraspi-configコマンドを使用します。
pi@raspberrypi ~/work/lcd $ sudo raspi-config [8 Advanced Options -> A5 SPI -> Yes]
SCP1000はSPIインターフェースを備えています。以下の通り配線を行いました。どちらも物理ピン番号を記載しています。
[scp1000] - [RaspberryPi] 3pin(SCK) - 23pin(SCKL) 4pin(GND) - 25pin(GND) 5pin(MOSI) - 0.01uF - 19pin(MOSI) 6pin(MISO) - 21pin(MISO) 7pin(CSB) - 24pin(CE0) 8pin(VCC) - 17pin(3.3v)
本来、上記接続で問題なく接続できるはずですが、Raspberry Piからどんなコマンドを送っても、読み出すと0x00又は0xFFの値しか返ってこない状態となりました。そこで、SCP1000側のMOSIの近くに0.01uFを挟むと4回に1回程度ですが信号が届くようになりました。原因は不明ですが、ブレッドボードで接続しているため、ノイズ等の影響を受けて不安定になっている可能性があります。
表示デバイスは、秋月電子で販売されているSC2004CSWB-XA-LB-Gを使用しました。このデバイスは、4行x20桁が表示でき、バックライトに白色LEDを備えています。接続は以下の通り行っています。
[LCD] - [RaspberryPi] 1pin(VSS) - 3pin(GND) 2pin(VDD) - 1pin(3.3v) 3pin(Vo) - 2kΩ - 14pin(GND) 4pin(RS) - 16pin(GPIO23) 5pin(R/W) - 5pin(GND P5-Header) 6pin(E) - 18pin(GPIO24) 11pin(DB4) - 11pin(GPIO17) 12pin(DB5) - 12pin(GPIO18) 13pin(DB6) - 13pin(GPIO21) 14pin(DB7) - 15pin(GPIO22) [LCD LED] - [Raspberry Pi] A - 2pin(5v) K - 180Ω - 20pin(GND)
なお、コントラスト調整用のボリュームは測定すると1.98kΩでした。
また、LEDの駆動電源は5Vを用いています。Raspberry Piの3.3vピンは電流の制限値があることから、USBコネクタと直接つながっている5Vを使用し、制限抵抗は180Ωを用いました。
動作が安定しなかったため初期化に失敗した場合は、再試行を繰り返す動作にしています。一度でも正しく値を送信できると、極めて安定して気圧データを取得することができました。MOSIにコンデンサを挟むことで安定性が多少増しましたが、以下のコードの初期化の処理が誤っている可能性もあります。参考程度に留めておいて下さい。
#include<wiringPiSPI.h> #include<wiringPi.h> #include<stdio.h> #include<time.h> #include<lcd.h> #define SPI_CHANNEL 0 #define SPI_CLK 500000 int main(void){ double temp = 0; double pressure = 0; unsigned char buf[3]; char str[24]; time_t time_now; struct tm *local_time; int i,fd,flag1=0, flag2=0; //wiringPiライブラリの初期化 if (wiringPiSetup () == -1){ return (1) ; } /* LCDデバイスの初期化 */ if((fd = lcdInit(4,20,4, 4,5, 0,1,2,3,0,0,0,0)) < 0){ printf("[Error] open lcd device\n"); return(-1); } //SPIデバイスの初期化 if((wiringPiSPISetup(SPI_CHANNEL, SPI_CLK))<0){ printf("error SPI\n"); } //初期処理。失敗した場合は繰り返す。 while(1){ flag1 = 0; flag2 = 0; //時間の表示 time_now = time(NULL); local_time = localtime(&time_now); strftime(str, sizeof(str), "%Y/%m/%d %a %H:%M:%S", local_time); printf("%s\n", str); //ソフトリセットコード buf[0] = 0x1a; buf[1] = 0x01; wiringPiSPIDataRW(SPI_CHANNEL, buf, 2); printf("delay...\n"); delay(60); //STATUS確認 buf[0] = 0x1c; buf[1] = 0xff; wiringPiSPIDataRW(SPI_CHANNEL, buf, 2); printf("STATUS:%02x\n", buf[1]); i= buf[1]&0x01; if(i == 0){ flag1 = 1; printf("STATUS OK!\n"); } //DATARD8確認 buf[0] = 0x7c; buf[1] = 0xff; wiringPiSPIDataRW(SPI_CHANNEL, buf, 2); printf("DATARD8:%02x\n", buf[1]); i= buf[1]&0x01; if(i == 1){ flag2 = 1; printf("DATARD8 OK!\n"); } if(flag1==1 && flag2==1){ printf("START...\n"); break; } } //オペレーションレジスタに高精度測定モードを設定 buf[0] = 0x0e; buf[1] = 0x0a; wiringPiSPIDataRW(SPI_CHANNEL, buf, 2); //メインループ while(1){ //温度の測定 buf[0] = 0x84; buf[1] = 0x00; buf[2] = 0x00; wiringPiSPIDataRW(SPI_CHANNEL, buf, 3); temp = (double)((buf[1] << (8+2)) + (buf[2] << 2)) / 20 / 4; //気圧の測定 buf[0] = 0x7c; buf[1] = 0x00; wiringPiSPIDataRW(SPI_CHANNEL, buf, 2); pressure = buf[1] << 16; buf[0] = 0x80; buf[1] = 0x00; buf[2] = 0x01; wiringPiSPIDataRW(SPI_CHANNEL, buf, 3); pressure += (buf[1] << 8) + buf[2]; pressure = pressure / 4 / 100; //時間の取得 time_now = time(NULL); local_time = localtime(&time_now); //LCD表示 lcdClear(fd); lcdPosition(fd,0,0); strftime(str, sizeof(str), "%Y/%m/%d %a", local_time); lcdPrintf(fd, "DATE=%s", str); lcdPosition(fd,0,1); strftime(str, sizeof(str), "%H:%M:%S", local_time); lcdPrintf(fd, "TIME=%s", str); lcdPosition(fd,0,2); lcdPrintf(fd, "TEMP=%2.2f[C]", temp); lcdPosition(fd,0,3); lcdPrintf(fd, "PRESS=%4.4f[hPa]", pressure); delay(1000); } }
合同会社パレットソフト:気圧センサ SCP1000 (試食)
byobuは、標準でロードアベレージやメモリ使用量などを表示することができますが、あくまでLinuxを対象に作成されているためFreeBSDではまったく情報を表示することができなくなります。そのため、自作のスクリプトを作成し表示を行うようにしました。
試しに、メモリの表示を行うスクリプト作成しました。FreeBSDはメモリの情報表示に関して、Linuxのように/procを読み込んで表示することはできません。sysctlを叩いて情報を取得します。
% more /usr/local/bin/mem #!/bin/sh a=`echo "4098*$(sysctl -n vm.stats.vm.v_free_count)/1024/1024" | bc` b=`echo "$(sysctl -n sysctl hw.physmem)/1024/1024" | bc` echo "${a}MB/${b}MB"
~/.byobu/profileの設定を変更します。
backtickが、どうやらバックグラウンドで表示を行うスクリプトと表示番号を定義している模様です。今回は、138と139という番号でスクリプトを作成しました。
hardstatus stringが、表示位置やレイアウトを設定している模様です。一番最後に138と139を追加しました。
backtick 138 5 5 mem backtick 139 5 5 sysctl -n vm.loadavg hardstatus string '%99`%{-}%{=r}%12` %100`%112`%=%117`%133`%130`%135`%102`%101`%129`%131`%127`%114`%115`%108`%134`%128`%125`%126`%113`%119`%116`%106`%104`%103`%105`%107`%136`%123`%137`%132`%120`%121` %138` %139`'
/usr/local/lib/byobu/profile以下に各情報を取得するためのシェルスクリプトが格納されています。手っ取り早く表示させるならば、ここのスクリプトを書き換えてしまうのが、もっとも手軽です。
Raspberry Piと秋月電子で販売されている、ADT7410を使用して温度計を作成しました。前回作成した、NTP時計にセンサーを追加して動作するようにしています。
ADT7410は、I2Cインターフェースを備えています。接続は大変簡単で、3.3VとGNDをRaspberry Piの1Pinと6Pinに差し、センサーのSCAとSDAをRaspberry Piの3Pin(SDA)と5Pin(SCA)に接続するだけで完了です。
I2Cのモジュールを有効化します。まず、modprobeのブラックリストから、I2Cモジュールを除きます。
pi@raspberrypi ~ $ more /etc/modprobe.d/raspi-blacklist.conf # blacklist spi and i2c by default (many users don't need them) blacklist spi-bcm2708 #blacklist i2c-bcm2708
次に、moduleの有効化を行います。
pi@raspberrypi ~ $ more /etc/modules # /etc/modules: kernel modules to load at boot time. # # This file contains the names of kernel modules that should be loaded # at boot time, one per line. Lines beginning with "#" are ignored. # Parameters can be specified after the module name. snd-bcm2835 i2c-bcm2708 i2c-dev
最後にツールをインストールします。
pi@raspberrypi ~ $ sudo apt-get install i2c-tools
この状態で再起動すると、起動後にモジュールが有効化されます。
接続したI2Cデバイスが正しく認識されているかを確認します。ADT7410のデバイス番号は0x48なので、この値が表示されれば正しく接続されていることが分かります。
pi@raspberrypi ~ $ sudo i2cdetect -y 1 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- --
ADT7410とRaspberry PiのI2Cは、相性があまり良くないようです。詳細なバグ情報までは調査できませんでしたが、WiringPiのライブラリやi2cset/i2cgetコマンドを使用して読み書きができませんでした。やむを得ないので、単純にC言語で直接呼び出し使用することとしました。
読み込みに関しては、4byte単位であれば温度情報(0x00, 0x01)、ステータス情報(0x02)及び設定情報(0x03)を読み出すことが可能でした。
書き込みに関しては、2byte単位で、書き込みたいレジスタ(1byte)と設定したい値(1byte)を同時に書き込むことで設定可能でした。今回は、温度センサーの分解能を13bit動作から16bit動作に変更するために、センサー接続後にレジスタを設定しています。
いずれも、試したら動いたといった挙動で正しい制御を行っているかは分かりません。
#include <wiringPi.h> #include <lcd.h> #include <string.h> #include <time.h> #include <linux/i2c-dev.h> #include <sys/ioctl.h> #include <fcntl.h> #include <stdio.h> #define DATA_NUM 10 double get_temp(int rtc){ char buf[4]; double temp; if (read(rtc, buf, 4) != 4) {buf[0] = 0x00; buf[1] = 0x00;} //temp = ((double)((int)buf[0] << 5) + ((int)buf[1] >> 3)) / 16; //13bitモード temp = ((double)((int)buf[0] << 8) + (int)buf[1]) / 128; //16bitモード return temp; } int main(void){ int fd, rtc, i, count; char str[32]; char REG[2] = {0x03,0x80}; //0x03レジスタに0x80を書き込む。 time_t time_now; time_t time_prev; struct tm *local; double temp; double temp_arr[DATA_NUM]; if (wiringPiSetup () == -1) return (1) ; /* LCDデバイスの初期化 */ if((fd = lcdInit(2,16,4, 10,11, 2,3,4,5,0,0,0,0)) < 0){ printf("[Error] open lcd device\n"); return(-1); } /* LCD表示テスト */ lcdClear(fd); lcdPosition(fd,0,0); lcdPrintf(fd, "NTP LCD_CLOCK"); sleep(2); lcdClear(fd); /*i2c温度センサーの初期化*/ if ((rtc = open("/dev/i2c-1", O_RDWR)) < 0){ printf("[Error] open i2c port\n"); return(-1); } if(ioctl(rtc, I2C_SLAVE, 0x48) < 0){ printf("[Error] access to 0x48\n"); return(-1); } if(write(rtc, REG, 2) != 2){ printf("[Error] set register 0x03\n"); return(-1); } /* 温度センサーの平均算出用データの取得 */ for(i=0; i<DATA_NUM;i++){ temp_arr[i] = get_temp(rtc); lcdClear(fd); lcdPosition(fd,0,0); lcdPrintf(fd, "Init. TEMP_COUNT=%d/%d", i+1, DATA_NUM); sleep(1); } /* メインループ(1秒間隔で情報を更新) */ count = 0; while(1){ time_now = time(NULL); /* 時刻が前回時刻から変化した場合、表示更新処理を実行 */ if(time_now != time_prev){ /* 時計表示ロジック */ local = localtime(&time_now); strftime(str, sizeof(str), "%Y/%m/%d %a %H:%M:%S", local); /* 温度表示ロジック */ count = (count + 1) % DATA_NUM; temp_arr[count] = get_temp(rtc); temp = 0; for(i=0;i<DATA_NUM;i++) temp += temp_arr[i]; temp = temp / DATA_NUM; /* LCDへ表示 */ lcdPosition(fd,0,0); lcdPrintf(fd, "%s %2.2fC", str, temp); time_prev = time_now; usleep(800*1000);//800msec処理を停止。上記処理が200msで終わらなくなったら修正。 } usleep(1*1000);//1msec処理を停止 } return(0); }
wsnakのブログ:I2C温度センサADT7410を使う(1)