Ⅶ. LoRa Gatewayの開発

 LoRa通信実験の最終回は、前回に完成したセントラル制御装置のスケッチをベースにして LoRaゲートウェイを作成します。LoRa端末からデータを受信し、インターネット経由でデータベースサーバーにデータを転送する装置に仕上げます。サーバーではデータの自動記録・保管・加工ができるので、データを多面的に活用することが可能になります。
 サーバーには Raspberry Pi 4を使用。プライベートLoRaの特長を活かして、利用者が機器やプロトコルを自在に制御できる、低コストで弾力的なネットワークの完成です。

  ※スケッチのダウンロードは右側の[Download]ボタンをクリックしてください。    


1.ネットワークと LoRaゲートウェイ

 LoRaゲートウェイは、ネットワーク上で図のように位置付けられます。複数のLoRa端末 Terminal-1~nからデータ収集を行って、受信データをデータベースサーバーに送信し登録する仕組みです。ネットワーク上でのゲートウェイは、LoRa無線ネットワークとインターネットを介して、データをサーバーへ中継する機能を担うものです。端末の場所に応じて、複数のゲートウェイを分散設置することも可能です。

    

 LoRa端末からデータを収集する部分はすでに出来上がっているので、インターネットを経由したサーバーへの接続とデータベース操作をどのようにするかが今回のテーマになります。
 テスト環境のサーバーには Raspberry Pi 4を使用しますが、他のサーバーでもかまいません。また Raspberry Piでも、OSがありインターネット接続ができれば機種は問いません。ネットワークもデータベースも、そしてここでは取り上げませんがデータベースを活用するアプリケーションの開発も、すべてこの Raspberry Piで行うことができます。

2.データベースサーバーの準備

(1) 前提条件

 Raspberry Piをデータベースサーバーにするために関連パッケージをインストールする必要がありますが、その前に、次の最低限の準備が整っていることを前提とします
  ・OSのインストールが完了していること。
    テスト環境では、Raspberry Pi OS (64-bit)
     「A port of Debian Bookworm the Raspberry Pi Desktop (recommended)」です。
  ・固定IPアドレスが設定済みであること。
    テスト環境では 192.168.0.40に設定しています。
  ・テキストエディターが使用できること(ここでは nanoエディターを使っています)。


(2) 必要なパッケージのインストール

  次のパッケージが必要です。
   ・Apache: Webサーバーソフトウェア。
   ・PHP: サーバーサイドで動作する Web系のプログラム言語。
   ・MariaDB: MySQL派生版のリレーショナルデータベース。
   ・phpMyAdmin: MySQLをブラウザで管理するためのツール。


  それぞれのインストールは以下のとおりです。

  まず、インストール前にパッケージリストをアップデートします。     
$ sudo apt update

 ○ Apacheのインストール

$ sudo apt -y install apache2
  インストール結果のバージョンを確認。    
$ apache2 -v
    Server version: Apache/2.4.62 (Debian) などと表示されればOK!

 ○ PHPのインストール

$ sudo apt install -y php php-cgi php-pear libapache2-mod-php
  インストール結果のバージョンを確認   
$ php -v
    PHP 8.2.28 (cli) (built: Mar 13 2025 18:21:38) (NTS) などと表示されればOK!

 ○ MariaDBのインストール

$ sudo apt install -y mariadb-server php-mysql
  mysql_secure_installationというツールを使ってパスワードやユーザ権限などを書き換えます。    
$ sudo mariadb-secure-installation
    Enter current password for root (enter for none): <-- 何も押さずにEnter
    Switch to unix_socket authentication [Y/n]: <-- 'n'を入力
    Change the root password? [Y/n]: <-- 'y'を入力
    New password: <-- DBパスワードを入力
    Re-enter new password: <-- DBパスワードを入力
    Remove anonymous users? [Y/n]: <-- 'y'を入力
    Disallow root login remotely? [Y/n]: <-- 'n'を入力
    Remove test database and access to it? [Y/n]: <-- 'y'を入力
    Reload privilege tables now? [Y/n]: <-- 'y'を入力
  nanoエディターで、localhost以外からアクセスできるようにします。    
$ sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf
    bind-address  = 127.0.0.1 を見つけて、コメントになってないことを確認します。
  動作確認のため MariaDBにログインして、外部端末からのアクセスを許可するように設定します。
  your passwordの部分はパスワードを入力します。    
