RS232Cの制御電文におけるBCC(Block Checking Code)算出について

解決


 2022-10-25 14:28:23  No: 150617  IP: 192.*.*.*

Delphiアプリケーションにクレジット決済機能を付けるために、決済端末を借用し開発を行うところですが、BCC(Block Checking Code)を求めなければならず、コードを書いてみたのですが上手くいかず悩んでおります。
教えて頂けませんでしょうか。
<BCCの仕様>
<STX>より後ろから<ETX>または<ETB>までを1バイトごとにEX-OR(排他的論理和)をとった値

<求めるBCC>
コマンド電文例①<STX>10110110000001001<ETX> この場合は、2
コマンド電文例②<STX>2011010000200000<ETX> この場合は、<02H>

<書いてみたコード>
const
  STX = #$02;
  ETX = #$03;
  EOT = #$04;
  ENQ = #$05;
  ACK = #$06;
  NAK = #$07;
function BCC(str: String): String
var
  i, x: Integer;
begin
  x := 0;
  for i := 1 to Length(str) do
    x := x xor Ord(str[i]);   // 最終的なxの値は51と出ました
  Result := Char(x);
end;

function TForm1.Button1Click(Sender: Object);
var
  bccTarget: String;
  sendStr: Striing;
begin
  bccTarget := Edit1.Text + ETX;
  sendStr := STX + Edit1.Text + BCC(bccTarget);
  ShowMessage(sendStr);
}

<教えて頂きたいこと>
ES232Cの電文を作成するのが初めてなので、何かコードが間違っていましたら是非教えて頂けませんでしょうか(m_m)

編集 削除
take  2022-10-25 23:19:41  No: 150618  IP: 192.*.*.*

BCCを手動で計算しました。
BCCは'2'で合っているように思います。

まず数値の表記揺れを統一したほうが理解しやすいですね。

「2」と書かれても それが何を示す2なのかわかりません。
多分文字列の2なのでしょう

>コードが間違っていましたら
不安要素としては string型と char 型が使われていることかな

STXやETXのコードを拡張コードとして認識されてうまく処理が出来ない可能性もあるので
この場合は AnsiString型 AnsiChar型を使った方が良いですね。

1   :0011 0001
0   :0011 0000
1   :0011 0001
1   :0011 0001
0   :0011 0000
1   :0011 0001
1   :0011 0001
0   :0011 0000
0   :0011 0000
0   :0011 0000
0   :0011 0000
0   :0011 0000
0   :0011 0000
1   :0011 0001
0   :0011 0000
0   :0011 0000
1   :0011 0001
ETX :0000 0011

XOR  0011 0010

32H = 50 = '2'

編集 削除
 2022-10-26 08:41:07  No: 150629  IP: 192.*.*.*

takeさん、ありがとうございます。
大変お手数をお掛けしました。わたしの勘違いですね。結果は正しそうですね。
RS232C接続している端末が反応しなくてBCCの計算が間違っているものと思い込んでおりました。
うまく反応しなかったのは、最後に余計な<EOT>を入れてしまった結果でした。
本当に申し訳ございませんでした。

編集 削除
 2022-10-26 14:04:00  No: 150632  IP: 192.*.*.*

度々申し訳ございません。
ただ、上で書きましたのはBCCを算出するところまでで、RS232Cテストツールで成功は致しました。
これをDelphi2007で実際の通信を行わなければなりません。通信にはTCommというコンポーネントがあるので、それを使ってみて欲しいと言われてはいますが、使い方が今一つ分からず、次の様にコードを書いてみました。
unit Unit1;

interface
  uses
    ::::::::::::::, Comm;

Type
  TFrom1 = class(TForm)
  ::::::
  CommTerm: TComm;
  ::::::

implementation
   
proecedure TForm1.SendButtonClick(Sender: TObject);
begin
  try
    // 接続
    CommTerm.Open;
    // 電文送信
    CommTerm.SendString(SendStr);  // SendStrは、<STX>+送るデータ+<ETX>+BCC
    
  except
    on Exception do
    begin
        ShowMessage('ERROR');
    end;
  end;
