サーバールームの室温監視を行う

はじめに

こんばんは。「PowerShell Memo」管理人のnewpopsこと吉岡洋です。

この度、牟田口さんが企画された「PowerShell Advent Calendar 2011」に参加することになりました。
本イベントの12日目を務めさせていただきます。
今回紹介するのは「サーバールームの室温監視を行う」スクリプトです。

作成の経緯

今から3年前の夏、とあるサーバルームのエアコンの温度センサーの不正動作で「時々、設定温度通りに室温が下がらない」という事象に遭遇しました。この「時々」というのが曲者で、通常は22℃前後に保たれているのですが、週に1回程度、センサーが温度を正しく検知できずに、温度が上昇してしまうのです。
気づくのが遅れると、室温が30℃に上昇することもあるとのこと。夜に問題が起こると、朝まで気づかないので厄介ですね。


運用責任者の方いわく、エアコン業者が調査しても原因が分からないとのことで、室温の監視ができないかと相談を受けました。


・・・ということで、PowerShellで室温の監視スクリプトを作る事にしました。

室温監視スクリプトの要件

  • 室温の閾値(許容上限値)を設定できる
  • サーバールームの室温を定期的に取得する
  • 取得した温度をメールのサブジェクトに設定して、管理者に通知する
  • 閾値超過に関係なく必ずメールを送る(スクリプトが稼働している確証が欲しい)
  • 閾値温度を超えている場合は、「警報メール」を管理者に通知する
  • 室温の取得に失敗した場合は「取得失敗メール」を管理者に通知する
  • 室温を定期的にCSVに出力する
  • 設定はファイルで定義できること
  • 動作試験が容易であること

ハードウェア調達

まずは温度計ですが、ストロベリー・リナックス社の「USB温度・湿度計モジュール」を調達。


この製品は外部から温度モジュールにアクセスするためのDLL(USBMeter.dll)が付属しています。
また、組み立て済みの完成品で4980円と値段もお手頃です。

スクリプト作成におけるポイントと解説

本投稿の後方に掲載したスクリプトについて簡単に解説します。

定義ファイルはXML

今回、室温の閾値やメールの宛先など多くのものを定義ファイルに記述することになります。
私は、PowerShellでツールを作成する場合、定義ファイルは必ずXMLファイルにしています。
理由はPowerShellXMLファイルへのアクセスが非常に容易であるからです。

$xml = [xml](Get-Content ./config.xml)
$value = $xml.xxx.yyy.zzz

このような簡単な記述によりXMLの内容を「System.Xml.XmlDocument」型で取得し、値にアクセスできます。

Windows Native DLLへのアクセス

温度系モジュールに付属している「USBMeter.dll」は.Netで作成されたDLLではなく、C++で作成されたWindows Native DLLです。
PowerShellからWindows Native DLLにアクセスする方法はいくつかありますが、本スクリプトでは、USBMeter.dllへのアクセッサをVB.Netで記述し、VBCodeProviderクラスを用いて動的コンパイルするアプローチを採用しています。
動的コンパイルで得たアセンブリから、GetMethodメソッドで各メソッドへの参照を取得し、Invokeメソッドで実行することができます。

2つの起動モード

スクリプトの動作確認を容易にするために、以下の2つの起動モードを用意し、モード毎に定義ファイルを分けています。

  1. 通常モード → 定義ファイル:config.xml
  2. テストモード → 定義ファイル:config_test.xml

また、起動時のモードの指定方法にはSwitchParameterを利用しています。
SwitchParameterはコマンドレットで多用されている方式で、以下の例では「-Recurse」がSwitchParameterです。

Get-ChildItem -Recurse

SwitchParameterはパラメータ指定の有無で分岐する実装が容易であるため、私は好んで利用しています。

メールの送信

メールの送信には「System.Net.Mail.SmtpClient」クラスを利用します。
ただし、本スクリプトでは、メール送信サーバに認証が不要な場合を想定しています。
もし、認証が必要な場合は、SmtpClientのCredentialsプロパティに適切な設定が必要です。

スクリプト

室温監視スクリプト本体(CheckTemperature.ps1)


