○ パソコンで照明を操作する

パソコンとマイコン(Arduino)とリレーモジュール、フォトレジスタ(光センサー)で家の照明を操作したいと思います。
回路の概略はこんな感じです。


電球に介入するリレーモジュールはB接点(電気を流すとOFF)を使用しており、パソコンやマイコン、その他の障害が起きた時に電球は常に点灯します。
なにより、室内の配線やスイッチを流用する意味でもB接点を使った方が都合が良いです。

フォトレジスタは室外の明るさを取得します。
実際には屋根の上に設置して一日中、日影が出来ない場所がのぞましい。
なぜ室外?室内の明るさの測定するべきではないだろうかと思うかもしれません。
地球の自転による太陽の位置、公転による季節の変化と日照時間などは世界を構成する物理法則であり予測可能です。
それに比べて、室内の明るさは場所や照明のON/OFFにより変化しますし、暗くなったと感じるタイミングは個人の感覚であり予測不可能です。
やってみないとわからないですが、変えられない大きな所で制御して、個人の感覚のようなあやふやな所は調整するのが良いのではないだろうか?と思ったからです。

なぜ、パソコンのような大きなものを取り付けるのか?疑問に思うかもしれません。
それは複雑な制御をする可能性が高いので、パソコンを取り付けています。
でも、これだけの理由では理解しがたいでしょう。
なぜ、こんな制御装置を作るのか?その理由は「規模の問題」を回避したいが為なのです。

例えば、犬小屋を作った人がいるとしましょう。
犬小屋を作るのは比較的に簡単でした、材料も適当に買ってきて釘で打ち付けて、目分量で入り口を開けます。
比較的に簡単にできました。
彼は自信を付けます、俺は犬小屋を作れるのだから小屋も作れると言い張ります。
さて、彼は小屋を作れるでしょうか?
試行錯誤をして徒労にくれればできるかもしれませんが、小屋になると簡単な設計図を書いて風で飛ばないように基礎を作り、材料だって一人で運べるとは限りません。
犬小屋を作った時のルールと違い格段にむつかしくなります。
なら、住宅ならどうでしょう、もっともっとルールが変わりむつかしくなります。
ビルなら、橋なら、ダムなら・・・・・・ただ単に規模が大きくなるだけでもこの様です。

ある人が仲間内で使える道具を作っていたとしましょう。
ある日、この道具を売ったら行けるんじゃないか?と本気で考え始めます。
小規模の数人に売るには問題ないでしょう、しかし百人規模で売ろうとすると生産方法や安全性や対策やマーケティング、財務や在庫管理など全く違うルールが出てきます。
千人規模だとどうでしょうか?材質のばらつきや設計の考え方が確立的に確実に問題を発生させ始めます。
ただ単に数が大きくなるだけでも極端に難しくなってゆきます。

「規模の問題」とはなんであれ事柄が大きくなるとルールが変わり極端にむつかしくなるということなのです。
これはすべての物や考え方に起きる問題と思っています。
なにが起きるのかわからないからこそ、過剰な性能を準備しておくのが必要だと思います。

概要はこの辺で、実際のハードを構成を示します。

▽ リレーモジュール
リレーモジュールにて調べました。
信号線5Vで動作するリレーモジュールでポートが光で分離され、高圧側と低圧側が分離され低価格ながらすばらしい設計と思います。

▽ マイコン
mega328p 8bitマイコンを使ったArduinoボードを使用しており、Arduinoでシリアル通信の "シリアル通信でリレーモジュールを操作するプログラム" をそのまま書き込んでいます。
USBシリアル変換にはCH340を使った互換Arduinoボードです。

▽ フォトレジスタ
安価なフォトレジスタを使用しており、抵抗の定数を変えるだけでどのような種類の部品にでも交換できると思います。
マイコンに接続する方法は、フォトレジスタで明るさを測定するでの回路を使用しています。
センサー受光面は北側に向け、45degで斜め上向きで設置しました。

このようなシンプルなハードを作成してパソコンと接続しています。


この記事は机上の空論でありません、設計して作成して実際に稼働させて実験しています。
ハードを作成して設備に取り付けました、USBを経由してパソコンと接続します。
こんな高価な物を買ってと陰口を叩かれますがコントローラの部品はすべて自前で1500円以下、
ホームページで公開する以上は記事にする部品の自前は必須(作成時間も)だと思っています。
低信頼性で低価格のガラクタのような部品を設計とソフトウエアの力で如何に信頼性を上げるかが肝です。


■ パソコンのソフトの作成
ベースとなるパソコンソフトを事前に作成しておきました、Arduinoでシリアル通信のソフトを改造していきます。
動作するパソコンはWindowsで、開発環境は1998年が最新版のVisual Basic 6.0
なぜ、こんな古い開発環境を使うのか?
たしかに言語仕様はクソですし、なにより古すぎて使うべきでない言語です。
まあ、どんなものを作る事になるのかよくわからないから、Windowsの画面が作れていじくりまわして即座にトラブルが解決しやすいインタプリタが最適っちゃ最適だと思います。
まあ言語が気に入らなかったら後で書き直すだけです。

