【NASync】UGOS 復元イメージの作成

はじめに

UGREEN NASync には標準で UGOS Pro という OS がインストールされている。
しかしインストールディスクは配布されていないので、OS を TrueNAS で上書きしたときや ssh で繋いで環境を書き換えてしまったときは OS の復旧が難しくなる。
そのため、NASync を酷使する予定のときは UGOS の復元イメージを作成しておくことを推奨する。

テクニカルサポートに問い合わせることで、UGOS Pro のセットアップデータをもらうことはできるが製品のシリアルナンバーでロックされていてインストールが 1 回に制限されているので、手間を考えると自分で復旧できるようにしておくとよい。

作業手順

  • Ubuntu 起動用の USB メモリを作る
    NAS で作業を行う OS (Ubuntu) を起動するための USB メモリをセットアップする
  • 起動用の USB メモリに NTFS パーティションを追加する
    UGOS のイメージ配置用に Ubuntu からも Windows からもアクセスできる NTFS パーティションを USB メモリに追加する
  • USB メモリから Ubuntu を起動して抽出作業を行う

用意する物

  • 作業用の Windows パソコン
  • USB メモリ。容量は 16GB くらい
  • NASync 本体
  • NASync の画面表示ディスプレイ。HDMI 接続
  • NASync 操作用のキーボード。USB 接続

Ubuntu 起動用の USB メモリを作る

NASync にプリインストールされている SSD から OS のデータを抽出するには NAS で Linux を動かして作業する必要がある。そのため、Ubuntu 起動用の USB メモリを作成する。

はじめに USB メモリに書き込む Ubuntu の OS イメージを https://www.ubuntulinux.jp/download/ja-remix からダウンロードする。
ファイル名は ubuntu-ja-22.04-desktop-amd64.iso のようになる。

USB 起動メモリ作成ツール「Rufus」を https://rufus.ie/ja/ からダウンロードする。
ダウンロードが終わったら USB メモリを Windows PC に挿して Rufus.exe を起動する。

  • デバイス
    書き込み先の USB メモリを選択する。ここで選択した USB メモリのデータは完全に消去されるので慎重に選択する。
  • ブートの種類
    「選択」ボタンを押して Ubuntu のディスクイメージ .iso を選択する
  • 保存領域のサイズ
    casper-rw パーティションのサイズを指定。2GB くらいにしておく。
  • ファイル システム
    「NTFS」を選択。この NTFS 領域を縮小して UGOS イメージの配置パーティションを追加する
  • その他
    デフォルト

このように設定したら「スタート」を押して USB メモリに Ubuntu OS を書き込む。以下は Rufus のスクリーンショット

画像に alt 属性が指定されていません。ファイル名: NASync_RufusSettings-1-194x300.png

起動用の USB メモリに NTFS パーティションを作る

Rufus で作成した起動用メモリには OS 用の NTFS パーティションと casper-rw の ext3 パーティションが作られる。
UGOS イメージ抽出用のパーティションが欲しいので OS 用の NTFS パーティションを縮小してデータ配置用の NTFS パーティションを作る。

以下、その手順。

  1. デスクトップの「PC」アイコン上で Shift + 右クリック して「管理(G)」を選択する
    「コンピュータの管理」ウィンドウが出る
  2. 左のリストで「ディスクの管理」を選択する
    ドライブ・パーティション管理パネルが出る
  3. USB メモリの NTFS パーティション右クリックして「ボリュームの縮小(H)」を選択。パーティションサイズを 4GB 程度に縮小する
    縮小した分、ディスクに空き領域ができる
  4. 空き領域を右クリックして「新しいシンプルボリューム(I)」を選択。NTFS パーティションを作成する

パーティションが正しく作られると画像のようになる。

画像に alt 属性が指定されていません。ファイル名: NASync_RecoveryUSBPartitions-300x215.png

NAS で Ubuntu を起動して UGOS イメージを抽出する

NAS にディスプレイ、キーボードを繋ぎ、USB メモリを挿して電源を入れる。
Ctrl + F10 を押しっぱなしにしておくことで USB メモリの OS が起動される。

起動オプションの選択画面が表示されたら「Try Ubuntu with Japanese input support」を選択して Enter キーを押す。
Ubuntu が起動するまでしばらく待つ。

Ubuntu が起動したら左下の三本線からメニューを開いて「端末」を起動する。
端末で以下の操作を行っていく。

キーボードが英語配列の場合、setxkbmap でキーボード配列の設定を変更する。

ubuntu@ubuntu:~$ setxkbmap us

lsblk でストレージ情報を取得する。

ubuntu@ubuntu:~$ lsblk
NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
loop0         7:0    0   2.1G  0 loop /rofs
loop1         7:1    0     4K  1 loop /snap/bare/5
loop2         7:2    0  61.9M  1 loop /snap/core20/1405
loop3         7:3    0 155.6M  1 loop /snap/firefox/1232
loop4         7:4    0 248.8M  1 loop /snap/gnome-3-38-2004/99
loop5         7:5    0  81.3M  1 loop /snap/gtk-common-themes/1534
loop6         7:6    0  43.6M  1 loop /snap/snapd/15177
loop7         7:7    0  45.9M  1 loop /snap/snap-store/575
loop8         7:8    0   284K  1 loop /snap/snapd-desktop-integration/10
sda           8:0    1     0B  0 disk 
sdb           8:1    1     0B  0 disk 
sdc           8:2    1     0B  0 disk 
sdd           8:48   1  28.8G  0 disk   # ← USB メモリ
├─sdd1        8:49   1     4G  0 part /cdrom
├─sdd2        8:50   1     1K  0 part 
├─sdd3        8:51   1     4G  0 part /media/ubuntu/casper-rw 
├─sdd4        8:52   1     1M  0 part 
└─sdd5        8:53   1  20.8G  0 part   # ← 後で作成した NTFS パーティション
sde           8:64   1     0B  0 disk 
nvme0n1     259:0    0   1.8T  0 disk   # ← 後付けの M.2 SSD
├─nvme0n1p1 259:1    0  15.3G  0 part 
└─nvme0n1p2 259:2    0   1.8T  0 part 
nvme1n1     259:3    0 119.2G  0 disk   # ← プリインストール SSD
├─nvme1n1p1 259:4    0   256M  0 part 
├─nvme1n1p2 259:5    0     2G  0 part 
├─nvme1n1p3 259:6    0    10M  0 part 
├─nvme1n1p4 259:7    0     2G  0 part 
├─nvme1n1p5 259:8    0     2G  0 part 
├─nvme1n1p6 259:9    0     4G  0 part 
└─nvme1n1p7 259:10   0 108.9G  0 part 

