MSX0でWebAPIへのHTTP通信を試してみた #MSX #MSX0 #Arduino


前回の投稿ではMSX0でI2C通信を試してみましたが、

mobileFF's blog: MSX0でI2C通信を試してみた #MSX #MSX0 #Arduino


添付されているサンプルコードにはインターネット上のサーバに対するHTTP通信を行うものもあります。

HTTP通信ができると、遊べることの幅がかなり広がるし通信も無線になるので柔軟性も増すように思えたので、こちらも試してみました。

MSX0側は、添付のサンプルコードを書き換えながら作りました。サンプルコード、チュートリアル大事。

構成図?概念図?はこんな感じです。


時空を超えたMSX0のちょっと面倒な問題

サンプルとしてぱっと思いついたのが、郵便番号から住所を取得するようなWeb APIへのアクセスだったのですが、ここに一つ、時空を超えてしまったMSXならではの問題が。

今どき、Web系のあらゆるサービスの文字エンコーディングは「UTF-8(Unicode)」というのを使っているわけですが、MSXのアーキテクチャが作られた当時はまだUnicodeなんて影も形もありませんでした。いやもしかすると理論的にはあったのかもしれませんが、当時の日本のパソコンで実装しているものなど皆無で、みなすべからく「シフトJISコード」で全角文字を表現していました。

そのため、どこかしらでUTF-8コードをシフトJISコードに変換してもらわねばならない、という問題があります。

また、もう一つは、MSX0単体でも解決できないわけではありませんが、WebAPIから返却されてくるレスポンスの内容は「JSON」という形式で書かれているものがほとんどで、ここから目的のデータを取り出すのが、若干面倒という問題があります。


Arduino(ESP8266)でカスタムなWebサーバを作る


ということで、上記2つを別の誰かにやらせようと思ったとき、手元にWiFi内蔵のArduino(ESP8266)があることを思い出しました。オークションでクーポン使って安く買えるので、特に何の気もなしに買っておいたものでした。

全くもって、今回のような使い方を予見していたわけではありませんw

このArduinoに

  • MSX0の代理でWebAPIサーバにリクエストを出してもらう
  • サーバからのレスポンス(JSON)から抜粋して応答として必要な最低限の内容に整理
  • レスポンスの内容がUTF-8になっているのをシフトJISの文字列に変換してMSX0側に返却
という機能を持たせます。

Arduino(ESP8266)のカスタムなWebサーバとしての機能や、HTTPクライアントとしての機能については、ESP8266用のライブラリをインストールした際にサンプルのスケッチがあるのでそれを参考にしました。ESP8266のArduinoをセットアップする方法はこちらなどを参考にしました。

Arduino用 ESP8266モジュールを使う http://7ujm.net/micro/arduino_esp8266.html

問題の「UTF-8→シフトJISへのエンコーディング変換」ですが、こちらもすでに対応するライブラリをArduino用に作ってくださっている方がいらっしゃるので、ありがたく使わせていただきました。

UTF-8 文字列から Shift_JIS へ変換する WROOM(ESP8266)用 Arduino IDE ライブラリを作ってみました | mgo-tec電子工作
https://www.mgo-tec.com/blog-entry-utf8-sjis-wroom-arduino-lib.html


さらに、JSONを取り扱うためのArduino用のライブラリもあるので、それも利用して、JSONのレスポンスから住所情報を組み合わせて、単純なテキストデータとしてMSX0側に返却します。

【M5StickC Plus/Arduino】Arduinoでjsonを扱う - ソースに絡まるエスカルゴ https://rikoubou.hatenablog.com/entry/2021/04/16/162258


これらを使って作ったカスタムWebサーバのソースがこちら。


#include <Arduino.h>
#include <ESP8266WebServer.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <uri/UriBraces.h>

#include "secrets.h"  // add WLAN Credentials in here.

// name of the server. You reach it using http://webserver
#define HOSTNAME "webserver"

// local time zone definition (Berlin)
#define TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3"

// need a WebServer for http access on port 80.
ESP8266WebServer server(80);
ESP8266WiFiMulti WiFiMulti;

