【MSX0】IOT.LIB

# はじめに
理屈はよくわからないけれど、**Turbo Pascal** で『MSX0 Stack』の IoT 関係の機能が使えそうだったので、ライブラリとして整備してみました。

# ライブラリ

汎用ライブラリの `SYSUTILS.LIB` と、IoT 関係の機能を使うための `IOT.LIB` から構成されます。

## SYSUTILS.LIB

よく使うであろう機能を入れてあります。

```pascal:SYSUTILS.LIB
(*
    SYSUTILS.LIB
*)

type
  HexStr  = string[8];
  BinStr  = string[32];
  ItemStr = string[80];

  PItem = ^TItem;
  TItem = record
    Data: ItemStr;
    Prev: PItem;
    Next: PItem;
  end;

  procedure AddListItem(var List, Last: PItem; s: ItemStr);
  var
    p: PItem;
  begin
    New(p);
    if List = nil then
      begin
        List := p;
        Last := nil;
      end
    else
      Last^.Next := p;
    p^.Data := s;
    p^.Prev := Last;
    p^.Next := nil;
    Last := p;
  end;

  procedure ClearListItem(var List: PItem);
  var
    p, n: PItem;
  begin
    p := List;
    while p <> nil do
      begin
        n := p^.Next;
        Dispose(p);
        p := n;
      end;
    List := nil;
  end;

  function UpperCase(s: ItemStr): ItemStr;
  var
    r: ItemStr;
    i: byte;
  begin
    r[0] := s[0];
    for i:=1 to Length(s) do
      begin
        if s[i] in ['a'..'z'] then
          r[i] := Chr(Ord(s[i]) - 32)
        else  
          r[i] := s[i];
      end;
    UpperCase := r;  
  end;

  function LowerCase(s: ItemStr): ItemStr;
  var
    r: ItemStr;
    i: byte;
  begin
    r[0] := s[0];
    for i:=1 to Length(s) do
      begin
        if s[i] in ['A'..'Z'] then
          r[i] := Chr(Ord(s[i]) + 32)
        else  
          r[i] := s[i];
      end;
    LowerCase := r;  
  end;

  function Inkey: char;
  var
    c: char;
  begin
    Inkey := #$00;
    if KeyPressed then
    begin
      Read(kbd, c);
      Inkey := c;
    end;
  end;

  function IntToHex(v: Integer; w: byte): HexStr;
  var
    s: HexStr;
  begin
    if w < 1 then w := 1;
    if w > 4 then w := 4;
    s[0] := Chr(w);
    while w > 0 do
    begin
      if v = 0 then
        s[w] := '0'
      else  
      begin
        s[w] := Chr($30 + Ord(v and $000F > 9) * 7 + v and $000F);
        v := v shr 4;
      end;  
      w := w - 1;
    end; 
    IntToHex := s;
  end; 

  function IntToBin(v: Integer; w: byte): BinStr;
  var
    s: BinStr;
  begin
    if w <  1 then w :=  1;
    if w > 16 then w := 16;
    s[0] := Chr(w);
    while w > 0 do
    begin
      if v = 0 then
        s[w] := '0'
      else  
      begin
        s[w] := Chr($30 + Ord(v and 1));
        v := v shr 4;
      end;  
      w := w - 1;
    end; 
    IntToBin := s;
  end; 
```

動的配列のない Turbo Pascal のために、文字列リストを実装しました。

| ルーチン | 説明 |
|:---|:---|
| AddListItem() | リストにアイテムを追加します。 |
| ClearListItem() | リストのアイテムを全削除します。 |
| UpperCase() | 文字列を大文字にしたものを返します。 |
| LowerCase() | 文字列を小文字にしたものを返します。 |
| Inkey() | 押されたキーの文字を返します。 |
| IntToHex() | 整数を 16 進文字列に変換します。 |
| IntToBin() | 整数を 2 進文字列に変換します。 |

:::note warn
Turbo Pascal 3.0 では、プログラム中で一度も使わないルーチンがあってもライブラリとしてリンクされてしまいます。実行形式ファイル (*.COM) が肥大化しがちなので、不要なルーチンはコメントアウトしてお使いください。
:::