$ mysql -u root -p
Enter password: ******* <=== DBパスワードを入力
> grant all privileges on *.* to root@"%" identified by 'your password' with grant option;
> exit
  MariaDBを再起動します。    
$ sudo systemctl restart mysql.service

 ○ phpMyAdminのインストール

$ sudo apt install -y phpmyadmin
  ブラウザでアクセスできるように、Apache2にリンクを張ります。    
$ sudo ln -s /etc/phpmyadmin/apache.conf /etc/apache2/conf-available/phpmyadmin.conf
  phpmyadminのコンフィグレーションファイルを/etc/apache2/conf-enable 内に登録します。    
$ sudo a2enconf phpmyadmin
  Apache2を再起動します。    
$ sudo systemctl restart apache2.service
  動作確認のためブラウザから(ここでは前述の 192.168.0.40に)アクセスしてみます。
   http://192.168.0.40/phpmyadmin/
   次の「phpMyAdminへようこそ」画面が表示されるとOK!
    
   ユーザ名: root
   パスワード: DBパスワード(MariaDBインストールで入力したもの)
 を入力するとログインすることができます。


(3) データベースの作成

 では、さっそく計測データを記録するデータベースを作成しましょう。テスト用のデータベースなので、利用者とパスワードは以下のように設定します。
 ・データベース名: ObservedDB
 ・テーブル名: ObservedData
 ・利用者名: host
 ・パスワード: host123
○テーブルの構成
  テーブルは次のようなフィールドで構成します。

(Field name) (Type) (Attribute)   (Note)
idseq int Primary key Auto inclement(自動採番)
kind int 種別(0:通常、8:異常、9:警告、?:Future use)
region int 区域No.
device int 区域内端末No. (1, 2, 3, ・・・・)
temperature float 温度
humidity float 湿度
daytime datetime 計測日時


 先にインストールした phpMyAdminを使ってテーブルの作成などをすることができますが、ここではコマンドで行うことにします。まずターミナルソフトを立ち上げて MySQLを起動します。   
$ mysql -u root -p
Enter password: ******* <=== DBパスワードを入力
 続いて次のコマンドを入力してテーブルを作成します。
CREATE DATABASE ObservedDB;

USE ObservedDB;

CREATE TABLE ObservedData (
	idseq  INT AUTO_INCREMENT,
	kind   INT NOT NULL,
	region INT NOT NULL,
	device INT NOT NULL,
	temperature FLOAT NOT NULL,
	humidity FLOAT NOT NULL,
	daytime DATETIME NOT NULL,
	PRIMARY KEY (idseq)
);

ALTER TABLE ObservedData
  ADD PRIMARY KEY (idseq),
  ADD INDEX dev_idx (region, device);

COMMIT;

CREATE USER host IDENTIFIED BY 'host123';
GRANT SELECT,INSERT ON ObservedDB.* TO host;
 ・1行目: データベース ObservedDBを作成します。
 ・3行目: ObservedDBを選択します。
 ・5~14行: テーブルの構成を定義します。
 ・16~18行: idseqをプライマリーキーに、
       regionとdeviceの組に dev_idxという名前のインデックスを作成します。
 ・20行目: 設定を確定します。
 ・22行目: hostというユーザーを作成し、パスワード 'host123'を設定します。
 ・23行目: ユーザー hostに対して SELECTと INSERTの実行権を付与します。

 以上でデータベースの作成は完了です。

3.PDOによるデータベース操作

 LoRaゲートウェイは、計測データをサーバーのデータベースに登録するために、サーバーに設置した PHPプログラムに向けてデータを送信します。その仕組みは次のとおりです。

