メインコンテンツまでスキップ

「copilot」タグの記事が1件件あります

全てのタグを見る

copilotにクラサバ書かせてみた

福永
ディアシステム(株)開発一部第3課

1部3課 福永です。

私の担当している作業では、周辺機器と通信するプログラムを作成することが多くあります。
しかし、周辺機器ごとに通信仕様が異なるため、毎回仕様を確認しながら個別に対応する必要があります。
似たような仕様のコードをネットで検索しても、なかなか見つからなかったり、見つかっても不要なコードが多く含まれていて、修正に手間がかかることがよくあります。
そこで、ある程度の仕様を自分で整理し、それをもとにCopilotにコードを作成させてみるのはどうか?と考え、実際に試してみました。

Copilotとは?

Copilotは、Microsoftが提供するAIアシスタントで、高度なAI技術を活用し、情報検索、文章作成、画像生成、データ分析など、さまざまな作業をサポートします。
Windows 11には標準で搭載されており、インストール不要ですぐに利用できます。

仕様を纏める

Copilotに正確なコードを生成してもらうためには、あらかじめ仕様を整理しておくことが重要です。
今回は「ソケット通信」を行うプログラムを、C# 4.8で作成することを想定しています。
なお、通信方式や使用言語を変更するだけで、他の環境にも柔軟に対応できる構成にすることが可能です。

今回の使用は以下にします
後で気づいたのですが、いくつか間違えがありますがCopilotがうまく処理をしてくれている箇所もあります。

「ソケット通信」を行うプログラム

  • .Net4.8のC#で作成

  • ServerとClientを作成

  • コメントやログメッセージ等は英語で明記

  • ServerとClient通信

    • ① Address:127.0.0.1
    • ② Port:50001
  • 通信情報

    • ① リクエスト
      • リクエストはClient➡Serverへの送信データで128Byte
        内訳は以下
        • SeqNo:4Byte
        • Cmd:4Byte
        • ReqData:64Byte
        • Reserved:上記の余りByte
    • ② レスポンス
      • レスポンスはServer➡Clientへの返信データで128Byte
        内訳は以下
        • SeqNo:4Byte
        • Cmd:4Byte
        • Result:4Byte
        • ResData:64Byte
        • Reserved:上記の余りByte
  • Server要望

    • ① 接続待ちをしてClientからリクエスト受信する
    • ② 受信した命令で各命令の処理へ遷移
    • ③ Clientへレスポンスを返す。
      レスポンス内容は以下
      • (ア) SeqNo:リクエストのSeqNoを設定
      • (イ) Cmd:リクエストのCmdを設定
      • (ウ) Result:Result一覧のOKを設定
      • (エ) ResData:とりあえず全て”0”
    • ④ 受信ループはExitフラグで抜けれるように設定。Exitフラグは別のスレッドなどから制御
    • ⑤ 各命令の処理は命令一覧のメソッドが用意され受信時に呼ばれる
    • ⑥ 命令一覧のメソッドはDictionaryでCmd,メソッドが用意され、受信時に検索して呼ばれる
  • Client要望

    • ① 各命令Methodが呼び出されるとServerへリクエスト送信
    • ② Serverへリクエスト送信は各命令の処理で設定
      リクエスト内容
      • (ア) SeqNo:グローバルにあるSeqNoを加算して設定
      • (イ) Cmd:各命令を設定
      • (ウ) ReqData:とりあえず全て”0”
    • ③ レスポンス受信時はリクエストしたSeqNoとレスポンスのSeqNoを比較して異なる場合はエラー
    • ④ 各命令の処理は命令一覧のメソッドが用意され、この処理を呼ぶとリクエスト要求される
    • ⑤ 命令一覧のメソッドはDictionaryでCmd,メソッドが用意され、受信時に検索して呼ばれる
  • 命令一覧

    • ① リクエスト/レスポンスの命令は以下が用意される。命令はConst Classで定義される
      • Start:"0001" 測定開始
      • GetValue:"0002" 現在の測定値を取得
      • End:"0003" 測定終了
    • ② 各命令Methodは命令一覧にある命令がServer/Clientに対し全て用意される
  • Result一覧

    • ① ResultはConst Classで定義される
      • OK:”0000”
      • NG:”0000”

Spec in, code out!

Copilotにコード生成を依頼する際は、以下の手順で進めます。

まず、Copilotに次のように入力します。
「Socket通信の雛形を以下の要件で作成してもらえますでしょうか?」
この文の直後に、整理した仕様をそのまま貼り付けます。

一旦テキストエディタに貼り付けて送るのが良いかもしれませんね。

画像1

入力後、数十秒かからずコードが生成されます。

Check the code

Copilotが指定通りC#でコードを生成してくれました。
では、見てみましょう!