## IOT.LIB
HRA! さんのコードを参考にしています。

https://github.com/hra1129/for_MSX0/tree/main/sample_program/002_device/2023_05_30_1st_update_version/basicn

```pascal:IOT.LIB
(*
    IOT.LIB
*)

const
  _IOTADR1 = $58; { ver0.05.04: $08 }
  _IOTADR2 = $57; { ver0.05.04: $10 }

  procedure IotSetNode(s: LibStr);
  var
    i, l: byte;
  begin
    port[_IOTADR1] := $E0; port[_IOTADR1] := $01;
    port[_IOTADR1] := $53; port[_IOTADR1] := $C0;
    l := Length(s); port[_IOTADR1] := l;
    for i:=1 to l do port[_IOTADR1] := Ord(s[i]);
    port[_IOTADR1] := $00; l := port[_IOTADR1];
  end;

  function IoTFindCount(s: LibStr): integer;
  var
    r: array [0..1] of byte;
    v: integer absolute r;
  begin
    IotSetNode(s);
    port[_IOTADR1] := $E0; port[_IOTADR1] := $01;
    port[_IOTADR1] := $11; port[_IOTADR1] := $80;
    r[0] := port[_IOTADR1]; r[0] := port[_IOTADR1]; r[1] := port[_IOTADR1];
    IoTFindCount := v;
  end; { IoTFindCount }

  procedure IoTFind(s: LibStr; var List, Last: PItem);
  var
    r: LibStr;
    i, j, l, Cnt: byte;
  begin
    ClearListItem(List);
    Cnt := IoTFindCount(s);
    IotSetNode(s);
    port[_IOTADR1] := $E0; port[_IOTADR1] := $01;
    port[_IOTADR1] := $13; port[_IOTADR1] := $80;
    for j:=1 to Cnt do
      begin
        l := port[_IOTADR1]; r[0] := Chr(l);
        for i:=1 to l do r[i] := Chr(port[_IOTADR1]);
        AddListItem(List, Last, r);
      end;
  end; { IoTFind }

  function IoTGetInt(s: LibStr): integer;
  var
    r: array [0..1] of byte;
    v: integer absolute r;
  begin
    IotSetNode(s);
    port[_IOTADR1] := $E0; port[_IOTADR1] := $01;
    port[_IOTADR1] := $01; port[_IOTADR1] := $80;
    r[0] := port[_IOTADR1]; r[0] := port[_IOTADR1]; r[1] := port[_IOTADR1];
    IoTGetInt := v;
  end; { IoTGetInt }

  function IoTGetReal(s: LibStr): real;
  { ver0.15.20 or later }
  var
    r, l: byte;
    i, j: integer;
    sv, sv2: LibStr;
    v: real;
  begin
    IotSetNode(s);
    port[_IOTADR1] := $E0; port[_IOTADR1] := $01;
    port[_IOTADR1] := $02; port[_IOTADR1] := $80;
    l := port[_IOTADR1]; r := port[_IOTADR1];
    j := (r and $7F) - 64;
    sv := '0.'; if (r and $80) = $80 then sv := '-' + sv;
    for i:=2 to l do
    begin
      r := port[_IOTADR1];
      sv := sv + Chr(r shr 4 + $30) + Chr(r and $0F + $30);
    end;
    if j <> 0 then
    begin
      Str(j, sv2);
      sv := sv + 'E'; if j > 0 then sv := sv + '+';
      sv := sv + sv2;
    end;
    Val(sv, v, i); if i <> 0 then v := 0;
    IoTGetReal := v
  end; { IoTGetReal }

  function IoTGetStr(s: LibStr): LibStr;
  var
    r: LibStr;
    i, l: byte;
  begin
    IotSetNode(s);
    port[_IOTADR1] := $E0; port[_IOTADR1] := $01;
    port[_IOTADR1] := $03; port[_IOTADR1] := $80;
    l := port[_IOTADR1]; r[0] := Chr(l);
    for i:=1 to l do r[i] := Chr(port[_IOTADR1]);
    IoTGetStr := r;
  end; { IoTGetStr }

  procedure IoTPutInt(s: LibStr; v: integer);
  begin
    IotSetNode(s);
    port[_IOTADR1] := $E0; port[_IOTADR1] := $01;
    port[_IOTADR1] := $41; port[_IOTADR1] := $C0;
    port[_IOTADR1] := SizeOf(integer);
    port[_IOTADR1] := v and $00FF; port[_IOTADR1] := v shr 8;
  end; { IoTPutInt }

  procedure IoTPutStr(s: LibStr; v: LibStr);
  var
    i, l: byte;
  begin
    IotSetNode(s);
    port[_IOTADR1] := $E0; port[_IOTADR1] := $01;
    port[_IOTADR1] := $43; port[_IOTADR1] := $C0;
    l := Length(v); port[_IOTADR1] := l;
    for i:=1 to l do port[_IOTADR1] := Ord(v[i]);
    port[_IOTADR1] := $00; l := port[_IOTADR1];
  end; { IoTPutStr }
```

