📱 HTML Guide with SoifGo

💡 Copy this entire page into an AI assistant (like DeepSeek) and describe what you want — the AI will build the HTML for you.

📖 Overview

🤖 Build Your Custom App Without Hard Coding!

Don't stress! You don't need to be an HTML or JavaScript expert to use this golden feature. Just copy this entire guide page (the text you're reading right now) and paste it into an AI assistant like DeepSeek or any other AI. Then, simply describe what you want in plain English, for example: "Create an HTML page with 4 directional buttons to control a robot, and display ultrasonic sensor data received via Bluetooth." The AI will generate the complete code for you!

📋 How to use it in SoifGo:

  1. In SoifGo, create a new button, set its Behavior to Web, select Web Offline HTML, and tap Create.
  2. Tap the Edit button, then choose Play.
  3. In the Play screen, tap the button you just created, then open the left‑hand menu (top‑left) and select Edit.
  4. You are now in the edit screen. You can manually Select All, delete everything, and Paste your code, or simply use the "Select All and Paste" option from the menu to do it all in one step. Wait a moment for the syntax highlighting to appear.
  5. Wait until the code changes from solid black to colored (syntax highlighting has finished loading). Once it's ready, tap the Close (×) icon to exit the editor and see your custom app in action!

🎯 That's it! Ask the AI to build any app you want and run it inside SoifGo instantly.

SoifGo turns your HTML pages into a real Android app. Inside its WebView, your page can talk to Bluetooth, read phone sensors, request microphone access, connect to MQTT brokers, send/receive API data, and store data — all with plain HTML, CSS, and JavaScript.

This guide shows you how to:

📦 Working Examples

Ready-to-use controllers. Each example includes: HTML live demo, HTML Code copyable code, and where available Arduino ↓ sketch.

📥 Download all examples as ZIP

🔌 The Bridge — How HTML Talks to SoifGo

Inside SoifGo's WebView, a JavaScript object called window.soifgo is automatically injected. Use it to call native functions from JavaScript.

📄 Bridge call pattern
// Basic call pattern
window.soifgo.CallSub('subName', true, arg1, arg2, arg3);

// Real example — send text to Bluetooth
window.soifgo.CallSub('html_bluetooth_rx', true, txt);

// Real example — connect to MQTT broker (step 1)
window.soifgo.CallSub('connecte_mqtt_ai', true, address, id);

// Real example — send data to API
window.soifgo.CallSub('api_send', true, address, key, value);

// Real example — receive data from API
window.soifgo.CallSub('api_recive', true, address, key);
✅ Golden rules:
  • Always call window.soifgo.CallSub(...) directly.
  • The second argument is a boolean flag — pass true or false.
  • After the flag, you can pass up to 3 arguments.
  • Empty strings must still be passed as '' — never omit them.

🔵 1. Bluetooth — Send Data

WebView → SoifGo → Bluetooth

This page allows the user to enter text and send it via Bluetooth. The data goes from the WebView to SoifGo, then to the connected Bluetooth device.

📄 html_bluetooth_send.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Send Command</title>
</head>
<body style="text-align:center; margin-top:50px; font-family:sans-serif;">

  <h3>📤 Send Command to Bluetooth</h3>
  <input type="text" id="txt" placeholder="Enter text...">
  <br><br>
  <button onclick="sendBluetooth()"
          style="padding:10px 30px; font-size:16px; background:#2196f3;
                 color:white; border:none; border-radius:6px; cursor:pointer;">
    Send
  </button>

  <script>
    function sendBluetooth() {
      const txt = document.getElementById('txt').value;
      window.soifgo.CallSub('html_bluetooth_rx', true, txt);
    }
  </script>
</body>
</html>
✅ How it works:
  1. User types text in the input box.
  2. Clicks the Send button.
  3. JavaScript calls window.soifgo.CallSub('html_bluetooth_rx', true, txt).
  4. SoifGo sends the text to the connected Bluetooth device.

🔵 2. Bluetooth — Receive Data

Bluetooth → SoifGo → WebView

This page displays data received from Bluetooth. SoifGo forwards incoming data to the WebView using JavaScript functions.

📄 html_bluetooth_recive.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>BT Receive</title>
</head>
<body>
    <div id="output"
         style="font-size:20px;color:white;background:black;
                padding:20px;min-height:100vh;">
        waiting for data...
    </div>

    <script>
        // This function is called by SoifGo when Bluetooth data arrives
        function html_bluetooth_tx(data) {
            document.getElementById('output').innerHTML = data;
        }
        // Expose to window so SoifGo can find it
        window.html_bluetooth_tx = html_bluetooth_tx;
    </script>
</body>
</html>
✅ How it works:
  1. Bluetooth device sends data to SoifGo.
  2. SoifGo calls html_bluetooth_tx(data) in the WebView.
  3. JavaScript updates the page with the received data.

🟢 3. Sensor Data — Receive

Sensors → SoifGo → WebView

This page displays data from 3 sensors:

⚠️ IMPORTANT: To activate sensors, you must call ALL THREE commands:
window.soifgo.CallSub('sensor_light', true);   // Activate light sensor
window.soifgo.CallSub('sensor_move', true);    // Activate movement sensor
window.soifgo.CallSub('sensor_magno', true);   // Activate magneto sensor

These 3 commands are called inside window.onload to auto-start sensors when the page loads.

📄 html_sensor_recive.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sensor Receive</title>
</head>
<body>
    <div id="light"  style="font-size:20px;color:white;background:#333;padding:15px;margin:5px;border-radius:8px;">
        Light: waiting...
    </div>
    <div id="movex"  style="font-size:20px;color:white;background:#444;padding:15px;margin:5px;border-radius:8px;">
        Move X: waiting...
    </div>
    <div id="movey"  style="font-size:20px;color:white;background:#555;padding:15px;margin:5px;border-radius:8px;">
        Move Y: waiting...
    </div>
    <div id="magno"  style="font-size:20px;color:white;background:#666;padding:15px;margin:5px;border-radius:8px;">
        Magneto: waiting...
    </div>

    <script>
        // ---- Receiver functions (called by SoifGo) ----
        function html_sensor_light(data) {
            document.getElementById('light').innerHTML = 'Light: ' + data;
        }
        function html_sensor_movex(data) {
            document.getElementById('movex').innerHTML = 'Move X: ' + data;
        }
        function html_sensor_movey(data) {
            document.getElementById('movey').innerHTML = 'Move Y: ' + data;
        }
        function html_sensor_magno(data) {
            document.getElementById('magno').innerHTML = 'Magneto: ' + data;
        }

        // Expose them
        window.html_sensor_light = html_sensor_light;
        window.html_sensor_movex = html_sensor_movex;
        window.html_sensor_movey = html_sensor_movey;
        window.html_sensor_magno = html_sensor_magno;

        // ---- Auto-start sensors on page load ----
        window.onload = function() {
            window.soifgo.CallSub('sensor_light', true);
            window.soifgo.CallSub('sensor_move', true);
            window.soifgo.CallSub('sensor_magno', true);
        };
    </script>
</body>
</html>

🎤 4. Microphone — Permission Request

If your project needs microphone access — for example, to record audio, do speech-to-text conversion, or build a voice → Bluetooth pipeline — you must request permission the first time. After the user allows it once, Android remembers the choice and no dialog appears again.

✅ Golden rule: Call request_mic_perm once, before you start using the microphone. That is all you need.
📄 Request microphone permission
// Call this once, before starting speech recognition or audio recording
window.soifgo.CallSub('request_mic_perm', true, null);
⚠️ Notes:
  • SoifGo shows the native Android permission dialog the first time.
  • After the user taps Allow, the permission stays granted.
  • On later runs, SoifGo silently ignores the call — no dialog appears.
  • If you prefer not to call request_mic_perm in your code, the user can grant microphone permission manually in Android Settings → Apps → SoifGo → Permissions → Microphone.
📄 Example — voice command → Bluetooth
<button id="micBtn">🎤 Start</button>

<script>
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
recognition.lang = 'en-US';

document.getElementById('micBtn').addEventListener('click', () => {
    // 1. Ask for microphone permission (first time only)
    window.soifgo.CallSub('request_mic_perm', true, null);

    // 2. Start speech recognition
    recognition.start();
});

recognition.onresult = (event) => {
    const text = event.results[0][0].transcript;
    if (text.trim()) {
        // 3. Send recognized text to Bluetooth
        window.soifgo.CallSub('html_bluetooth_rx', true, text);
    }
};
</script>
💡 Tip: For the full multilingual voice tutorial (22 languages, continuous listening, language switcher), see the official page: Voice → Serial Bluetooth Tutorial .

🟣 5. MQTT — Connect, Publish & Receive

WebView ↔ Broker

SoifGo has a built-in MQTT client. Your HTML page can connect to any MQTT broker, publish messages, and receive them — all through simple JavaScript calls. No WebSocket, no CORS, no external library needed.

⚠️ IMPORTANT: MQTT only works inside SoifGo's WebView. If you open the same HTML in a normal browser, the bridge won't exist and nothing will happen (that's by design — the page won't crash either).
📄 MQTT API Reference
// ============================================================
// 1. CONNECT — step 1: address + client id
// ============================================================
// Native Sub: connecte_mqtt_ai(address, id)
//
//  address : "tcp://host:1883"  or  "ssl://host:8883"
//            Empty → defaults to "tcp://test.mosquitto.org:1883"
//  id      : any unique string. Empty → auto-generated "soifgo_XXXX"
//
window.soifgo.CallSub('connecte_mqtt_ai', true, address, id);

// ============================================================
// 2. CONNECT — step 2 (optional): username + password
// ============================================================
// Native Sub: connecte_mqtt_up(username, password)
//
// Only call this if the broker requires authentication.
// If both are empty, nothing happens.
//
window.soifgo.CallSub('connecte_mqtt_up', true, username, password);

// ============================================================
// 3. PUBLISH a message (also subscribes to the same topic!)
// ============================================================
// Native Sub: mqtt_publish(topic, message, qos)
//
//  topic   : topic string (e.g. "soifgo/led")
//  message : text payload
//  qos     : "0", "1", or "2"  (as a string)
//
// 💡 Publishing to a topic also subscribes you to it,
//    so you receive messages published by other clients.
//
window.soifgo.CallSub('mqtt_publish', true, topic, message, qos);

// ============================================================
// 4. DISCONNECT from the broker
// ============================================================
// Native Sub: mqtt_Disconnect  (no arguments)
//
window.soifgo.CallSub('mqtt_Disconnect', true);

// ============================================================
// 5. CHECK connection status
// ============================================================
// Native Sub: mqtt_status  (no arguments)
// SoifGo replies by calling html_mqtt_status('true'/'false')
//
window.soifgo.CallSub('mqtt_status', true);

// ============================================================
// RECEIVERS — functions SoifGo calls on your page
// ============================================================

// Incoming message on a subscribed topic
function mqtt_rx(topic, message) {
    // topic   : string
    // message : string
}
window.mqtt_rx = mqtt_rx;

// Connection status changed
// ⚠️ The value is sent as a STRING ('true' or 'false'),
//    NOT as a real boolean. Convert it before using it!
function html_mqtt_status(connected) {
    const isConnected = (connected === true || connected === 'true');
    // use isConnected (true/false)
}
window.html_mqtt_status = html_mqtt_status;
✅ How it works:
  1. Your page calls connecte_mqtt_ai(address, id) to start the connection.
  2. If the broker needs auth, call connecte_mqtt_up(username, password) right after.
  3. When a message arrives on a subscribed topic, SoifGo calls mqtt_rx(topic, message).
  4. Publishing with mqtt_publish also auto-subscribes you to that topic — so you receive messages from other clients on it.
  5. To query connection state, call mqtt_status — SoifGo answers with html_mqtt_status('true') or ('false').
  6. To stop, call mqtt_Disconnect.

🟣 MQTT Full Example

Complete working example — connect, publish, receive, and check status from a single page.

📄 html_mqtt_full.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MQTT Control</title>
    <style>
        body { font-family: sans-serif; background:#1e1e1e; color:#e0e0e0; padding:16px; }
        input, button { padding:10px; border-radius:6px; border:1px solid #444;
                        background:#2b2b2b; color:#fff; font-size:16px; margin:4px 0; width:100%; }
        button { background:#9b59b6; border:none; cursor:pointer; font-weight:600; }
        button:hover { background:#8e44ad; }
        button.on  { background:#27ae60; }
        button.off { background:#c0392b; }
        button.check { background:#f39c12; color:#10131a; }
        .status { padding:10px; border-radius:6px; margin:10px 0; font-weight:600; }
        .status.connected    { background:#1a2a1a; color:#4caf50; border:1px solid #4caf50; }
        .status.disconnected { background:#2a1a1a; color:#e74c3c; border:1px solid #e74c3c; }
        #log { background:#111; padding:12px; border-radius:8px; height:220px;
               overflow-y:auto; font-family:monospace; font-size:0.85rem; margin-top:12px; }
        #log div { margin:2px 0; }
        .row { display:flex; gap:8px; }
        .row button { flex:1; }
        label { font-size:0.8rem; color:#888; display:block; margin-top:6px; }
    </style>
</head>
<body>

    <h3>🟣 MQTT Control Panel</h3>

    <!-- Connection -->
    <label>Broker URL</label>
    <input type="text" id="broker" value="tcp://test.mosquitto.org:1883">

    <label>Client ID (empty = auto)</label>
    <input type="text" id="clientId" placeholder="myTest_001">

    <label>Username (optional)</label>
    <input type="text" id="username" placeholder="">

    <label>Password (optional)</label>
    <input type="password" id="password" placeholder="">

    <label>QoS (0, 1, 2)</label>
    <input type="text" id="qos" value="0">

    <div class="row">
        <button onclick="connectMQTT()">Connect</button>
        <button class="check" onclick="checkStatus()">Check</button>
        <button onclick="disconnectMQTT()">Disconnect</button>
    </div>

    <div id="status" class="status disconnected">Disconnected</div>

    <label>Topic</label>
    <input type="text" id="pubTopic" value="soifgo/demo/tx">

    <label>Message</label>
    <input type="text" id="pubMsg" value="ON">

    <div class="row">
        <button class="on"  onclick="publishMsg('ON')">Publish ON</button>
        <button class="off" onclick="publishMsg('OFF')">Publish OFF</button>
    </div>
    <div class="row">
        <button onclick="publishMsg()">Send Custom</button>
    </div>

    <div id="log"></div>

    <script>
        function log(msg) {
            const el = document.getElementById('log');
            const t  = new Date().toLocaleTimeString();
            el.innerHTML += `<div>[${t}] ${msg}</div>`;
            el.scrollTop = el.scrollHeight;
        }

        function setStatus(connected) {
            const el = document.getElementById('status');
            el.className = 'status ' + (connected ? 'connected' : 'disconnected');
            el.textContent = connected ? 'Connected' : 'Disconnected';
        }

        /* ---- Connect (two calls: address+id, then user+pass) ---- */
        function connectMQTT() {
            const broker = document.getElementById('broker').value.trim();
            const cid    = document.getElementById('clientId').value.trim();
            const user   = document.getElementById('username').value.trim();
            const pass   = document.getElementById('password').value;
            log('Connect → ' + broker + (cid ? ' as ' + cid : ' (auto ID)'));
            window.soifgo.CallSub('connecte_mqtt_ai', true, broker, cid);
            if (user || pass) {
                window.soifgo.CallSub('connecte_mqtt_up', true, user, pass);
            }
        }

        function disconnectMQTT() {
            log('Disconnect');
            window.soifgo.CallSub('mqtt_Disconnect', true);
            setStatus(false);
        }

        function checkStatus() {
            log('Check status');
            window.soifgo.CallSub('mqtt_status', true);
        }

        function publishMsg(preset) {
            const topic = document.getElementById('pubTopic').value.trim();
            const qos   = document.getElementById('qos').value.trim() || '0';
            const text  = (preset !== undefined) ? preset : document.getElementById('pubMsg').value;
            if (!topic) { alert('Enter a topic'); return; }
            log('TX → ' + topic + ' : ' + text + ' (QoS ' + qos + ')');
            window.soifgo.CallSub('mqtt_publish', true, topic, text, qos);
        }

        /* ---- Receivers (called by SoifGo) ---- */
        function mqtt_rx(topic, message) {
            log('RX ← ' + topic + ' : ' + message);
        }
        window.mqtt_rx = mqtt_rx;

        function html_mqtt_status(connected) {
            const isConnected = (connected === true || connected === 'true');
            setStatus(isConnected);
            log('Status: ' + (isConnected ? 'CONNECTED' : 'DISCONNECTED'));
        }
        window.html_mqtt_status = html_mqtt_status;
    </script>
</body>
</html>
✅ Key points:
  • Built-in MQTT — no external library, no WebSocket, no CORS.
  • Two-step connect — first connecte_mqtt_ai, then connecte_mqtt_up if auth needed.
  • Publish auto-subscribes — you receive messages on the topic you publish to.
  • Client ID auto-generated if left empty.
  • Status is a string — always convert with === 'true'.

🔗 MQTT ↔ Bluetooth Relay

Combine MQTT and Bluetooth on the same page. For example: forward incoming Bluetooth data to MQTT, or the other way around.

📄 html_mqtt_relay.html — Bluetooth ↔ MQTT relay
/* ---- Bluetooth → MQTT ---- */
function html_bluetooth_tx(data) {
    window.soifgo.CallSub('mqtt_publish', true, 'soifgo/relay/from_bt', data, '0');
}
window.html_bluetooth_tx = html_bluetooth_tx;

/* ---- MQTT → Bluetooth ---- */
function mqtt_rx(topic, message) {
    window.soifgo.CallSub('html_bluetooth_rx', true, message);
}
window.mqtt_rx = mqtt_rx;
💡 Use case: a robot with an HC-05 module can be controlled locally via Bluetooth and simultaneously report status to an MQTT dashboard — all from one HTML page.

🔧 MQTT Troubleshooting

Connection fails immediately
Check the broker URL. Use tcp:// for plain, ssl:// for TLS. Default ports: 1883 (plain), 8883 (TLS).
Messages not arriving
Publish at least once to the topic you want to listen to — publishing auto-subscribes you. Also verify the topic name is exactly the same (MQTT is case-sensitive).
Status always shows "Connected"
The status value arrives as a string ('true' / 'false'). In JavaScript the string 'false' is truthy — always compare with === 'true'.
Client ID conflict
If two clients use the same ID, the broker kicks one off. Leave the ID empty to get a unique auto-generated one.
Bridge not found
If window.soifgo is undefined, you're running in a normal browser — the bridge only exists inside SoifGo.

🟠 6. API — Send & Receive

WebView ↔ Server

SoifGo has a built-in API bridge. Your HTML page can send key/value data to a server and request data back — no fetch, no CORS, no external library.

⚠️ IMPORTANT: API only works inside SoifGo's WebView. In a normal browser the bridge doesn't exist — nothing will happen (and nothing will crash).
📄 API Reference
// ============================================================
// 1. SEND — POST a key/value pair to a server
// ============================================================
// Native Sub: api_send(address, key, value)
//
//  Sends:   POST 
// Body: {"":""} // Header: Content-Type: application/json // window.soifgo.CallSub('api_send', true, address, key, value); // ============================================================ // 2. RECEIVE — GET data from a server // ============================================================ // Native Sub: api_recive(address, key) // // Sends: GET
// Returns: the "" branch of the JSON response, // passed to api_rx() as a string // // ⚠️ The second parameter "key" is REQUIRED — it tells // SoifGo which branch of the JSON you want. // window.soifgo.CallSub('api_recive', true, address, key); // ============================================================ // 3. RECEIVER — SoifGo calls this with the response // ============================================================ // SoifGo calls: api_rx('') // // ⚠️ The response format depends on the API provider. // See the section below — "Two Response Formats". // function api_rx(number) { // number : string — either a plain value or a wrapped one } window.api_rx = api_rx;
✅ How it works:
  1. Send: api_send(address, key, value) → POST with JSON body.
  2. Receive: api_recive(address, key) → GET request. The key tells SoifGo which branch of the JSON to extract.
  3. Response: passed to api_rx(number) as a string.
  4. Format varies: see the section below.

📥 Two Response Formats — Read This Carefully

Different API providers return their data in different shapes. When SoifGo extracts the value at the given key, it can arrive at api_rx in two very different forms:

Format A — Plain value

The API returns a flat JSON where the key points directly to a primitive value.

Example API: DIA
Raw JSON:

{
  "Symbol": "BTC",
  "Price": 86846.51694166221
}

What api_rx receives:

86846.51694166221

Just the raw value. No braces, no key name.

Format B — Wrapped value

The API returns a nested JSON where the key points to another object.

Example API: CoinGecko
Raw JSON:

{
  "bitcoin": {
    "usd": 86750
  }
}

What api_rx receives:

{usd=86750}

The inner object, wrapped in B4A Map format.

💡 Why this matters: Your code must handle both cases. If you only handle Format A, CoinGecko will break your parser. If you only handle Format B, DIA will break your parser. Write a robust extractor that detects the format automatically.

🧪 Live Example — Bitcoin Price (DIA)

DIA is used here because it has no strict rate limit and returns a simple, flat JSON. This makes it perfect for testing.

⏱ Why does the code use a timer?
Price APIs return a new value every few seconds. A timer (setInterval) lets the page refresh the price automatically without the user tapping a button. But polling too fast will trigger rate limits and the API will start rejecting your requests. Choose a safe interval for the API you're using (see the Rate Limits section below).
📄 html_api_btc.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>BTC Price</title>
    <style>
        body { font-family: sans-serif; background:#1e1e1e; color:#e0e0e0;
               padding:20px; text-align:center; }
        h2 { color:#f7931a; }
        .price { font-size:2.5rem; font-weight:bold; color:#f7931a;
                 margin:20px 0; min-height:3rem; }
        .status { font-size:0.85rem; color:#888; }
        button { background:#f7931a; color:#fff; border:none;
                 padding:14px 30px; border-radius:8px; font-size:16px;
                 font-weight:600; cursor:pointer; margin:6px; }
        button.stop { background:#c0392b; }
        #raw { background:#111; padding:12px; border-radius:8px;
               font-family:monospace; font-size:0.75rem;
               text-align:left; margin-top:12px;
               word-break:break-all; max-height:120px; overflow-y:auto; }
    </style>
</head>
<body>
    <h2>₿ Bitcoin Price</h2>
    <div class="price" id="price">--</div>
    <div class="status" id="status">Stopped</div>

    <div style="margin-top:14px;">
        <button onclick="startBTC()">▶ Start</button>
        <button class="stop" onclick="stopBTC()">■ Stop</button>
    </div>

    <div id="raw"></div>

    <script>
        // DIA endpoint — flat JSON, no rate limit issues
        const URL = 'https://api.diadata.org/v1/assetQuotation/Bitcoin/0x0000000000000000000000000000000000000000';
        const KEY = 'Price';
        const INTERVAL = 5000;   // 5 seconds — safe for DIA

        let timer = null;

        function startBTC() {
            stopBTC();
            fetchBTC();
            timer = setInterval(fetchBTC, INTERVAL);
            document.getElementById('status').textContent = 'Live · every ' + (INTERVAL/1000) + 's';
        }

        function stopBTC() {
            if (timer) { clearInterval(timer); timer = null; }
            document.getElementById('status').textContent = 'Stopped';
        }

        function fetchBTC() {
            window.soifgo.CallSub('api_recive', true, URL, KEY);
        }

        /* ============================================================
           Robust extractor — handles BOTH response formats
           ============================================================ */
        function extractNumber(raw) {
            const s = String(raw).trim();

            // Format A — plain number: "86846.51694166221"
            if (/^-?\d+(\.\d+)?$/.test(s)) {
                return parseFloat(s);
            }

            // Format B — B4A Map: "{usd=86750}"
            const mapMatch = s.match(/[a-zA-Z_]\w*[=:]\s*(-?[\d.]+)/);
            if (mapMatch) {
                return parseFloat(mapMatch[1]);
            }

            // Fallback — any number in the string
            const anyNum = s.match(/-?\d+(\.\d+)?/);
            return anyNum ? parseFloat(anyNum[0]) : null;
        }

        function api_rx(number) {
            document.getElementById('raw').textContent = String(number);

            const price = extractNumber(number);
            if (price !== null && !isNaN(price)) {
                document.getElementById('price').textContent =
                    '$' + price.toLocaleString('en-US', {
                        minimumFractionDigits: 2,
                        maximumFractionDigits: 2
                    });
            } else {
                document.getElementById('price').textContent = '❌ Parse error';
            }
        }
        window.api_rx = api_rx;
    </script>
</body>
</html>
✅ Key points:
  • Timer is optional — you can call fetchBTC() manually without setInterval.
  • Interval is 5 s — safe for DIA. If using CoinGecko, use ≥ 10 s.
  • Stop button — always give the user a way to stop polling.
  • Robust extractor — handles both plain numbers and Map-wrapped values.

💡 Parsing the Response — The Robust Helper

Different providers return different shapes. Use this single helper to handle both cases — plain numbers, Map-wrapped values, and JSON.

function extractValue(raw, fieldName) {
    const s = String(raw).trim();

    // 1. Plain number:  "86846.51694166221"
    if (/^-?\d+(\.\d+)?$/.test(s)) return s;

    // 2. B4A Map format:  {usd=86750}  or  {Price=86846.51}
    const mapMatch = s.match(
        new RegExp(fieldName + '[=:]\\s*([^,}\\s]+)', 'i')
    );
    if (mapMatch) return mapMatch[1];

    // 3. Valid JSON:  {"usd":86750}
    try {
        const obj = JSON.parse(s);
        if (obj[fieldName] !== undefined) return obj[fieldName];
    } catch (e) {}

    // 4. Fallback: any number
    const numMatch = s.match(/-?\d+(\.\d+)?/);
    return numMatch ? numMatch[0] : null;
}

// Usage in api_rx:
function api_rx(number) {
    const price = extractValue(number, 'Price');   // or 'usd'
    if (price) {
        document.getElementById('out').textContent = '$' + price;
    }
}
window.api_rx = api_rx;
💡 Tip: Put this helper in a shared JS file and reuse it in every page that talks to an API. It will save you a lot of debugging.

⚠️ Rate Limits

Don't hammer the API. Every provider has a limit. If your timer is too fast, the API will start returning error 429:
{"status":{"error_code":429,"error_message":"You've exceeded the Rate Limit"}}
✅ Safe intervals:
  • DIA: ~ 5 seconds
  • CoinGecko: ≥ 10 seconds
  • Binance: ≥ 1 second (1200 req/min)
  • Coinbase: ≥ 5 seconds
  • Blockchain.info: ≥ 10 seconds
💡 Best practice: Always include a Stop button so the user can halt polling. Never leave a timer running in the background without a way to stop it.

📌 Classic Method vs WebView Method

SoifGo supports two ways of talking to an API. The Classic method works entirely inside SoifGo's native UI; the WebView method uses the HTML bridge you've been reading about.

Classic Method

  • Configured entirely in SoifGo's button settings.
  • No HTML, no JavaScript.
  • You set the address, key, word start, and word end in the UI.
  • SoifGo extracts the value itself and displays it on the button.
  • Perfect for simple “show one value” cases.

WebView Method

  • Configured in HTML/JS inside the page.
  • Full control over parsing, display, and behaviour.
  • You call api_recive(address, key) and handle the response in api_rx.
  • Can combine API + Bluetooth + MQTT + sensors on one page.
  • Perfect for dashboards, custom UIs, and complex logic.
💡 Which one should I use? Use Classic if you just need a single value on a button. Use WebView if you want custom layout, multiple values, timers, charts, or any interactivity.

🔧 API Troubleshooting

Nothing happens when api_recive is called
Make sure you're passing both parameters: api_recive(address, key). Missing the key will silently fail.
api_rx never fires
Check that api_rx is registered on window: window.api_rx = api_rx;
JSON.parse fails on the response
SoifGo may send the response as a B4A Map string ({usd=86750}) or as a plain value (86846.51) — neither is valid JSON. Use the extractValue() helper above.
Response is just a number, not a Map
Some APIs return a flat JSON. In that case api_rx receives the raw value (86846.51) with no wrapper. Handle this case with the plain-number branch of the extractor.
Rate limit errors (429)
Increase the timer interval. See the Rate Limits section above.
Bridge not found
If window.soifgo is undefined, you're in a normal browser. The bridge only exists inside SoifGo.

💾 7. Save & Reload Data (Persistent Storage)

✅ Good news: alert(), confirm(), and prompt() all work natively in SoifGo's WebView. You can use standard browser dialogs in your code.
💡 Tip: If you want a fully themed look that matches your UI, build a custom HTML modal.

Use localStorage to save and reload data. It persists across page reloads and WebView restarts, so your users never lose their settings.

📄 html_storage.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Save & Reload</title>
    <style>
        body { font-family: sans-serif; background:#1e1e1e; color:#e0e0e0; padding:16px; }
        input, button { padding:10px; border-radius:6px; border:1px solid #444;
                        background:#2b2b2b; color:#fff; font-size:16px; margin:4px 0; }
        button { background:#3498db; border:none; cursor:pointer; }
        button.danger { background:#c0392b; }
        .row { display:flex; gap:8px; align-items:center; margin:4px 0; }
        .row button { flex:0 0 auto; }
        #output { margin-top:16px; }
        .item { padding:8px 12px; background:#2b2b2b; border-radius:6px;
                margin:4px 0; display:flex; justify-content:space-between; }
        .item .time { color:#888; font-size:0.85rem; }
    </style>
</head>
<body>

    <h3>💾 Save & Reload Demo</h3>

    <div class="row">
        <input type="text" id="nameInput" placeholder="Enter name...">
        <button onclick="saveItem()">Save</button>
        <button onclick="loadItems()">Reload</button>
        <button class="danger" onclick="clearAll()">Clear</button>
    </div>

    <div id="output"></div>

    <script>
        const KEY = 'my_data';

        /* ---- Save ---- */
        function saveItem() {
            const name = document.getElementById('nameInput').value.trim();
            if (!name) {
                alert('Please enter a name.');
                return;
            }
            let list = load();
            list.push({ name: name, time: Date.now() });
            persist(list);
            render(list);
            document.getElementById('nameInput').value = '';
        }

        /* ---- Delete one item ---- */
        function removeItem(index) {
            const list = load();
            list.splice(index, 1);
            persist(list);
            render(list);
        }

        /* ---- Clear all ---- */
        function clearAll() {
            if (!confirm('Delete all saved items?')) return;
            localStorage.removeItem(KEY);
            render([]);
        }

        /* ---- Storage helpers ---- */
        function load() {
            try {
                return JSON.parse(localStorage.getItem(KEY)) || [];
            } catch (e) {
                console.error('Load failed:', e);
                return [];
            }
        }

        function persist(list) {
            try {
                localStorage.setItem(KEY, JSON.stringify(list));
            } catch (e) {
                console.error('Save failed (quota?):', e);
                alert('Could not save. Storage may be full.');
            }
        }

        /* ---- Render ---- */
        function render(list) {
            const out = document.getElementById('output');
            if (!list.length) {
                out.innerHTML = '<p style="color:#888;">No items saved yet.</p>';
                return;
            }
            out.innerHTML = list.map((x, i) => `
                <div class="item">
                    <span>${i + 1}. ${escapeHtml(x.name)}</span>
                    <span>
                        <span class="time">${new Date(x.time).toLocaleTimeString()}</span>
                        <button class="danger" onclick="removeItem(${i})">×</button>
                    </span>
                </div>`).join('');
        }

        /* ---- XSS guard ---- */
        function escapeHtml(s) {
            return String(s)
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;');
        }

        /* ---- Auto-load on page open ---- */
        window.onload = function() {
            render(load());
        };
    </script>
</body>
</html>
✅ Key points:
  • alert(), confirm(), prompt() work — no custom modal needed.
  • Wrap localStorage calls in try/catch to handle quota errors gracefully.
  • Call render(load()) inside window.onload so data reloads every time the page opens.
  • Use a unique key like 'my_data' per page to avoid clashes with other pages.
  • Always escape user input before injecting it into innerHTML (XSS guard).

⬅ Back to Home