▼ 制御ロジックのイメージ
ベースとなるソフトには事前に自動制御ルーチンを組み込んでおきました。
そこに追加するロジックのイメージです。

現在、必要だと思える機能が光センサーでのON/OFFとタイマーによるスケジュールでのON/OFFです。
この二つを両立させるためにこのような制御ロジックを考えました。
では、実際に一つ一つ簡単な所から積み上げるとしましょう。

▼ スケジュールでのON/OFF
条件分岐による簡単なロジックです。
これは、ベースとなるソフトを作成した段階で組み込んでいます。

▼ アナログ入力の移動平均値を取る
アナログ入力ラインがノイズによりチョコチョコと変動します、多少の変動を抑えるために移動平均を取ります。
落雷や車のライトなどの急激で大きな変動を考えるとたくさんの値で移動平均を取りたいのですが、そうすると反応速度が遅くなります。
とりあえず、1分間の値を取得して移動平均を取る事にします。
アプリでは2秒サイクルで値を取得しますので、30個の値の移動平均になります。
中心となるクラスでは次のようになります。
Option Explicit
Dim data(30)  As Long
Dim ptr As Integer
Private Sub Class_Initialize()
    Dim i As Integer
    For i = 0 To 30
        data(i) = -1
    Next i
End Sub
'移動平均を計算する -1は無視される
Function average(str_val As String) As Long
    Dim val As Long
    On Error GoTo err
    val = CLng(str_val)
    On Error GoTo 0
    If -1 <> val Then
        data(ptr) = val
        If 30 - 1 = ptr Then
            ptr = 0
        Else
            ptr = ptr + 1
        End If
        Dim i As Integer
        Dim pos As Long
        pos = 0
        For i = 0 To 30 - 1 Step 1
            If -1 <> data(i) Then
                pos = pos + data(i)
            Else
                Exit For
            End If
        Next i
        average = Int(pos / i)
        Exit Function
    End If
err:
    average = val
End Function

上記プログラムを組み込んだスクリーンショットです。
赤で囲まれた部分が1分間の移動平均を表示します。
途中段階のソースはこちらですshoumei_20230617.zip

▼ ON/OFFの不感時間を作る
ONになった直後にOFFになる、OFFになった直後にONになる、この状態を避けるために、

1、 目標がON → OFF になってから実際には10分間 ON 状態を保つ、待機中に一瞬でも目標が ON になったらそこから実際には10分間 ON 状態を保つ
(点灯状態は常に10分以上連続する)
2、 目標がOFF → ON になってから実際には1分間 OFF 状態を保つ、待機中に一瞬でも目標が OFF になったらそこから実際には1分間 OFF 状態を保つ
(消灯状態は常に1分以上連続する)
3、 待機中に実際のON/OFF状態が変化したら(タイマーなどで ON/OFF が切り替わったら)リセットし不感時間が経過した事にする
4、 アプリの設定を即時反映するためにタイマーのリセット手段が必要(無いと設定後にタイマーがカウントアップするまで待つことになる)

不感時間の待機中のタイマーは減算によるタイマーが必要になり、0になると出力に反映させる方法が考えられます。
不感時間ではこのような制御をしようと思います。
上記仕様に従って作成したクラスです。
Option Explicit
Dim timer As Long
Dim power_change As Integer
Dim power_real As Integer

Private Sub Class_Initialize()
    power_change = 1
    timer = 0
End Sub
'タイマーを減算する、現実の出力が変化したらリセットする
Public Sub clock(real As Integer)
    If timer <> 0 Then
        timer = timer - 1
    Else
        timer = 0
    End If
    If power_real <> real Then
        power_real = real
        timer = 0
    End If
End Sub
'出力の変化を要請
Public Property Let Power(change As Integer)
    If power_change = 1 And change = 0 Then
        power_change = 0
        'リレーがON状態からOFFの要請
        timer = 60 / 2 '60秒のタイマーセット
    ElseIf power_change = 0 And change = 1 Then
        power_change = 1
        'リレーがOFF状態からONの要請
        timer = (10 * 60) / 2 '10分のタイマーセット
    End If
End Property
'出力の状態を取得
Public Property Get Power() As Integer
    If timer = 0 Then
        Power = power_change
    Else
        Power = power_real
    End If
End Property
'タイマーのリセット
Public Sub reset()
    timer = 0
End Sub

▼ 光センサーでのON/OFFの判定(前編)
ここはたぶんめちゃくちゃむつかしい。
日が高い時間帯は外の明るさと室内の明るさが相関しているが、朝方と夕方が外の明るさと室内が比例していない。
山や丘などの障害物や入射角の関係、時間帯や季節により要求度合いが変化する。
そのために用意するデータ構造の関係で、後々ちょっと長くなりそうです。
まずは、単純な制御である、センサーの値が400以上になったらONにして、350以下になるとOFFにする。
それ以外の値では現状維持をする、このような考えでプログラムを作成して組み込みました。
ちなみにセンサーの値は暗くなると数字が増えて、明るくなると数字が減ってきます。
'センサーの値を判定する sensor センサーの値 real_output 現在の出力 enable 有効/無効
Public Function calc(sensor As Long, real_output As Integer, enable As Integer) As Integer
    If 1 = enable Then
        If sensor > 400 Then
            calc = 0
        ElseIf sensor < 350 Then
            calc = 1
        Else
            calc = real_output
        End If
    Else
        calc = 0
    End If