復元イメージを保存する NTFS パーティションをマウントする。

# 既に /dev/sdd5 が自動でマウントされていた場合は先にマウントを解除する
ubuntu@ubuntu:~$ sudo umount /dev/sdd5

ubuntu@ubuntu:~$ sudo mount -t ntfs3 /dev/sdd5 /media/share

プリインストール SSD のディスクイメージを NTFS パーティションに抽出する。

ubuntu@ubuntu:~$ sudo dd if=/dev/nvme1n1 bs=64M status=progress conv=sync | gzip > /media/share/ugos.img.gz
128043712512 bytes (128 GB, 119 GiB) copied, 784 s, 163 MB/s
1907+1 レコード入力
1908+0 レコード出力
128043712512 bytes (128 GB, 119 GiB) copied, 787.021 s, 163 MB/s

OS をシャットダウンする。

ubuntu@ubuntu:~$ shutdown -h now

シャットダウン画面が表示されたら、USB メモリを抜き Enter を押して電源を切る。

USB メモリを Windows パソコンに挿して NTFS パーティションをマウントすることで Windows から抽出データにアクセスすることができる。
NTFS パーティションのマウントには NTFS パーティションを作るときに使った「ディスクの管理」機能を使う。

抽出イメージからの復元

抽出イメージから UGOS を復元する方法。動作は未確認だが、この手順で成功した報告があるので大丈夫なはず。
抽出作業を行った USB メモリで Ubuntu を起動して、以下の作業を行う。

キーボードが英語配列の場合、配列設定を変更する。

ubuntu@ubuntu:~$ setxkbmap us

復元イメージを保存した NTFS パーティションをマウントする。

# 既に /dev/sdd5 が自動でマウントされていた場合は先にマウントを解除する
ubuntu@ubuntu:~$ sudo umount /dev/sdd5

ubuntu@ubuntu:~$ sudo mount -t ntfs3 /dev/sdd5 /media/share

イメージから UGOS を復元する。

ubuntu@ubuntu:~$ sudo gunzip -c /media/share/ugos.img.gz | dd of=/dev/nvme1n1 bs=64M status=progress conv=sync,noerror

OS をシャットダウンする。

ubuntu@ubuntu:~$ shutdown -h now

シャットダウン画面が表示されたら、USB メモリを抜き Enter を押して電源を切る。

参考ページ

【NASync】Frigate で監視カメラシステムを構築

はじめに

監視カメラシステムのソフトウェアは Shinobi, ZoneMinder, motionEye などがある。
Shinobi, ZoneMinder はいずれも NASync で動作させるための手順が複雑で、インストールできても CPU 使用率が 15% くらいと高負荷になった。
調査を進めたところ Frigate というソフトウェアが Docker で簡単にインストールでき、カメラの映像を中継するだけであれば CPU 使用率が 9% くらいと低負荷で動作した。
motionEye は試していないが Frigate が優秀だったのでこれを使うことにする。

接続するカメラは RTSP 配信機能があるものがよい。USB 接続の Web カメラも使えるようだが Frigate では試してはいない。
Shinobi の環境を作って v4l2loopback を使って Web カメラを使ったときは CPU 負荷が常時 20% 超えるようになってしまった。
RTSP 対応カメラは TP-Link 社の Tapo シリーズ や ATOM 社の ATOM シリーズ がおすすめ。今回は Tapo C120 を使った。

Frigate のインストール

まず Docker でプロジェクトを作成する。

プロジェクト名: frigate-ipcam-station
保存パス: 共有フォルダ/docker/frigate-ipcam-station
Compose設定: 以下

# version: "3.9"
services:
  frigate:
    container_name: frigate-ipcam-station
    # privileged: true # this may not be necessary for all setups
    restart: unless-stopped
    stop_grace_period: 30s # allow enough time to shut down the various services
    image: ghcr.io/blakeblackshear/frigate:stable
    shm_size: "512mb" # update for your cameras based on calculation above
    devices:
      # - /dev/bus/usb:/dev/bus/usb # Passes the USB Coral, needs to be modified for other versions
      # - /dev/apex_0:/dev/apex_0 # Passes a PCIe Coral, follow driver instructions here https://coral.ai/docs/m2/get-started/#2a-on-linux
      # - /dev/video11:/dev/video11 # For Raspberry Pi 4B
      - /dev/dri/renderD128:/dev/dri/renderD128 # For intel hwaccel, needs to be updated for your hardware
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./config:/config
      - ./media:/media/frigate
      - type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
        target: /tmp/cache
        tmpfs:
          size: 1000000000
    ports:
      - "8971:8971"
      - "5000:5000" # Internal unauthenticated access. Expose carefully.
      # - "8554:8554" # RTSP feeds
      - "8555:8555/tcp" # WebRTC over tcp
      - "8555:8555/udp" # WebRTC over udp
    environment:
      FRIGATE_RTSP_PASSWORD: "password"