1: ###############################################################################
2: # スクリプト名 :CheckTemperature.ps1
3: # 概要 :USB接続型温度計の監視を行う
4: # Powered by Hiroshi Yoshioka
5: ###############################################################################
6: # 詳細
7: #
8: # 以下の2つのモードがあり、モード毎に定義ファイルを分けています。
9: # 1.通常モード → 定義ファイル:config.xml
10: # 2.テストモード → 定義ファイル:config_test.xml
11: # 動作確認を行う場合は、config_test.xmlを修正し、
12: # テストモードで実行してください。
13: #
14: # 使用例
15: # 1.通常モードで起動する
16: # C:\> ./CheckTemperature.ps1
17: #
18: # 2.テストモードで起動する
19: # C:\> ./CheckTemperature.ps1 -Test
20: #
21: ###############################################################################
22:  
23: Param([Switch]$Test)
24:  
25: # 初期定義
26: $CONFIG_DIRNAME = 'config'
27: $CONFIG_FILENAME = 'config.xml'
28: $CONFIG_FILENAME_TEST = 'config_test.xml'
29:  
30: $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
31: $CONFIG_DIR = Join-Path $scriptDir $CONFIG_DIRNAME
32: $CONFIG_PATH = Join-Path $CONFIG_DIR $CONFIG_FILENAME
33: $CONFIG_PATH_TEST = Join-Path $CONFIG_DIR $CONFIG_FILENAME_TEST
34: $CHECK_FAULT = -1
35:  
36: # USBMetr.dllが提供する関数を利用可能にする
37: $provider = New-Object Microsoft.VisualBasic.VBCodeProvider
38: $params = New-Object CodeDom.Compiler.CompilerParameters
39: $params.GenerateInMemory = $True
40: $source = @'
41: Module USBMeter
42:  
43: Public Declare Function GetVers Lib "USBMeter.dll" Alias "_GetVers@4" (ByVal dev As String) As String
44: Public Declare Function FindUSB Lib "USBMeter.dll" Alias "_FindUSB@4" (ByRef index As Integer) As String
45: Public Declare Function GetTempHumid Lib "USBMeter.dll" Alias "_GetTempHumid@12" (ByVal dev As String, ByRef temp As Double, ByRef humid As Double) As Integer
46: Public Declare Function ControlIO Lib "USBMeter.dll" Alias "_ControlIO@12" (ByVal dev As String, ByVal port As Integer, ByVal val_Renamed As Integer) As Integer
47: Public Declare Function SetHeater Lib "USBMeter.dll" Alias "_SetHeater@8" (ByVal dev As String, ByVal val_Renamed As Integer) As Integer
48: Public Declare Function GetTempHumidTrue Lib "USBMeter.dll" Alias "_GetTempHumidTrue@12" (ByVal dev As String, ByRef temp As Double, ByRef humid As Double) As Integer
49:  
50: Public g_temp As Double
51: Public g_humid As Double
52:  
53: Function GetVers_PS(ByVal dev As String) As String
54: GetVers_PS = GetVers(dev)
55: End Function
56:  
57: Function FindUSB_PS(ByRef index As Integer) As String
58: FindUSB_PS = FindUSB(index)
59: End Function
60:  
61: Function GetTempHumid_PS(ByVal dev As String) As Integer
62: GetTempHumid_PS = GetTempHumid(dev, g_temp, g_humid)
63: End Function
64:  
65: Function ControlIO_PS(ByVal dev As String, ByVal port As Integer, ByVal val_Renamed As Integer) As Integer
66: ControlIO_PS = ControlIO(dev, port, val_Renamed)
67: End Function
68:  
69: Function SetHeater_PS(ByVal dev As String, ByVal val_Renamed As Integer) As Integer
70: SetHeater_PS = SetHeater(dev, val_Renamed)
71: End Function
72:  
73: Function GetTempHumidTrue_PS(ByVal dev As String) As Integer
74: GetTempHumidTrue_PS = GetTempHumidTrue(dev, g_temp, g_humid)
75: End Function
76:  
77: End Module
78: '
@
79:  
80: $compilerResults = $provider.CompileAssemblyFromSource($params, $source)
81: $assembly = $compilerResults.CompiledAssembly
82: $USBMeter = $assembly.GetType("USBMeter")
83:  
84: # メソッド
85: $GetVers_PS = $USBMeter.GetMethod("GetVers_PS")
86: $FindUSB_PS = $USBMeter.GetMethod("FindUSB_PS")
87: $GetTempHumid_PS = $USBMeter.GetMethod("GetTempHumid_PS")
88: $ControlIO_PS = $USBMeter.GetMethod("ControlIO_PS")
89: $SetHeater_PS = $USBMeter.GetMethod("SetHeater_PS")
90: $GetTempHumidTrue_PS = $USBMeter.GetMethod("GetTempHumidTrue_PS")
91:  
92: #############################################################
93: # 関数名 Get-Temperature
94: # 概要 温度を取得する
95: # 引数 なし
96: # 戻り値 温度(取得できなかった場合は-1を返す)
97: # 戻り値型 Double
98: #############################################################
99: function Get-Temperature()
100: {
101: $retFault = -1
102:
103: # 温度計のデバイス名を取得する
104: $device = $FindUSB_PS.Invoke($null, @(0))
105:  
106: # デバイス名が空文字の場合は -1 を返す。
107: if ($device -eq ''){return $retFault}
108:  
109: # 温度/湿度を取得する
110: $ret = $GetTempHumidTrue_PS.Invoke($null, @($device))
111:
112: # 取得に失敗した場合は -1 を返す。
113: if ($ret -ne 0){return $retFault}
114:  
115: # 取得に成功した場合の処理
116: $temp = $USBMeter.GetField("g_temp").GetValue($null)
117: return $temp
118: }
119:  
120:  
121: #############################################################
122: # 関数名 Send-Mail
123: # 概要 メールを送信する
124: # 引数 以下のスイッチの中から1つ指定する
125: # -Normal 正常メール
126: # -Alarm 警報メール
127: # -Fault 失敗メール
128: # 戻り値 なし
129: # 戻り値型 なし
130: #############################################################
131: function Send-Mail($tempValue, [Switch]$Normal, [Switch]$Alarm, [Switch]$Fault)
132: {
133: if ($Normal.isPresent)
134: {
135: $mailConfig = $xml.USBMeter.NormalMail
136: }
137: elseif ($Alarm.isPresent)
138: {
139: $mailConfig = $xml.USBMeter.AlarmMail
140: }
141: elseif ($Fault.isPresent)
142: {
143: $mailConfig = $xml.USBMeter.FaultMail
144: }
145:  
146: ## メール設定(共通)
147: $common = $xml.USBMeter.Common
148: $from = $common.Mail.From
149: $smtp = $common.Mail.Smtp
150: ## メール設定(個別)
151: # 送信アドレス
152: $OFS = ','
153: $to = [String]$mailConfig.Address
154: $OFS = ' '
155: # サブジェクト
156: $subject = $mailConfig.Subject
157: $tempStr = "{0,4:##0.0}℃" -F $tempValue
158: $subject = $subject.Replace('[TempValue]', $tempStr)
159: $date = Get-Date -Uformat "%Y/%m/%d"
160: $subject = $subject.Replace('[Date]', $date)
161: $time = Get-Date -Uformat "%H:%M:%S"
162: $subject = $subject.Replace('[Time]', $time)
163:  
164: # メール送信
165: $mailer = New-Object System.Net.Mail.SmtpClient($smtp)
166: $mailer.Send($from, $to, $subject, "")
167:
168: # ログ出力
169: Write-Log $subject
170:  
171: # CSV出力
172: Write-Csv $tempValue
173: }
174:  
175: #############################################################
176: # 関数名 Write-Log
177: # 概要 ログファイルに文字列を書き込む
178: # 引数 書き込む文字列
179: # 戻り値 なし
180: # 戻り値型 なし
181: #############################################################
182: function Write-Log($msg)
183: {
184: $logMonth = Get-Date -Uformat "%Y%m"
185: $logDate = Get-Date -Uformat "%Y%m%d"
186: $now = Get-Date -Uformat "[%Y/%m/%d %H:%M:%S]"
187: $logfilePath = $logfilePathDef.Replace('[LogMonth]', $logMonth)
188: $logfilePath = $logfilePath.Replace('[LogDate]', $logDate)
189:  
190: $logfileDir = Split-Path $logfilePath -Parent
191: if ((Test-Path $logfileDir) -eq $false){[void](md $logfileDir)}
192: $msg = "$now " + $msg
193: Write-Host $msg
194: $msg >> $logfilePath
195: }
196:  
197: #############################################################
198: # 関数名 Write-Csv
199: # 概要 CSVファイルに時刻と温度データを書き込む
200: # 引数 温度
201: # 戻り値 なし
202: # 戻り値型 なし
203: #############################################################
204: function Write-Csv($temp)
205: {
206: $csvfileDir = Split-Path $csvfilePath -Parent
207: if ((Test-Path $csvfileDir) -eq $false){[void](md $csvfileDir)}
208:  
209: $date = Get-Date -Uformat "%Y/%m/%d %H:%M:%S"
210: $data = [String]$date + "," + [String]$temp
211: $data | Out-File $csvfilePath -Append -Encoding Default
212: }
213:  
214: ######################################################################
215: # メイン
216: ######################################################################
217:  
218: # 各種設定を読み込む
219: if ($Test.isPresent)
220: {
221: # テストモードの場合
222: $xml = [xml](Get-Content $CONFIG_PATH_TEST)
223: }
224: else
225: {
226: # 通常モードの場合
227: $xml = [xml](Get-Content $CONFIG_PATH)
228: }
229:  
230: # 温度の閾値
231: $tempThreshold = $xml.USBMeter.Temperature.Threshold
232: # チェック間隔(秒)
233: [int]$checkIntervalSec = $xml.USBMeter.CheckInterval.Second
234: # チェック間隔(分)
235: [String]$checkIntervalMin = "{0,4:0.00}" -F ($checkIntervalSec / 60)
236:  
237: # ログファイルの相対パス
238: $logfilePathDef = Join-Path $scriptDir $xml.USBMeter.Common.Logfile
239: # CSVファイルの相対パス
240: $csvfilePath = Join-Path $scriptDir $xml.USBMeter.Common.Csvfile
241:  
242: Write-Log '========================================='
243: if ($Test.isPresent)
244: {
245: # テストモードの場合
246: Write-Log '◆◆◆テストモードで起動しました◆◆◆'
247: }
248: else
249: {
250: # 通常モードの場合
251: Write-Log '◆◆◆通常モードで起動しました◆◆◆'
252: }
253: Write-Log '========================================='
254: Write-Log '   ■温度チェック:開始■'
255: Write-Log "    閾値:$tempThreshold ℃"
256: Write-Log "    間隔:$checkIntervalSec 秒($checkIntervalMin 分)"
257: Write-Log '========================================='
258:  
259: # 指定されたチェック間隔で、温度のチェックを行う
260: while($true)
261: {
262: # 温度を取得する
263: $temperature = Get-Temperature
264: # 温度の取得に失敗した場合は「失敗メールを送る」
265: if($temperature -eq $CHECK_FAULT)
266: {
267: $temperature = -1
268: Send-Mail $temperature -Fault
269: }
270: # 温度が閾値を超えているかチェックする
271: elseif ($temperature -gt $tempThreshold)
272: {
273: # 閾値を超えている場合は「警報メールを送る」
274: Send-Mail $temperature -Alarm
275: }
276: else
277: {
278: # 閾値を超えていない場合は「正常メールを送る」
279: Send-Mail $temperature -Normal
280: }
281: Start-Sleep -Seconds $checkIntervalSec
282: }