End Function

上記プログラムを組み込んだスクリーンショットです。
赤で囲まれた部分のチェックボックスをONにすることによりアナログピン0番のセンサーで制御が行われます。
即時反映ボタンを押すと、不感時間を設定するタイマーがリセットされて即時反映されます。
ただ、タイミングの関係で複数回押さないと完全に即時反映されない事があります。
途中段階のソースはこちらですshoumei_20230620.zip
ココの続きは(後編)で延々とやります

▼ その他の設定
▽ 制御を停止する時間帯の設定
絶対に制御したくない時間帯が出てくると思います。
その場合には、自動制御を記述する部分の最後の行にて、
auto_control_digital_pin(0) = 0
このようにして制御を停止することにしました。
しかし、停止から再開した時に値が0になります。
センサー値が閾値の間で反応しない場合にはこのまま出力されますので1に変更したい場合には不都合です。
そのため、関数開始の初期値を強制変更するために変数を追加しました。
'自動制御にて関数開始状態を設定する変数 初期値:0
Public auto_control_func_next_state_setting As Integer
Public Const NONE_SETTING = 0 '何もしない
Public Const ENABLE_SETTING_1 = 1 '有効
Public Const DISABLE_SETTING_0 = -1 '無効
shoumei_20230727.zip以降のコードには入っています。

▽ スケジュールでのON/OFFで周囲が暗すぎる場合の設定
周囲が暗すぎる時に照明を落とすと問題になりそうです。
そこで、閾値を設定するクラスに次のように設定しました。
threshold.setThreshold(650, 0)
復帰はしない一方通行の閾値で650より暗くなった場合には照明を消さないようにしています。

その他の設定を取り込んだソースはこちらですshoumei_20230624.zip
基本的な設定を終えて、だいぶソースが複雑になってきました。

▼ 光センサーでのON/OFFの判定(後編)
▽ 時間や日付による動的な閾値
ココが制御の一番難しい所だと思います。
制御のポイントは線形補間だと考えています。 線形補間にて事前にVB6用のコードを作成しました。
これにより複数次元のデータ構造を作成して閾値を引こうと考えています。
まずは、二次元で時間により閾値を変化させるクラスを作成しました。
Option Explicit
Dim threshold_ As New threshold
Dim map1() As Variant
Dim map1_w As Long

Private Sub Class_Initialize()
    '数列の作成
    map1 = Array( _
    0, 900, 1100, 1300, 1500, 2400, _
    380, 380, 385, 385, 380, 380 _
    )
    '数列の幅を計算
    map1_w = (UBound(map1) + 1) / 2
End Sub

'線形補間のマクロ
Private Function INTERP(xi As Long, xi1 As Long, yi As Long, yi1 As Long, x As Long) As Long
    INTERP = (yi + (((yi1 - yi) * (x - xi)) / (xi1 - xi)))
End Function

'線形補間 x:補間する値 ar:数列 w:数列の横の長さ
Private Function interp1dim(x As Long, ByRef ar_() As Variant, w As Long) As Long
    Dim i As Long
    'xの値が範囲外の場合はxが最大最小値の値を返す
    If x <= ar_(0) Then
        interp1dim = ar_(w)
        Exit Function
    ElseIf x >= ar_(w - 1) Then
        interp1dim = ar_(w * 2 - 1)
        Exit Function
    End If
    For i = 0 To (w - 1) Step 1
        If ar_(i) >= x Then Exit For
    Next i
    ' y=yi + (yi+1-yi)(x-xi)/(xi+1-xi) を行い値を返す
    interp1dim = INTERP(CLng(ar_(i - 1)), CLng(ar_(i)), CLng(ar_(i + w - 1)), CLng(ar_(i + w)), x)
End Function

'センサーの値を判定する sensor センサーの値 real_output 現在の出力 enable 有効/無効
Public Function calc(sensor As Long, real_output As Integer, enable As Integer) As Integer
    Dim str As String
    str = Format(Now, "HH:MM:ss")
    Dim m, h, v, pos As Long
    m = CLng((CLng(Mid(str, 4, 2)) / 60) * 100)
    h = CLng(Mid(str, 1, 2) & "00")
    v = h + m
    pos = interp1dim(CLng(v), map1, map1_w)
'Form1.Caption = CStr(pos) + " " + CStr(350) 'デバッグのため
    Call threshold_.setThreshold(pos, 350)
    calc = threshold_.calc(sensor, real_output, enable)
End Function
このクラスでは時間を軸として閾値を動的に変化させます。
現在のマップでは昼には閾値が上がり、明るさに対する感度を鈍くして午後にかけて感度を高くしています。
グラフで書くとこのように閾値が変化する事になります。

今回追加した機能を取り込んだソースはこちらですshoumei_20230713.zip


▽ちょっと寄り道
改良1
ソフトを走らせていると、ログファイルが一日当たり10MBほど大きくなってゆきます。
非力なマシンでは日を追うごとにCPUの消費が大きくなるためログを1日単位でファイルを切り替えるようにしました。

