せっかなくので、やってみた。

日々のあんなこと、こんなこと、せっかくなのでやってみた

X68のレースゲーム OverTakeでネットワーク対戦してみた。(後編)

この記事は令和の時代もなお続くX68000用ディスクマガジン「ハリキリマガジン」の 38号(2019年7月)に収録していただいた内容に修正を加えたものです。文章が長くなったので、前編・後編に分割しています。

こちらの記事の続きです。
X68のレースゲーム OverTakeでネットワーク対戦してみた。(前編) - せっかなくので、やってみた。

ついにネット対戦

いよいよOverTakeの対戦を試します。手順は省略しますが、事前にSerial2Tcpなしの状態で通信対戦できることを確認済みです。
(XM6の完成度の高さに、改めて感動です!)
先ほどのSerial2Tcpを起動した構成でOverTakeを立ち上げ、2playersで対戦を行うと...
"not connected"が表示されて対向を認識してくれませんでした。

この後かなりの期間、Null-modem emulatorの設定やXM6の設定を変えては失敗を繰り返し、手詰まりの状態がしばらく続きました。
USBシリアルコンバータを購入し、x68実機(REDZONE)との接続も試してみましたが、繋がる事はありませんでした。
試している際中、Serial2Tcpのログ出力を見ると送受信しているデータの内容がボーレートによって変わるので、おそらくボーレートを合わせなければ 正しく通信できないだろうという事は分かったのですが、9600や19200、38400などの速度も試しても、どれもダメでした。
実際の通信設定はゲームプログラムを解析しない限り分かるはずもありません。
そもそもUSB-シリアルによってはDSR/DTRの信号が省略されている?とかの情報もあり、繋がらない原因がどこにあるのか つかめませんでした。(結局USBシリアルコンバータも3本購入することに...)

半ばあきらめかけているなかで、たまたまXM6のモニター機能でSCCの状態を表示したところ、 BaudRateの項目がx68起動直後の状態では1201、OverTakeが起動した状態では31250と表示が変わる事に気づきました。
(表示>デバイス>シリアルコミュニケーションコントローラ で表示できます。)

f:id:moneci:20191116174254p:plain:w300
ボーレート表示

どちらも見慣れない数値なので、実際のボーレートとは異なる値が表示されているのだろう思っていましたが、 試しに31250をSerial2Tcpのボーレートに設定してみたところ、なんと対戦モードが起動しました!!
どうもこの31250という値はMIDI機器を接続する時のスピードとしてよく使われるらしく、実際のボーレートである事に 気づきませんでした。
(ちなみにこの値はSWITCH.Xでは指定できませんし、9600を指定した時もSCCの表示では19531と表示されたり、値の関係性がよくわかりませんでした。)

しかしXM6のこの機能が無ければ、実際の通信設定を知る事はできませんでした。制作者様に感謝です!!
この後、実機とエミュレータでの接続も試し、

REDZONE - USBシリアルコンバータ - Windows10(Serial2TcpServer - Serial2TcpClient) - Null-modem(COM8) - TypeG(COM4)

の構成でも無事通信対戦する事ができました。 *1
(USBシリアルコンバータはラトックシステムのREX-USB60Fを使用しました。)

ラトックシステム USBシリアルコンバータ REX-USB60F

ラトックシステム USBシリアルコンバータ REX-USB60F

  • 発売日: 2005/06/20
  • メディア: Personal Computers

で、RasPiはどうなったの?

ここまででx68実機とx68エミュレータを、COMポートエミュレータ、USBシリアルコンバータを介してネットワーク対戦させることができました。
ただ、全てWindowsノートにつなげての確認だったので、当初の目的であるRasPiを使用していませんし、 ネットワークといいつつも、1台のPC内での動作なのでネットワーク感がありません。
RasPiでJava、USBシリアルコンバータが動作することはネットの情報で確認済みなので、後はインストールして試すだけです。

RasPiにログインし、以下でJava、シリアル通信ライブラリをインストールします。
ちなみにRasPiはRetroPiとして使用しているので、動作しているOSはRaspbian Stretch(RetroPi4.4)です。

# sudo apt-get install oracle-java8-jdk
# sudo apt-get install librxtx-java

USBシリアルコンバータのドライバも標準でサポートされているので、挿せば/dev/ttyUSB0として認識されました。

次にUSBシリアルコンバータのシリアル側とREDZONEをクロスケーブルで接続します。構成は以下のようになりました。

REDZONE - USBシリアルコンバータ - RasPi(Serial2TcpServer) - LAN - Windows10(Serial2TcpClient) - Null-modem(COM8) - TypeG(COM4)

RasPi側では以下でSerial2Tcpサーバを起動し、WindowsではSerial2TcpクライアントとTypeGを起動後、毎度のtype auxで疎通確認します。

# export LD_LIBRARY_PATH=/usr/lib/jni
# java -cp "./bin:/usr/share/java/RXTXcomm.jar" serial2tcp.Serial2TcpServer 9999 /dev/ttyUSB0 31250

ところが、実機からTypeGへの送信はできるものの、TypeGから実機への送信はできませんでした。(当然OverTakeも動作しませんでした)
REDZONE側のSWITHC.XとSerial2Tcpでボーレートを38400を設定しても、動作は変わりませんでした。
しかし、ボーレートを9600に変更すると、双方から送受信できたので、ここでも通信速度の問題の可能性があります。
WindowsとUSBシリアルコンバータの組み合わせでは31250でも動作していたので、RasPiとUSBシリアルコンバータ特有の問題かもしれません。

