MELSEC PLC Android スマホで I/O チェック 2019/03/24
MX Cpmponent 不要で、GX Works2, (3) を使った I/O チェッカーはこちらです。(2019/04/01)
・2019/03/24 Android アプリで、手動操作時のブザー音を追加.
・2019/03/08 Android アプリで、一度接続されると、接続先を変更できないのを手直し.
・2019/03/06.2 グリッドの列幅を調整可能にした.
・2019/03/06.1 音声メッセージを修正.
・2019/03/06 FX で一部取得できないアドレスがあるのを手直し.コメントファイルの保存、Windows での音声メッセージを追加.
・2019/02/20.1 アイコンを作成しました.
・2019/02/20 PC名の入力方法を変更.アドレス表記を修正.
・2019/02/17A FX5U (Ethernet) に対応
・2019/02/17 FX3 / QCPU に対応
・2019/02/14 初版作成(FX3 用)
これまでに iOS / Android 版 の PLC モニタを数種類試作してきましたが、COM ポートか、Ethernet ポートと接続に伴う設定が必要で、
現場でちょっと使うには、無理がありました。
今回は違います。
PLC との通信に MX Component 4 を使っているため、PLC の USB / COM / Ethernet ポートに接続するだけで、
Android 端末で PLC の I/O チェックが行えるようになります。
MX Component 付属のツール「通信設定ユーティリティ」で接続可能な PLC であれば、使用可能です。GX Works 2 / 3
との同時使用も可能です。
↓の画像では、FX3G/S (USB接続) になっていますが、FX5CPU (Eternet接続)、QCPU (USB接続)、FX3U (COM接続)
にも対応しています。
キーエンス KV Com + 、オムロン CX Compolet を使って同様のことができるのではないかと思います。
[ 弱点 ]
・MX Component が必要なこと。(実売価格は、そう高価でもない。)
[ MX Component の恩恵 ]
・CPU 直結接続が可能。シリアル通信よりかなり速いです。PLC 側での設定が不要。
・「通信設定ユーティリティ」を使うと、プログラムで接続パラメータを書かなくて良いので非常に楽。
■仕組み
PLC とパソコンは、MX Component USB または LAN ケーブルで直結接続し、250 msec 周期でポーリング。データを保持します。
パソコン内蔵の Bluetooth と Android 端末で通信し、250 msec 周期でパソコンの格納データをポーリングします。
(一度にモニタ出来るビットデバイス数は、128点。 表示は Q の場合: 128 点、FX の場合: 先頭 64 点のみ。)
外付けの Bluetooth アダプタでも使えると思いますが、試してはいません。
Android 端末から PLC と直接やりとりするよりかなり高速になります。
パソコンから見ると、Android 端末は Bluetooth 経由の COM ポートで、普通のシリアル通信と何ら変わりなく、
Bluetooth 用の特別なプログラムを書く必要はありません。
※あらかじめ、Bluetooth 経由の COM ポートの追加が必要です。
※Bluetooth は Classic Bluetooth で、iOS でも使用可能な Bluetooth LE とは異なります。
※パソコンと Andoroid 端末は最初の一度だけペアリングが必要です。
■パソコン側のアプリ
・単体で、I/O チェック (X, Y のみ)が可能です。
・ON / OFF の変化があったデバイスを大きく表示します。
※あらかじめコメントファイルを作っておくと、デバイスコメントが表示されます。
・SAPI (音声合成)がインストールされている場合、デバイス名とコメントを読み上げます。(2019/03/06)
※音声終了まで次の操作はできません。かなり作業効率が悪くなります。
※こちらの環境(Windows 10) では、SAPI 5 がインストールされていました。
FXの場合、表示されるのは先頭 64 点ですが、以降の 64 点を含めて 計 128 点の ON/OFF の状態変化をモニタしています。
例えば、先頭が 'X000 'の場合は、'X177' まで。これは、スマホ側も同様です。
・あらかじめ論理局番の説明を入力しておくと、接続先を選択しやすくなります。
・コメントファイルを作っておくと、変化にあったデバイスのコメントが表示されるようになります。
■スマホ側のアプリ
・パソコン側のアプリと連動して、周囲 10m 程度の範囲で、I/O チェック(X,Yのみ)が可能です。
・ON / OFF の変化があったデバイスを大きく表示します。
・ON / OFF の変化があったデバイスとコメントを音声で知らせます。(ポケットに入れたままで、I/O チェックができます。)
・入力チェック中のスクリーンショット
■Bluetooth 経由の COM ポートを使うには
・あらかじめ、「Bluetooth 設定」 で COM ポートを追加しておきます。
こちらの環境 (Winodws10) では、タスクトレイの [ ^ ] をクリック。Bluetooh のアイコンを右クリック。
ポップアップメニュから「設定を開く」を選択。「その他の Bluetooth オプション」をクリックすると「Bluetooth 設定」が出てきます。
・デバイスマネージャーから 「ポートの設定」を確認、変更できます。
■通信設定ユーティリティー
・あらかじめ、MX Component 付属の 「通信設定ユーティリティ」 で通信設定が必要です。
■ダウンロード
MXC4_IO.zip (Winodws 側アプリ EXE のみ Ver. 2019.03.06.2)
※MX Component 4 がインストールされている必要があります。
※高解像度環境で作成しているため、画面サイズが大きすぎる、配置が崩れることがあります。
・「通信設定ユーティリティ」で設定した論理局番、追加した「Bluetooth 経由 ComPort」 のポート番号を選択してください。
あらかじめ、論理局番の説明を入力しておくと、分かりやすいです。
・コメントファイルは、"X000, コメント1" のような CSVファイルです。エクセルでも作成できます。
一時的に使う場合、[アドレス作成] ボタンでアドレスを作成し、GX Works のコメントをクリップボードにコピーし、[ Ctrl ]
+ V キーでペーストできます。
MXC4_IO.apk (Android 側アプリ APK のみ Ver. 2019.03.24 手動操作時のブザー音追加版)
・最初の1回だけペアリングが必要です。
・初回起動時は、接続エラーになります。一番上のコンボボックスで接続先の PC 名を選択し、再起動します。
・通信異常の時は、Android 側のアプリを終了し、PC 側で [PLC CLOSE] -> [PLC OPEN] し、再度 Android 側のアプリを起動してください。
それでも通信異常になる時は、Android 側のアプリを終了し、PC 側のアプリを一度終了し、再度起動。その後 Android 側のアプリを起動してください。
・通信できない場合は、上記を数回繰り返してみてください。
※本アプリをインストール、使用したことによる事故、損害等の一切について、作者はその責を負いません。
※本アプリの著作権は、作者 f.izawa が所有し、これを主張します。
■連絡先
e-mail : f.izawa@dream.com (@を小文字に変えてください)
URL: http://www.izawa-web.com/
// --------------------------------------------------------------- // Windows 側 // --------------------------------------------------------------- unit Unit4; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.OleCtrls, ActProgTypeLib_TLB, Vcl.StdCtrls, ActUtlTypeLib_TLB, System.Bluetooth, System.Bluetooth.Components, AdPacket, OoMisc, AdPort, Math, Vcl.ComCtrls, Vcl.ExtCtrls, Vcl.Grids, AdSelCom, Vcl.Buttons, IniFiles, System.UITypes, ClipBrd, Vcl.ExtDlgs, SpeechLib_TLB, ComObj; type TBitAry = array [0..127] of Boolean; TWordAry = array [0..7] of SmallInt; type TForm4 = class(TForm) ActProgType1: TActProgType; ActUtlType1: TActUtlType; PageControl1: TPageControl; TabSheet1: TTabSheet; TabSheet2: TTabSheet; Label1: TLabel; Label2: TLabel; Label3: TLabel; Label4: TLabel; Label5: TLabel; SpeedButton1: TSpeedButton; SpeedButton2: TSpeedButton; SpeedButton3: TSpeedButton; SpeedButton4: TSpeedButton; Button5: TButton; Button6: TButton; Edit4: TEdit; Edit7: TEdit; Edit8: TEdit; Button3: TButton; Edit9: TEdit; ComboBox1: TComboBox; ComboBox2: TComboBox; Edit5: TEdit; Edit6: TEdit; Button4: TButton; ApdComPort1: TApdComPort; ApdDataPacket1: TApdDataPacket; Timer1: TTimer; Edit3: TEdit; StringGrid1: TStringGrid; StringGrid2: TStringGrid; StringGrid3: TStringGrid; Button1: TButton; Edit10: TEdit; Edit11: TEdit; SpeedButton5: TSpeedButton; CheckBox1: TCheckBox; StringGrid4: TStringGrid; ComboBox3: TComboBox; ComboBox4: TComboBox; Edit1: TEdit; Label6: TLabel; OpenTextFileDialog1: TOpenTextFileDialog; SaveTextFileDialog1: TSaveTextFileDialog; Button2: TButton; CheckBox2: TCheckBox; CheckBox3: TCheckBox; procedure ApdDataPacket1StringPacket(Sender: TObject; Data: AnsiString); procedure Timer1Timer(Sender: TObject); procedure Button5Click(Sender: TObject); procedure Button6Click(Sender: TObject); procedure FormCreate(Sender: TObject); procedure SpeedButton1Click(Sender: TObject); procedure SpeedButton2Click(Sender: TObject); procedure Button3Click(Sender: TObject); procedure StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer; Rect: TRect; State: TGridDrawState); procedure StringGrid1Click(Sender: TObject); procedure ComboBox1Change(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure Button1Click(Sender: TObject); procedure FormShow(Sender: TObject); procedure StringGrid2KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure SpeedButton5Click(Sender: TObject); procedure CheckBox1Click(Sender: TObject); procedure Edit1Change(Sender: TObject); procedure Button2Click(Sender: TObject); private { Private 宣言 } public { Public 宣言 } // PLC からの受信データ保持 WordAryOld : TWordAry; WordAryNew : TWordAry; // 内部用データ保持 BitAryOld : TBitAry; BitAryNew : TBitAry; // デバイス名 GB_DeviceHead : string; // 先頭番号 GB_DeviceStartNo : integer; // 「通信設定ユーティリティ」での論理局番 Gb_PLCStationNo : integer; GB_fxFlag : boolean; GB_SgScale : double; function GetDeviceComment(const devStr : string): string; procedure ReadCommentFile(const FileName : TFileName); procedure SaveCommentFile(const FileName : TFileName); end; var Form4: TForm4; SpVoice: OleVariant; TTSFlag : boolean; function OctToIntDef(const Value: string; def : integer): integer; function IntToOct(Value: integer; digits: Integer): string; function IntPower(n, k : integer):integer; implementation {$R *.dfm} uses Unit1; // n の k 乗 (Math ユニット不要) function IntPower(n, k : integer):integer; var i : integer; begin result := 1; for i := 1 to k do result := result * n; end; // 8 進数 -> 10進数 function OctToIntDef(const Value: string; Def :integer): integer; var i, len, n : integer; begin result := 0; len := Length(Value); for i := 1 to len do begin n := StrToIntDef(Value[i], -1); if (n >= 0 ) and (n < 8) then Inc(result, n * IntPower(8, len - i)) else begin result := Def; break; end; end; end; // 10 進数 -> 8 進数 function IntToOct(Value: integer; digits: Integer): string; var rest: Longint; oct: string; i: Integer; begin oct := ''; while Value <> 0 do begin rest := Value mod 8; Value := Value div 8; oct := IntToStr(rest) + oct; end; if Length(oct) < digits then for i := Length(oct) + 1 to digits do oct := '0' + oct; result := oct; end; // ***************************** // StringGrid でのキー操作 // ***************************** procedure SgKeyDown(SG: TSTringGrid; var Key: Word; Shift: TShiftState); var i, j, k, n : integer; sl : TStringList; s, s1 : string; xflag : boolean; begin if Key = VK_DELETE then begin with SG do begin if (Selection.Top <> Selection.Bottom) or (Selection.Left <> Selection.Right) then begin Key := 0; for i := Selection.Top to Selection.Bottom do for j := Selection.Left to Selection.Right do Cells[j, i] := ''; end; end; end; if ssCtrl in Shift then begin if true then begin xflag := (Key = Ord('X')) or (Key = Ord('x')); if (Key = Ord('C')) or (Key = Ord('c')) or xflag then begin Key := 0; Clipboard.AsText := ''; with SG do begin for i := Selection.Top to Selection.Bottom do begin for j := Selection.Left to Selection.Right do begin Clipboard.AsText := Clipboard.AsText + Cells[j, i]; if j < Selection.Right then Clipboard.AsText := Clipboard.AsText + #9 else Clipboard.AsText := Clipboard.AsText + #13#10; end; end; if xflag then begin for i := Selection.Top to Selection.Bottom do for j := Selection.Left to Selection.Right do Cells[j, i] := ''; end; end; end else if (Key = Ord('V')) or (Key = Ord('v')) then begin //with SG do // if EditorMode then EditorMode := false; Key := 0; with SG do begin sl := TStringList.Create; try s := Clipboard.AsText; while true do begin k := Pos(#13#10, s); if k = 0 then break else begin sl.Add(Copy(s, 1, k - 1)); Delete(s, 1, k + 1); end; end; for i := 0 to sl.Count-1 do begin s := SL[i]; j := 0; while true do begin k := Pos(#9, s); if k = 0 then s1 := Copy(s, 1, Length(s)) else begin s1 := Copy(s, 1, k - 1); Delete(s, 1, k); end; Cells[Selection.Left + j, Selection.Top + i] := s1; n := 1; while true do begin if Selection.Bottom < Selection.Top + i + (sl.Count * n) then break else Cells[Selection.Left + j, Selection.Top + i + (sl.Count * n)] := s1; Inc(n); end; if k = 0 then break; Inc(j); end; end; finally sl.Free; end; end; end; end; end; end; // -------------------------------------------- procedure TForm4.ReadCommentFile(const FileName : TFileName); // コメントファイル読み込み var sl : TStringList; cnt1, cnt2 : integer; i, n : integer; s, s1, s2 :string; begin cnt1 := 0; cnt2 := 0; sl := TStringList.Create; try sl.LoadFromFile(FileName); for i := 0 to sl.Count - 1 do begin n := Pos(',', sl[i]); s1 := Copy(sl[i], 1, n- 1); s2 := Copy(sl[i], n + 1); s := Uppercase(Copy(s1, 1, 1)); if s = 'X' then begin with StringGrid2 do begin Inc(cnt1); if RowCount <= cnt1 then RowCount := RowCount + 1; Cells[0, cnt1] := s1; Cells[1, cnt1] := s2; end; end else if s = 'Y' then begin with StringGrid3 do begin Inc(cnt2); if RowCount <= cnt2 then RowCount := RowCount + 1; Cells[0, cnt2] := s1; Cells[1, cnt2] := s2; end; end; end; finally sl.Free; end; with StringGrid2 do begin if cnt1 > 0 then begin if cnt1 < RowCount then RowCount := cnt1; end else begin RowCount := 2; Cells[0, 1] := ''; Cells[1, 1] := ''; end; end; with StringGrid3 do begin if cnt2 > 0 then if cnt2 < RowCount then RowCount := cnt2 else begin RowCount := 2; Cells[0, 1] := ''; Cells[1, 1] := ''; end; end; end; procedure TForm4.SaveCommentFile(const FileName : TFileName); // コメントファイル保存 var sl : TStringList; i : integer; begin sl := TStringList.Create; try with StringGrid2 do begin for i := 1 to RowCount -1 do sl.Add(Cells[0, i] + ',' + Cells[1, i]); end; with StringGrid3 do begin for i := 1 to RowCount -1 do sl.Add(Cells[0, i] + ',' + Cells[1, i]); end; sl.SaveToFile(FileName); finally sl.Free; end; end; function TForm4.GetDeviceComment(const devStr : string): string; var i, n : integer; sg : TStringGrid; s : string; begin result := ''; n := StrToInt('$' + Copy(devStr, 2)); if Copy(devStr, 1, 1) = 'X' then sg := StringGrid2 else sg := StringGrid3; with sg do begin for i := 1 to RowCount - 1 do begin s := Copy(Cells[0, i], 2); if (s <> '') and (n = StrToInt('$' + s)) then begin result := Cells[1, i]; break; end; end; end; end; procedure TForm4.ApdDataPacket1StringPacket(Sender: TObject; Data: AnsiString); // Bluetooth 仮想 COM ポートから、Android 端末からのコマンドを受信 var s, s0 : string; szDevice : WideString; lData : integer; n : integer; i : integer; res, res1, res2 : string; begin s := Trim(string(Data)); // Android からのコマンド Edit7.Text := s; s0 := Trim(Copy(s, 1, 2)); // CPU Type if s = 'CPU' then begin Timer1.Enabled := False; res := Edit9.Text; ApdComPort1.PutString(res + #13#10); Edit5.Text := res; Timer1.Enabled := True; end // 一括読み出し else if s0 = 'RD' then begin Timer1.Enabled := False; // 内部データを返信 res := ''; res1 := ''; res2 := ''; // 16進2桁×4 左から若い順 // 今回値 for i := 0 to 7 do res1 := res1 + WordAryNew[i].ToHexString(4); // 前回値 for i := 0 to 7 do res2 := res2 + WordAryOld[i].ToHexString(4); // デバイス名と開始番号(16進表示) res := res1 + res2 + ' ' + GB_DeviceHead + ' ' + GB_DeviceStartNo.ToHexString(4); // コメント if Edit3.Text <> '' then begin s := GetDeviceComment(Edit3.Text); if s <> '' then res := res + ' ' + s; end; ApdComPort1.PutString(res + #13#10); Edit5.Text := res; // 前回値を更新 WordAryOld := WordAryNew; Timer1.Enabled := True; end else if s0 = 'WR' then begin // 'WR Y0 1' Timer1.Enabled := False; with ActUtlType1 do begin // PLC に 書き込み s0 := Copy(s, 4); n := Pos(' ', s0); if n > 0 then begin // デバイス名 szDevice := Copy(s0, 1, n -1); // 無視 // lData := StrToIntDef(Copy(s0, n + 1), 0); // 反転 GetDevice(szDevice, lData); lData := abs(lData - 1); if SetDevice(szDevice, lData) = 0 then if lData = 1 then res := 'ON' else res := 'OFF' else res := 'NG'; end else res := 'NG'; ApdComPort1.PutString(res + #13#10); Edit5.Text := res; end; Timer1.Enabled := True; end // デバイス番号をセット // 'CF X 0', CF X 16'... else if s0 = 'CF' then begin Timer1.Enabled := False; // デバイス名 s0 := Copy(s, 4); n := Pos(' ', s0); if n > 0 then begin res := 'OK'; Edit5.Text := res; ApdComPort1.PutString(res + #13#10); GB_DeviceHead := Trim(Copy(s0, 1, n - 1)); // デバイス名 if GB_DeviceHead = 'Y' then ComboBox1.ItemIndex := 1 else ComboBox1.ItemIndex := 0; // 開始アドレス GB_DeviceStartNo := StrToIntDef('$' + Copy(s0, n + 1), 1); if not Gb_fxFlag then ComboBox2.ItemIndex := GB_DeviceStartNo div 32 else ComboBox2.ItemIndex := GB_DeviceStartNo div 16; // 変更を反映 ComboBox1Change(self); end; Timer1.Enabled := True; end else ApdComPort1.PutString('??' + #13#10); end; procedure TForm4.Button1Click(Sender: TObject); // コメントファイル読み込み begin OpenTextFileDialog1.InitialDir := ExtractFileDir(Edit10.Text); if OpenTextFileDialog1.Execute then begin Edit10.Text := OpenTextFileDialog1.FileName; ReadCommentFile(Edit10.Text); end; end; procedure TForm4.Button2Click(Sender: TObject); // コメントファイル保存 var fname : TFileName; flag : boolean; begin if (StringGrid2.Cells[0, 1] <> '') or (StringGrid3.Cells[0, 1] <> '') then begin SaveTextFileDialog1.InitialDir := ExtractFileDir(Edit10.Text); if SaveTextFileDialog1.Execute then begin fname := SaveTextFileDialog1.FileName; if ExtractFileExt(fname) = '' then fname := fname + '.csv'; flag := True; if FileExists(fname) then flag := MessageDlg('すでにファイルが存在します.上書きしますか?', mtInformation, [mbYes, mbNo], 0) = mrYes; if flag then begin SaveCommentFile(fname); Edit10.Text := fname; end; end; end; end; procedure TForm4.Button3Click(Sender: TObject); // 出力反転 var szDevice : string; lData : integer; edt : TEdit; begin if Sender as TButton = Button3 then edt := Edit8 else edt := Edit6; with ActUtlType1 do begin Timer1.Enabled := False; with edt do begin // デバイス名 szDevice := UpperCase(Text); // 反転 if GetDevice(szDevice, lData) = 0 then begin Font.Color := clWhite; lData := abs(lData - 1); if SetDevice(szDevice, lData) = 0 then if lData = 1 then Font.Color := clRed else Font.Color := clLime else Font.Color := clYellow; Timer1.Enabled := True; end else Font.Color := clYellow; end; end; end; procedure TForm4.Button5Click(Sender: TObject); // Bluetooth SPP 通信(仮想 COM ポート)接続 var s : string; begin Edit9.Text := ''; // CPU Name Edit7.Text := ''; Edit5.Text := ''; with ApdComPort1 do begin s := Copy(ComboBox4.Text, 4); s := Copy(s, 1, Length(s) -1); ComNumber := StrToIntDef(s, 4); Baud := 9600; StopBits := 1; DataBits := 8; Parity := TParity.pNone; SWFlowOptions := TSWFlowOptions.swfNone; end; with ApdDataPacket1 do begin Enabled := False; EndCond := [ecString]; EndString := #13#10; StartCond := scAnyData; TimeOut := 500; end; try ApdComPort1.Open := True; if ApdComPort1.Open then begin ApdDataPacket1.Enabled := True; end; except ShowMessage('ComPort Open Error'); end; with ActUtlType1 do begin // 対象デバイス if ComboBox1.ItemIndex = 1 then GB_DeviceHead := 'Y' else GB_DeviceHead := 'X'; // 「通信設定ユーティリティ」での論理局番 Gb_PLCStationNo := ComboBox3.ItemIndex; ActLogicalStationNumber := Gb_PLCStationNo; Timer1.Enabled := Open = 0; end; end; procedure TForm4.Button6Click(Sender: TObject); // PLC、Android 通信終了 begin with ActUtlType1 do begin Close; Timer1.Enabled := False; end; // Bluetooth SPP 通信(仮想 COM ポート)切断 if ApdComPort1.Open then ApdComPort1.Open := False; Label5.Caption := ''; end; procedure TForm4.CheckBox1Click(Sender: TObject); // CPU Type = FX / Other var i :integer; begin GB_fxFlag := CheckBox1.Checked; with StringGrid1 do begin for i := 0 to 15 do Cells[i + 1, 0] := i.ToHexString(1); for i := 0 to 15 do Cells[0, i + 1] := (i * 16).ToHexString(3); Cells[0, 0] := 'X'; Row := 1; Col := 1; end; with ComboBox2 do begin Items.Clear; if not GB_fxFlag then for i := 0 to 255 do Items.Add(IntToHex(i * 32, 3)) else for i := 0 to 255 do Items.Add(IntToOct(i * 16, 3)); ItemIndex := 0; end; with ComboBox1 do begin ItemIndex := 0; end; Edit6.Text := 'X000'; Edit8.Text := 'Y000'; end; procedure TForm4.ComboBox1Change(Sender: TObject); // デバイス X or Y var i : integer; begin if ComboBox1.ItemIndex = 0 then GB_DeviceHead := 'X' else GB_DeviceHead := 'Y'; if GB_fxFlag then begin GB_DeviceStartNo := ComboBox2.ItemIndex * 16; Edit6.Text := 'X' + IntToOct(GB_DeviceStartNo, 3); Edit8.Text := 'Y' + IntToOct(GB_DeviceStartNo, 3); end else begin GB_DeviceStartNo := ComboBox2.ItemIndex * 32; Edit6.Text := 'X' + IntToHex(GB_DeviceStartNo, 3); Edit8.Text := 'Y' + IntToHex(GB_DeviceStartNo, 3); end; with StringGrid1 do begin Cells[0, 0] := GB_DeviceHead; if GB_fxFlag then for i := 0 to 15 do Cells[0, i + 1] := IntToOct(GB_DeviceStartNo + i * 8, 3) else for i := 0 to 15 do Cells[0, i + 1] := IntToHex(GB_DeviceStartNo + i * 16, 3); Row := 1; Col := 1; end; for i := 0 to 127 do BitAryOld[i] := False; for i := 0 to 7 do WordAryOld[i] := 0; Edit3.Text := ''; Edit4.Text := ''; end; procedure TForm4.Edit1Change(Sender: TObject); begin GB_SgScale := StrToFloatDef(Edit1.Text, 1.0); StringGrid1.Repaint; end; procedure TForm4.FormCreate(Sender: TObject); var ini : TInifile; i : integer; begin GB_DeviceHead := 'X'; GB_DeviceStartNo := 0; GB_fxFlag := True; AdSelCom.ShowPortsInUse := False; for i := 0 to 32 do if AdSelCom.IsPortAvailable(i) then ComboBox4.Items.Add (AdPort.ComName(i) + '.'); with StringGrid4 do begin RowCount := 101; ColWidths[0] := 150; ColWidths[1] := 550; Cells[0, 0] := ' 論理番号'; Cells[1, 0] := ' コメント'; for i := 0 to 99 do begin Cells[0, i + 1] := i.ToString; end; end; ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini')); with ini do begin try with StringGrid4 do begin for i := 0 to 9 do Cells[1, i + 1] := ReadString('MXCompo', 'StationNo' + i.ToString, ''); end; with ComboBox3 do begin with StringGrid4 do for i := 0 to 99 do Items.Add(' '+ i.ToString + ' : ' + Cells[1, i + 1]); //ItemIndex := 0; end; ComboBox3.ItemIndex := ReadInteger('PLC', 'StationNo', 0); with ComboBox4 do ItemIndex := Items.IndexOf(ReadString('COM', 'PortNo', '')); Edit10.Text := ReadString('Device', 'CommentFileName', ''); GB_SgScale := StrToFloatDef(ReadString('Grid', 'TextScale', '2.0'), 1.0); Edit1.Text := Format('%.2f', [GB_SgScale]); finally Free; end; end; Edit3.Text := ''; Edit4.Text := ''; Edit5.Text := ''; Edit7.Text := ''; Label5.Caption := ''; with StringGrid2 do begin Cells[0, 0]:= ' デバイス'; Cells[1, 0]:= ' コメント'; ColWidths[1] := 450; end; with StringGrid3 do begin Cells[0, 0]:= ' デバイス'; Cells[1, 0]:= ' コメント'; ColWidths[1] := 450; end; Edit11.Text := ''; PageControl1.ActivePageIndex := 0; CheckBox1Click(self); TTSFlag := False; try SpVoice := CreateOleObject('SAPI.SpVoice'); TTSFlag := True; CheckBox2.Enabled := True; CheckBox3.Enabled := True; except ; end; end; procedure TForm4.FormDestroy(Sender: TObject); var ini : TIniFile; i : integer; begin with ActUtlType1 do Close; if ApdComPort1.Open then ApdComPort1.Open := False; ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini')); with ini do begin try WriteInteger('PLC', 'StationNo', ComboBox3.ItemIndex); WriteString('COM', 'PortNo', ComboBox4.Text); WriteString('Device', 'CommentFileName', Edit10.Text); with StringGrid4 do begin for i := 0 to 99 do WriteString('MXCompo', 'StationNo' + i.ToString, Cells[1, i + 1]); end; WriteString('Grid', 'TextScale', GB_SgScale.ToString); finally Free; end; end; end; procedure TForm4.FormShow(Sender: TObject); begin if (Edit10.Text <> '') and FileExists(Edit10.Text) then begin if MessageDlg( '前回終了時のコメントファイル' + #13 + Edit10.Text + #13 + 'を、読み込みますか?', mtInformation, [mbYes, mbNo], 0) = mrYes then ReadCommentFile(Edit10.Text) else if MessageDlg( '前回終了時のコメントファイル名' + #13 + Edit10.Text + #13 + 'を、削除しますか?', mtInformation, [mbYes, mbNo], 0) = mrYes then Edit10.Text := ''; end; end; procedure TForm4.SpeedButton1Click(Sender: TObject); // [ + ] ボタン var n, r, c : integer; edt : TEdit; begin if Sender as TSpeedButton = SpeedButton1 then edt := Edit8 else edt := Edit6; if not GB_fxFlag then begin with edt do begin n := StrToIntDef('$' + Copy(Text, 2), 0); Inc(n); Text := Copy(Text, 1, 1) + n.ToHexString(3); Font.Color := RGB($FF, $A5, $00); end; n := n - GB_DeviceStartNo; if n >= 0 then begin with StringGrid1 do begin r := n div 16 + 1; c := n mod 16 + 1; if (r < RowCount) and (c < ColCount) then begin OnClick := nil; Row := r; Col := c; OnClick := StringGrid1Click; end; end; end; end else begin with edt do begin n := OctToIntDef(Copy(Text, 2), 0); Inc(n); Text := Copy(Text, 1, 1) + IntToOct(n, 3); Font.Color := RGB($FF, $A5, $00); end; n := n - GB_DeviceStartNo; if n >= 0 then begin with StringGrid1 do begin r := n div 8 + 1; c := n mod 8 + 1; if (r < RowCount) and (c < ColCount) then begin OnClick := nil; Row := r; Col := c; OnClick := StringGrid1Click; end; end; end; end; end; procedure TForm4.SpeedButton2Click(Sender: TObject); // [ - ] ボタン var n, r, c : integer; edt : TEdit; begin if Sender as TSpeedButton = SpeedButton2 then edt := Edit8 else edt := Edit6; if not GB_fxFlag then begin with edt do begin n := StrToIntDef('$' + Copy(Text, 2), 0); Dec(n); if n < 0 then n := 0; Text := Copy(Text, 1, 1) + n.ToHexString(3); Font.Color := RGB($FF, $A5, $00); end; n := n - GB_DeviceStartNo; if n >= 0 then begin with StringGrid1 do begin r := n div 16 + 1; c := n mod 16 + 1; if (r > 0) and (c > 0) and (r < RowCount) and (c < ColCount) then begin OnClick := nil; Row := r; Col := c; OnClick := StringGrid1Click; end; end; end; end else begin with edt do begin n := OctToIntDef(Copy(Text, 2), 0); Dec(n); if n < 0 then n := 0; Text := Copy(Text, 1, 1) + IntToOct(n, 3); Font.Color := RGB($FF, $A5, $00); end; n := n - GB_DeviceStartNo; if n >= 0 then begin with StringGrid1 do begin r := n div 8 + 1; c := n mod 8 + 1; if (r > 0) and (c > 0) and (r < RowCount) and (c < ColCount) then begin OnClick := nil; Row := r; Col := c; OnClick := StringGrid1Click; end; end; end; end; end; procedure TForm4.SpeedButton5Click(Sender: TObject); begin Form1.ShowModal; end; procedure TForm4.StringGrid1Click(Sender: TObject); // 出力先 var s : string; begin with StringGrid1 do begin if not GB_fxFlag or (GB_fxFlag and (Col <= 8)) then begin s := IntToHex(StrToIntDef('$'+Cells[0, Row], 0) + StrToIntDef('$'+Cells[Col, 0], 0), 3); Edit11.Text := GetDeviceComment(Cells[0, 0] + s); with Edit8 do begin Text := 'Y' + s; Font.Color := RGB($FF, $A5, $00); end; with Edit6 do begin Text := 'X' + s; Font.Color := RGB($FF, $A5, $00); end; end; end; end; procedure TForm4.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer; Rect: TRect; State: TGridDrawState); var ARect : TRect; s : string; scale : double; flag : boolean; n : integer; begin flag := False; scale := GB_SgScale; ARect := Rect; ARect.Top := Rect.Top + 1; ARect.Bottom := Rect.Bottom - 1; ARect.Left := Rect.Left + 1; ARect.Right := Rect.Right - 1; with StringGrid1 do begin s := Cells[ACol, ARow]; if (ARow = 0) or (ACol = 0) then begin if (ARow = 0) and (ACol = 0) then begin Canvas.Brush.Color := clLime; Canvas.FillRect(Rect); Canvas.Font.Height := Trunc(20 * scale); Canvas.Font.Color := clBlack; end else begin Canvas.Brush.Color := clSilver; Canvas.FillRect(Rect); Canvas.Font.Height := Trunc(20 * scale); Canvas.Font.Color := clGray; end; DrawText(Canvas.Handle, PChar(s), Length(s), ARect, DT_CENTER); end else begin if (Edit4.Text = 'ON') or (Edit4.Text = 'OFF') then begin if not GB_fxFlag then n := StrToIntDef('$' + Copy(Edit3.Text, 2), -1) else n := OctToIntDef(Copy(Edit3.Text, 2), -1); if n >= GB_DeviceStartNo then begin n := n - GB_DeviceStartNo; if (not GB_fxFlag and (ARow = n div 16 + 1) and (ACol = n mod 16 + 1)) or (GB_fxFlag and (ARow = n div 8 + 1) and (ACol = n mod 8 + 1)) then begin flag := True; if Edit4.Text = 'ON' then begin Canvas.Brush.Color := clRed; Canvas.FillRect(ARect); Canvas.Font.Height := Trunc(20 * scale); Canvas.Font.Color := clWhite; end else begin Canvas.Brush.Color := clLime; Canvas.FillRect(ARect); Canvas.Font.Height := Trunc(20 * scale); Canvas.Font.Color := clBlack; s := Copy(Edit3.Text, 2); end; end; end; end; if not flag and (s <> '') then begin Canvas.Brush.Color := RGB($FF, $A5, $00); Canvas.FillRect(ARect); Canvas.Font.Height := Trunc(20 * scale); Canvas.Font.Color := clWhite; end; if s <> '' then DrawText(Canvas.Handle, PChar(s), Length(s), ARect, DT_CENTER); end; end; end; procedure TForm4.StringGrid2KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin SgKeyDown(Sender as TStringGrid, Key, Shift); end; function NumToSpeechText(const hex : string): string; var i : integer; s : string; begin result := ''; for i := 1 to hex.Length do begin s := Copy(hex, i, 1); if s = '0' then result := result + 'ゼロ' else if s = '1' then result := result + 'イチ' else if s = '2' then result := result + 'ニイ' else if s = '3' then result := result + 'サン' else if s = '4' then result := result + 'ヨン' else if s = '5' then result := result + 'ゴー' else if s = '6' then result := result + 'ロク' else if s = '7' then result := result + 'ナナ' else if s = '8' then result := result + 'ハチ' else if s = '9' then result := result + 'キュウ' else if s = 'A' then result := result + 'エイ' else if s = 'B' then result := result + 'ビイ' else if s = 'C' then result := result + 'シイ' else if s = 'D' then result := result + 'デー' else if s = 'E' then result := result + 'イイ' else if s = 'F' then result := result + 'エフ' else result := result + s; result := result + ' '; end; end; procedure TForm4.Timer1Timer(Sender: TObject); var i, j, devN : integer; lSize : integer; szDevice : WideString; s : string; flag : boolean; SpeechFlag : boolean; begin SpeechFlag := False; Timer1.Enabled := False; with ActUtlType1 do begin // CPU Type if Edit9.Text = '' then begin GetCpuType(szDevice, lSize); Edit9.Text := szDevice; // FX であるか flag := Pos('FX', Edit9.Text) = 1; if flag <> GB_fxFlag then begin CheckBox1.Checked := flag; CheckBox1Click(self); end; end; if GB_fxFlag then devN := 8 else devN := 16; lSize := 1; // 1 ワード(ビットデバイスでは16点) for i := 0 to 7 do begin WordAryNew[i] := 0; if not GB_fxFlag then szDevice := GB_DeviceHead + (GB_DeviceStartNo + i * 16).ToHexString else szDevice := GB_DeviceHead + IntToOct(GB_DeviceStartNo + i * 16, 4); if ReadDeviceBlock2(szDevice, lSize, WordAryNew[i]) <> 0 then begin WordAryNew[i] := 0; break; end; end; // 内部データに格納 8 x 16 = 128 個 // FX の時グリッド表示は、 8 x 8 = 64 個 for i := 0 to 7 do begin for j := 0 to 15 do BitAryNew[i * 16 + j] := WordAryNew[i] and IntPower(2, j) > 0; end; for i := 0 to 128 - 1 do begin if not GB_fxFlag then s := IntToHex(GB_DeviceStartNo + i, 3) else s := IntToOct(GB_DeviceStartNo + i, 3); if BitAryNew[i] and not BitAryOld[i] then begin Edit3.Text := GB_DeviceHead + s; Edit3.Font.Color := clRed; Edit4.Text := 'ON'; Edit4.Font.Color := clRed; StringGrid1.Repaint; Edit11.Text := GetDeviceComment(Edit3.Text); SpeechFlag := True; end else if not BitAryNew[i] and BitAryOld[i] then begin Edit3.Text := GB_DeviceHead + s; Edit3.Font.Color := clLime; Edit4.Text := 'OFF'; Edit4.Font.Color := clLime; StringGrid1.Repaint; Edit11.Text := GetDeviceComment(Edit3.Text); SpeechFlag := True; end; with StringGrid1 do begin if BitAryNew[i] then begin if Cells[i mod devN + 1, i div devN + 1] <> s then Cells[i mod devN + 1, i div devN + 1] := s ; end else Cells[i mod devN + 1, i div devN + 1] := ''; end; end; BitAryOld := BitAryNew; if Label5.Caption <> '■' then Label5.Caption := '■' else Label5.Caption := '□'; // テキストスピーチ if CheckBox2.Checked and SpeechFlag and TTSFlag then begin Application.ProcessMessages; s := Copy(Edit3.Text, 1,1) {+ #13} + NumToSpeechText(Copy(Edit3.Text, 2)); if CheckBox3.Checked then s := s + Edit11.Text + #13; if Edit4.Text = 'ON' then s := s + 'オンン' else s := s + 'オフ'; SpVoice.Speak(s, SVSFDefault); end; end; Timer1.Enabled := True; end; end. // --------------------------------------------------------------- // Android 側 // // TextToSpeech : // Androidapi.JNI.TTS, AndroidAPI.JNIBridge は、GitHub よりダウンロード // https://github.com/jimmckeeth/FireMonkey-Android-Voice/tree/master/JNIBridge // --------------------------------------------------------------- unit Unit4; interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, System.Bluetooth, System.Bluetooth.Components, FMX.ScrollBox, FMX.Memo, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Layouts, FMX.Edit, System.Rtti, FMX.Grid.Style, FMX.Grid,{ Math,} FMX.Objects, System.UIConsts, FMX.ListBox, System.IOUtils, System.IniFiles, // for TTS Androidapi.JNI.TTS,AndroidAPI.JNIBridge; type TBitAry = array [0..127] of Boolean; type TBtThread = class(TThread) private { Private 宣言 } procedure BtOpen; protected procedure Execute; override; public constructor Create; virtual; end; type TForm4 = class(TForm) ScaledLayout1: TScaledLayout; Bluetooth1: TBluetooth; Button6: TButton; Timer1: TTimer; StringGrid1: TStringGrid; StringColumn1: TStringColumn; StringColumn2: TStringColumn; StringColumn3: TStringColumn; StringColumn4: TStringColumn; StringColumn5: TStringColumn; StringColumn6: TStringColumn; StringColumn7: TStringColumn; StringColumn8: TStringColumn; StringColumn9: TStringColumn; Label1: TLabel; Label2: TLabel; Label3: TLabel; Rectangle1: TRectangle; Label4: TLabel; Label5: TLabel; Rectangle2: TRectangle; Rectangle3: TRectangle; Rectangle4: TRectangle; Rectangle5: TRectangle; ComboBox1: TComboBox; ComboBox2: TComboBox; Label7: TLabel; StringColumn10: TStringColumn; StringColumn11: TStringColumn; StringColumn12: TStringColumn; StringColumn13: TStringColumn; StringColumn14: TStringColumn; StringColumn15: TStringColumn; StringColumn16: TStringColumn; StringColumn17: TStringColumn; Rectangle6: TRectangle; Label8: TLabel; Rectangle7: TRectangle; Label9: TLabel; Rectangle8: TRectangle; Label10: TLabel; Button1: TButton; Label11: TLabel; Rectangle9: TRectangle; CheckBox1: TCheckBox; ComboBox3: TComboBox; Button2: TButton; Switch1: TSwitch; procedure Button6Click(Sender: TObject); procedure Timer1Timer(Sender: TObject); procedure FormCreate(Sender: TObject); procedure Rectangle1Click(Sender: TObject); procedure Rectangle2Click(Sender: TObject); procedure StringGrid1DrawColumnCell(Sender: TObject; const Canvas: TCanvas; const Column: TColumn; const Bounds: TRectF; const Row: Integer; const Value: TValue; const State: TGridDrawStates); procedure StringGrid1DrawColumnHeader(Sender: TObject; const Canvas: TCanvas; const Column: TColumn; const Bounds: TRectF); procedure ComboBox1Change(Sender: TObject); procedure StringGrid1CellClick(const Column: TColumn; const Row: Integer); procedure CheckBox1Change(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure Button2Click(Sender: TObject); // TTS type TttsOnInitListener = class(TJavaLocal, JTextToSpeech_OnInitListener) private [weak] FParent : TForm4; public constructor Create(AParent : TForm4); procedure onInit(status: Integer); cdecl; end; private { private 宣言 } ttsListener : TttsOnInitListener; tts : JTextToSpeech; procedure SpeakOut(const s :string); procedure InitTTS; public { public 宣言 } BitAryOld : TBitAry; BitAryNew : TBitAry; GB_DeviceName : string; GB_DeviceStartIndex : integer; GB_fxFlag : boolean; constructor Create(AOwner : TComponent); override; destructor Destroy; override; end; var Form4: TForm4; ADevice : TBluetoothDevice; ASocket : TBluetoothSocket; GThdMode : integer; GCmdMode : integer; ThBt : TBtThread; OpenNGcnt : integer; OpenMsecCnt : integer; Counter : integer; BtDeviceHead : string; const // SPP(Serial Port Profile) による通信のUUID ServiceUUID = '{00001101-0000-1000-8000-00805F9B34FB}'; thdTHSTART = 1000; thdTHTERM = 2000; cmdSCCREATE = 200; cmdSCCONNECT = 201; cmdSCNG = 202; implementation uses Androidapi.JNI.JavaTypes, FMX.Helpers.Android {$IF CompilerVersion >= 27.0} , Androidapi.Helpers {$ENDIF} ; {$R *.fmx} // n の k 乗 (Math ユニット不要) function IntPower(n, k : integer):integer; var i : integer; begin result := 1; for i := 1 to k do result := result * n; end; // 8 進数 -> 10進数 function OctToIntDef(const Value: string; Def :integer): integer; var i, len, n : integer; begin result := 0; len := Length(Value); for i := 1 to len do begin n := StrToIntDef(Copy(Value, i, 1), -1); if (n >= 0 ) and (n < 8) then Inc(result, n * IntPower(8, len - i)) else begin result := Def; break; end; end; end; // 10 進数 -> 8 進数 function IntToOct(Value: integer; digits: Integer): string; var rest: Longint; oct: string; i: Integer; begin oct := ''; while Value <> 0 do begin rest := Value mod 8; Value := Value div 8; oct := IntToStr(rest) + oct; end; if Length(oct) < digits then for i := Length(oct) + 1 to digits do oct := '0' + oct; result := oct; end; // ----------------------------------------------------------------------------- // Bluetooth を Open し、接続する procedure TBtThread.BtOpen; var ABluetoothManager : TBluetoothManager; APairedDevices : TBluetoothDeviceList; ADevice : TBluetoothDevice; idx, i : integer; begin GThdMODE := thdTHSTART; try try ABluetoothManager := TBluetoothManager.Current; if ABluetoothManager.ConnectionState = TBluetoothConnectionState.Connected then begin // PC名 //Synchronize(procedure() begin // Form4.Label6.Text := // '[' + ABluetoothManager.CurrentAdapter.AdapterName + ']' //end); // 過去にペアリングされたデバイスの一覧から、ターゲット を探す APairedDevices := ABluetoothManager.GetPairedDevices; if APairedDevices.Count > 0 then begin idx := -1; for i := 0 to APairedDevices.Count -1 do begin Synchronize(procedure() begin with Form4.ComboBox3 do begin BeginUpdate; Items.Add(APairedDevices[i].DeviceName ); EndUpdate; end; end); if (BTDeviceHead = APairedDevices[i].DeviceName) then begin Synchronize(procedure() begin with Form4.ComboBox3 do begin ItemIndex := i; end; end); idx := i; break; end; end; if idx >= 0 then begin ADevice := APairedDevices[idx]; if ADevice <> nil then begin ASocket := ADevice.CreateClientSocket(StringToGUID(ServiceUUID), False); if ASocket <> nil then begin GCMDMODE := cmdSCCREATE; // 接続 ASocket.Connect; if ASocket.Connected then GCMDMODE := cmdSCCONNECT; end; end; end; end; end; except on E : Exception do begin GCMDMODE := cmdSCNG; end; end; finally // 明示的にスレッドを終了(破棄される) // スレッド実行中にアプリを終了した時エラーになるため Terminate; WaitFor; FreeAndNil(ThBt); GThdMODE := thdTHTERM; end; end; constructor TBtThread.Create; begin // スレッドを生成、直ちに実行 inherited Create(False); // スレッド終了時、スレッドオブジェクトを破棄 FreeOnTerminate := True; end; procedure TBtThread.Execute; begin BtOpen; end; // ----------------------------------------------------------------------------- procedure TForm4.InitTTS; begin tts := TJTextToSpeech.JavaClass.init(TAndroidHelper.Context, ttsListener); end; procedure TForm4.SpeakOut(const s : string); var text : JString; begin text := StringToJString(s); tts.speak(text, TJTextToSpeech.JavaClass.QUEUE_FLUSH, nil); end; { TForm4.TttsOnInitListener } constructor TForm4.TttsOnInitListener.Create(AParent: TForm4); begin inherited Create; FParent := AParent end; procedure TForm4.TttsOnInitListener.onInit(status: Integer); var Result : Integer; begin if (status = TJTextToSpeech.JavaClass.SUCCESS) then begin //result := FParent.tts.setLanguage(TJLocale.JavaClass.US); result := FParent.tts.setLanguage(TJLocale.JavaClass.JAPAN); if (result = TJTextToSpeech.JavaClass.LANG_MISSING_DATA) or (result = TJTextToSpeech.JavaClass.LANG_NOT_SUPPORTED) then ShowMessage('This Language is not supported'); end else ShowMessage('Initilization Failed!'); end; constructor TForm4.Create(AOwner: TComponent); begin inherited; ttsListener := TttsOnInitListener.Create(self); end; destructor TForm4.Destroy; begin if Assigned(tts) then begin tts.stop; tts.shutdown; tts := nil; end; end; // ----------------------------------------------------------------------------- function ASocketReceiveData(ASocket: TBluetoothSocket; ATimeout: Cardinal): string; var AData : TBytes; ReadData : TBytes; i : integer; res : string; Ticks : Cardinal; idx : integer; loop : boolean; cnt : integer; begin res := ''; cnt := 0; SetLength(ReadData, 1024); idx := 0; Ticks := TThread.GetTickCount; loop := True; while loop and (cnt < 500) do begin Sleep(1); AData := ASocket.ReceiveData; if Length(AData) > 0 then begin for i := 0 to Length(AData) - 1 do begin ReadData[idx] := AData[i]; Inc(idx); if (AData[i] = Ord(#10)) or (idx >= 1024) then begin loop := False; break; end; end; end; Inc(cnt); if loop then loop := TThread.GetTickCount - Ticks < ATimeout; end; SetLength(ReadData, idx); res := TEncoding.ANSI.GetString(ReadData); result := Trim(res); // 制御コードを含まない end; procedure TForm4.Button2Click(Sender: TObject); // 接続先保存 var IniFile: TMemIniFile; begin IniFile := TMemIniFile.Create(System.IOUtils.TPath.Combine( System.IOUtils.TPath.GetDocumentsPath, 'MXC4_IO.ini'), TEncoding.UTF8); with IniFile do begin try with ComboBox3 do begin if ItemIndex >= 0 then begin WriteString('Target', 'PCName', Items[ItemIndex]); ShowMessage('接続先: ' + Items[ItemIndex] + 'を保存しました.' + #13#10 + '次回起動時から有効になります.' + #13#10 + 'このアプリを再起動して下さい.'); end else ShowMessage('接続先が選択されていません.'); end; IniFile.UpdateFile; finally Free; end; end; end; procedure TForm4.Button6Click(Sender: TObject); // デバイスの値をセット var AData : TBytes; res : string; ATimeout: Cardinal; lbl : TLabel; begin if Sender as TButton = Button6 then lbl := Label3 else lbl := Label8; if (ASocket <> nil) and ASocket.Connected then begin Timer1.Enabled := False; ATimeout := 250; AData := TEncoding.ANSI.GetBytes('WR ' + lbl.Text + ' 1' + #13#10); // 送信 ASocket.SendData(AData); // 受信 res := ASocketReceiveData(ASocket, ATimeout); with lbl.TextSettings do begin if res = 'ON' then FontColor := TAlphaColorRec.Red else if res = 'OFF' then FontColor := TAlphaColorRec.Lime else FontColor := TAlphaColorRec.White; end; Timer1.Enabled := True; end; end; procedure TForm4.CheckBox1Change(Sender: TObject); var i :integer; begin GB_fxFlag := CheckBox1.IsChecked; // 初期に戻す with StringGrid1 do begin if not GB_fxFlag then for i := 0 to 7 do Cells[0, i] := (i * 16).ToHexString(3) else for i := 0 to 7 do Cells[0, i] := IntToOct(i * 8, 3); Row := 0; Col := 1; end; with ComboBox2 do begin BeginUpdate; Items.Clear; if not GB_fxFlag then for i := 0 to 255 do Items.Add(IntToHex(i * 32, 3)) else for i := 0 to 255 do Items.Add(IntToOct(i * 16, 3)); EndUpdate; ItemIndex := 0; end; // X に戻す with ComboBox1 do begin ItemIndex := 0; end; Label8.Text := 'X000'; Label3.Text := 'Y000'; end; procedure TForm4.ComboBox1Change(Sender: TObject); var AData : TBytes; s2, s1, res : string; ATimeout: Cardinal; i : integer; begin // ここでは、StringGrid のデバイス番号を変更しない // PC 側へ先頭アドレスを送信するだけ if (ASocket <> nil) and ASocket.Connected then begin Timer1.Enabled := False; // 初期化 Label1.Text := ''; Label2.Text := ''; for i := 0 to 127 do BitAryNew[i] := False; BitAryOld := BitAryNew; // PC の値を変更 ATimeout := 250; // デバイス名 with ComboBox1 do begin if ItemIndex < 0 then ItemIndex := 0; s1 := ListBox.Items[ItemIndex]; end; with ComboBox2 do begin if ItemIndex < 0 then ItemIndex := 0; if ItemIndex < 0 then s2 := '000' else begin if not GB_fxFlag then s2 := IntToHex(ItemIndex * 32, 4) else s2 := IntToHex(ItemIndex * 16, 4); end; end; AData := TEncoding.ANSI.GetBytes('CF ' + s1 + ' ' + s2 + #13#10); // 送信 ASocket.SendData(AData); res := ASocketReceiveData(ASocket, ATimeout); Rectangle4.Fill.Color := TAlphaColorRec.Black; Rectangle5.Fill.Color := TAlphaColorRec.Black; Timer1.Enabled := True; end; end; procedure TForm4.FormCreate(Sender: TObject); var IniFile: TMemIniFile; // uses .... System.IniFiles; begin GB_DeviceName := 'X'; GB_DeviceStartIndex := 0; GB_fxFlag := True; StringColumn1.Header := 'X'; StringColumn2.Header := '0'; StringColumn3.Header := '1'; StringColumn4.Header := '2'; StringColumn5.Header := '3'; StringColumn6.Header := '4'; StringColumn7.Header := '5'; StringColumn8.Header := '6'; StringColumn9.Header := '7'; StringColumn10.Header := '8'; StringColumn11.Header := '9'; StringColumn12.Header := 'A'; StringColumn13.Header := 'B'; StringColumn14.Header := 'C'; StringColumn15.Header := 'D'; StringColumn16.Header := 'E'; StringColumn17.Header := 'F'; // 縦画面に固定 Application.FormFactor.Orientations := [TFormOrientation.Portrait, TFormOrientation.InvertedPortrait]; // use ..... System.IOUtils; IniFile := TMemIniFile.Create(System.IOUtils.TPath.Combine( System.IOUtils.TPath.GetDocumentsPath, 'MXC4_IO.ini'), TEncoding.UTF8); with IniFile do begin try BtDeviceHead := ReadString('Target', 'PCName', ''); finally Free; end; end; // TTS InitTTS; // Bruetooth スレッド Timer1.Interval := 10; Timer1.Enabled := True; ThBt := TBtThread.Create; // FX モード で起動 CheckBox1.IsChecked := True; CheckBox1Change(self); end; procedure TForm4.FormDestroy(Sender: TObject); begin if ASocket <> nil then begin ASocket.Close; ASocket.Free; ASocket := nil; end; end; procedure TForm4.Rectangle1Click(Sender: TObject); // [ + ] var n : integer; lbl : TLabel; begin if Sender as TRectangle = Rectangle1 then lbl := Label3 else lbl := Label8; if not GB_fxFlag then begin n := StrToIntDef('$' + Copy(lbl.Text, 2), 0); Inc(n); with lbl do begin Text := Copy(Text, 1, 1) + n.ToHexString(3); TextSettings.FontColor := TAlphaColorRec.Orange; end; n := n - GB_DeviceStartIndex; if n >= 0 then begin with StringGrid1 do begin OnCellClick := nil; Row := n div 16; Col := n mod 16 + 1; OnCellClick := StringGrid1CellClick; SetFocus; end; end; end else begin n := OctToIntDef(Copy(lbl.Text, 2), 0); Inc(n); with lbl do begin Text := Copy(Text, 1, 1) + IntToOct(n, 3); TextSettings.FontColor := TAlphaColorRec.Orange; end; n := n - GB_DeviceStartIndex; if n >= 0 then begin with StringGrid1 do begin OnCellClick := nil; Row := n div 8; Col := n mod 8 + 1; OnCellClick := StringGrid1CellClick; SetFocus; end; end; end; end; procedure TForm4.Rectangle2Click(Sender: TObject); // [ - ] var n : integer; lbl : TLabel; begin if Sender as TRectangle = Rectangle2 then lbl := Label3 else lbl := Label8; if not GB_fxFlag then begin n := StrToIntDef('$' + Copy(lbl.Text, 2), 0); Dec(n); if n < 0 then n := 0; with lbl do begin Text := Copy(Text, 1, 1) + n.ToHexString(3); TextSettings.FontColor := TAlphaColorRec.Orange; end; n := n - GB_DeviceStartIndex; if n >= 0 then begin with StringGrid1 do begin OnCellClick := nil; Row := n div 16; Col := n mod 16 + 1; OnCellClick := StringGrid1CellClick; SetFocus; end; end; end else begin n := OctToIntDef(Copy(lbl.Text, 2), 0); Dec(n); if n < 0 then n := 0; with lbl do begin Text := Copy(Text, 1, 1) + IntToOct(n, 3); TextSettings.FontColor := TAlphaColorRec.Orange; end; n := n - GB_DeviceStartIndex; if n >= 0 then begin with StringGrid1 do begin OnCellClick := nil; Row := n div 8; Col := n mod 8 + 1; OnCellClick := StringGrid1CellClick; SetFocus; end; end; end; end; procedure TForm4.StringGrid1CellClick(const Column: TColumn; const Row: Integer); var n : integer; begin // 出力反転の対象 if not GB_fxFlag or (GB_fxFlag and (Column.Index <= 8)) then begin n := StrToIntDef('$' + StringGrid1.Cells[0, Row], 0) + StrToIntDef('$' + Column.Header, 0); with Label3 do begin Text := 'Y'+ n.ToHexString(3); TextSettings.FontColor := TAlphaColorRec.Orange; end; with Label8 do begin Text := 'X'+ n.ToHexString(3); TextSettings.FontColor := TAlphaColorRec.Orange; end; end; end; procedure TForm4.StringGrid1DrawColumnCell(Sender: TObject; const Canvas: TCanvas; const Column: TColumn; const Bounds: TRectF; const Row: Integer; const Value: TValue; const State: TGridDrawStates); // AlphaColor uses ... System.UIConsts; var s : string; n : integer; flag : boolean; begin if not Value.IsEmpty then s := Value.ToString else s := ''; with Canvas do begin if Column.Index = 0 then begin if s <> '' then begin Fill.Color := claSilver;//claAqua;//claSilver;//Yellow; FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round ); Fill.Color := claBlack; Font.Size := 15; FillText(Bounds, s, False, 1.0, [], TTextAlign.Center); end; end else begin flag := False; if (Label2.Text = 'OFF') or (Label2.Text = 'ON') then begin if not GB_fxFlag then n := StrToIntDef('$' + Copy(Label1.Text, 2), -1) else n := OctToIntDef(Copy(Label1.Text, 2), -1); if (n >= GB_DeviceStartIndex) then begin n := n - GB_DeviceStartIndex; if (not GB_fxFlag and (Row = n div 16) and (Column.Index = n mod 16 + 1)) or (GB_fxFlag and (Row = n div 8) and (Column.Index = n mod 8 + 1)) then begin if Label2.Text = 'OFF' then begin Fill.Color := claGray;//Black; FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round ); Fill.Color := claLime; end; if Label2.Text = 'ON' then begin Fill.Color := claRed; FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round ); Fill.Color := claWhite; end; if not GB_fxFlag then s := IntToHex(n mod 16, 1) else s := IntToHex(n mod 8, 1); Font.Size := 16; FillText(Bounds, s, False, 1.0, [], TTextAlign.Center); flag := true; end; end; end; if not flag and (s <> '') then begin Fill.Color := claOrange;//Red; FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round ); Fill.Color := claWhite; Font.Size := 16; FillText(Bounds, s, False, 1.0, [], TTextAlign.Center); end; end; end; end; procedure TForm4.StringGrid1DrawColumnHeader(Sender: TObject; const Canvas: TCanvas; const Column: TColumn; const Bounds: TRectF); var s: string; begin s := Column.Header; if s <> '' then begin with Canvas do begin if Column.Index = 0 then begin Fill.Color := claLime; FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round ); Fill.Color := claBlack; Font.Size := 18; FillText(Bounds, s, False, 1.0, [], TTextAlign.Center); end else begin Fill.Color := claSilver; FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round ); Fill.Color := claBlack; Font.Size := 15; FillText(Bounds, s, False, 1.0, [], TTextAlign.Center); end; end; end; end; function NumToSpeechText(const hex : string): string; var i : integer; s : string; begin result := ''; for i := 1 to hex.Length do begin s := Copy(hex, i, 1); if s = '0' then result := result + 'ゼロ' else if s = '1' then result := result + 'イチ' else if s = '2' then result := result + 'ニイ' else if s = '3' then result := result + 'サン' else if s = '4' then result := result + 'ヨン' else if s = '5' then result := result + 'ゴー' else if s = '6' then result := result + 'ロク' else if s = '7' then result := result + 'ナナ' else if s = '8' then result := result + 'ハチ' else if s = '9' then result := result + 'キュウ' else if s = 'A' then result := result + 'エイ' else if s = 'B' then result := result + 'ビイ' else if s = 'C' then result := result + 'シイ' else if s = 'D' then result := result + 'デー' else if s = 'E' then result := result + 'イイ' else if s = 'F' then result := result + 'エフ' else result := result + s; result := result + ' '; end; end; procedure TForm4.Timer1Timer(Sender: TObject); var ATimeout : Cardinal; AData : TBytes; res : string; i : integer; Ticks : Cardinal; j : integer; s, s1 : string; n, idx : integer; flag : boolean; fxFlag : boolean; devN, devM : integer; ttsFlag : boolean; begin ttsFlag := False; if not ((GCMDMODE = cmdSCCONNECT) and ASocket.Connected) then begin Inc(OpenMsecCnt); CheckBox1.Text := IntToStr(OpenMsecCnt * 10) + 'msec'; if GCMDMODE = cmdSCNG then begin Inc(OpenNgCnt); if OpenNgCnt > 4 then begin Timer1.Enabled := False; ShowMessage(BTDeviceHead + ' に、接続できません.'); end; end; if OpenMsecCnt > 100 then begin Timer1.Enabled := False; ShowMessage('接続先が無効です.'); end; end; if (GCMDMODE = cmdSCCONNECT) and ASocket.Connected then begin Timer1.Interval := 250; flag := True; Timer1.Enabled := False; try Ticks := TThread.GetTickCount; ATimeout := 250; // 初回は CPU TYPE 取得のみ if Label7.Text = '' then begin AData := TEncoding.ANSI.GetBytes('CPU' + #13#10); // 送信 ASocket.SendData(AData); // 受信 res := ASocketReceiveData(ASocket, ATimeout); Label7.Text := res; flag := res <> ''; fxFlag := (Pos('FX', Label7.Text) = 1) or (res = ''); if GB_fxFlag <> fxFlag then begin CheckBox1.IsChecked := fxFlag; CheckBox1Change(self); // GB_fxFlag := fxFlag; // CheckBox1Chenge イベントに含まれる end; end else begin if GB_fxFlag then begin devN := 8; devM := 64; end else begin devN := 16; devM := 128; end; if Flag then begin // デバイス一括読み出しコマンド AData := TEncoding.ANSI.GetBytes('RD' + #13#10); // 送信 ASocket.SendData(AData); // 受信 res := ASocketReceiveData(ASocket, ATimeout); flag := res <> ''; // データ格納 if res.Length >= 64 then begin for i := 0 to 7 do begin s := Copy(res, i * 4 + 1, 4); n := StrToIntDef('$' + s, 0); for j := 0 to 15 do BitAryNew[i * 16 + j] := n and IntPower(2, j) > 0; s := Copy(res, i * 4 + 1 + 32, 4); n := StrToIntDef('$' + s, 0); for j := 0 to 15 do BitAryOld[i * 16 + j] := n and IntPower(2, j) > 0; end; s := Copy(res, 66); // スペース1個ある if s <> '' then begin n := Pos(' ', s); if n > 0 then begin // デバイス番号 s1 := Copy(s, 1, n - 1); s := Copy(s, n + 1); n := Pos(' ', s); if n = 0 then begin idx := StrToIntDef('$' + s, 0); Label11.Text := ''; // コメント end else begin // 先頭デバイス番号(PC からの応答は 16 進表記) idx := StrToIntDef('$' + Copy(s, 1, n - 1), 0); // コメント Label11.Text := Copy(s, n + 1); end; if (GB_DeviceName <> s1) or (GB_DeviceStartIndex <> idx) then begin GB_DeviceName := s1; GB_DeviceStartIndex := idx; // イベント無効 (PC へ送り返すため) ComboBox1.OnChange := nil; ComboBox2.OnChange := nil; with ComboBox1 do begin if GB_DeviceName = 'X' then ItemIndex := 0 else ItemIndex := 1; end; // 先頭デバイス番号 with ComboBox2 do begin if Items.Count > 0 then begin if not GB_fxFlag then ItemIndex := GB_DeviceStartIndex div 32 else ItemIndex := GB_DeviceStartIndex div 16; end; end; // イベントを戻す ComboBox1.OnChange := ComboBox1Change; ComboBox2.OnChange := ComboBox1Change; // X or Y StringColumn1.Header := GB_DeviceName; // アドレス番号を変える with StringGrid1 do begin if not GB_fxFlag then begin for i := 0 to 7 do Cells[0, i] := (GB_DeviceStartIndex + i * 16).ToHexString(3); end else begin for i := 0 to 7 do Cells[0, i] := IntToOct(GB_DeviceStartIndex + i * 8, 3); end; Row := 0; Col := 1; end; // デバイス ON/OFF の表示を初期化 Label1.Text := ''; Label2.Text := ''; Rectangle4.Fill.Color := TAlphaColorRec.Black; Rectangle5.Fill.Color := TAlphaColorRec.Black; // 反転デバイス番号を更新 if not GB_fxFlag then begin Label8.Text := 'X' + IntToHex(GB_DeviceStartIndex, 3); Label3.Text := 'Y' + IntToHex(GB_DeviceStartIndex, 3); end else begin Label8.Text := 'X' + IntToOct(GB_DeviceStartIndex, 3); Label3.Text := 'Y' + IntToOct(GB_DeviceStartIndex, 3); end; end; end; end; end; end; // 表示 with StringGrid1 do begin for i := 0 to devM -1 do begin if BitAryNew[i] then begin s := (i mod devN).ToHexString(1); if Cells[i mod devN + 1, i div devN] <> s then Cells[i mod devN + 1, i div devN] := s ; end else begin if Cells[i mod devN + 1, i div devN] <> '' then Cells[i mod devN + 1, i div devN] := ''; end; end; end; // 比較 // 内部データ数 = 128, FX は先頭 64 データのみ表示される for i := 0 to 128 -1 do begin idx := i + GB_DeviceStartIndex; if BitAryNew[i] and not BitAryOld[i] then begin Rectangle4.Fill.Color := TAlphaColorRec.Red; with Label1 do begin if not GB_fxFlag then Text := GB_DeviceName + idx.ToHexstring(3) else Text := GB_DeviceName + IntToOct(idx, 3); TextSettings.FontColor := TAlphaColorRec.White; end; Rectangle5.Fill.Color := TAlphaColorRec.Red; with Label2 do begin Text := 'ON'; TextSettings.FontColor := TAlphaColorRec.White; end; ttsFlag := True; end else if not BitAryNew[i] and BitAryOld[i] then begin Rectangle4.Fill.Color := TAlphaColorRec.Black; with Label1 do begin if not GB_fxFlag then Text := GB_DeviceName + idx.ToHexstring(3) else Text := GB_DeviceName + IntToOct(idx, 3); TextSettings.FontColor := TAlphaColorRec.Lime; end; Rectangle5.Fill.Color := TAlphaColorRec.Black; with Label2 do begin Text := 'OFF'; TextSettings.FontColor := TAlphaColorRec.Lime; end; ttsFlag := True; end; if ttsFlag then begin s :=Copy(Label1.Text, 1, 1) + #13 + NumToSpeechText(Copy(Label1.Text, 2)); if Switch1.IsChecked then s := s + '。' + Label11.Text; if Label2.Text = 'ON' then s := s + '。' + 'オン' else s := s + '。' + 'オフ'; SpeakOut(s); end; end; end; if flag then CheckBox1.Text := (TThread.GetTickCount - Ticks).ToString else CheckBox1.Text := 'PC 接続失敗'; if flag then Timer1.Enabled := True; except CheckBox1.Text := 'PC 応答なし'; Timer1.Enabled := True; end; end; end; end.