MSX0からコンソール経由でファイルを受信(Python版)

# はじめに
皆さん、こんにちは。
前回は、MSX-BASICでbase64エンコーダを作りました。MSX0上でこのプログラムを動かして、ファイルをエンコードして出力された結果をクライアントPC側でbase64デコードしてしまえば、ファイル転送できるという算段です。

https://qiita.com/enu7/items/52169ca8619cb8a5f111

前々回と同様に面倒なところはPythonの兄貴に頑張ってもらいましょう。

https://qiita.com/enu7/items/23cab122141fb8d07c6d


# 環境
MSX: MSX0Stack
クライアントPC: Windows+WSL,TeraTerm
Python: Python3.5 on WSL

# 実装コード「msx0babaget.py」
それでは、実装したコードです。プログラムは以下のような流れで動作して、ファイルを受信します
１．MSX0のBASIC画面にtelnet接続(DOS画面だったらBASICモードに変更します)
２．newコマンドで初期化
３．転送する対象のファイルのファイルサイズを取得
４．BASICコマンドをMSX0のBASICインタープリタに流し込み
５．runコマンドでプログラムを実行
６．実行されたプログラムは指定されたファイルを54文字ずつ読み込みbase64エンコード、結果を出力。
７．runコマンドでプログラムを実行した後は、1行ごと結果を受け取り、base64デコードして出力先ファイルに出力。
８．BASICプログラムが終わったら(「Ok\r\n」を拾ったら)、ファイルをクローズして終了
９．実行にかかった時間をを表示し、終了。

