ArduinoとAnyEventを使って,モールス信号でSOS !

AnyEventでタイマー

YAPC::Asiaでの宮川さんの発表を聞いて

perl -MAnyEvent -le '
  map{
    $i = $_;
    my $c = AnyEvent->condvar;
    my $w; $w = AnyEvent->timer(
      after => $i,
      cb => sub {$w; print "OK"; $c->send});
      $c->recv
  }(0.1,0.5,1,2,0.1)'

なんてワンライナーで簡単なタイマーを作れる事を知りました.それで思いついたのがモールス信号,テキストを入力すると何かちかちかと点滅するものを作ってみようと思いました.まず,CPANでMorseをキーワードにして検索してみたところ,モジュールはいくつかでてきましたが,テキストを短点('-')と長点('ー')に変換するものばかりで,長点が短点の何倍の時間なのかなんてことはわかりませんでした.そこでwikipediaを探してみた所,詳しい説明が載っていました.

長点1つは短点3つ分の長さに相当し、各点の間は短点1つ分の間隔をあける。また、文字間隔は短点3つ分、語間隔は短点7つ分あけて区別する。

おまけに「符号化方式詳説」というのがあって,これを元にすることで簡単に符号化することができました.それが
以下のConverterモジュールです.

package Converter;

use Moose::Role;
use namespace::clean -except => 'meta';

requires 'output';

has 'encoding_rule' => (
    is         => 'rw',
    isa        => 'HashRef[Str]',
    lazy_build => 1,
);

sub _build_encoding_rule {
    return {
        A    => '[-_---_]',
        B    => '[---_-_-_-_]',
        C    => '[---_-_---_-_]',
        D    => '[---_-_-_]',
        E    => '[-_]',
        F    => '[-_-_---_-_]',
        G    => '[---_---_-_]',
        H    => '[-_-_-_-_]',
        I    => '[-_-_]',
        J    => '[-_---_---_---_]',
        K    => '[---_-_---_]',
        L    => '[-_---_-_-_]',
        M    => '[---_---_]',
        N    => '[---_-_]',
        O    => '[---_---_---_]',
        P    => '[-_---_---_-_]',
        Q    => '[---_---_-_---_]',
        R    => '[-_---_-_]',
        S    => '[-_-_-_]',
        T    => '[---_]',
        U    => '[-_-_---_]',
        V    => '[-_-_-_---_]',
        W    => '[-_---_---_]',
        X    => '[---_-_-_---_]',
        Y    => '[---_-_---_---_]',
        Z    => '[---_---_-_-_]',
        '.'  => '[-_---_-_---_-_---_]',
        ','  => '[---_---_-_-_---_---_]',
        '/'  => '[---_-_-_-_---_]',
        ':'  => '[---_---_---_-_-_-_]',
        '\'' => '[-_---_---_---_---_-_]',
        '-'  => '[---_-_-_-_-_---_]',
        '?'  => '[-_-_---_---_-_-_]',
        '!'  => '[-_-_---_---_-_]',
        '@'  => '[-_-_-_---_-_---_]',
        '+'  => '[-_---_-_---_-_]',
        0    => '[---_---_---_---_---_]',
        1    => '[-_---_---_---_---_]',
        2    => '[-_-_---_---_---_]',
        3    => '[-_-_-_---_---_]',
        4    => '[-_-_-_-_---_]',
        5    => '[-_-_-_-_-_]',
        6    => '[---_-_-_-_-_]',
        7    => '[---_---_-_-_-_]',
        8    => '[---_---_---_-_-_]',
        9    => '[---_---_---_---_-_]',
        ' '  => '[__]',
    };
}

sub encode {
    my $self = shift;
    my $str  = $self->{plain_text};
    $str =~ s/(\w)/uc($1)/eg;
    $str =~ s/(.)/$self->encoding_rule->{$1}/eg;
    return $str;
}

1;

これを使って,コンソール上で短点と長点を表示することができるようになりました.

Arduinoを使ってLED出力