改良2
他のアプリで値を利用するために値を特定のファイルに連続上書きする関数を作成しました。
Private Sub share_data_file(str As String, filename As String)
改行なしでファイルに毎度、上書き保存します。
同一ファイルの連続保存のため基本的にキャッシュの中で処理されるためディスクの負荷は少ないと思います。
今回追加した機能と、他の改良を取り込んだソースはこちらですversion 0_14_4_1


■ 人間の明るさの感じ方の解釈の修正
照度のログを見ていると実際の明るさの感じ方と比例していない。
常になにかゆらぎや不自然さを感じる。

明るさで室内の照明を制御する、この文章を書いている2023年7月の時点では製品として見当たらない。
他社がカメラを使ったり、照度計を部屋の中に複数付けて制御しようと試みたりしたのを聞いたことがある。
いずれにしても成功例を聞いたことが無い。
死屍累々(沢山の人が挑戦して失敗し死体の山になっている)感じがする問題のような気がする。
たぶん、安易に考えられる事はすべて試されているだろうと想像できる。
この事実から推測すると、照度のルクスと人間の感じる明るさは対比していないのではないかと感じました。
ネットを検索しても答えが見つからないので独自の理論と屁理屈を展開して問題を解決していこうと思います。

誰も答えを見つけた事が無い問題みたいですので、うまくいったら inaba理論 とでも呼んでください。
なにか問題があったら
トップページの「ご意見ご感想」からメールを送って教えていただけるとうれしいです。

思考実験の一つの例として、晴れた日のバスを考えてみます。

バスは非常に明るい太陽の光に照らされています、しかしそのカゲはどうでしょうか?非常に暗く感じます。
カゲの部分が暗いと感じるのですが、現実には明るい部屋の中よりも明るいはずです。
照度のルクスの数字だけを見るとカゲの部分が暗いということが認識できません。

■ 日の高さによる明るさの感じ方の違い
照度計のセンサー値と自分自身の明るさの感じ方を比較してみました。


センサー値を見ると暗いのですが、自分の感覚としては明るく感じる。

▼ 太陽が斜めの角度にあり、カゲができる場合

センサー値は明るいはずですが、カゲとの明暗が視界に入り薄暗く感じる。

▼ 太陽が水平に近い位置にありカゲが長くなる場合

センサー値は明るくてもカゲの面積が大きくなり、まぶしいしい反面、カゲの部分が暗くて識別が難しく感じる。
よく観察するとカゲの部分は実際にはそれほど暗くない。
感覚的には暗い状態。
この現象は晴れた日の夕方に多い。

以上の事から推測すると、人間は明るい部分と暗い部分の比率を比較して明るさを判断している気がする。
たぶん、眼球内の化学反応や伝達物質の消費量から来るものだと思う。
この現象を [1匹目の悪魔] と呼ぶことにしました。

逆に、カゲができにくい曇りの日はどうなのだろう?と疑問に思うでしょう。
感覚的に曇りの日はセンサー値が暗くても明るく感じるのが不思議に思っていましたがカゲの比率が少なくて明るく感じていたようです。
これで説明がつきました。


■ 時系列での明るさの感じ方
風が強く、太陽を厚い雲がかすめて突然暗くなりました。
正午付近で天気は晴れ、暗くなる要素は見当たりませんが一瞬にして夜のような暗さになりました。

イメージとしてはこのような感じです、そこで実際の照度のログのデータを見てみます。

12時48分50秒付近から個人の感覚としては非常に暗いと感じましたが、照度自体はそれほど暗くなっていない。
照明を点灯するほどの暗さではないですが、暗かったです (照明を点灯開始する閾値は400以上です)
ログを確認してみると赤線2本の間の急激な照度の変化が暗いという感覚を感じさせたと考えられます。

以上の事柄から推測するに、人間は短期の記憶の中で発生する明るさの差分を見て、部屋の中が明るいとか暗いとか判断するのではないかと仮定することにしました。

これにより、ライトを当てるとそこの部分が明るくなって明るいと感じる、周囲より暗いカゲの部分は暗く感じる事柄が、照度「ルクス」とは関係なく説明できます。
人間は直射日光の下でも、薄暗がりの下でも、文字を読んだり物を識別したり出来るかなり高性能な目を持っています。
ひょっとしたら、進化の過程での脳の構造の一部、暗いと感じると見る事や考える事をあきらめる節約の機能なのかもしれないとも思いました。
普段は暗いと思って認識しない場所を、意識して見てみると意外に見えたりしますもんね。
この現象を [2匹目の悪魔] と呼ぶことにします。
色々考察した結果、[2匹目の悪魔] の復帰(消灯)には作業者に気が付かれないように(ゆっくりと)消灯する必要があり、調光機能付き照明設備でないと実現できない為、あきらめました。
現象自体は音声認識AIと同様の考え方で識別できるようです。



▼ 光センサーでのON/OFFの判定(延長1回目)
今回は、 [1匹目の悪魔] である「日の高さによる明るさの感じ方の違い」を解決する事を目的とします。
一般的に考えられる方法は世の中の失敗した人たちがすべて試したと思いますので排除します。
まずは、そのための下準備として、次の事柄を定義します。