| ルーチン | 説明 |
|:---|:---|
| IoTFindCount() | 対象のノードが持つアイテムの個数を取得します。 |
| IoTFind() | 対象のノードが持つアイテム名のリストを取得します。 |
| IoTGetInt() | ノードから整数値を読み出します。 |
| IoTGetReal() | ノードから実数値を読み出します。 |
| IoTGetStr() | ノードから文字列を読み出します。 |
| IoTPutInt() | ノードに対して整数値を書き込みます。 |
| IoTPutStr() | ノードに対して文字列を書き込みます。 |

:::note
ファームウエアバージョン 0.07.08 以降で IoT 命令用の I/O アドレスが変更になっています。初期ファームウエア (0.05.04) の場合には `_IOTADR1` と `_IOTADR2` の値を書き替えてください。
:::

:::note
IoTGetReal() はファームウエアバージョン 0.15.20 以降で利用可能です。
:::


## 使い方

`SYSUTILS.LIB` と `IOT.LIB` は Turbo Pascal のフォルダに放り込んでください。`{$I ファイル名}` としてインクルードすると使えます。

```pascal
program IOTTEST;
type
  LibStr = string[80];
{$I SYSUTILS.LIB}
{$I IOT.LIB}

var
  List, Last, p: PItem;
begin
  { Get Data }
  writeln(IoTGetInt('host/battery/level'));
  writeln(IoTGetStr('host/ip'));

  { Put Data }
  IoTPutInt('conf/sound/volume', 7);
  IotPutStr('conf/wifi/list/3/ssid', 'DEADBEEF');
  IotPutInt('conf/save', 1);
  writeln(IoTGetInt('conf/sound/volume'));
  writeln(IoTGetStr('conf/wifi/list/3/ssid'));

  { Find Data }
  writeln(IoTFindCount('host/media/dsk/');
  List := nil;
  IoTFind('host/media/dsk/', List, Last);
  p := List;
  while p <> nil do
    begin
      Writeln(p^.Data);
      p := p^.Next;
    end;
  ClearListItem(List);
end.
```

`IoTFind()` にはリストを使っています。`List`, `Last` というリスト (の先頭) を指すポインタと、リストの最後のアイテムを指すポインタを用意すると、リストが生成されます。リストが不要になったら `ClearListItem()` を使ってリストアイテムを解放してください。

