畑から https://hatakekara.com Sun, 10 Mar 2024 00:56:47 +0000 ja hourly 1 https://wordpress.org/?v=6.4.3 https://hatakekara.com/wp-content/uploads/2019/12/cropped-stockfoto_40290447_S-32x32.jpg 畑から https://hatakekara.com 32 32 Raspberry pi WebRTC UV4L https://hatakekara.com/raspberry-pi-webrtc-uv4l/ Sat, 02 Mar 2024 01:46:54 +0000 https://hatakekara.com/?p=9453
]]>
UV4Lを理解する(2) https://hatakekara.com/raspberry-pi-webrtc-uv4l-02-2/ Sat, 02 Mar 2024 01:41:26 +0000 https://hatakekara.com/?p=9458 UV4L、ソケット通信、P2P通信、WebRTC 各々の関係が良く理解出来ていません。今回はこれらに付いて調べてみます。

そもそも、UV4Lとは

UV4LはUser space Video4Linuxの略です。Video4LinuxはLinuxで映像や音声を扱う時に使用するデバイスの1つです。Video4Linuxを元にそれに付加するプラグイン(例えば、UBSカメラを使う等)の集まり全体をUV4Lと言っているようです。UV4Lをインストールした時にインストールされたパッケージを以下に示します。

uv4lを理解する2

コマンドでインストールしたのは、uv4l、 uv4l-webrtc、 uv4l-uvc の3つ。 

  • uv4l:     Framework Core
  • uv4l-webrtc: WebRTC用のプラグイン
  • uv4l-uvc:   USBカメラ用のプラグイン

その他の4つは自動的にインストールされました。また、UV4Lに関する資料ですが下記を見つけました。

ソケット通信、P2P通信、WebRTC とは

  • WebRTC:”Web Real-Time Communication”
    • Webブラウザやモバイルアプリでリアルタイム通信を実現するAPI。
  • P2P通信:”Peer-to-Peer”
    • サーバを介さずに、端末同士で直接データを通信する技術。
    • WebRTCの通信方式として使用される。
  • ソケット通信
    • 通信プロトコルの1つ。
    • P2Pが確立する前にPC間で情報を交換する通信手段として使用。
  • 3者の関係は以下の様になっている
    • Webブラウザやモバイル間でリアルタイム通信を行いたい
      • ー> 通信方法として ”Peer-to-Peer” が便利
        • ー> P2P接続準備の為にソケット通信が必要
  • 実際にプログラムを書く時には
    • ソケット通信の作製 ー> P2Pの作製 ー> WebRTCの使用

ソケット通信(シグナルサーバ)

P2P接続する為に先ずお互いの情報を交換する必要が有ります。WebRTCを行いたいクライアント同士が特定のサーバにアクセスしサーバを通してお互いの情報を交換します。このサーバをシグナルサーバと呼びます。そして交換される情報は、Session Description Protocol (SDP)とICE Candidateの2つです。

  • Session Description Protocol (SDP)は以下の情報を含んでいます。
    • 通信するメディアの種類(音声、映像)、メディアの形式(コーデック)
    • IPアドレス、ポート番号
    • 暗号化の鍵 等
  • ICE Candidate
    • P2P通信を行う際に使われる通信経路の候補
    • どのような通信経路が使えるかは、お互いのネットワーク環境に依存。
    • 通信相互で経路の候補を挙げ、通信が繋がった時にその経路を使う。

シグナルサーバの説明は、WebRTC signalingに有ります。サーバとの通信はフォーマットが使用されます。

{
   what: "call",
   options: {
      force_hw_vcodec: true,
      vformat: 30,
      trickle_ice: true
   }
}
  • what:リクエストの内容を表す
    • call:  最初にサーバにアクセスする時に使用
    • offer: 自分の通信情報を相手に送りたい時に使用。
    • answer:相手からもらったデータに対する返答をする時に使用。
    • など…
  • options: 実際のデータ。

サーバとの通信はデータを送る時もサーバからデータを受信する時もこのフォーマットにしたがって行われます。

UV4Lとは

WebRTCを使用するためにはP2P接続を開始しする必要が有りますが、P2P接続が出来ればWebRTCを使用出来るとは限りません。WebRTCの実行にはWebRTCに対応したWebブラウザが必要です。今回Raspberry PIはCUIでOSをインストールしているのでWebブラウザを実行出来ません。Webブラウザを持たないRaspberry PIとWebRTCを実行出来るのはUV4Lがブラウザの代わりを務めていると言う事になります。

だいぶ状況が分かって来ました。ここで気付いたのですが、前回の最後で使用したRaspberry PIとのStreamingのHPでRaspberry PIのP2P接続部をそのまま使用すれば、それ以外の部分はカスタマイズ出来る事になります。具体的には

  • Raspberry PIとのStreamingのHPを表示
  • ブラウザの機能を使用してHPのソースコードを取得
  • ソースコードのRaspberry PIのP2P接続部をそのままに、その他の部分をカスタマイズ
  • 新しく出来たコードをRaspberry PIでは無くPCに保存
  • PCのブラウザでそのコードを実行すれば(ソースを拡張子HTMLで保存すれば、ファイルをクリックするだけ)Raspberry PIとのStreaming出来る。

となるはずです。という事でStreamingのHPのカスタマイズをやって見ます。

コードの簡略化

最終的にRaspberry PIを使ってインターフォン(もしくは監視カメラ)を作りたいので、その為に必要な部分を残してコードを簡略化して見ました。

  • ”remote”, “local” と2つ有った画像の表示を、”remote”のみにする。
  • ”Pause” , “Mute”, “Fullscreen”, “Recording” ボタンを廃止。
  • ”Call”、”Hangup” ボタンはそのまま残す。
  • 画面サイズの選択機能は廃止して、width=”640″ height=”480″に固定。
  • Cast local Audio/Video sources機能を廃止。
  • Data Channelsを廃止。
  • Advanced optionsで、”Trickle ICE”をtrueに固定。
  • その他コメントを廃止。