(1) PDOによるデータベース接続

 外部からデータベースサーバーに接続するもっとも簡単な方法は、PDO(PHP Data Object)を使うことです。データベース管理システムはいくつもの種類があり、それぞれ専用のドライバーを使用しますが、PDOを使用すればどのデータベースも同じ方法で操作することができます。
 その準備として、データベースへの接続とSQL文によるデータ挿入処理を記述した PHPファイルを、サーバーのドキュメントルートに設置します。Raspberry Piの Bookworm OSの場合、ドキュメントルートは '/var/www/html' ディレクトリーです。下記(3)のコードを、適当なエディターでファイル名 'insObservedDB_PostMethod.php'として書き込んでください。
 下図のように、LoRaゲートウェイはデータベースサーバーに、HTTPの POSTメソッドでリクエストを送信します。送信先はサーバーに設置した上記の PHPファイルです。POSTで送信したデータは、HTTPリクエストのリクエスト本体(URLではなくメッセージボディ)に格納されて、外部からは参照できない形で送信されます。それが PHPプログラムに受け渡されて、データベースに追加記録されます。
 この時、必要ならリクエストに対するレスポンスを確認することができます。正常に処理されると 2で始まる3桁の数値(2xx: Success)を返します。失敗すると 4で始まる3桁の数値(4xx: Client Error)やマイナスの値が返されます。
    

(2) POSTリクエストの内容

 HTTPの POSTリクエストで送信する文字列を例示します。
    val1=1&val2=3&val3=1&val4=24.30&val5=48.00&val6=2025-06-03T18:15:38
 これは次のような内容で構成されています。
   val1=: データ種別
   val2=: 区域No.(ゲートウェイの管理番号)
   val3=: 区域内端末No.(1台のゲートウェイ内の端末順No.)
   val4=: 温度
   val5=: 湿度
   val6=: 計測日時(日付と時刻の間は'T'で連結)
 この文字列を受けて PHPファイル 'insObservedDB_PostMethod.php' が実行されます。


(3) PHPファイルの内容/p>

 以下が 'insObservedDB_PostMethod.php'の内容です。メッセージボディからパラメータを取り出して SQL文を組立てデータベースを更新します。ポイントを簡単に説明しておきましょう。
 ・2行目: PHP実行エラーが発生すれば、すべてのエラーを表示させます。
 ・3行目: dsnは Data Source Nameで、データベースの接続情報です。
 ・4~5行:「データベースの作成で」で設定した利用者名とパスワードです。
 ・8行目: dsnとユーザー名、パスワードでデータベースに接続し、PDOをインスタンス化します。
 ・13~18行: 送信された値は $_POST配列に格納されています。変数にその値を取り出します。
 ・20~21行: ObservedDataテーブルにデータを挿入する SQL文です。
      VALUES句は名前付きプレースホルダーにしています。
 ・24行目: SQL文実行時のエラーを捕捉するように指示しています。
 ・26行目: PDOの preparedメソッドで SQL文の実行準備をします。PDOStatementのインスタンスを返します。
 ・28~33行: 名前付きプレースホルダーのパラメータに変数の値をバインドします。
      preparedメソッドと bindValueメソッドを使うことで、SQLインジェクション攻撃を回避できます。
 ・34行目: SQL文を実行します。
 ・38行目: データベースの接続を解除します。
<?php
  error_reporting(E_ALL);		// Setting to show all PHP errors
  $dsn= 'mysql:host=localhost;dbname=ObservedDB;';
  $user = 'host';
  $pwd  = 'host123';

  try{
    $pdo= new PDO($dsn,$user,$pwd);
  }catch(PDOException $e){
    echo "Can't connect database!\n";
  }

  $kind = $_POST['val1'];
  $region = $_POST['val2'];
  $device = $_POST['val3'];
  $temperature = $_POST['val4'];
  $humidity = $_POST['val5'];
  $daytime = $_POST['val6'];

  $sql = "INSERT INTO ObservedData (kind, region, device, temperature, humidity, daytime)
          VALUES(:kind, :region, :device, :temperature, :humidity, :daytime)";
try {
  // Showing SQL query errors when doing pdo
  $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

  $stmt = $pdo->prepare($sql);

  $stmt->bindValue(':kind', $kind);
  $stmt->bindValue(':region', $region);
  $stmt->bindValue(':device', $device);
  $stmt->bindValue(':temperature', $temperature);
  $stmt->bindValue(':humidity', $humidity);
  $stmt->bindValue(':daytime', $daytime);
  $stmt->execute();
} catch (PDOException $e) {
  echo $e->getMessage();
}
  $pdo = null;
?>

4.LoRa Gatewayのコード

 Lora Gateway(esp32loraGateway.ino)のコードは、一部の #define定数定義と HTTPによる PHP起動スクリプトの編集・実行を除けば、ほとんど esp32loraCentral.inoと同じです。したがって、ここでは異なる部分だけを取り上げます。