C#でコードを生成し、コメントも指定通り英語表記になっています。

Common

まずは結果報告があるようです。仕様の概要を明記してくれています。

その後に共通個所となりえるCommand/Result/ Messageクラスを纏めて返してくれています。

画像2

  • 命令一覧:Command constants/ Result一覧:Result constants/通信情報のリクエスト:Request message/通信情報のレスポンスResponse messageがほぼ仕様書通りに作成してくれています。命令一覧でも、Server/Clientに対しての命令は共通でないと判断したのかここでは書かれておりません。 仕様書では、NG:”0000”と間違えて明記していたのですが、”9999”にしてくれています。
  • Byte変換等も考慮してくれています。ありがたいです。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Collections.Generic;

namespace SocketSample
{
// Command constants
public static class Commands
{
public const string Start = "0001";
public const string GetValue = "0002";
public const string End = "0003";
}

// Result constants
public static class Results
{
public const string OK = "0000";
public const string NG = "9999";
}

// Request message (128 bytes)
public class RequestMessage
{
public string SeqNo { get; set; } = "0000";
public string Cmd { get; set; } = "0000";
public string ReqData { get; set; } = new string('0', 64);
public string Reserved { get; set; } = new string('0', 56); // 128 - (4+4+64)

public byte[] ToBytes()
{
string msg = SeqNo + Cmd + ReqData + Reserved;
return Encoding.ASCII.GetBytes(msg.PadRight(128, '0'));
}

public static RequestMessage FromBytes(byte[] data)
{
string msg = Encoding.ASCII.GetString(data);
return new RequestMessage
{
SeqNo = msg.Substring(0, 4),
Cmd = msg.Substring(4, 4),
ReqData = msg.Substring(8, 64),
Reserved = msg.Substring(72, 56)
};
}
}

public static RequestMessage FromBytes(byte[] data)
{
string msg = Encoding.ASCII.GetString(data);
return new RequestMessage
{
SeqNo = msg.Substring(0, 4),
Cmd = msg.Substring(4, 4),
ReqData = msg.Substring(8, 64),
Reserved = msg.Substring(72, 56)
};
}
}

// Response message (128 bytes)
public class ResponseMessage
{
public string SeqNo { get; set; } = "0000";
public string Cmd { get; set; } = "0000";
public string Result { get; set; } = Results.OK;
public string ResData { get; set; } = new string('0', 64);
public string Reserved { get; set; } = new string('0', 56);

public byte[] ToBytes()
{
string msg = SeqNo + Cmd + Result + ResData + Reserved;
return Encoding.ASCII.GetBytes(msg.PadRight(128, '0'));
}

public static ResponseMessage FromBytes(byte[] data)
{
string msg = Encoding.ASCII.GetString(data);
return new ResponseMessage
{
SeqNo = msg.Substring(0, 4),
Cmd = msg.Substring(4, 4),
Result = msg.Substring(8, 4),
ResData = msg.Substring(12, 64),
Reserved = msg.Substring(76, 52)
};
}
}
}

Server

次にServerクラスを返してくれています。 指定していなくても、Server/Clientと共通箇所を判断して分けてくれています。

画像3

  • 指定したAddressとPortを使用し受信処理を行い、受信ループはExitフラグで抜けるように作成されています。
  • 各命令用のメソッドとDictionaryでCmd, 各命令用のメソッドが用意され、受信時に検索して呼ばれています。
  • 呼び出し後にClientへレスポンスを返す要件を代入して返しています。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Collections.Generic;

namespace SocketSample
{
public class Server
{
private TcpListener listener;
private bool exitFlag = false;

// Command dictionary
private Dictionary<string, Action<RequestMessage>> commandHandlers;

public Server()
{
commandHandlers = new Dictionary<string, Action<RequestMessage>>
{
{ Commands.Start, HandleStart },
{ Commands.GetValue, HandleGetValue },
{ Commands.End, HandleEnd }
};
}

public void StartServer()
{
listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 50001);
listener.Start();
Console.WriteLine("Server started. Waiting for connection...");

while (!exitFlag)
{
if (!listener.Pending())
{
Thread.Sleep(100);
continue;
}

TcpClient client = listener.AcceptTcpClient();
Console.WriteLine("Client connected.");

NetworkStream stream = client.GetStream();
byte[] buffer = new byte[128];

while (!exitFlag)
{
int read = stream.Read(buffer, 0, buffer.Length);
if (read == 0) break;

RequestMessage req = RequestMessage.FromBytes(buffer);
Console.WriteLine($"Received Cmd={req.Cmd}, SeqNo={req.SeqNo}");

// Call handler
if (commandHandlers.ContainsKey(req.Cmd))
commandHandlers[req.Cmd](req);

// Build response
ResponseMessage res = new ResponseMessage
{
SeqNo = req.SeqNo,
Cmd = req.Cmd,
Result = Results.OK,
ResData = new string('0', 64)
};

stream.Write(res.ToBytes(), 0, 128);
Console.WriteLine("Response sent.");
}

client.Close();
}

listener.Stop();
Console.WriteLine("Server stopped.");
}

// Command handlers
private void HandleStart(RequestMessage req) => Console.WriteLine("Start command executed.");
private void HandleGetValue(RequestMessage req) => Console.WriteLine("GetValue command executed.");
private void HandleEnd(RequestMessage req) => Console.WriteLine("End command executed.");
public void StopServer() => exitFlag = true;
}
}

