ESP8266をWifiモデムとして使う

ATコマンドによるリモートコマンド実行(telnet通信)


こちらでESP8266をWifiモデムとして使って、UNOや NANOなどEthernetを持たないボードからのMQTT通信を紹介しました。
MQTTなどプロトコルが公開されている通信ならどのような通信も行う事ができます。
そこで、今回はATコマンドによるTelnet通信を紹介します。

サーバーマシンにはIA64 Ubuntuを使い、telnetサーバーのインストールはこ のページのままです。
Telnetプロトコルでは、ログインの前に、クライアントとサーバ間でネゴシエーション(Telnet Negotiation)を行います。
Telnet Negotiationに関する日本語の資料はこちらに 有りますが、あまり詳しく書かれていません。
英語の資料が、こちらこ ちらで公開されていますが、今ひとつ良く分かりません。
そこで、こ ちらに紹介されているやり方で、実際にtelnetでログインし、ネゴシエーションオプションの通信内容を調べてみました。
>マークの通信がクライアント→サーバー、<マークの通信がサーバー→クライアントへの電文です。
:の後ろは翻訳した内容です。
> FF FB 18 :>WILL  18
> FF FD 03 :>DO    03
> FF FB 03 :>WILL  03
> FF FD 01 :>DO    01
> FF FB 1F :>WILL  1F
< FF FD 18 :           <DO    18
< FF FD 20 :           <DO    20
> FF FC 20 :>WON'T 20
< FF FD 23 :           <DO    23
> FF FC 23 :>WON'T 23
< FF FD 27 :           <DO    27
> FF FC 27 :>WON'T 27
< FF FB 03 :           <WILL  03
< FF FD 03 :           <DO    03
< FF FB 01 :           <WILL  01
< FF FD 1F :           <DO    1F
> FF FA 1F 00 50 00 18 FF F0
< FF FA 18 01 FF F0
> FF FA 18 00 78 74 65 72 6D FF F0
< FF FD 01 :           <DO    01
> FF FC 01 :>WON'T 01
< FF FB 05 :           <DO    05
> FF FE 05 :>WON'T 05
< FF FD 21 :           <DO    21
> FF FC 21 :>WON'T 21

サーバーの23番ポートにコネクトすると、すぐに以下のレスポンスが有りました。
+IPD,12:[0xFF][0xFD][0x18][0xFF][0xFD][0x20][0xFF][0xFD][0x23][0xFF][0xFD][0x27]

TeraTermで採取したログを並べ替えると以下の様になります。
> FF FB 18 :>WILL  18
< FF FD 18 :           <DO    18
< FF FA 18 01 FF F0
> FF FA 18 00 78 74 65 72 6D FF F0
> FF FD 03 :>DO    03
> FF FB 03 :>WILL  03
< FF FB 03 :           <WILL  03
< FF FD 03 :           <DO    03
> FF FD 01 :>DO    01
< FF FB 01 :           <WILL  01
> FF FB 1F :>WILL  1F
< FF FD 1F :           <DO    1F
> FF FA 1F 00 50 00 18 FF F0
< FF FD 20 :           <DO    20
> FF FC 20 :>WON'T 20
< FF FD 23 :           <DO    23
> FF FC 23 :>WON'T 23
< FF FD 27 :           <DO    27
> FF FC 27 :>WON'T 27
< FF FD 01 :           <DO    01
> FF FC 01 :>WON'T 01
< FF FB 05 :           <DO    05
> FF FE 05 :>WON'T 05
< FF FD 21 :           <DO    21
> FF FC 21 :>WON'T 21

何となく以下の関係であることが分かります。
クライアント
サーバー
WILL


DO
DO


WILL

WON'T


DO


サーバーはクライアントの要望は何でも聞いてくれますが、クライアントはサーバーの要望は全て否定しています。
試行錯誤の結果のスケッチが以下のスケッチです。
サーバのIPやログインユーザ名などは適当に変更してください。
/*
 * Telnet CLIENT using standard AT firmware
 *
 * ESP01 ---------- ATmega
 * TX    ---------- RX
 * RX    ---------- TX
 *
 */

#include <SoftwareSerial.h>

#define TELNET_SERVER   "192.168.10.143" // Change your Server
#define TELNET_PORT     23
#define TELNET_USER     "orangepi\n\r" // Change your UserName
#define TELNET_PASS     "orangepi\n\r" // Change your Password
#define SHELL_PROMPT    "@orangepipc:" // Change your Prompt
#define ON_BOARD_LED    13
#define SERIAL_RX       10
#define SERIAL_TX       11