屋根の上に建物のミニチュア模型を作成して中に光センサーを置きます。
そうすると、容易に建物内部と明るさは同じだと推測できます。
曇りや晴れの日、なにか異常気象があっても明るさは同等だと考えられます。
次に、建物模型を光センサー自体に置き換えます。

光センサーの値は前の例に従って建物内部の明るさと同じだと考えられます。
つまり、建物内部の明るさは光センサーの値で測定できて、他には測定する必要が無いと定義できました。
これにより、一般的に考えられるような「建物内部にカメラを付けて明るさを計る」や「いたるところにルクスメータを置いて統計を取る」などをする必要が無くなります。

この定義がなんだ?と思うかもしれません、無駄な思考や混乱、他人からの意見を減らす「オッカムの剃刀」的な物だと思っています。


▽ データー構造の作成
明るさの感じ方は人間の感覚によるものだと仮定して、照明を点灯するタイミングをデータ構造に落とし込みます。
正確な腕時計・メモ帳を準備して照明を制御する部屋の中に一日中居座って明るさを意識しながら照明を必要とするタイミングを自分自身で感じてその時間をメモに記録します。
正確な時間で記録される屋根上センサーのログと突き合わせて時間毎に照明が必要になる明るさの表を作成していきます。
これにより、日の傾き(時間)と照明を点灯する明るさの人間の感覚の閾値が明らかになるはずです。

日々の記録により得られた自分の感覚の閾値のデータ

日々の記録ですから、約1ヶ月ほどのデータ記録中に季節が移り変わり、太陽の角度が変わるため値の精度にゆらぎがあります。
そのままのデータではガタガタで使えないのでエクセルでそれらしいカーブに補正しました。
このデータに従って照明を制御した所、自分の中では100点満点中60点程度の精度で制御ができました。
まだまだ調整する必要がありますが、とりあえず自分の中では合格ラインだと思います。


▼ 季節による太陽の角度の補正
とりあえず、日々の制御はそれなりにできましたが、季節が移り替わると太陽の角度が変化してゆきます。
一年で昼が最も短い日である12月22日あたりの冬至と、1年のうちで最も昼の時間(日の出から日没までの時間)が長くなる6月21日頃の夏至があります。
この太陽の角度の変化により、明るさに対する認識、感覚と閾値のデータが変化するのは容易に想像できます。

・太陽の角度の計算
地球の地軸の傾きと太陽の位置を考えて冬至と夏至の太陽の角度がどれほど変化するのか計算してみたいと思います。

● 夏至の太陽の角度の計算
  夏至の地球

夏至の地球と日本の位置を調べます、地球の地軸は23.4度傾いており、日本(岐阜県)は北緯35度に位置しています


次に太陽の方向を書きます、そうすると真上から11.6度傾いていることがわかります。
水平線を基準とすれば、90 - 11.6 = 78.4 で夏至の太陽角は78.4度であると求められます。

● 冬至の太陽の角度の計算
  冬至の地球

同様に冬至の地球と日本の位置を調べます。


太陽の方向を書き、角度を計算します。
35 + 23.4 = 58.4 であり、真上から58.4度傾いていることがわかります。
水平線を基準とすれば、90 - 58.4 = 31.6 であり、冬至の太陽角は31.6度であると求められます。

計算によると一年の間に太陽の角度が46.8度と大きく変化します。
一日は24時間で太陽が一周360度、1時間当たりは15度になります、46.8度の角度は3時間ほど。
閾値が3時間ほどずれると考えると、閾値のデータのグラフでの3時間の最大変化量は30ほどでしたので、
冬至と夏至の制御では最大30ほどオフセット値が必要になると考えられます。
調整しながら体感とログとの比較を行い理屈が正しいのか様子を見る事にします。


▼ 季節ごとの変化をアプリへ適用
季節毎の変化が最大30程度だと求められましたので、次のような設定データを作成しました。
    '季節の変化によるオフセット値
    map1_Season = Array( _
        '1月 2月   3月   4月  5月  6月  7月  8月   9月  10月  11月 12月  全部21日基準
      0, 20,  51,  79,  110, 140, 171, 201, 232,  263,  293,  324, 354, 366, _
    -30,-30, -20, -15,   -7,   0,   0,   0,  -7,  -15,  -20,  -30, -30, -30  _
    )


このデータを元に線形補間を行い、年初からの日数により値を求めて日々の制御の閾値を動的に変更していきます。


■ ノイズ対策
近くにあるモーターや他の電力を消費する機器、電源の安定性などによりノイズが侵入し制御がうまくいきません。
ノイズによりA/D変換が10%以上揺らいて不安定なセンサーの値しか取れないのが原因でした。
そこで、光センサーと別に仮想センサー(値を固定)を用意してノイズによる誤差を校正します。

(ポート0が光センサー、赤枠ポート1が仮想センサー)
仮想センサーは光センサー部分にボリュームを接続し感度が固定してあり( 制御によく使う値付近 )、値の変動値を使って光センサーの値を校正します。