:::note warn
LibStr は [MDL-LIB](https://qiita.com/ht_deko/items/6f15aee0bd25b550e4df) でも定義されています。[MDL-LIB](https://qiita.com/ht_deko/items/6f15aee0bd25b550e4df) を使う場合には定義不要です。
:::

# トランジェントコマンド

本記事にあるライブラリを用いて作られたトランジェントコマンド (外部コマンド) です。実行形式ファイル (`*.COM`) とソースファイル (`*.PAS`) があります。

48K システムを想定してコンパイルされているため、少なくとも MSX0 上で動作しない事はないと思います (MSX-DOS の方が TPA の空き領域が広いため、MSX-DOS 上でそのままコンパイルすると MSX-DOS2 で動作しない事があり得る)。

:::note info
記事中のトランジェントコマンドのバイナリ/ソースコードは GitHub からダウンロードできます。

 - [MSX0_TP3/TRANSIENT (GitHub: @ht_deko)](https://github.com/ht-deko/MSX0_TP3/tree/main/TRANSIENT)
:::


## ■ M0CHGAP
M0CHGAP: Wi-Fi の接続先を変更する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/9a2d9752-2c28-ce36-49da-7a655b7b4a98.png)

```txt:Usage: 
Usage: M0CHGAP [登録AP番号]
```

 - パラメータなしで実行すると、登録APリストを表示します。
 - 登録AP番号を指定するとその AP に切り替わり、Wi-Fi リスタート。
 - 0 で OFF です。
 - 4 で AP Mode です [^1]。
 - AP 登録には M0SETAP を使います。


## ■ M0CHGDSK
ディスク交換を行う。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/9d01769a-4f30-4ea9-49ec-bfde3802dae0.png) ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/bd946fcc-7027-2f6d-0670-47f189dccff4.png)

```txt:Usage: 
M0CHGDSK [ディスクイメージ]
```

 - パラメータなしで実行すると、ファイルリストと現在のディスクを示すインジケーターを表示します。
 - ディスクイメージファイル名を指定すると、ディスクを変更します。
 - ファイル名は自動的に大文字に変換されます。
 - ファイル名は拡張子の省略が可能です。省略されると `.DSK` を追加します (拡張子のないファイルは使えません)。
 - 空白を含むファイル名のためにダブルクォーテーション処理が入っていますが、簡易的なので可能な限り空白を含むファイル名は使わないでください。
 - ディスク交換に失敗するとメッセージが表示されます。
 - 交換先に M0CHGDSK があるとは限らないので、RAM ディスクにコピーして使う事をオススメします。
 - [MSX-C の環境ディスクを作るのに使ってみました。](https://qiita.com/ht_deko/items/c81fdf6a9758cbf824ac)


## ■ M0CHGINP
M0CHGINP: 入力デバイスの変更

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/14ccb35d-991e-fcca-cad0-d7eb3560b698.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/0c58b335-4701-d11d-bbbd-9fc2c73e2d81.png)

```txt:Usage: 
Usage: M0ChgInp [/Gnum] [/Pnum] [/Jnum] [/Anum] [/Bnum]
```

[Setup Utility の Input](https://qiita.com/ht_deko/items/b284cb93ac34e2b77d9a#input) と同等の設定を行います。

| スイッチ | 説明 |
|:---|:---|
| /Gn | GamePad Face の接続先を指定します (n=0..3)。 |
| /Pn | タッチパネルの接続先を指定します (n=0..3)。 |
| /Jn | タッチジョイパッドの接続先を指定します (n=0..3)。 |

接続先として設定可能な値です。

| n の値 | 説明 |
|:---:|:---|
| 0 | 割り当てない|
| 1 | キーボード|
| 2 | ジョイスティックポート #1|
| 3 | ジョイスティックポート #2|

ゲームパッドの連射に関する設定です。

| スイッチ | 説明 |
|:---|:---|
| /An | A ボタンの連射速度を変更する (n=0..3)。 |
| /Bn | B ボタンの連射速度を変更する (n=0..3)。 |

連射速度として設定可能な値です。

| n の値 | 説明 |
|:---:|:---|
| 0 | 連射なし |
| 1 | 低速 |
| 2 | 中速 |
| 3 | 高速 |

 - パラメータ未指定の場合には使い方と現在の設定を表示します。
 - GamePad / TouchPad / TouchJoypad の接続先とジョイパッドのボタンの連射速度を変更可能です。
 - スイッチは `-` でも `/` でもよく、大文字/小文字も判断しません。
 - 範囲外の値は 0 とみなされます。
 - 無効なスイッチは無視されます。


## ■ M0CLKUP
エミュレータのクロックアップを行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/a9097df1-7a60-f1bb-b223-99e1603918ff.png)

```txt:Usage: 
M0CLKUP [パーセント]  
```

 - パーセントを指定しない場合には現在の速度が表示されます。
 - パーセントは 32..255 の間で有効です。
 - C コンパイラでコンパイルする前にクロックアップして、実行前にクロックを元に戻すとかできます。