Compose 設定は公式の記述を元にした。
以下、Compose 設定についての説明。

    # privileged: true # this may not be necessary for all setups

privileged フラグは有効にせず動いたので無効にしておく。

    shm_size: "512mb" # update for your cameras based on calculation above

以下の式で計算されるサイズを入れる。メモリに余裕があるので多めにした。
# Replace <width> and <height>
$ python -c 'print("{:.2f}MB".format((<width> * <height> * 1.5 * 20 + 270480) / 1048576))'

    devices:
      # - /dev/bus/usb:/dev/bus/usb # Passes the USB Coral, needs to be modified for other versions
      # - /dev/apex_0:/dev/apex_0 # Passes a PCIe Coral, follow driver instructions here https://coral.ai/docs/m2/get-started/#2a-on-linux
      # - /dev/video11:/dev/video11 # For Raspberry Pi 4B
      - /dev/dri/renderD128:/dev/dri/renderD128 # For intel hwaccel, needs to be updated for your hardware

ハードウェアアクセラレータの設定。
USB Coral, PCIe Coral は Google Edge TPU(Tensor Processing Unit) を載せたデバイスで、TensorFlow という機械学習用の演算デバイスのこと。
Frigate は機械学習で物体検出を行っていて、Google Edge TPU での演算に対応している。
NASync であれば USB タイプか M.2 タイプのデバイスを繋げば有効にできるが、持っていないので無効にする。
NASync は、Google Edge TPU のドライバーがインストールされておらずデバイスが読み込まれないので無効にする。
renderD128 はおそらく Intel 内蔵 GPU のこと。コメントアウトしたら CPU 使用率が数パーセント上がってしまったので有効にしておく。

      - type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
        target: /tmp/cache
        tmpfs:
          size: 1000000000

映像処理用のキャッシュ領域。メモリに余裕があれば有効にしておく。

    ports:
      - "8971:8971"
      - "5000:5000" # Internal unauthenticated access. Expose carefully.
      # - "8554:8554" # RTSP feeds
      - "8555:8555/tcp" # WebRTC over tcp
      - "8555:8555/udp" # WebRTC over udp

8971 番ポートはユーザー名・パスワードを入力してログインする https 接続を行うポート。
5000 番ポートは認証なしで見られる http 接続を行うポート。
8555 番ポートはブラウザに映像を転送するポート。
8554 番ポートは RTSP 送信ポート。RTSP 送信機能は使わない。

カメラへ接続

frigate-ipcam-station プロジェクトを実行すると初期設定ファイルが生成される。
“共有フォルダ/docker/frigate-ipcam-station/config/config.yaml” を編集する。
最低限、カメラの名前と RTSP パスを設定しておく。

mqtt:
  enabled: False

cameras:
  home_cam: # <------ Name the camera
    enabled: True
    ffmpeg:
      inputs:
        - path: rtsp://frigate:passwrd/stream1 # <----- The stream you want to use for detection
          roles:
            - detect
    detect:
      enabled: False # <---- disable detection until you have a working camera feed
      width: 1280
      height: 720

config.yaml を書き換えたら http://192.168.56.56:5000 を開く。
カメラの映像が映らないときは左下の歯車アイコンをクリックして “Restart Frigate” でサービスを再起動する。
config.yaml が間違えていた場合は書き直して ”Restart Frigate” でやり直す。

録画機能を有効化する

カメラの動作が確認できたら録画機能を有効化する。
http://192.168.56.56:5000 が開けるようになれば Frigate のページから config.yaml を編集できるようになる。
Settings >> Configuration editor を選択して config.yaml を編集する。

mqtt:
  enabled: False

cameras:
  home_cam: # <------ Name the camera
    enabled: true
    ffmpeg:
      inputs:
        - path: rtsp://frigate:passwrd@192.168.56.120/stream1 # <----- The stream you want to use for detection
          roles:
            - record  # <-- record に変更
    # record セクションを追加
    # 設定内容: https://docs.frigate.video/configuration/record/
    record:
      enabled: True
      retain:
        days: 14  # <-- 録画データ保持日数を指定
    #detect:
    #  enabled: false # <---- disable detection until you have a working camera feed
    #  width: 1280
    #  height: 720

録画データは “共有フォルダ/docker/frigate-ipcam-station/media/recordings/$yyyy-$mm-$dd/$HH/home_cam/$MM.$SS.mp4” の形式で約 10 秒置きの連番動画ファイルが生成される。
このような短い尺の動画ファイルが生成されるのには理由があり、カメラの通信が途切れたときや Frigate のプロセスが中断したときも録画途中のデータが破棄されないようにするための仕組みとなっており、ここに手を加えるのは非推奨となっている。

Tapo C120 を使って 2560 x 1440 の解像度で録画した場合は 1 ファイルにつき 0.8 ~ 2MB くらいのサイズで、1 日で 13 GB くらいのサイズになった。
“共有フォルダ/docker/frigate-ipcam-station” を SSD に指定すると SSD の寿命を短くする原因になるので、出力先を USB メモリか SD カードに変更したい。

録画用のメディアはいくつか選択肢があるが、今回は以下を考慮して SD カードに決めた。

  • M.2 SSD ― M.2 接続の Google Edge TPU があるのと、ミラーリング用の M.2 SSD を追加する可能性もあり開けておきたい。
  • USB メモリ ― USB 接続のキーボード、Google Edge TPU、その他を USB 接続するためポートを開けておきたい。デバイスが飛び出るのも好みではない。
  • SD カード ― 特に用途なし。今後も埋まる予定なし。

