MSMQTT_Test for SenseWay LoRaWAN 2019/09/05..11

・2019/09/11 データの読み方が間違っていたのを手直し。ついでに CSV ファイル保存を追加。

Delphi で MQTT、JSON を扱うサンプルです。
SenseWay LoRaWAN のデータを読み込み (Subscribe)、書き込み (Publish)のテストに使えます。
※ Trial 版の MSMQTT コンポーネントを使用しています。
  1時間を超えると、エラーダイアログが出ます。一度終了させて再起動すると、また1時間ほど使えるようです。

ClientID (Username と同じ), Username, Password を入力し、[ Connect ] ボタンをクリックすると、センスウェイサーバにつながります。
[ Subscribe] ボタンをクリックすると、サーバ(MQTT ブローカー)からデータが取得されます。
devEUI の入力は [Publish] の時に必要です。[ Subscribe ] で取得対象を制限しない(すべてのデータを取得)ときは、不要です。



■ダウンロード

 MSMQTT_Test.zip (Exe 本体のみ。アイコンは作成していません。)


// Delphi 10.3 Community Edition
{
  2019/09/11 データの読み方が間違っていたのを手直し
             CSV 保存を追加
}
unit TMQTT_TESTUnit4;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, TMS.MQTT.Global, Vcl.StdCtrls,
  TMS.MQTT.Client,
  System.JSON, System.Generics.Collections,
  DateUtils, Vcl.Grids,
  IniFiles, Vcl.Buttons;

type
  TForm4 = class(TForm)
    TMSMQTTClient1: TTMSMQTTClient;
    Button1: TButton;
    Memo1: TMemo;
    Button4: TButton;
    GroupBox1: TGroupBox;
    Button3: TButton;
    GroupBox2: TGroupBox;
    Button2: TButton;
    Edit6: TEdit;
    Label6: TLabel;
    Label8: TLabel;
    Edit8: TEdit;
    Label9: TLabel;
    Edit9: TEdit;
    Label10: TLabel;
    Edit10: TEdit;
    Label11: TLabel;
    Edit11: TEdit;
    Label12: TLabel;
    Edit12: TEdit;
    GroupBox3: TGroupBox;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    Edit1: TEdit;
    Edit2: TEdit;
    Edit3: TEdit;
    Edit4: TEdit;
    Edit5: TEdit;
    Label7: TLabel;
    Edit7: TEdit;
    StringGrid1: TStringGrid;
    Label13: TLabel;
    SpeedButton1: TSpeedButton;
    procedure Button1Click(Sender: TObject);
    procedure TMSMQTTClient1SubscriptionAcknowledged(ASender: TObject;
      APacketID: Word; ASubscriptions: TTMSMQTTSubscriptions);
    procedure Button2Click(Sender: TObject);
    procedure TMSMQTTClient1PublishReceived(ASender: TObject; APacketID: Word;
      ATopic: string; APayload: TArray<System.Byte>);
    procedure TMSMQTTClient1PublishReceivedEx(ASender: TObject; APacketID: Word;
      ATopic: string; APayload: TTMSMQTTBytes);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure TMSMQTTClient1PacketReceived(ASender: TObject;
      APacketInfo: TTMSMQTTPacketInfo);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure SpeedButton1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    FSubscribeRequestPacketId : Word;
    cmdMode : integer;
    procedure rxToSg(const src: string);
  end;

var
  Form4: TForm4;

implementation

{$R *.dfm}

// Delphi XE 10でJSON文字列から返された日付時刻をTDateTimeに解析する方法
// https://codeday.me/jp/qa/20190628/1129895.html
function JSONDateToDatetime(JSONDate: string): TDatetime;
var
  Year, Month, Day, Hour, Minute, Second, Millisecond: Word;
begin
  Year        := StrToInt(Copy(JSONDate, 1, 4));
  Month       := StrToInt(Copy(JSONDate, 6, 2));
  Day         := StrToInt(Copy(JSONDate, 9, 2));
  Hour        := StrToInt(Copy(JSONDate, 12, 2));
  Minute      := StrToInt(Copy(JSONDate, 15, 2));
  Second      := StrToInt(Copy(JSONDate, 18, 2));
  Millisecond := Round(StrToFloat(Copy(JSONDate, 19, 4)));

  result := EncodeDateTime(Year, Month, Day, Hour, Minute, Second, Millisecond);
