504 lines
14 KiB
Arduino
504 lines
14 KiB
Arduino
|
|
/*
|
||
|
|
* ESP8266 based virtual modem
|
||
|
|
* Copyright (C) 2016 Jussi Salin <salinjus@gmail.com>
|
||
|
|
*
|
||
|
|
* This program is free software: you can redistribute it and/or modify
|
||
|
|
* it under the terms of the GNU General Public License as published by
|
||
|
|
* the Free Software Foundation, either version 3 of the License, or
|
||
|
|
* (at your option) any later version.
|
||
|
|
*
|
||
|
|
* This program is distributed in the hope that it will be useful,
|
||
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
* GNU General Public License for more details.
|
||
|
|
*
|
||
|
|
* You should have received a copy of the GNU General Public License
|
||
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <ESP8266WiFi.h>
|
||
|
|
#include <algorithm>
|
||
|
|
|
||
|
|
// Global variables
|
||
|
|
String cmd = ""; // Gather a new AT command to this string from serial
|
||
|
|
bool cmdMode = true; // Are we in AT command mode or connected mode
|
||
|
|
bool telnet = true; // Is telnet control code handling enabled
|
||
|
|
#define SWITCH_PIN 0 // GPIO0 (programmind mode pin)
|
||
|
|
#define DEFAULT_BPS 9600 // 2400 safe for all old computers including C64
|
||
|
|
//#define USE_SWITCH 1 // Use a software reset switch
|
||
|
|
//#define DEBUG 1 // Print additional debug information to serial channel
|
||
|
|
#undef DEBUG
|
||
|
|
#undef USE_SWITCH
|
||
|
|
#define LISTEN_PORT 23 // Listen to this if not connected. Set to zero to disable.
|
||
|
|
#define RING_INTERVAL 3000 // How often to print RING when having a new incoming connection (ms)
|
||
|
|
WiFiClient tcpClient;
|
||
|
|
WiFiServer tcpServer(LISTEN_PORT);
|
||
|
|
unsigned long lastRingMs=0;// Time of last "RING" message (millis())
|
||
|
|
long myBps; // What is the current BPS setting
|
||
|
|
#define MAX_CMD_LENGTH 256 // Maximum length for AT command
|
||
|
|
char plusCount = 0; // Go to AT mode at "+++" sequence, that has to be counted
|
||
|
|
unsigned long plusTime = 0;// When did we last receive a "+++" sequence
|
||
|
|
#define LED_PIN 2 // Status LED
|
||
|
|
#define LED_TIME 1 // How many ms to keep LED on at activity
|
||
|
|
unsigned long ledTime = 0;
|
||
|
|
#define TX_BUF_SIZE 256 // Buffer where to read from serial before writing to TCP
|
||
|
|
// (that direction is very blocking by the ESP TCP stack,
|
||
|
|
// so we can't do one byte a time.)
|
||
|
|
uint8_t txBuf[TX_BUF_SIZE];
|
||
|
|
|
||
|
|
// Telnet codes
|
||
|
|
#define DO 0xfd
|
||
|
|
#define WONT 0xfc
|
||
|
|
#define WILL 0xfb
|
||
|
|
#define DONT 0xfe
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Arduino main init function
|
||
|
|
*/
|
||
|
|
void setup()
|
||
|
|
{
|
||
|
|
Serial.begin(DEFAULT_BPS);
|
||
|
|
myBps = DEFAULT_BPS;
|
||
|
|
|
||
|
|
#ifdef USE_SWITCH
|
||
|
|
pinMode(SWITCH_PIN, INPUT);
|
||
|
|
digitalWrite(SWITCH_PIN, HIGH);
|
||
|
|
#endif
|
||
|
|
|
||
|
|
Serial.println("Virtual modem");
|
||
|
|
Serial.println("=============");
|
||
|
|
Serial.println();
|
||
|
|
Serial.println("Connect to WIFI: ATWIFI<ssid>,<key>");
|
||
|
|
Serial.println("Change terminal baud rate: AT<baud>");
|
||
|
|
Serial.println("Connect by TCP: ATDT<host>:<port>");
|
||
|
|
Serial.println("See my IP address: ATIP");
|
||
|
|
Serial.println("Disable telnet command handling: ATNET0");
|
||
|
|
Serial.println("HTTP GET: ATGET<URL>");
|
||
|
|
Serial.println();
|
||
|
|
if (LISTEN_PORT > 0)
|
||
|
|
{
|
||
|
|
Serial.print("Listening to connections at port ");
|
||
|
|
Serial.print(LISTEN_PORT);
|
||
|
|
Serial.println(", which result in RING and you can answer with ATA.");
|
||
|
|
tcpServer.begin();
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Serial.println("Incoming connections are disabled.");
|
||
|
|
}
|
||
|
|
Serial.println("");
|
||
|
|
Serial.println("OK");
|
||
|
|
|
||
|
|
pinMode(LED_PIN, OUTPUT);
|
||
|
|
digitalWrite(LED_PIN, HIGH);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Turn on the LED and store the time, so the LED will be shortly after turned off
|
||
|
|
*/
|
||
|
|
void led_on(void)
|
||
|
|
{
|
||
|
|
digitalWrite(LED_PIN, LOW);
|
||
|
|
ledTime = millis();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Perform a command given in command mode
|
||
|
|
*/
|
||
|
|
void command()
|
||
|
|
{
|
||
|
|
cmd.trim();
|
||
|
|
if (cmd == "") return;
|
||
|
|
Serial.println();
|
||
|
|
String upCmd = cmd;
|
||
|
|
upCmd.toUpperCase();
|
||
|
|
|
||
|
|
long newBps = 0;
|
||
|
|
|
||
|
|
/**** Just AT ****/
|
||
|
|
if (upCmd == "AT") Serial.println("OK");
|
||
|
|
|
||
|
|
/**** Dial to host ****/
|
||
|
|
else if ((upCmd.indexOf("ATDT") == 0) || (upCmd.indexOf("ATDP") == 0) || (upCmd.indexOf("ATDI") == 0))
|
||
|
|
{
|
||
|
|
int portIndex = cmd.indexOf(":");
|
||
|
|
String host, port;
|
||
|
|
if (portIndex != -1)
|
||
|
|
{
|
||
|
|
host = cmd.substring(4, portIndex);
|
||
|
|
port = cmd.substring(portIndex + 1, cmd.length());
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
host = cmd.substring(4, cmd.length());
|
||
|
|
port = "23"; // 23 Telnet default, 2300 --> Paolo
|
||
|
|
}
|
||
|
|
Serial.print("Connecting to ");
|
||
|
|
Serial.print(host);
|
||
|
|
Serial.print(":");
|
||
|
|
Serial.println(port);
|
||
|
|
char *hostChr = new char[host.length() + 1];
|
||
|
|
host.toCharArray(hostChr, host.length() + 1);
|
||
|
|
int portInt = port.toInt();
|
||
|
|
tcpClient.setNoDelay(true); // Try to disable naggle
|
||
|
|
if (tcpClient.connect(hostChr, portInt))
|
||
|
|
{
|
||
|
|
tcpClient.setNoDelay(true); // Try to disable naggle
|
||
|
|
Serial.print("CONNECT ");
|
||
|
|
Serial.println(myBps);
|
||
|
|
cmdMode = false;
|
||
|
|
Serial.flush();
|
||
|
|
if (LISTEN_PORT > 0) tcpServer.stop();
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Serial.println("NO CARRIER");
|
||
|
|
}
|
||
|
|
delete hostChr;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**** Connect to WIFI ****/
|
||
|
|
else if (upCmd.indexOf("ATWIFI") == 0)
|
||
|
|
{
|
||
|
|
int keyIndex = cmd.indexOf(",");
|
||
|
|
String ssid, key;
|
||
|
|
if (keyIndex != -1)
|
||
|
|
{
|
||
|
|
ssid = cmd.substring(6, keyIndex);
|
||
|
|
key = cmd.substring(keyIndex + 1, cmd.length());
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ssid = cmd.substring(6, cmd.length());
|
||
|
|
key = "";
|
||
|
|
}
|
||
|
|
char *ssidChr = new char[ssid.length() + 1];
|
||
|
|
ssid.toCharArray(ssidChr, ssid.length() + 1);
|
||
|
|
char *keyChr = new char[key.length() + 1];
|
||
|
|
key.toCharArray(keyChr, key.length() + 1);
|
||
|
|
Serial.print("Connecting to ");
|
||
|
|
Serial.print(ssid);
|
||
|
|
Serial.print("/");
|
||
|
|
Serial.println(key);
|
||
|
|
WiFi.begin(ssidChr, keyChr);
|
||
|
|
for (int i=0; i<100; i++)
|
||
|
|
{
|
||
|
|
delay(100);
|
||
|
|
if (WiFi.status() == WL_CONNECTED)
|
||
|
|
{
|
||
|
|
Serial.println("OK");
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (WiFi.status() != WL_CONNECTED)
|
||
|
|
{
|
||
|
|
Serial.println("ERROR");
|
||
|
|
}
|
||
|
|
delete ssidChr;
|
||
|
|
delete keyChr;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**** Change baud rate from default ****/
|
||
|
|
else if (upCmd == "AT300") newBps = 300;
|
||
|
|
else if (upCmd == "AT1200") newBps = 1200;
|
||
|
|
else if (upCmd == "AT2400") newBps = 2400;
|
||
|
|
else if (upCmd == "AT9600") newBps = 9600;
|
||
|
|
else if (upCmd == "AT19200") newBps = 19200;
|
||
|
|
else if (upCmd == "AT38400") newBps = 38400;
|
||
|
|
else if (upCmd == "AT57600") newBps = 57600;
|
||
|
|
else if (upCmd == "AT115200") newBps = 115200;
|
||
|
|
|
||
|
|
/**** Change telnet mode ****/
|
||
|
|
else if (upCmd == "ATNET0")
|
||
|
|
{
|
||
|
|
telnet = false;
|
||
|
|
Serial.println("OK");
|
||
|
|
}
|
||
|
|
else if (upCmd == "ATNET1")
|
||
|
|
{
|
||
|
|
telnet = true;
|
||
|
|
Serial.println("OK");
|
||
|
|
}
|
||
|
|
|
||
|
|
/**** Answer to incoming connection ****/
|
||
|
|
else if ((upCmd == "ATA") && tcpServer.hasClient())
|
||
|
|
{
|
||
|
|
tcpClient = tcpServer.available();
|
||
|
|
tcpClient.setNoDelay(true); // try to disable naggle
|
||
|
|
tcpServer.stop();
|
||
|
|
Serial.print("CONNECT ");
|
||
|
|
Serial.println(myBps);
|
||
|
|
cmdMode = false;
|
||
|
|
Serial.flush();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**** See my IP address ****/
|
||
|
|
else if (upCmd == "ATIP")
|
||
|
|
{
|
||
|
|
Serial.println(WiFi.localIP());
|
||
|
|
Serial.println("OK");
|
||
|
|
}
|
||
|
|
|
||
|
|
/**** HTTP GET request ****/
|
||
|
|
else if (upCmd.indexOf("ATGET") == 0)
|
||
|
|
{
|
||
|
|
// From the URL, aquire required variables
|
||
|
|
// (12 = "ATGEThttp://")
|
||
|
|
int portIndex = cmd.indexOf(":", 12); // Index where port number might begin
|
||
|
|
int pathIndex = cmd.indexOf("/", 12); // Index first host name and possible port ends and path begins
|
||
|
|
int port;
|
||
|
|
String path, host;
|
||
|
|
if (pathIndex < 0)
|
||
|
|
{
|
||
|
|
pathIndex = cmd.length();
|
||
|
|
}
|
||
|
|
if (portIndex < 0)
|
||
|
|
{
|
||
|
|
port = 80;
|
||
|
|
portIndex = pathIndex;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
port = cmd.substring(portIndex+1, pathIndex).toInt();
|
||
|
|
}
|
||
|
|
host = cmd.substring(12, portIndex);
|
||
|
|
path = cmd.substring(pathIndex, cmd.length());
|
||
|
|
if (path == "") path = "/";
|
||
|
|
char *hostChr = new char[host.length() + 1];
|
||
|
|
host.toCharArray(hostChr, host.length() + 1);
|
||
|
|
|
||
|
|
// Debug
|
||
|
|
Serial.print("Getting path ");
|
||
|
|
Serial.print(path);
|
||
|
|
Serial.print(" from port ");
|
||
|
|
Serial.print(port);
|
||
|
|
Serial.print(" of host ");
|
||
|
|
Serial.print(host);
|
||
|
|
Serial.println("...");
|
||
|
|
|
||
|
|
// Establish connection
|
||
|
|
if (!tcpClient.connect(hostChr, port))
|
||
|
|
{
|
||
|
|
Serial.println("NO CARRIER");
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Serial.print("CONNECT ");
|
||
|
|
Serial.println(myBps);
|
||
|
|
cmdMode = false;
|
||
|
|
|
||
|
|
// Send a HTTP request before continuing the connection as usual
|
||
|
|
String request = "GET ";
|
||
|
|
request += path;
|
||
|
|
request += " HTTP/1.1\r\nHost: ";
|
||
|
|
request += host;
|
||
|
|
request += "\r\nConnection: close\r\n\r\n";
|
||
|
|
tcpClient.print(request);
|
||
|
|
}
|
||
|
|
delete hostChr;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**** Unknown command ****/
|
||
|
|
else Serial.println("ERROR");
|
||
|
|
|
||
|
|
/**** Tasks to do after command has been parsed ****/
|
||
|
|
if (newBps)
|
||
|
|
{
|
||
|
|
Serial.println("OK");
|
||
|
|
delay(150); // Sleep enough for 4 bytes at any previous baud rate to finish ("\nOK\n")
|
||
|
|
Serial.begin(newBps);
|
||
|
|
myBps = newBps;
|
||
|
|
}
|
||
|
|
|
||
|
|
cmd = "";
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Arduino main loop function
|
||
|
|
*/
|
||
|
|
void loop()
|
||
|
|
{
|
||
|
|
/**** AT command mode ****/
|
||
|
|
if (cmdMode == true)
|
||
|
|
{
|
||
|
|
// In command mode but new unanswered incoming connection on server listen socket
|
||
|
|
if ((LISTEN_PORT > 0) && (tcpServer.hasClient()))
|
||
|
|
{
|
||
|
|
// Print RING every now and then while the new incoming connection exists
|
||
|
|
if ((millis() - lastRingMs) > RING_INTERVAL)
|
||
|
|
{
|
||
|
|
Serial.println("RING");
|
||
|
|
lastRingMs = millis();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// In command mode - don't exchange with TCP but gather characters to a string
|
||
|
|
if (Serial.available())
|
||
|
|
{
|
||
|
|
char chr = Serial.read();
|
||
|
|
|
||
|
|
// Return, enter, new line, carriage return.. anything goes to end the command
|
||
|
|
if ((chr == '\n') || (chr == '\r'))
|
||
|
|
{
|
||
|
|
command();
|
||
|
|
}
|
||
|
|
// Backspace or delete deletes previous character
|
||
|
|
else if ((chr == 8) || (chr == 127))
|
||
|
|
{
|
||
|
|
cmd.remove(cmd.length() - 1);
|
||
|
|
// We don't assume that backspace is destructive
|
||
|
|
// Clear with a space
|
||
|
|
Serial.write(8);
|
||
|
|
Serial.write(' ');
|
||
|
|
Serial.write(8);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
if (cmd.length() < MAX_CMD_LENGTH) cmd.concat(chr);
|
||
|
|
Serial.print(chr);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**** Connected mode ****/
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// Transmit from terminal to TCP
|
||
|
|
if (Serial.available())
|
||
|
|
{
|
||
|
|
led_on();
|
||
|
|
|
||
|
|
// In telnet in worst case we have to escape every byte
|
||
|
|
// so leave half of the buffer always free
|
||
|
|
int max_buf_size;
|
||
|
|
if (telnet == true)
|
||
|
|
max_buf_size = TX_BUF_SIZE / 2;
|
||
|
|
else
|
||
|
|
max_buf_size = TX_BUF_SIZE;
|
||
|
|
|
||
|
|
// Read from serial, the amount available up to
|
||
|
|
// maximum size of the buffer
|
||
|
|
size_t len = std::min(Serial.available(), max_buf_size);
|
||
|
|
Serial.readBytes(&txBuf[0], len);
|
||
|
|
|
||
|
|
// Disconnect if going to AT mode with "+++" sequence
|
||
|
|
for (int i=0; i<(int)len; i++)
|
||
|
|
{
|
||
|
|
if (txBuf[i] == '+') plusCount++; else plusCount = 0;
|
||
|
|
if (plusCount >= 3)
|
||
|
|
{
|
||
|
|
plusTime = millis();
|
||
|
|
}
|
||
|
|
if (txBuf[i] != '+')
|
||
|
|
{
|
||
|
|
plusCount = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Double (escape) every 0xff for telnet, shifting the following bytes
|
||
|
|
// towards the end of the buffer from that point
|
||
|
|
if (telnet == true)
|
||
|
|
{
|
||
|
|
for (int i = len - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
if (txBuf[i] == 0xff)
|
||
|
|
{
|
||
|
|
for (int j = TX_BUF_SIZE - 1; j > i; j--)
|
||
|
|
{
|
||
|
|
txBuf[j] = txBuf[j - 1];
|
||
|
|
}
|
||
|
|
len++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Write the buffer to TCP finally
|
||
|
|
tcpClient.write(&txBuf[0], len);
|
||
|
|
yield();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Transmit from TCP to terminal
|
||
|
|
while (tcpClient.available())
|
||
|
|
{
|
||
|
|
led_on();
|
||
|
|
uint8_t rxByte = tcpClient.read();
|
||
|
|
|
||
|
|
// Is a telnet control code starting?
|
||
|
|
if ((telnet == true) && (rxByte == 0xff))
|
||
|
|
{
|
||
|
|
#ifdef DEBUG
|
||
|
|
Serial.print("<t>");
|
||
|
|
#endif
|
||
|
|
rxByte = tcpClient.read();
|
||
|
|
if (rxByte == 0xff)
|
||
|
|
{
|
||
|
|
// 2 times 0xff is just an escaped real 0xff
|
||
|
|
Serial.write(0xff); Serial.flush();
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// rxByte has now the first byte of the actual non-escaped control code
|
||
|
|
#ifdef DEBUG
|
||
|
|
Serial.print(rxByte);
|
||
|
|
Serial.print(",");
|
||
|
|
#endif
|
||
|
|
uint8_t cmdByte1 = rxByte;
|
||
|
|
rxByte = tcpClient.read();
|
||
|
|
uint8_t cmdByte2 = rxByte;
|
||
|
|
// rxByte has now the second byte of the actual non-escaped control code
|
||
|
|
#ifdef DEBUG
|
||
|
|
Serial.print(rxByte); Serial.flush();
|
||
|
|
#endif
|
||
|
|
// We are asked to do some option, respond we won't
|
||
|
|
if (cmdByte1 == DO)
|
||
|
|
{
|
||
|
|
tcpClient.write((uint8_t)255); tcpClient.write((uint8_t)WONT); tcpClient.write(cmdByte2);
|
||
|
|
}
|
||
|
|
// Server wants to do any option, allow it
|
||
|
|
else if (cmdByte1 == WILL)
|
||
|
|
{
|
||
|
|
tcpClient.write((uint8_t)255); tcpClient.write((uint8_t)DO); tcpClient.write(cmdByte2);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#ifdef DEBUG
|
||
|
|
Serial.print("</t>");
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// Non-control codes pass through freely
|
||
|
|
Serial.write(rxByte); Serial.flush();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Disconnect if programming mode PIN (GPIO0) is switched to GND
|
||
|
|
#ifdef USE_SWITCH
|
||
|
|
if ((tcpClient.connected()) && (digitalRead(SWITCH_PIN) == LOW))
|
||
|
|
{
|
||
|
|
tcpClient.stop();
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
|
||
|
|
// If we have received "+++" as last bytes from serial port and there
|
||
|
|
// has been over a second without any more bytes, disconnect
|
||
|
|
if (plusCount >= 3)
|
||
|
|
{
|
||
|
|
if (millis() - plusTime > 1000)
|
||
|
|
{
|
||
|
|
tcpClient.stop();
|
||
|
|
plusCount = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Go to command mode if TCP disconnected and not in command mode
|
||
|
|
if ((!tcpClient.connected()) && (cmdMode == false))
|
||
|
|
{
|
||
|
|
cmdMode = true;
|
||
|
|
Serial.println("NO CARRIER");
|
||
|
|
if (LISTEN_PORT > 0) tcpServer.begin();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Turn off tx/rx led if it has been lit long enough to be visible
|
||
|
|
if (millis() - ledTime > LED_TIME) digitalWrite(LED_PIN, HIGH);
|
||
|
|
}
|