■ 光センサーの定量化
光センサー部分は フォトレジスタで明るさを測定する の回路を使っていますが、アナログ回路ですので同じものを複製できません。
そこで回路に GY-30 照度センサー を取り付けてアナログ値と明るさを表す単位のルクスの相関がわかるグラフを作成します。
ルクスとの相関がわかれば、新たに作成したアナログセンサーを校正できますし他のブランドのセンサーに置き換える事ができるようになります。
なによりセンサー自体をデジタル化すると同一の装置を簡単に複製できるようになり利便性が向上します。
しかし、デジタルセンサーでは感度の上限が思ったより高くないようで、直射日光下では値が飽和してしまうためフィルタにより調整する必要があるようです。

▼ コントローラの改造
まずは、コントローラとして使用しているArduinoに GY-30 照度センサー の回路と同様に接続しました。
次に、コントローラとセンサーを接続するためにプログラムを書き換えます。
デジタルセンサーからのルクス値をパソコンから読み出すための命令 "x[CR][LF]" を追加しました。

#include <avr/wdt.h>
#include <Wire.h>

//GY-30 接続先のアドレス
#define ADDRESS 0b00100011
unsigned char buff_gy30[10];

#define SYSTEM_TIMEOUT 60  //通信途絶後60秒で初期化
#define BUF_SIZE 255

void setup() {
  wdt_enable(WDTO_4S);  //WDTを4秒で設定し有効化
  //デジタルピン2番から11番まで出力に設定
  for (int i = 2; i < 12; i++) {
    pinMode(i, OUTPUT);
  }
  Serial.begin(9600, SERIAL_8E1);  //偶数パリティ

  Wire.begin();
  Wire.beginTransmission(ADDRESS);
  Wire.write(0b00010000);
  Wire.endTransmission();

  serial_wdt_reset();
}

char buff[BUF_SIZE];
char buff_old[BUF_SIZE];