end;

procedure TForm4.Button1Click(Sender: TObject);
// 接続
begin
  TMSMQTTClient1.BrokerHostName := Edit1.Text;
  TMSMQTTClient1.BrokerPort := StrToIntDEf(Edit2.Text, 1883); // 1883;

  TMSMQTTClient1.ClientID := Edit3.Text;
  TMSMQTTClient1.Credentials.Username := Edit4.Text;
  TMSMQTTClient1.Credentials.Password := Edit5.Text;

  TMSMQTTClient1.LastWillSettings.Topic := '';
  cmdMode := 101;
  // 接続
  TMSMQTTClient1.Connect();
end;

procedure TForm4.Button2Click(Sender: TObject);
// Subscribe
var
  Topic : string;
begin
  Topic := Edit6.Text;
  Topic := stringReplace(Topic, '<username>', Edit4.Text, []);
  // MQTTでSubscribeすればデータを取得することができる
  if TMSMQTTClient1.IsConnected then begin
    Memo1.Lines.Clear;

    FSubscribeRequestPacketId :=
      TMSMQTTClient1.Subscribe(
        //'lora/' + UserNAme + '/+/#',   // ワイルドカード(すべてのデータ)
        //'lora/'+ UserName+ '/+/rx', // 受信データのみの時
        Topic,
        TTMSMQTTQoS.qosAtMostOnce
      );
  end;
end;

procedure TForm4.Button3Click(Sender: TObject);
// Publish
var
  Payload : TBytes;
  s : string;
  Topic : string;
begin
  Topic := Edit8.Text;
  Topic := stringReplace(Topic, '<username>', Edit4.Text, []);
  Topic := stringReplace(Topic, '<devEUI>', Edit7.Text, []);

  // データ書き込み
  s := '{"conf":false,"ref":"'+ Edit10.Text + '","port":' + Edit11.Text + ',"data":"' +
    Edit12.Text + '"}';

  Payload := TEncoding.ANSI.GetBytes(s);

  if TMSMQTTClient1.IsConnected then begin
    TMSMQTTClient1.Publish(
      //'lora/' + UserName + '/' + devEUI + '/tx',
      Topic,
      Payload,
      TTMSMQTTQoS.qosAtMostOnce,
      True
    );
  end;
end;

procedure TForm4.Button4Click(Sender: TObject);
// Disconnect
begin
  // 切断
  if TMSMQTTClient1.IsConnected then begin
    TMSMQTTClient1.Disconnect;
  end;
end;

procedure TForm4.FormCreate(Sender: TObject);
var
  ini : TIniFile;
begin
  with StringGrid1 do begin
    Cells[0, 0] := 'date(JST)';
    Cells[1, 0] := 'date(UTC)';
    Cells[2, 0] := 'rssi';
    Cells[3, 0] := 'snr';
    Cells[4, 0] := 'gwid';
    Cells[5, 0] := 'fq';
    Cells[6, 0] := 'cnt';
    Cells[7, 0] := 'data';
    Cells[8, 0] := 'mt';
    Cells[9, 0] := 'devEUI';
    Cells[10, 0] := 'dr';
    Cells[11, 0] := 'port';

    Cells[12, 0] := 'data1';
    Cells[13, 0] := 'data2';
    Cells[14, 0] := 'data3';
    Cells[15, 0] := 'data4';
  end;
  ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
  try
    Edit1.Text := ini.ReadString('Host', 'HostName', Edit1.Text);
    Edit2.Text := ini.ReadString('Host', 'Port', Edit2.Text);
    Edit3.Text := ini.ReadString('Host', 'ClientID', '');
    Edit4.Text := ini.ReadString('Host', 'UserName', '');
    Edit5.Text := ini.ReadString('Host', 'Password', '');

    Edit7.Text := ini.ReadString('Gateway', 'devEUI', '');
    Edit6.Text := ini.ReadString('Subscribe', 'Topic', Edit6.Text);

    Edit8.Text := ini.ReadString('Publish', 'Topic', Edit8.Text);
    Edit9.Text := ini.ReadString('Publish', 'conf', Edit9.Text);
    Edit10.Text := ini.ReadString('Publish', 'ref', Edit10.Text);
    Edit11.Text := ini.ReadString('Publish', 'port', Edit11.Text);
    Edit12.Text := ini.ReadString('Publish', 'data', Edit12.Text);
  finally
    ini.Free;
  end;
