こんにちは。
最近、本職方面でもLED屋さんしている、ゆうかりです。
まあ、昨今の流行に則り。
オイラも、「PCをLEDで光らせたく」なったので。
やらかしてみましたよ。
結果
準備
以下条件に当てはまる素材を集めて、ツボ錬金でもやらかせばOKです。
- 中にLED仕込めそうで、外からLEDがちゃんと見えそうなケース
- 適当なマトリクスLED
- 多分窒息するので、それなりな放熱手段
まあ、「前面がガラスかアクリル」なケースで探してたのですが。
NZXTのH510i Elite ってケースにしてみました。
んでまあ、CPU排熱を外に出せるように、「kraken M22」という簡易水冷も導入、と。
LEDの選定
昨今流行りの、「HUB75な表面実装フルカラーマトリクスLED」は、ツブの大きさで複数の種類があります。
型番の「P」の後ろの数字が、ツブの大きさです。
今回選定のケースは、前面に12cm ~ 14cmのファンを2つカマすことができるっぽいので。
それっぽい大きさを選定します。
例えば、P5の32x64であれば、大きさは「16cm x 32cm」、ちょっとデカいです。
まあ、代表的な解像度と大きさを表にすると、こんな感じ。
ピッチ | サイズ | 大きさ |
---|---|---|
P2 | 64x128 | 12.8cm x 25.6cm |
P2.5 | 32x64 | 8.0cm x 16.0cm |
P3 | 32x64 | 9.6cm x 19.2cm |
P4 | 32x64 | 12.8cm x 25.6cm |
P5 | 32x64 | 16.0cm x 32.0cm |
P6.67 | 32x64 | 21.3cm x 42.6cm |
ま、今回は、一番上「P2 64x128」を使うことにしてみました。
解像度あるほうが楽しいしね、多分。
なお、短辺64ドット以上の場合、アドレス線の「E」が必要になる可能性が高いです。
後述の基板選定時に、ご留意を。
ラズパイとインターフェースの準備
まあ光らすのにはRaspberryPiを使います。
今回は「RaspberryPi4 4GB」をチョイス。
ケースは適当に、但しGPIOを使うため、公式ケースとかは厳しいです。
次にLEDのインターフェース基板。
今回は余らせまくってた、Adafruitの古い基板を使うことにしました。
但し!、以下2つの改造が必要です。
- ハードウェアPWM駆動化
- アドレス線「E」の追加
まあ、この記載を参考に、線を2本追加と、パターンカット1箇所って感じです。
なお、アドレスE線は、2パターンあるので、まずお使いのパネルが「どっち」か、テスターなどで確認してくださいな。
使われないほうのアドレスE線は、GNDに落ちてます。
*なので双方接続はNGですよっと。
組み立て
裏は、12cmファン用のステーに、M3ネジで固定しておりますよと。
このマグネットは、秋葉原の千石電商の隣のLED屋とかで扱っています、べんりべんり。
LEDは底側のLEDをラズパイに繋げ、そこからチェーンで前面LEDに接続しています。
これは純粋に配線都合です。
電源はSATAから取ることに。
まあ昔からのお約束ですが、パソコンの電源は一般的に、「黄色:12V」「赤:5V」「オレンジ:3.3V」「黒:GND」です。
RaspberryPi4とLEDで、まあ5A以上使いそうですが、パソコン電源なので大丈夫でしょう(適当)。
Raspberry PiとPCのインターフェースは、LANを使用します。
PC側がUSBLANです。
USBは、マザーボードのヘッダから、ケース用の変換コネクタを使って。
あとはH510i Eliteの電源裏にある隙間にラズパイ押し込んで、完成です。
組み込み完成
まあつまり、このPCは。
何気に2環境同居な、「デュアル構成PC」なのですが。
まあ外側からは普通のPCに見えます。
NZXT純正の制御機構「HUE2」ともバッティングしてません。
*ただまあ最初からついていたファンは、全部外しちゃいましたが。
ここまでやらかしておきながら、「ケース無改造」なのです、何げにヒドい・・・。
PC側の準備
PC側は、モニタリング用のソフトが必要です。
今回は、「OpenHardwareMonitor」を使いました。
このソフト、Webサーバ機能があり、外から状況を把握できるのですが。
レンダリングに使っているJSONを、ラズパイ側でもらっちゃおう作戦です。
https://forest.watch.impress.co.jp/docs/review/383668.html
但し上記ソフト、最近のアーキティクチャに対応していないので。
実際は、以下URLから、野良ビルドの「LibreHardwareMonitor」を使っています。
http://takkun87.blog40.fc2.com/blog-category-6.html
Webサーバ有効にして、F/Wを適当に設定、外部から以下JSONをゲットできれば成功です。
http://[IPアドレス]:8085/data.json
内容はこんな感じです(一部抜粋):
{
"id": 0,
"Text": "Sensor",
"Min": "Min",
"Value": "Value",
"Max": "Max",
"ImageURL": "",
"Children": [
{
"id": 1,
"Text": "DESKTOP-HOGEHOGE",
"Min": "",
"Value": "",
"Max": "",
"ImageURL": "images_icon/computer.png",
"Children": [
{
"id": 2,
"Text": "ASRock Z390 Phantom Gaming 6",
"Min": "",
"Value": "",
"Max": "",
"ImageURL": "images_icon/mainboard.png",
"Children": [
{
"id": 3,
"Text": "Nuvoton NCT6791D",
"Min": "",
"Value": "",
"Max": "",
"ImageURL": "images_icon/chip.png",
"Children": [
{
"id": 4,
"Text": "Voltages",
"Min": "",
"Value": "",
"Max": "",
"ImageURL": "images_icon/voltage.png",
"Children": [
{
"id": 5,
"Text": "Vcore",
"Min": "1.392 V",
"Value": "1.392 V",
"Max": "1.408 V",
"SensorId": "/lpc/nct6791d/voltage/0",
"Type": "Voltage",
"ImageURL": "images/transparent.png",
"Children": []
},
{
"id": 6,
"Text": "Voltage #2",
"Min": "1.680 V",
"Value": "1.688 V",
"Max": "1.688 V",
"SensorId": "/lpc/nct6791d/voltage/1",
"Type": "Voltage",
"ImageURL": "images/transparent.png",
"Children": []
},
{
"id": 7,
"Text": "AVCC",
"Min": "3.488 V",
"Value": "3.488 V",
"Max": "3.488 V",
"SensorId": "/lpc/nct6791d/voltage/2",
"Type": "Voltage",
"ImageURL": "images/transparent.png",
"Children": []
},
{
"id": 8,
"Text": "3VCC",
"Min": "3.488 V",
"Value": "3.488 V",
"Max": "3.488 V",
"SensorId": "/lpc/nct6791d/voltage/3",
"Type": "Voltage",
"ImageURL": "images/transparent.png",
"Children": []
},
{
"id": 9,
"Text": "Voltage #5",
"Min": "1.016 V",
"Value": "1.032 V",
"Max": "1.032 V",
"SensorId": "/lpc/nct6791d/voltage/4",
"Type": "Voltage",
"ImageURL": "images/transparent.png",
"Children": []
},
{
"id": 10,
"Text": "Voltage #6",
"Min": "1.088 V",
"Value": "1.088 V",
あとは、PCのUSBLANとラズパイに、適当なローカルIPを振り、通信を担保しておきましょう。
ラズパイ側準備
やることはこんな感じ
- LEDパネル用ライブラリの追加
- プログラム
- RAMDISKだのSYSTEMDだの定義
LEDパネル用ライブラリは、以下URLを参照のこと。
https://qiita.com/eucalyhome/items/e871e297bfd527ccaf2c
プログラム
「ステータス表示」と、「ライフゲーム(温度によって忙しさが変わる)」を実装してみました。
LEDはチェーン接続してあるので、最初の128ドットはライフゲーム、後の128ドットが縦書きのステータス表示、な感じで。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from PIL import Image, ImageDraw, ImageOps, ImageFont, ImageFilter, ImageChops, ImageColor
from rgbmatrix import RGBMatrix, RGBMatrixOptions
import time, re, copy, os, codecs, random, datetime, json
import numpy as np
from scipy import signal
class ledworker(object):
def ledinit(self):
self.options = RGBMatrixOptions()
self.options.hardware_mapping = "adafruit-hat-pwm"
self.options.led_rgb_sequence = "RGB"
self.options.rows = 64
self.options.chain_length = 8
self.options.parallel = 1
self.options.pwm_bits = 11
self.options.brightness = 50
self.options.pwm_lsb_nanoseconds = 130
self.options.gpio_slowdown = 1
self.options.panel_type = "FM6126A"
self.matrix = RGBMatrix(options = self.options)
def ledoutput(self,handle):
self.matrix.SetImage(handle)
def imagenew(self):
imagecolorspace = 'RGB'
imagecolorfill = (0, 0, 80)
imagesizex = 256
imagesizey = 64
image = Image.new(imagecolorspace, (imagesizex, imagesizey), imagecolorfill)
return (image)
class lifeworker(object):
def initstate(self, width, height, init_alive_prob=0.5):
N = width*height
v = np.array(np.random.rand(N) + init_alive_prob, dtype=int)
return v.reshape(height, width)
def addstate(self, F, width, height, init_alive_prob=0.5):
N = width*height
v = np.array(np.random.rand(N) + init_alive_prob, dtype=int).reshape(height, width)
v = F + v
return v
def countneighbor(self, F):
mask = np.ones((3, 3), dtype=int)
return signal.correlate2d(F, mask, mode="same", boundary="wrap")
def nextgeneration(self, F):
N = self.countneighbor(F)
G = (N == 3) + F * (N == 4)
return G
def toimage(self, F):
nparray = np.array(F, dtype=np.uint8)*255
image = Image.fromarray(np.uint8(nparray)).convert("1")
return image
class monitorworker(object):
def getdata(self):
datafile_hwdata = '/ramdisk/hwdata.json'
outdata = {}
outdata['cpuload'] = 0
outdata['cputemp'] = 0
outdata['gpuload'] = 0
outdata['gputemp'] = 0
outdata['cpuclock'] = 0
if not os.path.exists(datafile_hwdata):
return (outdata)
try:
with open(datafile_hwdata) as f:
hwdata = json.load(f)
except:
return (outdata)
for primarykey in hwdata['Children'][0]['Children']:
if 'cpu' in primarykey['ImageURL']:
for secondarykey in primarykey['Children']:
if 'Clocks' in secondarykey['Text']:
for thirdkey in secondarykey['Children']:
if 'CPU Core #1' in thirdkey['Text']:
outdata['cpuclock'] =re.sub("[^0-9\.]","",thirdkey['Value'])
if 'Load' in secondarykey['Text']:
for thirdkey in secondarykey['Children']:
if 'CPU Total' in thirdkey['Text']:
outdata['cpuload'] =re.sub("[^0-9\.]","",thirdkey['Value'])
if 'nvidia' in primarykey['ImageURL']:
for secondarykey in primarykey['Children']:
if 'Temperatures' in secondarykey['Text']:
outdata['gputemp'] = re.sub("[^0-9\.]","",secondarykey['Children'][0]['Value'])
if 'Load' in secondarykey['Text']:
outdata['gpuload'] = re.sub("[^0-9\.]","",secondarykey['Children'][0]['Value'])
if 'mainboard' in primarykey['ImageURL']:
for secondarykey in primarykey['Children'][0]['Children']:
if 'Temperatures' in secondarykey['Text']:
outdata['cputemp'] = re.sub("[^0-9\.]","",secondarykey['Children'][0]['Value'])
outdata['cpuload'] = "{0:5.1f}".format(float(outdata['cpuload']))
outdata['gpuload'] = "{0:5.1f}".format(float(outdata['gpuload']))
outdata['cpuclock'] = "{0:4d}".format(int(outdata['cpuclock']))
return (outdata)
def writetext(draw,hpos,message,color,font):
w, h = draw.textsize(str(message), font)
w = 48 - w
draw.text((w,hpos), str(message), color,font=font)
def main():
led = ledworker()
led.ledinit()
image = led.imagenew()
fontdata = "./GDhwGoJA-OTF112b2.otf"
life = lifeworker()
lifedatablue = life.initstate(128, 64, init_alive_prob=0.08)
lifedatagreen = life.initstate(128, 64, init_alive_prob=0.08)
monitor = monitorworker()
outdata = monitor.getdata()
imagebasel = Image.open('/data/ledstat/nzx_log_w.png').convert("RGB")
imagebasel = imagebasel.transpose(Image.ROTATE_180)
imagebaseh = copy.copy(imagebasel)
imagebaseh = imagebaseh.transpose(Image.ROTATE_90)
imageh = copy.copy(imagebasel)
image = Image.new('RGB', (256, 64), (0, 0, 0))
image.paste(imageh,(128, 0))
imagehtemp = copy.copy(imagebaseh)
draw = ImageDraw.Draw(imagehtemp)
font = ImageFont.truetype(fontdata, 16)
fontsmall = ImageFont.truetype(fontdata, 7)
addcount = 0
while True:
imageblue = life.toimage(lifedatablue)
imagegreen = life.toimage(lifedatagreen)
imagel = copy.copy(imagebasel)
imagel.paste(Image.new("RGB", imagel.size, (0,255,255)), mask=imageblue)
imagel.paste(Image.new("RGB", imagel.size, (0,0,255)), mask=imagegreen)
image.paste(imagel,(128, 0))
led.ledoutput(image)
lifedatablue = life.nextgeneration(lifedatablue)
lifedatagreen = life.nextgeneration(lifedatagreen)
time.sleep(0.05)
addcount = addcount + 1
if addcount > 20:
addcount = 0
outdatatemp = monitor.getdata()
if outdatatemp['cputemp'] != 0:
outdata = outdatatemp
blueseed = (float(outdata['cputemp'])-40) / 200
greenseed = (float(outdata['gputemp'])-40) / 200
if blueseed < 0:
blueseed = 0
if greenseed < 0:
greenseed = 0
lifedatablue = life.addstate(lifedatablue,128, 64, init_alive_prob=blueseed)
lifedatagreen = life.addstate(lifedatagreen,128, 64, init_alive_prob=greenseed)
lifedatablue = life.nextgeneration(lifedatablue)
lifedatagreen = life.nextgeneration(lifedatagreen)
imagehtemp.paste(imagebaseh,(0, 0))
draw.text((0,0), "CPU", (0, 255, 255),font=font)
writetext(draw,16,outdata['cpuload'],(255, 255, 255),font)
draw.text((48,24), "%", (0, 255, 255),font=fontsmall)
writetext(draw,32,outdata['cputemp'],(255, 255, 255),font)
draw.text((48,40), "'C", (0, 255, 255),font=fontsmall)
writetext(draw,48,outdata['cpuclock'],(255, 255, 255),font)
draw.text((48,56), "MHz", (0, 255, 255),font=fontsmall)
draw.text((0,74), "GPU", (0, 0, 255),font=font)
writetext(draw,90,outdata['gpuload'],(255, 255, 255),font)
draw.text((48,98), "%", (0, 0, 255),font=fontsmall)
writetext(draw,106,outdata['gputemp'],(255, 255, 255),font)
draw.text((48,116), "'C", (0, 0, 255),font=fontsmall)
imageh = copy.copy(imagehtemp)
imageh = imageh.transpose(Image.ROTATE_90)
image.paste(imageh,(0, 0))
if __name__ == '__main__':
main()
ライフゲームの実装は、以下URLを参考にしました。
https://qiita.com/sage-git/items/c6c175887faa4cf737fb
前提となるファイルは、以下の通り
128x64 背景ロゴ:/data/ledstat/nzx_log_w.png
PCステータス用data.json:/ramdisk/data.json
フォント:./GDhwGoJA-OTF112b2.otf
フォントは高速道路の奴使ってます、まあお好みで。
https://forest.watch.impress.co.jp/library/software/gdhighway/
次に、PCからJSONをゲットする適当なプログラムを
import urllib.request
import json
import time
url = 'http://[ip address]:8085/data.json'
datafile = '/ramdisk/hwdata.json'
req = urllib.request.Request(url)
while True:
try:
with urllib.request.urlopen(req) as res:
body = json.load(res)
fw = open(datafile,'w')
json.dump(body,fw,indent=4)
fw.close()
except:
time.sleep(30)
time.sleep(2)
こいつはPython3で動きます注意、まあつまり無節操。
RAMDISK定義は簡単。
まず「/ramdisk/」をrootで777なディレクトリを作り。
FSTABに「tmpfs /ramdisk tmpfs defaults,size=16m 0 0」を書き足し。
「mount -a」かリブートすればOK。
dfして「tmpfs 16384 84 16300 1% /ramdisk」があればOKな感じです。
systemdへの登録は、以下な感じ。
[Unit]
Description=getdata
[Service]
ExecStart=/usr/bin/python3 /data/ledstat/getdata.py
Restart=always
Type=simple
User=pi
[Install]
WantedBy=multi-user.target
[Unit]
Description=ledstat
[Service]
WorkingDirectory=/data/ledstat
ExecStart=/usr/bin/python /data/ledstat/ledstat.py
Restart=always
Type=simple
[Install]
WantedBy=multi-user.target
んで、「systemctl daemon-reload」からの
systemctl start getdata
systemctl enable getdata
systemctl start ledstat
systemctl enable ledstat
な感じで。
エラーが起きたらstatusで確認、ね。
動作確認
なかなかに、たのしいw。
やらかしてみて
いやあ、なんというか。
オイラっぽいのが、できたかなと。
ちなみに、ゲームしてる最中は別のパソコンなんか見ないし。
LEDパネルで色々塞いでるので、温度上がり気味だし。
というかそもそもLEDで発熱してるし。
真面目に考えるといいことなんもないですが。
楽しいからいいのです!。
キレイだし!。
まんぞく!。