簡略化したコードはRaspberry PI 上では無くPCに”webrtc.html” として保存します。予めRaspberry PIでUV4Lを立ち上げPCに保存した”webrtc.html”をブラウザ(FireFox)を使って表示すると以下の様にちゃんと立ち上がりました。

オリジナルのコードは約1250行でしたが変更後のコードは約230行となりました。このコードでRaspberry PIとの映像と音声のStreamingが出来ることを確認しています。

webrtc.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>UV4L WebRTC</title>
        <script type="text/javascript">

            var ws = null;
            var pc;
            var audio_video_stream;
            var pcConfig = {/*sdpSemantics : "plan-b"*,*/ "iceServers": [
                    {"urls": ["stun:stun.l.google.com:19302", "stun:" + location.hostname + ":3478"]}
                ]};
            var mediaConstraints = {
                optional: [],
                mandatory: {
                    OfferToReceiveAudio: true,
                    OfferToReceiveVideo: true
                }
            };
            var iceCandidates = [];

            const constraints = {
                  audio: true,
                  video: false
            };

            function createPeerConnection() {
                try {
                    var pcConfig_ = pcConfig;
                    console.log(JSON.stringify(pcConfig_));
                    pc = new RTCPeerConnection(pcConfig_);
                    pc.onicecandidate = onIceCandidate;
                    pc.ontrack = onTrack;
                    pc.onremovestream = onRemoteStreamRemoved;
                    console.log("peer connection successfully created!");
                } catch (e) {
                    console.error("createPeerConnection() failed");
                }
            }

            function onIceCandidate(event) {
                if (event.candidate && event.candidate.candidate) {
                    var candidate = {
                        sdpMLineIndex: event.candidate.sdpMLineIndex,
                        sdpMid: event.candidate.sdpMid,
                        candidate: event.candidate.candidate
                    };
                    var request = {
                        what: "addIceCandidate",
                        data: JSON.stringify(candidate)
                    };
                    ws.send(JSON.stringify(request));
                } else {
                    console.log("End of candidates.");
                }
            }

            function addIceCandidates() {
                iceCandidates.forEach(function (candidate) {
                    pc.addIceCandidate(candidate,
                        function () {
                            console.log("IceCandidate added: " + JSON.stringify(candidate));
                        },
                        function (error) {
                            console.error("addIceCandidate error: " + error);
                        }
                    );
                });
                iceCandidates = [];
            }

            function onRemoteStreamAdded(event) {
                console.log("Remote stream added:", event.stream);
                var remoteVideoElement = document.getElementById('remote-video');
                remoteVideoElement.srcObect = event.stream;
            }

            function onTrack(event) {
                console.log("Remote track!");
                var remoteVideoElement = document.getElementById('remote-video');
                remoteVideoElement.srcObject = event.streams[0];
            }

            function onRemoteStreamRemoved(event) {
                var remoteVideoElement = document.getElementById('remote-video');
                remoteVideoElement.srcObject = null;
                remoteVideoElement.src = ''; // TODO: remove
            }

            function start() {
                if ("WebSocket" in window) {
                    document.getElementById("stop").disabled = false;
                    document.getElementById("start").disabled = true;
                    document.documentElement.style.cursor = 'wait';

                    var protocol = location.protocol === "https:" ? "wss:" : "ws:";
                    ws = new WebSocket(protocol + '//raspberrypi.local:8090/stream/webrtc');

                    ws.onopen = function () {
                        iceCandidates = [];
                        remoteDesc = false;
                        createPeerConnection();
                        var request = {
                            what: "call",
                            options: {
                                force_hw_vcodec: false,
                                vformat: "30",
                                trickle_ice: true
                            }
                        };
                        ws.send(JSON.stringify(request));
                        console.log("call(), request=" + JSON.stringify(request));
                    };

                    ws.onmessage = function (evt) {
                        var msg = JSON.parse(evt.data);
                        if (msg.what !== 'undefined') {
                            var what = msg.what;
                            var data = msg.data;
                        }
                        console.log("message =" + what);

                        switch (what) {
                            case "offer":
                                pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)),
                                        function onRemoteSdpSuccess() {
                                            remoteDesc = true;
                                            addIceCandidates();
                                            console.log('onRemoteSdpSucces()');
                                            pc.createAnswer(function (sessionDescription) {
                                                pc.setLocalDescription(sessionDescription);
                                                var request = {
                                                    what: "answer",
                                                    data: JSON.stringify(sessionDescription)
                                                };
                                                ws.send(JSON.stringify(request));
                                                console.log(request);

                                            }, function (error) {
                                                alert("Failed to createAnswer: " + error);

                                            }, mediaConstraints);
                                        },
                                        function onRemoteSdpError(event) {
                                            alert('Failed to set remote description (unsupported codec on this browser?): ' + event);
                                            stop();
                                        }
                                );
                                break;

                            case "iceCandidate": // when trickle is enabled
                                if (!msg.data) {
                                    console.log("Ice Gathering Complete");
                                    break;
                                }
                                var elt = JSON.parse(msg.data);
                                let candidate = new RTCIceCandidate({sdpMLineIndex: elt.sdpMLineIndex, candidate: elt.candidate});
                                iceCandidates.push(candidate);
                                if (remoteDesc)
                                    addIceCandidates();
                                document.documentElement.style.cursor = 'default';
                                break;
                        }
                    };

                    ws.onclose = function (evt) {
                        if (pc) {
                            pc.close();
                            pc = null;
                        }
                        document.getElementById("stop").disabled = true;
                        document.getElementById("start").disabled = false;
                        document.documentElement.style.cursor = 'default';
                    };

                    ws.onerror = function (evt) {
                        alert("An error has occurred!");
                        ws.close();
                    };

                } else {
                    alert("Sorry, this browser does not support WebSockets.");
                }
            }

            function stop() {
                if (audio_video_stream) {
                    try {
                        if (audio_video_stream.getVideoTracks().length)
                            audio_video_stream.getVideoTracks()[0].stop();
                        if (audio_video_stream.getAudioTracks().length)
                            audio_video_stream.getAudioTracks()[0].stop();
                        audio_video_stream.stop(); // deprecated
                    } catch (e) {
                        for (var i = 0; i < audio_video_stream.getTracks().length; i++)
                            audio_video_stream.getTracks()[i].stop();
                    }
                    audio_video_stream = null;
                }
                document.getElementById('remote-video').srcObject = null;
                document.getElementById('remote-video').src = ''; // TODO; remove
                if (pc) {
                    pc.close();
                    pc = null;
                }
                if (ws) {
                    ws.close();
                    ws = null;
                }
                document.getElementById("stop").disabled = true;
                document.getElementById("start").disabled = false;
                document.documentElement.style.cursor = 'default';
            }

        </script>
        <style>
            video {
                background: #eee none repeat scroll 0 0;
                border: 1px solid #aaa;
            }
        </style>
    </head>
    <body>
        <h1><span>WebRTC two-way Audio/Video Intercom</span></h1>
        <video id="remote-video" autoplay="" width="640" height="480">
            Your browser does not support the video tag.
        </video><br>
        <button id="start" style="background-color: green; color: white" onclick="start();">Call!</button>
        <button disabled id="stop" style="background-color: red; color: white" onclick="stop();">Hang up</button>
    </body>