最初はタイマーを使って,コンソール上に'-'と'ー'を出力しようかと思っていたのですが,考えただけでも非常に地味です.やる気が落ちて来るほど.そこでArduinoを使ってLEDを点滅させることにしました.まず,Arduinoを使って,port13とGNDに脚を刺したLEDを点滅させるprocessingのコードは以下のようになります.

void setup()   {
  Serial.begin(9600);
  pinMode(13, OUTPUT);    
}

void loop()                    
{
  if (Serial.available() > 0) {
    int inByte = Serial.read();
    if (inByte == 'I') {
      digitalWrite(13, HIGH);
    }
    if (inByte == 'O') {
      digitalWrite(13, LOW);
    }
  }
}

これをコントロールするperlワンライナーは例えば次のように(macの場合)なります.

perl -MDevice::SerialPort -le '
  $p = Device::SerialPort->new("/dev/tty.usbserial-A6008iod");
  $p->baudrate(9600);
  $p->databits(8);
  $p->parity("none");
# $p->stopbits(1);
  $p->write("I")'

以前試した時にはstopbits(1)を入れないと動かなかったはずなんですが,今回Arduino017で動かしてみたら,外さないと動かなくなってました.理由はよくわかりません.それはともかく,これを元に作ってみたコードは以下のようになりました.

package Morse;
use Moose;
use AnyEvent;
use Device::SerialPort;

with 'Converter';

has 'plain_text' => (
    is       => 'rw',
    isa      => 'Str',
    required => 1,
);

has 'morse_code' => (
    is         => 'rw',
    isa        => 'Str',
    lazy_build => 1,
);

has 'port' => (
    is         => 'rw',
    isa        => 'Device::SerialPort',
    default    => sub {Device::SerialPort->new("/dev/tty.usbserial-A6008iod")},
);

sub _build_morse_code {
    my $self = shift;
    return $self->encode;
}

__PACKAGE__->meta->make_immutable;

no Moose;

sub output {
    my $self = shift;
    my $str  = $self->morse_code;
    
    $self->port->baudrate(9600);
    $self->port->databits(8);
    $self->port->parity("none");
    $str =~ s/[\[\]]/_/sg;
    $str =~ s/(-+)/I$1E/g;
    $str =~ s/(-+)/length($1)/eg;
    $str =~ s/(_+)/O$1E/g;
    $str =~ s/(_+)/length($1)/eg;
    my @timer = split /E/, $str;

    map {
        my ( $state, $time ) = $_ =~ m/(.)(.)/;
        my $c = AnyEvent->condvar;
        $self->port->write($state);
        my $w;
        $w = AnyEvent->timer(
            after => $time * 0.1,
            cb    => sub {
                undef $w;
                $c->send;
            }
        );
        $c->recv
    } @timer;
}

1;

これらのモジュールを使って動かすスクリプトは簡単で,例えばこんな感じになります.

package main;
$a = Morse->new( plain_text => "sos" );
$a->output;

動かしてみると,「ちゃっ,ちゃっ,ちゃっ,ちー,ちー,ちー,ちゃっ,ちゃっ,ちゃっ」とLEDが光ります.

まとめ

当初はAcme::MorseCordeとかってモジュールにしてアップしようと思っていたのですが,連休中に作れなかったので挫折してます.ついでに言うと,Mooseの使い方がいまいちよくわからないので,色々変更した方がいいように思ってます.それから,上のスクリプトは一つのファイルにして動かすように作っています.つまり,以下のようになります.これからの展望としては,LEDの灯りをセンサーで読み込んでそれを文字列に変換するプログラムを作ってみたいんですが,うまい方法をまだ思いついていません.そのうちやってみたいと思っています.ま,その前にArduinoでセンサーを動かしてAD変換する方法を調べないといけないんですよね…

package Converter;

use Moose::Role;
use namespace::clean -except => 'meta';

requires 'output';

has 'encoding_rule' => (
    is         => 'rw',
    isa        => 'HashRef[Str]',
    lazy_build => 1,
);