end;

procedure TForm4.FormDestroy(Sender: TObject);
var
  ini : TIniFile;
begin
  ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
  try
    ini.WriteString('Host', 'HostName', Edit1.Text);
    ini.WriteString('Host', 'Port', Edit2.Text);
    ini.WriteString('Host', 'ClientID', Edit3.Text);
    ini.WriteString('Host', 'UserName', Edit4.Text);
    ini.WriteString('Host', 'Password', Edit5.Text);

    ini.WriteString('Gateway', 'devEUI',  Edit7.Text);
    ini.WriteString('Subscribe', 'Topic', Edit6.Text);

    ini.WriteString('Publish', 'Topic', Edit8.Text);
    ini.WriteString('Publish', 'conf', Edit9.Text);
    ini.WriteString('Publish', 'ref', Edit10.Text);
    ini.WriteString('Publish', 'port', Edit11.Text);
    ini.WriteString('Publish', 'data', Edit12.Text);
  finally
    ini.Free;
  end;
end;

procedure TForm4.rxToSg(const src: string);
var
  JSONValue: TJSONValue;
  JSONObject: TJSONObject;
  JSONPair: TJSONPair;
  JSONArray: TJSONArray;
  S: string;
  i, j, gwCount : Integer;
  path : string;
begin
  JSONValue := TJSONObject.ParseJSONValue(src);
  if JSONValue is TJSONArray then begin
  end
  else if JSONVAlue is TJSONObject then begin
    gwCount := 0;
    // "gw"(ゲートウェイ)は1つとは限らない
    // "gw" と "mod "の2つのうち、最初の "gw" の配列数を得る
    JSONObject := JSONValue as TJSONObject;
    JSONPair := JSONObject.Pairs[0];
    if JSONPair.JsonValue is TJSONArray  then begin
      JSONArray := JSONPair.JsonValue as TJSONArray;
      gwCount := JSONArray.Count;
    end;
    with StringGrid1 do begin
      // 記入位置(行)を探す
      j := 1;
      for i := RowCount -1 downto 0 do begin
        if Cells[0, i] <> '' then begin
          j := i + 1;
          break;
        end;
      end;
      // 行を追加
      if j = RowCount then RowCount := RowCount + 1;
      Row := j;
      if gwCount > 0 then begin
        // すべて文字列として取得可能だが、JSON のデータ型に合わせている
        // "gw": ゲートウェイのデータ(複数ある場合は配列列挙)
        for i := 0 to gwCount -1 do begin

          path := 'gw[' + i.ToString + ']';
          // データ受信時刻(UTC)
          Cells[1, j] := JSONValue.GetValue<string>(path + '.date');
          // JST
          Cells[0, j] := FormatDateTime('YYYY/MM/DD hh:nn:ss', IncHour(JSONDateToDatetime(Cells[1, j]), 9));
          // 受信信号強度
          Cells[2, j] := IntToStr(JSONValue.GetValue<Integer>(path + '.rssi'));
          // 信号雑音比
          Cells[3, j] := FloatToStr(JSONValue.GetValue<double>(path + '.snr'));
          // Gateway ID
          Cells[4, j] := JSONValue.GetValue<string>(path + '.gwid');
        end;
      end;

      // "mod": モジュール(デバイス)のデータ
      // 使用周波数
      Cells[5, j] := FloatToStr(JSONValue.GetValue<double>('mod.fq'));
      // サーバでのカウント値
      Cells[6, j] := IntToStr(JSONValue.GetValue<integer>('mod.cnt'));
      // データ (16 進数表記)
      Cells[7, j] := JSONValue.GetValue<string>('mod.data');
      // ACK 要求データ(Confirm)か否(UnConfirm)か
      Cells[8, j] := JSONValue.GetValue<string>('mod.mt');
      // モジュール(デバイス)固有アドレス DevEUI
      Cells[9, j] := JSONValue.GetValue<string>('mod.devEUI');
      // DR 値 (SpreadFactor と BandWidth の組み合わせ)
      Cells[10, j] := JSONValue.GetValue<string>('mod.dr');
      // LoRa ポート番号(ユーザが 1~223 の間で使用可能)
      Cells[11, j] := IntToStr(JSONValue.GetValue<integer>('mod.port'));
      // データを 10 進表記に
      s := Cells[7, j];
      for i := 0 to 3 do begin
        if s.Length >= (i + 1) * 4 then
          Cells[12 + i, j] := IntToStr(StrToInt('$' + Copy(s, 1 + i * 4, 4)))
        else
          Cells[12 + i, j] := '';
      end;
    end;
  end;