#include <UTF8toSJIS.h>
const char* UTF8SJIS_file = "/Utf8Sjis.tbl"; //SPIFFSファイルシステムで予めこのファイルをアップロードしておくこと
UTF8toSJIS u8ts;

#include <ArduinoJson.h>

// ===== Simple functions used to answer simple GET requests =====
void handlePostalProxy() {
  
  WiFi.mode(WIFI_STA);

  // wait for WiFi connection
  if ((WiFiMulti.run() == WL_CONNECTED)) {

    WiFiClient client;
    HTTPClient http;

    Serial.print("[HTTP] begin...\n");
    
    // パスパラメータの取得
    String code = server.pathArg(0); 
    
    // 送信先URL(パラメータ含む)の指定
    if (http.begin(client, "http://zipcloud.ibsnet.co.jp/api/search?zipcode="+code)) {

      Serial.print("[HTTP] GET...\n");
      // start connection and send HTTP header
      int httpCode = http.GET();

      // httpCode will be negative on error
      if (httpCode > 0) {
        // HTTP header has been send and Server response header has been handled
        Serial.printf("[HTTP] GET... code: %d\n", httpCode);

        if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
          String payload = http.getString();
          Serial.println(payload);

          // WebAPIサーバから返されたレスポンスボディの内容をもとにJSONドキュメントを生成
          DynamicJsonDocument doc(512);
          deserializeJson(doc, payload);

          String utf8resp = "address=";
          utf8resp += (String)doc["results"][0]["address1"];
          utf8resp += (String)doc["results"][0]["address2"];
          utf8resp += (String)doc["results"][0]["address3"];
          Serial.println(utf8resp);

          // UTF-8の文字列(utf8resp)をSJIS(sj_txt)に変換
          uint8_t sj_txt[utf8resp.length()];
          uint16_t sj_length;
          u8ts.UTF8_to_SJIS_str_cnv(UTF8SJIS_file, utf8resp, sj_txt, &sj_length);
          
          // unit8_t型の配列はsendContent_P()メソッドに渡せないので、char型配列に変換
          char resp[sj_length];
          for(int i=0;i<sj_length;i++) {
            resp[i] = sj_txt[i];
          }

          // MSX0(HTTPクライアント)側にレスポンスを返却
          server.setContentLength(sj_length);
          // レスポンスコードとMIMEタイプ/エンコーディングのみ指定
          server.send(200, "text/plain;charset=Shift_JIS","");
          // レスポンスボディ(SJIS文字列)を返却
          server.sendContent_P(resp,sj_length);
          
        }
      } else {
        Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
        server.send(200, "text/plain; charset=Shift_JIS", "ERROR!");
      }
      http.end();
    } else {
      Serial.println("[HTTP] Unable to connect");
    }
  }
}

void setup(void) {
  delay(3000);  // wait for serial monitor to start completely.

  // Use Serial port for some trace information from the example
  Serial.begin(9600);
  Serial.printf("Starting WebServer...\n");

  if (strlen(ssid) == 0) {
    WiFi.begin();
  } else {
    WiFi.begin(ssid, passPhrase);
  }

  // allow to address the device by the given name e.g. http://webserver
  WiFi.setHostname(HOSTNAME);

  Serial.printf("Connect to WiFi...\n");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.printf(".");
  }
  Serial.printf("connected.\n");

  // Ask for the current time using NTP request builtin into ESP firmware.
  Serial.printf("Setup ntp...\n");
  configTime(TIMEZONE, "pool.ntp.org");

  Serial.printf("Register service handlers...\n");

  // 「/zipcode/郵便番号」というURLでGETリクエストを受信したらhandlePostalProxyメソッドを実行
  server.on(UriBraces("/zipcode/{}"), HTTP_GET, handlePostalProxy);

  // enable CORS header in webserver results
  server.enableCORS(true);
  // enable ETAG header in webserver results from serveStatic handler
  server.enableETag(true);

  // handle cases when file is not found
  server.onNotFound([]() {
    // standard not found in browser.
    server.send(404, "text/html", "404 Not Found");
  });

  server.begin();
  Serial.printf("hostname=%s\n", WiFi.getHostname());
  //Serial.printf("ip=%s\n",WiFi.localIP());
  Serial.printf("ip addr=%s\n", WiFi.localIP().toString().c_str());
}