そこで"raspi 31250"で検索すると、midi関連の情報が出てきました。
どうもRasPiは31250というボーレートに標準では対応していない様で、みなさんmidi機器との接続で困っている感じです。
その中で有力そうな情報である、 /boot/config.txt の設定を変更する方法ではOSバージョンの違いの為か、成功しませんでした。 今度はLinuxのシリアルドライバである"ftdi_sio 設定"で検索すると大変参考になりそうな記事が。

qiita.com

qiita.com

なかなか難しい内容ですが、ボーレートを38400に設定し、RasPiのシリアルのベースボーレートを割り算して31250になる値(divisor)を setserialコマンドで指定するとよいみたいです。(カーネルの再コンパイルまでは手が出せないのでやっていません)
まずはsetserialをインストールして現在のベースクロックを確認。

# sudo apt-get install setserial
# sudo setserial -a /dev/ttyUSB0
/dev/ttyUSB0, Line 0, UART: unknown, Port: 0x0000, IRQ: 0
        Baud_base: 24000000, close_delay: 0, divisor: 0
        closing_wait: infinite
        Flags: spd_normal

Baud_base: 24000000と出ました。24000000÷31250=768で、divisorに設定する値がピッタリ整数で導きだせました。 もしかしたらいけるかもしれません。次のコマンドでttyUSB0にボーレートとdivisorを設定します。

# sudo stty -F /dev/ttyUSB0 38400
# sudo setserial /dev/ttyUSB0 spd_cust divisor 768

再度確認します。正しく反映された様です。

# sudo setserial -a /dev/ttyUSB0
/dev/ttyUSB0, Line 0, UART: unknown, Port: 0x0000, IRQ: 0
        Baud_base: 24000000, close_delay: 0, divisor: 768
        closing_wait: infinite
        Flags: spd_cust

それではOverTakeを起動して確認してみます。
すると...遂に動きました!!!とうとうRasPiを使ってのネットワーク対戦成功です!
こうして、かなり苦労したものの、RasPiでの対戦も何とか実現にたどり着く事ができました。

動作の様子はこちら
youtu.be

ちなみに、ネットワーク対戦の確認で、実機を16MHz、TypeGを10MHzで動かしたときに実機が先にゴールしたにも 関わらずTypeG側が勝利となった事がありました。
TCP変換も入っているし、実機とエミュレータの違いもあるので仕方ないかなーと思っていましたが、 OverTakeのシステムディスクに収録さていたREADME.DOCに以下の様な記述を発見しました。

『あと、通信プレイ時には必ず動作環境の統一(*)を、くれぐれもおねがいいたします。

(*)
 画面モード・BGMの有無等の設定の差で、画面上の見た目と内部的な処理との間にタイミング差が 生じて、実際の勝敗の内容が変わってしまう事があります。この場合は表示されているトータルタイム にて勝敗結果を判断して下さい。
 けんかしないでね。』

との事なので、CPUクロックの違いなどでレース結果が正しくなくなるかもしれません。
一応CPUクロックを合わせた場合は、抜きつ抜かれつの場面でも問題なく動作しているように見えました。 (何分一人で試しているので、あまり動作検証はできておりません...)
いずれにしても、Serial2Tcpを入れたことによって、挙動が変わっている可能性がありますので、ご了承ください。

最後に

以上の様な感じで、数年前から「もしかしたらできるんじゃないか」と考えていたことが、 やっと実現できました。技術的にできたというよりは、動作環境等を調べまくって(ググって)の成果なので あまり自慢はできませんが、あきらめずに粘り強くトライしての結果なので、本当に嬉しい限りです。

そしてここまで書いておきながらではありますが、この試みを始めた本当の理由は、ジオグラフシールの ネットワーク対戦をやってみたい為だったのです。
ジオグラフシールの方はSerial2Tcpを使わない状態でも接続する事ができず、この方法自体がだめだかと 考えていたのですが、代わりに試したOverTakeがエミュレータ上で動作したので、最後まで試した次第です。
(ジオグラフシールの場合は、本物のRS232C同士で接続しないと、通信できないのかもしれません。)

この仕組みをさらに発展させて、いつかはインターネット経由での「誰とでも対戦」が実現できればなーとか、 対戦以外でも、例えばMIDI機器のネットワーク化など他の用途にも応用もできるかもしれませんので、 暇を見て引き続きトライしてみたいと思います。

最後に、XM6本家のPI.氏、XM6TyepGのGIMONS氏、XM6iのisaki氏、Null-modem emulator (com0com)開発者様に 感謝申し上げます。これらの素晴らしいソフトウェアにより今回の試みを実現する事ができました。
ありがとうございました。

動作確認に使用した環境

[Windows環境]

  • Windows10 Pro 1803
  • Java 1.8.0_192-b12
  • rxtx-2.2pre2
  • com0com 3.0.0.0
  • XM6 TyepG version 3.30 L35
  • XM6i version 0.55

[Raspberry Pi環境]

  • Raspberry Pi 2 Model B
  • Retropie4.4(Linux retropie 4.14.52-v7+ / Raspbian Stretch)
  • Java 1.8.0_65-b17
  • rxtx-2.2pre2-13

[USBシリアルコンバータ(接続OKなもの)]

ラトックシステム USBシリアルコンバータ REX-USB60F

ラトックシステム USBシリアルコンバータ REX-USB60F

  • 発売日: 2005/06/20
  • メディア: Personal Computers

  • その2 FTDI社のチップを使用したケーブル (たしか秋月で購入したはずだが同じものか自信なし。)
    akizukidenshi.com

