« CGIからrebootなどのroot権限が必要なコマンドを実行する | トップページ | meta name="viewport"の役割 »

2022.03.27

WebのユーザインタフェースからZYNQのboot.binを更新する

ZYNQやZYNQ UltraScale+は、SDカードの第一パーティションに書かれたboot.binから起動します。このboot.binを書き換えればFPGAのPLが更新できるのですが、やり方にはいろいろあります。

一番基本的で面倒なやり方はWindows PCにUSB SDカードリーダを挿して書き換えることです。納品した製品でお客様にファームウェア更新と称してSDカードの書き換えをやってもらうとかなりの確率で失敗します。Windows PCに挿したときに「このカードはWindowsでフォーマットされていません」と出てきて「はい」を押してしまい、第二パーティションを壊してしまうからです。手順書では防げません。

次の策は、SCPやFTPでファイルを転送してLinux上でcpコマンドを使ってSDカードに転送する方法。これをお客様にやってもらおうとしても、すべてのお客様がLinuxを使えるわけではないので、パーミッションとかroot権限とかsudoでハマってしまうので、これもよくありません。

やはり、ZYNQで動くLinuxの上でWeb画面を作って、Web上のユーザインタフェースからZYNQのboot.binを更新するようにするべきです。

実際にやってみた図が次のとおりです。

まず、Webアプリに管理者画面を作り、boot.binを送信するためのボタンを作ります。

Webupdate1

これを押すと、下の図のようなダイアログが開くようにしておきます。ダイアログはjQuery UIというもので簡単に作れます。

Webupdate2

「ファイルを選択」を押すと、Windowsのファイル選択ダイアログが開くので、boot.binを選びます。

Webupdate3

元の画面に戻り「送信」を押すと、ファイルが送信されて、そのファイルの情報が送り返されてきます。

Webupdate4

すると、SDカードの第一パーティション(/mnt にマウント済み)に、送ったファイルが書き込まれる、というしくみです。

Webupdate5

さて、これをどうやって実現しているかを説明します。まず、ダイアログとかをなくした必要最小限のHTMLファイルを示します。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<script type="text/javascript" src="./js/jquery-2.1.3.min.js"></script>
<script>
<!--
var bootfile = null;
$(document).ready(function(){
$("#file_uploader").change(function(e){
bootfile = e.target.files[0];
});
$("#upload_button").click(function(e){
var reader = new FileReader();
reader.onloadend = function() {
upload("●●●●", bootfile.name, Math.floor(bootfile.lastModified / 1000), reader.result);
}
reader.readAsArrayBuffer(bootfile);
});
});
function upload(password, filename, lastModified, binaryData)
{
$.ajax({
type: "POST",
url: './cgi-bin/upload.pl?password=' + password + '&filename=' + filename
+ '&lastModified=' + lastModified + '&_=' + Date.now(),
cache: false, //キャッシュを使用しない
processData: false, // エンコードしない
data: binaryData
}).done(function(data){
console.log(data);// 成功時の処理
}).fail(function(){ //失敗時
console.log(data);// 通信失敗時の処理
});
}
-->
</script>
</head>
<input type="file" id="file_uploader"/><input type="submit" id="upload_button">
</body>

簡単に説明すると、まず、

<input type="file" id="file_uploader"/><input type="submit" id="upload_button">

で、ファイル選択ボタンを作っています。非常にシンプルなフォームですが、

Simpleform

というのが出来ます。

「ファイルを選択」ボタンが押されてファイルが選択されたときにbootfile = e.target.files[0];が実行されて、そのファイルの情報がbootfileという変数に入ります。

送信ボタンが押されると、以下の処理が実行されます。

var reader = new FileReader(); // ①
reader.onloadend = function() {
upload("●●●●", bootfile.name, Math.floor(bootfile.lastModified / 1000), reader.result);//②
}
reader.readAsArrayBuffer(bootfile);//③

分かりにくいのですが、①③②の順序で実行されます。

FileReaderというのはJavaScriptでファイルを読み込むためのクラスで、バイナリファイルを読み込むには、reader.readAsArrayBuffer(bootfile)を実行します。なお、FileReaderにはreadAsBinaryString()という関数もあるのですが、0x80以上の値を読み込むとエスケープ処理が行われてしまって正しいデータになりません。readAsArrayBufferを使ってください。

readAsArrayBuffer()の処理は非同期動作になっていて、読み込みが完了するとreader.onloadendで指定した関数が呼び出されます。上のコードでは、完了処理の中でuploadという関数を呼び出しています。

uploadは第一引数がApacheユーザ(デフォルトではwww-data)のパスワード、第二引数がファイル名、第三引数がファイル更新時刻のUnix時刻、第四引数がファイルの実体です。

ファイル名と更新時刻は、bootfile.filename,とbootfile.lastModifiedに入っています。JavaScriptの時刻はミリ秒単位なのでUnix時刻に直すには1000で割ります。

upload関数は