char pin_status[11] = { '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '\0' };  //デジタルピン2番から11番の状態を保持し一度に転送
unsigned int str_pt;
unsigned long int timeout_timer;

//ピンの状態をまとめて転送
void trans_pin_status() {
  int j = 0;
  for (int i = 2; i < 12; i++) {
    if (pin_status[j] == '1') {
      digitalWrite(i, LOW);  //出力先がアクティブローのため反転
    } else {
      digitalWrite(i, HIGH);
    }
    j++;
  }
}

//ピンに転送前の元データを初期化する
void reset_pin_status() {
  for (int i = 0; i < 10; i++) {
    pin_status[i] = '0';
  }
  pin_status[10] = '\0';
}

unsigned long serial_wdt_counter;
//シリアル通信がSYSTEM_TIMEOUT秒停止した場合に出力をリセットする
void serial_wdt() {
  unsigned long pos = ((unsigned long)millis()) - serial_wdt_counter;
  if (pos > ((unsigned long)((unsigned long)SYSTEM_TIMEOUT * 1000))) {
    reset_pin_status();  //タイムアウトしているため初期化
  }
}

//リセットタイマーをリセットする
void serial_wdt_reset() {
  serial_wdt_counter = millis();
}

void loop() {
  wdt_reset();  //WDTのリセット
  serial_wdt();
  if (Serial.available()) {
    char c = char(Serial.read());
    timeout_timer = millis();  //msを取得
    buff[str_pt] = c;
    str_pt++;
    if (c == '\n') {  //LFを終端文字とする
      buff[str_pt] = '\0';
      if (strcmp(buff_old, buff)) {
        //比較文字列と違うため保存
        strcpy(buff_old, buff);
        Serial.println("NG");
      } else {
        //比較文字列と同じため命令を実行
        if (buff[0] == 'w') {
          int j = 0;
          for (int i = 1; i <= 10; i++) {
            if (buff[i] == '1') {
              pin_status[j] = '1';
            } else {
              pin_status[j] = '0';
            }
            j++;
          }
          Serial.println("OK");
          serial_wdt_reset();
        } else if (buff[0] == 'r') {
          //2回出力
          Serial.print("r,");
          Serial.println(pin_status);
          Serial.print("r,");
          Serial.println(pin_status);
          serial_wdt_reset();
        } else if (buff[0] == 'a') {
          static unsigned long c;
          int _ar[6];
          _ar[0] = analogRead(A0);
          _ar[1] = analogRead(A1);
          _ar[2] = analogRead(A2);
          _ar[3] = analogRead(A3);
          _ar[4] = analogRead(A4);
          _ar[5] = analogRead(A5);

          Serial.print("a,");
          for (int i = 0; i < 6; i++) {
            Serial.print(_ar[i]);
            Serial.print(',');
          }
          Serial.println(c);
          Serial.print("a,");
          for (int i = 0; i < 6; i++) {
            Serial.print(_ar[i]);
            Serial.print(',');
          }
          Serial.println(c++);
          serial_wdt_reset();
        } else if (buff[0] == 'x') {
          unsigned char*buf_pt;
          buf_pt = buff_gy30;
          Wire.requestFrom(ADDRESS, 2); //接続し2byte取得する
          while (Wire.available()) {
            *buf_pt++ = (unsigned char)Wire.read();
          }
          if ((buf_pt - buff_gy30) >= 2) {
            unsigned short pos;
            pos = buff_gy30[0] << 8;
            pos = pos + buff_gy30[1];
            pos = pos / 1.2;

            Serial.print("x,");
            Serial.println(pos);
            Serial.print("x,");
            Serial.println(pos);
          } else {
            Serial.println("NG");
          }
          serial_wdt_reset();
        } else if (buff[0] == 't') {
          Serial.println("NG");
        }
        buff_old[0] = '\0';  //命令が終了したため旧命令は破棄
      }
      str_pt = 0;
    }
    //バッファサイズを超えないようにする
    if (str_pt >= BUF_SIZE) {
      str_pt = 0;
    }
  } else {
    //データを受信せずに500ms経過した場合にはタイムアウトで初期化
    if (str_pt) {
      if ((millis() - timeout_timer) > 500) {
        str_pt = 0;
      }
    }
  }
  trans_pin_status();
}
デジタルセンサーの値をパソコンから読み出すために赤文字部分を追加しました。


▼ パソコンのソフトの改造
コントローラの改造が出来ましたので次にパソコンのソフトを改造します。
命令 "x[CR][LF]" を送信し、値を テキストボックス(Text5) に表示するようにしました。
form1.frm の Timer1_Timer() 関数を次のように修正しました。
Private Sub Timer1_Timer()
    If sleep_timer1 > 0 Then 'timer1を一時的に停止する
        sleep_timer1 = sleep_timer1 - 1
        Exit Sub
    End If
On Error GoTo Err1
    Do
        MSComm1.Output = "r" & vbCr & vbLf
    Loop While MSComm1.OutBufferCount >= 1
    Do
        MSComm1.Output = "r" & vbCr & vbLf
    Loop While MSComm1.OutBufferCount >= 1

    Do
        MSComm1.Output = "a" & vbCr & vbLf
    Loop While MSComm1.OutBufferCount >= 1
    Do
        MSComm1.Output = "a" & vbCr & vbLf
    Loop While MSComm1.OutBufferCount >= 1
    
    Do
        MSComm1.Output = "x" & vbCr & vbLf
    Loop While MSComm1.OutBufferCount >= 1
    Do
        MSComm1.Output = "x" & vbCr & vbLf
    Loop While MSComm1.OutBufferCount >= 1
On Error GoTo 0

    Dim str As String
    Dim st_arry() As String
    Do While data_ptr <> 0
        str = pop()
        
        If Mid(str, 1, 1) = "x" Then
            com_x_data_old = com_x_data
            com_x_data = str
            If com_x_data = com_x_data_old Then
                st_arry = Split(com_x_data, ",")
                Text5.Text = Replace(st_arry(1), vbCrLf, "") '最後のCRLFを取り除く
            End If
        End If
        
        If Mid(str, 1, 1) = "r" Then
            com_r_data_old = com_r_data
            com_r_data = str
            If com_r_data = com_r_data_old Then
                digital_pin(0) = CInt(Mid(str, 3, 1))
                digital_pin(1) = CInt(Mid(str, 4, 1))
                digital_pin(2) = CInt(Mid(str, 5, 1))
                digital_pin(3) = CInt(Mid(str, 6, 1))
                digital_pin(4) = CInt(Mid(str, 7, 1))
                digital_pin(5) = CInt(Mid(str, 8, 1))
                digital_pin(6) = CInt(Mid(str, 9, 1))
                digital_pin(7) = CInt(Mid(str, 10, 1))
                digital_pin(8) = CInt(Mid(str, 11, 1))
                digital_pin(9) = CInt(Mid(str, 12, 1))
            End If
        End If
    
        If Mid(str, 1, 1) = "a" Then
            com_a_data_old = com_a_data
            com_a_data = str
            If com_a_data = com_a_data_old Then
                'Dim st_arry() As String	変数宣言を削除
                st_arry = Split(com_a_data, ",")
                Text1(0).Text = st_arry(1)
                Text1(1).Text = st_arry(2)
                Text1(2).Text = st_arry(3)
	以下続くため省略
赤文字部分が追加した個所です。
次に、ログの書き出し部分 condition_log_write() を書き直し必要な値のみをファイルに出力するように変更しました。
'現在の状態をログに書き出す
Private Sub condition_log_write()
    str = ""
    str = "," + Text1(0).Text + "," + Text5.Text
    
    Call logWrite(str, Format(Now, "yyyymmdd") + ".csv")
End Sub
これにより、光センサーのアナログ値とデジタルセンサーのルクス値を同時に取得しファイルに保存できるようになりました。


▼ フォトレジスタ & 周辺回路のルクス値の測定
現在使用しているフォトレジスタと周辺回路は、夜の暗い時の最高値で 835 、夏の晴れた日の昼の最低値が 262 、薄暗いと思うのが 400前後の設定になっています。
(明るいと数値は小さく、暗いと数値が大きくなる)
フォトレジスタの種類によっては感度を同じにするために抵抗値の定数を変更する必要があると思います。

現在使用しているフォトレジスタ & 周辺回路とデジタルセンサーを比較してデータを取りました。
データを取るためには、受光面を同じにして(入射角度を合わせて)測定し二つのセンサーに均一に光が当たる事が必要になってきます。
そこで、デジタルセンサーにフォトレジスタを同じ角度で張り付けて入射角度が同じになるようにしました。

光の当たり方が均一になるように太陽光で測定しました。
フォトレジスタの値 ルクス値
530 7259
324 35386
292 48560
これらの数値を使ってマップを作成しグラフを描画しました。
const int ar[]={
/* x */  288,   292,   309,   324,   335,   351,   372,   402,   431,   460,   496,  530,
/* y */  54106, 48560, 39350, 35386, 31376, 26847, 21485, 16316, 12685, 10038, 8250, 7259
};

もうちょっと明るい所までデジタルセンサーで測定できればアナログセンサーを置き換える事が出来そうです。
(対候性のあるセンサーとフィルタがあればいいのですが)


▼ フォトレジスタの測定
使っているのは中国から個人輸入したフォトレジスタなのでスペックがよくわかりませんでした。
そこで、スペックの分かっているフォトレジスタを複数用意して似た性能の物を探してみました。

現在使用しているスペックのよくわからない物と秋月電子から購入した複数のフォトレジスタ

テスターで測定した結果、この2MΩのフォトレジスタが一番近い特性を持っているようです。
→後日使用しているフォトレジスタの型番がわかりました、GL5528 を使用しています。


■ 落雷サージ対策
制御する対象となる照明設備に落雷が落ちるとサージがリレーモジュールを突き抜けてマイコンのアースを通り、
USB接続のフォトカプラを突き抜けてパソコンに流れる事がわかりました。
被雷後、その場でマイコンが消去されリレーモジュールが2日後に故障しUSBシリアル変換モジュールが1週間後に故障しました。
(フォトカプラは信頼性が無くなったので破棄、マイコンは消去されているが無事のよう)
そこで、470Vバリスタをリレーモジュールの高圧側端子全てに取り付けてもう片方をアースに接続しました。
(これにより、高圧側が470V以上になるとアースに電気が流れるはず)

再び落雷が落ちた時の挙動を見てみたいと思います。

▼落雷サージ対策その2
パソコン側に絶対に落雷サージを流さないようにArduinoとパソコンを光ファイバーでつなぐ事にしました。
絶縁性能3000V程度のフォトカプラよりも圧倒的に絶縁性能が高いはずです。
シリアル通信を光ファイバー経由する
しかし、絶縁を得たもののデータの信頼性が低下しました(0.5秒に2コマンドを受信していると1週間に1度ほど間違ったコマンドを受信してしまう)。
そこでコマンドを全2重にして送受信していたものを、全3重にして3回同じコマンドを送受信した時にのみ実行する様に変更しエラーの発生確率を低下させました。


■ 最新ソースでの画面


1、の部分では、照明のON/OFFを手動で選択できるスイッチになっています。
 ON状態でオレンジ、OFF状態でグレー表示されて、自動制御がOFF状態ならボタンを押すごとにON/OFFを切り替えられます。
2、センサーの現在の値と移動平均値が表示されています。
3、プログラム内部の値を自由に表示できる窓にしてあります。
 現在は、No.1からNo.10までの内部値、H:点灯閾値 L:消灯閾値 OFFSET:季節によるオフセット timer:消灯までのタイマー を出力しています。
4、自動制御のON/OFF、明るさ制御のON/OFFを切り替えられます。
 自動制御の文字部分の色は現在の自動制御状態を表しており、チェックボックスをONにすることにより実際の照明が同期します。
5、センサー値の補正
 アナログピン1番にボリュームを取り付けて仮想センサーとしています、仮想センサーの検出値の誤差を差し引いて補正移動平均値としています。
6、グラフ描画エリア
 お好きなグラフを描画できます。
その他の部品
 命令カウントはアプリがコントローラに対して送信した命令をカウントしておりシステム動作中は常にカウントアップしています。
 即時反映ボタンは消灯までのタイマーのリセット時間を経過した事にすることにより閾値に対してのON/OFFが即時に反映されます。
 設定ボタンはコントローラの接続ポート先などを設定します。
 再接続/設定保存は、コントローラに対して再接続とアプリの設定内容の保存を行います。

その他機能
 一定の時間(19:10)になるとパソコンが自動的にシャットダウンされます(auto_control_stubに記述)


■ 最新ソースで動作する全回路図
落雷サージ対策としてセンサーの一端を接地しています。
電源もパソコンと別にして、パソコンとの通信にはフォトカプラを使用した絶縁を追加しました。
サージが流れた場合にはセンサーとコントローラを破壊して地面に抜けるつもりです。
この回路図の複雑さを増したほとんどの部品はノイズとの闘いとサージ対策により追加されたものです。
(最初にどれだけシンプルにしても結局は複雑になってしまうんですね・・・)


回路図に載っていませんが、
リレーモジュールの先には全ての配線にバリスタが取り付けられアースに接続されています。
絶縁として使用しているフォトカプラの代わりに光ファイバーでの通信経路に変更しています。


■最新のソースコード
version 1_5から信頼性を上げるために2重通信から3重通信に変更しました、パソコン側とArduino側両方を同時に更新しないと動作しません。
パソコン側の最新のソースはこちらですversion 1_5_4
Arduino側の最新のソースはこちらですArduinoでシリアル通信

随時更新中

▲トップページ