## ■ M0CLRSSD
ユニット固有ステートセーブデータをクリアする [^2]。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/c52152e6-af4b-96b4-4238-b952bc8e7208.png)

```txt:Usage: 
M0CLRSSD [/U0] [/U1]
```

 - レジューム機能で使われるユニット固有ステートセーブデータをクリアします。
 - スイッチは `-` でも `/` でもよく、大文字/小文字も判断しません。
 - 無効なスイッチは無視されます。


## ■ M0I2CSCN
M0I2CSCN: I<sup>2</sup>C スキャナ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/8c778b80-0db8-861a-154b-79615696d88d.png)

```txt:Usage: 
Usage: M0I2CSCN
```

 - I<sup>2</sup>C ノードのデバイスを検索し、その ID を表示します。
 - 内部接続 (Internal) は `device/i2c_i` にリストされます。
 - 充電用のポゴピンに出ているのも内部接続用の I<sup>2</sup>C バスです。
 - Port_A 接続 は `device/i2c_a` にリストされます。


## ■ M0SETAP
Wi-Fi の AP リストを登録する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/e7b661cf-10f4-0cee-7bee-5e29a7805823.png) ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/61574387-8b5e-e774-074c-3d12f318ba18.png)

```txt:Usage: 
Usage: M0SETAP [登録AP番号 SSID パスワード [ON]]
```

 - パラメータなしで実行すると、AP リストを表示します。
 - 登録AP番号は 1～3。SSID と パスワードは平文で入力する事になるので、セキュリティに注意。
 - 登録AP番号 4 は AP モード用 [^1]。
 - 4 番目のパラメータに `ON` (大文字/小文字の区別なし) を指定すると、登録した AP がアクティブになり Wi-Fi リスタート。
 - 現在の登録 AP の内容が変更されると Wi-Fi リスタート。
 - SSID に `-` を入力すると、登録 AP を空にできます。
 - 一度登録した AP を未登録状態にはできない。環境設定ファイルを直接いじる必要がある。
 - 接続先 AP の変更には M0CHGAP を使います。


## ■ M0SETUP
Setup Utility を起動する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/2937e9ef-f859-aa4b-ca1b-d7fab40d2023.png)


```txt:Usage: 
Usage: M0SETUP
```

 - Setup Utility を起動します。
 - 実行後にリセットが行われます。


## ■ M0SHTDN
電源断または再起動を行う。

```txt:Usage: 
M0SHTDN [-h|-r]
```

 - パラメータなしまたは `-h` で『MSX0 Stack』の電源断、`-r` で再起動します。
 - スイッチは `-` でも `/` でもよく、大文字/小文字も区別しません。


## ■ M0TPASZ
TPA サイズを表示。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/cc7a6ddd-5127-82e4-6065-0b339f54786a.png)

```txt:Usage: 
Usage: M0TPASZ
```

 - TPA サイズを表示します。
 - MSX 固有の機能を使っていないので、恐らく CP/M-80 で動作します。
 - コンパイラオプションで 40K システムをターゲットにしました。MSX0 (MSX-DOS2) よりメモリ搭載量の少ないシステムでも動作すると思います。 


## ■ M0VER
M0VER: バージョン情報。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/8b95602e-155b-1207-145e-4851afe61429.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/ececbe77-720f-3b5c-bd6d-1d802afe827b.png)

```txt:Usage: 
Usage: M0VER
```

 - MSX0 に関するバージョンを表示します。
 - MSX-DOS1 では MSX-DOS に関する詳しいバージョン情報を取得できません。


# おわりに
そのうち整理するっ！

**See also:**

 - [Turbo Pascal 3.0 の使い方 (Qiita)](https://qiita.com/ht_deko/items/ec212f5cc17cbe5f718b)
 - [Turbo Pascal (MSX) に関する情報 (Qiita)](https://qiita.com/ht_deko/items/4a622080b58fbc7d865d)

[^1]: MSX0 Stack ファームウエアバージョン 0.11.08 以降で AP モードに対応しています。
[^2]: MSX0 Stack ファームウエアバージョン 0.15.18 以降でレジューム機能に対応しています。