サイト収益をリアルタイムで表示する家具を作った
いつものようにYouTubeを漁っていると、何やら面白そうな機器を発見。
デジタルディスプレイ上に天気・株価・YouTubeのチャンネル登録者数などを表示できるらしい…
調べてみると、APIが叩けることが判明。
AdSenseの収益を常時表示してモチベーションを上げられないかと思い立ちました。
表示までの備忘録を残します。
システムアーキテクチャ
今回の構成はシンプルです。
[ Cloud ]
+---------------------+
| Google AdSense |
+----------+----------+
[ Local Network ]
|
+----------v----------+
| Raspberry Pi |
+----------+----------+
|
+----------v----------+
| Pixoo 64 |
+---------------------+- クラウド上の Google AdSense API から収益データを取得
- ローカルネットワーク内の Raspberry Pi (デバイスは何でも可)でPythonスクリプトを実行
- そのスクリプトが、Pixoo 64のローカルHTTP APIを叩く
- 最終的に収益をディスプレイへ表示
という流れです。
Pixoo APIの疎通確認
こちらのページにAPIにのリクエストがまとめてありました。
まずは疎通確認を行います。
まずは同じネットワークに接続したデバイスからPixooに直接リクエストを送ってみます。
<pixooのipアドレス>は自分のものに読み替えてください。
curl -v -X POST http://<pixooのipアドレス>/post \
-d '{"Command":"Device/GetDeviceTime"}'error_code: 0 が返れば疎通成功です。
次にテキストが表示できるか確かめます。
curl -sS -X POST http://<pixooのipアドレス>/post -H "Content-Type: application/json" \
-d '{"Command":"Draw/SendHttpText","TextId":1,"x":0,"y":40,"dir":0,"font":4,"TextWidth":64,"speed":0,"TextString":"HELLO PIXOO","color":"#FFFFFF","align":1}'しかしここで問題がありました。
error_code 0 が返っているのに、画面が変わりません。
Pixooの描画の仕組み
Pixooは単に SendHttpText を送るだけでは画面に表示することができないようです。
そのため、まず SendHttpGif を送って、画面を真っ暗にします。
curl -sS -X POST http://<pixooのipアドレス>/post -H "Content-Type: application/json" \
-d "{\"Command\":\"Draw/SendHttpGif\",\"PicNum\":1,\"PicID\":$PICID,\"PicOffset\":0,\"PicSpeed\":100,\"PicWidth\":64,\"PicData\":\"$PICDATA\"}"次にテキストが表示できるか確かめます。
curl -sS -X POST http://<pixooのipアドレス>/post -H "Content-Type: application/json" \
-d '{"Command":"Draw/SendHttpText","TextId":1,"x":0,"y":40,"dir":0,"font":4,"TextWidth":64,"speed":0,"TextString":"HELLO PIXOO","color":"#FFFFFF","align":1}'この順番で送ることで、表示が切り変わることがわかりました。
以下コマンドを送ると標準の表示に戻ります。
curl -X POST http://<pixooのipアドレス>/post \
-H "Content-Type: application/json" \
-d '{"Command":"Channel/SetIndex","SelectIndex":0}'Pythonから最小構成で表示してみる
curlで動作確認できたので、Pythonから画面リセット+テキスト表示のリクエストを叩きます。
import base64
import requests
from PIL import Image
PIXOO_IP = "<pixooのipアドレス>" # 自分のpixooのipを指定する
PIXOO_URL = f"http://{PIXOO_IP}/post"
def post(payload: dict) -> dict:
r = requests.post(PIXOO_URL, json=payload, timeout=5)
r.raise_for_status()
return r.json()
def make_black_frame_picdata_64x64() -> str:
"""64x64黒画像を作って、PixooのPicData用にbase64エンコードして返す"""
img = Image.new("RGB", (64, 64), (0, 0, 0))
raw = img.tobytes() # RGB 64*64*3 bytes
return base64.b64encode(raw).decode("ascii")
def main():
# 背景(黒フレーム)を生成
picdata = make_black_frame_picdata_64x64()
# PicID取得
picid_resp = post({"Command": "Draw/GetHttpGifId"})
pic_id = picid_resp.get("PicID") or picid_resp.get("PicId") or 1
print("Draw/GetHttpGifId:", picid_resp, "=> PicID:", pic_id)
# 背景送信
gif_resp = post({
"Command": "Draw/SendHttpGif",
"PicNum": 1,
"PicID": int(pic_id),
"PicOffset": 0,
"PicSpeed": 100,
"PicWidth": 64,
"PicData": picdata,
})
print("Draw/SendHttpGif:", gif_resp)
text_resp = post({
"Command": "Draw/SendHttpText",
"TextId": 1,
"x": 0,
"y": 40,
"dir": 0,
"font": 4,
"TextWidth": 64,
"speed": 0,
"TextString": "HELLO PIXOO",
"color": "#FFFFFF",
"align": 1
})
print("Draw/SendHttpText:", text_resp)
if __name__ == "__main__":
main()
これでPython経由で表示できることを確認しました。
Adsense APIから収益を取得する
AdSense APIの認証および環境構築は、以下の記事を参考にしました。
https://qiita.com/jianyi/items/9a1c02851de4a8a8f1c2
今回取得しているデータは以下の3つです。
TODAY →今日の収益
LAST_7_DAYS →1週間の収益
LAST_30_DAYS→1ヶ月の収益
これらをそのまま画面に表示してみます。
import os
import time
import base64
from pathlib import Path
from typing import Optional, Any, Dict
import requests
from PIL import Image
from google_auth_oauthlib.flow import InstalledAppFlow
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
# ====== 設定 ======
PIXOO_IP = "<pixooのipアドレス>" # ここを書き換える
PIXOO_URL = f"http://{PIXOO_IP}/post"
SCOPES = ["https://www.googleapis.com/auth/adsense.readonly"]
# AdSense dateRange
RANGE_TODAY = "TODAY"
RANGE_WEEK = "LAST_7_DAYS"
RANGE_MONTH = "LAST_30_DAYS"
# ==================
HERE = Path(__file__).resolve().parent
CLIENT_SECRET = HERE / "client_secret.json" # 手元のファイル名に合わせてください
TOKEN_FILE = HERE / "token.json"
def post(payload: Dict[str, Any]) -> Dict[str, Any]:
r = requests.post(PIXOO_URL, json=payload, timeout=5)
r.raise_for_status()
return r.json()
def pixoo_get_http_gif_id() -> int:
data = post({"Command": "Draw/GetHttpGifId"})
pic_id = data.get("PicID") or data.get("PicId") or data.get("picId") or 1
return int(pic_id)
def make_black_frame_picdata_64x64() -> str:
img = Image.new("RGB", (64, 64), (0, 0, 0))
raw = img.tobytes()
return base64.b64encode(raw).decode("ascii")
def pixoo_send_black_bg():
picdata = make_black_frame_picdata_64x64()
pic_id = pixoo_get_http_gif_id()
resp = post({
"Command": "Draw/SendHttpGif",
"PicNum": 1,
"PicID": pic_id,
"PicOffset": 0,
"PicSpeed": 100,
"PicWidth": 64,
"PicData": picdata,
})
print("Draw/SendHttpGif:", resp)
def pixoo_text(text_id: int, x: int, y: int, s: str):
resp = post({
"Command": "Draw/SendHttpText",
"TextId": text_id,
"x": x,
"y": y,
"dir": 0,
"font": 4,
"TextWidth": 64,
"speed": 0,
"TextString": s,
"color": "#FFFFFF",
"align": 1,
})
print("Draw/SendHttpText:", resp)
def get_adsense_service() -> Any:
creds: Optional[Credentials] = None
if TOKEN_FILE.exists():
creds = Credentials.from_authorized_user_file(str(TOKEN_FILE), SCOPES)
if not creds or not creds.valid:
flow = InstalledAppFlow.from_client_secrets_file(str(CLIENT_SECRET), SCOPES)
# 初回はローカルで認証(GUIなし環境なら run_console() に変える)
creds = flow.run_local_server(port=0)
TOKEN_FILE.write_text(creds.to_json(), encoding="utf-8")
return build("adsense", "v2", credentials=creds)
def pick_account_name(service: Any) -> str:
resp = service.accounts().list(pageSize=50).execute()
accounts = resp.get("accounts", [])
if not accounts:
raise RuntimeError("No AdSense accounts found.")
return accounts[0]["name"]
def get_total(service: Any, account_name: str, date_range: str) -> float:
report = service.accounts().reports().generate(
account=account_name,
dateRange=date_range,
metrics=["ESTIMATED_EARNINGS"],
).execute()
totals = report.get("totals")
if not totals:
return 0.0
# totals は dict の場合がある
if isinstance(totals, dict):
return float(totals["cells"][0]["value"])
if isinstance(totals, list):
return float(totals[0]["cells"][0]["value"])
return 0.0
def main():
# 1) AdSenseから取得
service = get_adsense_service()
account = pick_account_name(service)
today = get_total(service, account, RANGE_TODAY)
week = get_total(service, account, RANGE_WEEK)
month = get_total(service, account, RANGE_MONTH)
print("Fetched:", {"today": today, "week": week, "month": month})
# 2) Pixooへ表示(UI調整前のベタ表示)
pixoo_send_black_bg()
# SendHttpGif直後に少し待つ(表示が一瞬で消える対策)
time.sleep(0.2)
# 円記号は□になることがあるので、まずは数値だけ表示(必要なら "JPY" を別で)
pixoo_text(1, 0, 6, f"TODAY: {int(round(today))}")
pixoo_text(2, 0, 22, f"7D: {int(round(week))}")
pixoo_text(3, 0, 38, f"30D: {int(round(month))}")
if __name__ == "__main__":
main()
画面を暗くした後に少し待たないと、テキスト表示がうまくいかないことが頻発したため、
SendHttpGif の直後に少し待ち時間を入れることで安定して表示させるようにしました。
UIをドット絵風にいじる
せっかくドットなので、UIをいい感じに調整します。