sub _build_encoding_rule {
    return {
        A    => '[-_---_]',
        B    => '[---_-_-_-_]',
        C    => '[---_-_---_-_]',
        D    => '[---_-_-_]',
        E    => '[-_]',
        F    => '[-_-_---_-_]',
        G    => '[---_---_-_]',
        H    => '[-_-_-_-_]',
        I    => '[-_-_]',
        J    => '[-_---_---_---_]',
        K    => '[---_-_---_]',
        L    => '[-_---_-_-_]',
        M    => '[---_---_]',
        N    => '[---_-_]',
        O    => '[---_---_---_]',
        P    => '[-_---_---_-_]',
        Q    => '[---_---_-_---_]',
        R    => '[-_---_-_]',
        S    => '[-_-_-_]',
        T    => '[---_]',
        U    => '[-_-_---_]',
        V    => '[-_-_-_---_]',
        W    => '[-_---_---_]',
        X    => '[---_-_-_---_]',
        Y    => '[---_-_---_---_]',
        Z    => '[---_---_-_-_]',
        '.'  => '[-_---_-_---_-_---_]',
        ','  => '[---_---_-_-_---_---_]',
        '/'  => '[---_-_-_-_---_]',
        ':'  => '[---_---_---_-_-_-_]',
        '\'' => '[-_---_---_---_---_-_]',
        '-'  => '[---_-_-_-_-_---_]',
        '?'  => '[-_-_---_---_-_-_]',
        '!'  => '[-_-_---_---_-_]',
        '@'  => '[-_-_-_---_-_---_]',
        '+'  => '[-_---_-_---_-_]',
        0    => '[---_---_---_---_---_]',
        1    => '[-_---_---_---_---_]',
        2    => '[-_-_---_---_---_]',
        3    => '[-_-_-_---_---_]',
        4    => '[-_-_-_-_---_]',
        5    => '[-_-_-_-_-_]',
        6    => '[---_-_-_-_-_]',
        7    => '[---_---_-_-_-_]',
        8    => '[---_---_---_-_-_]',
        9    => '[---_---_---_---_-_]',
        ' '  => '[__]',
    };
}

sub encode {
    my $self = shift;
    my $str  = $self->{plain_text};
    $str =~ s/(\w)/uc($1)/eg;
    $str =~ s/(.)/$self->encoding_rule->{$1}/eg;
    return $str;
}

1;

package Morse;
use Moose;
use AnyEvent;
use Device::SerialPort;

with 'Converter';

has 'plain_text' => (
    is       => 'rw',
    isa      => 'Str',
    required => 1,
);

has 'morse_code' => (
    is         => 'rw',
    isa        => 'Str',
    lazy_build => 1,
);

has 'port' => (
    is         => 'rw',
    isa        => 'Device::SerialPort',
    default    => sub {Device::SerialPort->new("/dev/tty.usbserial-A6008iod")},
);

sub _build_morse_code {
    my $self = shift;
    return $self->encode;
}

__PACKAGE__->meta->make_immutable;

no Moose;

sub output {
    my $self = shift;
    my $str  = $self->morse_code;
    
    $self->port->baudrate(9600);
    $self->port->databits(8);
    $self->port->parity("none");
    $str =~ s/[\[\]]/_/sg;
    $str =~ s/(-+)/I$1E/g;
    $str =~ s/(-+)/length($1)/eg;
    $str =~ s/(_+)/O$1E/g;
    $str =~ s/(_+)/length($1)/eg;
    my @timer = split /E/, $str;

    map {
        my ( $state, $time ) = $_ =~ m/(.)(.)/;
        my $c = AnyEvent->condvar;
        $self->port->write($state);
        my $w;
        $w = AnyEvent->timer(
            after => $time * 0.1,
            cb    => sub {
                undef $w;
                $c->send;
            }
        );
        $c->recv
    } @timer;
}

1;

package main;
$a = Morse->new( plain_text => "sos" );
$a->output;

Arduino面白いですよ.