編集履歴一覧に戻る
memetanのアイコン画像

memetan が 2024年10月28日19時47分30秒 に編集

コメント無し

本文の変更

# 当選番号表示システム 元々私が作っていたLEDパネルの表示システムがあるのですが、それを地域のお祭りの抽選会で当選番号を表示するのに使用したいという要望をうけ、今回当選番号表示システムとして新しく作成しました。 # 材料 - [LEDパネル×9枚](https://ja.aliexpress.com/item/1005001870711168.html) - [LED送信カード](https://ja.aliexpress.com/item/4000926385809.html) - [LED受信カード](https://ja.aliexpress.com/item/4000926832957.html) - アルミフレーム - Raspberry Pi 400 - [スイッチング電源 直流安定化電源 5V 60A](https://www.amazon.co.jp/gp/product/B0784FRQVY/) # LEDパネルの組み立て LEDパネルは約2年前に製作しそれまでもラズパイをつなげて表示させていました。 LEDパネル自体の組み立てに関しては今回は新規には行っていない(今回はソフトのみ製作した)のですが、思い出して書きます。 ## LEDパネルへの配置と表示 LEDパネルを3×3に置いて送信・受信カードを繋ぎ画面表示できることを確認、配置を決定します。 ![テスト表示](https://camo.elchika.com/d115d2b486815ea19abfa0728a338402f2194a6e/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663265623434382d653938662d343938302d396665342d3062623566363764386665362f33336332613763392d646135352d346437352d393039652d366466353938373438653432/) ## アルミフレームで固定 アルミフレームや3Dプリンタ・レーザーカッターを使った部品で足を作ります。 有識者の方に大変ご協力をいただきました。 ![キャプションを入力できます](https://camo.elchika.com/bbc5893e2eb1fa34bae948b25d7a1e9e7871eaf9/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663265623434382d653938662d343938302d396665342d3062623566363764386665362f63366439653863352d326233612d343762612d623663632d613238393435353262373831/) ![キャプションを入力できます](https://camo.elchika.com/8b25f296d2d861c8f66f3e8f2f8ade779fb81311/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663265623434382d653938662d343938302d396665342d3062623566363764386665362f62326336393735332d333630302d343536332d626135622d323264393833313462343566/) ![キャプションを入力できます](https://camo.elchika.com/5d44038c8625722ea3adfb7a530c3ea8613183b6/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663265623434382d653938662d343938302d396665342d3062623566363764386665362f32303065633337642d353863352d346238662d613232622d643037626530313836333561/)

+

