Merge pull request 'Develop' (#2) from develop into main
Prepare Build with CMake and Testing executable binary / Prepare-Build-Testing-With-CMake (push) Successful in 23s
Prepare Build with Make and Testing executable binary / Prepare-Build-Testing-With-Make (push) Successful in 7s
Prepare Build with Meson and Testing executable binary / Prepare-Build-Testing-With-Meson (push) Successful in 21s

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
wt committed 2026-09-17 16:15:02 +07:00
commit de82cfe21b
9 files changed
+830 -103

No files matched your search

-40
View File
@@ -1,40 +0,0 @@
name: Prepare Build and Testing executable binary
run-name: ${{ gitea.actor }}
on: [push]
jobs:
Prepare-Build-Testing-With-Make:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Building binary with make
run: make -j$(nproc)
Prepare-Build-Testing-With-CMake:
runs-on: ubuntu-latest
steps:
- name: prepare-container
run: |
apt install -y cmake
- name: Check out repository code
uses: actions/checkout@v3
- name: Building binary with CMake
run: |
mkdir build && \
cd build && \
cmake .. && \
make -j$(nproc)
Prepare-Build-Testing-With-Meson:
runs-on: ubuntu-latest
steps:
- name: prepare-container
run: |
apt install -y meson
- name: Check out repository code
uses: actions/checkout@v3
- name: Building binary with meson
run: |
meson setup build && \
ninja -C build
+16
View File
@@ -0,0 +1,16 @@
name: Prepare Build with CMake and Testing executable binary
run-name: ${{ gitea.actor }}
on: [push]
jobs:
Prepare-Build-Testing-With-CMake:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Install build tools
run: apt update && apt install -y cmake
- name: Configure CMake
run: cmake -B ${{gitea.workspace}}/build
- name: Build project
run: cmake --build ${{gitea.workspace}}/build
+12
View File
@@ -0,0 +1,12 @@
name: Prepare Build with Make and Testing executable binary
run-name: ${{ gitea.actor }}
on: [push]
jobs:
Prepare-Build-Testing-With-Make:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Building binary with make
run: make -j$(nproc)
+16
View File
@@ -0,0 +1,16 @@
name: Prepare Build with Meson and Testing executable binary
run-name: ${{ gitea.actor }}
on: [push]
jobs:
Prepare-Build-Testing-With-Meson:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Install build tools
run: apt update && apt install -y meson
- name: Configure Project
run: meson setup build --buildtype=release
- name: Build Project
run: meson compile -C build
+124 -37
View File
@@ -1,6 +1,6 @@
# Bootloader Update Manager # 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).
## Возможности ## Возможности
@@ -8,27 +8,48 @@
- **Обновление ядра**: Загрузка `Image.gz` в boot раздел - **Обновление ядра**: Загрузка `Image.gz` в boot раздел
- **Обновление Device-Tree**: Загрузка `sun50i-h618-orangepi-zero2w.dtb` - **Обновление Device-Tree**: Загрузка `sun50i-h618-orangepi-zero2w.dtb`
- **U-Boot Environment**: Загрузка `uboot.env` - **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/mmcblk1` со смещением 8KB (SD карта, аналог `dd seek=8`)
- **Редактор extlinux.conf**: Просмотр и редактирование конфигурации загрузчика - **Редактор extlinux.conf**: Просмотр и редактирование конфигурации загрузчика
- **Индикатор прогресса**: Отображение прогресса загрузки в реальном времени - **Индикатор прогресса**: Отображение прогресса загрузки в реальном времени
- **Авто-монтирование**: Автоматически монтирует `/dev/mmcblk1p1` в `/media` - **Авто-монтирование**: Автоматически монтирует `/dev/mmcblk1p1` в `/media`
- **Поддержка больших файлов**: Загрузка файлов до 200 MB - **Поддержка больших файлов**:
- **Перезагрузка системы**: Кнопка безопасной перезагрузки с корректным отмонтированием - До 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) ├── src/
├── index.html # HTML веб-интерфейса │ ├── main.c # Основной сервер (C + mongoose + cJSON)
├── style.css # Стили веб-интерфейса │ └── lib/
├── app.js # Frontend JavaScript │ ├── mongoose.c/h # Mongoose web server (v7.19)
├── Makefile # Конфигурация сборки │ └── cJSON.c/h # JSON parser (v1.7.18)
├── README.md # Документация ├── web/
├── mongoose.h # Mongoose web server (v7.19) │ ├── index.html # HTML веб-интерфейса
├── mongoose.c # Mongoose web server (v7.19) │ ├── style.css # Стили веб-интерфейса
├── cJSON.h # JSON parser (v1.7.18) │ └── app.js # Frontend JavaScript
├── cJSON.c # JSON parser (v1.7.18) ├── mongoose_config.h # Конфигурация mongoose
└── mongoose_config.h # Конфигурация mongoose ├── Makefile # Конфигурация сборки
└── README.md # Документация
``` ```
## Требования ## Требования
@@ -57,51 +78,72 @@ sudo ./up
### System Status ### System Status
- Статус монтирования `/dev/mmcblk1p1` → `/media` - Статус монтирования `/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 ### File Upload
Три вкладки для загрузки: Четыре вкладки для загрузки:
- **Kernel** → `/media/Image.gz` - **Kernel** → `/media/Image.gz`
- **Device-Tree** → `/media/sun50i-h618-orangepi-zero2w.dtb` - **Device-Tree** → `/media/sun50i-h618-orangepi-zero2w.dtb`
- **U-Boot Environment** → `/media/uboot.env` - **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 Editor
- Загрузка текущего `extlinux.conf` - Загрузка текущего `extlinux.conf`
- Редактирование в текстовом поле - Редактирование в текстовом поле
- Сохранение изменений - Сохранение изменений (с записью версии)
- Очистка редактора
### System Control ### System Control
- Кнопка **Reboot System** с подтверждением - Кнопка **Reboot System** с подтверждением
- Безопасная перезагрузка: - Безопасная перезагрузка через `shutdown -r now`
- `sync()` - сброс буферов ФС
- `umount /media` - отмонтирование
- `shutdown -r now` - мягкая перезагрузка через finit
## API Endpoints ## API Endpoints
| Endpoint | Method | Описание | | Endpoint | Method | Описание |
|----------|--------|----------| |----------|--------|----------|
| `/api/upload` | POST | Загрузка файлов (kernel, dtb, uboot) | | `/api/upload?timestamp=...` | POST | Загрузка файлов (kernel, dtb, uboot, uboot_bin) |
| `/api/extlinux` | GET | Чтение extlinux.conf | | `/api/extlinux?timestamp=...` | GET | Чтение extlinux.conf |
| `/api/extlinux` | POST | Запись extlinux.conf | | `/api/extlinux?timestamp=...` | POST | Запись extlinux.conf (с версией) |
| `/api/progress` | GET | Статус загрузки | | `/api/progress` | GET | Статус загрузки |
| `/api/mount` | GET | Статус монтирования | | `/api/mount` | GET | Статус монтирования |
| `/api/temperature` | GET | Температура CPU |
| `/api/filedate?file=...` | GET | Дата версии файла |
| `/api/reboot` | POST | Перезагрузка системы | | `/api/reboot` | POST | Перезагрузка системы |
## Параметры загрузки ## Параметры загрузки
Для `/api/upload` (multipart/form-data): Для `/api/upload`:
- `type`: Тип файла (`kernel`, `dtb`, `uboot`) - `type`: Тип файла (`kernel`, `dtb`, `uboot`, `uboot_bin`)
- `file`: Загружаемый файл - `file`: Загружаемый файл
- `timestamp`: ISO 8601 timestamp (отправляется браузером автоматически)
Для `/api/extlinux` (POST):
- `content`: Содержимое файла extlinux.conf
- `timestamp`: ISO 8601 timestamp (отправляется браузером автоматически)
## Расположение файлов ## Расположение файлов
| Тип | Файл назначения | | Тип | Файл назначения | Версия |
|-----|-----------------| |-----|-----------------|--------|
| Ядро | `/media/Image.gz` | | Ядро | `/media/Image.gz` | `/media/Image.gz.version` |
| Device-Tree | `/media/sun50i-h618-orangepi-zero2w.dtb` | | Device-Tree | `/media/sun50i-h618-orangepi-zero2w.dtb` | `/media/sun50i-h618-orangepi-zero2w.dtb.version` |
| U-Boot Env | `/media/uboot.env` | | U-Boot Env | `/media/uboot.env` | `/media/uboot.env.version` |
| Extlinux | `/media/extlinux/extlinux.conf` | | 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` |
## Конфигурация ## Конфигурация
@@ -112,21 +154,66 @@ sudo ./up
``` ```
### Ограничения ### Ограничения
- Максимальный размер файла: **200 MB** - Максимальный размер файла: **200 MB** (обычные файлы)
- Максимальный размер U-Boot: **1 MB** (безопасно для boot area)
- Порт сервера: **43243** - Порт сервера: **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 окружении - Используйте с осторожностью в production окружении
- Рекомендуется ограничить доступ к веб-интерфейсу - Рекомендуется ограничить доступ к веб-интерфейсу (firewall, auth)
## Зависимости ## Зависимости
Все зависимости встроены в проект: Все зависимости встроены в проект:
- `mongoose.c/h` - веб-сервер (из репозитория Cesanta) - `mongoose.c/h` - веб-сервер (из репозитория Cesanta, v7.19)
- `cJSON.c/h` - JSON парсер (из репозитория DaveGamble) - `cJSON.c/h` - JSON парсер (из репозитория DaveGamble, v1.7.18)
## Лицензия ## Лицензия
+440 -19
View File
@@ -4,6 +4,8 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/mount.h> #include <sys/mount.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <sys/ioctl.h>
#include <mtd/mtd-user.h>
#include <unistd.h> #include <unistd.h>
#include <dirent.h> #include <dirent.h>
#include <errno.h> #include <errno.h>
@@ -18,9 +20,11 @@
#define KERNEL_FILE "Image.gz" #define KERNEL_FILE "Image.gz"
#define DTB_FILE "sun50i-h618-orangepi-zero2w.dtb" #define DTB_FILE "sun50i-h618-orangepi-zero2w.dtb"
#define UBOOT_FILE "uboot.env" #define UBOOT_FILE "uboot.env"
#define UBOOT_BIN_FILE "u-boot-sunxi-with-spl.bin"
#define EXTLINUX_FILE "extlinux/extlinux.conf" #define EXTLINUX_FILE "extlinux/extlinux.conf"
#define MAX_FILE_SIZE (200 * 1024 * 1024) // 200 MB #define MAX_FILE_SIZE (200 * 1024 * 1024) // 200 MB
#define UBOOT_MAX_SIZE (1 * 1024 * 1024) // 1 MB for U-Boot
static const char *s_http_port = "http://0.0.0.0:43243"; static const char *s_http_port = "http://0.0.0.0:43243";
static struct mg_mgr mgr; static struct mg_mgr mgr;
@@ -100,9 +104,135 @@ static const char* get_target_filename(const char *type) {
if (strcmp(type, "kernel") == 0) return KERNEL_FILE; if (strcmp(type, "kernel") == 0) return KERNEL_FILE;
if (strcmp(type, "dtb") == 0) return DTB_FILE; if (strcmp(type, "dtb") == 0) return DTB_FILE;
if (strcmp(type, "uboot") == 0) return UBOOT_FILE; if (strcmp(type, "uboot") == 0) return UBOOT_FILE;
if (strcmp(type, "uboot_bin") == 0) return UBOOT_BIN_FILE;
return NULL; return NULL;
} }
// Write U-Boot to SPI NOR (mtd0) and MMC (mmcblk1)
static int write_uboot_to_devices(const char *data, size_t len, const char *filepath) {
int success = 0;
size_t chunk_size = 4096;
// Allwinner H618 boot offset: 8KB (same as "bs=1024 seek=8" in dd)
#define MMC_BOOT_OFFSET 8192
printf("Writing U-Boot to devices (%zu bytes)...\n", len);
// Check size limit for SPI NOR (safe limit to avoid overwriting partitions)
if (len > UBOOT_MAX_SIZE) {
printf("U-Boot image too large for SPI NOR (%zu > %d bytes)\n", len, UBOOT_MAX_SIZE);
return -1;
}
// First, save to file in /media
FILE *f = fopen(filepath, "wb");
if (!f) {
perror("fopen uboot file");
return -1;
}
size_t written = 0;
while (written < len) {
size_t to_write = (len - written < chunk_size) ? (len - written) : chunk_size;
if (fwrite(data + written, 1, to_write, f) != to_write) {
fclose(f);
return -1;
}
written += to_write;
g_upload.progress = (int)(written * 30 / len); // First 30% for file write
}
fclose(f);
sync();
// Write to /dev/mtd0 (SPI NOR flash)
printf("Writing to /dev/mtd0 (SPI NOR)...\n");
int mtd_fd = open("/dev/mtd0", O_RDWR | O_SYNC);
if (mtd_fd >= 0) {
// Get erase block size for SPI NOR
struct mtd_info_user mtd_info;
memset(&mtd_info, 0, sizeof(mtd_info));
if (ioctl(mtd_fd, MEMGETINFO, &mtd_info) == 0) {
printf("SPI NOR info: type=%s, size=%u bytes, erasesize=%u bytes\n",
mtd_info.type == MTD_NORFLASH ? "NOR" : "UNKNOWN",
mtd_info.size, mtd_info.erasesize);
// Erase SPI NOR before writing
printf("Erasing SPI NOR...\n");
struct erase_info_user erase_info;
erase_info.start = 0;
erase_info.length = mtd_info.size; // Erase entire chip
if (ioctl(mtd_fd, MEMERASE, &erase_info) == 0) {
printf("SPI NOR erased successfully\n");
} else {
perror("MEMERASE failed");
printf("Continuing anyway (some systems auto-erase)...\n");
}
} else {
perror("MEMGETINFO failed");
printf("Could not get SPI NOR info, proceeding with write...\n");
}
// Write to SPI NOR
written = 0;
while (written < len) {
size_t to_write = (len - written < chunk_size) ? (len - written) : chunk_size;
ssize_t w = write(mtd_fd, data + written, to_write);
if (w < 0) {
perror("write mtd0 (SPI NOR)");
close(mtd_fd);
break;
}
written += w;
g_upload.progress = 30 + (int)(written * 35 / len); // 30-65% for SPI NOR
}
close(mtd_fd);
printf("Written %zu bytes to /dev/mtd0 (SPI NOR)\n", written);
success = 1;
} else {
perror("open /dev/mtd0");
printf("/dev/mtd0 (SPI NOR) not available, skipping...\n");
}
// Write to /dev/mmcblk1 at offset 8KB (Allwinner H618 boot location)
// This matches: dd if=u-boot.bin of=/dev/mmcblk1 bs=1024 seek=8
printf("Writing to /dev/mmcblk1 at offset %d (SD card boot area)...\n", MMC_BOOT_OFFSET);
int mmc_fd = open("/dev/mmcblk1", O_RDWR | O_SYNC);
if (mmc_fd >= 0) {
// Seek to 8KB offset (sector 16 for 512-byte sectors)
if (lseek(mmc_fd, MMC_BOOT_OFFSET, SEEK_SET) != MMC_BOOT_OFFSET) {
perror("lseek mmcblk1");
close(mmc_fd);
} else {
written = 0;
while (written < len) {
size_t to_write = (len - written < chunk_size) ? (len - written) : chunk_size;
ssize_t w = write(mmc_fd, data + written, to_write);
if (w < 0) {
perror("write mmcblk1");
close(mmc_fd);
break;
}
written += w;
g_upload.progress = 65 + (int)(written * 35 / len); // 65-100% for MMC
}
close(mmc_fd);
printf("Written %zu bytes to /dev/mmcblk1 at offset %d\n", written, MMC_BOOT_OFFSET);
success = 1;
}
} else {
perror("open /dev/mmcblk1");
printf("/dev/mmcblk1 not available, skipping...\n");
}
// Final sync to ensure all data is flushed
sync();
g_upload.progress = 100;
return success ? 0 : -1;
}
// Get MIME type based on file extension // Get MIME type based on file extension
static const char* get_mime_type(const char *path) { static const char* get_mime_type(const char *path) {
const char *ext = strrchr(path, '.'); const char *ext = strrchr(path, '.');
@@ -214,6 +344,25 @@ static void handle_extlinux_write(struct mg_connection *c, struct mg_http_messag
return; 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(&timestamp[i+1], &timestamp[i+3], strlen(&timestamp[i+3]) + 1);
}
}
}
}
if (mount_device() < 0) { if (mount_device() < 0) {
cJSON_Delete(root); cJSON_Delete(root);
mg_http_reply(c, 500, "", "{\"error\":\"Failed to mount device\"}\n"); mg_http_reply(c, 500, "", "{\"error\":\"Failed to mount device\"}\n");
@@ -232,6 +381,31 @@ static void handle_extlinux_write(struct mg_connection *c, struct mg_http_messag
fprintf(f, "%s", content->valuestring); fprintf(f, "%s", content->valuestring);
fclose(f); 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(); sync();
mg_http_reply(c, 200, "Content-Type: application/json\r\n", mg_http_reply(c, 200, "Content-Type: application/json\r\n",
@@ -282,6 +456,184 @@ static void handle_mount_status(struct mg_connection *c) {
cJSON_Delete(root); cJSON_Delete(root);
} }
// Handle temperature read
static void handle_temperature(struct mg_connection *c) {
float temperature = 0.0f;
int success = 0;
char path[256];
// Try all thermal zones (0-3)
for (int i = 0; i < 4 && !success; i++) {
snprintf(path, sizeof(path), "/sys/class/thermal/thermal_zone%d/temp", i);
FILE *f = fopen(path, "r");
if (f) {
// Check zone type if available
char type_path[256];
snprintf(type_path, sizeof(type_path), "/sys/class/thermal/thermal_zone%d/type", i);
FILE *type_f = fopen(type_path, "r");
if (type_f) {
char type[32] = {0};
if (fscanf(type_f, "%31s", type) == 1) {
// Prefer CPU or soc zones
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;
success = 1;
// If this is a CPU zone, use it immediately
if (is_cpu) {
fclose(f);
fclose(type_f);
break;
}
}
}
fclose(type_f);
}
fclose(f);
}
}
// If thermal zones failed, try hwmon (0-3)
if (!success) {
for (int i = 0; i < 4 && !success; i++) {
snprintf(path, sizeof(path), "/sys/class/hwmon/hwmon%d/temp1_input", i);
FILE *f = fopen(path, "r");
if (f) {
// Check sensor name
char name_path[256];
snprintf(name_path, sizeof(name_path), "/sys/class/hwmon/hwmon%d/name", i);
FILE *name_f = fopen(name_path, "r");
if (name_f) {
char name[32] = {0};
if (fscanf(name_f, "%31s", name) == 1) {
// Prefer CPU or sunxi sensors
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;
success = 1;
if (is_cpu) {
fclose(f);
fclose(name_f);
break;
}
}
}
fclose(name_f);
}
fclose(f);
}
}
}
// Last resort: try any temp1_input
if (!success) {
const char *fallback_paths[] = {
"/sys/class/hwmon/hwmon0/temp1_input",
"/sys/class/hwmon/hwmon1/temp1_input",
"/sys/class/hwmon/hwmon2/temp1_input",
"/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) {
int temp_raw;
if (fscanf(f, "%d", &temp_raw) == 1) {
temperature = temp_raw / 1000.0f;
success = 1;
}
fclose(f);
}
}
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "success", success);
cJSON_AddNumberToObject(root, "temperature", temperature);
cJSON_AddStringToObject(root, "unit", "Celsius");
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 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 // Handle system reboot
static void handle_reboot(struct mg_connection *c) { static void handle_reboot(struct mg_connection *c) {
cJSON *root = cJSON_CreateObject(); cJSON *root = cJSON_CreateObject();
@@ -344,8 +696,14 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
return; return;
} }
if (file_data.len > MAX_FILE_SIZE) { // Check file size limits
mg_http_reply(c, 400, "", "{\"error\":\"File too large (max 200MB)\"}\n"); size_t max_size = MAX_FILE_SIZE;
if (strcmp(type, "uboot_bin") == 0) {
max_size = UBOOT_MAX_SIZE; // 1MB limit for U-Boot (safe for boot area)
}
if (file_data.len > max_size) {
mg_http_reply(c, 400, "", "{\"error\":\"File too large\"}\n");
return; return;
} }
@@ -365,32 +723,91 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
printf("Upload: %s (%zu bytes) -> %s\n", type, file_data.len, filepath); printf("Upload: %s (%zu bytes) -> %s\n", type, file_data.len, filepath);
// Write file in chunks // Update upload state
FILE *fp = fopen(filepath, "wb"); memcpy(g_upload.type, type, strlen(type) + 1);
if (!fp) { g_upload.received = 0;
mg_http_reply(c, 500, "", "{\"error\":\"Failed to create file\"}\n"); g_upload.progress = 0;
return; 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(&timestamp[i+1], &timestamp[i+3], strlen(&timestamp[i+3]) + 1);
}
}
}
} }
size_t written = 0; // Special handling for U-Boot binary
size_t chunk_size = 65536; if (strcmp(type, "uboot_bin") == 0) {
while (written < file_data.len) { if (write_uboot_to_devices(file_data.buf, file_data.len, filepath) < 0) {
size_t to_write = (file_data.len - written < chunk_size) ? snprintf(g_upload.status, sizeof(g_upload.status), "Failed");
(file_data.len - written) : chunk_size; mg_http_reply(c, 500, "", "{\"error\":\"Failed to write U-Boot\"}\n");
if (fwrite(file_data.buf + written, 1, to_write, fp) != to_write) {
fclose(fp);
mg_http_reply(c, 500, "", "{\"error\":\"Failed to write file\"}\n");
return; return;
} }
written += to_write; } else {
// Write file in chunks for other types
FILE *fp = fopen(filepath, "wb");
if (!fp) {
mg_http_reply(c, 500, "", "{\"error\":\"Failed to create file\"}\n");
return;
}
size_t written = 0;
size_t chunk_size = 65536;
while (written < file_data.len) {
size_t to_write = (file_data.len - written < chunk_size) ?
(file_data.len - written) : chunk_size;
if (fwrite(file_data.buf + written, 1, to_write, fp) != to_write) {
fclose(fp);
mg_http_reply(c, 500, "", "{\"error\":\"Failed to write file\"}\n");
return;
}
written += to_write;
g_upload.progress = (int)(written * 100 / file_data.len);
}
fclose(fp);
sync();
g_upload.progress = 100;
} }
fclose(fp); // Write version file with timestamp
sync(); 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); printf("Upload complete: %s\n", filepath);
memcpy(g_upload.type, type, strlen(type) + 1);
g_upload.received = file_data.len; g_upload.received = file_data.len;
g_upload.progress = 100; g_upload.progress = 100;
snprintf(g_upload.status, sizeof(g_upload.status), "Complete"); snprintf(g_upload.status, sizeof(g_upload.status), "Complete");
@@ -417,6 +834,10 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
handle_progress(c); handle_progress(c);
} else if (mg_match_uri(hm->uri, "/api/mount")) { } else if (mg_match_uri(hm->uri, "/api/mount")) {
handle_mount_status(c); 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")) { } else if (mg_match_uri(hm->uri, "/api/reboot")) {
handle_reboot(c); handle_reboot(c);
} else if (mg_match_uri(hm->uri, "/")) { } else if (mg_match_uri(hm->uri, "/")) {
+105 -7
View File
@@ -15,13 +15,18 @@ document.addEventListener("DOMContentLoaded", function () {
const mountStatus = document.getElementById("mount-status"); const mountStatus = document.getElementById("mount-status");
const extlinuxEditor = document.getElementById("extlinux-editor"); const extlinuxEditor = document.getElementById("extlinux-editor");
const editorStatus = document.getElementById("editor-status"); const editorStatus = document.getElementById("editor-status");
const uploadHint = document.getElementById("upload-hint");
// Tab buttons // Tab buttons
const tabBtns = document.querySelectorAll(".tab-btn"); const tabBtns = document.querySelectorAll(".tab-btn");
// Initialize // Initialize
checkMountStatus(); checkMountStatus();
checkTemperature();
checkFileDates();
setInterval(checkMountStatus, 10000); // Check every 10 seconds 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 // Tab switching
tabBtns.forEach((btn) => { tabBtns.forEach((btn) => {
@@ -30,6 +35,13 @@ document.addEventListener("DOMContentLoaded", function () {
this.classList.add("active"); this.classList.add("active");
uploadType.value = this.dataset.type; uploadType.value = this.dataset.type;
hideResult(); hideResult();
// Show/hide U-Boot warning
if (this.dataset.type === "uboot_bin") {
uploadHint.classList.add("visible");
} else {
uploadHint.classList.remove("visible");
}
}); });
}); });
@@ -104,6 +116,9 @@ document.addEventListener("DOMContentLoaded", function () {
progressBytes.textContent = ""; progressBytes.textContent = "";
hideResult(); hideResult();
// Get current timestamp
const timestamp = new Date().toISOString();
try { try {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
@@ -125,6 +140,8 @@ document.addEventListener("DOMContentLoaded", function () {
`Successfully uploaded ${response.filename} (${formatBytes(response.size)})`, `Successfully uploaded ${response.filename} (${formatBytes(response.size)})`,
"success", "success",
); );
// Refresh file dates after upload
checkFileDates();
} else { } else {
let errorMsg = "Upload failed"; let errorMsg = "Upload failed";
try { try {
@@ -141,7 +158,10 @@ document.addEventListener("DOMContentLoaded", function () {
showResult("Network error", "error"); showResult("Network error", "error");
}); });
xhr.open("POST", "/api/upload"); xhr.open(
"POST",
`/api/upload?timestamp=${encodeURIComponent(timestamp)}`,
);
xhr.send(formData); xhr.send(formData);
} catch (error) { } catch (error) {
progressStatus.textContent = "Failed"; progressStatus.textContent = "Failed";
@@ -217,19 +237,27 @@ document.addEventListener("DOMContentLoaded", function () {
showEditorStatus("Saving...", ""); showEditorStatus("Saving...", "");
// Get current timestamp from browser
const timestamp = new Date().toISOString();
try { try {
const response = await fetch("/api/extlinux", { const response = await fetch(
method: "POST", `/api/extlinux?timestamp=${encodeURIComponent(timestamp)}`,
headers: { {
"Content-Type": "application/json", method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ content: content }),
}, },
body: JSON.stringify({ content: content }), );
});
const data = await response.json(); const data = await response.json();
if (data.status === "success") { if (data.status === "success") {
showEditorStatus("Successfully saved extlinux.conf", "success"); showEditorStatus("Successfully saved extlinux.conf", "success");
// Refresh file dates after save
checkFileDates();
} else { } else {
showEditorStatus(data.error || "Failed to save", "error"); showEditorStatus(data.error || "Failed to save", "error");
} }
@@ -306,4 +334,74 @@ document.addEventListener("DOMContentLoaded", function () {
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
} }
// Temperature functions
const temperatureStatus = document.getElementById("temperature-status");
const tempValue = temperatureStatus
? temperatureStatus.querySelector(".temp-value")
: null;
async function checkTemperature() {
if (!tempValue) return;
try {
const response = await fetch("/api/temperature");
const data = await response.json();
if (data.success) {
tempValue.textContent = data.temperature.toFixed(1);
tempValue.style.color = getTempColor(data.temperature);
} else {
tempValue.textContent = "N/A";
tempValue.style.color = "#888";
}
} catch (error) {
tempValue.textContent = "Error";
tempValue.style.color = "#ff4757";
}
}
function getTempColor(temp) {
if (temp < 50) return "#00ff88"; // Green - cool
if (temp < 70) return "#00d9ff"; // Blue - normal
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";
}
}
}
}); });
+52
View File
@@ -33,6 +33,50 @@
<span class="status-text">Checking...</span> <span class="status-text">Checking...</span>
</span> </span>
</div> </div>
<div class="status-item">
<span class="label">CPU Temperature:</span>
<span class="value" id="temperature-status">
<span class="temp-icon">🌡️</span>
<span class="temp-value">--</span>
<span class="temp-unit">°C</span>
</span>
</div>
</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> </div>
</section> </section>
@@ -48,6 +92,9 @@
<button class="tab-btn" data-type="uboot"> <button class="tab-btn" data-type="uboot">
U-Boot Environment U-Boot Environment
</button> </button>
<button class="tab-btn" data-type="uboot_bin">
U-Boot Binary (SPL)
</button>
</div> </div>
<div class="upload-area"> <div class="upload-area">
@@ -73,6 +120,11 @@
<span class="file-name" id="file-name"></span> <span class="file-name" id="file-name"></span>
</label> </label>
</div> </div>
<div class="upload-hint" id="upload-hint">
⚠️ U-Boot Binary will be written to SPI NOR
(/dev/mtd0) and SD card at 8KB offset (/dev/mmcblk1
+8KB)
</div>
<button type="submit" class="upload-btn">Upload</button> <button type="submit" class="upload-btn">Upload</button>
</form> </form>
+65
View File
@@ -98,6 +98,56 @@ h2 {
box-shadow: 0 0 10px #ff4757; box-shadow: 0 0 10px #ff4757;
} }
/* Temperature Display */
.temp-icon {
margin-right: 8px;
}
.temp-value {
color: #00d9ff;
font-size: 1.3rem;
}
.temp-unit {
color: #888;
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 Section */
.upload-tabs { .upload-tabs {
display: flex; display: flex;
@@ -131,6 +181,21 @@ h2 {
text-align: center; text-align: center;
} }
.upload-hint {
margin: 15px 0;
padding: 12px;
background: rgba(255, 165, 0, 0.15);
border: 1px solid rgba(255, 165, 0, 0.5);
border-radius: 8px;
color: #ffa500;
font-size: 0.9rem;
display: none;
}
.upload-hint.visible {
display: block;
}
.file-input-wrapper { .file-input-wrapper {
margin-bottom: 20px; margin-bottom: 20px;
} }