</html>

コードの説明

コードの開始はHP上の、”CALL” ボタンを押した所から始めます。

  • ”CALL” ボタンは229行で定義されており、これをクリックすると91行の”start()”へ飛びます。 
  • 91行: ”start()”関数はここから始まる。
  • 92行: ”WebSocket”が使えるか判断。使えれば次は、使えなければ182行へ
  • 98行: 新たなソケット通信を作成。Raspberry PIのシグナルサーバとの接続開始。
  • 100行 :  シグナルサーバとの接続が出来た時点でここが実行される。
  • 103行: P2P接続に必要な処理を開始。詳細は後ほど説明します。
  • 104行: Raspberry PIのシグナルサーバに送るデータの製作
  • 112行: ソケット通信でシグナルサーバにデータを送信
    • このデータをシグナルサーバに送信でP2P接続の準備が開始されます
    • 送っているデータは、Whatが”call”。内容は、画面の解像度やtrickle_iceのオンオフものです。
    • サーバは接続を要求しているクライアントを認識し自信のSDPデータを”offer”とてクライアントに送信します。
  • 116行: シグナルサーバからメッセージが送信されるとここが実行される。
  • 124行: メッセージの解析はここから
    • 先ずRaspberry PI側から”offer”として送られるのでPC側で”answer”の処理は無し
    • ICE Candidateの送信方法を trickle_ice: true としている。これは ”iceCandidate” で処理される
    • 125行: ”offer”の場合の処理。
      • ICE Candidateの追加
      • ”offer”に対する”answer”の送信
    • 152行: ”iceCandidate”の場合の処理。
      • “iceCandidate”の登録が行われます。
  • 何回かICE Candidateのやり取りの後、お互いの条件が合えばP2P接続が開始される。

130行で実行した関数 createPeerConnection(); により以下が並行して行われる。

  • 28行:createPeerConnection()関数。P2P接続に必要な処理を行う。
    • 32行: 新しいP2P通信、PCを作成。
    • 33行: icecandidateイベントが起きたら、onIceCandidate()を実行
    • 34行: trackイベントが起きたら、onTrack()を実行
    • 35行: removestreamイベントが起きたら、onRemoteStreamRemoved()を実行
  • 42行:onIceCandidate(event)。ICE Candidateを相手(Raspberry PI)に送信
    • 32行のRTCPeerConnection(); が実行されると自信のICE Candidateの作製が始まる。
    • ICE Candidateが出来るとトリガーがかかりここが実行される。
    • ”what”ラベルに、”addIceCandidate”。”data”ラベルに、ICE CandidateをセットしRaspberry PIに送信する。
  • 59行: addIceCandidates() ICE Candidateの追加
  • 79行: onTrack(event)。
    • 82行:ここでRaspberry PIから送られて来たデータ(画像と音声)をHTML画面に接続。

その他の説明

  • ”Hang up”ボタンは230行で定義。クリックすると、187行の”stop();”が起動。
  • 187行: stop()。通信の停止作業を行う
    • Streamの停止。
    • ソケット通信の停止
    • P2P通信の停止。

プログラムの実行

最後に実際にプログラムを実行して見ます。実行は以下の様に行いました。

  • Raspberry PI側:
    • UV4Lの起動が必要です。
    • 前回の ”UV4Lを使う(1)” 通りにセットアップしていればRaspberry PIの起動と共にUV4Lも起動します。
  • PC側:
    • PCに保存されたHTMLのコードファイルを実行(ファイルをクリック)します。
    • 今回ブラウザはFireFoxを使っています。
  • 実行結果:
    • FireFoxのURL欄にコードのPathを入力すると下記の画面が表示されます。
    • 画面下の、”Call”ボタンをクリックするとStreamingが始まります。
    • もちろん、音声もStreamingされています。
    • ”Hang Up”ボタンをクリックするとStreamingが終了します。

次は

P2Pの確立する仕組がまだ良く理解出来ていません。ただ、Raspberry PI HPのソースコードでその部分をそのまま使えばP2Pが繋がりWebRTCが出来ること。ソースコードもP2P接続以外の部分は変更出来る事が分かりました。HPのHTMLを書き換えれば自分のニーズに有ったHPが作れる事が分かったのは大きな収穫です。

現時点で音と映像が送られてくる監視カメラレベルになっています。これでPCからRaspberry PIに音が送れれば立派なインターフォンになります。次回はこれに挑戦します。

]]>
Raspi Zero(w)でWebラジオ(本体プログラム) https://hatakekara.com/raspi-zero-radio-2/ Fri, 23 Feb 2024 08:16:40 +0000 https://hatakekara.com/?p=9400 今回は本体プログラムを書いて行きます。