end;

という感じですが、SendStringメソッドで例外が発生してしまいます。
ECommReadWriteError(入出力エラーErrorCode=1997865560)が発生してしまいます。

<もしご存知でしたらご教授願いたいこと>
1)このTCommコンポーネントは有名なものでしょうか?
2)この例外はコードの記述の仕方が原因でしょうか?

編集 削除
take  2022-10-26 23:25:37  No: 150634  IP: 192.*.*.*

DEKOさんの情報によると
https://ht-deko.com/ft1302.html

TComm は大野さんが作った16bit版を32bit版に改変したもの
ほかには エックスさんが作ったTCommXというのがあり
元々32bit版でスレッドによる送受信が出来て最終版はWindowsXPでも安定した動作が可能、Windows10でも問題なし

コンポーネントを使ってみたということですが、
1.コンポーネントをDelphiにインストールして、Edit1など他のコンポーネントのようにフォームに配置した
2.プロジェクトファイルのフォルダにComm.pasをコピーして uses に Commを追加
  フォームのprivate宣言にCommTerm: TComm;として追加した

(1)の方法だとコンポーネントが正しく設定されていないと思われます。

Commのポート番号は何番か?存在しているポート番号か?
今のPCってシリアルポートがないので
USB変換だと思いますが
設定からデバイスマネージャーを開いて該当する変換器やポートから
シリアルポートが何番なのかを調べて下さい。

(2)の方法だとクラスが生成されていないのでエラーが出たと思われます。

フォームのOnCreateイベントで CommTerm := TComm.Create;として生成
OnDestroyイベントで CommTerm.Free;として破棄

TCommXを紹介しましたが、今現在は配布サイトがないかもしれませんね。
最終バージョンは CommX121.lzhです。
開発関係者の1人として再配布してあげたいとろころですが・・・

編集 削除
HFUKUSHI  2022-10-27 00:24:56  No: 150635  IP: 192.*.*.*

内容が異なる質問は別に質問を作成したほうがいいと思います(ここは解決済になってますし)。

それはともかく、Comm.Openをエラーなく通過しているということはコンポーネントのインスタンスは正しく生成されていて、ポート番号などの設定もあっているような気がします。
Comm.SendStringがエラーになるのはWriteFileが失敗してるからですが、試しに
CommTerm.SendString('A'); 
とか他のデータを送信しても同じエラーになるでしょうか?

編集 削除
 2022-10-27 00:52:42  No: 150636  IP: 192.*.*.*

takeさん
何度も回答頂きまして本当にありがとうございます。
今回はパッケージ開発環境(Delphi2007)を利用しておりまして、デスクトップPCになります。
PCとクレカ決済端末とはRS232Cのストレート?と思われるケーブルで接続されています。
既に、TCommコンポーネントが開発環境にインストールされていまして、それをフォームに貼り付けてプロパティは次の通り設定しています。
<プロパティ>
BaudRate:4800 // クレカ決済端末と合わせています、デバイスマネージャーの通信ポート設定も同じ値です
BaudRateUserDefined:4800
ByteSize:cbs8 // デバイスマネージャーの通信ポート設定でデータビットに8を選択しています
DsrState:True // すみません用途を理解していませんが、デフォルト値のままです
EventMask:[] // 全てFalseです(デフォルト値です)
FlowControls:[cfcRtsCts] // こちらも用途を理解していないのですがデフォルトでcfcRtsCtsのみがTrueだったかと
InQueueSize:4096 // こちらも用途が不明でしたのでデフォルト値です
Name:CommTerm
OutQueueSize:4096 // こちらもデフォルト値です
ParityBits:cpbNone // デバイスマネージャーの通信ポート設定でパリティ:なしを選択していますので、こちらにしました
Port:1 // COMポートは1つしか存在しませんのと、デバイスマネージャーの通信ポート設定でもCOM1となっております
ReceiveNotifySize:3072 // こちらもデフォルト値です
SendNotifySize:1024 // こちらもデフォルト値です
StopBits:1 // デバイスマネージャーの通信ポート設定でストップビットを1を選択しています
Tag:0 // ここでは関係しないと思いますが