//Telnet Commands
const byte cmdSE   = 0xF0;
const byte cmdNOP  = 0xF1;
const byte cmdDM   = 0xF2;
const byte cmdBRK  = 0xF3;
const byte cmdIP   = 0xF4;
const byte cmdAO   = 0xF5;
const byte cmdAYT  = 0xF6;
const byte cmdEC   = 0xF7;
const byte cmdEL   = 0xF8;
const byte cmdGA   = 0xF9;
const byte cmdSB   = 0xFA;
const byte cmdWILL = 0xFB;
const byte cmdWONT = 0xFC;
const byte cmdDO   = 0xFD;
const byte cmdDONT = 0xFE;
const byte cmdIAC  = 0xFF;

//Telnet Options
const byte op_echo                   = 0x01;
const byte op_suppress_go_ahead      = 0x03;
const byte op_status                 = 0x05;
const byte op_timing_mark            = 0x06;
const byte op_terminal_type          = 0x18;
const byte op_window_size            = 0x1F;
const byte op_terminal_speed         = 0x20;
const byte op_remote_flow_control    = 0x21;
const byte op_linemode               = 0x22;
const byte op_X_display_location     = 0x23;
const byte op_environment_variables  = 0x24;
const byte op_new_environment        = 0x27;

SoftwareSerial mySerial(SERIAL_RX, SERIAL_TX); // RX, TX

void putChar(char c) {
  char tmp[10];
  if ( c == 0x0a) {
    Serial.println();
  } else if (c == 0x0d) {
    
  } else if ( c < 0x20) {
    uint8_t cc = c;
    sprintf(tmp,"[0x%.2X]",cc);
    Serial.print(tmp);
  } else {
    Serial.print(c);
  }
}

//Wait for specific input string until timeout runs out
bool waitForString(char* input, int length, unsigned int timeout) {
  unsigned long end_time = millis() + timeout;
  char current_byte = 0;
  int index = 0;

   while (end_time >= millis()) {
    
      if(mySerial.available()) {
        
        //Read one byte from serial port
        current_byte = mySerial.read();
//        Serial.print(current_byte);
        putChar(current_byte);
        if (current_byte != -1) {
          //Search one character at a time
          if (current_byte == input[index]) {
            index++;
//            Serial.print("->index=");Serial.println(index);
            //Found the string
            if (index == length) {              
              return true;
            }
          //Restart position of character to look for
          } else {
            index = 0;
          }
        }
      }
  }  
  //Timed out
  return false;
}

void errorDisplay(char* buff) {
  Serial.print("Error:");
  Serial.println(buff);
  int led = 0;
  while(1) {
    digitalWrite(ON_BOARD_LED,led);
    led = ~led;
    delay(200);
  }
}

void clearBuffer() {
  while (mySerial.available())
    mySerial.read();
//  Serial.println();
}

void getResponse(int timeout){
  char c;
  bool flag = false;
  char tmp[10];
 
  long int time = millis() + timeout;
  while( time > millis()) {
    if (mySerial.available()) {
      flag = true;
      c = mySerial.read();
      if (c == 0x0d) {
           
      } else if (c == 0x0a) {
        Serial.println();
      } else if ( c < 0x20) {
        uint8_t cc = c;
        sprintf(tmp,"[0x%.2X]",cc);
        Serial.print(tmp);
      } else {
        Serial.print(c);
      }
    } // end if
  } // end while
  if (flag) Serial.println();
}


void hexDump(byte *buf, int msize) {
  Serial.print("\nmsize=");
  Serial.println(msize);
  for(int i=0;i<msize;i++) {
    if (buf[i] < 0x10) Serial.print("0");
    Serial.print(buf[i],HEX);
    Serial.print(" ");
  }
  Serial.println();
}

int sendCommand(char *cmd, int len) {
  char at[128];
//  hexDump(buf,msize);
  sprintf(at,"AT+CIPSEND=%02d\r\n",len);
  mySerial.print(at);
  if (!waitForString("OK", 2, 5000)) {
    errorDisplay("AT+CIPSEND Fail");
  }
  if (!waitForString(">", 1, 5000)) {
    errorDisplay("Server Not Response");
  }

//  Serial.print(buf);
  for (int i=0;i<len;i++) {
    mySerial.write(cmd[i]);
    putChar(cmd[i]);                  
  }
  if (!waitForString("SEND OK", 7, 5000)) {
    errorDisplay("Server Not Receive my data");
  }

  return 1;
}

