赤外線リモコン信号の解析アプリケーション

以前に用意したアプリケーションのおまけに解析器を付けておきました。

リモコン信号の解析器

信号解析アプリケーション

メニューのInfra-redページで
下のコードをコピペすると確認できます。
(あるいは参考リンクからCSVファイルをダウンロードしてコードをコピペする)

手持ちのリモコン信号解析

Toshiba テレビ(電源)

5601AC00190015001800150019001400180015001800150019001400180040001800150018003F0019003F00170040001700410018003F0018003E00190014001800400018003F001800400018003F001700400018001600170016001800150018001500170016001800150019001500180015001700400019003E0019003F001700400018004205

赤外線リモコンカスタムコード表をさがしてみると, カスタムコードBF40で合っているようです。
ビットパターンを見ると3オクテット目11110000(lsb first), 4オクテット目00001111(lsb first)でビット列が反転しているのが確認できる。

Panasonic LEDシーリングライト (点灯普段)

8700400015000E0015000E00150030001400300015000E0015002F0015000E0015000E0015000F001400300015000E0015000E0015002F0015000E001400300015000F001400300014000F0015000E0015002F0015000E0015000E001400100014000F001400300015000E0015002F001400300015000E001500300014000F001300100014000F0015000E0015002F0014000F0013000F00150030001300100014000F0015004205

Panasonic LEDシーリングライト (消灯)

8800400015000E0015000E0015002F0015002F0014000F001400300015000B0018000E0015000E0015002F0015000F0014000F001400300014000F0015002F0015000E001400300014000F0014000F001400300015000E0015000E0015000E0015000E0015003000140030001400300015002F0015000E0015002F0015000F0014000F0014000F001400300015002F0015000E0015000E001500300014000F0014000F0014004205

AEHA(家製協)はよくわからない。
カスタムコード522c

Hitachiエアコン

1を選択してDownloadを押すと出てくるコード

画面を埋め尽くす情報量, エアコンリモコン信号は長い。
カスタムコード1001

Daikinエアコン

2を選択してDownloadを押すと出てくるコード

2フレーム構成。
ON / OFFを繰り返すプリアンブルのような信号があるね。

イーサネットのフレームはプリアンブルから始まる。これはLANに接続しているインターフェイスにフレーム送信の開始を認識させ、同期をとるタイミングを与えるための信号である。DIXイーサネットでは、サイズが8オクテット(64bit)のフィールドで、1と0が交互に続き、最後の1ビット(64bit目)が1で終わる。

プリアンブル

もちろんイーサネットとは関係が無いが。
10101010…のような信号をみると連想してしまう。

Panasonicエアコン

3を選択してDownloadを押すと出てくるコード

同じく2フレーム構成。
カスタムコード2002

解析プログラムのソースコード

InfraredCode.purs
コメント付きで415行, 動作を簡単に説明します。

型宣言

Count型

38kHzの時間での何カウントになるか

newtype Count = Count Int
derive newtype instance eqCount               :: Eq Count
derive newtype instance ordCount              :: Ord Count
derive newtype instance showCount             :: Show Count
derive newtype instance semiringCount         :: Semiring Count
derive newtype instance ringCount             :: Ring Count
derive newtype instance commutativeRingCount  :: CommutativeRing Count
derive newtype instance eucideanRingCount     :: EuclideanRing Count

比較するためにクラス Eq, クラス Ordのインスタンス
表示するためにクラス Showのインスタンス
四則演算するためにクラス Semiring, クラス Ring, クラス CommutativeRing, クラス EuclideanRingのインスタンスにしている。

Baseband型

パーサーからデコーダーに渡される中間表現であってON時間, OFF時間の配列。

type Pulse = {on :: Count, off :: Count}

-- |
newtype Baseband = Baseband (Array Pulse)
derive newtype instance eqBaseband    :: Eq Baseband
derive newtype instance showBaseband  :: Show Baseband

Bit型

1ビットを表現する。
1/0, Hi/Lo, true/false, 付勢/消勢, Assert/Negate
2値を表せたら表現は何でもよいんですけどね


赤外線リモコン信号回路には負論理信号が混ざるので, 今回はAssert/Negateでいきます。

data Bit
  = Negate
  | Assert

InfraredLeader

リーダー信号
AEHA / NEC / SIRC(SONY) / 不明のどれか

data InfraredLeader
  = LeaderAeha Pulse 
  | LeaderNec Pulse 
  | LeaderSirc Pulse 
  | LeaderUnknown Pulse 

InfraredLeader のコンストラクタ

ON時間 / OFF時間からリーダー信号を得る
それぞれの判定に使う時間は参考リンクにある通りで,
許容誤差の上限下限を 0.2ms にしておいた。