録画データの出力先を SD カードに変更する

録画データは一定期間で自動削除させても 13 GB * 365 日 で毎年約 4.7TB の書き込みがストレージに発生する。
WD Blue 512GB (WDS500G4B0E) の場合、書き込み耐性は 300TBW なので余裕はあるが、メインの SSD のデータが破損すると復旧が苦労するので念のため SD カード出力に変更する。
SD カードは SanDisk の 512GB MicroSD カード SDSQQNR-512G-GN6IA を購入した。
これは MicroSD だが SD カードとして接続するための変換アダプタが付属している。

SD カードを NASync に挿し込んで、SD カードのフォーマットを行う。
UGOS の管理画面の ストレージマネージャー >> 外部ストレージ管理 でストレージの編集メニューを開き「フォーマット」を選択する。
ファイルシステムは exFat か ext4 のどちらかにする。
Windows に差し替えてデータを転送したいときは exFat。それ以外の場合は ext4。

パーティションラベルは自動で設定されてしまうので、SSH ログインして lsblk で対象の SD カードを探して一度アンマウントしてから exfatlabel または e2label でラベル名を変更する。

nasuser@MYNASYNC:/$ lsblk
NAME                                     MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
sda                                        8:0    0   7.3T  0 disk
├─sda1                                     8:1    0  15.3G  0 part
└─sda2                                     8:2    0   7.3T  0 part
  └─md2                                    9:2    0   7.3T  0 raid1
    └─ug_55EBAA_1749742835_pool2-volume1 253:1    0   7.3T  0 lvm   /volume2
sdb                                        8:16   0   3.6T  0 disk
├─sdb1                                     8:17   0  15.3G  0 part
└─sdb2                                     8:18   0   3.6T  0 part
sdc                                        8:32   0   3.6T  0 disk
├─sdc1                                     8:33   0  15.3G  0 part
└─sdc2                                     8:34   0   3.6T  0 part
sdd                                        8:48   1     0B  0 disk
sde                                        8:64   1 476.7G  0 disk
└─sde1                                     8:65   1 476.7G  0 part  /mnt/@usb/sde1
zram0                                    252:0    0   7.8G  0 disk  [SWAP]
zram1                                    252:1    0   7.8G  0 disk  [SWAP]
zram2                                    252:2    0   7.8G  0 disk  [SWAP]
zram3                                    252:3    0   7.8G  0 disk  [SWAP]
nvme0n1                                  259:0    0   1.8T  0 disk
├─nvme0n1p1                              259:11   0  15.3G  0 part
└─nvme0n1p2                              259:12   0   1.8T  0 part
  └─md1                                    9:1    0   1.8T  0 raid1
    └─ug_55EBAA_1749741907_pool1-volume1 253:0    0   1.8T  0 lvm   /volume1
nvme1n1                                  259:3    0 119.2G  0 disk
├─nvme1n1p1                              259:4    0   256M  0 part  /boot
├─nvme1n1p2                              259:5    0     2G  0 part  /rom
├─nvme1n1p3                              259:6    0    10M  0 part  /mnt/factory
├─nvme1n1p4                              259:7    0     2G  0 part
├─nvme1n1p5                              259:8    0     2G  0 part  [SWAP]
├─nvme1n1p6                              259:9    0     4G  0 part  /ugreen
└─nvme1n1p7                              259:10   0 108.9G  0 part  /overlay
nasuser@MYNASYNC:/$ sudo umount /dev/sde1
# /$ sudo exfatlabel /dev/sde1 RecCard
nasuser@MYNASYNC:/$ sudo e2label /dev/sde1 RecCard

SD カードは抜き差しすることで自動でマウントされるが、内部ストレージのマウント状態によってマウント先が /mnt/@usb/sdd1 になったり /mnt/@usb/sde1 に変わったりする。
録画データの保存先として docker-compose に SD カードのマウント先を指定したいので、fstab に SD カードのマウント先を固定させる設定を追加する。

ポートを調べるため、一度 SD カードを再マウントさせる。
再マウントするには UGOS の管理画面から ストレージマネージャー >> 外部ストレージ管理 でストレージの編集メニューから「取り出し」を選択。
物理的に SD カードを抜き差しすると自動で SD カードが再マウントされる。

再マウントできたら、by-path を見て SD カードのポートを見つける。