①ヘッダーファイルとデータ等の定義

 ・25行目: HTTPを使用するのでこのヘッダーファイルを追加しました。
 ・30行目: ゲートウェイの識別番号です。ゲートウェイも複数設置できるので、ここでは3を設定しています。
 ・31~33行: エラー状態を含む送信データ区分です。
 ・37~38行: テスト用の計測間隔とタイムアウトの秒数を、ここでは少し大きめに設定しました。
 ・40~42行: 環境に応じて設定してください。
 ・57行目: 今回新設した HTTP送信関数です。
#include "esp32_e220900t22s_jp_lib.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include "time.h"
#include "esp_sntp.h"

#define TERMINAL_NUMBER 0             // 計測端末対応の識別番号
#define GATEWAY_NUMBER 3              // ゲートウェイNo.
#define DATA_NORMAL 0                 // 送信データ区分:正常データ
#define DATA_TIMEOUT 8                // 送信データ区分:端末タイムアウト
#define DATA_ALERT 9                  // 送信データ区分:警告データ
#define LED_BUILTIN 2                 // 内蔵LED
#define NUMBER_OF_TERMINALS   2       // 端末台数
#define SEND_DATA_SIZE 29             // 32-3
#define MEASURE_INTERVAL 30           // 計測間隔(整数:秒)
#define MEASURE_TIMEOUT 10            // 計測タイムアウト(整数:秒)

#define WIFI_SSID "your_ssid"
#define WIFI_PASSWORD "your_password"
#define SERVER_URL "http://192.168.0.40/insObservedDB_PostMethod.php"

CLoRa lora;                           // LoRaクラスのインスタンス
struct LoRaConfigItem_t config;       // LoRaコンフィギュレーション
struct RecvFrameE220900T22SJP_t data; // 受信用構造体

// Function prototype
void LoRaRecvTask(void *pvParameters);    // 受信タスク
void LoRaSendTask(void *pvParameters);    // 送信タスク
void startWiFi(const char*, const char*); // WiFi接続
void prepareDayTime(void);                // NTP時刻との同期
char* getDayTime(void);                   // 現在の日時の取得
void strrep(char *, const char *,         // 文字列の置換
                      const char *);
void timeavailable(struct timeval*);      // 時刻同期用コールバック
void sendRequest(int, int, float, float, char*); // HTTPでPOSTリクエスト

unsigned long prev_time = 0;          // 前回の計測時刻
bool doSendSignal = false;            // シグナル送信せよ!
bool receiveCompleted = false;        // データ受信完了!
unsigned long sendCtr = 0;            // 計測指示回数

②setup()、loop()、送受信関数などの基本的な処理

 ・84行目: Gatewayで LoRaモジュールが初期化できなければ、アラートで HTTP送信関数を実行します。
  // E220-900T22S(JP)を初期設定する
  config.own_address = TERMINAL_NUMBER;
  if (lora.InitLoRaModule(config)) {
    SerialMon.printf("LoRa init error\n");
    sendRequest(DATA_ALERT, 0, 0.0, 0.0, getDayTime());
    return;                                               
  }

③ LoRa端末からの受信関数

 ・130行目: 計測データを受信すると、ここでデータをセットして HTTP送信関数を実行します。
void LoRaRecvTask(void *pvParameters) {
  char str[256], *dtime;
  int dev_no, dummy;
  float temp, humd;

  while (1) {
    if (lora.ReceiveFrame(&data) == 0) {  // 受信データが届くと表示処理を行う
      receiveCompleted = true;
      data.recv_data[data.recv_data_len] = '\0';
      sprintf(str, "%s", data.recv_data);
      strrep(str, "nan", "-99.99");    // 欠測値の処置
      sscanf(str, "%d,%d,%f,%f", &dev_no, &dummy, &temp, &humd);
      dtime = getDayTime();
      SerialMon.printf("Receive data: %s (%s)\n", data.recv_data, dtime);
      sendRequest(DATA_NORMAL, dev_no, temp, humd, dtime);
      digitalWrite(LED_BUILTIN, HIGH);
      delay(200);
      digitalWrite(LED_BUILTIN, LOW);
    }
    delay(1);
  }
}

