0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JetsonのPythonからキーエンスPLCへ上位リンクでDMを書く(KV-N24DT + KV-NC1EP)

0
Posted at

Jetson Orin Nano で動かしている物体検出の結果を、キーエンスのPLCへ渡して電磁弁を動かしています。

PLC側にライブラリを入れる必要はなく、Python標準の socket だけで書けました。ただ、動くまでにポート番号で丸一日溶かしたので、その辺りも含めて残します。

構成

型式・値
判定側 Jetson Orin Nano(YOLOv8 + TensorRT)
PLC KV-N24DT(トランジスタ出力・シンク)
通信ユニット KV-NC1EP(EtherNet/IP)
接続 TCP 192.168.0.10:8501(上位リンク)
PLC側の受け DM100(クラスコード)、DM101(開放時間ms)

Jetson から渡すのは 「種類」と「バルブを開ける時間」の2ワードだけです。遅延を数えるのも、出力を切るのもPLC側でやります。

先に、ポートの話

ここが最大の落とし穴でした。同じPLCに、用途の違う入口が3つ空いています。

ポート 用途
8500 KV STUDIO からのプログラム転送
8501 上位リンク(外部機器がデバイスを読み書き)
8502 タッチパネル(VT)専用

番号を1つ間違えると、ping は通る、TCP接続もできる、それなのに応答だけが返ってきません。

外から見て切り分けにくいのもここです。nc -z でポートが開いていることを確認しても、プロトコルが違えば会話は成立しません。上位リンクでデバイスを読み書きするなら 8501 です。

# 疎通だけなら通ってしまう。これで安心しないこと
nc -zv 192.168.0.10 8501

最小コード

改行コードは \r です。\n ではありません。

import socket

class KeyenceUplink:
    def __init__(self, host="192.168.0.10", port=8501, timeout=1.0):
        self.sock = socket.create_connection((host, port), timeout=timeout)

    def cmd(self, line: str) -> str:
        self.sock.sendall((line + "\r").encode("ascii"))
        return self.sock.recv(256).decode("ascii", "ignore").strip()

    def close(self):
        self.sock.close()


plc = KeyenceUplink()
print(plc.cmd("RD DM100"))        # 読み出し → "0" など
print(plc.cmd("WR DM101 250"))    # 書き込み → "OK"
  • RD <デバイス> で読み出し、値が返る
  • WR <デバイス> <値> で書き込み、OK が返る
  • 複数ワードをまとめるなら WRS DM100.U 2 1 250 のように書ける

応答が E0E1 で返る場合は、コマンドかデバイス指定の側の間違いです。無応答で固まる場合は、たいていポートが違います。

書き込む順番が大事

クラスコードを書いた瞬間にPLCが動き出す作りにしているので、開放時間を先に書きます。

def send(plc, class_code: int, duration_ms: int):
    plc.cmd(f"WR DM101 {duration_ms}")   # 先に開放時間
    plc.cmd(f"WR DM102 0")               # 上位ワードをクリア
    plc.cmd(f"WR DM100 {class_code}")    # 最後にクラスコード(これがトリガ)

逆にすると、前回の開放時間のままバルブが開きます。1回だけ変な動きをして再現しない、という一番厄介な形のバグになります。

PLC側は DM100 ≠ 0 を見つけたら1スキャンだけ受信フラグを立て、クラス別の固定遅延タイマを起動して、満了したら出力をONにします。処理が終わったら DM100 を0に戻します。

判定側が落ちた時に、出力を残さない

AI側が落ちても電磁弁が開きっぱなしにならないように、切るのはPLC側の仕事にしています。

出力をONにするのと同時にタイマを起動して、満了したら自分でOFFにする。Jetsonから「閉じろ」という指示は送りません。送信が途絶えたら、PLCは何もしないだけです。

この分け方にしておくと、Jetsonを再起動しても、LANケーブルを抜いても、装置側は安全側に倒れます。

送信はワーカスレッドに逃がす

推論ループの中で直接 sendall すると、PLC側の応答待ちでフレーム処理が止まります。キューに積んで別スレッドで送ると、検出の周期に影響しません。

import queue, threading

class Sender:
    def __init__(self, plc):
        self.plc, self.q = plc, queue.Queue()
        threading.Thread(target=self._loop, daemon=True).start()

    def _loop(self):
        while True:
            job = self.q.get()
            try:
                job()
            except OSError as e:
                print("PLC送信失敗:", e)
            finally:
                self.q.task_done()

    def post(self, fn):
        self.q.put(fn)

例外を握りつぶさずログに出しておくと、断線した時に「いつから届いていないか」が分かります。

ハマったところ

  • 8501 以外に繋いでいた — pingもTCP接続も通るので気づきにくい。無応答なら真っ先にポートを疑う
  • 改行を \n にしていた\r でないと応答が返らない
  • 書き込む順番が逆だった — 先にトリガを書くと、前回の値で動く
  • タッチパネルがポートを専有していた — VT用は8502。ここを外部機器で使わない
  • 設定値0のタイマ — PLC側でタイマのプリセットが0だと、即タイムアップして一瞬で閉じる
  • 推論ループ内で同期送信していた — FPSが落ちる。キューへ逃がす

まとめ

  • 上位リンクは標準 socket だけで書ける。ライブラリ不要
  • ポートは 8501。8500はプログラム転送、8502はタッチパネル
  • 改行は \r
  • 渡すのは最小限(種類と時間)。判断はAI側、動作と安全はPLC側
  • 出力を切るのはPLCの仕事にしておくと、AI側が落ちても装置が安全側に倒れる

この構成で選別装置を1台組んでいます。盤の中身や結線、I/O割付まで含めた話は note に書きました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?