ESPTerm - ESP8266 terminal emulator. Branches: [master] patches, [work] next release
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
espterm-firmware/user/cgi_sockets.c

397 lines
9.5 KiB

#include <esp8266.h>
#include <httpd.h>
#include <cgiwebsocket.h>
#include "cgi_sockets.h"
#include "screen.h"
#include "uart_buffer.h"
#include "ansi_parser.h"
#include "jstring.h"
#include "uart_driver.h"
// Heartbeat interval in ms
#define HB_TIME 1000
// Buffer size (sent in one go)
// Must be less than httpd sendbuf
#define SOCK_BUF_LEN 2000
// flags for screen update timeouts
volatile bool notify_available = true;
volatile bool notify_cooldown = false;
/** True if we sent XOFF to browser to stop uploading,
* and we have to tell it we're ready again */
volatile bool browser_wants_xon = false;
static ETSTimer notifyContentTim;
static ETSTimer notifyLabelsTim;
static ETSTimer notifyCooldownTim;
static ETSTimer heartbeatTim;
volatile int active_clients = 0;
// we're trying to do a kind of mutex here, without the actual primitives
// this might glitch, very rarely.
// it's recommended to put some delay between setting labels and updating the screen.
static void resetHeartbeatTimer(void);
/**
* Cooldown delay is over
* @param arg
*/
static void ICACHE_FLASH_ATTR
notifyCooldownTimCb(void *arg)
{
notify_cooldown = false;
}
/**
* Tell browser we have new content
* @param arg
*/
static void ICACHE_FLASH_ATTR
notifyContentTimCb(void *arg)
{
Websock *ws = arg;
void *data = NULL;
int max_bl, total_bl;
char sock_buff[SOCK_BUF_LEN];
cgiWebsockMeasureBacklog(URL_WS_UPDATE, &max_bl, &total_bl);
if (!notify_available || notify_cooldown || (max_bl > 2048)) { // do not send if we have anything significant backlogged
// postpone a little
TIMER_START(&notifyContentTim, notifyContentTimCb, 4, 0);
inp_dbg("postpone notify content");
return;
}
notify_available = false;
for (int i = 0; i < 20; i++) {
httpd_cgi_state cont = screenSerializeToBuffer(sock_buff, SOCK_BUF_LEN, &data);
int flg = 0;
if (cont == HTTPD_CGI_MORE) flg |= WEBSOCK_FLAG_MORE;
if (i > 0) flg |= WEBSOCK_FLAG_CONT;
if (ws) {
cgiWebsocketSend(ws, sock_buff, (int) strlen(sock_buff), flg);
} else {
cgiWebsockBroadcast(URL_WS_UPDATE, sock_buff, (int) strlen(sock_buff), flg);
resetHeartbeatTimer();
}
if (cont == HTTPD_CGI_DONE) break;
system_soft_wdt_feed();
}
// cleanup
screenSerializeToBuffer(NULL, SOCK_BUF_LEN, &data);
notify_cooldown = true;
notify_available = true;
TIMER_START(&notifyCooldownTim, notifyCooldownTimCb, termconf->display_cooldown_ms, 0);
}
/**
* Tell browsers about the new text labels
* @param arg
*/
static void ICACHE_FLASH_ATTR
notifyLabelsTimCb(void *arg)
{
Websock *ws = arg;
char sock_buff[SOCK_BUF_LEN];
if (!notify_available || notify_cooldown) {
// postpone a little
TIMER_START(&notifyLabelsTim, notifyLabelsTimCb, 7, 0);
inp_dbg("postpone notify labels");
return;
}
notify_available = false;
screenSerializeLabelsToBuffer(sock_buff, SOCK_BUF_LEN);
if (ws) {
cgiWebsocketSend(ws, sock_buff, (int) strlen(sock_buff), 0);
} else {
cgiWebsockBroadcast(URL_WS_UPDATE, sock_buff, (int) strlen(sock_buff), 0);
resetHeartbeatTimer();
}
notify_cooldown = true;
notify_available = true;
TIMER_START(&notifyCooldownTim, notifyCooldownTimCb, termconf->display_cooldown_ms, 0);
}
/** Beep */
void ICACHE_FLASH_ATTR
send_beep(void)
{
if (active_clients == 0) return;
// here's some potential for a race error with the other broadcast functions :C
cgiWebsockBroadcast(URL_WS_UPDATE, "B", 1, 0);
resetHeartbeatTimer();
}
/** Pop-up notification (from iTerm2) */
void ICACHE_FLASH_ATTR
notify_growl(char *msg)
{
if (active_clients == 0) return;
// TODO via timer...
// here's some potential for a race error with the other broadcast functions :C
cgiWebsockBroadcast(URL_WS_UPDATE, msg, (int) strlen(msg), 0);
resetHeartbeatTimer();
}
/**
* Broadcast screen state to sockets.
* This is a callback for the Screen module,
* called after each visible screen modification.
*/
void ICACHE_FLASH_ATTR screen_notifyChange(ScreenNotifyChangeTopic topic)
{
if (active_clients == 0) return;
// this is probably not needed here - ensure timeout is not 0
if (termconf->display_tout_ms == 0)
termconf->display_tout_ms = SCR_DEF_DISPLAY_TOUT_MS;
// NOTE: the timers are restarted if already running
if (topic == CHANGE_LABELS) {
// separate timer from content change timer, to avoid losing that update
TIMER_START(&notifyLabelsTim, notifyLabelsTimCb, termconf->display_tout_ms+2, 0); // this delay is useful when both are fired at once on screen reset
}
else if (topic == CHANGE_CONTENT) {
// throttle delay
TIMER_START(&notifyContentTim, notifyContentTimCb, termconf->display_tout_ms, 0);
}
}
/**
* mouse event rx
* @param evt - event type: p, r, m
* @param y - coord 0-based
* @param x - coord 0-based
* @param button - which button, 1-based. 0=none
* @param mods - modifier keys bitmap: meta|alt|shift|ctrl
*/
static void ICACHE_FLASH_ATTR sendMouseAction(char evt, int y, int x, int button, u8 mods)
{
// one-based
x++;
y++;
bool ctrl = (mods & 1) > 0;
bool shift = (mods & 2) > 0;
bool alt = (mods & 4) > 0;
bool meta = (mods & 8) > 0;
enum MTM mtm = mouse_tracking.mode;
enum MTE mte = mouse_tracking.encoding;
// No message on release in X10 mode
if (mtm == MTM_X10 && (button == 0 || evt == 'r')) {
return;
}
if (evt == 'm' && mtm != MTM_BUTTON_MOTION && mtm != MTM_ANY_MOTION) {
return;
}
if (evt == 'm' && mtm == MTM_BUTTON_MOTION && button == 0) {
return;
}
int eventcode = 0;
if (mtm == MTM_X10) {
eventcode = button-1;
}
else {
if (button == 0 || (evt == 'r' && mte != MTE_SGR)) eventcode = 3; // release
else if (button == 1) eventcode = 0;
else if (button == 2) eventcode = 1;
else if (button == 3) eventcode = 2;
else if (button == 4) eventcode = 64;
else if (button == 5) eventcode = 65;
if (shift) eventcode |= 4;
if (alt || meta) eventcode |= 8;
if (ctrl) eventcode |= 16;
if (mtm == MTM_BUTTON_MOTION || mtm == MTM_ANY_MOTION) {
if (evt == 'm') {
eventcode |= 32;
}
}
}
// Encode
char buf[20];
buf[0] = 0;
if (mte == MTE_SIMPLE || mte == MTE_UTF8) {
// strictly, for UTF8 this will break if any coord is over 127,
// but that is unlikely due to screen size limitations in ESPTerm
sprintf(buf, "\x1b[M%c%c%c", (u8)(32+eventcode), (u8)(32+x), (u8)(32+y));
}
else if (mte == MTE_SGR) {
sprintf(buf, "\x1b[<%d;%d;%d%c", eventcode, x, y, (evt == 'p'||(evt=='m'&&button>0)) ? 'M' : 'm');
}
else if (mte == MTE_URXVT) {
sprintf(buf, "\x1b[%d;%d;%dM", (u8)(32+eventcode), (u8)(32+x), (u8)(32+y));
}
UART_SendAsync(buf, -1);
}
/** Socket received a message */
static void ICACHE_FLASH_ATTR updateSockRx(Websock *ws, char *data, int len, int flags)
{
// Add terminator if missing (seems to randomly happen)
data[len] = 0;
inp_dbg("Sock RX str: %s, len %d", data, len);
int y, x, m, b;
u8 btnNum;
char c = data[0];
switch (c) {
case 's':
// pass string verbatim
if (termconf_live.loopback) {
for (int i = 1; i < len; i++) {
ansi_parser(data[i]);
}
}
UART_SendAsync(data+1, -1);
// TODO base this on the actual buffer empty space, not rx chunk size
if ((UART_AsyncTxGetEmptySpace() < 256) && !browser_wants_xon) {
UART_WriteChar(UART1, '-', 100);
cgiWebsockBroadcast(URL_WS_UPDATE, "-", 1, 0);
browser_wants_xon = true;
system_soft_wdt_feed();
}
break;
case 'b':
// action button press
btnNum = (u8) (data[1]);
if (btnNum > 0 && btnNum < 10) {
UART_SendAsync(termconf_live.btn_msg[btnNum-1], -1);
}
break;
case 'i':
// requests initial load
inp_dbg("Client requests initial load");
notifyContentTimCb(ws); // delay??
notifyLabelsTimCb(ws);
break;
case 'm':
case 'p':
case 'r':
if (mouse_tracking.mode == MTM_NONE) break; // no need to parse, not enabled
// mouse move
y = parse2B(data+1); // row, 0-based
x = parse2B(data+3); // column, 0-based
b = parse2B(data+5); // mouse button, 0 = none, 1-5 = button number
m = parse2B(data+7); // modifier keys held
sendMouseAction(c,y,x,b,m);
break;
default:
inp_warn("Bad command.");
}
}
/** Send a heartbeat msg */
static void ICACHE_FLASH_ATTR heartbeatTimCb(void *unused)
{
if (active_clients > 0) {
if (notify_available) {
inp_dbg(".");
// Heartbeat packet - indicate we're still connected
// JS reloads the page if heartbeat is lost for a couple seconds
cgiWebsockBroadcast(URL_WS_UPDATE, ".", 1, 0);
// schedule next tick
TIMER_START(&heartbeatTim, heartbeatTimCb, HB_TIME, 0);
} else {
// postpone...
TIMER_START(&heartbeatTim, heartbeatTimCb, 10, 0);
inp_dbg("postpone heartbeat");
}
}
}
static void ICACHE_FLASH_ATTR resetHeartbeatTimer(void)
{
TIMER_START(&heartbeatTim, heartbeatTimCb, HB_TIME, 0);
}
static void ICACHE_FLASH_ATTR closeSockCb(Websock *ws)
{
active_clients--;
if (active_clients <= 0) {
active_clients = 0;
if (mouse_tracking.focus_tracking) {
UART_SendAsync("\x1b[O", 3);
}
// stop the timer
os_timer_disarm(&heartbeatTim);
}
}
/** Socket connected for updates */
void ICACHE_FLASH_ATTR updateSockConnect(Websock *ws)
{
inp_info("Socket connected to "URL_WS_UPDATE);
ws->recvCb = updateSockRx;
ws->closeCb = closeSockCb;
if (active_clients == 0) {
if (mouse_tracking.focus_tracking) {
UART_SendAsync("\x1b[I", 3);
}
resetHeartbeatTimer();
}
active_clients++;
}
ETSTimer xonTim;
static void ICACHE_FLASH_ATTR notify_empty_txbuf_cb(void *unused)
{
UART_WriteChar(UART1, '+', 100);
cgiWebsockBroadcast(URL_WS_UPDATE, "+", 1, 0);
resetHeartbeatTimer();
browser_wants_xon = false;
}
void notify_empty_txbuf(void)
{
if (browser_wants_xon) {
TIMER_START(&xonTim, notify_empty_txbuf_cb, 1, 0);
}
}