void setup(){
  char at[128];
  byte data[128];
  int msize;

  Serial.begin(9600);
  mySerial.begin(4800);
  pinMode(ON_BOARD_LED,OUTPUT);
  digitalWrite(ON_BOARD_LED,LOW);
 
  while(!mySerial);
  mySerial.print("AT+RST\r\n");
  if (!waitForString("WIFI GOT IP", 11, 10000)) {
    errorDisplay("AT+RST Fail");
  }
  clearBuffer();

  //Establishes TCP Connection
  sprintf(at,"AT+CIPSTART=\"TCP\",\"%s\",%d\r\n",TELNET_SERVER,TELNET_PORT);
  mySerial.print(at);
  if (!waitForString("OK", 2, 5000)) {
    errorDisplay("AT+CIPSTART Fail");
  }
//  waitForString("mail.smtp2go.com", 16, 1000);
  getResponse(5000);


  //Send Telnet Negotiation
  data[0] = cmdIAC;
  data[1] = cmdWILL;
  data[2] = op_terminal_type;
  data[3] = cmdIAC;
  data[4] = cmdDO;
  data[5] = op_suppress_go_ahead;
  data[6] = cmdIAC;
  data[7] = cmdWILL;
  data[8] = op_suppress_go_ahead;
  data[9] = cmdIAC;
  data[10] = cmdDO;
  data[11] = op_echo;
  data[12] = cmdIAC;
  data[13] = cmdWILL;
  data[14] = op_window_size;
  sendCommand(data, 15);
  getResponse(1000);

  data[0] = cmdIAC;
  data[1] = cmdWONT;
  data[2] = op_terminal_speed;
  data[3] = cmdIAC;
  data[4] = cmdWONT;
  data[5] = op_X_display_location;
  data[6] = cmdIAC;
  data[7] = cmdWONT;
  data[8] = op_new_environment;
  sendCommand(data, 9);
  getResponse(1000);

  data[0] = cmdIAC;
  data[1] = cmdSB;
  data[2] = op_window_size;
  data[3] = 0x00;
  data[4] = 0x50; //80
  data[5] = 0x00;
  data[6] = 0x18; //24
  data[7] = 0xFF;
  data[8] = 0xF0;
  sendCommand(data, 9);
  getResponse(1000);

  data[0] = cmdIAC;
  data[1] = cmdSB;
  data[2] = op_terminal_type;
  data[3] = 0x00;
  data[4] = 0x78;
  data[5] = 0x74;
  data[6] = 0x65;
  data[7] = 0x72;
  data[8] = 0x6D;
  data[9] = 0xFF;
  data[10] = 0xF0;
  sendCommand(data, 11);
  getResponse(1000);

  data[0] = cmdIAC;
  data[1] = cmdWONT;
  data[2] = op_echo;
  data[3] = cmdIAC;
  data[4] = cmdDONT;
  data[5] = op_status;
  data[6] = cmdIAC;
  data[7] = cmdWONT;
  data[8] = op_remote_flow_control;
  sendCommand(data, 9);
//  getResponse(100);

  //Wait Login Prompt
  waitForString("login:", 6, 5000);
  msize = strlen(TELNET_USER);
  sendCommand(TELNET_USER,msize);
//  getResponse(100);

  //Wait Password Prompt
  waitForString("Password:", 9, 5000);
  msize = strlen(TELNET_PASS);
  sendCommand(TELNET_PASS,msize);
//  getResponse(10000);

  //Wait Shell Prompt
  msize = strlen(SHELL_PROMPT);
  waitForString(SHELL_PROMPT, msize, 10000);
  Serial.println("\n\n\nLogin!!!");
  getResponse(1000);

  strcpy(data,"uname -a\n\r");
  msize = strlen(data);
  sendCommand(data,msize);
  waitForString(SHELL_PROMPT, msize, 10000);
  Serial.println("\n\n\nComand execute!!!");
  getResponse(1000);

  strcpy(data,"pwd\n\r");
  msize = strlen(data);
  sendCommand(data,msize);
  waitForString(SHELL_PROMPT, msize, 10000);
  Serial.println("\n\n\nComand execute!!!");
  getResponse(1000);

  strcpy(data,"exit\n\r");
  msize = strlen(data);
  sendCommand(data,msize);
  waitForString("CLOSED", 6, 10000);
  Serial.println("\n\nDisconnected!!");

}

void loop(){

}

[uname -a]と[pwd]のコマンドを実行してexitします。
最近はsshでのリモートログインがよく使われますが、自宅内のLANではtelnetで十分です。
Arduinoで読み取ったセンサーの値に応じて、サーバーのコマンドを実行することができます。

次回はAT Version 1.5で追加されたSNTP機能を紹介します。