[USBシリアルコンバータ(接続NGなもの)]

https://www.amazon.co.jp/gp/product/B00QUZY4UG/ref=ppx_yo_dt_b_asin_title_o03_s00?ie=UTF8&psc=1

※Cannot set serial info: Inappropriate ioctl for deviceのエラーでsetserialコマンドが動作しない

*1:本文では説明を省略しておりましたが、USBシリアルコンバータはCOMポートを増設する趣旨のものなので、 x68実機への接続する際は、シリアルのクロスケーブルをはさむ必要があります。

X68のレースゲーム OverTakeでネットワーク対戦してみた。(前編)

この記事は令和の時代もなお続くX68000用ディスクマガジン「ハリキリマガジン」の 38号(2019年7月)に収録していただいた内容に修正を加えたものです。文章が長くなったので、前編・後編に分割しています。

はじめに

OverTakeはZoomから1992年に発売された、F1レースゲームです。
当時、Zoomから発売されるレースゲーム、しかもセガのスーパーモナコGPナムコファイナルラップの様な 疑似3D表示や、チーム、ドライバーが実名で登場するなど気合の入った内容で、雑誌に掲載されていた 開発中の画面からも期待度がかなり高く、発売と共にすぐに買った記憶があります。
実際のゲームの方の感想は今回の題材から外れるので言及しませんが、サウンドに関しては一聴の価値ありなソフトです。 (一応エンディングまでプレイした気もしますが、覚えておりません...)

さて、このOverTakeにはシリアル(RS232Cのクロス)ケーブルでx68同士を接続することにより、 対戦プレイができるモードがあります。
ただ、対戦をするには実機だけでなく、モニターも2台必要となり、ハードウェア環境のハードルが高くて ほとんど試したことがありませんでした。

現在はXM6系のRS232Cに対応したエミュレータもあり、気軽に2台のx68でOverTakeを動かす環境が手に入りました。
しかし実機やエミュレータRS232Cで接続して対戦させるだけではつまらないので、昨今x68関連でも 賑わいのあるRaspberryPiを使用してネットワーク対戦ができるんじゃないか?という事で、実際に試してみました。

f:id:moneci:20191116181024p:plain
ひとりで対戦している様子

どうやってネットワーク対戦させるか

RS232Cで接続された機器同士が通信する際、機器独自に決められた手順でデータの 送信/受信を繰り返すので、RS232Cに送信されたデータをプログラムで受け取り、そのままTCP/IPに流して、 対向のTCP/IPで受け取ったデータを今度はRS232Cに送る事ができれば実現できるのではと安易に考ました。

機器同士はボーレートなどの通信設定を合わせないと通信できないというのは理解していましたが、 この時点では深く考えずにとりあえず x68とRasPiをRS232Cで接続し、RasPiの中でシリアル-TCP/IPを 変換するソフトを動かすという事で進めてみました。
RasPiのGPIOは分からないので、x68とRasPiとの接続はお手軽なUSBシリアルコンバータを使用することにします。

シリアル-TCP/IPを変換するソフトの部分は、実はハードウェアでこれを実現するシリアル-LAN変換アダプタなるものが販売されているのですが、 業務用で数万円もする高価なものなので、今回の為にわざわざ購入するという判断はありませんでした。
TCP/IPRS232Cの通信はJavaで少しやった事があったので、この部分をプログラムでという目論見ですが、 高価なハードでやっている事をソフトで本当にできるのか?という疑問もあります。USBシリアルコンバータも本物のシリアルと挙動の違いがあるかもしれません。
RasPiが1台あるし、まあお試しでプログラムを組むのはタダなので、まずは検証してみることにしました。

とりあえずつなげてみる

ネットに落ちているプログラムを参考にサーバとクライアントのプログラムを作成しました。 名前は安易にSerial2Tcpです。
COMポートをオープンしたあと、TCPソケットをオープンし、対向からのデータを待ちます。
COMポートから受信したデータをTCPソケットに、TCPから受信したデータをCOMポートに送信するという単純なものです。 通信設定はとりあえず、9600bps、データビット8、ストップビット1、パリティなし、フロー制御なしに設定しました。

いざ確認ですが、RasPiは1台しか持ってないのと、この時点ではUSBシリアルコンバータも買っていなかったのでx68含め全て Windows10上のエミュレータで試してみました。
x68のエミュレータにはRS232Cに対応しているXM6の改造版であるXM6TypeGを使用しますが、 TypeGは同時に2つ起動する事はできないので、対向のx68には同じくXM6の改造版であるXM6iを使用しました。
また、TyepGとXM6iを接続させるCOMポートにはNull-modem emulatorを使用しました。
これはWindows上で仮想のCOMポートのペアを作成し、1台のWindowsPCの中だけでシリアル通信を確認できる便利なものです。

Null-modem emulator - Browse /com0com/3.0.0.0 at SourceForge.net

からcom0com-3.0.0.0-i386-and-x64-signed.zipをダウンロードし、COM4、COM8のペアを作成しました。 後で見つけた記事ですが、以下のサイトに手順が分かりやすく解説されています。 qiita.com

ご注意 上記の記事でも説明が追記されていますが、Windows10でcom0comは利用できなくなっています。代替の手段はこちら。→ *1

まずは、Serial2Tcpなしの状態で接続を確認します。
TypeGを起動し、ツール>オプション>ポート を開き、 ポートでCOM4を選択し、「ボーレートを38400bpsに固定する」はよく分からないのでチェックなしにしてから、 Human68Kのシステムディスクを入れてリセットします。XM6iも同様の手順でポートにCOM8を設定します。