下準備

  • プログラム言語
    • Pythonを使用します。
    • Raspi Zero-W OSインストール時に既にインストールされています。
  • HTTPサーバー
    • Raspi Zero-WにHTTPサーバ立ててラジオの管理を行います。
    • python_HTTP_Serverで説明されているサーバを元にしています。
  • HTTPクライアント
    • Webラジオのサイトにアクセスする為にHTTPクライアントの機能が必要です。
    • 今回は requests を使用しています。
    • requestsのインストール
      • requestsのインストールの前に PIP をインストールする必要が有ります。
        • PIPのインストールはモニターで sudo apt install pip を実行
      • その後 requestsのインストール
        • モニターで pip install requests を実行
  • GPIO
    • Raspberry PI最大の特徴であるIOポート。
    • 今回はPilot LEDの点滅用に使用しています。
    • 簡単は説明はここに有りますー>GPIO
  • IPアドレスの固定
    • 現在の環境ではスマホからRaspi Zero-Wにアクセス時DNSが効きません。
    • そこでRaspi Zero-WのIPアドレスを 192.168.3.245 固定しました。
    • 固定方法はここを参照下さい ー> IPアドレスを固定する

プログラムの説明

使用したプログラム一覧

プログラムはRaspi Zero-Wのホームディレクトリの下に radio フォルダを作りそこに保存しています。

  ファイル名        説明
1 radio_player.py本体プログラム。Pythonで書いています。
2 radiko.js , radiko_rec.js Java Script。 サーバーのHPで使用
3 radiko.cssサーバーHPのCSSファイル
4 list.txt , state.txt放送局リストとラジオの状態を保存したファイル
5 favicon.icoファビコンファイル
6 その他録音したファイル
1から5までが使用しているファイルです。その他はサンプルとしてこのアプリで録音したファイルです。
ここに今回使用したファイル一覧を保存しました。

アプリの説明

アプリは Player Recorder Editor の3部構成になっています。

Player

サーバーへはブラウザにIPアドレス 192.168.3.245:8080 でアクセス出来ます。(ポートも8080に指定しています)。サーバーにアクセスするとこの画面が最初に表示されます。 

このプログラムはこれを参考に書いています ー> Radikoを聞く(Arduino編) ESP32で書いたプログラムをRaspi Zero-Wで書き換えました。デコーダーとしてFFPlay、Mplayerを使用したのですが、これらが強力でRadikoだけでは無く他のラジオ局も簡単に聞けるようになりました。

  1.   サーバーのIPアドレスとポートを192.168.3.245:8080に固定してます。
  2.  ボリュームは0が最小 100が最大です。スライドバーで調整出来ます。
  3. 4 リスト選択とラジオ局リスト
    • リストはRadiko(上の段)とそれ以外のラジオ局(下段)
    • Radikoは東京エリアで聞ける15局が登録されています。
    • それ以外の局は後で説明する Edit機能で追加削除出来ます。
  1. 6,7 開始、ミュート、停止ボタン
  1.  Recorder(録音の機能)へ移動
  2.  Editor(ラジオ局の追加削除)
  3. 11 AutoチェックとSaveボタン
    • Saveボタン:ラジオの状態を保存。次回同じ状態で立ち上がります。
    • Autoがチェックされた状態で保存すると次回その局が自動再生されます。
  1.  終了ボタン。アプリを終了しRaspi Zero-Wの電源を落とします。

Recorder

ラジオを録音する録音部とそれを再生する再生部の構成。

  • 録音部
    • 対応しているのは ラジコ タイムフリー のみです。
    • ラジコ タイムフリー のルールに従って項目を指定して下さい。
      1.  このリストで録音したいRadikoのラジオ局を選択
      2.  開始時間の設定 各欄は 年、月日、時間設定です。
        • 年: 4桁で設定
        • 月日: 月日共に2桁で指定して下さい。 1月ー>01 5日ー>05
        • 時間: 時分秒の順で設定します。ここも各項目2桁で指定
      3.  終了時間ー>開始時間と同じ様に設定
      4.  ファイルネーム
        • このファイルネームで保存されます
        • 名前のみで拡張子は付けないで下さい。
      5.  録音開始
        • 録音が終了するとボタンの状態が戻ります。
  • 再生部
    • 上記で録音したファイルを再生します。
    • 追加機能として録音したファイルのダウンロード、削除も出来ます。
      1.  ボリューム
      2.  ファイルリスト
        • ここに録音されたファイルのリストです。
        • ここで再生したいファイルを選びます。
      3.  再生用、Play、Mute、Stop ボタン
        • Playerと同じ機能です
      4.  ファイルリストで選んだファイルを削除します。
      5.  ファイルリストで選んだファイルをダウンロードします。
      6.  Playerに戻ります。

Editor

  • ラジオ局の追加と削除を行います。
  • ラジコは自動的に登録されるので、それ以外のラジオ局を登録します。
  • 登録されたラジオ局はPlayerのラジオ局リストの下の段に表示されます。
  • 10局の放送局を登録出来ます。
  • 左側Title欄がプルダウンリストに表示されます。ここにラジオ局のタイトルを入力。
  • 右側URL欄のラジオ局のリンク先入力ですが、
    • SHOUTcastの場合
      • SHOUTcast のHPの行く       
      • 聞きたい局のDownLoadマークをクリック
      • 出て来たウインドウで、”Any player(.m3u)の上にマウスを置いて右クリック
      • 出て来たウインドウで、”URLのコピー” を選択。
      • コピーされたURL ”http://yp.shoutcast.com/sbin/tunein-station.m3u?id=22146” の前に、”-playlist “を付けた”-playlist http://yp.shoutcast.com/sbin/tunein-station.m3u?id=22146” を入力
      • SHOUTcastは最後の数字で放送局を区別している様です。
    • サイマルラジオの場合
      • HPで音声のみのアイコンの上で右クリック
      • リンクのURLのコピーを選ぶ
      • コピーされたURLが、http://www.simulradio.info/asx/fmpalulun.asxと .asxで終わるURLをそのまま入力
        • ー> サイマルラジは聞けない局が結構有りました。
  • 入力後、OKボタンを押せばリストが更新され、Backボタンを押せばリストの更新は無くPlayerに戻ります。

自動起動