④ LoRa端末への送信関数

 ・166行目: タイムアウトエラーが発生すると、エラーコードで HTTP送信関数を実行します。
        // 計測端末のタイムアウトを監視する
        unsigned long pivotTime = millis();
        while (1) {
          unsigned long now = millis();
          int differ = (now - pivotTime) / 1000;
          if (differ >= (int)MEASURE_TIMEOUT || receiveCompleted) {
            if (!receiveCompleted) {
              SerialMon.printf("<< Timeout occurred!  Terminal No.%d >>\n", i);
              sendRequest(DATA_TIMEOUT, i, 0.0, 0.0, getDayTime());
            }
            receiveCompleted = false;
            break;
          }
          delay(10);
        }

⑤サーバーへの送信関数
 LoRaGatewayで新設された関数です。HTTPの POSTメソッドを使って計測データをサーバーに送信します。

 ・248行目: MySQLの DATETIMEフィールドへの代入は、日付と時刻の間を 'T'で接続する必要があります。
 ・249~251行: 送信するデータ文字列を作成します。
 ・256行目: WiFiでアクセスする URLを登録します。
 ・257行目: HTTPヘッダーの "Content-Type"に "application/x-www-form-urlencoded"を追加します。
 ・258行目: 文字列データを POSTリクエストで送信し、レスポンスを取得します。
 ・260行目: コネクションを切断して資源を開放し HTTP通信を終了します。
/*
  計測結果をHTTPのPOSTメソッドで送信する
*/
void sendRequest(int kind, int dev_no, float temp, float humd, char *dtime) {
  WiFiClient client;
  HTTPClient http;
  char daytime[24];

  strncpy(daytime, dtime, sizeof(daytime));
  strrep(daytime, " ", "T"); 
  String data = "val1=" + String(kind) + "&val2=" + String(GATEWAY_NUMBER) + "&val3=" +
        String(dev_no) + "&val4=" + String(temp) + "&val5=" + String(humd) +
        "&val6=" + String(daytime);
  Serial.print("Send data: ");
  Serial.println(data);

  if(WiFi.status()== WL_CONNECTED){
    http.begin(client, SERVER_URL);
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    int httpCode = http.POST(data);
    Serial.printf("HTTP Response code: %d\n\n", httpCode);
    http.end();
  }
  else {
    Serial.println("*ER .... WiFi Disconnected!");
  }
}

5.総合テスト

○テストの進め方

 次の順序でテストを進めます。
  1) データベースの確認
    サーバーの Raspberry Piに MySQLのデータベースが作成できていることを確認します。
  2) PHPプログラムの確認
    サーバーのドキュメントルートに insObservedDB_PostMethod.phpが設置されていることを確認します。
  3) LoRa端末の起動
    2台の LoRa計測端末に電源を供給します。これでディープスリープの待ち状態になります。
  4) LoRaGatewayの起動
    LoRaGatewayの ESP32に電源を供給します。
 以上で LoRaネットワークシステムが稼働状態になります。

○動作の観察

 Arduino IDEで LoRaGatewayが接続されたポートをシリアルモニターで観察します。
 計測タイミングになると「Request to send message ....」と表示されます。Gatewayから各計測端末に「Go ahead!」の計測指示メッセージが送信されると、やや待って「Receive data:」に続いて受信データ(端末番号、計測回数、温度、湿度、計測日時)が表示されます。
 「Send data: 」の後に、受信データから生成した送信文字列データが表示され、同時にこれがサーバーに送信されます。これに続く「HTTP Response code: 」には HTTPからのレスポンスが、通常は200(OK:リクエスト成功)と表示されます。エラーが発生すると、-1など負の数値になります。また端末でタイムアウトが発生するとその旨が表示されます。タイムアウトは端末の電源を切断することで確認できます。
    

○実行結果の確認

 Raspberry Piに接続したターミナルソフトで MySQLを起動し、「USE ObservedDB;」に続けて次の SELECT文を実行します。    
> USE ObservedDB;
> SELECT * FROM ObservedData;
 記録されたデータの一覧が表示されます。
    
 データベースの記録状態は、ブラウザから phpAdminを起動することでビジュアルに確認することもできます。以前の「phpMyAdminのインストール」で動作確認した時と同じように、アドレスバーにサーバーの IPアドレスを指定して起動します。
   (例)http://192.168.0.40/phpmyadmin/
 左のタブから ObservedDBを展開して ObservedDataをクリックすると次の画面が現れます。
    