これで2台のx68がクロスケーブルで接続された状態になります。
それぞれSWITCH.Xを起動し、Serial2TcpのCOMポート設定と同じ 「9600ボー、8ビット、パリティなし、ストップ1、None」を設定しておきます。

x68のどちらか(受信側)で

A> type aux

を実行し、対向のx68(送信側)から

A> echo Hello > aux

を実行すると、受信側のtype auxの次の行に

Hello

と出力されました。
今度は受信側と送信側の手順を逆にして試します。
(ちなみに、type auxを終了する事が出来ないので、いつもCOPYキーを押してプリンタ割込みで終了させています。)

これでエミュレータ上でのシリアル通信が確認できました。
次はいよいよシリアル-TCP/IP変換を試します。
新たにNull-modem emulatorでCOM5-COM9のペアを作成しておき、接続の構成は以下の様にしました。

TypeG(COM4) - Null-modem(COM8) - Serial2TcpServer - Serial2TcpClient - Null-modem(COM9) - XM6i(COM5)

TypeGとXM6iのCOMポートの割り当ても、TypeG=COM4、XM6i=COM5に変更しておきます。

まず、

Download - Rxtx

からrxtx-2.2pre2-bins.zipをダウンロードし、RXTXComm.jarと環境にあったrxtxSerial.dllを 適当な場所(プロジェクトフォルダ/lib など)にコピーし、コピー先のディレクトリを環境変数%PATH%に追加します。

次にコマンドプロンプトを2つ起動し、以下のコマンドでサーバを起動します。
パラメータは ポート番号、シリアルポート名、ボーレートです。

java -cp ".\bin;.\lib\RXTXcomm.jar" serial2tcp.Serial2TcpServer 9999 COM8 9600

もう一つのコマンドプロンプトで同様にrxtxSerial.dllへのパスを通した後、以下でクライアントを起動します。
パラメータは サーバ名(IP)、ポート番号、シリアルポート名、ボーレートです。

java -cp ".\bin;.\lib\RXTXcomm.jar" serial2tcp.Serial2TcpClient localhost 9999 COM9 9600

無事接続できれば、サーバ側に"accept"が出力されます。
これで準備完了です。先ほどと同様にx68のどちらか(受信側)で

A> type aux

を実行し、対向のx68(送信側)から

A> echo Hello > aux

を実行すると...
動きました!ばっちりです。Serial2Tcpのログ出力にも、送受信した文字コードが表示されています。
数万円もするハードでやっていることが簡単なプログラムで実現できました。

ということで長くなったのでとりあえずここまで。
Serial2Tcpのソースを張り付けて、続きは次の記事へ。

yatte-mita.hateblo.jp

BaseSerial.java

package serial2tcp;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import gnu.io.CommPort;
import gnu.io.CommPortIdentifier;
import gnu.io.SerialPort;
import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;

/**
 * シリアル通信を処理する為の基底クラスです
 */
public class BaseSerial {

    /** シリアルポートオブジェクト */
    private SerialPort serialPort;

    /** TCPからの入力データのストリーム */
    protected InputStream tcpIn;

    /** TCPへの出力データのストリーム */
    protected OutputStream tcpOut;

    /** シリアルポートからの入力データのストリーム */
    private InputStream serialIn;

    /** シリアルポートへの出力データのストリーム */
    private OutputStream serialOut;

    /** データビット */
    private final static int SERIAL_DATABIT = SerialPort.DATABITS_8;

    /** ストップビット */
    private final static int SERIAL_STOPBIT = SerialPort.STOPBITS_1;

    /** パリティ */
    private final static int SERIAL_PARITY = SerialPort.PARITY_NONE;

    /** フロー制御 */
    //private final static int SERIAL_FLOW_CONTROL = SerialPort.FLOWCONTROL_XONXOFF_IN | SerialPort.FLOWCONTROL_XONXOFF_OUT; // ソフトフロー制御
    //private final static int SERIAL_FLOW_CONTROL = SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT; // ハードフロー制御
    private final static int SERIAL_FLOW_CONTROL = SerialPort.FLOWCONTROL_NONE; // フロー制御なし

    /** DTR有効か */
    private final static boolean SERIAL_ENABLE_DTR = false;

    /** RTS有効か */
    private final static boolean SERIAL_ENABLE_RTS = false;

    /**
     * シリアルポートに接続します。
     * @param portName
     * @param speed
     * @throws Exception
     */
    protected void connect(String portName, int speed) throws Exception {
        CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
        if (portIdentifier.isCurrentlyOwned()) {
            System.out.println("Error: Port is currently in use");
        } else {
            CommPort commPort = portIdentifier.open(this.getClass().getName(), 2000);
            if (commPort instanceof SerialPort) {
                serialPort = (SerialPort) commPort;
                serialPort.setSerialPortParams(speed, SERIAL_DATABIT, SERIAL_STOPBIT, SERIAL_PARITY);
                serialPort.setFlowControlMode(SERIAL_FLOW_CONTROL);
                serialIn = serialPort.getInputStream();
                serialOut = serialPort.getOutputStream();
                // Buffered系では正しくデータ通信できなかった
                //serialIn = new BufferedInputStream(serialPort.getInputStream());
                //serialOut = new BufferedOutputStream(serialPort.getOutputStream());
                System.out.println("connected to " + portName + " speed: " + speed);

                // DTR/RTS設定
                serialPort.setDTR(SERIAL_ENABLE_DTR);
                serialPort.setRTS(SERIAL_ENABLE_RTS);
            } else {
                System.out.println("Error: Only serial ports are handled by this example.");
            }
        }
    }