以下の流れで画面を表示させるようにしました。
- AdSense APIから収益を取得(today / 7days / 30days)
- Pixooに背景(GIF=64×64の画像)を送る
- ここで「収益以外の情報(タイトル・枠・アイコン等)」が描かれた背景を描画する
- 短い待ち(例:time.sleep(0.2))
- 背景描画がPixoo側で完全に反映されるのを待つ
- Pixooにテキストを送る(収益の数値を SendHttpText で複数行)
import base64
import time
import traceback
from pathlib import Path
from typing import Optional, Any, Dict
import requests
from PIL import Image, ImageDraw, ImageFont
from google_auth_oauthlib.flow import InstalledAppFlow
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
DEBUG = True
SCOPES = ["https://www.googleapis.com/auth/adsense.readonly"]
# ===== 設定 =====
PIXOO_IP = "<pixooのipアドレス>" # ここを書き換える
UPDATE_SECONDS = 30 * 60
RANGE_TODAY = "TODAY"
RANGE_WEEK = "LAST_7_DAYS"
RANGE_MONTH = "LAST_30_DAYS"
# =================
HERE = Path(__file__).resolve().parent
CLIENT_SECRET = HERE / "client_secret.json" # 手元のファイル名に合わせてください
TOKEN_FILE = HERE / "token.json"
PIXOO_URL = f"http://{PIXOO_IP}/post"
FONT_FILE = HERE / "PressStart2P-Regular.ttf"
# ---------------- AdSense ----------------
def get_adsense_service():
creds: Optional[Credentials] = None
if TOKEN_FILE.exists():
creds = Credentials.from_authorized_user_file(str(TOKEN_FILE), SCOPES)
if not creds or not creds.valid:
flow = InstalledAppFlow.from_client_secrets_file(str(CLIENT_SECRET), SCOPES)
creds = flow.run_local_server(port=0)
TOKEN_FILE.write_text(creds.to_json(), encoding="utf-8")
return build("adsense", "v2", credentials=creds)
def pick_account_name(service):
resp = service.accounts().list(pageSize=50).execute()
accounts = resp.get("accounts", [])
if not accounts:
raise RuntimeError("No AdSense accounts found.")
return accounts[0]["name"]
def get_total(service, account_name, date_range) -> float:
report = service.accounts().reports().generate(
account=account_name,
dateRange=date_range,
metrics=["ESTIMATED_EARNINGS"],
).execute()
totals = report.get("totals")
if not totals:
return 0.0
if isinstance(totals, dict):
return float(totals["cells"][0]["value"])
if isinstance(totals, list):
return float(totals[0]["cells"][0]["value"])
return 0.0
# ---------------- Pixoo ----------------
def pixoo_post(payload: Dict[str, Any]) -> Dict[str, Any]:
r = requests.post(PIXOO_URL, json=payload, timeout=5)
r.raise_for_status()
return r.json()
def pixoo_get_http_gif_id() -> int:
data = pixoo_post({"Command": "Draw/GetHttpGifId"})
pic_id = data.get("PicID") or data.get("PicId") or data.get("picId")
try:
return int(pic_id)
except Exception:
return 1
def pixoo_send_http_gif(picdata_b64: str):
pic_id = pixoo_get_http_gif_id()
payload: Dict[str, Any] = {
"Command": "Draw/SendHttpGif",
"PicNum": 1,
"PicID": pic_id,
"PicOffset": 0,
"PicSpeed": 100,
"PicWidth": 64,
"PicData": picdata_b64,
}
pixoo_post(payload)
def pixoo_text(text_id: int, x: int, y: int, text: str):
payload = {
"Command": "Draw/SendHttpText",
"TextId": text_id,
"x": x,
"y": y,
"dir": 0,
"font": 4,
"TextWidth": 64,
"speed": 0,
"TextString": text,
"color": "#FFFFFF",
"align": 1,
}
pixoo_post(payload)
def format_money(v: float) -> str:
return f"{int(round(v))}"
# ---------------- Pixel Background(アイコン復活版) ----------------
def _load_font(size: int):
try:
if FONT_FILE.exists():
return ImageFont.truetype(str(FONT_FILE), size)
except Exception:
pass
return ImageFont.load_default()
def _draw_bg(dr: ImageDraw.ImageDraw):
for y in range(64):
for x in range(64):
base = 10
if ((x // 4) + (y // 4)) % 2 == 0:
base = 12
dr.point((x, y), fill=(base, base, base))
dr.rectangle([0, 0, 63, 63], outline=(70, 70, 70))
dr.rectangle([2, 2, 61, 61], outline=(30, 30, 30))
def _draw_icon_clock(dr: ImageDraw.ImageDraw, x: int, y: int):
outline = (160, 220, 255)
fill = (30, 60, 70)
dr.rectangle([x, y, x + 7, y + 7], outline=outline, fill=fill)
dr.point((x + 4, y + 4), fill=outline)
dr.point((x + 4, y + 3), fill=outline)
dr.point((x + 5, y + 4), fill=outline)
def _draw_icon_calendar(dr: ImageDraw.ImageDraw, x: int, y: int):
outline = (220, 220, 220)
fill = (70, 70, 70)
top = (120, 160, 255)
dr.rectangle([x, y + 1, x + 7, y + 7], outline=outline, fill=fill)
dr.rectangle([x, y + 1, x + 7, y + 2], fill=top)
def _draw_icon_coin(dr: ImageDraw.ImageDraw, x: int, y: int):
"""8x8 round coin sprite (fills the existing 8x8 slot; no coordinate changes)."""
edge = (120, 90, 0) # dark gold edge
face = (255, 210, 0) # gold face
hi = (255, 240, 150) # highlight
# 8x8 pattern (top-left at x, y):
# ..####..
# .######.
# ##****##
# ##*+**##
# ##****##
# ##****##
# .######.
# ..####..
pattern = [
"..####..",
".######.",
"##****##",
"##*+**##",
"##****##",
"##****##",
".######.",
"..####..",
]
for py, row in enumerate(pattern):
for px, ch in enumerate(row):
if ch == ".":
continue
if ch == "#":
color = edge
elif ch == "*":
color = face
else: # '+'
color = hi
dr.point((x + px, y + py), fill=color)
def render_background_picdata() -> str:
im = Image.new("RGB", (64, 64), (0, 0, 0))
dr = ImageDraw.Draw(im)
_draw_bg(dr)
f_title = _load_font(8)
f_row = _load_font(7)
blue = (120, 180, 255)
dr.text((6, 2), "SITE", font=f_title, fill=blue)
dr.text((6, 11), "REVENUE", font=f_title, fill=blue)
dr.line([6, 22, 57, 22], fill=(60, 60, 60))
# アイコン + ラベル(TD→1Dのみ変更)
_draw_icon_clock(dr, 6, 26)
_draw_icon_calendar(dr, 6, 38)
_draw_icon_coin(dr, 6, 51)
dr.text((16, 24), "1D", font=f_row, fill=(220, 220, 220))
dr.text((16, 37), "7D", font=f_row, fill=(220, 220, 220))
dr.text((16, 49), "30D", font=f_row, fill=(220, 220, 220))
return base64.b64encode(im.tobytes()).decode("ascii")
BACKGROUND_PICDATA = render_background_picdata()
# ---------------- Main Loop ----------------
def main():
service = get_adsense_service()
account = pick_account_name(service)
while True:
try:
today = get_total(service, account, RANGE_TODAY)
week = get_total(service, account, RANGE_WEEK)
month = get_total(service, account, RANGE_MONTH)
pixoo_send_http_gif(BACKGROUND_PICDATA)
time.sleep(0.2) # 少し待ってからテキストを送らないと、背景が更新される前にテキストが送られてしまうことがある
x_val = 38
pixoo_text(11, x_val, 22, format_money(today))
pixoo_text(12, x_val, 34, format_money(week))
pixoo_text(13, x_val, 46, format_money(month))
except Exception as e:
print("Update failed:", repr(e))
traceback.print_exc()
time.sleep(UPDATE_SECONDS)
if __name__ == "__main__":
main()
Raspberry Piで常時起動できるようにする
テストはMacから行なっていましたが、
プログラムを常時実行させるため、家にあったRaspberry Pi 3Bにコードを移行しました。
systemdで常時起動させるようにしています。
この記事ではやり方については範囲外とします。
まとめ
Pixoo64を使ってGoogle Adsenseから取得した情報を表示させてみました。
コード全体はGitHubに公開しています。
https://github.com/kooooooooooh/pixoo-adsense-display
API連携できればなんでも表示できるので、他にもいろんなものに使えそうです。