最後にRaspi Zero-Wに電源を入れたらこのプログラムが自動的に立ち上がる設定にします。立ち上がりと同時にプログラムを実行させる方法をここで説明しています。ー> CUIで起動時にプログラムを自動実行させる方法 今回はこの中の 2. .bashrc を使いました。

  • Raspi Zero-WをCUI Auto Loginモードに設定します。
  • .bashrcを編集する。
    • .bashrcファイルの最後に 
      • if [ $(tty) == ‘/dev/tty1’ ]; then /home/pi/radio/python radio_player.py; sudo shutdown -h now; fi 
      • を追加しました。
    • /home/pi/radio/python radio_player.py; ー> 本体プログラムを実行
    • sudo shutdown -h now; ー> 本体プログラム終了後 シャットダウンを行う。

OSのインストールから実行までのおさらい

  • ハード関係
    •  RUNスイッチの追加
    •  Pilot LEDの追加
    •  USBスピーカの接続
  • ソフト関係 
    •  Raspberry Pi imagerでインストール用SDを作製
    •  Raspi Zero-WにSDカード入れてOSをインストール
    •  インストール完了後、DeskTop_PCからSSHでRaspi Zero-Wに接続
    •  日本語化、スピーカ接続を行う
    •  ホームの下にradioフォルダを作製しSambaをインストール
    •  ffmpeg, mplayerのインストール
    •  requestsのインストール
    •  Raspi Zero-W IPアドレスの固定
    •  radioフォルダにプログラム一式をコピーする。
    •  Raspi Zero-WをCUI Auto Loginモードにする
    •  .bashrcを編集
    •  モニターで sudo shutdown -h now を実行する
    •  Raspi Zero-W上のLEDが消えたらRUNスイッチを押す
    •  Raspi Zero-W上のLEDが点滅ししばらくするとPilot LEDが点灯します。
    •  アプリがレディーになるとJ-WAVEが自動で再生されます。
    •  DeskTop_PCのブラウザから 192.168.3.245:8080と入力しサーバにアクセス。Playerが立ち上がります。
    •  アプリのENDボタンを押すと終了しRaspi Zero-Wのパワーが落ちます

最後に

ESP32で書いたRadikoラジオをRaspi Zero-Wでと思い始めました。ESP32と違いRaspberry PI には多くの関数や機能が有りESP32より色んな事が比較的簡単に出来ました。ただ、Raspi Zero-Wは起動とシャットダウンに時間がかかるのが玉にキズって感じでしょうか。

]]>
CUIで起動時にプログラムを自動実行させる方法 https://hatakekara.com/raspi-auto-start/ Wed, 21 Feb 2024 09:18:29 +0000 https://hatakekara.com/?p=9411 CUIで起動時にプログラムを自動実行させる方法として下記4つ有ります。

  1.  /etc/rc.local
  2.  .bashrc
  3.  cron (crontab)
  4.  systemd

1.  /etc/rc.local

自動起動したいコマンドやスクリプトを/etc/rc.localに書くとにより、起動時にそれを実行出来ます。下記はエディターで/etc/rc.localを開いたものです。ファイルの最後に exit0 。その上に fi が有りますこの2行の間に実行したいプログラムを書きます。この場合は起動時に /home/pi/radioフォルダーのradio.sh が実行されます。

2. .bashrc

これはシェルを起動する時に読み込まれるファイルです。ユーザーのホームディレクトリーにある .bashrc を編集します。ファイル中にこれを追加ー>  if [ $(tty) == ‘/dev/tty1’ ]; then xxxxxxxx; yyyyyyyy; fi

  • $(tty) == ‘/dev/tty1’ : これでCUIが上がったらとなる様です。
  • then の後: ここに実行したいコマンドを書きます。コマンドは複数指定出来ます。

追加後Rebootして下さい。コマンドが実行されます。

3. cron (crontab)

これは定期的にプログラムを走らせたい時に使うアプリケーションです。@reboot を使うと起動した時にコマンドを実行出来ます。モニターで crontab -e を実行し、開いたファイルの最後に @reboot xxxxxxxx (xxxxxxはコマンド) を追加します。これでRebootすれば起動時にコマンドが実行されます。

4. systemd

ここに説明が有ります ー> systemdを使ったプログラム自動起動 参照下さい。

]]>
Boot / Auto Loginの設定 https://hatakekara.com/raspi-auto-login/ Wed, 21 Feb 2024 04:10:22 +0000 https://hatakekara.com/?p=9403 モニターから sudo raspi-config で設定出来ます。

raspi-configで 1 System Options ー>S5 Boot / Auto Login と進むと設定画面が表示されます。ここで希望の状態を選択します。最後にRebootするか聞かれます。Rebootして設定終了です。

]]>
Raspi Zero(w)でWebラジオ https://hatakekara.com/raspi-zero-radio-menu/ Tue, 20 Feb 2024 23:49:03 +0000 https://hatakekara.com/?p=9369
]]>
Raspi Zero(w)でWebラジオ(プログラム開発の準備) https://hatakekara.com/raspi-zero-w-radio-1/ Tue, 20 Feb 2024 23:47:15 +0000 https://hatakekara.com/?p=9356 Raspi Zero-Wと Raspi Zero-2W を買い、Raspi Zero-Wで遊んでいます。

びっくりするのはその大きさ。板ガムくらい(巷ではフリスク位って言うみたい)の大きさです。性能的にはRaspi b+(OSインストールの時にいちごマークが1個しか出ないやつ)と同じ位。試しにGUIのOSをインストールしたのですが、Raspi b+同様レスポンスが遅くとても使う気になれませんでした。そこでOSをCUIに変えてWebラジオを作って見ることにしました。

Raspi Zero-WにHTTPサーバーを上げ、PCのブラウザからサーバのHPにアクセス。そのHPでラジオをコントロールします。サーバーのHPにはラジオ管理用に下記の3画面が有ります。

  • Player: メイン画面。基本はラジオ局を選択しそれを聞く。この画面からRecorder/Editorを実行する
  • Recorder: ラジオ放送を録音する。 録音出来るのはRadikoのみ。 録音した放送の再生とDownloadが出来る。
  • Editor: 聞きたいラジオ局を追加する。 

1.OSのインストール

