diff --git a/README.md b/README.md new file mode 100644 index 0000000..6686352 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Serial Trigger + +ArduinoへシリアルUSB経由で100 msトリガーパルスを発行するPythonスクリプトです。 + +## 概要 + +`trigger.py` を実行すると、指定COMポートに接続されたArduinoへ `TRIG` コマンドを送信します。 +ArduinoはACK・DONEを返すことでトリガー完了を通知し、Pythonスクリプトはその応答を確認します。 + +### 通信シーケンス + +``` +PC (Python) Arduino + | | + |--- TRIG\n ------->| + | | (トリガー出力開始) + |<-- ACK -----------| + | | (100 ms パルス出力) + |<-- DONE ----------| + | | +``` + +## 要件 + +- Python 3.x +- pyserial + +## インストール + +```bash +pip install -r requirements.txt +``` + +## 設定 + +`trigger.py` 冒頭の定数を環境に合わせて変更してください。 + +| 定数 | デフォルト値 | 説明 | +|------|------------|------| +| `PORT` | `COM5` | 接続するCOMポート | +| `BAUD_RATE` | `115200` | ボーレート | +| `RESPONSE_TIMEOUT_SECONDS` | `2.0` | Arduino応答待ちタイムアウト (秒) | + +## 使い方 + +```bash +python trigger.py +``` + +実行例: + +``` +COM5を開きました。 +Arduino: ACK +Arduino: DONE +100 msトリガーが完了しました。 +``` + +## 別プログラムへの組み込み手順 + +main関数の流れは以下の通りです. +1. シリアルポート接続 +2. 安定化待機 +3. シグナル送信 + +ここで,1と2までは別プログラムの初期化ルーチンで事前に行ってください. +トリガー信号を出す瞬間に 3 シグナル送信 を実行してください. +このようにせず毎度 1~3 を処理すると,シグナル送信が遅延し,適時にトリガーが出せません. + +## エラー処理 + +| メッセージ | 原因 | +|-----------|------| +| `Arduinoは現在トリガー出力中です。` | Arduino がBUSY状態のため再送が必要 | +| `Arduinoからの応答がタイムアウトしました。` | ケーブル未接続・ボーレート不一致など | +| `シリアルポートエラー: ...` | COMポートが存在しない・アクセス権なし | +| `シリアル送信タイムアウト: ...` | 書き込みタイムアウト (デフォルト1秒) | diff --git a/Trigger_arduino/Trigger_arduino.ino b/Trigger_arduino/Trigger_arduino.ino new file mode 100644 index 0000000..3e85780 --- /dev/null +++ b/Trigger_arduino/Trigger_arduino.ino @@ -0,0 +1,154 @@ +#include +#include + +constexpr uint8_t TRIGGER_PIN = 9; +constexpr uint32_t TRIGGER_WIDTH_US = 100000UL; + +constexpr size_t RX_BUFFER_SIZE = 32; +char rxBuffer[RX_BUFFER_SIZE]; +size_t rxLength = 0; +bool rxOverflow = false; + +bool triggerActive = false; +uint32_t triggerStartUs = 0; + +/* + * 使用中のPro Micro互換基板では、 + * LED0 = 点灯、LED1 = 消灯として動作する。 + */ +void triggerLedOn() +{ + TXLED0; +} + +void triggerLedOff() +{ + TXLED1; +} + +void allCommunicationLedsOff() +{ + RXLED1; + TXLED1; +} + +void startTrigger() +{ + if (triggerActive) + { + Serial.println(F("BUSY")); + return; + } + + // TTL信号と赤色LEDを同時にON + digitalWrite(TRIGGER_PIN, HIGH); + triggerLedOn(); + + triggerStartUs = micros(); + triggerActive = true; + + Serial.println(F("ACK")); +} + +void updateTrigger() +{ + if (!triggerActive) + { + return; + } + + const uint32_t elapsedUs = micros() - triggerStartUs; + + if (elapsedUs >= TRIGGER_WIDTH_US) + { + // TTL信号と赤色LEDを同時にOFF + digitalWrite(TRIGGER_PIN, LOW); + triggerLedOff(); + + triggerActive = false; + + Serial.println(F("DONE")); + } +} + +void processCommand(const char* command) +{ + if (strcmp(command, "TRIG") == 0) + { + startTrigger(); + } + else if (strcmp(command, "PING") == 0) + { + Serial.println(F("PONG")); + } + else + { + Serial.print(F("ERR UNKNOWN_COMMAND: ")); + Serial.println(command); + } +} + +void receiveCommands() +{ + while (Serial.available() > 0) + { + const char received = static_cast(Serial.read()); + + if (received == '\n' || received == '\r') + { + if (rxOverflow) + { + Serial.println(F("ERR LINE_TOO_LONG")); + rxLength = 0; + rxOverflow = false; + continue; + } + + if (rxLength == 0) + { + continue; + } + + rxBuffer[rxLength] = '\0'; + processCommand(rxBuffer); + rxLength = 0; + continue; + } + + if (rxOverflow) + { + continue; + } + + if (rxLength < RX_BUFFER_SIZE - 1) + { + rxBuffer[rxLength++] = received; + } + else + { + rxOverflow = true; + } + } +} + +void setup() +{ + // TTL出力を初期状態LOWにする + digitalWrite(TRIGGER_PIN, LOW); + pinMode(TRIGGER_PIN, OUTPUT); + + // RX/TX LEDを出力として初期化 + TX_RX_LED_INIT; + + // 待機中は赤色LEDを両方消灯 + allCommunicationLedsOff(); + + Serial.begin(115200); +} + +void loop() +{ + updateTrigger(); + receiveCommands(); + updateTrigger(); +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f6c1a1f --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyserial diff --git a/trigger.py b/trigger.py new file mode 100644 index 0000000..51319f5 --- /dev/null +++ b/trigger.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import time + +import serial +from serial import SerialException, SerialTimeoutException + + +PORT = "COM5" +BAUD_RATE = 115200 + +# Arduinoの100 msパルスとUSB通信時間を考慮した応答待ち時間 +RESPONSE_TIMEOUT_SECONDS = 2.0 + + +def send_trigger(ser: serial.Serial) -> None: + """ + ArduinoへTRIGコマンドを送り、ACKとDONEを待つ。 + + Raises: + RuntimeError: + ArduinoがBUSYまたはエラーを返した場合、 + あるいは応答がタイムアウトした場合。 + """ + # 前回の不要な受信データを破棄 + ser.reset_input_buffer() + + # Arduinoへ改行付きコマンドを送信 + ser.write(b"TRIG\n") + ser.flush() + + deadline = time.monotonic() + RESPONSE_TIMEOUT_SECONDS + ack_received = False + + while time.monotonic() < deadline: + raw_line = ser.readline() + + # timeout時間内に1行受信できなかった場合 + if not raw_line: + continue + + message = raw_line.decode("ascii", errors="replace").strip() + + if not message: + continue + + print(f"Arduino: {message}") + + if message == "ACK": + ack_received = True + continue + + if message == "DONE": + if not ack_received: + raise RuntimeError("DONEを受信しましたが、ACKを受信していません。") + + return + + if message == "BUSY": + raise RuntimeError("Arduinoは現在トリガー出力中です。") + + if message.startswith("ERR"): + raise RuntimeError(f"Arduinoエラー: {message}") + + raise RuntimeError("Arduinoからの応答がタイムアウトしました。") + + +def main() -> None: + try: + with serial.Serial( + port=PORT, + baudrate=BAUD_RATE, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=0.1, + write_timeout=1.0, + xonxoff=False, + rtscts=False, + dsrdtr=False, + ) as ser: + print(f"{ser.port}を開きました。") + + # 接続直後の安定待ち + time.sleep(0.2) + + send_trigger(ser) + + print("100 msトリガーが完了しました。") + + except SerialTimeoutException as exc: + print(f"シリアル送信タイムアウト: {exc}") + + except SerialException as exc: + print(f"シリアルポートエラー: {exc}") + + except RuntimeError as exc: + print(f"通信エラー: {exc}") + + +if __name__ == "__main__": + main()