nasuser@MYNASYNC:/$ ls -al /dev/disk/by-path
total 0
drwxr-xr-x 2 root root 780 Jul 13 10:51 .
drwxr-xr-x 9 root root 180 Jul 13 10:51 ..
lrwxrwxrwx 1 root root   9 Jul 13 10:51 pci-0000:00:0d.0-usb-0:3:1.0-scsi-0:0:0:0 -> ../../sde
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:00:0d.0-usb-0:3:1.0-scsi-0:0:0:0-part1 -> ../../sde1
lrwxrwxrwx 1 root root  13 Jul 13 10:51 pci-0000:02:00.0-nvme-1 -> ../../nvme0n1
lrwxrwxrwx 1 root root  15 Jul 13 10:51 pci-0000:02:00.0-nvme-1-part1 -> ../../nvme0n1p1
lrwxrwxrwx 1 root root  15 Jul 13 10:51 pci-0000:02:00.0-nvme-1-part2 -> ../../nvme0n1p2
lrwxrwxrwx 1 root root  13 Jul 13 10:51 pci-0000:04:00.0-nvme-1 -> ../../nvme1n1
lrwxrwxrwx 1 root root  15 Jul 13 10:51 pci-0000:04:00.0-nvme-1-part1 -> ../../nvme1n1p1
lrwxrwxrwx 1 root root  15 Jul 13 10:51 pci-0000:04:00.0-nvme-1-part2 -> ../../nvme1n1p2
lrwxrwxrwx 1 root root  15 Jul 13 10:51 pci-0000:04:00.0-nvme-1-part3 -> ../../nvme1n1p3
lrwxrwxrwx 1 root root  15 Jul 13 10:51 pci-0000:04:00.0-nvme-1-part4 -> ../../nvme1n1p4
lrwxrwxrwx 1 root root  15 Jul 13 10:51 pci-0000:04:00.0-nvme-1-part5 -> ../../nvme1n1p5
lrwxrwxrwx 1 root root  15 Jul 13 10:51 pci-0000:04:00.0-nvme-1-part6 -> ../../nvme1n1p6
lrwxrwxrwx 1 root root  15 Jul 13 10:51 pci-0000:04:00.0-nvme-1-part7 -> ../../nvme1n1p7
lrwxrwxrwx 1 root root   9 Jul 13 10:51 pci-0000:05:00.0-ata-1 -> ../../sda
lrwxrwxrwx 1 root root   9 Jul 13 10:51 pci-0000:05:00.0-ata-1.0 -> ../../sda
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-1.0-part1 -> ../../sda1
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-1.0-part2 -> ../../sda2
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-1-part1 -> ../../sda1
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-1-part2 -> ../../sda2
lrwxrwxrwx 1 root root   9 Jul 13 10:51 pci-0000:05:00.0-ata-2 -> ../../sdc
lrwxrwxrwx 1 root root   9 Jul 13 10:51 pci-0000:05:00.0-ata-2.0 -> ../../sdc
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-2.0-part1 -> ../../sdc1
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-2.0-part2 -> ../../sdc2
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-2-part1 -> ../../sdc1
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-2-part2 -> ../../sdc2
lrwxrwxrwx 1 root root   9 Jul 13 10:51 pci-0000:05:00.0-ata-3 -> ../../sdd
lrwxrwxrwx 1 root root   9 Jul 13 10:51 pci-0000:05:00.0-ata-3.0 -> ../../sdd
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-3.0-part1 -> ../../sdd1
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-3.0-part2 -> ../../sdd2
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-3-part1 -> ../../sdd1
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-3-part2 -> ../../sdd2
lrwxrwxrwx 1 root root   9 Jul 13 10:51 pci-0000:05:00.0-ata-4 -> ../../sdb
lrwxrwxrwx 1 root root   9 Jul 13 10:51 pci-0000:05:00.0-ata-4.0 -> ../../sdb
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-4.0-part1 -> ../../sdb1
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-4.0-part2 -> ../../sdb2
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-4-part1 -> ../../sdb1
lrwxrwxrwx 1 root root  10 Jul 13 10:51 pci-0000:05:00.0-ata-4-part2 -> ../../sdb2

この場合 ../../sde1 にマウントしている pci-0000:00:0d.0-usb-0:3:1.0-scsi-0:0:0:0-part1 が対象のポートだった。
ポート ID が分かったら、/etc/fstab に以下の行を追加する。
(ポート番号、マウント先ディレクトリ、ファイルシステムは環境に合わせて書き換える)

/dev/disk/by-path/pci-0000:00:0d.0-usb-0:3:1.0-scsi-0:0:0:0-part1  /media/sdcard  ext4  defaults,user  0  0

マウント先のディレクトリを作成して再起動すると fstab で指定したディレクトリに SD カードがマウントされる。

nasuser@MYNASYNC:/$ sudo mkdir /media/sdcard
nasuser@MYNASYNC:/$ sudo shutdown -r now

Frigate の docker-compose の volumes セクションを書き換えて録画データを SD カードに出力させる。

    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /media/sdcard/frigate/config:/config  # ./config:/config から書き換え
      - /media/sdcard/frigate/media:/media/frigate  # ./media:/media/frigate から書き換え
      - type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
        target: /tmp/cache
        tmpfs:
          size: 1000000000

SD カードのマウント先を書き換えない場合

SD カードのマウント先を fstab で書き換えずに docker-compose の volumes セクションで /mnt/@usb/sde1 を指定したが構文エラーになった。
対策として “共有フォルダ/docker/frigate-ipcam-station/sdcard” から “/mnt/@usb/sde1/frigate” へのシンボリックリンクを張る。

# SD カードへのシンボリックリンクを張る
/$ cd /volume1/docker/frigate-ipcam-station
/volume1/docker/frigate-ipcam-station$ sudo ln -s /mnt/@usb/sde1/frigate sdcard
/volume1/docker/frigate-ipcam-station$ sudo mkdir /mnt/@usb/sde1/frigate

# 既存の設定データ・録画データを SD カードに移動させる
/volume1/docker/frigate-ipcam-station$ sudo mv config sdcard/config
/volume1/docker/frigate-ipcam-station$ sudo mv media sdcard/media

これでマウント先を書き換えない場合も Frigate の録画データ SD カードに保存されるようになる。
ただし、ストレージの接続状態によってマウント先が変わることがあるので、そのときはシンボリックリンクを張り直す必要がある。

参考ページ

磁石式パイプホルダーの製作

アクアリウムで使うエーハイムの外部式フィルターには取水パイプ・排水パイプを固定する吸盤(キスゴム)が付属してきますが、この吸盤は吸着力が弱かったり劣化したりで扱いにくいので磁石に付け替えました。