最近は Raspberry Pi imagerが有るのでOSのインストールは簡単です。

  1.  Raspberry Piデバイスの選択
    • リストから Raspberry PI Zero を選ぶ
  2.  OSの選択
    • リストから Raspberry PI OS(Legacy, 32−bit)Liteを選ぶ
  3.  ストレージ選択
    • 今回は32GのSDカードを使用しました。
  1.  ストレージ選択の下の 次へ をクリック
  2.  OS customization?が表示されます。ここで設定を編集する を選択
  3.  OS customization 一般
    • ホスト名:”rasp” としました。
    • ユーザー名:”pi” としました。
    • WiFiを設定する。
      • WiFiを使ってDeskTop_PCとSSHで接続します。
      • WiFiのデータを入力します
    • ロケールは
      • タイムゾーン:Asia/Tokyo
      • キーボード: jp
  4.  サービスのタブで 
    • SSHを有効化するにチェック
    • パスワード認証を使うにチェック
    • 最後に保存をクリック
  1.  ここで ”はい” を押す
  2.  ここでも ”はい” を押す
  3.  書き込みが始まりました。しばらく待っていると
  4.  書き込み終了の画面が表示されます。SDカードを取り出します。

この後はSDカードをRaspi Zero-Wにセットします。今回はWiFiを使ってSSH接続を行うのでRaspi Zero-Wには、キーボード、マウス、LCD等は接続していません。Raspi Zero-WのUSBに電源を供給してOSのインストールが始まります。

LCDをつないでいないのでインストールの状況が分かりません。Raspi Zero-Wの本体に実装されている緑のLEDが頼り。こまめに点滅している間はインストール中です。しばらくすると(10分位待つと)LEDの点滅が無くなり、点灯(点きっぱなし)となります。これがインストール終了の目安です。

  1.  PC側でモニターを上げて ssh pi@rasp.local と入力します
  2.  実はこのインストール前に違うPCとSSH接続を行っていた為、接続が変わった旨のメッセージがでました。
    • 今回が初めての場合はこのメーッセージと下記の14,15は表示されないはずです。
    • ssh-keygen -f “/home/op7060/.ssh/known_hosts” -R “rasp.local” を入力して仕切り直し
  3.  改めて ssh-keygen -f “/home/op7060/.ssh/known_hosts” -R “rasp.local” を入力
  4.  コマンドプロンプトが帰って来た所で、再度 ssh pi@rasp.local を入力
  5.  今度は続けるか聞いているので yes と入力 
  6.  ここでパスワードを入力すると
  7.  Raspi Zero-Wにつながりました。

ひたすらLEDを見てるだけのインストールでしたがOSは正常にインストールされいる様です。

2.OSの日本語化

OSインストールの”OS customization?”で日本語の設定にしたのですが、モニターに表示されるメッセージが英語表示になっています。これを日本語表示に変更します。Raspberry PI の日本語表示を参考にして下さい。Webラジオプレーヤーに日本語はいらないかもしれませんが取り敢えず日本語表示にして置きます。

3.スピーカーの接続

今回スピーカにSANWAのUSBスピーカーを使用したのですが、これ接続にかなり苦労しました。ALSAで音は出せるのですがCUIから音量調整が出来無いのです。このスピーカーにはボリュウムが付いているのでそれを回せば音量の調整は出来ますがRaspi Zero-Wからも調整したい所です。ALSAの代わりにPulseAudioを使用するとCUIから音量を調整出来る事が分かりました。詳細はここを参照下さいー>Soundの設定

4.Sambaのインストール

Raspi Zero-WにSambaをインストールしてDesk_Top_PCとの間でファイルを共有します。プログラムの開発はPC。Raspi Zero-Wは実行のみ。Raspi Zero-WのEditorはDesk_Top_PCのそれに比べ非力なのでこの方法で開発効率が上がります。Sambaのインストールはここを参照下さいー>Sambaのインストール

Sambaインストールにあたり、

  • 共有ファルダーとして、Raspi Zero-Wのホームディレクトリの下に ユーザー権限で ”radio” を作製しています。
  • /etc/samba/smb.confの最後に今回の設定に合わせて下記を追加しています。
    • path = /home/pi/radio  => 今回作製したフォルダー
    • force user = pi  => OSのインストールでユーザーを ”pi” としています。違うユーザーでインストールした場合はそれに合わせて下さい。

[raspberry_pi]
comment = Share
path = /home/pi/radio
public = yes
read only = no
browsable = yes
force user = pi

5.メディアプレーヤーのインストール

ラジオの録音に ffmpeg 。 再生にmplayerを使います。予めこれらをインストールして置きます。モニター画面で

ffmpegのインストール: sudo apt install ffmpeg

mplayerのインストール: sudo apt install mplayer

を実行すればインストール出来ます。

6.ハードの追加

リセットスイッチとPilot LEDを追加しました。

  • リセットスイッチ
    • Raspi Zero-Wカメラコネクターの横に RUN と印刷された端子が有ります。
    • この端子をショートするとRaspi Zero-Wにリセットがかかります。
    • Raspi Zero-Wの立ち上げとShutDown
      • 電源コネクターから電源を供給すると少ししてRaspi Zero-Wが立ち上がる。
      • モニターでShutdownをかけるとRaspi Zero-Wが落ちてLEDが消灯
      • この状態で端子をショートすると再びLEDが点滅を初めRaspi Zero-Wが立ち上がる
    • USBコネクターの抜き差しで電源のオンオフを行っていたのでそれに比べればかなりスマート。
    • Shutdown後パワセーブモードになってこのスイッチはリセットをかけているのみと思われます。
      • Raspi Zero-W起動時にこのスイッチを押すと強制的にリセットがかかります(Rebootします)
      • このスイッチで電源のオンオフは出来ません。
  • Pilot Led
    • Raspi Zero-WにもLEDは有るのですが、ラジオの状態表示用にLEDを追加しました。
    • GPIOポートの23にLEDをつなぎ1KΩの抵抗を通してGNDに接続しています。

次回は

