feature/web #4

Merged
master merged 5 commits from feature/web into main 2025-09-16 08:29:04 +02:00
13 changed files with 807 additions and 300 deletions
Showing only changes of commit 98fdc3e1ae - Show all commits

23
cmds.sh Normal file
View 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
View File

@@ -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
View 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>

View 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.

View 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();
}

View 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

Binary file not shown.

6
partitions_lfs.csv Normal file
View 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, # 192KB
app1, app, ota_0, 0x41000, 0x30000, # 192KB
littlefs, data, littlefs, 0x71000, 0x9000 # 36KB
1 # Name, Type, Subtype, Offset, Size, Flags
2 nvs, data, nvs, 0x9000, 0x6000,
3 otadata, data, ota, 0xf000, 0x2000,
4 app0, app, factory, 0x11000, 0x30000, # 192 KB
5 app1, app, ota_0, 0x41000, 0x30000, # 192 KB
6 littlefs, data, littlefs, 0x71000, 0x9000 # 36 KB

View File

@@ -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,
1 # Name Type SubType Offset Size Flags
3 otadata data ota 0x2000
4 app0 app ota_0 0x3E000
5 app1 app ota_1 0x3E000
6 spiffs littlefs data spiffs littlefs 0x1C000

View File

@@ -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 ; ESP01S 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>

View File

@@ -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");
}

View 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);
}