🎉 Replace standard webserver with async
This commit is contained in:
+6
-3
@@ -3,7 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||
<meta viewport-fit="cover">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<title>Spot Micro</title>
|
||||
<meta http-equiv="content-security-policy" content="">
|
||||
<style>
|
||||
body {
|
||||
@@ -42,7 +45,7 @@
|
||||
<script>
|
||||
const init = () => {
|
||||
|
||||
let websocket = new WebSocket("ws://192.168.0.175:81")
|
||||
let websocket = new WebSocket(`ws://${location.hostname}`)
|
||||
websocket.onopen = (event) => {
|
||||
console.log(event);
|
||||
};
|
||||
@@ -50,7 +53,7 @@
|
||||
console.log(event.data);
|
||||
}
|
||||
|
||||
document.getElementById("stream").src = "//192.168.0.175/mjpeg/1"
|
||||
document.getElementById("stream").src = `//${location.hostname}/stream`
|
||||
}
|
||||
init()
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <esp_camera.h>
|
||||
|
||||
typedef struct {
|
||||
camera_fb_t * fb;
|
||||
size_t index;
|
||||
} camera_frame_t;
|
||||
|
||||
#define PART_BOUNDARY "123456789000000000000987654321"
|
||||
static const char* STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
|
||||
static const char* STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
|
||||
static const char* STREAM_PART = "Content-Type: %s\r\nContent-Length: %u\r\n\r\n";
|
||||
|
||||
static const char * JPG_CONTENT_TYPE = "image/jpeg";
|
||||
|
||||
class AsyncJpegStreamResponse: public AsyncAbstractResponse {
|
||||
private:
|
||||
camera_frame_t _frame;
|
||||
size_t _index;
|
||||
size_t _jpg_buf_len;
|
||||
uint8_t * _jpg_buf;
|
||||
uint64_t lastAsyncRequest;
|
||||
public:
|
||||
AsyncJpegStreamResponse(){
|
||||
_callback = nullptr;
|
||||
_code = 200;
|
||||
_contentLength = 0;
|
||||
_contentType = STREAM_CONTENT_TYPE;
|
||||
_sendContentLength = false;
|
||||
_chunked = true;
|
||||
_index = 0;
|
||||
_jpg_buf_len = 0;
|
||||
_jpg_buf = NULL;
|
||||
lastAsyncRequest = 0;
|
||||
memset(&_frame, 0, sizeof(camera_frame_t));
|
||||
}
|
||||
~AsyncJpegStreamResponse(){
|
||||
if(_frame.fb){
|
||||
if(_frame.fb->format != PIXFORMAT_JPEG){
|
||||
free(_jpg_buf);
|
||||
}
|
||||
esp_camera_fb_return(_frame.fb);
|
||||
}
|
||||
}
|
||||
bool _sourceValid() const {
|
||||
return true;
|
||||
}
|
||||
virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override {
|
||||
size_t ret = _content(buf, maxLen, _index);
|
||||
if(ret != RESPONSE_TRY_AGAIN){
|
||||
_index += ret;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
size_t _content(uint8_t *buffer, size_t maxLen, size_t index){
|
||||
if(!_frame.fb || _frame.index == _jpg_buf_len){
|
||||
if(index && _frame.fb){
|
||||
uint64_t end = (uint64_t)micros();
|
||||
int fp = (end - lastAsyncRequest) / 1000;
|
||||
log_printf("Size: %uKB, Time: %ums (%.1ffps)\n", _jpg_buf_len/1024, fp);
|
||||
lastAsyncRequest = end;
|
||||
if(_frame.fb->format != PIXFORMAT_JPEG){
|
||||
free(_jpg_buf);
|
||||
}
|
||||
esp_camera_fb_return(_frame.fb);
|
||||
_frame.fb = NULL;
|
||||
_jpg_buf_len = 0;
|
||||
_jpg_buf = NULL;
|
||||
}
|
||||
if(maxLen < (strlen(STREAM_BOUNDARY) + strlen(STREAM_PART) + strlen(JPG_CONTENT_TYPE) + 8)){
|
||||
//log_w("Not enough space for headers");
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
//get frame
|
||||
_frame.index = 0;
|
||||
|
||||
_frame.fb = esp_camera_fb_get();
|
||||
if (_frame.fb == NULL) {
|
||||
log_e("Camera frame failed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(_frame.fb->format != PIXFORMAT_JPEG){
|
||||
unsigned long st = millis();
|
||||
bool jpeg_converted = frame2jpg(_frame.fb, 80, &_jpg_buf, &_jpg_buf_len);
|
||||
if(!jpeg_converted){
|
||||
log_e("JPEG compression failed");
|
||||
esp_camera_fb_return(_frame.fb);
|
||||
_frame.fb = NULL;
|
||||
_jpg_buf_len = 0;
|
||||
_jpg_buf = NULL;
|
||||
return 0;
|
||||
}
|
||||
log_i("JPEG: %lums, %uB", millis() - st, _jpg_buf_len);
|
||||
} else {
|
||||
_jpg_buf_len = _frame.fb->len;
|
||||
_jpg_buf = _frame.fb->buf;
|
||||
}
|
||||
|
||||
//send boundary
|
||||
size_t blen = 0;
|
||||
if(index){
|
||||
blen = strlen(STREAM_BOUNDARY);
|
||||
memcpy(buffer, STREAM_BOUNDARY, blen);
|
||||
buffer += blen;
|
||||
}
|
||||
//send header
|
||||
size_t hlen = sprintf((char *)buffer, STREAM_PART, JPG_CONTENT_TYPE, _jpg_buf_len);
|
||||
buffer += hlen;
|
||||
//send frame
|
||||
hlen = maxLen - hlen - blen;
|
||||
if(hlen > _jpg_buf_len){
|
||||
maxLen -= hlen - _jpg_buf_len;
|
||||
hlen = _jpg_buf_len;
|
||||
}
|
||||
memcpy(buffer, _jpg_buf, hlen);
|
||||
_frame.index += hlen;
|
||||
return maxLen;
|
||||
}
|
||||
|
||||
size_t available = _jpg_buf_len - _frame.index;
|
||||
if(maxLen > available){
|
||||
maxLen = available;
|
||||
}
|
||||
memcpy(buffer, _jpg_buf+_frame.index, maxLen);
|
||||
_frame.index += maxLen;
|
||||
|
||||
return maxLen;
|
||||
}
|
||||
};
|
||||
|
||||
void streamJpg(AsyncWebServerRequest *request){
|
||||
AsyncJpegStreamResponse *response = new AsyncJpegStreamResponse();
|
||||
if(!response){
|
||||
request->send(501);
|
||||
return;
|
||||
}
|
||||
response->addHeader("Access-Control-Allow-Origin", "*");
|
||||
request->send(response);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){
|
||||
if(type == WS_EVT_CONNECT){
|
||||
Serial.printf("ws[%s][%u] connect\n", server->url(), client->id());
|
||||
client->printf("Hello Client %u :)", client->id());
|
||||
client->ping();
|
||||
} else if(type == WS_EVT_DISCONNECT){
|
||||
Serial.printf("ws[%s][%u] disconnect\n", server->url(), client->id());
|
||||
} else if(type == WS_EVT_ERROR){
|
||||
Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data);
|
||||
} else if(type == WS_EVT_PONG){
|
||||
Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:"");
|
||||
} else if(type == WS_EVT_DATA){
|
||||
AwsFrameInfo * info = (AwsFrameInfo*)arg;
|
||||
String msg = "";
|
||||
if(info->final && info->index == 0 && info->len == len){
|
||||
//the whole message is in a single frame and we got all of it's data
|
||||
Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len);
|
||||
|
||||
if(info->opcode == WS_TEXT){
|
||||
for(size_t i=0; i < info->len; i++) {
|
||||
msg += (char) data[i];
|
||||
}
|
||||
} else {
|
||||
char buff[3];
|
||||
for(size_t i=0; i < info->len; i++) {
|
||||
sprintf(buff, "%02x ", (uint8_t) data[i]);
|
||||
msg += buff ;
|
||||
}
|
||||
}
|
||||
Serial.printf("%s\n",msg.c_str());
|
||||
|
||||
if(info->opcode == WS_TEXT)
|
||||
client->text("I got your text message");
|
||||
else
|
||||
client->binary("I got your binary message");
|
||||
} else {
|
||||
//message is comprised of multiple frames or the frame is split into multiple packets
|
||||
if(info->index == 0){
|
||||
if(info->num == 0)
|
||||
Serial.printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary");
|
||||
Serial.printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len);
|
||||
}
|
||||
|
||||
Serial.printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT)?"text":"binary", info->index, info->index + len);
|
||||
|
||||
if(info->opcode == WS_TEXT){
|
||||
for(size_t i=0; i < len; i++) {
|
||||
msg += (char) data[i];
|
||||
}
|
||||
} else {
|
||||
char buff[3];
|
||||
for(size_t i=0; i < len; i++) {
|
||||
sprintf(buff, "%02x ", (uint8_t) data[i]);
|
||||
msg += buff ;
|
||||
}
|
||||
}
|
||||
Serial.printf("%s\n",msg.c_str());
|
||||
|
||||
if((info->index + len) == info->len){
|
||||
Serial.printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len);
|
||||
if(info->final){
|
||||
Serial.printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary");
|
||||
if(info->message_opcode == WS_TEXT)
|
||||
client->text("I got your text message");
|
||||
else
|
||||
client->binary("I got your binary message");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -14,9 +14,9 @@ board = esp32cam
|
||||
monitor_speed = 115200
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.7
|
||||
teckel12/NewPing@^1.9.7
|
||||
rfetick/MPU6050_light@^1.1.0
|
||||
adafruit/Adafruit PWM Servo Driver Library@^2.4.1
|
||||
adafruit/Adafruit SSD1306@^2.5.7
|
||||
adafruit/Adafruit GFX Library@^1.11.5
|
||||
links2004/WebSockets@^2.4.1
|
||||
|
||||
+11
-201
@@ -1,8 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include "OV2640.h"
|
||||
#include <WiFi.h>
|
||||
#include <WebServer.h>
|
||||
#include <WiFiClient.h>
|
||||
|
||||
#include <index_other.h>
|
||||
|
||||
@@ -18,6 +16,8 @@
|
||||
#include <FFat.h>
|
||||
#endif
|
||||
#if FILESYSTEM == SPIFFS
|
||||
#include <AsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <SPIFFS.h>
|
||||
#endif
|
||||
|
||||
@@ -36,25 +36,20 @@
|
||||
#ifdef USE_PWM
|
||||
#include <Adafruit_PWMServoDriver.h>
|
||||
#endif
|
||||
#ifdef USE_WEBSOCKET
|
||||
#include <WebSocketsServer.h>
|
||||
#endif
|
||||
|
||||
#include <AsyncJpegStreamHandler.h>
|
||||
#include <CaptivePortalHandler.h>
|
||||
#include <WebsocketHandler.h>
|
||||
|
||||
#define CAMERA_MODEL_AI_THINKER
|
||||
#include <camera_pins.h>
|
||||
|
||||
const char* ssid = "";
|
||||
const char* password = "";
|
||||
|
||||
const char HEADER[] = "HTTP/1.1 200 OK\r\n" \
|
||||
"Access-Control-Allow-Origin: *\r\n" \
|
||||
"Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321\r\n";
|
||||
const char BOUNDARY[] = "\r\n--123456789000000000000987654321\r\n";
|
||||
const char CTNTTYPE[] = "Content-Type: image/jpeg\r\nContent-Length: ";
|
||||
const int hdrLen = strlen(HEADER);
|
||||
const int bdrLen = strlen(BOUNDARY);
|
||||
const int cntLen = strlen(CTNTTYPE);
|
||||
DNSServer dnsServer;
|
||||
AsyncWebSocket ws(WEBSOCKET_PATH);
|
||||
AsyncEventSource events(EVENTSOURCE_PATH);
|
||||
AsyncWebServer server(HTTP_PORT);
|
||||
|
||||
#ifdef USE_BUTTON
|
||||
int buttonLed = 2;
|
||||
@@ -87,196 +82,26 @@ NewPing sonar[2] = {
|
||||
bool OLED_READY = false;
|
||||
#endif
|
||||
|
||||
#ifdef USE_WEBSOCKET
|
||||
WebSocketsServer webSocket = WebSocketsServer(81);
|
||||
#endif
|
||||
OV2640 cam;
|
||||
|
||||
WebServer server(80);
|
||||
|
||||
long timer = 0;
|
||||
|
||||
String formatBytes(size_t bytes) {
|
||||
if (bytes < 1024) {
|
||||
return String(bytes) + "B";
|
||||
} else if (bytes < (1024 * 1024)) {
|
||||
return String(bytes / 1024.0) + "KB";
|
||||
} else if (bytes < (1024 * 1024 * 1024)) {
|
||||
return String(bytes / 1024.0 / 1024.0) + "MB";
|
||||
} else {
|
||||
return String(bytes / 1024.0 / 1024.0 / 1024.0) + "GB";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String getContentType(String filename) {
|
||||
if (server.hasArg("download")) {
|
||||
return "application/octet-stream";
|
||||
} else if (filename.endsWith(".htm")) {
|
||||
return "text/html";
|
||||
} else if (filename.endsWith(".html")) {
|
||||
return "text/html";
|
||||
} else if (filename.endsWith(".css")) {
|
||||
return "text/css";
|
||||
} else if (filename.endsWith(".js")) {
|
||||
return "application/javascript";
|
||||
} else if (filename.endsWith(".png")) {
|
||||
return "image/png";
|
||||
} else if (filename.endsWith(".gif")) {
|
||||
return "image/gif";
|
||||
} else if (filename.endsWith(".jpg")) {
|
||||
return "image/jpeg";
|
||||
} else if (filename.endsWith(".ico")) {
|
||||
return "image/x-icon";
|
||||
} else if (filename.endsWith(".xml")) {
|
||||
return "text/xml";
|
||||
} else if (filename.endsWith(".pdf")) {
|
||||
return "application/x-pdf";
|
||||
} else if (filename.endsWith(".zip")) {
|
||||
return "application/x-zip";
|
||||
} else if (filename.endsWith(".gz")) {
|
||||
return "application/x-gzip";
|
||||
}
|
||||
return "text/plain";
|
||||
|
||||
}
|
||||
|
||||
bool exists(String path){
|
||||
bool yes = false;
|
||||
File file = FILESYSTEM.open(path, "r");
|
||||
if(!file.isDirectory()){
|
||||
yes = true;
|
||||
}
|
||||
file.close();
|
||||
return yes;
|
||||
}
|
||||
|
||||
bool loadFromSdCard(String path) {
|
||||
Serial.println("handleFileRead: " + path);
|
||||
if (path.endsWith("/")) {
|
||||
path += "index.htm";
|
||||
}
|
||||
String contentType = getContentType(path);
|
||||
String pathWithGz = path + ".gz";
|
||||
if (exists(pathWithGz) || exists(path)) {
|
||||
if (exists(pathWithGz)) {
|
||||
path += ".gz";
|
||||
}
|
||||
File file = FILESYSTEM.open(path, "r");
|
||||
Serial.println(file);
|
||||
server.streamFile(file, contentType);
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef USE_WEBSOCKET
|
||||
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
|
||||
Serial.printf("Got event [%u]", type);
|
||||
switch(type) {
|
||||
case WStype_DISCONNECTED:
|
||||
Serial.printf("[%u] Disconnected!\n", num);
|
||||
break;
|
||||
case WStype_CONNECTED:
|
||||
{
|
||||
IPAddress ip = webSocket.remoteIP(num);
|
||||
Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
|
||||
|
||||
// send message to client
|
||||
webSocket.sendTXT(num, "Connected");
|
||||
}
|
||||
break;
|
||||
case WStype_TEXT:
|
||||
Serial.printf("[%u] get Text: %s\n", num, payload);
|
||||
|
||||
// send message to client
|
||||
// webSocket.sendTXT(num, "message here");
|
||||
|
||||
// send data to all connected clients
|
||||
// webSocket.broadcastTXT("message here");
|
||||
break;
|
||||
case WStype_BIN:
|
||||
Serial.printf("[%u] get binary length: %u\n", num, length);
|
||||
//hexdump(payload, length);
|
||||
|
||||
// send message to client
|
||||
// webSocket.sendBIN(num, payload, length);
|
||||
break;
|
||||
case WStype_ERROR:
|
||||
case WStype_FRAGMENT_TEXT_START:
|
||||
case WStype_FRAGMENT_BIN_START:
|
||||
case WStype_FRAGMENT:
|
||||
case WStype_FRAGMENT_FIN:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BUTTON
|
||||
static unsigned long last_interrupt_time = 0;
|
||||
#endif
|
||||
void handle_jpg_stream(void)
|
||||
{
|
||||
char buf[32];
|
||||
int s;
|
||||
|
||||
WiFiClient client = server.client();
|
||||
|
||||
client.write(HEADER, hdrLen);
|
||||
client.write(BOUNDARY, bdrLen);
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (!client.connected()) break;
|
||||
cam.run();
|
||||
s = cam.getSize();
|
||||
client.write(CTNTTYPE, cntLen);
|
||||
sprintf( buf, "%d\r\n\r\n", s );
|
||||
client.write(buf, strlen(buf));
|
||||
client.write((char *)cam.getfb(), s);
|
||||
client.write(BOUNDARY, bdrLen);
|
||||
}
|
||||
}
|
||||
|
||||
const char JHEADER[] = "HTTP/1.1 200 OK\r\n" \
|
||||
"Content-disposition: inline; filename=capture.jpg\r\n" \
|
||||
"Content-type: image/jpeg\r\n\r\n";
|
||||
const int jhdLen = strlen(JHEADER);
|
||||
|
||||
void handle_jpg(void)
|
||||
{
|
||||
WiFiClient client = server.client();
|
||||
|
||||
if (!client.connected()) return;
|
||||
cam.run();
|
||||
client.write(JHEADER, jhdLen);
|
||||
client.write((char *)cam.getfb(), cam.getSize());
|
||||
}
|
||||
|
||||
void handle_stream_viewing()
|
||||
{
|
||||
char temp[index_simple_html_len];
|
||||
|
||||
snprintf(temp, index_simple_html_len, index_simple_html);
|
||||
server.send(200, "text/html; charset=utf-8", index_simple_html);
|
||||
}
|
||||
|
||||
void handleNotFound()
|
||||
{
|
||||
if (loadFromSdCard(server.uri())) {
|
||||
Serial.println("Sending file from SPIFFS");
|
||||
return;
|
||||
}
|
||||
String message = "Server is running!\n\n";
|
||||
message += "URI: ";
|
||||
message += server.uri();
|
||||
message += "\nMethod: ";
|
||||
message += (server.method() == HTTP_GET) ? "GET" : "POST";
|
||||
message += "\nArguments: ";
|
||||
message += server.args();
|
||||
message += "\n";
|
||||
server.send(200, "text/plain", message);
|
||||
}
|
||||
#ifdef USE_MPU
|
||||
bool setupMPU(){
|
||||
byte status = mpu.begin();
|
||||
@@ -396,27 +221,12 @@ void setup() {
|
||||
}
|
||||
#endif
|
||||
|
||||
server.on("/mjpeg/1", HTTP_GET, handle_jpg_stream);
|
||||
server.on("/jpg", HTTP_GET, handle_jpg);
|
||||
server.on("/", HTTP_GET, handle_stream_viewing);
|
||||
server.onNotFound(handleNotFound);
|
||||
server.begin();
|
||||
|
||||
webSocket.begin();
|
||||
webSocket.onEvent(webSocketEvent);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
server.handleClient();
|
||||
webSocket.loop();
|
||||
|
||||
|
||||
if(millis() - timer > 500) {
|
||||
Serial.println("Sending message to websocket client");
|
||||
String letme = "LET ME TELL YOU SOMETHING";
|
||||
if(webSocket.broadcastTXT(letme)){
|
||||
Serial.println("Send message to websocket client");
|
||||
}
|
||||
timer = millis();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user