尚、RS232C接続ツールでは、同じ電文を送信して<ACK>(※肯定応答)が受信され、その後に<EOT>を送信して電文が受信できております。
まだまだ勉強不足で大変申し訳ございませんが、何かヒントがございましたら何卒ご教授のほどよろしくお願い致します。

編集 削除
 2022-10-27 00:59:17  No: 150637  IP: 192.*.*.*

HFUKUSHIさん
すみません、解決済にチェックを入れておきながら、追記してしまいました。
それでも見て頂きまして本当にありがとうございます。
試しに、CommTerm.SendString('A'); を実行してみました。
ErrorCode=1242504というエラーコードで、エラーになりました。

編集 削除
take  2022-10-27 01:55:11  No: 150638  IP: 192.*.*.*

RS-232C接続ツールでポートを開いているときに実行したということはないですか?
シリアルポートはポートをプログラムが占有しますのでアプリは1つしか通信出来ません。

編集 削除
 2022-10-27 02:01:16  No: 150639  IP: 192.*.*.*

takeさん、返信ありがとうございます。
RS-232C接続ツールは接続を切断またはアプリを落とした上でDelphiアプリケーションを実行しております。

編集 削除
take  2022-10-27 02:58:15  No: 150641  IP: 192.*.*.*

違いましたか

TCommがおかれているサイトがあったので抜粋してみたのですが特におかしいところがありません
https://basicc.exblog.jp/i11/

procedure TComm.SendString(S: string);
begin
  Write(PChar(S), Length(S));
end;

procedure TComm.Write(Buffer: PChar; Size: Integer);
var
  dwError: DWord;
  Stat: TComStat;
  dwBytesWritten: DWord;
begin
  if FHandle = INVALID_HANDLE_VALUE then
  raise ECommError.Create('通信が開始されていません。');
  
  if FOutQueueSize < Size then
  raise ECommError.Create('送信データ長が長すぎます。');

  repeat
    ClearCommError(FHandle, dwError, @Stat);
  until (FOutQueueSize - Stat.cbOutQue) >= Word(Size);

  if FWinVersion <> VER_PLATFORM_WIN32_NT then begin
    if (cfcRtsCts in FFlowControls) and (FRtsControls = crcToggle) then begin
      while not IOCheck do ;
      if RtsSendHi then begin
        EscapeCommFunction(FHandle, SETRTS);
      end else begin
        EscapeCommFunction(FHandle, CLRRTS);
      end;
    end;
  end;

  if not WriteFile(FHandle, Buffer^, Size, dwBytesWritten, @FWriteOs) then begin
    if FWinVersion <> VER_PLATFORM_WIN32_NT then begin
      if (cfcRtsCts in FFlowControls) and (FRtsControls = crcToggle) then begin
        while not IOCheck do ;
        if RtsSendHi then begin
          EscapeCommFunction(FHandle, CLRRTS);
        end else begin
          EscapeCommFunction(FHandle, SETRTS);
        end;
      end else begin
        EscapeCommFunction(FHandle, SETRTS);
      end;
    end;
    if GetLastError = ERROR_IO_PENDING then begin
      while not GetOverlappedResult(FHandle, FWriteOs, dwBytesWritten, True) do begin
        if GetLastError = ERROR_IO_INCOMPLETE then Continue
        else
        begin
          ClearCommError(FHandle, dwError, @Stat);
          Break;
        end;
      end;
    end
    else begin
      ClearCommError(FHandle, dwError, @Stat);
      raise ECommReadWriteError.Create(dwError);
    end;
  end;
