Closed3

Boost.ASIOのserial_portでPOSIX標準にないボーレートを設定する

onihusubeonihusube

Unix/Linux環境を前提。

#include <boost/asio.hpp>

namespace asio = boost::asio;

int main() {
  asio::io_context ioc{};
  asio::serial_port serial{ioc, "/dev/cu.usbserial-xxxxx"};
  boost::system::error_code ec{};

  // これが失敗する
  serial.set_option(asio::serial_port_base::baud_rate(460800), ec);

  if (ec) {
    std::cout << ec.message() << std::endl;  // Invalid argument
  }
}

デバイス側は対応しているものとして、なぜかASIO内部で失敗する。エラーメッセージはInvalid argumentとしか言わないので何が悪いのかわからない・・・

onihusubeonihusube

serial.set_optionをデバッガで追いかけると、非常に入り組んでいるが結果としてserial_port_base::baud_rate::store()を呼び出している

定義をコピペすると

ASIO_SYNC_OP_VOID serial_port_base::baud_rate::store(
    ASIO_OPTION_STORAGE& storage, asio::error_code& ec) const
{
#if defined(ASIO_WINDOWS) || defined(__CYGWIN__)
  storage.BaudRate = value_;
#else
  speed_t baud;
  switch (value_)
  {
  // Do POSIX-specified rates first.
  case 0: baud = B0; break;
  case 50: baud = B50; break;
  case 75: baud = B75; break;
  case 110: baud = B110; break;
  case 134: baud = B134; break;
  case 150: baud = B150; break;
  case 200: baud = B200; break;
  case 300: baud = B300; break;
  case 600: baud = B600; break;
  case 1200: baud = B1200; break;
  case 1800: baud = B1800; break;
  case 2400: baud = B2400; break;
  case 4800: baud = B4800; break;
  case 9600: baud = B9600; break;
  case 19200: baud = B19200; break;
  case 38400: baud = B38400; break;
  // And now the extended ones conditionally.
# ifdef B7200
  case 7200: baud = B7200; break;
# endif
# ifdef B14400
  case 14400: baud = B14400; break;
# endif
# ifdef B57600
  case 57600: baud = B57600; break;
# endif
# ifdef B115200
  case 115200: baud = B115200; break;
# endif
# ifdef B230400
  case 230400: baud = B230400; break;
# endif
# ifdef B460800
  case 460800: baud = B460800; break;
# endif
# ifdef B500000
  case 500000: baud = B500000; break;
# endif
# ifdef B576000
  case 576000: baud = B576000; break;
# endif
# ifdef B921600
  case 921600: baud = B921600; break;
# endif
# ifdef B1000000
  case 1000000: baud = B1000000; break;
# endif
# ifdef B1152000
  case 1152000: baud = B1152000; break;
# endif
# ifdef B2000000
  case 2000000: baud = B2000000; break;
# endif
# ifdef B3000000
  case 3000000: baud = B3000000; break;
# endif
# ifdef B3500000
  case 3500000: baud = B3500000; break;
# endif
# ifdef B4000000
  case 4000000: baud = B4000000; break;
# endif
  default:
    ec = asio::error::invalid_argument;
    ASIO_SYNC_OP_VOID_RETURN(ec);
  }
# if defined(_BSD_SOURCE) || defined(_DEFAULT_SOURCE)
  ::cfsetspeed(&storage, baud);
# else
  ::cfsetispeed(&storage, baud);
  ::cfsetospeed(&storage, baud);
# endif
#endif
  ec = asio::error_code();
  ASIO_SYNC_OP_VOID_RETURN(ec);
}

最初の#if defined(ASIO_WINDOWS) || defined(__CYGWIN__)でWindowsの場合は何も気にせずボーレート設定をしている(のでWindowsは今回関係ない)。

ここでのvalue_serial_port_base::baud_rate構造体のメンバ変数で、set_opitonで私たボーレート値。ASIO_OPTION_STORAGEtermios構造体(MacOSの場合)。

問題はその下、// Do POSIX-specified rates first.とコメントにある通り、まずvalue_がPOSIXに規定されているボーレートであるかをチェックし、それに一致していればPOSIXのマクロ(BNNN)を使ってボーレートを設定している。

その後、POSIX規定外のボーレートについては、BNNNの形式のマクロが定義されているときに限ってそれを使って設定する。

問題はここにあり、460800のようなPOSIX規定外のボーレートを設定しようとしても、B460800のようなマクロが定義されていないため、ASIOは内部でそのような指定を失敗とみなす(switchdefault節)。

環境によっては、termios関連のヘッダでそのようなマクロが定義されているので問題は起きないかもしれない。それがないと問題が起きる。

onihusubeonihusube

解決策は簡単で、定義してやれば良い。

// これを定義しておく
#define B460800 460800
#include <boost/asio.hpp>

namespace asio = boost::asio;

int main() {
  asio::io_context ioc{};
  asio::serial_port serial{ioc, "/dev/cu.usbserial-xxxxx"};
  boost::system::error_code ec{};

  // 失敗しなくなる
  serial.set_option(asio::serial_port_base::baud_rate(460800), ec);

  if (ec) {
    std::cout << ec.message() << std::endl;  // ここには来ない
  }
}

こうすると、所望のボーレートが設定可能となる。ただし、先ほどのserial_port_base::baud_rate::store()の実装を見てわかるように、そのswitch文で予め調べられているボーレートしかこの方法でも有効にならず、完全に任意の値が設定できるわけではない(多分困らないと思うけど)。

もう一つ注意として、B460800のようなマクロが定義されている環境だと多重定義とかになって別のコンパイルエラーが発生するかもしれない。

このスクラップは2021/12/16にクローズされました