Chumby の夢を、別の DNA で生まれ直す —— Canby 誕生
音声ガイド(サーバー描画で蘇るChumbyの夢)
Canby は Chumby の改造でも復活でもない。
Chumby の「夢」を、別の DNA で生まれ直させた存在である。
ハードもソフトも全く別だが、あの画面、あの動き、あの常時表示される情報体験だけは確かに受け継いでいる。 血はつながっていなくても、Canby は Chumby の「夢の兄弟」と呼ぶにふさわしい。
目に見えない情報の灯りを宿す端末
Chumby が静かに姿を消して久しい。しかし、その「常に何かを語り続ける存在感」は今も記憶に残る。 Canby は、過去の Chumby の精神を再解釈し、別の形で現代に再生させた。 単なる表示端末ではなく、画面そのものが語る情報体験を宿す装置である。
Canby の画面では、Chumby の SWF が動く限り、ほとんどの画像表示系アプリが再現可能だ。 つまり、過去の夢をコピーするのではなく、新しい DNA で再生している。 そのため、血統やハードウェアの継承はないが、体験の継承は確かにある。
Canby(クライアント)の仕様
- ESP32-3248S035C 3.5インチ容量性タッチスクリーン、320×480 TFT LCD
- Arduino IDE で独自プログラムを開発
- サーバー上のデータを 1 秒ごとに取得して画面更新
- 初期は JPG 形式 → 展開に時間がかかり、更新は約 5 秒ごと
- .bin 形式に変更 → 1 秒以内の高速更新を実現
- 画像サイズに応じたバックライト制御
- 100B 以下:消灯
- 500B 以下:約 50% 減光
- SWF の再生により、ほとんどの Chumby 画像表示系アプリを再現可能
- 表示の細部まで再現することで、「画面が情報を語り続ける体験」を完全に維持
サーバー側の仕組み
- 高速ローカルサーバー上で稼働
- Chumby の
.swfを仮想ディスプレイで再生 ffmpegで毎秒キャプチャ → Canby が 1 秒ごとに取得- 指定時間(デフォルト 23:00~8:00)にはバックライトを自動オフ
- Linux Mint 22 上の bash プログラムで稼働
- この仕組みにより、過去の Chumby の動きや画面の雰囲気をほぼ完全に再現
- config 設定により、夜間はバックライトオフ設定可能
技術的工夫と発想の源
JPG では高速更新ができない → そこで .bin 形式を導入 ESP32 の描画能力に制約 → 画像容量によるバックライト制御を採用 SWF をそのまま表示 → 互換性と「Chumbyらしさ」を保持。こうした技術的制約と工夫が、単なる表示端末ではない、「夢を再現する端末」という発想を突き上げた。
誰も喜ばなくても
そして、誰も喜ばない。それでも画面は 1 秒ごとに更新され、SWF は淡々と動き続ける。 それは、誰のためでもなく、「作者自身のためだけの情報体験」である。 時間や労力の対価を求めず、純粋に画面に没入する行為──それが Canby の本質だ。
まとめ
Canby は Chumby ではない。血統は別だ。DNA も違う。 しかし、画面を見ればわかる。SWF が動き、情報が絶えず流れ続ける。 それこそが Chumby が目指していた 「常時情報を灯す存在」である。
血はつながっていないが、夢を共にした兄弟。
Canby は、Chumby の夢を別の DNA で再現した存在である。
SWF to Binary 連続キャプチャシステム 動作仕様書
本システムは、Adobe Flash (SWF) ファイルを Linux 上で実行し、その描画画面をリアルタイムにキャプチャして、マイコン(ESP32等)が直接読み取り可能な RGB565 形式のバイナリファイル へ変換・更新し続けるものです。
1. システム概要
本システムは、物理的なモニターを必要としない「仮想画面」をメモリ上に作成し(Xvfb)、そこで再生されるFlashアニメーション(Ruffle)をFFmpegで高速に連写することで、バイナリデータとして抽出します。
2. コンポーネント構成
- Xvfb (仮想ディスプレイ): 物理的なモニターを必要としないメモリ上の画面を作成します。
- Ruffle (Flashプレイヤー): SWFファイルを読み込み、Xvfb上の画面に描画します。
- FFmpeg (レコーダー): 仮想画面をキャプチャし、指定のピクセル形式に変換して保存します。
- CC.sh (オーケストレーター): 上記3つの起動・停止・ループ・ファイル管理を制御するメインスクリプト。
3. 動作フロー
■ ステップ1:初期化(クリーンアップ)
以前の実行で残ってしまったプロセスを pkill で強制終了し、ロックファイルを削除します。これにより、多重起動によるエラーを防ぎます。
■ ステップ2:仮想環境の構築
- Xvfb 起動: ディスプレイ番号 :99 に、480x320などの仮想画面を展開します。
- Ruffle 起動: DISPLAY=:99 をターゲットに、SWFファイルを再生します。
■ ステップ3:キャプチャループ(メイン処理)
以下の処理を while true で繰り返します。
- キャプチャ: FFmpeg が画面を抽出し、一時ファイル(.tmp)に書き出します。
- 整合性チェック: ファイルサイズが期待通りか確認します。
- アトミック更新: チェック後、mv コマンドで本番ファイル(.bin)へ一瞬で置き換えます。
※重要: mv を使うことで、クライアント側が書き込み途中の不完全なデータを読むのを防ぎます。
4. プロセス管理と強制終了について
スクリプトを終了する際、Ctrl+C で止めれば trap により stop_all 関数が呼ばれ、裏側のプロセスも掃除されます。居残った場合は以下のコマンドで掃除が必要です。
pkill -9 -f "Xvfb :99"
pkill -9 -f "ruffle"
pkill -9 -f "ffmpeg"
5. 主要パラメータ
- FPS: フレームレート。高いと滑らかになりますが、CPU負荷が増大します。
- PIX_FMT: rgb565be(多くのマイコン用液晶が採用している Big Endian 形式)。
Canyby のアルバイト(笑)
Canby はまあまあの速度(0.9秒毎)で(ローカル)サーバーに画像データを取りに行けます。
ということは、Canby のプログラムは改造なしで:
o サーバ側で仮想画面を立ち上げる。
o その画面で Chrome を立ち上げ、任意のURLを表示させる。(当然、コンテンツは変化していてよい)
o それをキャプチャして .bin フォーマット画像を生成する。
o Canbyは、それを取りに行く。
下記は、温度表示一覧を縦型に表示させてみたもです。(縦画像に変換するのは、サーバー側で処理している)

