feature/web #4
23
cmds.sh
Normal file
23
cmds.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
# -> file not found
|
||||
|
||||
mklittlefs -c data \
|
||||
-s 0x9000 \
|
||||
-b 4096 \
|
||||
-p 256 \
|
||||
littlefs.bin
|
||||
|
||||
|
||||
esptool.py --port /dev/ttyUSB0 \
|
||||
--baud 115200 \
|
||||
write_flash 0x71000 littlefs.bin
|
||||
|
||||
# pio -->
|
||||
|
||||
"mklittlefs" -c data -p 256 -b 4096 -s 262144 .pio/build/esp01_1m/littlefs.bin
|
||||
|
||||
esptool.py --before no_reset \
|
||||
--after soft_reset \
|
||||
--chip esp8266 \
|
||||
--port "/dev/ttyUSB0" \
|
||||
--baud 115200 \
|
||||
write_flash 765952 .pio/build/esp01_1m/littlefs.bin
|
||||
File diff suppressed because it is too large
Load Diff
19
ctl.sh
19
ctl.sh
@@ -27,6 +27,25 @@ function flash {
|
||||
${@:-info}
|
||||
}
|
||||
|
||||
function mkfs {
|
||||
~/bin/mklittlefs -c data \
|
||||
-s 0x9000 \
|
||||
-b 4096 \
|
||||
-p 256 \
|
||||
littlefs.bin
|
||||
}
|
||||
|
||||
function flashfs {
|
||||
esptool.py --port /dev/ttyUSB0 \
|
||||
--baud 115200 \
|
||||
write_flash 0xbb000 littlefs.bin
|
||||
}
|
||||
|
||||
function uploadfs {
|
||||
echo "Uploading files to LittleFS..."
|
||||
pio run -e $1 -t uploadfs
|
||||
}
|
||||
|
||||
function ota {
|
||||
function update {
|
||||
echo "Updating node at $1 with $2... "
|
||||
|
||||
165
data/index.html
Normal file
165
data/index.html
Normal file
@@ -0,0 +1,165 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Spore 🍄 hehehehe </title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.status-card {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.status-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
.status-value {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.api-section {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.api-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
.api-link {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 25px;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
.api-link:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🍄 Spore Node</h1>
|
||||
|
||||
<div class="status-grid">
|
||||
<div class="status-card">
|
||||
<h3>Node Status</h3>
|
||||
<div class="status-value" id="nodeStatus">Loading...</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Network</h3>
|
||||
<div class="status-value" id="networkStatus">Loading...</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Tasks</h3>
|
||||
<div class="status-value" id="taskStatus">Loading...</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Cluster</h3>
|
||||
<div class="status-value" id="clusterStatus">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h2>API Endpoints</h2>
|
||||
<div class="api-links">
|
||||
<a href="/api/node/status" class="api-link">Node Status</a>
|
||||
<a href="/api/network/status" class="api-link">Network Status</a>
|
||||
<a href="/api/tasks/status" class="api-link">Tasks Status</a>
|
||||
<a href="/api/cluster/members" class="api-link">Cluster Members</a>
|
||||
<a href="/api/node/endpoints" class="api-link">All Endpoints</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Load initial data
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const [nodeResponse, networkResponse, taskResponse, clusterResponse] = await Promise.all([
|
||||
fetch('/api/node/status').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/network/status').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/tasks/status').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/cluster/members').then(r => r.json()).catch(() => null)
|
||||
]);
|
||||
|
||||
// Update node status
|
||||
if (nodeResponse) {
|
||||
document.getElementById('nodeStatus').textContent =
|
||||
nodeResponse.uptime ? `${Math.floor(nodeResponse.uptime / 1000)}s` : 'Online';
|
||||
}
|
||||
|
||||
// Update network status
|
||||
if (networkResponse) {
|
||||
document.getElementById('networkStatus').textContent =
|
||||
networkResponse.connected ? 'Connected' : 'Disconnected';
|
||||
}
|
||||
|
||||
// Update task status
|
||||
if (taskResponse) {
|
||||
const activeTasks = taskResponse.tasks ?
|
||||
taskResponse.tasks.filter(t => t.status === 'running').length : 0;
|
||||
document.getElementById('taskStatus').textContent = `${activeTasks} active`;
|
||||
}
|
||||
|
||||
// Update cluster status
|
||||
if (clusterResponse) {
|
||||
const memberCount = clusterResponse.members ?
|
||||
Object.keys(clusterResponse.members).length : 0;
|
||||
document.getElementById('clusterStatus').textContent = `${memberCount} nodes`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load status on page load
|
||||
loadStatus();
|
||||
|
||||
// Refresh status every 5 seconds
|
||||
setInterval(loadStatus, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
82
examples/static_web/README.md
Normal file
82
examples/static_web/README.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Static Web Server Example
|
||||
|
||||
This example demonstrates how to serve static HTML files from LittleFS using the Spore framework.
|
||||
|
||||
## Features
|
||||
|
||||
- Serves static files from LittleFS filesystem
|
||||
- Beautiful web interface with real-time status updates
|
||||
- Responsive design that works on mobile and desktop
|
||||
- Automatic API endpoint discovery and status monitoring
|
||||
|
||||
## Files
|
||||
|
||||
- `main.cpp` - Main application entry point
|
||||
- `data/index.html` - Web interface (served from LittleFS)
|
||||
|
||||
## Web Interface
|
||||
|
||||
The web interface provides:
|
||||
|
||||
- **Node Status** - Shows uptime and system information
|
||||
- **Network Status** - Displays WiFi connection status
|
||||
- **Task Status** - Shows active background tasks
|
||||
- **Cluster Status** - Displays cluster membership
|
||||
- **API Links** - Quick access to all available API endpoints
|
||||
|
||||
## Building and Flashing
|
||||
|
||||
### Build for D1 Mini (recommended for web interface)
|
||||
|
||||
```bash
|
||||
# Build the firmware
|
||||
pio run -e d1_mini_static_web
|
||||
|
||||
# Flash to device
|
||||
pio run -e d1_mini_static_web -t upload
|
||||
```
|
||||
|
||||
### Upload Files to LittleFS
|
||||
|
||||
After flashing the firmware, you need to upload the HTML files to the LittleFS filesystem:
|
||||
|
||||
```bash
|
||||
# Upload data directory to LittleFS
|
||||
pio run -e d1_mini_static_web -t uploadfs
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Flash the firmware to your ESP8266 device
|
||||
2. Upload the data files to LittleFS
|
||||
3. Connect to the device's WiFi network
|
||||
4. Open a web browser and navigate to `http://<device-ip>/`
|
||||
5. The web interface will load and display real-time status information
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The web interface automatically discovers and displays links to all available API endpoints:
|
||||
|
||||
- `/api/node/status` - Node status and system information
|
||||
- `/api/network/status` - Network and WiFi status
|
||||
- `/api/tasks/status` - Task management status
|
||||
- `/api/cluster/members` - Cluster membership
|
||||
- `/api/node/endpoints` - Complete API endpoint list
|
||||
|
||||
## Customization
|
||||
|
||||
To customize the web interface:
|
||||
|
||||
1. Modify `data/index.html` with your desired content
|
||||
2. Add additional static files (CSS, JS, images) to the `data/` directory
|
||||
3. Re-upload the files using `pio run -e d1_mini_static_web -t uploadfs`
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
data/
|
||||
├── index.html # Main web interface
|
||||
└── (other static files) # Additional web assets
|
||||
```
|
||||
|
||||
The StaticFileService automatically serves files from the LittleFS root directory, so any files placed in the `data/` directory will be accessible via HTTP.
|
||||
21
examples/static_web/main.cpp
Normal file
21
examples/static_web/main.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include <Arduino.h>
|
||||
#include "spore/Spore.h"
|
||||
|
||||
// Create Spore instance
|
||||
Spore spore;
|
||||
|
||||
void setup() {
|
||||
// Initialize Spore framework
|
||||
spore.setup();
|
||||
|
||||
// Start the framework (this will start the API server with static file serving)
|
||||
spore.begin();
|
||||
|
||||
Serial.println("Static web server started!");
|
||||
Serial.println("Visit http://<node-ip>/ to see the web interface");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Let Spore handle the main loop
|
||||
spore.loop();
|
||||
}
|
||||
24
include/spore/services/StaticFileService.h
Normal file
24
include/spore/services/StaticFileService.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include "spore/Service.h"
|
||||
#include "spore/core/ApiServer.h"
|
||||
#include "spore/core/NodeContext.h"
|
||||
#include <FS.h>
|
||||
#include <LittleFS.h>
|
||||
|
||||
class StaticFileService : public Service {
|
||||
public:
|
||||
StaticFileService(NodeContext& ctx, ApiServer& apiServer);
|
||||
void registerEndpoints(ApiServer& api) override;
|
||||
const char* getName() const override { return name.c_str(); }
|
||||
|
||||
private:
|
||||
static const String name;
|
||||
NodeContext& ctx;
|
||||
ApiServer& apiServer;
|
||||
|
||||
void handleRootRequest(AsyncWebServerRequest* request);
|
||||
void handleStaticFileRequest(AsyncWebServerRequest* request);
|
||||
void handleNotFound(AsyncWebServerRequest* request);
|
||||
String getContentType(const String& filename);
|
||||
bool fileExists(const String& path);
|
||||
};
|
||||
BIN
littlefs.bin
Normal file
BIN
littlefs.bin
Normal file
Binary file not shown.
6
partitions_lfs.csv
Normal file
6
partitions_lfs.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
# Name, Type, Subtype, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x6000,
|
||||
otadata, data, ota, 0xf000, 0x2000,
|
||||
app0, app, factory, 0x11000, 0x30000, # 192 KB
|
||||
app1, app, ota_0, 0x41000, 0x30000, # 192 KB
|
||||
littlefs, data, littlefs, 0x71000, 0x9000 # 36 KB
|
||||
|
@@ -3,4 +3,4 @@ nvs, data, nvs, , 0x4000,
|
||||
otadata, data, ota, , 0x2000,
|
||||
app0, app, ota_0, , 0x3E000,
|
||||
app1, app, ota_1, , 0x3E000,
|
||||
spiffs, data, spiffs, , 0x1C000,
|
||||
littlefs, data, littlefs, , 0x1C000,
|
||||
|
@@ -9,7 +9,7 @@
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
;default_envs = esp01_1m
|
||||
default_envs = esp01_1m
|
||||
src_dir = .
|
||||
|
||||
[common]
|
||||
@@ -24,9 +24,16 @@ board = esp01_1m
|
||||
framework = arduino
|
||||
upload_speed = 115200
|
||||
monitor_speed = 115200
|
||||
board_build.partitions = partitions_ota_1M.csv
|
||||
board_build.flash_mode = dout ; ESP‑01S uses DOUT on 1 Mbit flash
|
||||
board_build.flash_size = 1M
|
||||
board_build.f_cpu = 80000000L
|
||||
board_build.flash_mode = qio
|
||||
board_build.filesystem = littlefs
|
||||
;board_build.ldscript = eagle.flash.1m256.ld
|
||||
;board_build.partitions = partitions_ota_1M.csv
|
||||
;board_build.partitions = partitions_lfs.csv
|
||||
#board_build.filesystem_image_size = 0x9000
|
||||
|
||||
#build_flags = -DESP8266_PARTITION_TABLE_OFFSET=0x8000
|
||||
# -DESP8266_PARTITION_TABLE_FILE=\"partitions_lfs.csv\"
|
||||
lib_deps = ${common.lib_deps}
|
||||
build_src_filter =
|
||||
+<examples/base/*.cpp>
|
||||
@@ -145,3 +152,21 @@ build_src_filter =
|
||||
+<src/spore/services/*.cpp>
|
||||
+<src/spore/types/*.cpp>
|
||||
+<src/internal/*.cpp>
|
||||
|
||||
[env:d1_mini_static_web]
|
||||
platform = platformio/espressif8266@^4.2.1
|
||||
board = d1_mini
|
||||
framework = arduino
|
||||
upload_speed = 115200
|
||||
monitor_speed = 115200
|
||||
board_build.flash_mode = dio
|
||||
board_build.flash_size = 4M
|
||||
board_build.filesystem = littlefs
|
||||
lib_deps = ${common.lib_deps}
|
||||
build_src_filter =
|
||||
+<examples/static_web/*.cpp>
|
||||
+<src/spore/*.cpp>
|
||||
+<src/spore/core/*.cpp>
|
||||
+<src/spore/services/*.cpp>
|
||||
+<src/spore/types/*.cpp>
|
||||
+<src/internal/*.cpp>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "spore/services/NetworkService.h"
|
||||
#include "spore/services/ClusterService.h"
|
||||
#include "spore/services/TaskService.h"
|
||||
#include "spore/services/StaticFileService.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
|
||||
@@ -119,12 +120,14 @@ void Spore::registerCoreServices() {
|
||||
auto networkService = std::make_shared<NetworkService>(network);
|
||||
auto clusterService = std::make_shared<ClusterService>(ctx);
|
||||
auto taskService = std::make_shared<TaskService>(taskManager);
|
||||
auto staticFileService = std::make_shared<StaticFileService>(ctx, apiServer);
|
||||
|
||||
// Add to services list
|
||||
services.push_back(nodeService);
|
||||
services.push_back(networkService);
|
||||
services.push_back(clusterService);
|
||||
services.push_back(taskService);
|
||||
services.push_back(staticFileService);
|
||||
|
||||
Serial.println("[Spore] Core services registered");
|
||||
}
|
||||
|
||||
109
src/spore/services/StaticFileService.cpp
Normal file
109
src/spore/services/StaticFileService.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "spore/services/StaticFileService.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
const String StaticFileService::name = "StaticFileService";
|
||||
|
||||
StaticFileService::StaticFileService(NodeContext& ctx, ApiServer& apiServer)
|
||||
: ctx(ctx), apiServer(apiServer) {
|
||||
}
|
||||
|
||||
void StaticFileService::registerEndpoints(ApiServer& api) {
|
||||
// Initialize LittleFS
|
||||
if (!LittleFS.begin()) {
|
||||
Serial.println("[StaticFileService] LittleFS Mount Failed");
|
||||
return;
|
||||
}
|
||||
Serial.println("[StaticFileService] LittleFS mounted successfully");
|
||||
|
||||
// Root endpoint - serve index.html
|
||||
api.addEndpoint("/", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleRootRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
|
||||
// Static file serving for any path
|
||||
api.addEndpoint("/*", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleStaticFileRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
}
|
||||
|
||||
void StaticFileService::handleRootRequest(AsyncWebServerRequest* request) {
|
||||
// Serve index.html from root
|
||||
String path = "/index.html";
|
||||
|
||||
if (!fileExists(path)) {
|
||||
request->send(404, "text/plain", "File not found");
|
||||
return;
|
||||
}
|
||||
|
||||
File file = LittleFS.open(path, "r");
|
||||
if (!file) {
|
||||
request->send(500, "text/plain", "Failed to open file");
|
||||
return;
|
||||
}
|
||||
|
||||
String contentType = getContentType(path);
|
||||
request->send(LittleFS, path, contentType);
|
||||
file.close();
|
||||
}
|
||||
|
||||
void StaticFileService::handleStaticFileRequest(AsyncWebServerRequest* request) {
|
||||
String path = request->url();
|
||||
|
||||
// Remove leading slash for LittleFS path
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
|
||||
// If path is empty or just "/", serve index.html
|
||||
if (path.isEmpty() || path == "/") {
|
||||
path = "index.html";
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if (!fileExists("/" + path)) {
|
||||
request->send(404, "text/plain", "File not found: " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
String contentType = getContentType(path);
|
||||
request->send(LittleFS, "/" + path, contentType);
|
||||
}
|
||||
|
||||
void StaticFileService::handleNotFound(AsyncWebServerRequest* request) {
|
||||
// Try to serve index.html as fallback
|
||||
if (fileExists("/index.html")) {
|
||||
request->send(LittleFS, "/index.html", "text/html");
|
||||
} else {
|
||||
request->send(404, "text/plain", "Not found");
|
||||
}
|
||||
}
|
||||
|
||||
String StaticFileService::getContentType(const String& filename) {
|
||||
if (filename.endsWith(".html") || filename.endsWith(".htm")) {
|
||||
return "text/html";
|
||||
} else if (filename.endsWith(".css")) {
|
||||
return "text/css";
|
||||
} else if (filename.endsWith(".js")) {
|
||||
return "application/javascript";
|
||||
} else if (filename.endsWith(".json")) {
|
||||
return "application/json";
|
||||
} else if (filename.endsWith(".png")) {
|
||||
return "image/png";
|
||||
} else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) {
|
||||
return "image/jpeg";
|
||||
} else if (filename.endsWith(".gif")) {
|
||||
return "image/gif";
|
||||
} else if (filename.endsWith(".svg")) {
|
||||
return "image/svg+xml";
|
||||
} else if (filename.endsWith(".ico")) {
|
||||
return "image/x-icon";
|
||||
} else if (filename.endsWith(".txt")) {
|
||||
return "text/plain";
|
||||
} else {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
bool StaticFileService::fileExists(const String& path) {
|
||||
return LittleFS.exists(path);
|
||||
}
|
||||
Reference in New Issue
Block a user