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
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:
commit
de82cfe21b
9 files changed
+810
-83
No files matched your search
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
│ └── cJSON.c/h # JSON parser (v1.7.18)
|
||||||
|
├── web/
|
||||||
|
│ ├── index.html # HTML веб-интерфейса
|
||||||
|
│ ├── style.css # Стили веб-интерфейса
|
||||||
|
│ └── app.js # Frontend JavaScript
|
||||||
|
├── mongoose_config.h # Конфигурация mongoose
|
||||||
├── Makefile # Конфигурация сборки
|
├── Makefile # Конфигурация сборки
|
||||||
├── README.md # Документация
|
└── 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
@@ -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)
|
||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
|
|
||||||
|
|||||||
+425
-4
@@ -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(×tamp[i+1], ×tamp[i+3], strlen(×tamp[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,7 +723,40 @@ 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
|
||||||
|
memcpy(g_upload.type, type, strlen(type) + 1);
|
||||||
|
g_upload.received = 0;
|
||||||
|
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) {
|
||||||
|
snprintf(g_upload.status, sizeof(g_upload.status), "Failed");
|
||||||
|
mg_http_reply(c, 500, "", "{\"error\":\"Failed to write U-Boot\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Write file in chunks for other types
|
||||||
FILE *fp = fopen(filepath, "wb");
|
FILE *fp = fopen(filepath, "wb");
|
||||||
if (!fp) {
|
if (!fp) {
|
||||||
mg_http_reply(c, 500, "", "{\"error\":\"Failed to create file\"}\n");
|
mg_http_reply(c, 500, "", "{\"error\":\"Failed to create file\"}\n");
|
||||||
@@ -383,14 +774,40 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
written += to_write;
|
written += to_write;
|
||||||
|
g_upload.progress = (int)(written * 100 / file_data.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
sync();
|
sync();
|
||||||
|
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);
|
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, "/")) {
|
||||||
|
|||||||
+101
-3
@@ -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(
|
||||||
|
`/api/extlinux?timestamp=${encodeURIComponent(timestamp)}`,
|
||||||
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user