MSX0にコンソール経由でファイルを送信(Python編)

## はじめに
さて、前回はMSX-BASIC経由でファイル転送を受けるためのbase64デコーダをBASICで作成しました。

https://qiita.com/enu7/items/9808af07b35b82f9ae26

その後、SCREEN5画像を転送しようと数百行を1行ずつコピペを試みて途中で挫折したわけですが、今回は強い味方、Pythonの兄貴に登場していただき、退屈なコピペ作業から何から何まで一気に片付けてもらいましょう！

2023/10/29改訂・・・ 接続先がMSX-DOSの場合への対処を追加しました。

## 実装コード「msx0babaput.py」
プログラムは以下のような流れでファイルを転送します。
１．MSX0のBASIC画面にtelnet接続
　　DOS画面だったら、BASIC画面に変更
２．newコマンドで初期化し
３．basicプログラムを流し込み
４．runコマンドで流し込んだBASICプログラムを実行
５．実行したBASICプログラムは、入力待ち状態で待機
６．そこに指定されたファイルをbase64でエンコードして、76文字ずつ送信する
７．入力を受けたBASICプログラムはbase64でデコードし結果をファイルに書き込み、また入力待ち状態で待機
８．ファイルをすべて送信終わったら、終了文字「\`」を送信する
９．「\`」を受けたBASICプログラムはファイルをクローズして終了
１０．実行にかかった時間を表示し、終了
```python msx0babaput.py
import sys
import telnetlib
import time
import re
import base64
import os

def is_valid_filename(filename):
    # ファイル名が8文字+拡張子3文字であるかどうかをチェックする正規表現
    pattern = r'^[A-Za-z0-9_]{1,8}\.[A-Za-z0-9_]{3}$'
    return re.match(pattern, filename)