$.ajax({
type: "POST",
url: './cgi-bin/upload.pl?password=' + password + '&filename=' + filename
+ '&lastModified=' + lastModified + '&_=' + Date.now(),
cache: false, //キャッシュを使用しない
processData: false, // エンコードしない
data: binaryData
}).done(function(data){

を呼んでいます。

送信方法はPOSTにします。GETだとデータがURIにエンコードされて送られるので、ファイルという大きな実体を送信するのには向いていません。ところが、POSTは標準入力に送られるので「区切り」というものがなく、あえて?や=やエンコードを駆使して「区切り」を作るのは無駄だし処理速度も落ちます。

そこで、GETとPOSTを併用した方法で送るようにします。ファイルの実体はPOSTで送るためtype: "POST"としておきつつ、URLにファイル名や最終更新日などを自分でくっつけてGET風にすればよいのです。

これで、ファイルの情報はGETで、ファイルの本体はPOSTで送られるというハイブリッドな送信になります。

それから、生のバイナリデータを送るためには、$.ajax関数のオプションを

cache: false, //キャッシュを使用しない
processData: false, // エンコードしない
data: binaryData

とします。送っているbinaryDataというのは、FileReaderのreader.resultの部分です。

 

さて、これを受ける側のCGIですが、Perlで書くことにします。

作ったPerlのスクリプトの全体像は以下のようになります。

#!/usr/bin/perl
if ($ENV{'REQUEST_METHOD'} ne 'POST') {
die "not POST";
}
read(STDIN, $bindata, $ENV{'CONTENT_LENGTH'});
foreach $data (split(/&/, $ENV{'QUERY_STRING'})) {
($key, $value) = split(/=/, $data);
$value =~ s/\+/ /g;
$value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack('C', hex($1))/eg;
$value =~ s/\t//g;
$in{"$key"} = $value;
}
my ($sec, $min, $hour, $mday, $mon, $year);
($sec, $min, $hour, $mday, $mon, $year) = localtime($in{'lastModified'});
$year += 1900;
$mon += 1;
print "Content-Type: text/plain; charset=UTF-8\n\n";
if ($in{'filename'} eq '') {
print "ファイル名が空です";
}
print "ファイル名は $in{'filename'} です。<br>";
print "ファイルの日付は";
printf("%04d/%02d/%02d %02d:%02d:%02d\n", $year ,$mon, $mday, $hour, $min, $sec);
print "です。<br>";
open(OUT, "> /var/tmp/$in{'filename'}"); # /var/tmpに一時ファイルを保存
binmode OUT; # バイナリモード指定
print(OUT $bindata); # 読み込んだデータを出力
close(OUT);
utime $in{'lastModified'}, $in{'lastModified'}, "/var/tmp/$in{'filename'}"; # 更新時刻を変更
chmod 0666, "/var/tmp/$in{'filename'}"; # パーミッションを666にして誰でも消せるようにする
my $file_size = -s "/var/tmp/$in{'filename'}"; # ファイルサイズを取得
print "保存したファイルサイズは $file_size です。";
system("echo $in{'password'} | sudo -S cp -p /var/tmp/$in{'filename'} /mnt/$in{'filename'}"); # コピー
unlink "/var/tmp/$in{'filename'}"; # 一時ファイルを削除
exit;

最初に read(STDIN, $bindata, $ENV{'CONTENT_LENGTH'}); を実行し、POSTで送られてきたファイルの実体を$bindataに入れます。

次に、$ENV{'QUERY_STRING'}で、URIにエンコードされたGET部分のデータを取ってきて連想配列に格納します。

そうしたら、$bindataを一時ファイル "/var/tmp/<ファイル名>"に出力し、そのパーミッションと日付を変更します。

最後に、この一時ファイルを目的の場所に配置するため、

echo $in{'password'} | sudo -S cp -p /var/tmp/$in{'filename'} /mnt/$in{'filename'}

というコマンドを実行します。

ここで、/mntというのは、

/dev/mmcblk0p1 /mnt

で、SDカードの第一パーティションをマウントしたディレクトリです。/mntに書き込むにはroot権限が必要です。

root権限が必要なファイルをCGIから書き換えるために、一度 /var/tmp に作った後でsudo cpをします。

sudoにはパスワードを与えるそのもののオプションはありませんが、sudo -Sとすることでパスワードを標準入力から受け取ることができるようになります。そのため、echoからパイプでパスワードを渡して、sudo -Sを実行しています。

sudo cpをCGIから実行できるようにするには、/etc/sudoersに

www-data ALL=(ALL:ALL) /bin/cp

の設定が必要です。

以上のようにすることで、WebアプリからCGIを通じてboot.binが書き換えできるようになります。

ZYNQだけではなくZYNQ UltraScale+でも同じようにできるはずですし、このやり方を応用すれば、システムの重要なファイルをWebから送り込んで更新することもできるようになります。

GETで自由にオプションを送れるので、正規の者が作ったファイルかどうかを確かめるような鍵やハッシュなどを送るようにすることもでき、必要に応じてセキュリティを高めることもできます。

 

|

« CGIからrebootなどのroot権限が必要なコマンドを実行する | トップページ | meta name="viewport"の役割 »

コメント

コメントを書く



(ウェブ上には掲載しません)




« CGIからrebootなどのroot権限が必要なコマンドを実行する | トップページ | meta name="viewport"の役割 »