自作PCにLEDを仕込んでみる（やらかし気味の）

こんにちは。

最近、本職方面でもLED屋さんしている、ゆうかりです。
まあ、昨今の流行に則り。
オイラも、「PCをLEDで光らせたく」なったので。
やらかしてみましたよ。

#結果
こんな事態にっ！
![Dsc03705.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/f3ec81e0-abec-ed6d-1aef-b59c3f23c73f.jpeg)

#準備
以下条件に当てはまる素材を集めて、ツボ錬金でもやらかせばOKです。

- 中にLED仕込めそうで、外からLEDがちゃんと見えそうなケース
- 適当なマトリクスLED
- 多分窒息するので、それなりな放熱手段

まあ、「前面がガラスかアクリル」なケースで探してたのですが。
NZXTのH510i Elite ってケースにしてみました。
んでまあ、CPU排熱を外に出せるように、「kraken M22」という簡易水冷も導入、と。

#LEDの選定
昨今流行りの、「HUB75な表面実装フルカラーマトリクスLED」は、ツブの大きさで複数の種類があります。
型番の「P」の後ろの数字が、ツブの大きさです。
今回選定のケースは、前面に12cm ～ 14cmのファンを２つカマすことができるっぽいので。
それっぽい大きさを選定します。

例えば、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を使うため、公式ケースとかは厳しいです。
![Dsc03691.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/ee5ec80d-8d72-a896-70ec-7f0a7249c0a8.jpeg)

次にLEDのインターフェース基板。
今回は余らせまくってた、Adafruitの古い基板を使うことにしました。
![Dsc03690.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/d33b11a6-1662-91d1-09c9-09d9b6d7f9c0.jpeg)

但し！、以下２つの改造が必要です。

- ハードウェアPWM駆動化
- アドレス線「E」の追加

まあ、この記載を参考に、線を２本追加と、パターンカット１箇所って感じです。
なお、アドレスE線は、２パターンあるので、まずお使いのパネルが「どっち」か、テスターなどで確認してくださいな。
使われないほうのアドレスE線は、GNDに落ちてます。
　＊なので双方接続はNGですよっと。
![Image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/4bb6355a-c60f-5eaf-0198-c1b960057f78.png)

https://github.com/hzeller/rpi-rgb-led-matrix

配線は適当にポリイミドで留めてます。
![Dsc03689.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/587c358f-91cf-d563-8476-0c9e74f86289.jpeg)


#組み立て
まずはフロント側、こんな感じ。
![Dsc03697.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/2f0029d8-f4ce-9226-c46c-d612952cc079.jpeg)

裏は、12cmファン用のステーに、M3ネジで固定しておりますよと。
![Dsc03698.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/0623aacb-50e3-24b4-5d42-a63ab97e2905.jpeg)

ハメ込むとこんな感じになります。
![Dsc03684.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/921291d7-20f6-5cfd-1307-50270723ce7f.jpeg)

これだけじゃ寂しいので、底面にもLEDを敷きました。
![Dsc03685.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/858a45cc-54f8-9f16-8ab8-6821098ecd79.jpeg)

こいつの固定はマグネット、４隅にマウント。
![Dsc03686.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/0ca2b8a0-26d6-5f07-8213-dd3b323ba3e3.jpeg)

このマグネットは、秋葉原の千石電商の隣のLED屋とかで扱っています、べんりべんり。
![Dsc03687.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/a09dbb49-d5d5-a2aa-6341-309fc6a51218.jpeg)

https://www.akiba-led.jp/product/1197

LEDは底側のLEDをラズパイに繋げ、そこからチェーンで前面LEDに接続しています。
これは純粋に配線都合です。

電源はSATAから取ることに。
まあ昔からのお約束ですが、パソコンの電源は一般的に、「黄色：１２Ｖ」「赤：５Ｖ」「オレンジ：３．３Ｖ」「黒：ＧＮＤ」です。
RaspberryPi4とLEDで、まあ5A以上使いそうですが、パソコン電源なので大丈夫でしょう（適当）。
![Dsc03693.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/a94efde4-53cb-372e-76e2-a83da5b45c81.jpeg)

Raspberry PiとPCのインターフェースは、LANを使用します。
PC側がUSBLANです。
USBは、マザーボードのヘッダから、ケース用の変換コネクタを使って。
![Dsc03694.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/ded2318d-ab85-ad9b-9335-da4f52d9033a.jpeg)

Raspberry Pi周りの接続は、こんな感じで。
![Dsc03695.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/b6e3fe28-18f1-790d-bb15-84a0d8697f9d.jpeg)

あとはH510i Eliteの電源裏にある隙間にラズパイ押し込んで、完成です。
![Dsc03696.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/6c6c48db-ec0c-0c17-5ca2-ee60d486b746.jpeg)

#組み込み完成
まあつまり、このPCは。
何気に２環境同居な、「デュアル構成PC」なのですが。
まあ外側からは普通のPCに見えます。
![Dsc03682.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/f0c7e2e7-0add-04ba-4791-4c44853e1c77.jpeg)



後ろも普通のPCです。
![Dsc03683.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/5ee98dda-5cd3-a668-a9b4-540f9e7de074.jpeg)

NZXT純正の制御機構「HUE2」ともバッティングしてません。
![Dsc03688.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/405af32a-619b-eb0e-763a-bdff5823880b.jpeg)
＊ただまあ最初からついていたファンは、全部外しちゃいましたが。

ここまでやらかしておきながら、「ケース無改造」なのです、何げにヒドい・・・。

#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

内容はこんな感じです（一部抜粋）：

```json: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はチェーン接続してあるので、最初の１２８ドットはライフゲーム、後の１２８ドットが縦書きのステータス表示、な感じで。

```python:ledstat.py
#!/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をゲットする適当なプログラムを

```python:getdata.py
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への登録は、以下な感じ。

```sh:/etc/system/getdata.service
[Unit]
Description=getdata

[Service]
ExecStart=/usr/bin/python3 /data/ledstat/getdata.py
Restart=always
Type=simple
User=pi

[Install]
WantedBy=multi-user.target
```

```sh:/etc/systemd/system/ledstat.service
[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で確認、ね。

#動作確認
前面パネルは、こんな感じ！。
![Dsc03709.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/63b356c6-7df0-586f-3fe7-9236a3d6945b.jpeg)

底面パネルは、忙しくないとこんな感じで。
![Dsc03700.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/e6fafb76-d0ea-abc9-5b5d-5e2a61e276f2.jpeg)

忙しくなって温度上がってくると、わちゃわちゃします。
![Dsc03710.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216885/1b56f8e7-c517-3473-aaeb-64f3c41b937c.jpeg)

なかなかに、たのしいｗ。

#やらかしてみて
いやあ、なんというか。
オイラっぽいのが、できたかなと。

ちなみに、ゲームしてる最中は別のパソコンなんか見ないし。
LEDパネルで色々塞いでるので、温度上がり気味だし。
というかそもそもLEDで発熱してるし。

真面目に考えるといいことなんもないですが。
楽しいからいいのです！。
キレイだし！。

まんぞく！。