```python msx0babaget.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 create_new_file(file_name):
    open(file_name, 'wb').close()

def decode_and_write_to_file(encoded_text, file_name):
    decoded_data = base64.b64decode(encoded_text)
    with open(file_name, 'ab') as file:
        file.write(decoded_data)

def execute_full_process_telnetlib(host, program, message_file_path,local_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.", time.time() - start_time
    try:
        # Telnet接続
        print("msx0babaput started connecting to msx0")
        tn = telnetlib.Telnet(host, 2223)
        tn.write(b'\r\n')

        # DOSモードで起動している場合、BASICインタープリタモードに切り替える
        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')

        # newコマンドで初期化
        sys.stdout.write("Start initilizing.")
        sys.stdout.flush()
        tn.write(b'new\r\n')	
        tn.read_until(b'Ok\r\n')
        sys.stdout.write("\rStart initilizing..Done.")
        sys.stdout.flush()
        print()

        # ファイルサイズを取得
        oneline='OPEN"' + message_file_name + '"AS#1:PRINT LOF(1):CLOSE#1\r\n'
        tn.write(oneline.encode('ascii'))
        output = tn.read_until(b"Ok\r\n").decode('ascii').strip()
        match = re.search(r'\s+(\d+)\s+', output)
        message_file_size = 0
        if match:
            message_file_size = int(match.group(1))
        else:
            print("Can not get file size!")
            return "Error", time.time() - start_time
        print("Target file size:" + str(message_file_size))

        # BASICプログラムをBASICインタープリタに送信する
        sys.stdout.write("Start building base64encoder on msx0 basic.")
        sys.stdout.flush()
        program = program + '20 LET F$ = "' + message_file_name + '"\r\n'#ファイル名を上書き
        tn.write(program.encode('ascii') + b"\r\n") # BASICプログラムをサーバに送信
        result = tn.read_until(b'\r\n\r\n')
        sys.stdout.write("\rStart building base64encoder on msx0 basic..Done")
        sys.stdout.flush()
        print()

        # BASICプログラムを実行する
        print("Start transfer a target file on msx0 basic to local PC.")
        create_new_file(local_file_path)
        tn.write(b'run\r\n') 
        tn.read_until(b'run\r\n')
        chunk_counter=0
        total_chunks= message_file_size // 57 +1
        # BASICプログラムの結果を1行ずつ読み取り、base64デコードしファイルに書き込む
        while True:
            sys.stdout.write("\rTransferring: {}/{} chunks".format(chunk_counter, total_chunks))
            sys.stdout.flush()
            result = tn.read_until(b'\n')  
            if "Ok\r\n" == result.decode('ascii'):
                break
            decode_and_write_to_file(result.decode('ascii').strip(),local_file_path)
            chunk_counter += 1
        print()
        result="Done"

        return result,  time.time() - start_time

    except Exception as e:
        return str(e), time.time() - start_time

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


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

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

    # BASICプログラム
    basic_program = '''
10 DEFINT A-Z:DIM I$, O$, B$, B(3), OB(4):CLEAR 1000:ON ERROR GOTO 2000\r\n
20 LET F$ = "messages.txt"\r\n
100 S = 57:OPEN F$ AS #1 LEN=S\r\n
110 L = LOF(1): C = L \ S\r\n
120 FOR H=0 TO C\r\n
130   IF C = H THEN S = ( L - 1 ) MOD S\r\n
140   FIELD #1, INT(S) AS I$\r\n
150   GET #1\r\n
160   GOSUB 1000\r\n
170   PRINT O$\r\n
180 NEXT H\r\n
190 CLOSE #1\r\n
999 END\r\n
1000 'B64E(I$, O$, B$)\r\n
1020 DIM T$, C$\r\n
1030 LET O$ = ""\r\n
1040 LET B$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"\r\n
1050 FOR I = 1 TO LEN(I$) STEP 3\r\n
1060   T$ = MID$(I$, I, 1)\r\n
1070   IF LEN(I$) >= I + 1 THEN T$ = T$ + MID$(I$, I + 1, 1)\r\n
1080   IF LEN(I$) >= I + 2 THEN T$ = T$ + MID$(I$, I + 2, 1)\r\n
1090   FOR J = 1 TO 3\r\n
1200     B(J) = 0\r\n
1210   NEXT J\r\n
1220   FOR J = 1 TO LEN(T$)\r\n
1230     B(J) = ASC(MID$(T$, J, 1)) \r\n
1240   NEXT J\r\n
1250   OB(1) = B(1) \ 4\r\n
1260   OB(2) = (B(1) AND 3) * 16 + B(2) \ 16\r\n
1270   OB(3) = (B(2) AND 15) * 4 + B(3) \ 64\r\n
1280   OB(4) = B(3) AND 63\r\n
1290   FOR J = 1 TO 4\r\n
1300     IF J >= 3 AND LEN(T$) = 1 THEN O$ = O$ + "=" ELSE IF J = 4 AND LEN(T$) = 2 THEN O$ = O$ + "=" ELSE O$ = O$ + MID$(B$, OB(J) + 1, 1)\r\n
1330   NEXT J\r\n
1340 NEXT I\r\n
1350 RETURN\r\n
2000 IF ERR=55 THEN CLOSE #1:END\r\n
3000 ON ERROR GOTO 0\r\n'''

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

# 実行
前回、MSX0に送信したじゅげむの一節を受信しましょう。

```※【免責】十分な試験を行っていませんので、まだまだバグが含まれていると思います。その点を理解いただいての使用をお願いします。実行した結果についての責任は一切負えません。```

実行するコマンドは以下です。
　MSX0のIP：192.168.0.10
　受信対象のファイル名：messages.txt
　受信後に保存するファイル名：dddd.txt
```
python3 msx0babaget.py 192.168.0.10 messages.txt dddd.txt
```
実行した結果は、以下のようになります。
```shell-session
nshichi@DESKTOP-BML9KQH:~$ python3 msx0babaget.py 192.168.0.10 messages.txt dddd.txt
msx0babaput started connecting to msx0
Start initilizing..Done.
Target file size:428
Start building base64encoder on msx0 basic..Done
Start transfer a target file on msx0 basic to local PC.
Transferring: 8/8 chunks
Done
Execution time: 42.076099157333374 seconds
nshichi@DESKTOP-BML9KQH:~$
nshichi@DESKTOP-BML9KQH:~$ more dddd.txt
じゅげむじゅげむ　ごこうのすりきれず　かいじゃりすいぎょの　すいぎょうばつ　ふうらいばつ　くねるところにすむところ　やー
ぶらこうじのぶらこうじ　ぱいぽぱいぽ　ぱいぽのしゅーりんがん　しゅーりんがんのぐーりんだい　ぐーりんだいのぽんぽこぴー　
ぽんぽこなーの　ちょうきゅうめいのちょうすけ
```

無事受信できました。しかし・・・

```
Execution time: 42.076099157333374 seconds
```
遅い！チューニング版はいずれ。
実行結果の動画はXにアップしましたので、参考まで。

https://twitter.com/n710jp/status/1718618661949456700

現場からは以上です！

---以下、追記。
# チューニング①
BASICプログラム部分を見直します。
```python
    # BASICプログラム
    basic_program = '''
10 DEFINT A-Z:DIM I$, O$, B$, OB(4):CLEAR 1000:ON ERROR GOTO 3000\r\n
20 LET F$ = "messages.txt"\r\n
30 LET B$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"\r\n
100 S = 3:OPEN F$ AS #1 LEN=S\r\n
110 L = LOF(1): C = L \ S\r\n
120 FIELD #1, 3 AS I$\r\n
130 FOR H=0 TO C\r\n
140   IF C = H THEN 300\r\n
150   GET #1\r\n
160   GOSUB 1000\r\n
170   O$ = MID$(B$, OB(1) + 1, 1)+MID$(B$, OB(2) + 1, 1)+MID$(B$, OB(3) + 1, 1)+MID$(B$, OB(4) + 1, 1)\r\n
180   PRINT O$;\r\n
190   IF H MOD 19 = 18 THEN PRINT  \r\n
200 NEXT H\r\n
300 S = ( L - 1 ) MOD 3\r\n
320 GET #1\r\n
330 GOSUB 1000\r\n
340 LET O$ = ""\r\n
350 FOR J = 1 TO 4\r\n
360   IF J >= 3 AND S= 1 THEN O$ = O$ + "=" 
       ELSE IF J = 4 AND S= 2 THEN O$ = O$ + "=" ELSE O$ = O$ + MID$(B$, OB(J) + 1, 1)\r\n
370 NEXT J\r\n
380 PRINT O$\r\n
900 CLOSE #1\r\n
999 END\r\n
1000 'B64E(B1%,B2%,B3%,OB(4))\r\n
1160   B1% = ASC(MID$(I$, 1, 1))\r\n
1170   B2% = ASC(MID$(I$, 2, 1))*(1+S=1)\\r\n
1180   B3% = ASC(MID$(I$, 3, 1))*(1+(S=1 OR S=2))\\r\n
1190   OB(1) = B1% \ 4\r\n
1200   OB(2) = (B1% AND 3) * 16 + B2% \ 16\r\n
1210   OB(3) = (B2% AND 15) * 4 + B3% \ 64\r\n
1220   OB(4) = B3% AND 63\r\n
1230 RETURN\r\n
3000 IF ERR=55 THEN CLOSE #1:END\r\n
3010 ON ERROR GOTO 0\r\n'''
```
無駄な処理を取り除きました。
実行した結果は以下です。
```
nshichi@DESKTOP-BML9KQH:~/msx0-repo$ python3 msx0babaget.py 192.168.0.10 messages.txt dddd.txt
msx0babaput started connecting to msx0
Start initilizing..Done.
Target file size:428
Start building base64encoder on msx0 basic..Done
Start transfer a target file on msx0 basic to local PC.
Transferring: 8/8 chunks
Done
Execution time: 20.64774775505066 seconds
```
42秒の処理が21秒に。10秒台に乗せたいですね。

# チューニング②
さてさらにチューニングしてきます。
方針1:GOUSUB止める
方針2:出力用の文字変数は不要。出力してしまえ。
方針3:変数名見直し。
```python
    basic_program = '''
10 DEFINT A-Z:DIM B$,I$,X%,Y%,Z%,O%,P%,Q%,R%,OB%(4) :CLEAR 1000\r\n
20 F$ = "messages.txt"\r\n
30 B$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"\r\n
100 S = 3:OPEN F$ AS #1 LEN=S\r\n
110 L = LOF(1): C = (L - 1) \ S\r\n
120 FIELD #1, 3 AS I$\r\n
130 FOR H=1 TO C\r\n
150   GET #1\r\n
160   X% = ASC(MID$(I$, 1, 1))\r\n
170   Y% = ASC(MID$(I$, 2, 1))\r\n
180   Z% = ASC(MID$(I$, 3, 1))\r\n
190   PRINT  MID$(B$, X% \ 4 + 1, 1) + MID$(B$, (X% AND 3) * 16 + Y% \ 16 + 1, 1);\r\n
200   PRINT  MID$(B$, (Y% AND 15) * 4 + Z% \ 64 + 1, 1) + MID$(B$, (Z% AND 63) + 1, 1);\r\n
210   IF H MOD 19 = 0 THEN PRINT  \r\n
220 NEXT\r\n
300 S = ( L - 1 ) MOD 3\r\n
320 GET #1\r\n
340 X% = ASC(MID$(I$, 1, 1))\r\n
350 Y% = ASC(MID$(I$, 2, 1))*(1+S=1)\r\n
360 Z% = ASC(MID$(I$, 3, 1))*(1+(S=1 OR S=2))\r\n
370 OB%(1) = X% \ 4\r\n
380 OB%(2) = (X% AND 3) * 16 + Y% \ 16\r\n
390 OB%(3) = (Y% AND 15) * 4 + Z% \ 64\r\n
400 OB%(4) = Z% AND 63\r\n
420 FOR J = 1 TO 4\r\n
430   IF J >= 3 AND S= 1 THEN PRINT"="; 
       ELSE IF J = 4 AND S= 2 THEN PRINT"="; ELSE PRINT MID$(B$, OB%(J) + 1, 1);\r\n
440 NEXT\r\n
450 PRINT \r\n
460 CLOSE #1\r\n
999 END\r\n'''
```
さて、結果は、、
```shell-session
nshichi@DESKTOP-BML9KQH:~/msx0-repo$ python3 msx0babaget.py 192.168.0.10 messages.txt dddd.txt
msx0babaput started connecting to msx0
Start initilizing..Done.
Target file size:428
Start building base64encoder on msx0 basic..Done
Start transfer a target file on msx0 basic to local PC.
Transferring: 8/8 chunks
Done
Execution time: 16.69895577430725 seconds
```
16秒台！

# チューニング③
すっかり忘れていたんですが、MSX0はBASICコマンドで動的にクロックアップができます。
```bbcbasic
CALL IOTPUT("msx/u0/pm/cpu/percent",100)
```
詳細仕様は要確認ですが、今回は255に設定してみます。

BASICプログラムの前後にクロック変更コマンドを入れて実行してみましょう。
行番号5と行番号500にクロック変更コマンドを埋め込みました。

```python
    basic_program = '''
5 CALL IOTPUT("msx/u0/pm/cpu/percent",255)\r\n
10 DEFINT A-Z:DIM B$,I$,X%,Y%,Z%,O%,P%,Q%,R%,OB%(4) :CLEAR 1000\r\n
20 F$ = "messages.txt"\r\n
30 B$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"\r\n
100 S = 3:OPEN F$ AS #1 LEN=S\r\n
110 L = LOF(1): C = (L - 1) \ S\r\n
120 FIELD #1, 3 AS I$\r\n
130 FOR H=1 TO C\r\n
150   GET #1\r\n
160   X% = ASC(MID$(I$, 1, 1))\r\n
170   Y% = ASC(MID$(I$, 2, 1))\r\n
180   Z% = ASC(MID$(I$, 3, 1))\r\n
190   PRINT  MID$(B$, X% \ 4 + 1, 1) + MID$(B$, (X% AND 3) * 16 + Y% \ 16 + 1, 1);\r\n
200   PRINT  MID$(B$, (Y% AND 15) * 4 + Z% \ 64 + 1, 1) + MID$(B$, (Z% AND 63) + 1, 1);\r\n
210   IF H MOD 19 = 0 THEN PRINT  \r\n
220 NEXT\r\n
300 S = ( L - 1 ) MOD 3\r\n
320 GET #1\r\n
340 X% = ASC(MID$(I$, 1, 1))\r\n
350 Y% = ASC(MID$(I$, 2, 1))*(1+S=1)\r\n
360 Z% = ASC(MID$(I$, 3, 1))*(1+(S=1 OR S=2))\r\n
370 OB%(1) = X% \ 4\r\n
380 OB%(2) = (X% AND 3) * 16 + Y% \ 16\r\n
390 OB%(3) = (Y% AND 15) * 4 + Z% \ 64\r\n
400 OB%(4) = Z% AND 63\r\n
420 FOR J = 1 TO 4\r\n
430   IF J >= 3 AND S= 1 THEN PRINT"="; 
       ELSE IF J = 4 AND S= 2 THEN PRINT"="; ELSE PRINT MID$(B$, OB%(J) + 1, 1);\r\n
440 NEXT\r\n
450 PRINT \r\n
460 CLOSE #1\r\n
500 CALL IOTPUT("msx/u0/pm/cpu/percent",100)\r\n
999 END\r\n'''
```

実行結果は以下の通り。

```shell-session
nshichi@DESKTOP-BML9KQH:~/msx0-repo$ python3 msx0babaget.py 192.168.0.10 messages.txt dddd.txt
msx0babaput started connecting to msx0
Start initilizing..Done.
Target file size:428
Start building base64encoder on msx0 basic..Done
Start transfer a target file on msx0 basic to local PC.
Transferring: 8/8 chunks
Done
Execution time: 12.947779417037964 seconds
```
ついに12秒台！