定義ファイル(./config/config.xml


1: <?xml version="1.0" encoding="Shift_JIS"?>
2: <USBMeter>
3: <Temperature>
4: <Threshold>35</Threshold>
5: </Temperature>
6:  
7: <CheckInterval>
8: <Second>900</Second>
9: </CheckInterval>
10:  
11: <Common>
12: <Mail>
13: <From>'室温チェッカー'&lt;admin@xxx.com&gt;</From>
14: <Smtp>xxx.smtp.com</Smtp>
15: </Mail>
16: <Logfile>log/[LogMonth]/Temperature_[LogDate].log</Logfile>
17: <CsvFile>csv/Temperature.csv</CsvFile>
18: </Common>
19:  
20: <NormalMail>
21: <Subject>[温度]OK : [TempValue] : [Date] [Time]</Subject>
22: <Address>admin@xxx.com</Address>
23: <Address>yoshioka@xxx.com</Address>
24: </NormalMail>
25:  
26: <AlarmMail>
27: <Subject>[温度]NG : [TempValue] : [Date] [Time]</Subject>
28: <Address>admin@xxx.com</Address>
29: <Address>yoshioka@xxx.com</Address>
30: </AlarmMail>
31:  
32: <FaultMail>
33: <Subject>[温度]NG : 検出失敗 : [Date] [Time]</Subject>
34: <Address>admin@xxx.com</Address>
35: <Address>yoshioka@xxx.com</Address>
36: </FaultMail>
37: </USBMeter>

久々に書き込み

お久しぶりです。

業務に没頭していたらあっという間に2年経ってしまいました。(^^;


業務でちょっとしたツールが欲しいときは、たいていPowerShellで作っています。
マイペースで紹介していこうと思います。

BASIC認証つきHTTPSページへのアクセス

わりと最近作ったものとしては、
BASIC認証つきHTTPSページにあるXMLに定期的にアクセスして、情報を取得するスクリプトですかね。


CA署名のない証明書のサイトへアクセスする際、処理に苦労しました。


Delegateを使わなければ実現できなかったので、
以下のページからDownloadできるNew-Delegate.ps1を利用させてもらいました。
http://www.microsoft.com/japan/windowsserver2008/countdown2008/powershell/default.mspx

オセロゲームを作ろう(8):盤面描画とカーソル移動(3)

盤面描画とカーソル移動

引き続き、盤面描画とカーソル移動のサンプルについての解説です。


本アプリでは、エラーやデバッグ情報など、オセロ盤の下方にメッセージを表示します。
メッセージを表示する際、一時的にカーソルをオセロ盤下方に移動しますが、その後、カーソルを元の位置に復帰しなければなりません。


そこで必要になる処理が、カーソル位置のバックアップ/リストアです。
それぞれ、Push-Cursor関数、Pop-Cursor関数が担当します。

カーソル位置のバックアップ


170 : function Push-Cursor()
171 : {
172 : $script:savedCursorPositionX = $rui.CursorPosition.X
173 : $script:savedCursorPositionY = $rui.CursorPosition.Y
174 : }

カーソル位置のリストア


176 : function Pop-Cursor()
177 : {
178 : Move-Cursor $script:savedCursorPositionX $script:savedCursorPositionY
179 : }

オセロゲームを作ろう(7):盤面描画とカーソル移動(2)

盤面描画とカーソル移動

前回紹介した、盤面描画とカーソル移動のサンプルについて解説します。

盤面描画


135 : # オセロ盤の描画
136 : function Draw-Board()
137 : {
138 : Write-Host "┌─┬─┬─┬─┬─┬─┬─┬─┐" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
139 : for($i=1; $i -le 7;$i++)
140 : {
141 : Write-Host "│ │ │ │ │ │ │ │ │" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
142 : Write-Host "├─┼─┼─┼─┼─┼─┼─┼─┤" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
143 : }
144 : Write-Host "│ │ │ │ │ │ │ │ │" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
145 : Write-Host "└─┴─┴─┴─┴─┴─┴─┴─┘" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
146 : }

Draw-Init関数で最初の3行のメッセージを、
そこからコールされるDraw-Board関数でオセロ盤を描画します。
#139〜143行目はfor文を使っているので可読性が悪いですね。。。(^^;


結果はこんな感じ。


==================================
Othello by PowerShell
==================================
┌─┬─┬─┬─┬─┬─┬─┬─┐
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
├─┼─┼─┼─┼─┼─┼─┼─┤
│ │ │ │ │ │ │ │ │
└─┴─┴─┴─┴─┴─┴─┴─┘

カーソル移動


152 : # 指定した位置にカーソルを移動する
153 : function Move-Cursor([int]$newX, [int]$newY)
154 : {
155 : $coordinate = New-Object System.Management.Automation.Host.Coordinates $newX, $newY
156 : $rui.CursorPosition = $coordinate
157 : }

Move-Cursor関数は引数に指定した座標にカーソルを移動します。
引数は移動先のX座標、Y座標です。
座標を指定してCoordinatesオブジェクトを生成し、CursorPositionに代入すればOKです。


ちなみに、オセロ盤の左上のセルの座標は、X=2、Y=4(上から5行目、左から3byte目)です。

上下左右キー操作


46 : # 上下左右のキー操作
47 : if($keyCode -eq $keyMap::Up) {Move-CursorByDirection ([String]$keyMap::Up); continue}
48 : if($keyCode -eq $keyMap::Down) {Move-CursorByDirection ([String]$keyMap::Down); continue}
49 : if($keyCode -eq $keyMap::Left) {Move-CursorByDirection ([String]$keyMap::Left); continue}
50 : if($keyCode -eq $keyMap::Right){Move-CursorByDirection ([String]$keyMap::Right);continue}

キー入力の監視で解説した、Get-InputKey関数の中で上下左右キーを捕捉します。
上下左右キーのカーソル移動は処理が似ているので、Move-CursorByDirection関数に集約させており、
その際、引数として、Stringにキャストした$KeyMap(Keys列挙体)を渡しています。


159 : # 上下左右のキーを押した時のカーソル移動
160 : function Move-CursorByDirection([String]$key)
161 : {
162 : ($oldX, $oldY) = ($rui.CursorPosition.X, $rui.CursorPosition.Y)
163 : if($key -eq "Up") {$newX = $oldX; if($oldY -gt 0){$newY = $oldY - 1}}
164 : elseif($key -eq "Down") {$newX = $oldX; $newY = $oldY + 1}
165 : elseif($key -eq "Left") {if($oldX -gt 0){$newX = $oldX - 1}; $newY = $oldY}
166 : elseif($key -eq "Right"){$newX = $oldX + 1; $newY = $oldY}
167 : Move-Cursor $newX $newY
168 : }

Move-CursorByDirection関数では、まず162行目で現在のカーソル位置を取得。
163〜166行目で引数(Up,Down,Left,Rightの4種類)に応じて、カーソルの移動先を計算しています。

オセロゲームを作ろう(6):盤面描画とカーソル移動(1)

盤面描画とカーソル移動

今回はオセロの盤面描画とカーソル移動です。
以下のサンプルを実行してみてください。


1 : ###############################################################################
2 : # 初期処理/定義
3 : ###############################################################################
4 : # アセンブリ読み込み/参照
5 : $rui = $host.UI.RawUI
6 : [void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
7 : $keyMap = [Windows.Forms.Keys]
8 : # 初期化/定義
9 : $boardStartLine = 3
10 : ($startX, $startY) = (2, ($boardStartLine + 1))
11 : ($msgStartLine, $debugMsgStartLine) = (21, 23)
12 : $script:screen = $null
13 : #色定義
14 : ($boardFgColor, $boardBgColor) = ("Black", "White")
15 :
16 : ###############################################################################
17 : # メイン
18 : ###############################################################################
19 : function Start-Game()
20 : {
21 : Backup-ScrBuf
22 : Draw-Init
23 : Get-InputKey
24 : Restore-ScrBuf
25 : }
26 :
27 : ###############################################################################
28 : # キー入力受付関連の関数
29 : ###############################################################################
30 :
31 : # キー入力関数
32 : function Get-InputKey()
33 : {
34 : while($true)
35 : {
36 : # キー取得
37 : $keyInfo = $rui.ReadKey("NoEcho,IncludeKeyDown")
38 : $keyCode = $keyInfo.VirtualKeyCode
39 :
40 : # Enterを入力
41 : if($keyCode -eq $keyMap::Enter){continue}
42 :
43 : # Escを入力→終了
44 : if($keyCode -eq $keyMap::Escape){return}
45 :
46 : # 上下左右のキー操作
47 : if($keyCode -eq $keyMap::Up) {Move-CursorByDirection ([String]$keyMap::Up); continue}
48 : if($keyCode -eq $keyMap::Down) {Move-CursorByDirection ([String]$keyMap::Down); continue}
49 : if($keyCode -eq $keyMap::Left) {Move-CursorByDirection ([String]$keyMap::Left); continue}
50 : if($keyCode -eq $keyMap::Right){Move-CursorByDirection ([String]$keyMap::Right);continue}
51 :
52 : # Aを入力→現在の座標を表示
53 : if($keyCode -eq $keyMap::A)
54 : {
55 : $dX = $rui.CursorPosition.X
56 : $dY = $rui.CursorPosition.Y
57 : $dMsg = "X : " + $dX + " Y : " + $dY
58 : Show-DebugMsg $dMsg
59 : }
60 :
61 : }
62 : }
63 :
64 : ###############################################################################
65 : # メッセージ表示関数
66 : ###############################################################################
67 :
68 : # メッセージ表示
69 : function Show-Msg($msg)
70 : {
71 : Show-MsgInner $msgStartLine $msg $msgFgColor $msgBgColor
72 : }
73 :
74 : function Show-DebugMsg($msg)
75 : {
76 : Show-MsgInner $debugMsgStartLine $msg $boardFgColor $boardBgColor
77 : }
78 :
79 : function Show-MsgInner([int]$pos, [String]$msg, [ConsoleColor]$fg, [ConsoleColor]$bg)
80 : {
81 : Push-Cursor
82 : Move-Cursor 0 $pos
83 : Write-Host -NoNewLine " "
84 : Move-Cursor 0 $pos
85 : Write-Host -NoNewLine $msg -ForegroundColor $fg -BackgroundColor $bg
86 : Pop-Cursor
87 : }
88 :
89 : ###############################################################################
90 : # スクリーン操作関数
91 : ###############################################################################
92 :
93 : # スクリーンバッファのバックアップ
94 : function Backup-ScrBuf()
95 : {
96 : $rect = New-Object System.Management.Automation.Host.Rectangle
97 : $rect.Left = 0
98 : $rect.Top = 0
99 : $rect.Right = $rui.WindowSize.Width
100 : $rect.Bottom = $rui.CursorPosition.Y
101 : $script:screen = $rui.GetBufferContents($rect)
102 : }
103 :
104 : # スクリーンバッファのリストア
105 : function Restore-ScrBuf()
106 : {
107 : Clear-Host
108 : $origin = New-Object System.Management.Automation.Host.Coordinates(0, 0)
109 : $rui.SetBufferContents($origin, $script:screen)
110 : $pos = New-Object System.Management.Automation.Host.Coordinates(0, $script:screen.GetUpperBound(0))
111 : $rui.CursorPosition = $pos
112 : }
113 :
114 : ###############################################################################
115 : # 描画関連の関数
116 : ###############################################################################
117 :
118 : # 初期描画
119 : function Draw-Init
120 : {
121 : # タイトルの描画
122 : Clear-Host
123 : Write-Host "=================================="
124 : Write-Host " Othello by PowerShell "
125 : Write-Host "=================================="
126 :
127 : # オセロ盤の描画
128 : Move-Cursor 0 $boardStartLine
129 : Draw-Board
130 :
131 : # カーソル位置の初期化
132 : Move-Cursor $startX $startY
133 : }
134 :
135 : # オセロ盤の描画
136 : function Draw-Board()
137 : {
138 : Write-Host "┌─┬─┬─┬─┬─┬─┬─┬─┐" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
139 : for($i=1; $i -le 7;$i++)
140 : {
141 : Write-Host "│ │ │ │ │ │ │ │ │" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
142 : Write-Host "├─┼─┼─┼─┼─┼─┼─┼─┤" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
143 : }
144 : Write-Host "│ │ │ │ │ │ │ │ │" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
145 : Write-Host "└─┴─┴─┴─┴─┴─┴─┴─┘" -ForegroundColor $boardFgColor -BackgroundColor $boardBgColor
146 : }
147 :
148 : ###############################################################################
149 : # カーソル操作関数
150 : ###############################################################################
151 :
152 : # 指定した位置にカーソルを移動する
153 : function Move-Cursor([int]$newX, [int]$newY)
154 : {
155 : $coordinate = New-Object System.Management.Automation.Host.Coordinates $newX, $newY
156 : $rui.CursorPosition = $coordinate
157 : }
158 :
159 : # 上下左右のキーを押した時のカーソル移動
160 : function Move-CursorByDirection([String]$key)
161 : {
162 : ($oldX, $oldY) = ($rui.CursorPosition.X, $rui.CursorPosition.Y)
163 : if($key -eq "Up") {$newX = $oldX; if($oldY -gt 0){$newY = $oldY - 1}}
164 : elseif($key -eq "Down") {$newX = $oldX; $newY = $oldY + 1}
165 : elseif($key -eq "Left") {if($oldX -gt 0){$newX = $oldX - 1}; $newY = $oldY}
166 : elseif($key -eq "Right"){$newX = $oldX + 1; $newY = $oldY}
167 : Move-Cursor $newX $newY
168 : }
169 :
170 : function Push-Cursor()
171 : {
172 : $script:savedCursorPositionX = $rui.CursorPosition.X
173 : $script:savedCursorPositionY = $rui.CursorPosition.Y
174 : }
175 :
176 : function Pop-Cursor()
177 : {
178 : Move-Cursor $script:savedCursorPositionX $script:savedCursorPositionY
179 : }
180 :
181 : # 処理スタート
182 : Start-Game

上記コードを実行すると、オセロの盤面を描画します。

実行直後


上下左右キーを押下すると、キーの方向にカーソルが「1」移動します。
また、Aキーを押下すると現在のカーソル位置を表示します。
#カーソル位置を表示する処理はデバッグ目的で実装しています。

下に移動+カーソル位置表示

起動後、下キーを押下し、下方向に1移動。
その後、Aキーを押下し、カーソル位置を表示しました。



ただ、このままではオセロ盤の枠上にもカーソルが移動してしまいます。
このオセロゲームを、

  • 上下左右キーでカーソル移動
  • Enterで駒を置く

という仕様にする場合、カーソルが枠上に移動可能なのは好ましくありません。
駒が配置可能な8×8のマス上にカーソル移動を制限する必要があります。


つづく。。。

オセロゲームを作ろう(5):スクリーンバッファ操作(3)

スクリーンバッファの操作

前回に引き続き、前々回紹介した、スクリーンバッファ操作のサンプルについて解説します。

スクリーンバッファのリストア


# スクリーンバッファのリストア
function Restore-ScrBuf()
{
Clear-Host
$origin = New-Object System.Management.Automation.Host.Coordinates(0, 0)
$rui.SetBufferContents($origin, $script:screen)
$pos = New-Object System.Management.Automation.Host.Coordinates(0, $script:screen.GetUpperBound(0))
$rui.CursorPosition = $pos
}

Restore-ScrBuf関数は、Backup-ScrBuf関数でバックアップしたスクリーンバッファをリストアします。
手順は以下です。

  1. コンソールのクリア(Clear-Host)
  2. スクリーンバッファの描画座標の生成(Coordinatesオブジェクト)
  3. スクリーンバッファの描画(SetBufferContentsメソッド)
  4. カーソル位置を描画領域の最終行に移動(CursorPositionプロパティ)

まず、Clear-Host関数でコンソールをクリアします。
描画座標はコンソール左上ですので、X=0,Y=0のCoordinatesオブジェクトを生成します。
次に、SetBufferContentsメソッドでスクリーンバッファを描画します。

SetBufferContentsメソッド
System.Void SetBufferContents(Coordinates origin, BufferCell[,] contents)
System.Void SetBufferContents(Rectangle r, BufferCell fill)

SetBufferContentsメソッドは2種類ありますが、今回利用するのは前者で、
引数は、描画座標(Coordinatesオブジェクト)とスクリーンバッファ(BufferCellオブジェクトの2次元配列)です。


最後に、カーソル位置を描画領域の最終行に移動します。
最終行は、リストアするスクリーンバッファの1次元目の最終配列要素(最終行)になります。


つづく。。。

オセロゲームを作ろう(4):スクリーンバッファ操作(2)

スクリーンバッファの操作

前回紹介した、スクリーンバッファ操作のサンプルについて解説します。

スクリーンバッファのバックアップ


# スクリーンバッファのバックアップ
function Backup-ScrBuf()
{
$rect = New-Object System.Management.Automation.Host.Rectangle
$rect.Left = 0
$rect.Top = 0
$rect.Right = $rui.WindowSize.Width
$rect.Bottom = $rui.CursorPosition.Y
$script:screen = $rui.GetBufferContents($rect)
}

PowerShellでコンソールの特定領域を取得するには、以下の処理を行います。

  1. 取得する領域を指定する。(Rectangleオブジェクト)
  2. コンソールの指定領域を取得する(GetBufferContentsメソッド)

まず、Rectangleオブジェクトを生成し、指定領域の座標をLeft,Top,Right,Bottomプロパティに設定します。
このサンプルでは、Rightプロパティに「コンソールWindowの横幅」、Bottomプロパティに「現在カーソル位置の行」を設定することで、全描画領域を指定しています。


次に、GetBufferContentsメソッドで領域を取得します。

GetBufferContentsメソッド
System.Management.Automation.Host.BufferCell[,] GetBufferContents(Rectangle r)

引数はRectangleオブジェクトで、戻り値はBufferCellオブジェクトの2次元配列となっています。


サンプルでは、$script:screenに取得しており、内容は以下のように取得します。

$script:screen[0,5]  → 1行目の6桁目
$script:screen[1,10] → 2行目の11桁目


つづく。。。