(注意)
   そのまま放置するとデータベースにデータが追加され続けます。適当なタイミングで LoRaGatewayを停止
  してください。
   すべてのデータを消去する場合は、ターミナルソフトで MySQLを起動して次の SQL文を実行します。
     DELETE FROM ObservedData;
  なお、テーブル作成で自動採番(Auto inclement)を指定した idseq列は、どこまでも値が増え続けます。
  これをリセットして 1からやり直したい場合は、データ消去に続いて次のコマンドを実行します。
      ALTER TABLE ObservedData auto_increment = 1;


 以上で一連のテストが完了です。
 このように、計測データは指定した計測間隔に従って自動的にデータベースに記録されます。SQLについての解説は省きますが、MySQLのデータ処理能力は強力です。ソートや条件付きデータの抽出、平均値などの計算もできます。また phpMyAdminも、データをバックアップしたり CSV形式で変換出力することなどが簡単にできます。これらの機能を使いこなして、表計算やグラフ作成ソフトと連携してデータを活用することが可能になります。
 また、kindフィールドには異常発生や警告発生などのコードが記録されるので、これらを定期的に抽出することでシステムの運用管理をすることもできます。


6.今後の課題

 LoRa端末も LoRaゲートウェイも比較的シンプルなコードで動作し、しかもプライベートLoRaなら運用費ゼロでネットワーク運用ができることがわかりました。しかし、私的な事情で、まだテストや確認ができてない事項やいくつかの改善点があるので、最後にこれらを列記しておきます。

○積み残したこと

・消費電力の計測
  LoRa端末がディープスリープ状態で、実際にどの程度の電力を消費しているかは未確認です。
・LoRa端末への電池接続
  端末装置は電池駆動が基本になると思いますが、これはまだ試していません。
・通信距離の確認
  適当に離れた位置で試していますが、実際にどのような環境でどの程度の距離まで通信できるかは未確認です。

○改善および機能拡張など

・LoRa端末の電池残量計測
  電池残量をソフトウェアで計測して、閾値を割り込むとサーバーにアテンション情報を送信するなど。
・ゲートウェイの電池駆動
  LoRa Gatewayをタイマー起動型の Deep sleepにして、電池駆動することも考えられます。
  ただし電力消費が大きい Wi-Fiを使用するので、この場合は電池の選択と電池残量の管理が必須になる。
・ゲートウェイのアラーム機能
  サーバー接続が失敗した時に、LED表示などで異常を知らせる機能を追加すべきでしょう。
・セキュリティ対策
  LoRa通信(LoRaモジュール間)は、コンフィギュレーションの「暗号化キー」を設定することで対応が可能。
  今回はテスト環境の都合で送信先URLに httpを使用しましたが、傍受やデータ改ざんを防ぐ必要があれば httpsの
  検討が必要です。

○その他(留意事項)

・計測間隔
  すでに各所で述べていますが、スケッチでは計測間隔に比較的短い時間(今回は30秒)を、また計測タイムアウト
  は10秒を設定しています。これは短時間でテストをしたいための設定値です。端末数が少ない場合はこの程度の値
  で問題ないのですが、端末台数が増えるとデータの受信漏れやタイムアウトが発生するので注意が必要です。
  計測間隔が30分とか1時間単位であればほぼ心配はありません、
  参考までに、実測から求めた計測端末のタイムオーバー定数は4.5秒ですが、5秒の設定では1,000回の通信テストで
  18回のタイムアウトエラーが発生しました。これを6秒にするとエラーはゼロです。
  このことから、タイムアウト定数は 6秒、計測間隔はおおよそ次の条件を満たせば良いのではと思われます。
    計測間隔(秒) ≧ (計測端末数 + 1)x 6秒


 LoRa通信の原理や技術の詳細は力及ばず割愛して、専ら実験に終始しましたが、何とか ESP32による LoRa端末と LoRa Gatewayを作り上げることができました。かなり手間取ったことも多かったですが、とてもシンプルなコードにおさめることができました。これを参考に多方面に応用していただければ大変嬉しいです。
 長らくお付き合いいただきまして、ありがとうございました。皆さまのご活躍を期待いたします。


 
Copyright (C) 2011-2025 Marchan, All rights reserved.