makeInfraredLeader :: Pulse -> InfraredLeader 
makeInfraredLeader = case _ of
  p | aeha p    -> LeaderAeha p
    | nec p     -> LeaderNec p
    | sirc p    -> LeaderSirc p
    | otherwise -> LeaderUnknown p
  where

  -- | upper lower tolerance 0.2ms
  typical = withTolerance {upper: Milliseconds 0.2, lower: Milliseconds 0.2}

  -- | H-level width, typical 3.4ms
  -- | L-level width, typical 1.7ms
  aeha :: Pulse -> Boolean
  aeha pulse =
    let on_   = typical (Milliseconds 3.4)
        off_  = typical (Milliseconds 1.7)
    in
    (Array.any (_ == pulse.on) on_) && (Array.any (_ == pulse.off) off_)

  -- | H-level width, typical 9.0ms
  -- | L-level width, typical 4.5ms
  nec :: Pulse -> Boolean
  nec pulse =
    let on_   = typical (Milliseconds 9.0)
        off_  = typical (Milliseconds 4.5)
    in
    (Array.any (_ == pulse.on) on_) && (Array.any (_ == pulse.off) off_)

  -- | H-level width, typical 2.4ms
  -- | L-level width, typical 0.6ms
  sirc :: Pulse -> Boolean
  sirc pulse =
    let on_   = typical (Milliseconds 2.4)
        off_  = typical (Milliseconds 0.6)
    in
    (Array.any (_ == pulse.on) on_) && (Array.any (_ == pulse.off) off_)

InfraredCode

赤外線リモコン信号。
正しい入力なら, 最後はこれに変換される。

data InfraredCode
  = Unknown (Array Bit)
  | AEHA {custom :: LsbFirst, parity :: LsbFirst, data0 :: LsbFirst, data :: Array LsbFirst, stop :: Bit}
  | NEC  {custom :: LsbFirst, data :: LsbFirst, invData :: LsbFirst, stop :: Bit}
  | SIRC {command :: LsbFirst, address :: LsbFirst}

関数

パーサー

参考リンクのADRSIR文書情報で定義された入力をデコーダーに渡す中間表現に変換する。

infraredHexStringParser:: Parser InfraredHexString Baseband
infraredHexStringParser = do
    arr <- Array.some (pulse <* skipSpaces)
    eof
    pure (Baseband arr)
  where

  pulse = do
    -- 入力値はon -> offの順番
    ton <- valueOf32Bit <?> "on-counts"
    toff <- valueOf32Bit <?> "off-counts"
    pure {on: Count ton, off: Count toff}

  valueOf32Bit = do
    -- 入力値はLower -> Higherの順番
    lower <- hexd16bit <?> "lower-pair hex digit"
    higher<- hexd16bit <?> "higher-pair hex digit"
    -- ここは普通の数字の書き方(位取り記数法: 高位が前, 下位が後)
    let str = higher <> lower
        maybeNum = Int.fromStringAs Int.hexadecimal str
    -- 入力値は検査済みなのでfromJustでよい
    pure (unsafePartial $ fromJust maybeNum)

  hexd16bit = do
    a <- hexDigit
    b <- hexDigit
    pure $ fromCharArray [ a, b ]

デコーダー

中間表現から赤外線リモコン信号に変換する
見て解るとおり Phase1, Phase2, Phase3 の3段階で砕く
<<< は関数合成演算子でHaskellの . ドット
traverse は Haskellの mapM
<=< はモナドの合成演算子でHaskellと同じ

decodeBaseband :: Baseband -> Either ProcessError (Array InfraredCode)
decodeBaseband =
  traverse (decodePhase3 <=< decodePhase2) <<< decodePhase1 

デコード第1段階

リモコンから複数フレームが送られる事があるので, 1段目は入力を各フレームに変換する。
OFF時間 8ms継続でフレームを切り離す。
8msの根拠は参考リンクにある通りAEHA規格であって, SIRC(SONY)規格でも有効

decodePhase1 :: Baseband -> Array (Array Pulse)
decodePhase1 (Baseband bb) =
  unfoldr1 chop bb
  where

  chop :: Array Pulse -> Tuple (Array Pulse) (Maybe (Array Pulse))
  chop xs = 
    case frames xs of
      { init: a, rest: [] } -> Tuple a Nothing
      { init: a, rest: b_ } -> Tuple a (Just b_)

  frames :: Array Pulse -> {init :: Array Pulse, rest :: Array Pulse}
  frames pulses = 
    let sep = Array.span (\count -> count.off < threshold) pulses
    in
    { init: sep.init <> Array.take 1 sep.rest
    , rest: Array.drop 1 sep.rest
    }

  threshold :: Count
  threshold = fromMilliseconds (Milliseconds 8.0)

デコード第2段階

入力フレームをリーダ部とビット配列にする

decodePhase2 :: Array Pulse -> Either ProcessError (Tuple InfraredLeader (Array Bit))
decodePhase2 tokens =
  case Array.uncons tokens of
    Just {head: x, tail: xs} ->
      let leader = makeInfraredLeader x
      in
      Right $ Tuple leader (demodulate leader xs)

    Nothing ->
      Left "Unexpected end of input"

