Reload OS

App kit guide

The guide that comes with the app kit of Reload OS 1.4.0 (README.md in the kit), as it is in the kit.

Build your own apps for a Reload OS device - the 7" ESP32-P4 touch kiosk by Reloader Co.,Ltd. - without the firmware's source. An app is a small ELF file that you copy into the apps/ folder of the device's flash drive or SD card; the launcher shows it as a tile, and the device loads it when the tile is tapped. hello/ is a complete example. The kit matches the firmware version in reloados/VERSION.

appkit/
  README.md          this file
  build.sh           ./build.sh <app> [jc|jc4880|jc8012|matouch]   builds one app for one board
  new.sh             ./new.sh <name>                 starts a new app from the Hello example
  hello/             the example: CMakeLists.txt, main/hello.c, main/CMakeLists.txt, main/idf_component.yml
  reloados/          from the firmware - do not edit:
    VERSION          which firmware this kit belongs to
    api.txt          every function and variable an app may call
    main/ros.h       the Reload OS API (and ros_board.h: the boards)
    sdkconfig.*      the firmware's configuration, which the app is built against
    elfapp.cmake     the build rules; components/: the firmware's menu, needed by ros_board.h

#Requirements

  • ESP-IDF v5.5.4 - exactly this version, the one the firmware is built with (an app is linked against the firmware on the device; LVGL's and FreeRTOS's structures must be laid out the same way). Install it as Espressif describes (https://docs.espressif.com/projects/esp-idf/en/v5.5.4/esp32p4/get-started/), with the ESP32-P4 target, and run its export.sh in the terminal you build from. build.sh finds an installation in ~/.espressif/v5.5.4/esp-idf or ~/esp/esp-idf by itself.
  • Internet for the first build: the component manager downloads LVGL 9.2.2, the ELF loader 1.3.3 and cmake_utilities 0.5.3 (the versions in main/idf_component.yml; don't change them).
  • macOS or Linux (bash). Keep the kit outside folders that a cloud drive syncs (iCloud Drive, OneDrive, Dropbox): they evict and re-download the thousands of files a build makes.

#Build the example

./build.sh hello              # -> hello/jc1060p470c/hello.elf   (the JC1060P470C, the production board)
./build.sh hello matouch      # -> hello/matouch-p4-7/hello.elf  (the Makerfabs MaTouch development board)
./build.sh hello jc4880       # -> hello/jc4880p443/hello.elf    (the Guition JC4880P443, 4.3" 480x800)
./build.sh hello jc8012       # -> hello/jc8012p4a1/hello.elf    (the Guition JC8012P4A1, 10.1" 800x1280)

An app is built for one board (it gets that board's configuration and pins); the device refuses the other board's file with a message naming both. The script ends with a check that everything the app calls is in the firmware's table - see "What an app can call".

#In VS Code

Open the appkit folder (File › Open Folder). Terminal › Run Build Task (⇧⌘B) builds the app whose file is open in the editor (hello when none is) with ./build.sh, for the JC1060P470C; the other tasks build for the MaTouch and start a new app. Build in the integrated terminal with ./build.sh hello works just the same.

VS Code opens a new folder in Restricted Mode, where tasks don't run: trust the folder (the banner's Manage › Trust). The ESP-IDF extension picks the newest ESP-IDF installed and the esp32 target for a folder it has not seen: set them to v5.5.4 and esp32p4 in the status bar. (build.sh looks for v5.5.4 itself when the terminal holds another version, and stops when there is none.)

Don't use the ESP-IDF extension's Build / Flash / Monitor buttons on this folder: they build and link a whole firmware, which an app is not (its functions are the firmware's, found when it is loaded), so that build fails - and an app is never flashed, it is copied to the device. The extension may still write its .vscode/settings.json and .clangd here; they do no harm. For IntelliSense after the first build, point clangd or the C/C++ extension at <app>/build_jc/compile_commands.json.

#Put it on the device

Copy the .elf into the apps/ folder (make it if it is not there) of

  • the device's flash drive - the usual place. From a browser: the remote desktop's Files page, http://<device>/files (Settings › Remote desktop on the device gives the address and the login) - Flash › apps › Upload. Or with the device plugged into a PC as a USB drive (Settings › USB › USB_OTG port: Drive for PC), or from a card with the device's Files app (long-press the file › Copy › Flash).
  • or the SD card (apps/ on the card, in a card reader).

Then restart the device: the launcher reads the folders at start. The flash drive is read first, the card second; a file both have opens from the flash drive (the card's copy opens only when the flash drive's can't be used). To update an app that is on the flash drive, replace it there - a newer file on the card alone does not run.

The tile shows the app's name, icon and colour from its ROS_ELFAPP_META(...) line (read from the file without loading it). Tap it: the file is read into PSRAM, checked, linked against the firmware and run. Back (swipe from the left edge) or the home button closes it; when nothing of it runs any more it is unloaded.

#Your own app

./new.sh myapp                # -> myapp/ : a copy of hello/ under the new name
# edit myapp/main/myapp.c
./build.sh myapp              # -> myapp/jc1060p470c/myapp.elf

The shape of an app (hello/main/hello.c):

#include "ros.h"

static void create(lv_obj_t *body)   /* build the screen inside body; runs on the LVGL task */
{
    ros_ui_label(body, &lv_font_montserrat_32, ROS_C_TEXT, "Hello");
}

static void destroy(void)            /* the app closes: delete timers, stop what you started */
{
}

ROS_ELFAPP_META("Hello", LV_SYMBOL_SD_CARD, 0x6B4FBB);      /* the tile: name, glyph, colour */

static const ros_elfapp_t s_app = {
    .abi = ROS_ELFAPP_ABI, .board = ROS_BOARD_SHORT, .meta = &ros_elfapp_meta,
    .app = { .name = "Hello", .icon = LV_SYMBOL_SD_CARD, .color = 0x6B4FBB, .create = create, .destroy = destroy },
};

int main(int argc, char *argv[])     /* only hands the app back; the work starts in create() */
{
    if (argc > 0 && argv[0]) *(const ros_elfapp_t **)argv[0] = &s_app;
    return 0;
}
  • body is an empty full-screen LVGL object under the status bar; everything you create inside it is deleted by the system when the app closes. The screen is 1024×600 or 600×1024: the device may be mounted horizontally or vertically. Use ROS_SCR_W, ROS_SCR_H and ROS_PORTRAIT (values, not constants) and check both ways.
  • ros.h is the API: ros_ui_label(), ros_ui_button(), ros_toast(), the colours ROS_C_* (they follow the device's light / dark theme), ros_lock() / ros_unlock() for LVGL calls from your own tasks, ros_audio_*, ros_settings(), the network, SD card, camera, USB, printer and card reader services. Every LVGL 9.2.2 function the firmware itself uses is available too (api.txt), and the fonts lv_font_montserrat_14 ... 48 and the Thai fonts rld_font_thai_*.
  • The icon is an LV_SYMBOL_* glyph, or "@name" for one of the firmware's drawn icons. A picture instead: ROS_ELFAPP_META_FLAGS(..., ROS_ELFAPP_ON_WHITE) and ROS_ELFAPP_TILE("path/to/icon.png") (PNG, at most 64 KB; the path is best given from main/CMakeLists.txt with target_compile_definitions). Files an app needs (pictures, sounds) go into the app with EMBED_FILES in main/CMakeLists.txt, as any ESP-IDF project embeds them.
  • Headers of ESP-IDF parts you use (nvs_flash, esp_driver_uart, ...) are added to REQUIRES in main/CMakeLists.txt - for the headers only; the functions are the firmware's, if it exports them.

#What an app can call

Only what the firmware exports: the names in reloados/api.txt (the Reload OS API from ros.h, the LVGL functions the firmware uses, the fonts, the C library, FreeRTOS and ESP-IDF basics). build.sh compares your app with the list and refuses to call it usable when a name is missing - on the device such an app is refused with "Needs a function this firmware doesn't have", and the device's log names it. If you need something that is not in the list, tell Reloader: the next firmware can export it (a name costs 8 bytes), and your app then needs that firmware or newer.

The interface number ROS_ELFAPP_ABI (in ros.h) is checked when the app is opened: a kit of a later firmware may carry a new number, and apps are then rebuilt with it ("Made for app interface N, this firmware has M" on the screen). Apps built with this kit run on this firmware version and on later ones with the same number.

#Rules - the ones that restart the device when broken

An app runs with the firmware's full rights, in the firmware's memory: a crash in it restarts the device, and so does blocking the screen.

  • Nothing that waits on the LVGL task. create(), destroy(), LVGL event and timer callbacks run on the LVGL task: no network, file, serial or card-reader work there - start a task for it and send the result back (ros_lock(ms) / ros_unlock() around LVGL calls from the task, and check the app is still open). A screen that does not respond for 60 s restarts the device.
  • Count your tasks. The app's code is freed when it is unloaded, so nothing may still run it: every task of your own adds 1 to a counter before it is created (__atomic_add_fetch(&s_live, 1, __ATOMIC_SEQ_CST)), the app gives .tasks = &s_live, and each task ends with ros_task_end(&s_live, false) - never with vTaskDelete(). The firmware unloads the app only when the counter is back to 0 (a task still finishing a network call simply ends first).
  • Clean up in destroy(): LVGL timers, anything you put on lv_layer_top() (keyboards, message boxes), display event callbacks, sounds you started.
  • Every load starts with fresh variables. What the app makes once and keeps across opens (a queue, a mutex, a buffer) is made again at every load: give .unload = fn and free it there, or it leaks.
  • Big buffers come from PSRAM: heap_caps_malloc(size, MALLOC_CAP_SPIRAM). Internal RAM is the scarce one (about 100 KB free), and malloc() of less than 4 KB takes it.
  • A stream buffer you delete gets its own memory (xStreamBufferCreateStatic() on a PSRAM block, then vStreamBufferDelete() + free()): ESP-IDF 5.5.4's vStreamBufferDeleteWithCaps() frees it twice.
  • Don't edit reloados/: ros.h describes the firmware's structures as they are - a changed header builds an app that reads the wrong bytes.

#When something is wrong

On the deviceWhy
no tile for the filenot in apps/ of the flash drive or the card, or the device was not restarted; a file name longer than about 65 characters is left out (the log says)
"Built for the X, this is a Y"built for the other board: ./build.sh <app> jc for the JC1060P470C
"Made for app interface N, this firmware has M"the kit and the firmware don't match: use the kit of that firmware
"Needs a function this firmware doesn't have"a name not in api.txt (build.sh would have said) - or a kit newer than the firmware
"Damaged or not a Reload OS app"the copy is incomplete - copy the file again
the device restarts when the app runsa crash in the app; the device's log (USB serial, 115200) and Settings › About show the crash record

Questions and names to export: Reloader Co.,Ltd.