end;
procedure TForm4.SpeedButton1Click(Sender: TObject);
// CSV 保存
var
  i, j : integer;
  fname :  TFileName;
  s : string;
  sl : TStringList;
begin
  fname := ExtractFilePath(ParamStr(0)) + FormatDateTime('YYYYMMDDHHNNSS', Now) + '.csv';
  sl := TStringList.Create;
  try
    with StringGrid1 do begin
      for i := 0 to RowCount -1 do begin
        if Cells[0, i] <> '' then begin
          s := '';
          for j := 0 to ColCount - 1 do begin
            s := s + Cells[j, i];
            if j <  ColCount - 1 then s := s + ',';
          end;
          sl.Add(s);
        end
        else
          break;
      end;
    end;

    if sl.Count > 1 then begin
      sl.SaveToFile(fname);
      ShowMessage(IntToStr(sl.Count -1) + ' データを保存しました.');
    end
    else
      ShowMessage('データがありません.');
  finally
    sl.Free;
  end;
end;

procedure TForm4.TMSMQTTClient1PacketReceived(ASender: TObject;
  APacketInfo: TTMSMQTTPacketInfo);
begin
  if (cmdMode = 101) and TMSMQTTClient1.IsConnected then begin
    Memo1.Lines.Clear;
    Memo1.Lines.Add('Connected');
    cmdMode := 0;
  end;
end;

procedure TForm4.TMSMQTTClient1PublishReceived(ASender: TObject;
  APacketID: Word; ATopic: string; APayload: TArray<System.Byte>);
var
  Payload : string;
begin
  Payload := TEncoding.ANSI.GetString(APayload);
  Memo1.Lines.Add(#13#10 + FormatDateTime('YYYY/MM/DD hh:nn:ss', Now));
  Memo1.Lines.Add('Topic = ' + ATopic);
  Memo1.Lines.Add('Payload = ' + Payload);
  if (Pos('/rx', ATopic) > 0) and (Pos('gw', Payload)>0) and (Pos('mod', Payload)>0) then
    rxToSG(Payload);
end;

procedure TForm4.TMSMQTTClient1PublishReceivedEx(ASender: TObject;
  APacketID: Word; ATopic: string; APayload: TTMSMQTTBytes);
begin
  // こちらも同じ結果
  {
  Memo1.Lines.Add(#13#10 + FormatDateTime('YYYY/MM/DD hh:nn:ss', Now));
  Memo1.Lines.Add('TopicEx = ' + ATopic);
  Memo1.Lines.Add('PayloadEx = ' +  TEncoding.ANSI.GetString(APayload));
  }
end;

procedure TForm4.TMSMQTTClient1SubscriptionAcknowledged(ASender: TObject;
  APacketID: Word; ASubscriptions: TTMSMQTTSubscriptions);
begin
  if (APacketID = FSubscribeRequestPacketId) and ASubscriptions[0].Accepted then begin
    Memo1.Lines.Add('subscribed');
  end;
end;

end.