![キャプションを入力できます](https://camo.elchika.com/5ea06c1b1097fc85fcf8260c1ddfafe96dae3cd4/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663265623434382d653938662d343938302d396665342d3062623566363764386665362f31313662643236372d623337382d346363382d626333332d623132383361633432373530/)

# システム構成 ![システム構成図](https://camo.elchika.com/56004c14743aaa79fb422fa63ea19605f3443440/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663265623434382d653938662d343938302d396665342d3062623566363764386665362f64383766303961622d626632642d343261652d613562362d653136396363333731663763/) ラズパイ上ではNode.js、Express、HTMXでWebシステムを構成しており、スマホから操作できるようにしました。XfceはLightDMのオートログインをかませて、アプリの実行はXfceのオートスタートで実行しています。 ## スマホの操作画面 WiFi接続後にLEDパネルの操作画面を開くのは面倒であるため、Captive Portalを使用してWiFi接続時にLEDパネル操作画面を即表示するようにしています。 ![WiFi接続後に表示するLEDパネル操作画面](https://camo.elchika.com/aac3e2dac98549f032cf3c6d14712b900c1b88b6/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663265623434382d653938662d343938302d396665342d3062623566363764386665362f61386335303830652d363235342d346334372d396634632d333837343936393462323735/) 上記操作画面の表示のテキストボックスに入力することで、文字を表示させることができます。 文字を自動スクロールする必要はない(短文しか表示しない)とのことだったので、シンプルに作りました。 文字の長さによって画面の幅になるように自動でフォントサイズを調整しています。 表示番号が切り替わったタイミングでは、それがわかるように2回色を変えて点滅表示させるようにしました。 ## 動作の様子 @[x](https://x.com/siroitori0413/status/1845785880180711711) # プログラム プログラム一式は下記に置いています。 https://github.com/MeemeeLab/remote-signage ## 画面表示部分のソースコード(TypeScript) 上記GitHubの中の「index.tsx」についてこちらにも記載します。 ```TypeScript:index.tsx import express from "express"; import { strict as assert } from "assert"; import { fetch, Agent } from "undici"; import ChildProcess from "child_process"; const nodeHost = process.env["HOST"]; const nodePort = process.env["PORT"] ? parseInt(process.env["PORT"]) : 80; assert(nodeHost); assert(nodePort); const nodeHostString = nodeHost + ":" + nodePort.toString(); function response(html: () => Promise<string>) { return async (_req: express.Request, res: express.Response) => { res.status(200).end((await html()).toString()); }; } async function getCurrentText() { return fetch("http://0.0.0.0/text", { dispatcher: new Agent({ connect: { socketPath: "/tmp/remote-signage.sock" } }) }).then((resp) => resp.json() as Promise<string>); } async function updateCurrentText(text: string) { return fetch("http://0.0.0.0/text", { dispatcher: new Agent({ connect: { socketPath: "/tmp/remote-signage.sock" } }), method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) }); } const app = express(); app.use((req, res, next) => { console.log(req.method, req.path, req.header("Host")); const host = req.header("Host"); if (host !== nodeHost && host !== nodeHostString) { res.redirect("http://" + nodeHostString); return; } next(); }); app.use(express.static("./assets/static")); app.use(express.urlencoded({ extended: true })); function Body({ text }: { text?: string }) { return <> <div class="container"> <div class="box"> <h1 class="title has-text-centered"> {text} </h1> </div> <form> <div class="box"> <div class="field"> <label class="label">表示</label> <div class="control"> <input name="text" class="input" type="text" placeholder="1234..." {...(text ? { value: text } : {})} /> </div> </div> </div> <div class="box"> <div class="field is-grouped"> <div class="control"> <button class="button is-link" hx-post="/update" hx-target="body" > 更新 </button> </div> <div class="control"> <button class="button is-link is-danger" hx-post="/shutdown" hx-confirm="シャットダウンしますか?" hx-target="body" > シャットダウン </button> </div> </div> </div> </form> </div> </>; } app.get("/", response(async () => <> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Remote Signage</title> <link rel="stylesheet" href="/bulma.min.css" /> <script src="/htmx.min.js" /> </head> <body class="m-3"> <Body text={await getCurrentText()} /> </body> </html> </>)); app.post("/update", async (req, res) => { await updateCurrentText(req.body.text); res.status(200).end( (<Body text={req.body.text} />).toString() ); }); app.post("/shutdown", (_req, res) => { ChildProcess.spawnSync("shutdown", ["now"]); res.status(200).end(); }); app.listen(nodePort); ``` # お祭りの抽選会で使用しました お祭りの抽選会で実際に使用しました。 なにしろ手作り作品のため雨が降ったらおしまいだったのですが、抽選会の時間には雨が降らず助かりました。 ![ステージの端で当選番号を入力](https://camo.elchika.com/ecabae2f9861c692ba87c57422f2a35cfae2abb6/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663265623434382d653938662d343938302d396665342d3062623566363764386665362f33616439393533352d646534372d343030372d393033612d393530396135343737626364/)

+

とても景品の多い抽選会で、当選番号は30名くらい?読み上げられたと思います。ステージの端でアナウンスを聞きながら都度表示を切り替えました。

![会場の後ろの方から見たところ](https://camo.elchika.com/d537ae007560078f172777f57676f89cce5581b6/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f65663265623434382d653938662d343938302d396665342d3062623566363764386665362f35303763623931372d336461342d346634622d383066622d396332386361393335626132/)

-

人が多くざわざわした中で抽選番号が発表され、マイクで読み上げられたとはいえ聞き取りづらい方もいたと思います。私をはじめ感覚過敏の人にはつらい環境で

+

人が多くざわざわした中で抽選番号が発表され、マイクで読み上げられたとはいえ聞き取りづらい方もいたと思います。私をはじめ感覚過敏の人にはつらい環境でしたまた耳の聞こえが悪い方もいらっしゃったかもしれません。

この抽選番号表示板が少しでも役に立っていたら嬉しいなと思います。 # 関連情報 https://blog.memetan.dev/entry/2021/09/27/135513 https://blog.memetan.dev/entry/2022/07/09/010046