これでプログラム開発の準備が出来ました。次回は実際にプログラムを書いて行きます。

]]>
Raspberry PI の日本語表示 https://hatakekara.com/raspi-display-japanese/ Sun, 11 Feb 2024 12:07:05 +0000 https://hatakekara.com/?p=9329 OSインストール時に国指定を日本としたのですが日本語表示になっていません。今回は日本語が表示出来る様にします。

  1.  raspi-configを上げる
  2.  5 Location Optionsを選択
  3.  L1 Locateを選択
  1.  左下の青い枠の中から使用言語(ja_JP.UTF-8)を選択するのだが
  2.  途中に有る(en_GB.UTF-8 UTF-8)が選択されているのでこれを外す(外さなくてもOK?)
  3.  ja_JP.UTF-8 UTF-8を選択
  4.  最後にもう一度 ja_JP.UTF-8 UTF-8を選択して OKを押します
  5.  表示がモニター画面に変わり、Fontが読み込まれます。
  6.  しばらくするとraspi-configの画面に戻ります。戻ったらそのまま終了します。

再起動して下さい。立ち上がったら モニターから ls a と入力して下さい。ちゃんと日本語になっています。

]]>
Soundの設定 https://hatakekara.com/ras-pi-sound/ Sun, 11 Feb 2024 05:06:48 +0000 https://hatakekara.com/?p=9279 Raspberry piにスピーカやヘッドフォンをつないでそれを有効にする手順の説明です。

大抵の設定は raspi-config で

大抵の設定は raspi-configで出来ます。何も接続されていないrasperry pi zero Wで モニター画面からsudo raspi-config と入力し 1 System Options ー> S2 Audio と進むと現在のAudio outputのリストが表示されます。

VC4-hdmiのみ表示されます。スピーカーを持ったHDMIインターフェイスのディスプレーに接続すればそこから音が出る状態になってます。

次にUSBハブを使ってUSBスピーカー、ヘッドフォンの2つを同時に接続し同様にAudio outputリストを見ると下記の様にリストアップされます。

この画面で矢印キーを使って音を出したい機器を選択すればその機器がアクティブになります。ここでは仮に0のヘッドフォンを選択します。

確認

Raspberry piは音声入出力管理モジュールとして ALSAがデフォルトでインストールされます。このコマンドを使用して機器の確認が出来ます。先ずは選択した機器からサンプル音源を出力して見ます。サンプル音源は、/usr/share/sounds/alsa/に保存されています。その中の1つをaplayコマンドを使って再生して見ます。音声が聞こえたと思います。

aplay

pi@ras:~ $ ls /usr/share/sounds/alsa/
Front_Center.wav  Noise.wav        Rear_Right.wav
Front_Left.wav    Rear_Center.wav  Side_Left.wav
Front_Right.wav   Rear_Left.wav    Side_Right.wav
pi@ras:~ $ aplay /usr/share/sounds/alsa/Front_Center.wav
Playing WAVE '/usr/share/sounds/alsa/Front_Center.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono
pi@ras:~ $ 

聞こえなかった場合、機器のリュームが小さいからかもしれません。モニターに alsamixerコマンドを入力すると下記のボリュウム設定画面が表示されます。

ヘッドフォンのボリュームは45(左右)となっているのでしっかり聞こえました。

ボリュームはコマンドラインからも調整出来ます。モニターに amixer と入力すると現在の状態が表示されます

amixer

i@ras:~ $ amixer 
Simple mixer control 'Headphone',0
  Capabilities: pvolume pswitch pswitch-joined
  Playback channels: Front Left - Front Right
  Limits: Playback 0 - 38
  Mono:
  Front Left: Playback 30 [79%] [-20.00dB] [on]
  Front Right: Playback 30 [79%] [-20.00dB] [on]
Simple mixer control 'Mic',0
  Capabilities: pvolume pvolume-joined cvolume cvolume-joined pswitch pswitch-joined cswitch cswitch-joined
  Playback channels: Mono
  Capture channels: Mono
  Limits: Playback 0 - 127 Capture 0 - 16
  Mono: Playback 64 [50%] [11.99dB] [off] Capture 8 [50%] [11.90dB] [on]
Simple mixer control 'Auto Gain Control',0
  Capabilities: pswitch pswitch-joined
  Playback channels: Mono
  Mono: Playback [on]
pi@ras:~ $ 

Simple mixer control ‘Headphone’,0の’Headphone’を使ってボリュームを調整します。例えば、ボリュームを50%にしたければ amixer sset Headphone 50% と入力します(最大:100% 最小:0%)

amixer sset

pi@ras:~ $ amixer sset Headphone 50%
Simple mixer control 'Headphone',0
  Capabilities: pvolume pswitch pswitch-joined
  Playback channels: Front Left - Front Right
  Limits: Playback 0 - 38
  Mono:
  Front Left: Playback 19 [50%] [-47.50dB] [on]
  Front Right: Playback 19 [50%] [-47.50dB] [on]
pi@ras:~ $ 

確かにヘッドフォンの音量が変化しました。

実は

raspi-config で音声出力機器を選択するとホームディレクトリに .asoundrc というファイルが出来ます

ls -al

pi@ras:~ $ ls -al
total 36
drwxr-xr-x 4 pi   pi   4096 Feb 10 19:23 .
drwxr-xr-x 3 root root 4096 Dec  5 11:39 ..
-rw-r--r-- 1 root root  215 Feb 11 08:59 .asoundrc
-rw-r--r-- 1 pi   pi    220 Dec  5 11:39 .bash_logout
-rw-r--r-- 1 pi   pi   3523 Dec  5 11:39 .bashrc
-rw-r--r-- 1 pi   pi    807 Dec  5 11:39 .profile
pi@ras:~ $

内容は以下の通り

.asoundrc

pi@ras:~ $ cat .asoundrc
pcm.!default {
  type asym
  playback.pcm {
    type plug
    slave.pcm "output"
  }
  capture.pcm {
    type plug
    slave.pcm "input"
  }
}

pcm.output {
  type hw
  card 0
}

ctl.!default {
  type hw
  card 0
}
pi@ras:~ $ 

16,21行に card 0 という項目が有り、これが現在選択されたいる音声カードの様です。card 0の”0”は raspi-configのAudio outputリストの番号に対応している様です。現在0のヘッドフォンを選択しているので ”0”となります。