    /**
     * シリアルポートのlistenを開始
     * @throws Exception
     */
    protected void runConnection() throws Exception {
        // シリアルポート書込み用のスレッド開始
        (new Thread(new SerialWriter())).start();
        serialPort.addEventListener(new SerialReader());
        serialPort.notifyOnDataAvailable(true);
    }

    /**
     * シリアルポートからデータを受信した時に起動するリスナーです。
     */
    private class SerialReader implements SerialPortEventListener {
        @Override
        public void serialEvent(SerialPortEvent event) {
            // イベントの種類で処理を分ける(実際に動かしたところ、DATA_AVAILABLE以外のイベントは発生しなかった。)
            switch (event.getEventType()) {
            case SerialPortEvent.BI:
            case SerialPortEvent.OE:
            case SerialPortEvent.FE:
            case SerialPortEvent.PE:
            case SerialPortEvent.CD:
            case SerialPortEvent.CTS:
            case SerialPortEvent.DSR:
            case SerialPortEvent.RI:
            case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
                // Data Available 以外のイベントは処理しない
                System.out.print("not Data Available event " + event.getEventType() + "\n");
                break;
            case SerialPortEvent.DATA_AVAILABLE:
                // Data Available の処理
                int data = 0;
                try {
                    while ((data = serialIn.read()) > -1) {
                        // Buffered系では正しくデータ通信できなかった
                        //String byteData = ByteBuffer.wrap(ByteBuffer.allocate(4).putInt(data).array()).order(ByteOrder.BIG_ENDIAN).toString();
                        //String byteData = ByteBuffer.allocate(4).putInt(data).toString();
                        //System.out.print("data received from serial in: " + byteData + "\n");
                        System.out.print("data received from serial in: " + data + "\n");
                        tcpOut.write(data);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    closeAndExit();
                }
                break;
            }
        }
    }

    /**
     * シリアルポートにデータを送信するスレッドです。
     */
    private class SerialWriter implements Runnable {
        public void run() {
            int data = 0;
            try {
                while ((data = tcpIn.read()) > -1) {
                    System.out.print("received from tcp in: " + data + "\n");
                    serialOut.write(data);
                }
            } catch (IOException e) {
                e.printStackTrace();
                closeAndExit();
            }
        }
    }

    /**
     * 各リソースを閉じて終了します。
     */
    private void closeAndExit() {
        try {
            if (tcpIn != null) tcpIn.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if (tcpOut != null) tcpOut.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if (serialIn != null) serialIn.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if (serialOut != null) serialOut.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (serialPort != null) serialPort.close();

        System.exit(-1);
    }
}

Serial2TcpServer.java

package serial2tcp;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * シリアル-TCP変換のサーバです。
 */
public class Serial2TcpServer extends BaseSerial {

    /** サーバソケット */
    private ServerSocket server;

    /** クライアントソケット */
    private Socket socket;