void loop(void) {
  server.handleClient();
}


WebサーバとHTTPクライアントの両方の役割を兼任するので若干ややこしいです。

そして、結構はまってしまったのがSJIS形式に変換した後のコンテンツを返却するところです。上記にリンクしたライブラリでUTF-8文字列をSJISに変換すること自体は簡単にできたのですが、いざ、それをArduinoのWebサーバから返そうとすると、なぜか同じ文字列が2回繰り返し送信されてしまったりして、最初うまく行きませんでした。

ESP8266WebServerクラスには、レスポンスを送信するメソッドとしてsend()メソッドと、sendContent_P()メソッドというのがあるのですが、後者のメソッドですと送信するバイト数が限定できるので、こちらを使って実際のバイト数きっちりしか送り出さないようにしています。


MSX0でHTTPクライアントを作る

サーバ側が整いましたのでいよいよMSX0側にHTTPクライアントを実装します。

ざっくりいうとステップは以下です。

  1. [1000~2030] 初期設定等 
  2. [3000~4000] Webサーバに接続
  3. [4000~5000] 検索したい郵便番号を入力
  4. [5000~7000] HTTPリクエストを送信
  5. [7000~7500] レスポンスを受信し、レスポンスを1つの文字列に連結していく
  6. [7500~8000] 特定のキーワード("address=")が含まれる箇所を特定しそこから後ろにある文字列だけを表示
  7. [8000~] 切断処理

リストはこんな感じです。いろいろ試行錯誤はありましたが動いてみるとシンプルなコードです。

1 'SAVE"POSTAL2.BAS"
1000 'initialize
1010   _KANJI
1020   CLEAR 800
1030   NL$=CHR$(13)+CHR$(10)
2000 'user setting
2010   SV$="192.168.1.228"
2020   PA$="msx/me/if/NET0/"  'path
2030   KY$="address="
3000 'connect
3010   _IOTPUT(PA$+ "conf/addr",SV$)
3020   _IOTPUT(PA$+ "conf/port",80)
3030   _IOTPUT(PA$+ "connect",1)
3500 'check connect status
3510   FOR I=0 TO 100:NEXT
3520   _IOTGET(PA$+ "connect",S)
3530   IF S<>1 THEN PRINT "connect fail":GOTO 8000
4000 'input postal code
4010   PRINT "POSTAL CODE=";
4020   INPUT P$
5000 'create message
5010   SM$(0)="GET /zipcode/"+P$+" HTTP/1.1"+NL$
5020   SM$(1)="Host: "+SV$+NL$
5030   SM$(2)="Content-Type: application/json"+NL$
5040   SM$(3)=""+NL$
6000 'send message
6010   PRINT NL$+"---- Send Message ----"
6020   I=0
6030   IF SM$(I)="" THEN 7000
6040   PRINT SM$(I);
6050   _IOTPUT(PA$+ "msg",SM$(I))
6060   I=I+1
6070   GOTO 6030
7000 'receive message
7010   _IOTGET(PA$+"msg",RM$):IF RM$="" THEN 7010
7020   PRINT NL$+"---- Receive Message ----"
7030   RS$="":ER=0
7040   FOR J=0 TO 15
7050     IF RM$<>"" THEN RS$=RS$+RM$:ER=0
7060     IF RM$="" THEN ER=ER+1:IF ER=2 THEN 7500
7070     _IOTGET(PA$+ "msg",RM$)
7080     FOR I=0 TO 100:NEXT I
7090   NEXT J
7500   P = INSTR(RS$,KY$)
7510   IF P<>0 THEN PRINT MID$(RS$,P+LEN(KY$))
8000 'disconnet
8010   _IOTPUT(PA$+ "connect",0)
8020   _IOTINIT()
8030   END




これで、住所を入力するMSXアプリケーションで住所入力が楽になりましたね・・・って、果たしてそんなアプリがどれだけあるというのかw

現場からは以上です。




コメント