added versioning
Prepare Build and Testing executable binary / Prepare-Build-Testing-With-Make (push) Successful in 6s
Prepare Build and Testing executable binary / Prepare-Build-Testing-With-CMake (push) Successful in 11s
Prepare Build and Testing executable binary / Prepare-Build-Testing-With-Meson (push) Successful in 9s
Prepare Build and Testing executable binary / Prepare-Build-Testing-With-Make (push) Successful in 6s
Prepare Build and Testing executable binary / Prepare-Build-Testing-With-CMake (push) Successful in 11s
Prepare Build and Testing executable binary / Prepare-Build-Testing-With-Meson (push) Successful in 9s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Bootloader Update Manager
|
||||
|
||||
Веб-приложение на C для обновления ядра, device-tree и U-Boot environment на Orange Pi Zero 2W.
|
||||
Веб-приложение на C для обновления ядра, device-tree и U-Boot environment на Orange Pi Zero 2W (Allwinner H618).
|
||||
|
||||
## Возможности
|
||||
|
||||
@@ -10,30 +10,46 @@
|
||||
- **U-Boot Environment**: Загрузка `uboot.env`
|
||||
- **U-Boot Binary**: Загрузка `u-boot-sunxi-with-spl.bin` с записью в:
|
||||
- `/media/u-boot-sunxi-with-spl.bin` (файл)
|
||||
- `/dev/mtd0` (SPI NOR flash)
|
||||
- `/dev/mtd0` (SPI NOR flash, с предварительным стиранием)
|
||||
- `/dev/mmcblk1` со смещением 8KB (SD карта, аналог `dd seek=8`)
|
||||
- **Редактор extlinux.conf**: Просмотр и редактирование конфигурации загрузчика
|
||||
- **Индикатор прогресса**: Отображение прогресса загрузки в реальном времени
|
||||
- **Авто-монтирование**: Автоматически монтирует `/dev/mmcblk1p1` в `/media`
|
||||
- **Поддержка больших файлов**: Загрузка файлов до 200 MB (до 1 MB для U-Boot Binary)
|
||||
- **Перезагрузка системы**: Кнопка безопасной перезагрузки с корректным отмонтированием
|
||||
- **Мониторинг температуры**: Отображение температуры CPU с цветовой индикацией
|
||||
- **Поддержка больших файлов**:
|
||||
- До 200 MB для обычных файлов
|
||||
- До 1 MB для U-Boot Binary (безопасный лимит)
|
||||
- **Перезагрузка системы**: Кнопка безопасной перезагрузки:
|
||||
- `sync()` - сброс буферов ФС
|
||||
- `umount /media` - отмонтирование
|
||||
- `shutdown -r now` - мягкая перезагрузка через finit
|
||||
- **Мониторинг температуры**: Отображение температуры CPU с цветовой индикацией:
|
||||
- < 50°C: 🟢 Зелёный (холодно)
|
||||
- < 70°C: 🔵 Синий (норма)
|
||||
- < 80°C: 🟠 Оранжевый (тепло)
|
||||
- ≥ 80°C: 🔴 Красный (горячо)
|
||||
- **Отслеживание версий файлов**:
|
||||
- Автоматическое сохранение даты/времени загрузки
|
||||
- Файлы `.version` для каждого загруженного файла
|
||||
- Timestamp передаётся от браузера (не зависит от времени на плате)
|
||||
- Человекочитаемый формат: `ГГГГ-ММ-ДД ЧЧ:ММ:СС`
|
||||
- Отображение последней даты модификации в веб-интерфейсе
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── main.c # Основной сервер (C + mongoose + cJSON)
|
||||
├── index.html # HTML веб-интерфейса
|
||||
├── style.css # Стили веб-интерфейса
|
||||
├── app.js # Frontend JavaScript
|
||||
├── Makefile # Конфигурация сборки
|
||||
├── README.md # Документация
|
||||
├── mongoose.h # Mongoose web server (v7.19)
|
||||
├── mongoose.c # Mongoose web server (v7.19)
|
||||
├── cJSON.h # JSON parser (v1.7.18)
|
||||
├── cJSON.c # JSON parser (v1.7.18)
|
||||
└── mongoose_config.h # Конфигурация mongoose
|
||||
├── src/
|
||||
│ ├── main.c # Основной сервер (C + mongoose + cJSON)
|
||||
│ └── lib/
|
||||
│ ├── mongoose.c/h # Mongoose web server (v7.19)
|
||||
│ └── cJSON.c/h # JSON parser (v1.7.18)
|
||||
├── web/
|
||||
│ ├── index.html # HTML веб-интерфейса
|
||||
│ ├── style.css # Стили веб-интерфейса
|
||||
│ └── app.js # Frontend JavaScript
|
||||
├── mongoose_config.h # Конфигурация mongoose
|
||||
├── Makefile # Конфигурация сборки
|
||||
└── README.md # Документация
|
||||
```
|
||||
|
||||
## Требования
|
||||
@@ -62,53 +78,72 @@ sudo ./up
|
||||
|
||||
### System Status
|
||||
- Статус монтирования `/dev/mmcblk1p1` → `/media`
|
||||
- Информация о устройстве
|
||||
- Температура CPU (обновление каждые 5 секунд)
|
||||
|
||||
### File Versions
|
||||
Отображение даты последней загрузки/изменения для файлов:
|
||||
- Kernel (Image.gz)
|
||||
- Device-Tree (DTB)
|
||||
- U-Boot Env
|
||||
- U-Boot Binary
|
||||
- Extlinux (обновляется при сохранении через редактор)
|
||||
|
||||
**Формат даты**: `2026-03-14 12:34:56`
|
||||
|
||||
**Примечание**: Время берётся из браузера клиента, поэтому корректно даже если на плате не настроено системное время.
|
||||
|
||||
### File Upload
|
||||
Три вкладки для загрузки:
|
||||
Четыре вкладки для загрузки:
|
||||
- **Kernel** → `/media/Image.gz`
|
||||
- **Device-Tree** → `/media/sun50i-h618-orangepi-zero2w.dtb`
|
||||
- **U-Boot Environment** → `/media/uboot.env`
|
||||
- **U-Boot Binary (SPL)** → `/media/u-boot-sunxi-with-spl.bin` + `/dev/mtd0` + `/dev/mmcblk1` (+8KB)
|
||||
|
||||
При загрузке браузер автоматически отправляет текущий timestamp, который сохраняется в `.version` файл.
|
||||
|
||||
### extlinux.conf Editor
|
||||
- Загрузка текущего `extlinux.conf`
|
||||
- Редактирование в текстовом поле
|
||||
- Сохранение изменений
|
||||
- Сохранение изменений (с записью версии)
|
||||
- Очистка редактора
|
||||
|
||||
### System Control
|
||||
- Кнопка **Reboot System** с подтверждением
|
||||
- Безопасная перезагрузка:
|
||||
- `sync()` - сброс буферов ФС
|
||||
- `umount /media` - отмонтирование
|
||||
- `shutdown -r now` - мягкая перезагрузка через finit
|
||||
- Безопасная перезагрузка через `shutdown -r now`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Endpoint | Method | Описание |
|
||||
|----------|--------|----------|
|
||||
| `/api/upload` | POST | Загрузка файлов (kernel, dtb, uboot) |
|
||||
| `/api/extlinux` | GET | Чтение extlinux.conf |
|
||||
| `/api/extlinux` | POST | Запись extlinux.conf |
|
||||
| `/api/upload?timestamp=...` | POST | Загрузка файлов (kernel, dtb, uboot, uboot_bin) |
|
||||
| `/api/extlinux?timestamp=...` | GET | Чтение extlinux.conf |
|
||||
| `/api/extlinux?timestamp=...` | POST | Запись extlinux.conf (с версией) |
|
||||
| `/api/progress` | GET | Статус загрузки |
|
||||
| `/api/mount` | GET | Статус монтирования |
|
||||
| `/api/temperature` | GET | Температура CPU |
|
||||
| `/api/filedate?file=...` | GET | Дата версии файла |
|
||||
| `/api/reboot` | POST | Перезагрузка системы |
|
||||
|
||||
## Параметры загрузки
|
||||
|
||||
Для `/api/upload` (multipart/form-data):
|
||||
- `type`: Тип файла (`kernel`, `dtb`, `uboot`)
|
||||
Для `/api/upload`:
|
||||
- `type`: Тип файла (`kernel`, `dtb`, `uboot`, `uboot_bin`)
|
||||
- `file`: Загружаемый файл
|
||||
- `timestamp`: ISO 8601 timestamp (отправляется браузером автоматически)
|
||||
|
||||
Для `/api/extlinux` (POST):
|
||||
- `content`: Содержимое файла extlinux.conf
|
||||
- `timestamp`: ISO 8601 timestamp (отправляется браузером автоматически)
|
||||
|
||||
## Расположение файлов
|
||||
|
||||
| Тип | Файл назначения |
|
||||
|-----|-----------------|
|
||||
| Ядро | `/media/Image.gz` |
|
||||
| Device-Tree | `/media/sun50i-h618-orangepi-zero2w.dtb` |
|
||||
| U-Boot Env | `/media/uboot.env` |
|
||||
| U-Boot Binary | `/media/u-boot-sunxi-with-spl.bin` + `/dev/mtd0` + `/dev/mmcblk1` (+8KB offset) |
|
||||
| Extlinux | `/media/extlinux/extlinux.conf` |
|
||||
| Тип | Файл назначения | Версия |
|
||||
|-----|-----------------|--------|
|
||||
| Ядро | `/media/Image.gz` | `/media/Image.gz.version` |
|
||||
| Device-Tree | `/media/sun50i-h618-orangepi-zero2w.dtb` | `/media/sun50i-h618-orangepi-zero2w.dtb.version` |
|
||||
| U-Boot Env | `/media/uboot.env` | `/media/uboot.env.version` |
|
||||
| U-Boot Binary | `/media/u-boot-sunxi-with-spl.bin` + `/dev/mtd0` + `/dev/mmcblk1` (+8KB) | `/media/u-boot-sunxi-with-spl.bin.version` |
|
||||
| Extlinux | `/media/extlinux/extlinux.conf` | `/media/extlinux.conf.version` |
|
||||
|
||||
## Конфигурация
|
||||
|
||||
@@ -119,21 +154,66 @@ sudo ./up
|
||||
```
|
||||
|
||||
### Ограничения
|
||||
- Максимальный размер файла: **200 MB**
|
||||
- Максимальный размер файла: **200 MB** (обычные файлы)
|
||||
- Максимальный размер U-Boot: **1 MB** (безопасно для boot area)
|
||||
- Порт сервера: **43243**
|
||||
|
||||
## Детали реализации
|
||||
|
||||
### U-Boot Binary загрузка
|
||||
1. Сохранение файла в `/media/u-boot-sunxi-with-spl.bin`
|
||||
2. Получение информации о SPI NOR через `ioctl(MEMGETINFO)`
|
||||
3. Стирание SPI NOR через `ioctl(MEMERASE)`
|
||||
4. Запись в `/dev/mtd0` (SPI NOR flash)
|
||||
5. Запись в `/dev/mmcblk1` со смещением 8KB (сектор 16)
|
||||
6. `sync()` для гарантии записи
|
||||
|
||||
**Прогресс бар**:
|
||||
- 0-30%: Запись файла в `/media`
|
||||
- 30-65%: Запись в SPI NOR
|
||||
- 65-100%: Запись в SD карту
|
||||
|
||||
### Temperature monitoring
|
||||
- Чтение из `/sys/class/thermal/thermal_zone*` (0-3)
|
||||
- Приоритет: зоны с типом `cpu`, `soc`
|
||||
- Резервный источник: `/sys/class/hwmon/hwmon*/temp1_input` (0-3)
|
||||
- Приоритет: сенсоры с именем `cpu`, `sunxi`, `acpitz`
|
||||
- Обновление каждые 5 секунд
|
||||
|
||||
### File version tracking
|
||||
1. Браузер создаёт ISO 8601 timestamp: `2026-03-14T12:34:56.789Z`
|
||||
2. Timestamp кодируется для URL: `2026-03-14T12%3A34%3A56.789Z`
|
||||
3. Сервер декодирует `%3A` → `:`
|
||||
4. Конвертирует в человекочитаемый формат: `2026-03-14 12:34:56`
|
||||
5. Сохраняет в `<filename>.version`
|
||||
6. Frontend опрашивает `/api/filedate` каждые 10 секунд
|
||||
|
||||
**Важно**: Время берётся из браузера клиента, поэтому:
|
||||
- ✅ Корректное время даже если на плате не настроено
|
||||
- ✅ Часовой пояс клиента
|
||||
- ✅ Синхронизация через NTP браузера
|
||||
|
||||
### Extlinux.conf editor
|
||||
- Сохранение через POST с timestamp от браузера
|
||||
- Автоматическое обновление списка версий после сохранения
|
||||
- Версия записывается в `/media/extlinux.conf.version`
|
||||
|
||||
## Безопасность
|
||||
|
||||
⚠️ **Предупреждение**:
|
||||
- Приложение требует права **root** для монтирования и перезагрузки
|
||||
- Приложение требует права **root** для:
|
||||
- Монтирования устройств (`mount`, `umount`)
|
||||
- Записи в `/dev/mtd0` (SPI NOR flash)
|
||||
- Записи в `/dev/mmcblk1` (SD карта)
|
||||
- Перезагрузки системы (`shutdown`)
|
||||
- Используйте с осторожностью в production окружении
|
||||
- Рекомендуется ограничить доступ к веб-интерфейсу
|
||||
- Рекомендуется ограничить доступ к веб-интерфейсу (firewall, auth)
|
||||
|
||||
## Зависимости
|
||||
|
||||
Все зависимости встроены в проект:
|
||||
- `mongoose.c/h` - веб-сервер (из репозитория Cesanta)
|
||||
- `cJSON.c/h` - JSON парсер (из репозитория DaveGamble)
|
||||
- `mongoose.c/h` - веб-сервер (из репозитория Cesanta, v7.19)
|
||||
- `cJSON.c/h` - JSON парсер (из репозитория DaveGamble, v1.7.18)
|
||||
|
||||
## Лицензия
|
||||
|
||||
|
||||
+160
-5
@@ -344,6 +344,25 @@ static void handle_extlinux_write(struct mg_connection *c, struct mg_http_messag
|
||||
return;
|
||||
}
|
||||
|
||||
// Get timestamp from query string (sent by browser)
|
||||
char timestamp[64] = {0};
|
||||
if (hm->query.len > 0) {
|
||||
char query[512] = {0};
|
||||
memcpy(query, hm->query.buf, hm->query.len < sizeof(query) ? hm->query.len : sizeof(query)-1);
|
||||
struct mg_str ts_name = mg_str("timestamp");
|
||||
struct mg_str ts_value = mg_http_var(mg_str(query), ts_name);
|
||||
if (ts_value.len > 0) {
|
||||
mg_str_cpy(ts_value, timestamp, sizeof(timestamp));
|
||||
// URL-decode timestamp (replace %3A with :)
|
||||
for (size_t i = 0; i < strlen(timestamp) - 2; i++) {
|
||||
if (timestamp[i] == '%' && timestamp[i+1] == '3' && timestamp[i+2] == 'A') {
|
||||
timestamp[i] = ':';
|
||||
memmove(×tamp[i+1], ×tamp[i+3], strlen(×tamp[i+3]) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mount_device() < 0) {
|
||||
cJSON_Delete(root);
|
||||
mg_http_reply(c, 500, "", "{\"error\":\"Failed to mount device\"}\n");
|
||||
@@ -362,6 +381,31 @@ static void handle_extlinux_write(struct mg_connection *c, struct mg_http_messag
|
||||
|
||||
fprintf(f, "%s", content->valuestring);
|
||||
fclose(f);
|
||||
|
||||
// Write version file with timestamp from browser
|
||||
if (strlen(timestamp) > 0) {
|
||||
char version_path[512];
|
||||
snprintf(version_path, sizeof(version_path), "%s/%s.version", MOUNT_POINT, "extlinux.conf");
|
||||
FILE *vf = fopen(version_path, "w");
|
||||
if (vf) {
|
||||
// Convert ISO 8601 to human-readable format
|
||||
// Input: 2026-03-14T12:34:56.789Z
|
||||
// Output: 2026-03-14 12:34:56
|
||||
char readable_date[32] = {0};
|
||||
if (strlen(timestamp) >= 19) {
|
||||
strncpy(readable_date, timestamp, 10); // YYYY-MM-DD
|
||||
readable_date[10] = ' ';
|
||||
strncpy(readable_date + 11, timestamp + 11, 8); // HH:MM:SS
|
||||
readable_date[19] = '\0';
|
||||
} else {
|
||||
strncpy(readable_date, timestamp, sizeof(readable_date) - 1);
|
||||
}
|
||||
fprintf(vf, "%s\n", readable_date);
|
||||
fclose(vf);
|
||||
printf("Written extlinux.conf version: %s\n", readable_date);
|
||||
}
|
||||
}
|
||||
|
||||
sync();
|
||||
|
||||
mg_http_reply(c, 200, "Content-Type: application/json\r\n",
|
||||
@@ -431,10 +475,10 @@ static void handle_temperature(struct mg_connection *c) {
|
||||
char type[32] = {0};
|
||||
if (fscanf(type_f, "%31s", type) == 1) {
|
||||
// Prefer CPU or soc zones
|
||||
int is_cpu = (strstr(type, "cpu") != NULL ||
|
||||
int is_cpu = (strstr(type, "cpu") != NULL ||
|
||||
strstr(type, "soc") != NULL ||
|
||||
strstr(type, "CPU") != NULL);
|
||||
|
||||
|
||||
int temp_raw;
|
||||
if (fscanf(f, "%d", &temp_raw) == 1) {
|
||||
temperature = temp_raw / 1000.0f;
|
||||
@@ -467,11 +511,11 @@ static void handle_temperature(struct mg_connection *c) {
|
||||
char name[32] = {0};
|
||||
if (fscanf(name_f, "%31s", name) == 1) {
|
||||
// Prefer CPU or sunxi sensors
|
||||
int is_cpu = (strstr(name, "cpu") != NULL ||
|
||||
int is_cpu = (strstr(name, "cpu") != NULL ||
|
||||
strstr(name, "sunxi") != NULL ||
|
||||
strstr(name, "CPU") != NULL ||
|
||||
strstr(name, "acpitz") != NULL);
|
||||
|
||||
|
||||
int temp_raw;
|
||||
if (fscanf(f, "%d", &temp_raw) == 1) {
|
||||
temperature = temp_raw / 1000.0f;
|
||||
@@ -499,7 +543,7 @@ static void handle_temperature(struct mg_connection *c) {
|
||||
"/sys/class/hwmon/hwmon3/temp1_input",
|
||||
NULL
|
||||
};
|
||||
|
||||
|
||||
for (int i = 0; fallback_paths[i] != NULL && !success; i++) {
|
||||
FILE *f = fopen(fallback_paths[i], "r");
|
||||
if (f) {
|
||||
@@ -524,6 +568,72 @@ static void handle_temperature(struct mg_connection *c) {
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
|
||||
// Handle file date read
|
||||
static void handle_filedate(struct mg_connection *c, struct mg_http_message *hm) {
|
||||
// Get filename from query string
|
||||
char filename[128] = {0};
|
||||
char query[256] = {0};
|
||||
|
||||
if (hm->query.len > 0 && hm->query.len < sizeof(query)) {
|
||||
memcpy(query, hm->query.buf, hm->query.len);
|
||||
query[hm->query.len] = '\0';
|
||||
|
||||
// Parse query string: file=Image.gz
|
||||
struct mg_str name = mg_str("file");
|
||||
struct mg_str value = mg_http_var(mg_str(query), name);
|
||||
if (value.len > 0 && value.len < sizeof(filename)) {
|
||||
mg_str_cpy(value, filename, sizeof(filename));
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen(filename) == 0) {
|
||||
mg_http_reply(c, 400, "", "{\"error\":\"Missing file parameter\"}\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build version file path
|
||||
char version_path[512];
|
||||
snprintf(version_path, sizeof(version_path), "%s/%s.version", MOUNT_POINT, filename);
|
||||
|
||||
// Try to read version file
|
||||
FILE *f = fopen(version_path, "r");
|
||||
if (!f) {
|
||||
// No version file, return empty date
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(root, "status", "success");
|
||||
cJSON_AddStringToObject(root, "date", "");
|
||||
cJSON_AddStringToObject(root, "filename", filename);
|
||||
|
||||
char *json = cJSON_PrintUnformatted(root);
|
||||
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\n", json);
|
||||
free(json);
|
||||
cJSON_Delete(root);
|
||||
return;
|
||||
}
|
||||
|
||||
char date_str[64] = {0};
|
||||
if (fgets(date_str, sizeof(date_str), f) == NULL) {
|
||||
date_str[0] = '\0';
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
// Remove newline
|
||||
size_t len = strlen(date_str);
|
||||
if (len > 0 && date_str[len-1] == '\n') {
|
||||
date_str[len-1] = '\0';
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(root, "status", "success");
|
||||
cJSON_AddStringToObject(root, "date", date_str);
|
||||
cJSON_AddStringToObject(root, "filename", filename);
|
||||
|
||||
char *json = cJSON_PrintUnformatted(root);
|
||||
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\n", json);
|
||||
free(json);
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
|
||||
// Handle system reboot
|
||||
static void handle_reboot(struct mg_connection *c) {
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
@@ -619,6 +729,25 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
|
||||
g_upload.progress = 0;
|
||||
snprintf(g_upload.status, sizeof(g_upload.status), "Uploading...");
|
||||
|
||||
// Get timestamp from query string (sent by browser)
|
||||
char timestamp[64] = {0};
|
||||
if (hm->query.len > 0) {
|
||||
char query[512] = {0};
|
||||
memcpy(query, hm->query.buf, hm->query.len < sizeof(query) ? hm->query.len : sizeof(query)-1);
|
||||
struct mg_str ts_name = mg_str("timestamp");
|
||||
struct mg_str ts_value = mg_http_var(mg_str(query), ts_name);
|
||||
if (ts_value.len > 0) {
|
||||
mg_str_cpy(ts_value, timestamp, sizeof(timestamp));
|
||||
// URL-decode timestamp (replace %3A with :)
|
||||
for (size_t i = 0; i < strlen(timestamp) - 2; i++) {
|
||||
if (timestamp[i] == '%' && timestamp[i+1] == '3' && timestamp[i+2] == 'A') {
|
||||
timestamp[i] = ':';
|
||||
memmove(×tamp[i+1], ×tamp[i+3], strlen(×tamp[i+3]) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for U-Boot binary
|
||||
if (strcmp(type, "uboot_bin") == 0) {
|
||||
if (write_uboot_to_devices(file_data.buf, file_data.len, filepath) < 0) {
|
||||
@@ -653,6 +782,30 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
|
||||
g_upload.progress = 100;
|
||||
}
|
||||
|
||||
// Write version file with timestamp
|
||||
if (strlen(timestamp) > 0) {
|
||||
char version_path[512];
|
||||
snprintf(version_path, sizeof(version_path), "%s/%s.version", MOUNT_POINT, filename);
|
||||
FILE *vf = fopen(version_path, "w");
|
||||
if (vf) {
|
||||
// Convert ISO 8601 to human-readable format
|
||||
// Input: 2026-03-14T12:34:56.789Z
|
||||
// Output: 2026-03-14 12:34:56
|
||||
char readable_date[32] = {0};
|
||||
if (strlen(timestamp) >= 19) {
|
||||
strncpy(readable_date, timestamp, 10); // YYYY-MM-DD
|
||||
readable_date[10] = ' ';
|
||||
strncpy(readable_date + 11, timestamp + 11, 8); // HH:MM:SS
|
||||
readable_date[19] = '\0';
|
||||
} else {
|
||||
strncpy(readable_date, timestamp, sizeof(readable_date) - 1);
|
||||
}
|
||||
fprintf(vf, "%s\n", readable_date);
|
||||
fclose(vf);
|
||||
printf("Written version file: %s (%s)\n", version_path, readable_date);
|
||||
}
|
||||
}
|
||||
|
||||
printf("Upload complete: %s\n", filepath);
|
||||
|
||||
g_upload.received = file_data.len;
|
||||
@@ -683,6 +836,8 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
|
||||
handle_mount_status(c);
|
||||
} else if (mg_match_uri(hm->uri, "/api/temperature")) {
|
||||
handle_temperature(c);
|
||||
} else if (mg_match_uri(hm->uri, "/api/filedate")) {
|
||||
handle_filedate(c, hm);
|
||||
} else if (mg_match_uri(hm->uri, "/api/reboot")) {
|
||||
handle_reboot(c);
|
||||
} else if (mg_match_uri(hm->uri, "/")) {
|
||||
|
||||
+62
-7
@@ -23,8 +23,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
// Initialize
|
||||
checkMountStatus();
|
||||
checkTemperature();
|
||||
checkFileDates();
|
||||
setInterval(checkMountStatus, 10000); // Check every 10 seconds
|
||||
setInterval(checkTemperature, 5000); // Check temperature every 5 seconds
|
||||
setInterval(checkFileDates, 10000); // Check file dates every 10 seconds
|
||||
|
||||
// Tab switching
|
||||
tabBtns.forEach((btn) => {
|
||||
@@ -114,6 +116,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
progressBytes.textContent = "";
|
||||
hideResult();
|
||||
|
||||
// Get current timestamp
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
try {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
@@ -135,6 +140,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
`Successfully uploaded ${response.filename} (${formatBytes(response.size)})`,
|
||||
"success",
|
||||
);
|
||||
// Refresh file dates after upload
|
||||
checkFileDates();
|
||||
} else {
|
||||
let errorMsg = "Upload failed";
|
||||
try {
|
||||
@@ -151,7 +158,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
showResult("Network error", "error");
|
||||
});
|
||||
|
||||
xhr.open("POST", "/api/upload");
|
||||
xhr.open(
|
||||
"POST",
|
||||
`/api/upload?timestamp=${encodeURIComponent(timestamp)}`,
|
||||
);
|
||||
xhr.send(formData);
|
||||
} catch (error) {
|
||||
progressStatus.textContent = "Failed";
|
||||
@@ -227,19 +237,27 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
showEditorStatus("Saving...", "");
|
||||
|
||||
// Get current timestamp from browser
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/extlinux", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const response = await fetch(
|
||||
`/api/extlinux?timestamp=${encodeURIComponent(timestamp)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ content: content }),
|
||||
},
|
||||
body: JSON.stringify({ content: content }),
|
||||
});
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "success") {
|
||||
showEditorStatus("Successfully saved extlinux.conf", "success");
|
||||
// Refresh file dates after save
|
||||
checkFileDates();
|
||||
} else {
|
||||
showEditorStatus(data.error || "Failed to save", "error");
|
||||
}
|
||||
@@ -349,4 +367,41 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (temp < 80) return "#ffa500"; // Orange - warm
|
||||
return "#ff4757"; // Red - hot
|
||||
}
|
||||
|
||||
// File dates functions
|
||||
const fileDates = {
|
||||
"Image.gz": document.getElementById("date-Image.gz"),
|
||||
"sun50i-h618-orangepi-zero2w.dtb": document.getElementById(
|
||||
"date-sun50i-h618-orangepi-zero2w.dtb",
|
||||
),
|
||||
"uboot.env": document.getElementById("date-uboot.env"),
|
||||
"u-boot-sunxi-with-spl.bin": document.getElementById(
|
||||
"date-u-boot-sunxi-with-spl.bin",
|
||||
),
|
||||
"extlinux.conf": document.getElementById("date-extlinux.conf"),
|
||||
};
|
||||
|
||||
async function checkFileDates() {
|
||||
for (const [filename, element] of Object.entries(fileDates)) {
|
||||
if (!element) continue;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/filedate?file=${encodeURIComponent(filename)}`,
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "success" && data.date) {
|
||||
element.textContent = data.date;
|
||||
element.style.color = "#00ff88";
|
||||
} else {
|
||||
element.textContent = "Not available";
|
||||
element.style.color = "#888";
|
||||
}
|
||||
} catch (error) {
|
||||
element.textContent = "Error";
|
||||
element.style.color = "#ff4757";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,6 +44,42 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="files-section">
|
||||
<h2>File Versions</h2>
|
||||
<div class="files-grid">
|
||||
<div class="file-item">
|
||||
<span class="file-label">Kernel (Image.gz):</span>
|
||||
<span class="file-date" id="date-Image.gz">--</span>
|
||||
</div>
|
||||
<div class="file-item">
|
||||
<span class="file-label">Device-Tree (DTB):</span>
|
||||
<span
|
||||
class="file-date"
|
||||
id="date-sun50i-h618-orangepi-zero2w.dtb"
|
||||
>--</span
|
||||
>
|
||||
</div>
|
||||
<div class="file-item">
|
||||
<span class="file-label">U-Boot Env:</span>
|
||||
<span class="file-date" id="date-uboot.env">--</span>
|
||||
</div>
|
||||
<div class="file-item">
|
||||
<span class="file-label">U-Boot Binary:</span>
|
||||
<span
|
||||
class="file-date"
|
||||
id="date-u-boot-sunxi-with-spl.bin"
|
||||
>--</span
|
||||
>
|
||||
</div>
|
||||
<div class="file-item">
|
||||
<span class="file-label">Extlinux:</span>
|
||||
<span class="file-date" id="date-extlinux.conf"
|
||||
>--</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="upload-section">
|
||||
<h2>File Upload</h2>
|
||||
<div class="upload-tabs">
|
||||
|
||||
@@ -113,6 +113,41 @@ h2 {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* File Versions Section */
|
||||
.files-section {
|
||||
border: 1px solid rgba(0, 217, 255, 0.3);
|
||||
}
|
||||
|
||||
.files-section h2 {
|
||||
color: #00d9ff;
|
||||
}
|
||||
|
||||
.files-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.file-label {
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.file-date {
|
||||
color: #00ff88;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Upload Section */
|
||||
.upload-tabs {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user