ちなにみ1のスピーカをraspi-configで選べば、16,21行は card 1 となります。ALSAは起動時にこのファイルを読み込んで機器の状態を判断している様です。root権限で .asoundrc を編集すれば(0を1に又は 1を0に)raspi-configを起動しないでデフォルトの機器を変更出来ます。

PulseAudioのインストール

raspi-configで1のUSBスピーカを選択すると音は出るのですが音量の調整が出来ない事が分かりました。ALSAではこのスピーカを設定出来ないようです。そこで PulseAudioを試してみる事にしました。

インストールは sudo apt-get install pulseaudio pulseaudio-module-zeroconf avahi-daemon

自動起動させるには、

  • pulseaudio の設定ファイル /etc/pulse/daemon.confで
    • local-server-type = userの行がコメントになっている事を確認
  • /systemd 用の設定ファイル /lib/systemd/system/pulseaudio.serviceを製作し保存
    • 内容は以下の通り。
lib/systemd/system/pulseaudio.service

[Unit]
Description=pulseaudio sound server
[Service]
Type=forking
PIDFile=/var/run/pulse/pid
ExecStart=/usr/bin/pulseaudio --system --daemonize
[Install]
WantedBy = multi-user.target
  • 設定の有効
    • sudo systemctl enable pulseaudio.service
    • sudo systemctl start pulseaudio
    • pulseaudio -D

pulseaudoが起動したので機器の設定を行います。

  • pulseaudioが認識している機器を表示
    • pacmd list-sinks | grep name: を実行
    • スピーカーとヘッドフォンが認識されている事が分かります。
pacmd list-sinks | grep name:

pi@ras:~ $ pacmd list-sinks  | grep name:
	name: <alsa_output.usb-C-Media_INC._USB_Sound_Device-00.analog-stereo>
	name: <alsa_output.usb-Sennheiser_Communications_Sennheiser_USB_headset-00.analog-stereo>
pi@ras:~ $ 
  • デフォルト機器の設定
    • pacmd set-default-sink を使用。
    • 今回はUSBスピーカを指定したいので
    • pacmd set-default-sink alsa_output.usb-C-Media_INC._USB_Sound_Device-00.analog-stereo
  • ボリュームの設定
    • pacmd set-sink-volume @DEFAULT_SINK@ XXXXX を使用
    • XXXXXはボリュームの大きさで 最大値:0x10000 最小値:0

pulseaudioではヘッドフォンもスピーカもボリュームの設定が出来ました。

この状態でホームディレクトリをみると

ls -al

pi@ras:~ $ ls -al
total 28
drwxr-xr-x 3 pi   pi   4096 Feb 11 14:45 .
drwxr-xr-x 3 root root 4096 Dec  5 11:39 ..
-rw-r--r-- 1 root root  215 Feb 11 14:27 .asoundrc
-rw-r--r-- 1 pi   pi    220 Dec  5 11:39 .bash_logout
-rw-r--r-- 1 pi   pi   3523 Dec  5 11:39 .bashrc
drwx------ 3 pi   pi   4096 Feb 11 14:45 .config
-rw-r--r-- 1 pi   pi    807 Dec  5 11:39 .profile
pi@ras:~ $ 

新しく、.configフォルダ が作製されその下に pulseフォルダ が有りその中には以下のファイルが有りました。

~/.config/pulse ls

pi@ras:~/.config/pulse $ ls
9de95719478749a3b320703beb1ccb8e-card-database.tdb  9de95719478749a3b320703beb1ccb8e-device-volumes.tdb
9de95719478749a3b320703beb1ccb8e-default-sink       9de95719478749a3b320703beb1ccb8e-stream-volumes.tdb
9de95719478749a3b320703beb1ccb8e-default-source     cookie
pi@ras:~/.config/pulse $ 

最後に

音声出力の選択は raspi-config で行えば良い。コマンドラインからボリュームを調整しようとすると機器に合わせてALSA か Pulseaudioを使用する。でしょうか。

]]>
IPアドレスを固定する https://hatakekara.com/pi-ip-address-fix/ Fri, 02 Feb 2024 10:04:31 +0000 https://hatakekara.com/?p=9269 Raspberry PIのIPアドレスを固定するには /etc/dhcpcd.conf を編集します。エディターでファイルを開くとファイルの中にIPアドレス固定の例が有ります。

dhcpcd.conf

#

# Example static IP configuration:
#interface eth0
#static ip_address=192.168.0.10/24
#static ip6_address=fd51:42f8:caae:d92e::ff/64
#static routers=192.168.0.1
#static domain_name_servers=192.168.0.1 8.8.8.8 fd51:42f8:caae:d92e::1

# 
  • 文頭の#を外して使います。
  • 4行: 有線なら:interface eth0。 無線なら:interface wlan0。
  • 5行: 希望IPアドレス
  • 6行: 希望IPアドレス(IP6の場合)
  • 7行: Gatewayアドレス
  • 8行: DNSアドレス

例えば、無線でIPアドレスを192.168.3.245に固定したい場合、下記の様になります(routersとdomain_name_serversは自宅のルータに合わせて下さい)

dhcpcd.conf

interface wlan0
static ip_address=192.168.3.245/24
static routers=192.168.3.1
static domain_name_servers=192.168.3.1

これを /etc/dhcpcd.conf の最後に追加して保存。Raspberry PIをReboot。立ち上がったらモニターから ifconfig を実行して下さい。eth0のアドレスが192.168.3.245に固定されている事が分かります。

monitor

pi0@raspi:~ $ ifconfig
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (ローカルループバック)
        RX packets 25  bytes 3498 (3.4 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 25  bytes 3498 (3.4 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

wlan0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.3.245  netmask 255.255.255.0  broadcast 192.168.3.255
        inet6 fe80::8d0c:a6fe:6f4e:1640  prefixlen 64  scopeid 0x20<link>
        inet6 2400:2411:40c0:3400:1234:5994:4589:4e3c  prefixlen 64  scopeid 0x0<global>
        ether b8:27:eb:3b:81:a5  txqueuelen 1000  (イーサネット)
        RX packets 54  bytes 8201 (8.0 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 109  bytes 16127 (15.7 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

pi0@raspi:~ $ 
]]>