RaspberryPiで「ほんわか お知らせランプ」Part 1

## RaspberryPiで監視をお助け
RaspberryPiでシステムからの通知を優しく知らせてくれるデバイスを作ります。
目標としては、こんな感じ。

* Cloudから通知すべき変化を取得
* 変化を見つけたら、ランプとチャイムで知らせる
* 通知内容によって、色合い、明滅の速さ、チャイムの種別を分ける

![Blue.gif](https://dl.dropboxusercontent.com/u/433851/qiita/Rpi/Blue.GIF) 

## RaspberryPiの入手
RaspberryPiは、アマゾンで購入しました。

| 販売元 | 品名 | 価格 | 数量 |
|---|---|---:|---:|
| TechShare |[Raspberry Pi2 Model B ボード＆ケースセット (Standard, Clear)](http://www.amazon.co.jp/dp/B00TBKFAI2) | ¥6,300 | 1 |
| Amazon.com Int'l Sales, Inc. | [Transcend microSDHCカード 16GB Class10 TS16GUSDHC10E](http://www.amazon.co.jp/dp/B008UR8TZ8) | ¥980 | 1 |

付属の専用ケースは、GPIOソケット部分に開口部があるものでケースの外の基盤と接続するのに適しています。
![Rpi-Case.jpeg](https://dl.dropboxusercontent.com/u/433851/qiita/Rpi/Rpi-Case.jpg)  ![Magenta-Side.jpeg](https://dl.dropboxusercontent.com/u/433851/qiita/Rpi/Magenta-Side.jpeg)

## システム構成
お知らせランプの構成は、以下のとおりです。

この記事では「Color LED」のハードウエア部分と、それを操作する「Led Driver」部分について書きます。
![SystemDiagram](https://dl.dropboxusercontent.com/u/433851/qiita/Rpi/SystemDiagram1.png)

| 項目 | 内容 | コメント |
|---|---|---|
| OS | Raspbian | Linux raspberrypi 3.18.7-v7+ #755 |
| 開発言語 | Python| Ver. 2.7.3, Raspbianに含まれる |
| GPIO操作 | RPi.GPIO | Raspbianに含まれる |


## ハードウェア
「お知らせランプ」のハードウェアを作ります。
RaspberryPiからLEDを操作するしくみは[多くの方が書かれている「Lチカ」](http://qiita.com/search?utf8=✓&sort=rel&q=Lチカ+raspberryPi&sort=rel) とほぼ同じです。

カラーLEDにもいろいろなタイプがあり、明るさや色の制御方法も様々ですがここでは、以下をベースに設計します。

* トランジスタアレイによるPower LEDのドライブ
Power LEDに十分な電流を流せる様に、RaspberryPiのGPIO出力をトランジスタ（アレイ）で増幅します。

* ソフトウエアPWM(Puls Width Moduration)による色合い制御
ソフトウエアで3色のLEDの輝度を自由に制御できる様に、GPIOの操作でRGB各色を独立してON/OFFできる回路とします。

### 配線図
![NotificationLampCircuit.png](https://dl.dropboxusercontent.com/u/433851/qiita/Rpi/NotificationLampCircuit.png)
使用するGPIOポートは、変更してもOKです。
その場合、後述のプログラムの定義も変更します。

### 使用部品
部品は[秋月電子通商（通販）](http://akizukidenshi.com/catalog/top.aspx)で購入しました。

| 品番 | 内容 | 価格 | 数量 |
|---|---|---:|---:|
| [P-03231](http://akizukidenshi.com/catalog/g/gP-03231/) | 両面スルーホールガラスコンポジット・ユニバーサル基板Ｃタイプ　めっき仕上げ　７２ｘ４７ｍｍ　日本製 | ¥100 | 1 |
| [C-07607](http://akizukidenshi.com/catalog/g/gC-07607) | ＵＳＢケーブル　Ａオス－マイクロＢオス　１．５ｍ　Ａ－ｍｉｃｒｏＢ| ¥120 | 1 |
| [R-07799](http://akizukidenshi.com/catalog/g/gR-07799) | カーボン抵抗（炭素皮膜抵抗）　１／２Ｗ　４７Ω （１００本入）| ¥100 | 1 |
| [C-02485](http://akizukidenshi.com/catalog/g/gC-02485) |連結ピンソケット　２×２０　（４０Ｐ） | ¥110 | 1 |
| [I-01587](http://akizukidenshi.com/catalog/g/gI-01587) |トランジスタアレイ（７ｃｈダーリントンシンクドライバ）ＴＤ６２００３ＡＰＧ（２個入）|¥100| 1 |
| [I-07233](http://akizukidenshi.com/catalog/g/gI-07233) | 放熱基板付１ＷハイパワーフルカラーRGB LED OSTCWBTHC1S | ¥250 | 1 |

### 工具や消耗品
制作には、以下の工具や消耗品があると良いでしょう。

| 品名 | メモ |
|---|---|
| はんだごて + はんだごて台 | 30Wくらいの電子工作用 |
| はんだ | 直径0.6mm程度の細いもの |
| ニッパ + ラジオペンチ + ピンセット | |
| テスター | 導通チェックなどにあると便利 |

### 部品配置
制作したボードの外観は、以下のようになりました。
![BoardOutline.jpeg](https://dl.dropboxusercontent.com/u/433851/qiita/Rpi/BoardOutline.jpeg)

右端の裏面には、RaspberryPiとの接続用に40ピンのピンソケットが配置されています。
このピンソケットには、長いピンが付いていますが、切らずに先端部分ではんだ付けすると、ちょうどケースの上に基盤が乗る位置で接続できます。
写真で、基盤の上中央のグレーのパーツは、「プッシュスイッチ」ですが、今回の説明では使わないので、無視しておいてください。
Power LEDの基盤は、アルミニウムですので、配線がエッジ部分に接触しないように気を付けて制作します。
写真では見えていませんが、47Ωの抵抗3本は、LEDの下に配置されています。

## Led Driver
LEDの色や点滅を制御するモジュールをここでは「Led Driver」と呼びます。
「お知らせランプ」では、呼び出し元のプロセスの子プロセスとして構成することで、上位のモジュールとは、非同期に動作できる様にします。

Led Driverの機能はシンプルです。
「色」と「明滅周期(秒)」を指定すると、その通りにLEDを明滅し続けます。
明滅処理の途中で指定を変更した場合は１秒以内に反映されます。

モジュール構成は以下の通りです。

* LEDの明滅を実行する Led Class
* Deamon化とプロセス間通信を処理する LedDeamon Class

Deamonとの通信には、IPCのキューを使用しています。
LedDeamonは、80秒間点灯モードの設定（set_mode）が実行されないと、規定のDown状態の表示に変わります。
これは、上位プログラムのダウン検出用の機能です。

詳細は、以下のソースコードをご覧ください。

Python初心者ですので、皆様からの温かいツッコミを期待しています！

テスト実行方法：

``` bash:
$ sudo ./led.py
```

``` Python:led.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import RPi.GPIO as GPIO
import time


class Led(object):
    # Define your GPIO Ports
    RED_PORT = 13
    GREEN_PORT = 26
    BLUE_PORT = 5

    # Controll resolution Params
    # You don't have to change following 3 Params usually
    FREQUENCY = 200  # PWM Frequency
    RESOLUTION = 50.0  # Brightness control per span
    SPAN = 1.0  # Drive time in second at one call

    # Brightness
    MAX_BRIGHTNESS = 1.0 # Master Brightness parameter 0.1 - 1.0

    # Color Table
    # You can append your own colors
    COLORS = {
        "Red": [1.0, 0.0, 0.0],
        "Pink": [1.0, 0.3, 0.3],
        "Green": [0.0, 1.0, 0.0],
        "LightGreen": [0.2, 1.0, 0.2],
        "Blue": [0.0, 0.0, 1.0],
        "LightBlue": [0.3, 0.3, 1.0],
        "Yellow": [1.0, 1.0, 0.0],
        "Orange": [1.0, 0.3, 0.0],
        "Cyan": [0.0, 1.0, 1.0],
        "Lime": [0.0, 1.0, 0.3],
        "Magenta": [1.0, 0.0, 1.0],
        "Violet": [0.3, 0.0, 1.0],
        "White": [1.0, 1.0, 1.0],
        "Black": [0.0, 0.0, 0.0]
    }
    # Color index for Color Table
    RED = 0
    GREEN = 1
    BLUE = 2

    def color_names(self):
        return Led.COLORS.keys()

    def __init__(self):
        self.phase = 0.0
        self.color = Led.COLORS["Black"]

        GPIO.setmode(GPIO.BCM)

        # set GPIO port as output
        GPIO.setup(Led.RED_PORT, GPIO.OUT)
        GPIO.setup(Led.GREEN_PORT, GPIO.OUT)
        GPIO.setup(Led.BLUE_PORT, GPIO.OUT)

        # set port as software PWM with f Hz
        self.red = GPIO.PWM(Led.RED_PORT, Led.FREQUENCY)
        self.green = GPIO.PWM(Led.GREEN_PORT, Led.FREQUENCY)
        self.blue = GPIO.PWM(Led.BLUE_PORT, Led.FREQUENCY)

        # start software PWM with 0.0%
        self.red.start(0.0)
        self.green.start(0.0)
        self.blue.start(0.0)
        return

    def _set_brightness(self, brightness):
        percent = Led.MAX_BRIGHTNESS * (brightness ** 2) * 100.0
        # set duty cycle
        self.red.ChangeDutyCycle(min(100.0, percent * self.color[Led.RED]))
        self.green.ChangeDutyCycle(min(100.0, percent * self.color[Led.GREEN]))
        self.blue.ChangeDutyCycle(min(100.0, percent * self.color[Led.BLUE]))

    def set(self, color, interval):
        # Color name to RGB value
        self.color = Led.COLORS[color]

        # interval in second
        self.interval = float(interval)

        # control resolution parameter
        if self.interval > 0.0:
            self.pitch = 1.0 / (Led.RESOLUTION * self.interval)

        # Reset phase
        self.phase = 0.0

    def run(self):
        if self.interval == 0.0:
            # No Blink
            self._set_brightness(0.5)
            time.sleep(Led.SPAN)
        else:
            for i in range(int(Led.RESOLUTION * Led.SPAN)):
                self.phase += self.pitch
                if self.phase >= 1.0:
                    self.phase = 0.0
                if self.phase < 0.5:
                    br = self.phase * 2.0
                else:
                    br = 1.0 - ((self.phase - 0.5) * 2.0)
                # keep a little light
                br += 0.0001
                self._set_brightness(br)
                time.sleep(1.0 / Led.RESOLUTION)

    def destroy(self):
        self.red.stop()
        self.green.stop()
        self.blue.stop()
        GPIO.cleanup()

if __name__ == ("__main__"):

    l = Led()
    l.set("Blue", 0.5)
    l.run()
    l.set("Black", 0)
    l.run()
    for interval in [0, 0.5, 1.0]:
        print("- Inrerval = %2.1f" % interval )
        for c in l.color_names():
            if c == "Black":
                continue
            print(c)
            l.set(c, interval)
            for i in range(max(1, int(interval * 2.0))):
                l.run()
    l.destroy()
```

テスト実行方法：

``` bash:
$ sudo ./led_deamon.py
```

``` Python:led_deamon.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


import time
import multiprocessing
import Queue
from led import Led


class LedDeamon(object):
    DOWN = {"color": "Magenta", "interval": 0}

    def __init__(self):
        self.queue = multiprocessing.Queue()
        self.lighting_mode = None

    def start(self):
        p = multiprocessing.Process(target=self._led_control, args=(self.queue, ))
        # Set deamon flug to kill process when parent process died
        p.daemon = True
        p.start()

    def set_mode(self, lightning_mode):
        self.queue.put(lightning_mode)

    # Child loop
    def _led_control(self, queue):
        count = 0
        lighting_mode = LedDeamon.DOWN
        self.led = Led()
        while True:
            # get mode from queue
            try:
                lighting_mode = self.queue.get(False)
                count = 0
                # print("lighting_mode = %s" % lighting_mode)
            except Queue.Empty:
                count += 1
                # parent seems dead
                if count > 80:
                    lighting_mode = LedDeamon.DOWN
            self._drive(lighting_mode)  # Drive for about 1 sec.

    def _drive(self, lighting_mode):
        if self.lighting_mode != lighting_mode:
            self.lighting_mode = lighting_mode
            self.led.set(lighting_mode["color"], lighting_mode["interval"])
            # print("lighting_mode = %s" % lighting_mode)
        self.led.run()

if __name__ == '__main__':
    mode_map = {
        "DOWN": {"color": "Magenta", "interval": 0},
        "ERR_UNACKED": {"color": "Red", "interval": 0.5},
        "WARN_UNACKED": {"color": "Yellow", "interval": 2},
        "ERR_ACKED": {"color": "Green", "interval": 4},
        "WARN_ACKED": {"color": "Green", "interval": 8},
        "NORMAL": {"color": "Blue", "interval": 8}}

    # Create and start LED Driver
    ld = LedDeamon()
    ld.set_mode(mode_map["NORMAL"])
    ld.start()
    time.sleep(8)
    ld.set_mode(mode_map["ERR_UNACKED"])
    time.sleep(8)
    ld.set_mode(mode_map["WARN_UNACKED"])
    time.sleep(8)
    ld.set_mode(mode_map["ERR_ACKED"])
    time.sleep(8)
    ld.set_mode(mode_map["WARN_ACKED"])
    time.sleep(8)
    ld.set_mode(mode_map["DOWN"])
    time.sleep(8)
```
## 制限事項

* 複数のプロセスからの同時使用は考慮していません
* 完全なソフトウエア制御なので、システムに負荷がかかると、少しちらつくことがあります。
* 他の用途にもGPIOを使う場合、修正が必要です。
* ソフトウエア制御なので、少々CPUを消費します。
Pythonプロセスが1Coreの2.5%くらい。4Core全体の1%程度消費している様です。

## おまけ

重要な問題が起きたときの光り方はこんな感じになります。
適当なチャイムと合わせると、程よい緊迫感が演出できます。（あまり見たくないけど。。。）
![Red.gif](https://dl.dropboxusercontent.com/u/433851/qiita/Rpi/Red.GIF)

LEDにかぶせているドーム状のカバーは、ダイソーで売っていたLEDライトのカバーを転用したものです。
ハードウエア制作で一番苦労したのは、実は、このカバー探しでした。（笑）

## 次回の予定

次回は、通知制御を行う モジュール Detectorについて書く予定です。