デコード第3段階

AEHA / NEC / SIRC / 不明
それぞれ対応する関数で変換する

decodePhase3 :: Tuple InfraredLeader (Array Bit) -> Either ProcessError InfraredCode
decodePhase3 (Tuple leader bitarray) =
  evalState (runExceptT decoder) bitarray
  where

  decoder :: DecodeMonad ProcessError InfraredCode
  decoder = case leader of
    LeaderAeha _    -> decodeAeha
    LeaderNec _     -> decodeNec
    LeaderSirc _    -> decodeSirc
    LeaderUnknown _ -> decodeUnknown

デコード用モナド

この後で変数を使うので (これまではeitherモナド)
eitherモナドにstate モナドを結合したdecodeモナドを定義しておきます。

type DecodeMonad e a = ExceptT e (State (Array Bit)) a

もちろん runState / evalState の外側に変数を持ち出していませんよ。
純粋関数型言語ですもの。

デコード (AEHA)

カスタムコード(16ビットlsb first)
パリティ(4ビット lsb first)
data0(4ビット lsb first)
dataN(8ビット lsb first)
ストップビット

aehaProtocol :: DecodeMonad ProcessError InfraredCode
aehaProtocol = do
  custom <- takeBits 16 "fail to read: custom code (AEHA)"
  parity <- takeBits 4 "fail to read: parity (AEHA)"
  data_0 <- takeBits 4 "fail to read: data0 (AEHA)"
  data_N <- takeEnd "fail to read: data (AEHA)"
  let init = NEA.init data_N
      last = NEA.last data_N
      octets = toArrayNonEmptyArray 8 init
  pure $ AEHA { custom: LsbFirst custom
              , parity: LsbFirst parity
              , data0: LsbFirst data_0
              , data: map LsbFirst octets
              , stop: last
              }

デコード(NEC)

カスタムコード(16ビットlsb first)
data(8ビット lsb first)
dataの反転(8ビット lsb first)

necProtocol :: DecodeMonad ProcessError InfraredCode
necProtocol = do
  custom <- takeBits 16 "fail to read: custom code (NEC)"
  data__ <- takeBits 8 "fail to read: data (NEC)"
  i_data <- takeBits 8 "fail to read: inv-data (NEC)"
  stopbt <- takeBit "fail to read: stop bit (NEC)"
  pure $ NEC  { custom: LsbFirst custom
              , data: LsbFirst data__
              , invData: LsbFirst i_data
              , stop: stopbt
              }

デコード(SIRC)

コマンド(7ビットlsb first)
アドレス(5/8/13ビット lsb first)
ストップビット

sircProtocol :: DecodeMonad ProcessError InfraredCode
sircProtocol = do
  comm <- takeBits 7 "fail to read: command code (SIRC)"
  addr <- takeEnd "fail to read: address (SIRC)"
  pure $ SIRC { command: LsbFirst comm
              , address: LsbFirst addr
              }

さいごに

エンディアンネスが怪しい(資料不足と矛盾した情報の為)がたぶん合っていると思う。

入力文字列 → カウントの配列 → フレームの配列
→ ビット列 → 赤外線リモコン信号

入力文字列が各段階で変換される様子を見ているのが上のページ。
多めに関数シグネチャを書いたので, 入出力の変換はそれを見て。

プログラミングは

データの変換をするものだ


プログラミングElixir

このプログラムの動作はまさにコレ
(まあフロントエンドはPureScript単独で動いていますけどね。でもバックエンド側はElixir / Phoenixなので引用してみました。)

参考リンク

赤外線信号

赤外線リモコンの通信フォーマット
SB-Projects – IR Index
FAQ 1007798 : 赤外線リモコンの信号はどうなっているのですか?
FAQ 1006539 : リモコン・コードの受信/解析はどのように行うのか?
AN1064 – IR Remote Control Transmitter
Infrared Remote Control Implementation With MSP430FR4xx
STM32Cubeによる赤外線リモコンプロトコル用のトランスミッタとレシーバの実装

ADRSIR

メーカーによるソフトウェア
http://bit-trade-one.co.jp/support/download/
I2C仕様, リモコンコードCSV

リモコンデーター

1 赤外線データのON-OFF-ON-OFF…の時間を38kHzの時間での何カウントになるかを求めます
3.2msの場合は、3.2ms / 0.026ms(38kHz) = 123 (0x7B)
2 各カウント値を2バイトデータとして先頭からカウント値のLoバイト、Hiバイトの順番にデータを送信します
データ
送信データ長は、ON-OFFで1データとなります。よって、この場合のデータ長は8となります。
0x7B, 0x00, 0x3D, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x2E, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x0F, 0x00, 0x2E, 0x00, 0x0F, 0x00, 0x2E, 0x00, 0x0F, 0x00, 0x0F, 0x00


ADRSIR I2C仕様文書

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください