Skip to content

Application Development

Application development on UGOS Pro system mainly includes the following steps:

  • Develop and compile the application backend service
  • Develop the application frontend interface using web frontend technologies
  • Use the developer tool ugcli to create an application project
  • Write the application configuration file project.yaml
  • Copy the backend service executable and frontend files to the corresponding directories in the application project
  • Create the application icon and copy it to the corresponding directory in the application project
  • Use the developer tool ugcli to package the application

The following guide walks you through creating a simple example application from scratch. You will learn the basic development workflow and how to use the ugcli tool.

Develop the Backend Service

Currently, UGOS Pro application backend services must be developed using compiled languages like C/C++, or Go, and built into executables that can run directly on Linux.

For this example, we will create a simple backend service in Go that provides a basic HTTP heartbeat API.

Create the backend service directory backend and write the main.go file:

Click to view code
go
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"net/http"
	"time"
)

// HeartbeatResponse defines the heartbeat response structure
type HeartbeatResponse struct {
	Timestamp string `json:"timestamp"`
	Status    string `json:"status"`
}

// heartbeatHandler handles heartbeat requests
func heartbeatHandler(w http.ResponseWriter, r *http.Request) {
	// Only allow GET method
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	// Set response headers
	w.Header().Set("Content-Type", "application/json")

	// Create response data
	response := HeartbeatResponse{
		Timestamp: time.Now().Format(time.DateTime),
		Status:    "healthy",
	}

	// Encode JSON response
	if err := json.NewEncoder(w).Encode(response); err != nil {
		http.Error(w, "Failed to encode response", http.StatusInternalServerError)
		return
	}
}

func main() {
	// Define command line arguments
	port := flag.Int("port", 21010, "Port to listen on")
	flag.Parse()

	// Register routes
	http.HandleFunc("/api/heartbeat", heartbeatHandler)

	// Build listen address
	addr := fmt.Sprintf(":%d", *port)

	// Start server
	fmt.Printf("Server starting on %s\n", addr)
	if err := http.ListenAndServe(addr, nil); err != nil {
		fmt.Printf("Server failed to start: %v\n", err)
	}
}

Run go build -o myapp_serv to compile and generate the backend service executable myapp_serv.

The final directory structure of backend is as follows:

shell
backend/
├── go.mod
├── main.go
└── myapp_serv

Develop the Frontend Page

You can freely choose a frontend framework such as Vue, React, etc., to develop application's interface. After development is complete, you need to place the build artifacts (HTML, CSS, JS, etc.) in the www directory of the application package.

The frontend page of this example application will be developed using HTML + CSS + JavaScript, implementing a simple user interface to display the heartbeat status of the backend service.

Create the www/index.html file with the following content:

Click to view code
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MyApp</title>

    <link rel="stylesheet" href="./css/style.css">
</head>
<body>
    <div class="container">
        <h1>MyApp Status Monitor</h1>

        <div class="auto-start">Monitoring starts automatically after page loads...</div>

        <div id="status" class="status disconnected">
            <div class="status-indicator"></div>
            <span>Initializing...</span>
        </div>

        <div class="response-display" id="responseDisplay">
            Monitoring data will be displayed here...
        </div>
    </div>

    <script src="./js/app.js"></script>
</body>
</html>

Create the www/js/app.js file with the following content:

Click to view code
javascript
let heartbeatInterval = null;
let requestCount = 0;
let isAutoStarted = false;
let maxEntries = 2;

// Update status display
function updateStatus(connected, message) {
    const statusElement = document.getElementById('status');
    const statusText = statusElement.querySelector('span');

    if (connected) {
        statusElement.className = 'status connected';
        statusText.textContent = message || 'Connection OK';
    } else {
        statusElement.className = 'status disconnected';
        statusText.textContent = message || 'Connection Lost';
    }
}

// Format time display
function formatTime(dateString) {
    const date = new Date(dateString);
    return date.toLocaleString('zh-CN', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit'
    });
}

// Send heartbeat request
async function sendHeartbeat() {
    const apiUrl = "/api/heartbeat";
    const responseDisplay = document.getElementById('responseDisplay');

    requestCount++;

    try {
        const response = await fetch(apiUrl);

        if (!response.ok) {
            throw new Error(`HTTP Error: ${response.status}`);
        }

        const data = await response.json();

        // Update response display
        const newEntry = `Request #${requestCount} - ${new Date().toLocaleTimeString()}\n` +
            JSON.stringify(data, null, 2) + '\n\n';

        // Keep response display area concise, show only recent records
        const currentContent = responseDisplay.textContent;
        const entries = currentContent.split('\n\n').filter(entry => entry.trim());
        entries.unshift(newEntry.trim());

        // Keep only the most recent records
        if (entries.length > maxEntries) {
            entries.length = maxEntries;
        }

        responseDisplay.textContent = entries.join('\n\n');

        updateStatus(true, `Monitoring - Last update: ${new Date().toLocaleTimeString()}`);

    } catch (error) {
        // Update response display
        const newEntry = `Request #${requestCount} - ${new Date().toLocaleTimeString()}\n` +
            `Error: ${error.message}\n\n`;

        const currentContent = responseDisplay.textContent;
        const entries = currentContent.split('\n\n').filter(entry => entry.trim());
        entries.unshift(newEntry.trim());

        if (entries.length > maxEntries) {
            entries.length = maxEntries;
        }

        responseDisplay.textContent = entries.join('\n\n');

        updateStatus(false, `Connection error: ${error.message}`);
    }
}

// Start heartbeat test
function startHeartbeat() {
    if (heartbeatInterval) {
        clearInterval(heartbeatInterval);
    }

    // Send a request immediately
    sendHeartbeat();

    // Set timer to send request every second
    heartbeatInterval = setInterval(sendHeartbeat, 1000);

    updateStatus(true, 'Monitoring started...');
}

// Stop heartbeat test
function stopHeartbeat() {
    if (heartbeatInterval) {
        clearInterval(heartbeatInterval);
        heartbeatInterval = null;
    }

    updateStatus(false, 'Monitoring paused');
}

// Automatically start monitoring after page loads
document.addEventListener('DOMContentLoaded', function () {
    // Auto start monitoring
    setTimeout(() => {
        startHeartbeat();
        isAutoStarted = true;
    }, 10);
});

Create the www/css/style.css file with the following content:

Click to view code
css
body {
    font-family: Arial, sans-serif;
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
    background-color: #f5f5f5;
}

.container {
    background: white;
    padding: 30px;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

h1 {
    color: #333;
    text-align: center;
    margin-bottom: 30px;
}

.status {
    display: flex;
    align-items: center;
    margin-bottom: 20px;
    padding: 10px;
    border-radius: 4px;
}

.status.connected {
    background-color: #d4edda;
    color: #155724;
}

.status.disconnected {
    background-color: #f8d7da;
    color: #721c24;
}

.status-indicator {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    margin-right: 10px;
}

.status.connected .status-indicator {
    background-color: #28a745;
}

.status.disconnected .status-indicator {
    background-color: #dc3545;
}

.response-display {
    background-color: #f8f9fa;
    border: 1px solid #e9ecef;
    border-radius: 4px;
    padding: 15px;
    margin-top: 20px;
    font-family: 'Courier New', monospace;
    white-space: pre-wrap;
    min-height: 100px;
    max-height: 300px;
    overflow-y: auto;
}

.timestamp {
    font-size: 1.2em;
    font-weight: bold;
    color: #007bff;
    text-align: center;
    margin: 20px 0;
}

.controls {
    text-align: center;
    margin: 20px 0;
}

button {
    padding: 10px 20px;
    margin: 0 10px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
}

.start-btn {
    background-color: #007bff;
    color: white;
}

.stop-btn {
    background-color: #dc3545;
    color: white;
}

.config {
    margin-bottom: 20px;
}

label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
}

input[type="text"] {
    width: 100%;
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
    box-sizing: border-box;
}

.auto-start {
    text-align: center;
    color: #666;
    font-style: italic;
    margin-bottom: 10px;
}

The final directory structure of www is as follows:

shell
www
├── css
   └── style.css
├── index.html
└── js
    └── app.js

Browser Cache Handling for css/js Files

The system web server sets Cache-Control headers when responding to css/js files to allow browser caching. To prevent browsers from still using old versions of files after a new application version is released, it is recommended to use one of the following methods:

  • Filename hashing, e.g., app.a1b2c3d4.js, change the filename when modifying file content
  • Query parameter with version number, e.g., app.js?v=1.0.1, update the version number when releasing a new version

Create the Application Project

Run ugcli create com.mycompany.myapp to create an application project. After creation, a com.mycompany.myapp directory will be generated in the current directory with the following structure:

shell
com.mycompany.myapp
├── project.yaml            # Application configuration file
├── rootfs_amd64            # Application files for x86_64 architecture
   └── bin
├── rootfs_arm64            # Application files for arm64 architecture
   └── bin
├── rootfs_common           # Common application files for both architectures
   ├── icon.png            # Application icon
   └── www                 # Application frontend files

Write the Application Configuration File

Modify the project.yaml file in the application project root directory to configure the basic information of the application. The main configuration items are as follows:

yaml
spec_version: "2.1"                     # Specification version number
app_id: com.mycompany.myapp             # Unique application identifier
version: 0.1.0                          # Application version number
support_arch:                           # Supported CPU architectures
  - amd64
  - arm64
start_cmd: bin/myapp_serv --port=21010  # Application backend service startup command
port: 21010                             # Application backend HTTP service port
proxy_path: api                         # Application backend HTTP service proxy path
open_type: inner                        # Application frontend page open type
tag_types:                              # Application tags
  - utility
  - devtool
depend_fw_version: 1.13.0.0000
i18n:                                               # Internationalization configuration
  en-US:                                            # Language name
    name: My APP                                    # Application display name
    description: My APP Desc                        # Application description
    author: My Company                              # Application developer
    official: https://myapp.example.com             # Application official website link
    help: https://myapp.example.com/help            # Application help page link
    publisher: My Company                           # Application publisher
    publisher_link: https://mycompany.example.com   # Application publisher website link
  zh-CN:
    name: 演示应用
    description: 演示应用描述
    author: 演示应用开发者
    official: https://myapp.example.com.cn
    help: https://myapp.example.com.cn/help
    publisher: 演示应用发布者
    publisher_link: https://mycompany.example.com.cn

Copy Application Files

Copy the application backend service executable and frontend files to the corresponding directories in the application project.

  • Copy the backend service executables for both architectures to the corresponding rootfs directories, e.g., copy the amd64 architecture executable to rootfs_amd64/bin/myapp_serv
  • Copy all files from the frontend www directory to the rootfs_common/www directory.

Create the Application Icon

Please refer to the application icon design guidelines to create the application icon and replace the rootfs_common/www/icon.png file. (This step can be skipped during development, using the default icon)

Icon Design Guidelines:

  • Icon size: 256*256 pixels
  • Icon format: PNG
  • File size: Less than 100KB
  • Icon background: White or light background with 0.5 thickness 8% black stroke
  • Icon shape: Square, requires smooth rounded corner template

Package the Application

Execute the ugcli pack command in the application project root directory to package the application and generate the upk installation package file.

Command example: ugcli pack --build 1, where the --build parameter is the build number, which will be combined with the version number in project.yaml to form the final application version number. For example, if the version number in project.yaml is 0.1.0, the final version number will be 0.1.0.1.

The final upk file naming format: {amd64/arm64}_{appid}_{version}.upk.