用意するもの

  • エーハイム キスゴム(クリップ部分のみ使います)
  • ステンレス製 なべ頭 タッピングねじ M3x10
  • マグエバー マグサンド

組み立てイメージ

組み立てイメージです。
マグサンドについている取っ手を外して、キスゴムのクリップをねじ止めします。
マグサンドは接着面が柔らかい素材で作られておりガラス面の使用に適しています。
単純ゆえ詳細な説明は不要かもしれませんが、以下に手順を書いていきます。

手順 1. キスゴム、マグサンドを分解する

キスゴムを分解してクリップを回収します。写真右の部品です。

マグサンドの取っ手がついているパーツを分解して、磁石を回収します。取っ手はねじで固定されているのでプラスドライバーを使って分解します。
ここで分解した取っ手とねじは今後の作業には使いません。

手順 2. クリップに下穴をあける

キスゴムのクリップにはねじ穴が開いていないので、タッピングねじをねじ込むための下穴をあけます。
写真のようにクリップの中心に 2.5mm のドリルで穴を貫通させます。
1.5mm のドリルで穴をあけてから 2.5mm のドリルで穴をあけるようにすると位置がずれにくいです。

手順 3. 回収した部品を組み立てる

最初のイメージ通り、マグサンドの磁石にエーハイムのパイプクリップをねじ止めします。
ねじ止めには別に用意したタッピングねじを使います。
少し固いですが下穴をあけてあるのでプラスドライバーでねじ込めます。

以上で完成です。
商品の価格だけ見ると少々値は張りますが、キスゴムが外れる手間とキスゴムを定期的に購入するコストを考えると支払う価値のあるコストと思います。

今回、タッピングねじを別に用意してねじ止めしましたが、M3 タッピングドリルを使ってねじ切りすることでマグサンドのねじを使うこともできそうです。
僕はタッピングドリルの存在を知らなかったのでこのような手順になりましたが、また機会があれば試してみます。

最後に、この磁石式パイプホルダーを使っている我が家の水槽の写真を貼っておきます。

水シミュレーション

昔販売されていた AQUAZONE 風のゲームを作ろうと思い、仕様を練っています。アクアリウムの主役は「魚」ですが、シミュレータの仕様を決めるにあたってキモとなるものは、タイトルにある通り「水」(水質)です。

そこで AQUAZONE ではどうなっていたかアプリを実行して確認してみました。

AQUAZONE ではこのように水質の情報を見ることができます。今回は新しい水槽を立ち上げたときのパラメータを確認しておきます。

ラベル名称初期値最小値最大値
Temp/℃水温26.5 ℃0.050.0
pH水素イオン濃度7.00.014.0
GH総硬度4.00.020.0
O2酸素8.000 mg/l0.010.0
CO2二酸化炭素15.000 mg/l0.0100.0
HNO3硝酸0.000 mg/l0.0100.0
NH3アンモニア0.000 mg/l0.010.0
Cl塩素1.100 mg/l0.020.0
Mgマグネシウム0.000 mg/l0.0300.0
Caカルシウム42.000 mg/l0.0300.0
Proteinタンパク質0.000 mg/l0.0100.0
Carbohydrate炭水化物0.000 mg/l0.0100.0
Fat脂質0.000 mg/l0.0100.0
Vitaminビタミン0.000 mg/l0.0100.0

現実で水槽を管理するときに気を付ける成分以外にも、食べ物に含まれる成分(タンパク質・炭水化物・脂質・ビタミン)が細かくパラメータ化されていました。これは餌が水質に与える影響を計算するためでしょうか。自分が考えていた仕様よりも細かい。。。他に気になるのは最初から水温が 26.5 ℃ で温水を汲んでいるのかとか、塩素濃度が 1.100 mg/l で国内では超えないようにされている目標値の 1.000 mg/l を超えている点とか。

水温は水換えのたびに調整するのは面倒そうだし、塩素濃度は薄すぎてもゲーム性が欠けるというのもあり、単純に現実に合わせればいいというわけでもないのでどう調整しようかといったところ。。。

C++からC#をスクリプトとして実行

この記事は 「プロ生ちゃん Advent Calender 2016」 6日目の記事です。

はじめに

ゲーム開発で C++ のコードからスクリプト言語を利用するとき Lua や AngelScript といった選択肢がありますが、そーいえば C# 自身が cs ファイルのコンパイル機能を持ってたなーと思い、テスト的に作ってみて COM を作ったりだとか面倒だった構成をどこまでシンプルにできるか挑戦してみました。

ここでは最適化の過程は省略して、今回得られた手順について説明していきます。

1. CLI でスタティックライブラリを作る

cs ファイルのコンパイル機能は C# の機能のため C++ からは直接呼び出せません。そこで、CLI で作ったライブラリを中継させます。CLI をスタティックライブラリにするのは正規の使い方から外れている気がしますが、動けばよしという方針でやっていきます。

とりあえず空の CLR プロジェクト (.Net Framework 4) を作ります。
プロジェクト名は LibCli としておきました。

プロジェクトのプロパティを開いて、
構成プロパティ >> 全般 >> 構成の種類
を アプリケーション (.exe) から スタティックライブラリ(.lib) に変えます。

cs スクリプトのコンパイル&実行を行うコードを書きます。

// LibCli.h - LibCli
#pragma once

// cs スクリプトのコンパイル&実行
_declspec(dllexport) void LibCliMain ();
// LibCli.cpp - LibCli
#include "LibCli.h"
#include "IScript.h"

using namespace System;
using namespace System::Reflection;
using namespace System::CodeDom::Compiler;
using namespace System::Collections::Generic;
using namespace System::Windows::Forms;
using namespace Microsoft::CSharp;
using namespace LibCli;