end;

編集 削除
 2022-10-27 04:27:20  No: 150642  IP: 192.*.*.*

takeさん
本当にありがとうございます。
わたしもTCommコンポーネントの挙動を確認していました。
Openすると、直ぐに抜けてしまうので(最初のif文:if FHandle < 0 then)、CreateFileが実行されないのが原因なのでしょうかね。
※FHandleの値の初期値は0です

尚、Writeメソッドの内容もこちらで改ざんされてしまっている模様です。すみません。

<Comm.pas抜粋>
{ 通信ポートのオープン }
procedure TComm.Open;
var
  szPort: array [0..9] of Char;             // ポート名
  CommTimeouts: TCommTimeouts;              // タイムアウト構造体
begin
  if FHandle < 0 then                       // 通信中でない時
  begin
    FReadOs.Offset := 0;                    // 受信オーバーラップ処理用
    FReadOs.OffsetHigh := 0;
                                            // イベントオブジェクトの作成
    FReadOs.hEvent := CreateEvent(nil,      // ハンドルを継承しない
                                  True,     // 手動リセットイベント
                                  False,    // 非シグナル状態で初期化
                                  nil);     // イベントオブジェクトの名前
    if FReadOs.hEvent = 0 then
      raise ECommOpenError(-2);             // イベントが作成できない

    FWriteOs.Offset := 0;                   // 送信オーバーラップ処理用
    FWriteOs.OffsetHigh := 0;
                                            // イベントオブジェクトの作成
    FWriteOs.hEvent := CreateEvent(nil,     // ハンドルを継承しない
                                   True,    // 手動リセットイベント
                                   False,   // 非シグナル状態で初期化
                                   nil);    // イベントオブジェクトの名前
    if FWriteOs.hEvent = 0 then
    begin
      CloseHandle(FReadOs.hEvent);          // 受信用イベントのクローズ
      raise ECommOpenError(-3);             // イベントが作成できない
    end;

    StrPCopy(szPort, 'COM'+ IntToStr(FPort));      // ポート名の作成
    FHandle := CreateFile(szPort,                  // ポート名
                          GENERIC_READ or GENERIC_WRITE,
                          0,                       // 排他的使用(*)
                          nil,                     // セキュリティー属性なし
                          OPEN_EXISTING,           // 既存(*)
                          FILE_ATTRIBUTE_NORMAL or // 通常
                          FILE_FLAG_OVERLAPPED,    // オーバーラップ入出力
                          0);                      // テンプレートなし(*)
    if FHandle < 0 then                     // エラー発生時
      raise ECommOpenError.Create(FHandle); // 例外の生成

    // 送受信バッファーサイズの設定
    SetupComm(FHandle, FInQueueSize, FOutQueueSize);

    // すべてのバッファー情報を破棄する
    PurgeComm(FHandle, PURGE_TXABORT or
                       PURGE_RXABORT or
                       PURGE_TXCLEAR or
                       PURGE_RXCLEAR);

    // タイムアウトのセットアップ
    CommTimeouts.ReadIntervalTimeout := MAXDWORD;
    CommTimeouts.ReadTotalTimeoutMultiplier := 0;
    CommTimeouts.ReadTotalTimeoutConstant := 1000;
    CommTimeouts.WriteTotalTimeoutMultiplier := 0;
    CommTimeouts.WriteTotalTimeoutConstant := 1000;
    SetCommTimeouts(FHandle, CommTimeouts);

    // 通信環境の設定
    SetBaudRate(FBaudRate);                 // 通信速度
    SetByteSize(FByteSize);                 // データビット数
    SetParityBits(FParityBits);             // パリティービット数
    SetStopBits(FStopBits);                 // ストップビット数

    SetFlowControls(FFlowControls);         // フロー制御
    SetReceiveNotifySize(FReceiveNotifySize);
    SetSendNotifySize(FSendNotifySize);

    SetEventMask(FEventMask);               // イベントマスク

    FThread := TCommWatch.Create(Self, FHandle);
                                            // 受信監視スレッドの起動

    EscapeCommFunction(FHandle, SETDTR);    // DTRをオンにする
  end;