■ WW.sh のソース
- URL は google になっています。
- ../Canby ディレクトリにデーターを送っています。
- 起動と停止などは、 -help で確認してください。
#!/bin/bash # WW.sh - Chrome画面キャプチャスクリプト(ステータス機能なし版) # ステータス確認は status.sh を使用してください # --- 設定項目 --- TARGET_URL="https://www.google.com/" LOGICAL_WIDTH=320 LOGICAL_HEIGHT=480 OUTPUT_WIDTH=480 OUTPUT_HEIGHT=320 DISPLAY_NUM=":99" FPS=2 PIX_FMT="rgb565be" OUTPUT_FILE="../Canby/XV1900CU.bin" PNG_FILE="../Canby/XV1900CU.png" TMP_FILE="${OUTPUT_FILE}.tmp" CHROME_PROFILE="/tmp/chrome-profile-web2bin" CHROME_PID_FILE="/tmp/chrome_web2bin.pid" MAIN_PID_FILE="/tmp/WW_main.pid" LOCK_FILE="/tmp/WW.lock" LOG_FILE="/var/log/WW.log" # 計算 EXPECTED_SIZE=$((OUTPUT_WIDTH * OUTPUT_HEIGHT * 2)) # rgb565: 2バイト/ピクセル # --- 関数定義 --- # ログ記録 log_message() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" > /dev/null 2>&1 } # すべてのプロセスを停止 stop_all() { log_message "停止中... プロセスを掃除します。" # メインプロセスの停止 if [ -f "$MAIN_PID_FILE" ]; then MAIN_PID=$(cat "$MAIN_PID_FILE") if kill -0 "$MAIN_PID" 2>/dev/null; then kill -TERM "$MAIN_PID" 2>/dev/null sleep 2 kill -9 "$MAIN_PID" 2>/dev/null > /dev/null 2>&1 log_message "メインプロセスを停止しました (PID: $MAIN_PID)" fi rm -f "$MAIN_PID_FILE" fi # Chromeプロセスの停止(プロファイルベースで検索) pkill -9 -f "chrome.*$CHROME_PROFILE" 2>/dev/null log_message "Chromeプロセスを停止しました" # その他のプロセス停止 pkill -9 -f "Xvfb $DISPLAY_NUM" 2>/dev/null pkill -9 -f "ffmpeg.*DISPLAY=$DISPLAY_NUM" 2>/dev/null pkill -9 -f "WW.sh.*--daemon" 2>/dev/null # ファイル掃除 rm -rf "$CHROME_PROFILE" 2>/dev/null rm -f /tmp/.X${DISPLAY_NUM:1}-lock 2>/dev/null rm -f "$LOCK_FILE" 2>/dev/null rm -f "$TMP_FILE" 2>/dev/null rm -f "$CHROME_PID_FILE" 2>/dev/null log_message "すべてのプロセスを停止しました。" echo "WW.sh を停止しました。" } # スクリプトの使用方法を表示 show_usage() { echo "使用方法: $0 [オプション]" echo "オプション:" echo " -start デーモンとして起動(ログアウト後も継続)" echo " -stop 実行中のデーモンを停止" echo " -png 単一PNGスナップショットを作成" echo " -log ログファイルを表示" echo " -help このヘルプを表示" echo "" echo "ステータス確認: ./status.sh を使用してください" echo "引数を指定しない場合は、対話型で起動します。" } # ログ表示 show_log() { if [ -f "$LOG_FILE" ]; then echo "=== WW.sh ログファイル (最新20行) ===" tail -20 "$LOG_FILE" else echo "ログファイルが存在しません: $LOG_FILE" fi } # PNGスナップショット作成 create_png() { echo "PNGスナップショットを作成します..." # 一時的なXvfb起動 Xvfb $DISPLAY_NUM -screen 0 ${LOGICAL_WIDTH}x${LOGICAL_HEIGHT}x24+32 -ac -nolisten tcp & XVFB_PID=$! sleep 3 # Chrome起動(一時的) export DISPLAY=$DISPLAY_NUM export GNOME_KEYRING_CONTROL="" export GNOME_KEYRING_PID="" google-chrome \ --user-data-dir="/tmp/chrome-snapshot" \ --password-store=basic \ --no-first-run \ --no-default-browser-check \ --app="$TARGET_URL" \ --window-size=$LOGICAL_WIDTH,$LOGICAL_HEIGHT \ --window-position=0,0 \ --disable-infobars \ --hide-scrollbars \ --disable-gpu \ --disable-software-rasterizer \ --no-sandbox \ --disable-dev-shm-usage \ --silent-launch \ --test-type > /dev/null 2>&1 & CHROME_PID=$! sleep 5 # PNGキャプチャ echo "キャプチャ中..." ffmpeg -loglevel quiet -f x11grab -draw_mouse 0 -video_size ${LOGICAL_WIDTH}x${LOGICAL_HEIGHT} -i ${DISPLAY_NUM}.0 \ -vframes 1 -vf "transpose=1" -y "$PNG_FILE" # 後処理 kill -9 "$CHROME_PID" 2>/dev/null kill -9 "$XVFB_PID" 2>/dev/null pkill -f "Xvfb $DISPLAY_NUM" 2>/dev/null rm -rf "/tmp/chrome-snapshot" 2>/dev/null rm -f /tmp/.X${DISPLAY_NUM:1}-lock 2>/dev/null if [ -f "$PNG_FILE" ]; then echo "完了: $PNG_FILE" else echo "エラー: PNGファイルの作成に失敗しました" exit 1 fi } # 完全なデーモン化関数 daemonize() { # 既存のファイルディスクリプタを閉じる exec 0</dev/null exec 1>/dev/null exec 2>/dev/null # 新しいセッションを作成(完全に親プロセスから分離) setsid "$0" --real-daemon "$@" & exit 0 } # 実際のデーモン処理(完全に分離された環境で実行) run_real_daemon() { log_message "=== WW.sh デーモン起動 ===" # 自身のPIDを保存 echo $$ > "$MAIN_PID_FILE" log_message "メインプロセスPID: $$" # ロックファイルを作成 touch "$LOCK_FILE" # 終了時の処理を設定 trap 'log_message "終了シグナルを受信しました。掃除します..."; stop_all; exit 0' SIGINT SIGTERM EXIT # ログディレクトリ作成 mkdir -p "$(dirname "$LOG_FILE")" # 掃除と準備 log_message "1. 既存プロセスを掃除中..." pkill -9 -f "Xvfb $DISPLAY_NUM" 2>/dev/null pkill -9 -f "chrome.*$CHROME_PROFILE" 2>/dev/null pkill -9 -f "ffmpeg.*DISPLAY=$DISPLAY_NUM" 2>/dev/null rm -rf "$CHROME_PROFILE" 2>/dev/null rm -f /tmp/.X${DISPLAY_NUM:1}-lock 2>/dev/null sleep 3 # 1. 仮想画面の作成 log_message "2. Xvfbを起動中..." nohup Xvfb $DISPLAY_NUM -screen 0 ${LOGICAL_WIDTH}x${LOGICAL_HEIGHT}x24+32 -ac -nolisten tcp > /dev/null 2>&1 & sleep 5 # Xvfbの起動確認 if ! pgrep -f "Xvfb $DISPLAY_NUM" > /dev/null; then log_message "エラー: Xvfbの起動に失敗しました" stop_all exit 1 fi log_message "Xvfb起動完了" # 2. ブラウザ起動 log_message "3. Chromeを起動中..." export DISPLAY=$DISPLAY_NUM export GNOME_KEYRING_CONTROL="" export GNOME_KEYRING_PID="" # Chrome起動(簡易オプションで) nohup google-chrome \ --user-data-dir="$CHROME_PROFILE" \ --app="$TARGET_URL" \ --window-size=$LOGICAL_WIDTH,$LOGICAL_HEIGHT \ --no-first-run \ --disable-infobars \ --hide-scrollbars \ --no-sandbox \ --disable-gpu \ --disable-software-rasterizer \ --silent-launch \ --test-type > /dev/null 2>&1 & CHROME_PID=$! # PIDファイルは作成するが、プロセスが変わっても問題ないようにする echo $CHROME_PID > "$CHROME_PID_FILE" log_message "Chrome起動完了。初期PID: $CHROME_PID" # Chromeの起動を確実にする sleep 10 # Chromeプロセスの生存確認(プロファイルベースで) CHROME_COUNT=$(pgrep -f "chrome.*$CHROME_PROFILE" | wc -l) if [ "$CHROME_COUNT" -eq 0 ]; then log_message "エラー: Chromeの起動に失敗しました" stop_all exit 1 fi log_message "4. キャプチャ設定" log_message " フォーマット: $PIX_FMT" log_message " 期待サイズ: $EXPECTED_SIZE バイト" log_message " ループ間隔: 0.9秒" log_message "連続キャプチャ開始" # メインキャプチャループ FRAME_COUNT=0 START_TIME=$(date +%s) LAST_LOG_TIME=$START_TIME while true; do FRAME_COUNT=$((FRAME_COUNT + 1)) CURRENT_TIME=$(date +%s) # 一時ファイル名 TEMP_FILE="/tmp/frame_$$_${FRAME_COUNT}.bin" # 単一フレームをキャプチャ ffmpeg -loglevel error -f x11grab -draw_mouse 0 \ -video_size ${LOGICAL_WIDTH}x${LOGICAL_HEIGHT} \ -i ${DISPLAY_NUM}.0 \ -vf "transpose=1,format=rgb24" \ -sws_flags neighbor \ -pix_fmt rgb565be \ -vcodec rawvideo \ -f rawvideo \ -frames 1 \ -y "$TEMP_FILE" 2>&1 if [ -f "$TEMP_FILE" ]; then ACTUAL_SIZE=$(stat -c%s "$TEMP_FILE" 2>/dev/null || echo 0) if [ "$ACTUAL_SIZE" -eq "$EXPECTED_SIZE" ]; then # 正しいサイズの場合、アトミックに上書き mv -f "$TEMP_FILE" "$OUTPUT_FILE" # 定期的なログ出力(2分ごと) if [ $((CURRENT_TIME - LAST_LOG_TIME)) -ge 120 ]; then ELAPSED=$((CURRENT_TIME - START_TIME)) if [ $ELAPSED -gt 0 ]; then ACTUAL_FPS=$(echo "scale=1; $FRAME_COUNT / $ELAPSED" | bc -l) log_message "動作中: $FRAME_COUNT フレーム処理 (平均FPS: $ACTUAL_FPS)" # プロセス状態もログに記録 CHROME_COUNT=$(pgrep -f "chrome.*$CHROME_PROFILE" | wc -l) FFMPEG_COUNT=$(pgrep -f "ffmpeg.*x11grab.*:99" | wc -l) log_message " プロセス状態: Chrome($CHROME_COUNT), FFmpeg($FFMPEG_COUNT)" LAST_LOG_TIME=$CURRENT_TIME fi fi else # サイズが異なる場合 if [ $((CURRENT_TIME - LAST_LOG_TIME)) -ge 300 ]; then # 5分ごと log_message "警告: フレームサイズ異常 ($ACTUAL_SIZEバイト, 期待: $EXPECTED_SIZEバイト)" LAST_LOG_TIME=$CURRENT_TIME fi # それでも最初の1フレーム分を抽出 if [ "$ACTUAL_SIZE" -ge "$EXPECTED_SIZE" ]; then dd if="$TEMP_FILE" of="$OUTPUT_FILE" bs=$EXPECTED_SIZE count=1 2>/dev/null fi rm -f "$TEMP_FILE" fi else if [ $((CURRENT_TIME - LAST_LOG_TIME)) -ge 300 ]; then # 5分ごと log_message "警告: フレームキャプチャ失敗" LAST_LOG_TIME=$CURRENT_TIME fi fi # 指定した間隔で待機(0.9秒) sleep 0.9 done } # --- メイン処理 --- case "$1" in -start|--start|start) # 既に実行中かチェック if [ -f "$LOCK_FILE" ] && [ -f "$MAIN_PID_FILE" ]; then PID=$(cat "$MAIN_PID_FILE") if kill -0 "$PID" 2>/dev/null; then echo "WW.sh は既に実行中です (PID: $PID)" echo "停止するには: $0 -stop" echo "状態確認: ./status.sh" exit 1 else # プロセスは死んでいるがロックファイルが残っている場合 rm -f "$LOCK_FILE" "$MAIN_PID_FILE" fi fi # 完全にデーモン化して起動 echo "WW.sh をデーモンとして起動します..." echo "ログファイル: $LOG_FILE" echo "出力ファイル: $OUTPUT_FILE" echo "" echo "ログアウトしても動作を継続します。" echo "停止するには: $0 -stop" echo "状態確認: ./status.sh" # デーモン化して起動 daemonize # 少し待ってからステータスを表示 sleep 3 echo "起動処理を開始しました。" echo "詳細はログファイルを確認してください:" echo "tail -f $LOG_FILE" ;; --real-daemon) # 実際のデーモンプロセス(完全に分離された環境) run_real_daemon ;; -stop|--stop|stop) if [ ! -f "$LOCK_FILE" ] && [ ! -f "$MAIN_PID_FILE" ]; then echo "WW.sh は実行中ではありません。" exit 1 fi echo "WW.sh を停止します..." stop_all ;; -png|--png|png) create_png ;; -log|--log|log) show_log ;; -help|--help|help) show_usage ;; *) # 引数がない場合は対話型メニュー if [ $# -eq 0 ]; then echo "=== WW.sh 画面キャプチャスクリプト ===" echo "ステータス確認: ./status.sh を使用してください" echo "" echo "現在の状態(簡易表示):" if [ -f "$LOCK_FILE" ]; then echo "- システム: 実行中(ロックファイルあり)" else echo "- システム: 停止中" fi if [ -f "$OUTPUT_FILE" ]; then FILE_AGE=$(($(date +%s) - $(stat -c %Y "$OUTPUT_FILE" 2>/dev/null || echo 0))) if [ "$FILE_AGE" -lt 10 ]; then echo "- 出力ファイル: 最新 ($FILE_AGE秒前)" else echo "- 出力ファイル: 更新あり ($FILE_AGE秒前)" fi fi echo "" echo "操作を選択してください:" echo "1) 起動 (-start)" echo "2) 停止 (-stop)" echo "3) PNGスナップショット (-png)" echo "4) ログ表示 (-log)" echo "5) 状態確認 (./status.sh を実行)" echo "6) ヘルプ (-help)" echo "7) 終了" echo "" read -p "選択 [1-7]: " choice case $choice in 1) bash "$0" -start ;; 2) bash "$0" -stop ;; 3) bash "$0" -png ;; 4) bash "$0" -log ;; 5) echo "状態確認は ./status.sh を実行してください" if [ -f "./status.sh" ]; then read -p "今すぐ実行しますか? (y/N): " exec_status if [[ "$exec_status" =~ ^[Yy]$ ]]; then ./status.sh fi else echo "status.sh が見つかりません。作成してください。" fi ;; 6) bash "$0" -help ;; 7) exit 0 ;; *) echo "無効な選択です。" exit 1 ;; esac else echo "不明なオプション: $1" show_usage exit 1 fi ;; esac exit 0======================================== WW.sh 実行環境 ======================================== [ OS ] Name : Pearl Version : 11 (Cade) Pretty : Pearl Desktop 11 (PDE) Kernel : Linux 5.15.0-101-generic x86_64 GNU/Linux Init : systemd [ CPU ] アーキテクチャ: x86_64 CPU 操作モード: 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual バイト順序: Little Endian CPU: 4 オンラインになっている CPU のリスト: 0-3 ベンダー ID: GenuineIntel モデル名: Intel(R) Core(TM) i5-6500T CPU @ 2.50GHz CPU ファミリー: 6 モデル: 94 コアあたりのスレッド数: 1 ソケットあたりのコア数: 4 ソケット数: 1 ステッピング: 3 CPU 最大 MHz: 3100.0000 CPU 最小 MHz: 800.0000 [ Memory ] total used free shared buff/cache available Mem: 15Gi 2.3Gi 2.1Gi 283Mi 11Gi 12Gi Swap: 1.7Gi 0B 1.7Gi [ Storage ] NAME TYPE SIZE MODEL sda disk 953.9G SA66001TBY ├─sda1 part 1M ├─sda2 part 513M └─sda3 part 953.4G [ Virtualization ] none none (bare metal) [ Software ] bash : GNU bash, バージョン 5.1.16(1)-release (x86_64-pc-linux-gnu) ffmpeg : ffmpeg version 4.4.2-0ubuntu0.22.04.1 Copyright (c) 2000-2021 the FFmpeg developers Xvfb : Unrecognized option: -version chrome : Google Chrome 144.0.7559.96 [ Generated ] 2026年 1月 26日 月曜日 18:21:43 JST
DIM(ナイトモード)バージョン
WW.sh をダウンロード
(URLは Google になっている)
ww_config をダウンロード
(.shを削除して使用する)
ガーデンカメラの映像も数秒毎で表示可能。
(rtsp://admin:@192.168.X.Y:10553 のようなアクセスが可能なカメラに限る)
WW.py をダウンロード
(Python .sh を削除して使用)