def execute_full_process_telnetlib(host, program, message_file_path):
    start_time = time.time()
    message_file_name = os.path.basename(message_file_path)
    if not is_valid_filename(message_file_name):
        return "Invalid file name format. ",0
    try:
        # Telnet接続
        print("msx0babaput started connecting to msx0")
        tn = telnetlib.Telnet(host, 2223)
        # DOSモードで起動している場合、BASICインタープリタモードに切り替える
        tn.write(b'\r\n')
        response = tn.read_until(b'A>', timeout=1)  # 1秒以内にプロンプトが表>示されない場合はタイムアウト
        if b'A>' in response:
            tn.write(b'basic\r\n')  # BASICインタープリタモードに切り替え
            tn.read_until(b'Ok')       
        sys.stdout.write("Start initilizing.")
        sys.stdout.flush()
        tn.write(b'new\r\n')	
        tn.read_until(b'Ok')
        sys.stdout.write("\rStart initilizing..Done.")
        sys.stdout.flush()
        print()
        sys.stdout.write("Start building base64decoder on msx0 basic.")
        sys.stdout.flush()
        program = program + '\r\n20 LET F$ = "' + message_file_name + '"\r\n'
        
        # BASICプログラムをサーバに送信
        tn.write(program.encode('ascii') + b"\r\n") 
        tn.write(b'run\r\n') 
        tn.read_until(b'?')
        sys.stdout.write("\rStart building base64decoder on msx0 basic..Done.")
        print()

        print("Start Transferring a target file to msx0 basic")
        # メッセージをBase64エンコードして送信
        with open(message_file_path, 'rb') as file:
            messages = file.read()
        
        base64_message = base64.b64encode(messages).decode('ascii')
        total_chunks = len(base64_message) // 76
        for i in range(0, len(base64_message), 76):
            tn.write(base64_message[i:i+76].encode('ascii') + b"\r\n")  # 改行コードをCR+LFに変更
            tn.read_until(b'?')
            # インジケータ表示
            sys.stdout.write("\rTransferring: {}/{} chunks".format(i//76, total_chunks))
            sys.stdout.flush()
        print()
        end_time = time.time()
        execution_time = end_time - start_time

        # "`"を送信し、BASICプログラムを終了
        tn.write(b'`\r\n') 

        tn.read_until(b'Ok')
        result="Done"

        return result,  execution_time

    except Exception as e:
        return str(e)

    finally:
        # Telnet接続を閉じる
        tn.close()

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: python msx0babaput.py <host> <message_file_path>")
        sys.exit(1)

    host = sys.argv[1]
    message_file_path = sys.argv[2]

    # BASICプログラム
    basic_program = '''
10 DEFINT A-Z:DIM F$, I$, O$, B$, T$, C$, B(4), OB(3):CLEAR 10000\r\n
20 LET F$ = "OUTPUT.BIN"\r\n
30 ON ERROR GOTO 9000:KILL F$\r\n
40 S=57:OPEN F$ AS #1 LEN=S\r\n
50 INPUT ":"; I$\r\n
60 IF I$="`" THEN CLOSE #1:END\r\n
70 GOSUB 100 'CALL Base64 decoder (I$:Input Text, O$:Output Text, B$:Base64 table)\r\n
80 GOSUB 1000'CALL SAVE(O$:decoded text)\r\n
90 GOTO 50\r\n
100 'Base64 decoder (I$:Input Text, O$:Output Text, B$:Base64 table)\r\n
110 LET O$ = ""\r\n
120 LET B$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"\r\n
130 FOR I = 1 TO LEN(I$) STEP 4\r\n
140   T$ = MID$(I$, I, 4)\r\n
170   FOR J = 1 TO 4\r\n
180     B(J) = 0\r\n
190   NEXT J\r\n
200   FOR J = 1 TO 4\r\n
210     B(J) = INSTR(B$, MID$(T$, J, 1)) - 1:IF B(J) < 0 THEN B(J)=0\r\n
220   NEXT J\r\n
230   OB(1) = B(1) * 4 + B(2) \ 16\r\n
240   OB(2) = (B(2) AND 15) * 16 + B(3) \ 4\r\n
250   OB(3) = (B(3) AND 3) * 64 + B(4)\r\n
260   FOR J = 1 TO 3\r\n
270     O$ = O$ + CHR$(OB(J))\r\n
280   NEXT J\r\n
290 NEXT I\r\n
300 LET O$ = LEFT$(O$, LEN(O$) + (MID$(T$, 3, 1) = "=") + (MID$(T$, 4, 1) = "="))\r\n
310 RETURN\r\n
1000 'SAVE APPEND(O$:decoded text)\r\n
1010 IF LEN(O$)<S THEN 1100 \r\n
1020 FIELD #1,INT(S) AS T$\r\n
1030 LSET T$=O$:PUT #1\r\n
1040 RETURN\r\n
1100 CLOSE#1\r\n
1110 S=1:OPEN F$ AS #1 LEN=S\r\n
1120 F=LOF(1)\r\n
1130 FIELD #1,1 AS T$\r\n
1140 FOR I=1 TO LEN(O$)\r\n
1150   LSET T$=MID$(O$,I,1)\r\n
1160   PUT #1,F+I\r\n
1170 NEXT I\r\n
1180 RETURN\r\n
9000 IF ERL=30 AND ERR=53 THEN RESUME 40\r\n
9100 ON ERROR GOTO 0\r\n
'''

    # 全プロセスを実行
    result, execution_time = execute_full_process_telnetlib(host, basic_program, message_file_path)
    print(result)
    print("Execution time: {} seconds".format(execution_time))
```

## 実行
さて、実行してみましょう。
クライアントPC側は、Python3を使用します。開発時はWSL上のPython3.5を使用しました。MSX0側はMSX-BASICを起動しておきます。MSX-DOSだと初期化で止まります。実行時はMSX-BASICを初期化して、プログラムを流し込みますので、保存されていないBASICプログラムなどありましたら、保存しておいてください。
【免責】十分な試験を行っていませんので、まだまだバグが含まれていると思います。その点を理解いただいての使用をお願いします。実行した結果についての責任は一切負えません。
#### 送付ファイル
じゅげむの一節をWikipediaからコピペしてmessages.txtとして保存しておきます。
```text messages.txt
じゅげむじゅげむ　ごこうのすりきれず　かいじゃりすいぎょの　すいぎょうばつ　ふうらいばつ　くねるところにすむところ　やーぶらこうじのぶらこうじ　ぱいぽぱいぽ　ぱいぽのしゅーりんがん　しゅーりんがんのぐーりんだい　ぐーりんだいのぽんぽこぴー　ぽんぽこなーの　ちょうきゅうめいのちょうすけ
```

#### 実行コマンド
MSX0のIPアドレスと送信ファイル名を引数として渡します。サンプルではMSX0のIPアドレスが192.168.0.4、ファイル名がmessages.txtになります。
```
python3 msx0babaput.py 192.168.0.4 messages.txt 
```

#### 実行結果
実行結果、以下のような出力となります。
```shell-session
$ python3 msx0babaput.py 192.168.0.4 messages.txt msx0babaput started connecting to msx0
Start initilizing..Done.
Start building base64decoder on msx0 basic..Done.
Start Transferring a target file to msx0 basic
Transferring: 7/7 chunks
Done
Execution time: 30.669430017471313 seconds
```
427バイトの送信に30秒、遅い！

#### 実行結果確認
さて、正常に送信できたか確認してみます。

以下のツールを使用します。msx0baview.pyです。

https://misskey.dev/notes/9l4wji6pgx


```shell-session
$ python3 msx0baview.py 192.168.0.4 messages.txt

10 CLEAR 1000
20 LET F$ = "MESSAGES.TXT"
30 OPEN F$ FOR INPUT AS #1
40 FOR I = 1 TO 9999
50   LINE INPUT#1, LI$
60   PRINT LI$
70   IF EOF(1) THEN 90
80 NEXT I
90 CLOSE #1
99 END
20 LET F$ = "messages.txt"

run
じゅげむじゅげむ　ごこうのすりきれず　かいじゃりすいぎょの　すいぎょうばつ　ふうらいばつ　くねるところにすむところ　やーぶらこうじのぶらこうじ　ぱいぽぱいぽ　ぱいぽのしゅ
ーりんがん　しゅーりんがんのぐーりんだい　ぐーりんだいのぽんぽこぴー　ぽんぽこなーの　ちょうきゅうめいのちょうすけ

Ok
Execution time: 4.0685014724731445 seconds
```
じゅげむじゅげむ・・・、無事転送されてそうですね。成功です。

## 最後に
転送速度が遅いですよね。
この問題を解決してくれるのが、前々回の記事への@fujitanozomu(藤田 望)さんコメントで記載いただいた↓プログラム、機械語実装です。
```bbcbasic
10 DEFINTA-Z
15 CLEAR300,&HDFFF:FORI=0TO81:POKE&HE000+I,VAL("&H"+MID$("EB7E235E2356424BF5C50604298F298F298F298F298F298FF51A13FE2B2818FE2F2819FE3A3808FE5B3808C6B9180FC604180BC6BF18073E3E3718023E3FB56FF110C9C102037C02037D0203F1D60420B7C9",2*I+1,2)):NEXT:DEFUSR=&HE000
20 INPUT "Please input the text using base64:"; I$
30 O$=LEFT$(USR(I$),(LEN(I$)\4)*3)
40 PRINT "Result:"; O$
50 END
```
https://qiita.com/enu7/items/64a82098a38ec5ff77fa#comment-6dcd925f68817aa3e55e

このプログラムを組み込んで実行した結果は↓。10秒ちょいで終わります。
```shell-session
$ python3 msx0babaput3.py 192.168.0.4 messages.txt
msx0babaput started connecting to msx0
Start initilizing..Done.
Start building base64decoder on msx0 basic..Done.
Start Transferring a target file to msx0 basic
Transferring: 7/7 chunks
Done
Execution time: 10.110921144485474 seconds
```
BASICプログラム部分を以下に置き換えることで簡単に導入できます。皆さんも試してみてください。
```python
    basic_program = '''
10 DEFINTA-Z:DIM F$, I$, O$:CLEAR 10000,&HDFFF\r\n
15 FORI=0TO81:POKE&HE000+I,VAL("&H"+MID$("EB7E235E2356424BF5C50604298F298F298F298F298F298FF51A13FE2B2818FE2F2819FE3A3808FE5B3808C6B9180FC604180BC6BF18073E3E3718023E3FB56FF110C9C102037C02037D0203F1D60420B7C9",2*I+1,2)):NEXT:DEFUSR=&HE000\r\n
20 LET F$ = "OUTPUT.BIN"\r\n
30 ON ERROR GOTO 9000:KILL F$\r\n
40 S=57:OPEN F$ AS #1 LEN=S\r\n
50 INPUT ":"; I$\r\n
60 IF I$="`" THEN CLOSE #1:END\r\n
70 GOSUB 100 'CALL Base64 decoder\r\n
80 GOSUB 1000'CALL SAVE\r\n
90 GOTO 50\r\n
100 'Base64 decoder(I$:encoded text, O$:output text)\r\n
110 L=LEN(I$):O$=LEFT$(USR(I$),(L \ 4)*3+(MID$(I$,L-1,1)="=")+(MID$(I$,L,1)="="))\r\n
200 RETURN\r\n
1000 'SAVE APPEND(O$:decoded text)\r\n
1010 IF LEN(O$)<S THEN 1100 \r\n
1020 FIELD #1,INT(S) AS T$\r\n
1030 LSET T$=O$:PUT #1\r\n
1040 RETURN\r\n
1100 CLOSE#1\r\n
1110 S=1:OPEN F$ AS #1 LEN=S\r\n
1120 F=LOF(1)\r\n
1130 FIELD #1,1 AS T$\r\n
1140 FOR I=1 TO LEN(O$)\r\n
1150   LSET T$=MID$(O$,I,1)\r\n
1160   PUT #1,F+I\r\n
1170 NEXT I\r\n
1180 RETURN\r\n
9000 IF ERL=30 AND ERR=53 THEN RESUME 40\r\n
9100 ON ERROR GOTO 0\r\n
'''
```
現場からは以上です！