// 以下の3つを参照に追加するべし
// Microsoft.CSharp
// System
// System.Windows.Forms

void LibCliMain ()
{
	// cs スクリプトのコンパイルパラメータ
	CompilerParameters^ params = gcnew CompilerParameters();
	params->GenerateInMemory = true;
	params->IncludeDebugInformation = false;
	params->ReferencedAssemblies->Add(Assembly::GetEntryAssembly()->Location);
	params->ReferencedAssemblies->Add("System.Windows.Forms.dll");

	// .Net Framework のバージョン
	Dictionary<String^, String^>^ providerOptions = gcnew Dictionary<String^, String^>();
	providerOptions->Add("CompilerVersion", "v4.0");

	// cs ファイル名
	array<String^>^ sources = { System::IO::File::ReadAllText("TestScript.cs") };

	// コンパイル!
	CSharpCodeProvider^ codeProvider = gcnew CSharpCodeProvider();
	CompilerResults^ compilerResults = codeProvider->CompileAssemblyFromSource(params, sources);

	// エラーがあれば出しとく
	for each (CompilerError^ error in compilerResults->Errors)
	{
		System::Windows::Forms::MessageBox::Show(error->ErrorText);
	}

	// コンパイルしたアセンブリを取得
	Type^ type = compilerResults->CompiledAssembly->GetType("Test.TestScript");
	IScript^ testScript = (IScript^)Activator::CreateInstance(type);

	// 実行!
	testScript->Execute();
}
// IScript.h - LibCli
#pragma once

namespace LibCli
{
	/// <summary>
	/// スクリプトのインターフェース。
	/// </summary>
	public interface class IScript
	{
	public:
		/// <summary>
		/// 何かしらの処理
		/// </summary>
		virtual bool Execute ();
	};
}

ヘッダに CLI 形式のコードが紛れていると C++ の方でエラーになってしまうので cpp とヘッダはきちんと分離しておきます。
IScript は cs スクリプトで実装するインターフェースです。
cs スクリプトのファイル名は “TestScript.cs” 、クラス名は “Test.TestScript” と決め打ちにしています。

あと、この3つのコードを追加しただけではビルドが失敗します。コメントに書いた
Microsoft.CSharp
System
System.Windows.Forms
をプロジェクトの参照に追加してからビルドします。

ここまでやってビルドが通らないときは .Net Framework が 4 意外になっている疑いがあります。

2. C++ で exe を作る

C++ で exe を作って、CLI のライブラリを組み込みます。
ソリューションに Win32 コンソールアプリケーションを新規追加します。
アプリケーション設定は、コンソールアプリケーションと、空のプロジェクトにチェック。Security Development Lifecycle のチェックは外した状態にします。
プロジェクト名は CppMain にしました。

CLI で作ったスタティックライブラリをリンクするようにします。
追加した CppMain のプロジェクト設定を開いて
構成プロパティ >> リンカー >> 入力 >> 追加の依存ファイル
に ..\$(Configuration)\LibCli.lib を追加します。
$(Configuration) はマクロで、ビルド構成に合わせて Debug や Release に置換されます。

LibCli.lib は LibCli をビルドした後に生成されるので、LibCli をビルドした後に CppMain をビルドするようにします。
CppMain のプロジェクトを右クリックして
ビルド依存関係 >> プロジェクト依存関係
を選択し、LibCli にチェックを入れます。

プロジェクトの設定は以上で、残りは CLI で書いた関数の呼び出しコードを書くだけです。

// main.cpp - CppMain
#include "..\LibCli\LibCli.h"

#include <cstdlib>

int main (void)
{
	LibCliMain();

	system("Pause");

	return 0;
}

CppMain をスタートアッププロジェクトに設定して、ビルドして、実行して、例外が出れば成功です。

3. cs スクリプトを書く

さきほどの例外は cs スクリプトを書いてなかったせいです。cs スクリプトさえ書いてやれば問題なく動きます。

// TestScript.cs
using System.Windows.Forms;
using LibCli;

namespace Test
{
	public class TestScript : IScript
	{
		public bool Execute ()
		{
			MessageBox.Show("プロ生ちゃんマジ天使!");

			return true;
		}
	}
}

これを CppMain プロジェクトのフォルダ、main.cpp と同じ所に置いて実行すると cs スクリプトに書いたメッセージボックスが出るようになります。

ここまで作業を行ったソリューションファイルです -> CsScriptTest_0.zip

4. cs スクリプトから C++ の関数を呼び出す

今までの説明で cs スクリプトのビルド&実行が最低限動くようになりましたが、cs スクリプトから C++ の関数の呼び出しができないと実用性が薄れるのでそこも試しにやってみます。Lua や AngelScript でいうバインディングってやつですね。

関数を共有するためには CppMain と LibCli から参照する新しいプロジェクトを追加するのが正攻法ですが、今回は最小限に抑えるため LibCli に共有インターフェースを追加してしまいます。

// SharedInterface.h - LibCli
#pragma once

class SharedInterface
{
public:
	virtual int SomeFunction (int a, int b) = 0;
};
// SharedWrapper.cs - LibCli
#pragma once

#include "SharedInterface.h"

namespace LibCli
{
	public ref class SharedWrapper
	{
	public:
		SharedWrapper (SharedInterface* pSharedData)
			: m_pSharedData(pSharedData)
		{
		};

		int SomeFunction (int a, int b)
		{
			return m_pSharedData->SomeFunction(a, b);
		};

	private:
		SharedInterface* m_pSharedData;
	};
}