Client

次にClientクラスを返してくれています。

画像4

  • 指定したAddressとPortを使用し送信処理を行うように作成されています。
  • レスポンス受信時にリクエストしたSeqNoとレスポンスSeqNoの比較を行いエラーチェックを行ってくれています。
  • リクエスト要求用の各命令用のメソッドがあります。
  • DictionaryでCmd, 各命令用のメソッドが用意されています。Response受信後に検索して呼ばれるように想像していたのですが、“受信時に検索して呼ばれる“ が良くなかったのか、リクエストメソッドを指定しており宣言のみになっています。こういう場合は、引き続き “commandMethods のメソッドは各命令のResponse受信後処理用を用意し、ClientのResponse受信後にcommandMethodsのメソッドが呼ばれるように変更して!” とCopilotに明記すると変更してくれると思います。
using System;
using System.Net.Sockets;
using System.Text;
using System.Collections.Generic;

namespace SocketSample
{
public class Client
{
private TcpClient client;
private NetworkStream stream;
private int globalSeqNo = 0;

private Dictionary<string, Action> commandMethods;

public Client()
{
commandMethods = new Dictionary<string, Action>
{
{ Commands.Start, Start },
{ Commands.GetValue, GetValue },
{ Commands.End, End }
};
}

public void Connect()
{
client = new TcpClient("127.0.0.1", 50001);
stream = client.GetStream();
Console.WriteLine("Connected to server.");
}
private void SendRequest(string cmd)
{
globalSeqNo++;
RequestMessage req = new RequestMessage
{
SeqNo = globalSeqNo.ToString("D4"),
Cmd = cmd,
ReqData = new string('0', 64)
};

stream.Write(req.ToBytes(), 0, 128);
Console.WriteLine($"Request sent. Cmd={cmd}, SeqNo={req.SeqNo}");

byte[] buffer = new byte[128];
int read = stream.Read(buffer, 0, buffer.Length);

ResponseMessage res = ResponseMessage.FromBytes(buffer);
if (res.SeqNo != req.SeqNo)
Console.WriteLine("Error: SeqNo mismatch!");
else
Console.WriteLine($"Response OK. Cmd={res.Cmd}, SeqNo={res.SeqNo}");
}

// Command methods
public void Start() => SendRequest(Commands.Start);
public void GetValue() => SendRequest(Commands.GetValue);
public void End() => SendRequest(Commands.End);

public void Close()
{
stream.Close();
client.Close();
}
}
}

How to use

さて、上記のクラスがCopilotで生成されました。
生成されるだけでなく、使用方法も教えてくれます。
“上記を元に色々機能を追加しますよ!”みたいな提案もしてくれます。今回はStart/GetValue/Endの命令の為、測定器と判断したのでしょうか、測定値を入れる提案をしてくています。

画像5

まとめ

結構使えるかなと思います。

今回は「ソケット通信」を行うプログラムを作成対象にしましたが、簡単な雛形や汎用クラスなどを作成、複数のクラスを指定し共通化できる箇所を見つけてもらったりする時等にも有効と思います。

難しい雛形を作成するより、初めに仕様を複雑にせず、“ソケット通信のServerとClientをC#で!“と明記して大まかな雛形を作成してもらい、仕様を徐々に明記していく手順もあります。

使用していて以下の問題がありましたので、気を付ける必要があります。

  • 貼り付けるごとに違う結果が返ることもあります。
  • 書き方が悪かったりすると判断されていないのか仕様が反映されません。

Bonus Track

Copilotでが出力を覚えていますので、上記の後、”上記のServer/Clientのフローをシーケンス図 (Markdown + Mermaid) で明記してもらえますか?” と入力するとシーケンス図 (Markdown + Mermaid) を作成してくれます。
同様にフロー等も作成可能です。

画像6

どの様の動作をするか、視覚的に確認できるのは便利です。

画像7

未経験から始める
システムエンジニア

一生モノのITスキルを身につけよう

あなたの経験とスキルを
ディアシステムで発揮してください!