    /**
     * メインルーチン
     * @param args ソケットのポート番号, COMポート名, シリアルの通信速度
     */
    private void start(String[] args) {
        try {
            // パラメータチェックは省略
            int tcpPort = Integer.parseInt(args[0]);
            String comPort = args[1];
            int speed = Integer.parseInt(args[2]);

            connect(comPort, speed);
            initSocket(tcpPort);
            runConnection();
            while (true) {
                // 空ループ
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        } finally {
            try {
                if (socket != null) socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (server != null) server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * サーバソケットを作成し、クライアントからの接続を待ちます。
     * @param port
     * @throws IOException
     */
    private void initSocket(int port) throws IOException {
        System.out.println("wating for client connection...");
        server = new ServerSocket(port);
        // クライアントからの通信開始要求が来るまで待機
        socket = server.accept();
        tcpIn = socket.getInputStream();
        tcpOut = socket.getOutputStream();
        System.out.println("accept");
    }

    public static void main(String[] args) {
        Serial2TcpServer server = new Serial2TcpServer();
        server.start(args);
    }
}

Serial2TcpClient.java

package serial2tcp;

import java.io.IOException;
import java.net.Socket;

/**
 * シリアル-TCP変換のクライアントです。
 */
public class Serial2TcpClient extends BaseSerial {

    /** クライアントソケット */
    private Socket socket = null;

    /**
     * メインルーチン
     * @param args サーバ名, ソケットのポート番号, COMポート名, シリアルの通信速度
     */
    private void start(String[] args) {
        try {
            // パラメータチェックは省略
            String server = args[0];
            int tcpPort = Integer.parseInt(args[1]);
            String comPort = args[2];
            int speed = Integer.parseInt(args[3]);

            connect(comPort, speed);
            initSocket(server, tcpPort);
            runConnection();
            while (true) {
                // 空ループ
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        } finally {
            try {
                if (socket != null) socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * socketを作成し、サーバに接続します。
     * @param server
     * @param port
     * @throws Exception
     */
    private void initSocket(String server, int port) throws Exception {
        System.out.println("connect to server " + server + "...");
        socket = new Socket(server, port);
        tcpIn = socket.getInputStream();
        tcpOut = socket.getOutputStream();
    }

    public static void main(String[] args) {
        Serial2TcpClient client = new Serial2TcpClient();
        client.start(args);
    }

}

*1:あらかじめWindows8にcom0comをインストールしていて、Windows10にアップデート した場合は、引き続き使用できた様ですが、新規にWindows10をインストールしたPCでは、 ポートは作成できるものの、デバイスマネージャーで com0com のポートを確認すると、
「このデバイスに必要なドライバーのデジタル署名を検証できません。...」
の表示となり、ポートが認識されない状態となってしまいました。

代替の仮想シリアルポートを探したところ、以下のFree Virtual Serial Portsというソフトが利用できました

FREE Virtual Serial Ports driver, Rs-232 null modem emulator

有償版とフリー版があり、フリー版は

  • 作成した仮想シリアルポートを保存できない(起動時毎回作成)
  • 作成できるシリアルポートのペアは1組だけ

の制約があるため、XM6TypeGとXM6iを同時に利用することが できなくなってしまいましたが、実機とXM6TypeGの組み合わせでは問題なく動作しました。 (フリー版のバージョン4.02.00.0482を使用)

一応イギリスの企業が作っている様なので怪しい類ではないかと思いますが、 利用される方は自己責任でお願いします。

無謀にもX68000compactXVIのコンデンサを総取り換えした件を振り返ってみた

※この記事はコンデンサ取り換え素人の私が独自に調べてトライした内容です。
道具の使い方など正しくない可能性があるので、参考に作業される方はあくまで自己責任でお願いします。

思い起こせば2013年の夏の終わり。
X68000 compact XVI(REDZONE)が起動しなくなってから1-2年経った頃だと思う。
なんとか自分の手で復活させたい(修理代が捻出できない)との思いから、遂に奮起してコンデンサ総取り換えを決意しました。
X68のコンデンサは、アルミ電解コンデンサや面実装コンデンサと呼ばれている取り外しの難しいタイプで、 パターンを剥がすなど失敗すると、再起不能になる可能性があり、ずっと作業を躊躇していました。
そのまま放置してもコンデンサから漏れ出した電解液でパターンが腐食し、どちらにしても再起不能になるのは 時間の問題なので、とにかくやってみるしかないという思いでした。
半田ごてなど、中学の技術の授業以来、数えるほどしか使ったことないので、まずは道具をそろえました。

  • テスター
  • 半田ごて40W、23W一本ずつ
  • 小手先クリーナー
  • 半田吸い取り線
  • 銅線
  • 精密機械用半田
  • フラックスクリーナー
  • 練習用のジャンク基盤(秋葉原のジャンク屋で購入)

f:id:moneci:20190921233128j:plain:w300
この写真のみ今回追加で撮影しました。練習用基板は当時のものですがパターンは剥がしまくっていました…。ページの最後にamazonのリンクを貼っておきます。

新しいコンデンサは選定の知識がないので、修理業者の方から一式取り寄せました。
コンデンサの外し方をネットで調べたところ、色々テクニックがあるみたいで、両方の電極を半田ごてで温めて少しずつコンデンサをずらす方法、 銅線をU字型に加工して電極にのせて一本の半田で同時に温める方法、銀色のコンデンサ本体をニッパで切り取り、台座を取り外す方法(これは後から知った※1)、色々テクニックがあります。
とりあえず、ジャンクの基盤で練習。
半田が古いせいか、こてを当ててもなかなか溶けない。両手で両端に半田ごてあてると、コンデンサをずらす手が足りない(w)ので、U字の銅線を使ったりして、なんとか取り外し成功しました。

※1 本体をニッパで切り取るのは、台座ごともぎ取ってしまいそうで怖いですが、こちらに動画が挙がっていました。こちらを見る限りは一番作業しやすそうです。
表面実装コンデンサーの交換作業風景 - YouTube

2,3個練習していよいよ実機に取り掛かりますが、まずはその前の分解がしんどい。
compactタイプの筐体のねじは一本のみで、ねじを外したあとは、パズルの様に分解してゆきます。各パーツがプラスチックの爪で 固定されているので、折らないよう注意しながら外してゆきます。(それでもフロントパネルの爪を一カ所折ってしまった。)
f:id:moneci:20190921174159j:plain:w300 f:id:moneci:20190921174210j:plain:w300 f:id:moneci:20190921174220j:plain:w300 f:id:moneci:20190921174229j:plain:w300 f:id:moneci:20190921174238j:plain:w300 f:id:moneci:20190921174248j:plain:w300

こちらのサイトに非常に詳しく書かれていてとても参考になりました。

x68k-retro.cocolog-nifty.com 今もサイトがあって良かった!

取り外したメインボードはこんな感じ。コンデンサを取り付けるときに分かるように現状を撮影しておきます。
f:id:moneci:20190921174258j:plain:w300
f:id:moneci:20190921174503j:plain:w300 f:id:moneci:20190921174453j:plain:w300

U字の銅線はこんな感じで使いましたが、なかなか練習通りいきません。
半田が古いのか、こての使い方が下手なのか、思うように半田が溶けてくれません。

f:id:moneci:20190921174342j:plain:w300

半分無理やりでやっとの事外した感じです。周りのプラスチックを溶かしてしまった。

f:id:moneci:20190921174352j:plain:w300 f:id:moneci:20190921174402j:plain:w300

パターンを剥がしてしまったら即アウトの中、この作業を60コ以上は心が折れそうになります。

がんばって外してくうち、だんだん慣れてきて油断したのか、とうとうパターンを剥がしてしまいました!
ADPCMの二階建て基盤のプラス側を剥がしてしまい涙目です。とりあえず辞めるわけにもいかないので、作業を進めました。
そんなこんなで3日かかってやっと全てのコンデンサを取り外しました。
結局、追い半田などでなんとか溶かす→吸い取り線で溶かした半田を吸い取る。というのが自分がやった中で外しやすかったと思います。

f:id:moneci:20190921234600j:plain:w300 f:id:moneci:20190921234551j:plain:w300
コンデンサの取り外し後がだいぶ汚いです。

その後、風呂場で基盤全体がひたひたになるほどクリーナーをスプレーして洗いました。
正しいやり方なのか分かりませんが、スプレーで汚れを洗い飛ばしたあと、自然乾燥させています。
コンデンサの端子部分はさらに綿棒にクリーナーをつけてきれいに汚れをふき取りました。
真ん中のSHARPの白い帯に印字されていた日付も消えてしまった…。
f:id:moneci:20190921174553j:plain:w300

端子もきれいになったので、コンデンサの取り付けはそれほど難しくありませんでした。半田も簡単に載ってくれました。
しかし二階建て基盤のコンデンサをどうにかしないといけません。
あまり覚えていないのですが、確かテスターでパターンの繋がっている先を調べたのだと思います。で、隣の白いコードにつながっていたので、 銅線で無理やり空中配線しました。(w)
錆びるのが心配なので、半田メッキもしてあります。これで動いてくれると良いのだけど…。
f:id:moneci:20190921174604j:plain:w300

そしてやっとのことメイン基板のコンデンサ取り付け完了!
バックアップ電池も合わせて付け替えています。今思えばソケット化すればよかった…。
f:id:moneci:20190921174615j:plain:w300

68000CPUやシャープのカスタムチップやYM2151などが配置されていて、凄いかっこいいなぁ。

しかしこれで終わりではありません。拡張スロットとFDD基板のコンデンサも交換します。
FDD基板は元に戻せるよう、構造をチェックしながら慎重に分解しました。
最終的にプラスチックみたいなケーブルでモーターと繋がる部分以外は全て分解しています。
左下の写真の3つのコンデンサを交換。後ろに写っているHDDは作業台です。w
f:id:moneci:20190921174634j:plain:w300 f:id:moneci:20190921174644j:plain:w300 f:id:moneci:20190921174624j:plain:w300 f:id:moneci:20190921174704j:plain:w300

これですべてのコンデンサの交換が完了しました!
とりあず電源だけ接続して、煙や変な匂いがでないかおっかなびっくり確認しました。
異常はなさそうなので、元通り組み立てて恐る恐るパワースイッチを押したところ…。無事起動しました!

f:id:moneci:20190921174713j:plain:w300

ADPCMも問題なく鳴っています。けどLINE OUTが片側しか出力しない…。
まぁ、素人の修理で起動までできただけでも良かったとしましょう。

ということで、素人でも練習と気力があればコンデンサ交換ができるという報告です。
しかし、その後電源が逝ってしまい、電源はさすがに怖くて手が出せないのでヤフオクの業者の方へ修理を依頼し、 LINE OUT含め修理していただきました。
正直、同じ作業をもう一度やれるかと言われるとしんどいです。
失敗のリスクや手間を考えると、現在ヤフオクで出品されている金額はかなり安い(部品代込みだし) と思うし、 まだメンテナンスしてくれる方がいるのは本当に助かります。

あと、コンデンサ交換の前からですが、たまにFDDを入れてもドライブランプが点灯せず認識してくれない症状がありました。
コンデンサ交換しても解消しなかったので、別の問題っぽいです。こちらの情報だと、
www.okqubit.net

下側のFDDのディスク挿入検知のマイクロスイッチ(入口左側)が動作不良になっていて、何回か押してちゃんと導通するようにしてやる必要があった。

との事なので、同じ問題かもしれません。現状ではあまりFDDは使用していないので放置。(w)
ただし、x68はFDDドライブが認識されないと起動しないので注意が必要です。

あれからもう6年たちましたが、今のところ問題無く動作しています。

【お知らせ】
YouTubeでもX68000に関する動画を配信しております。よろしかったらこちらもどうぞ。 www.youtube.com

Laravelで作ったAPIをaxiosから呼ぶとPUTのパラメータが渡されない件

普段PUTを使用するAPIを使うことがあまりないのだけど、Laravelとの組み合わせではまったのでメモ。
リクエストは受け付けてくれるのだけど、Laravel側でPUTされたパラメータを受け取ってくれない。
以下の様に、HTMLフォームのデータでは、PUTやDELETEなどはサポートされていないらしいので、 '_method'というパラメータを作成し、実際のmethodを仕込んでPOSTする。

ルーティング 5.5 Laravel

ソースはこんな感じ。

import Axios from 'axios';
const AjaxClient = Axios.create({
  withCredentials: true,
  xsrfHeaderName: 'X-CSRF-Token',
  headers:{
      'X-Requested-With': 'XMLHttpRequest',
      'Content-Type':'application / x-www-form-urlencoded'
  }
});

let url = 'http://....';
let params = {key1: 'val1', key2: 'val2'};
let formData = new FormData();
formData.append('_method', 'PUT');
for (let key in params) {
    formData.append(key, params[key]);
}
AjaxClient.post(url, formData);
    .then((response) => {
        console.log('success');
    }).catch((error) => {
        console.log('error');
    });

chromeで日本語を検索すると、「このサイトにアクセスできません」のエラーとなる件

chromeのアドレスバーで日本語の文字列で検索すると、「このサイトにアクセスできません」のエラーとなる場合がある。

f:id:moneci:20190630000840p:plain

表示されているサイトは、検索文字をピニュコードで変換したURLで、初めは変なウィルスにでも感染したのかと焦った。
ja.wikipedia.org

同じ文字列でも上記のようになる場合とちゃんと検索される場合があり、プラグインを外したりとか試してみたが 原因を突き止める事ができなかったが、やっと動作がわかった。

chromeでは、アドレスバーに検索文字を入力し、[Ctrl]+[Enter]で検索すると、自動的に
「www.検索文字.com」をアクセスする様だ。
私のIME切替えは、昔なつかしの「かんな」に合わせて、[Ctrl]+oを追加でバインドしているのだが、
アドレスバーに日本語入力したあと、つい癖で、[Ctrl]+oで日本語入力をOFF→すかさず[Enter]で検索 してしまっていたので、[Ctrl]+oと[Enter]が同時に反応してしまっていたらしい…。
とにかく原因が分かってすっきしりた。

これ普通に使っている人はまず発生しないエラーですね。

androidでAttempt to invoke virtual methodという謎のエラーに悩まされる件

androidでプログラムを変更した時に、何かのタイミングで

java.lang.NullPointerException: Attempt to invoke virtual method 'int android.view.View.getImportantForAccessibility()' on a null object reference

というエラーが発生するようになった。

スタックトレースを見ても、andoridフレームワークの中しか動いていなくて、自分のプログラムのどこで落ちているのか全く分からず。
viewのエラーなので、画面処理で発生しているのはなんとなく想像できるが、更新のタイミングが複数のスレッドからのコールバックで動いているので、 ステップ実行してもなかなか特定することができなかった。
仕方なく怪しいところ全てにログを仕込んでやっと発生個所を特定。

ArrayAdapterの継承クラスのgetView()メソッドで、nullを返却するルートがあった...。
そりゃ落ちますわな。

androidでbluetoothのiBeaconのデータを取得したい件

bluetooth難しいです。タイトルの日本語が正しいのかすら分かりません...

androidでは、標準のライブラリとして、android 4.3 (API level 18)以降から利用できるBluetooth low energy(BLE) APIが提供されています。
developer.android.com

ただし、Bluetooth機器全般を制御する為のAPIで、iBeacon対応デバイスの仕様書に書かれているようなデータを簡単に取得する事はできない。
で、AltBeaconライブラリを使えば簡単に取得できるのだけど、BLEのAPIと比べて若干機器検出の反応が鈍い気がする。(個人の感想です。)
github.com

じゃあやっぱりBLEのAPIで取得したいところですが、なかかな仕様が複雑です。 bluetoothの機器からは一定間隔でアドバタイズパケットが送信されているのですが、そのペイロード(データ?)の中身を解析し、 iBeaconをはじめとするいくつかのデータフォーマットに合わせて取得する必要があります。

この記事に全てが集約されています。

qiita.com

うれしいことに、記事の作者さんが公開されている nv-bluetooth を使用することにより、 BLE APIを使用してiBeaconのデータを簡単に取得することができます。
ありがたく使わせていただきます。
GitHub - TakahikoKawasaki/nv-bluetooth: Bluetooth utility library, mainly for Android.

使い方はREADMEに書かれていまます。

機器にもよりますが、ローバッテリーなどの情報は、iBeaconのデータから取得することができます。 ちなみに、iBeaconのデータを拾うだけならGATT接続する必要はありません。 こんな感じで取得できます。

ArrayList scanFilterList = new ArrayList();
// フィルターで色々な条件を指定できる。
scanFilterList.add(new ScanFilter.Builder().setDeviceName("機器名など").build()); 
// スキャンモードの作成
// SCAN_MODE_LOW_POWER     0.5秒スキャン→4.5秒待つを繰り返します。アプリがフォアグラウンドにいない場合、このモードが強制される。(default)
// SCAN_MODE_BALANCED      2秒スキャン→3秒待つを繰り返します。
// SCAN_MODE_LOW_LATENCY   ずっとスキャンします。アプリがフォアグラウンドにいるときのみの利用が推奨されている。
// SCAN_MODE_OPPORTUNISTIC 特別なモードです。他のアプリがBLEスキャンを実行するとスキャン結果が通知されます。
ScanSettings scanSettings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER).build();

// スキャン開始(mBluetoothLeScannerのインスタンスは取得済みの前提)
mBluetoothLeScanner.startScan(scanFilterList, scanSettings, new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        super.onScanResult(callbackType, result);

        // アドバタイズパケットをパースしたADStructureから、iBeaconのデータのみ取得する。
        List<ADStructure> structures = ADPayloadParser.getInstance().parse(result.getScanRecord().getBytes());
        IBeacon iBeacon = null;
        for (ADStructure structure : structures) {
            if (structure instanceof IBeacon) {
                iBeacon = (IBeacon)structure;
                break;
            }
        }
        if (iBeacon == null) {
            Log.d(TAG, "No iBeancon data found");
        } else {
            Log.d(TAG, "iBeacon data: "
                    + "major:" + iBeacon.getMajor() + "\n"
                    + "minor:" + iBeacon.getMinor() + "\n"
                    + "uuid:" + iBeacon.getUUID() + "\n"
                    + "power:" + iBeacon.getPower());
        }
    }

    @Override
    public void onBatchScanResults(List<ScanResult> results) {
        super.onBatchScanResults(results);
    }

    @Override
    public void onScanFailed(int errorCode) {
        super.onScanFailed(errorCode);
    }
});