SharedInterface は CppMain と LibCli で共有するインターフェース。SharedWrapper は C# から SharedInterface を使うためのラッパークラスです。

SharedInterface を利用するように LibCli を書き換えます。

// IScript.h - LibCli
#pragma once

#include "SharedWrapper.h"  // <= 追加

namespace LibCli
{
	/// <summary>
	/// スクリプトのインターフェース。
	/// </summary>
	public interface class IScript
	{
	public:
		/// <summary>
		/// 何かしらの処理
		/// </summary>
		virtual bool Execute (SharedWrapper^ sharedData);  // <= 修正
	};
}
// LibCli.h - LibCli
#pragma once

#include "SharedInterface.h"  // <= 追加

// cs スクリプトのコンパイル&実行
_declspec(dllexport) void LibCliMain (SharedInterface* pSharedData);  // <= 修正
// LibCli.cpp - LibCli
#include "LibCli.h"
#include "IScript.h"
#include "SharedWrapper.h"

using namespace System;
using namespace System::Reflection;
using namespace System::CodeDom::Compiler;
using namespace System::Collections::Generic;
using namespace System::Windows::Forms;
using namespace Microsoft::CSharp;
using namespace LibCli;

// 以下の3つを参照に追加するべし
// Microsoft.CSharp
// System
// System.Windows.Forms

void LibCliMain (SharedInterface* pSharedData)  // <= 修正
{
	// cs スクリプトのコンパイルパラメータ
	CompilerParameters^ params = gcnew CompilerParameters();
	params->GenerateInMemory = true;
	params->IncludeDebugInformation = false;
	params->ReferencedAssemblies->Add(Assembly::GetEntryAssembly()->Location);
	params->ReferencedAssemblies->Add("System.Windows.Forms.dll");

	// .Net Framework のバージョン
	Dictionary<String^, String^>^ providerOptions = gcnew Dictionary<String^, String^>();
	providerOptions->Add("CompilerVersion", "v4.0");

	// cs ファイル名
	array<String^>^ sources = { System::IO::File::ReadAllText("TestScript.cs") };

	// コンパイル!
	CSharpCodeProvider^ codeProvider = gcnew CSharpCodeProvider();
	CompilerResults^ compilerResults = codeProvider->CompileAssemblyFromSource(params, sources);

	// エラーがあれば出しとく
	for each (CompilerError^ error in compilerResults->Errors)
	{
		System::Windows::Forms::MessageBox::Show(error->ErrorText);
	}

	// コンパイルしたアセンブリを取得
	Type^ type = compilerResults->CompiledAssembly->GetType("Test.TestScript");
	IScript^ testScript = (IScript^)Activator::CreateInstance(type);

	SharedWrapper^ sharedWrapper = gcnew SharedWrapper(pSharedData);  // <= 追加

	// 実行!
	testScript->Execute(sharedWrapper);  // <= 修正
}

CppMain の方には SharedInterface の実装を追加します。

// SharedData.h - CppMain
#pragma once

#include "..\LibCli\SharedInterface.h"

class SharedData : public SharedInterface
{
public:
	int SomeFunction (int a, int b) override
	{
		return a + b;
	}
};

LibCliMain() の呼び出し時に SharedData を渡すようにします。

// main.cpp - CppMain
#include "..\LibCli\LibCli.h"
#include "SharedData.h"  // <= 追加

#include <cstdlib>

int main (void)
{
	SharedData shareData;  // <= 追加

	LibCliMain(&shareData);  // <= 修正

	system("Pause");

	return 0;
}

最後に、cs スクリプトを書き換えて完了です。

// TestScript.cs
using System.Windows.Forms;
using LibCli;

namespace Test
{
	public class TestScript : IScript
	{
		public bool Execute (SharedWrapper sharedData)  // <= 修正
		{
			MessageBox.Show("プロ生ちゃんマジ天丼!");

			MessageBox.Show(string.Format("SomeFunction(111, 901): {0}", sharedData.SomeFunction(111, 901)));  // <= 追加

			return true;
		}
	}
}

どこかのタイミングで dll を配置しないといけなくなるかと思っていましたが、終わりまで exe 1個で済んでしまいました。

あと、説明なしにさらっと回避処理を紛れ込ませていましたが、普通に cs ファイルのコンパイル・実行コードを書いた場合は TestScript.cs から LibCli への参照が登録されておらず LibCli.IScript や LibCli.SharedWrapper を参照できません。
でも LibCli はスタティックライブラリになってるしどうすっぺという感じですが、LibCli.cpp で

params->ReferencedAssemblies->Add(Assembly::GetEntryAssembly()->Location);

とやっている部分がミソです。実は exe も dll 的なふるまいをするので、ここで exe を dll として登録しています。

最終的にできあがったソリューションファイルです -> CsScriptTest_1.zip

おわりに

この方法は、設計的にはきれいにまとまりましたが、CLI でスタティックライブラリを使う場合にいろいろと問題が出てしまいました。

まず、.Net Framework 4 と指定していた所ですが、どうやら新しい .Net Framework ではスタティックライブラリが作れないようです。Debug ビルドはできても Release ビルド時に mscorlib.dll が見つからないとエラーになったりします。また無理にリンクして動かしたとしても、アプリケーション実行時にマネージド領域が初期化されないとか何かでC#のメソッドが正しく動きませんでした。

さらに、ソースコードの編集でインテリセンスが効かなくなるという問題もありました。これは結構きついです。

CLI でスタティックライブラリを作れるとこういう面白いことができるなーと思ったのですが、MS さん的にはサポートしていかない感じなんですかね。DLL にすれば解決する話でもあるのですが、やっぱりスタティックリンクがいいなー