end;

{ データの送信 }
procedure TComm.Write(const Buffer; Size: Integer);
var
  dwError: DWord;
  Stat: TComStat;
  dwBytesWritten: DWord;
begin
  if FHandle < 0 then
    raise ECommError.Create('通信が開始されていない。');

  if FOutQueueSize < Size then
    raise ECommError.Create('送信データ長が長すぎる。');

  repeat                                    // 送信キューが空くのを待つ
//    Application.ProcessMessages;          // これがあると送信が逆転する
    ClearCommError(FHandle, dwError, @Stat);
  until (FOutQueueSize - Stat.cbOutQue) >= Size;

  if not WriteFile(FHandle, Buffer, Size, dwBytesWritten, @FWriteOs) then
  begin
    if GetLastError = ERROR_IO_PENDING then // オーバーラップ処理時
    begin
      while not GetOverlappedResult(FHandle, FWriteOs,
                  dwBytesWritten, True) do
      begin
        if GetLastError = ERROR_IO_INCOMPLETE then  // まだ完了しない
          Continue
        else
        begin
          ClearCommError(FHandle, dwError, @Stat);
          Break;
        end;
      end;
    end
    else
    begin                                   // その他のエラー発生
      ClearCommError(FHandle, dwError, @Stat);
      raise ECommReadWriteError.Create(dwError);
    end;
  end;
end;

編集 削除
take  2022-10-27 05:18:26  No: 150643  IP: 192.*.*.*

> Openすると、直ぐに抜けてしまうので(最初のif文:if FHandle < 0 then)、

あーということは ここで引っかかってるのかもしれません。
掲示板で円マークが使えないみたいなのでバックスラッシュになりますが
コード上ではキーボード右上の円マークです

【元ソース】
    StrPCopy(szPort, 'COM'+ IntToStr(FPort));      // ポート名の作成

【修正ソース】
    StrPCopy(szPort, '\\.\COM'+ IntToStr(FPort));      // ポート名の作成

編集 削除
 2022-10-27 06:01:57  No: 150644  IP: 192.*.*.*

takeさん
色々と有難うございました。お陰様で問題が解決しそうです。
やはり、自社にありましたソースは何らかの理由で改ざんされている様に思います。
Openメソッドの最初の判定文が悪いため、CreateFileまで辿り着かなくなっております。
takaさんからご提示頂きました情報を元に解決することが出来ました。
本当にありがとうございました。
掲載頂きましたサイトのソースを利用させて頂こうと思います。

<Comm.pas抜粋>
{ 通信ポートのオープン }
procedure TComm.Open;
var
  szPort: array [0..9] of Char;             // ポート名
  CommTimeouts: TCommTimeouts;              // タイムアウト構造体
begin
  if FHandle < 0 then                       // ★⇐初期値が0なので、CreateFileに辿りつかない
  begin
  :::割愛させて頂きます
    StrPCopy(szPort, 'COM'+ IntToStr(FPort));      // ポート名の作成
    FHandle := CreateFile(szPort,                  // ポート名
  :::割愛させて頂きます

編集 削除
HFUKUSHI  2022-10-27 06:42:59  No: 150645  IP: 192.*.*.*

FHandleはファイルハンドルなので、初期値を(0ではなく)INVALID_HANDLE_VALUEに、比較も(0との比較ではなく)if FHandle <> INVALID_HANDLE_VALUE thenまたはif FHandle = INVALID_HANDLE_VALUE thenとするべきかと思います。

編集 削除
 2022-10-27 06:53:18  No: 150646  IP: 192.*.*.*

HFUKUSHIさん
お忙しいところアドバイス頂きまして有難うございます。
ご指導頂きました点をソースに反映したいと思います。

編集 削除