Bar
SpaceWire UK
Specialist providers of VHDL Intellectual Property & Design Services
BarBarBarBar
Tutorial
Missing Image!
Part 32 - Update System Information

Introduction

This tutorial details the steps required to allow the Sytem tab to use live data. Previously the System, CPU, Memory & Network information was provided via PHP on the server-side and only once on a page refresh. Now the client-side will update the information peridocally using Javascript (all the information other than just the uptime). The information will be provided by expanding the server-side uptime CGI.

The following additional live information will by provided :- The tutorial will make use of CGI (Bash), CSS, HTML, Javascript & PHP.

Note: The method of gathering statistics may need to be improved at a later date (not the easiest thing to get 100% correct out of the trap!).

Aims

The aims of this tutorial are as follows :-

    Part 1 - Setup environment

    1. Pre-requisites

    Part 2 - Update the Webserver files

    1. Create a new gather statistics CGI script
    2. Update System Webpage
    3. Update System Javascript
    4. Create a new Cascading Style sheet

    Part 3 - Check everything is working as expected (beta)

    1. Patch Zedboard PetaLinux
    2. Check new & updated System webpage elements

    Part 4 - Make the changes permanent

    1. Bump version
    2. Remove superfluous CGI script
    3. Update Website BitBake recipe

    Part 5 - Network time protocol & Timezone

    1. Clean up and correct the network time items

    Part 6 - Revision Control

    1. Commit new & updated files
    2. Create production release

    Part 7 - Hardware Deployment

    1. Copy BOOT files to the SD-Card
    2. Reload the Operating System using the Webserver

    Part 8 - Check everything is working as expected

    1. Final checks

    Part 9 - Final resulting build file(s)

    1. Resulting BOOT files
    #### Part 1 - Setup environment ####

    1. Pre-requisites

    Starting from this point is possible but requires a few pre-requisites.
    #### Part 2 - Update the Webserver files ####

    2. Create a new gather statistics CGI script

    Create a new CGI script to collect the following System information from PetaLinux :-
    1. Hostname
    2. Date
    3. Uptime
    4. Sequence number (request number from the Javascript query)
    5. CPU model
    6. CPU cores
    7. CPU load (1, 5 & 15 minute averages)
    8. CPU idle time
    9. CPU busy time
    10. Memory total
    11. Memory used
    12. Memory free
    13. Memory buffered/cached
    14. MAC address
    15. Internal IP address
    16. External IP address
    17. Time since 1970 in seconds
    18. Network Rx bytes transferred since power-on
    19. Network Tx bytes transferred since power-on
    20. rootfs disk total
    21. rootfs disk used
    22. rootfs disk free
    23. mmcblk0p1 disk total
    24. mmcblk0p1 disk used
    25. mmcblk0p1 disk free
    steve@Desktop:~/swuk_tutorial$ subl zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/get_stats.cgi

    get_stats.cgi

    #!/bin/sh
    
    #
    # File .......... get_stats.cgi
    # Author ........ Steve Haywood
    # Website ....... http://www.spacewire.co.uk
    # Project ....... Zedboard Linux (SpaceWire UK Tutorial)
    # Date .......... 25 Mar 2026
    # Version ....... 1.0
    # Description ...
    #   Simple CGI to gather & provide system information.
    #
    
    # Get Network interface name
    interface=$(ip route show default | awk '/default/ {print $5}')
    
    # Output Header
    printf "Content-type: text/html\n\n"
    
    # 0:Get System hostname
    hostname
    
    # 1:Get System time
    date
    
    # 2:Get System uptime
    awk '{print $1}' /proc/uptime
    
    # 3:Get System sequence number
    echo ${@}
    
    # 4:Get CPU model
    grep model /proc/cpuinfo | cut -d : -f2 | tail -1 | sed 's/\s//'
    
    # 5:Get CPU cores
    grep -c ^processor /proc/cpuinfo
    
    # 6:Get CPU load
    awk '{print $1" "$2" "$3}' /proc/loadavg
    
    # 7-8:Get CPU usage (idle, total)
    cpu=($(sed -n 's/^cpu\s//p' /proc/stat))
    echo ${cpu[3]}
    total=0
    for value in ${cpu[@]}; do
      total=$((total+value))
    done
    echo $total
    
    # 9-12:Get Memory usage (total, used, free, buffer/cache)
    free | awk 'NR==2{printf "%u\n%u\n%u\n%u\n", $2, $3, $4, $6}'
    
    # 13:Get Network MAC
    cat /sys/class/net/$interface/address
    
    # 14:Get Network internal IP
    ip a | grep inet | grep -vw lo | grep -v inet6 | cut -d \/ -f1 | sed 's/[^0-9\.]*//g'
    
    # 15:Get Network external IP
    echo $(wget -q -O- http://ipecho.net/plain)
    
    # 16:Get (Network) time since 1970 in seconds
    date +%s
    
    # 17-18:Get Network rx/tx bytes transferred since power-on
    cat /sys/class/net/$interface/statistics/rx_bytes
    cat /sys/class/net/$interface/statistics/tx_bytes
    
    # 19-21:Get Disks (rootfs) usage (total, used, free)
    # 22-24:Get Disks (mmcblk0p1) usage (total, used, free)
    df | awk '/^rootfs|^\/dev\/mmcblk0p1 /{printf "%u\n%u\n%u\n", $2, $3, $4}'
    
    Direct download available here :-
    steve@Desktop:~/swuk_tutorial$ wget https://spacewire.co.uk/tutorial/shared/repos/0038/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/get_stats.cgi -O zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/get_stats.cgi
    Check out the changes.
    steve@Desktop:~/swuk_tutorial$ git difftool zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/get_stats.cgi
    Make the script executable.
    steve@Desktop:~/swuk_tutorial$ chmod +x zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/get_stats.cgi

    3. Update System Webpage

    Make the following changes to the System webpage :-
    steve@Desktop:~/swuk_tutorial$ subl zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/index.php

    index.php

    <?php
    //
    // File .......... index.php
    // Author ........ Steve Haywood
    // Website ....... http://www.spacewire.co.uk
    // Project ....... Zedboard Linux (SpaceWire UK Tutorial)
    // Date .......... 25 Mar 2026
    // Version ....... 13.0
    // History .......
    //   9.0 zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/index.php
    //   7.0 zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/index.cgi
    // Description ...
    //   Webpage for System Information & Operating System / Firmware Load.
    //
    ?>
    
    <?php require '../share/header.php'; ?>
    
    <div class=content>
    
    <div class=inner>
    
    <h3 style="padding-bottom:8px">Product Identification</h3>
    
    <?php
    $tables = array("Operating System", "Firmware");
    $id = array("os", "fw");
    $fields = array("Description", "Company", "Author", "Version", "Timestamp", "Hash");
    for ($table = 0 ; $table < count($tables) ; $table++) {
    echo "
    <table>
    <tr>
    <th colspan=2>$tables[$table]</th><th><button id=$id[$table]_read>Read</button></th>
    </tr>
    ";
    for ($field = 0 ; $field < count($fields) ; $field++) {
    echo "
    <tr>
    <td style='text-align:right'>$fields[$field]</td>
    <td style='text-align:left' id=$id[$table]_id_$field>Unknown</td>
    <td><img id=$id[$table]_id_status_$field style='vertical-align:middle' src='../share/amber.gif' title='Unknown!' alt='Missing Image!'></td>
    </tr>
    ";
    }
    echo "
    </table>
    ";
    }
    ?>
    </div>
    
    <div class=inner>
    
    <h3 style="padding-bottom:8px">Component Reload (PS &amp; PL)</h3>
    
    <?php
    for ($table = 0 ; $table < count($tables) ; $table++) {
    $colspan = ($table == 0) ? 6 : 5;
    echo "
    <table>
    <tr>
    <th colspan=$colspan>$tables[$table]</th>
    </tr>
    <tr>
    <td style='text-align:right'>Select</td>
    <td style='text-align:left'>
    <select id=$id[$table]_select>
    <option value=none>N/A</option>
    ";
    $location = ($table == 0) ? '/media/sd-mmcblk0p1/petalinux/*' : '/media/sd-mmcblk0p1/firmware/*.bin';
    foreach(glob("$location") as $file) {
      if (($table == 0) ? is_dir($file) : is_file($file)) {
        $basename = pathinfo($file)["basename"];
        $filename = ($table == 0) ? $basename : pathinfo($file)["filename"];
        echo "<option value=$basename>$filename</option>";
      }
    }
    $count = ($table == 0) ? "<td id=$id[$table]_count>0/120 s</td>" : "";
    echo "
    </select>
    <td id=$id[$table]_action>Idle</td>
    </td><td><progress id=$id[$table]_progress value=0 max=120></progress></td>
    $count
    <td><img id=$id[$table]_status style='vertical-align:middle' src='../share/amber.gif' title='Unknown!' alt='Missing Image!'></td>
    </tr>
    </table>
    ";
    }
    ?>
    </div>
    
    <div class=inner>
    
    <h3 style="padding-bottom:8px">Operating System Information</h3>
    
    <table>
    <tr><td style='border:0'>
    
    <table>
    <tr>
    <th colspan=4>System</th>
    </tr>
    <tr>
    <td style='text-align:right'>Hostname</td>
    <td colspan=3 id=st_sys_hostname style='text-align:left'>-</td>
    </tr>
    <tr>
    <td style='text-align:right'>Time</td>
    <td colspan=3 id=st_sys_time style='text-align:left'>-</td>
    </tr>
    <tr>
    <td style='text-align:right'>Uptime</td>
    <td colspan=3 id=st_sys_uptime style='text-align:left'>-</td>
    </tr>
    <tr>
    <td style='text-align:right'>Sequence</td>
    <td style='text-align:left'>Sent: <span id=st_sys_seq_sent>0</span></td>
    <td style='text-align:left'>Received: <span id=st_sys_seq_rcvd>0</span></td>
    <td style='text-align:left'>Time: <span id=st_sys_seq_rtt>0</span></td>
    </tr>
    </table>
    
    
    <table>
    <tr><th colspan=5>CPU</th></tr>
    <tr>
    <td style='text-align:right'>Model</td>
    <td colspan=3 id=st_cpu_model style='text-align:left'>-</td>
    <td rowspan=4 style='padding:0'><div class=chart id=st_cpu_chart></div></td>
    </tr>
    <tr>
    <td style='text-align:right'>Cores</td>
    <td colspan=3 id=st_cpu_cores style='text-align:left'>-</td>
    </tr>
    <tr>
    <td style='text-align:right'>Load</td>
    <td style='text-align:left'>1m: <span id=st_cpu_load_1m>-</span></td>
    <td style='text-align:left'>5m: <span id=st_cpu_load_5m>-</span></td>
    <td style='text-align:left'>15m: <span id=st_cpu_load_15m>-</span></td>
    </tr>
    <tr>
    <td style='text-align:right'>Usage</td>
    <td colspan=3 style='text-align:left'><span id=st_cpu_util>-</span> %</td>
    </tr>
    </table>
    
    
    
    <table>
    <tr><th colspan=4>Memory</th></tr>
    <tr>
    <td style='text-align:right'>Total</td>
    <td><progress id=st_mem_bar_total value=0 max=100></progress></td>
    <td style='text-align:right'><span id=st_mem_pct_total>-</span> %</td>
    <td id=st_mem_txt_total style='text-align:right'>-</td>
    </tr>
    <tr>
    <td style='text-align:right'>Used</td>
    <td><progress id=st_mem_bar_used value=0 max=100></progress></td>
    <td style='text-align:right'><span id=st_mem_pct_used>-</span> %</td>
    <td id=st_mem_txt_used style='text-align:right'>-</td>
    </tr>
    <tr>
    <td style='text-align:right'>Free</td>
    <td><progress id=st_mem_bar_free value=0 max=100></progress></td>
    <td style='text-align:right'><span id=st_mem_pct_free>-</span> %</td>
    <td id=st_mem_txt_free style='text-align:right'>-</td>
    </tr>
    <tr>
    <td style='text-align:right'>Buffer/Cache</td>
    <td><progress id=st_mem_bar_buffer value=0 max=100></progress></td>
    <td style='text-align:right'><span id=st_mem_pct_buffer>-</span> %</td>
    <td id=st_mem_txt_buffer style='text-align:right'>-</td>
    </tr>
    </table>
    
    
    <table>
    <tr>
    <th colspan=4>Network</th>
    </tr>
    <tr>
    <td style='text-align:right'>MAC Address</td>
    <td colspan=2 id=st_net_mac style='text-align:left'>-</td>
    <td rowspan=2 style='padding:0'><div style='height:50px' class=chart id=st_rx_chart></div></td>
    </tr>
    <tr>
    <td style='text-align:right'>Internal IP</td>
    <td colspan=2 id=st_net_int_ip style='text-align:left'>-</td>
    </tr>
    <tr>
    <td style='text-align:right'>External IP</td>
    <td colspan=2 id=st_net_ext_ip style='text-align:left'>-</td>
    <td rowspan=2 style='padding:0'><div style='height:50px' class=chart id=st_tx_chart></div></td>
    </tr>
    <tr>
    <td style='text-align:right'>Speed</td>
    <td style='text-align:left'>Rx: <span id=st_net_rx_bytes>-</span></td>
    <td style='text-align:left'>Tx: <span id=st_net_tx_bytes>-</span></td>
    </tr>
    </table>
    
    <?php
    $tables = array("rootfs", "mmcblk0p1");
    $id = array("rootfs", "sdcard");
    $fields = array("Total", "Used", "Free");
    $fieldid = array("total", "used", "free");
    for ($table = 0 ; $table < count($tables) ; $table++) {
    echo "
    <table>
    <tr>
    <th colspan=4>Disks ($tables[$table])</th>
    </tr>
    ";
    for ($field = 0 ; $field < count($fields) ; $field++) {
    echo "
    <tr>
    <td style='text-align:right'>$fields[$field]</td>
    <td><progress id=st_$id[$table]_bar_$fieldid[$field] value=0 max=100></progress></td>
    <td style='text-align:right'><span id=st_$id[$table]_pct_$fieldid[$field]>-</span> %</td>
    <td id=st_$id[$table]_txt_$fieldid[$field] style='text-align:right'>-</td>
    </tr>
    ";
    }
    echo "
    <tr><td colspan=4></td></tr>
    </table>
    ";
    }
    ?>
    
    </td></tr>
    
    <tr><td style='border:0'>
    
    <table>
    <tr><th colspan=4>Update OS Information</th></tr>
    <tr>
    <td><button id=st_ref_once>Once</button></td>
    <td>Automatic (ms): <input id=st_ref_number type=number min=0 max=5000 value=1000 required><span class=validity></span></td>
    <td style='vertical-align:middle'>
    <input id=st_ref_range type=range min=0 max=5000 value=1000>
    </td>
    <td style='vertical-align:middle'><input id=st_ref_limit type=checkbox checked> Limit</td>
    </tr>
    </table>
    
    </td></tr>
    </table>
    
    </div>
    
    </div>
    
    
    <?php require '../share/footer.php'; ?>
    
    Direct download available here :-
    steve@Desktop:~/swuk_tutorial$ wget https://spacewire.co.uk/tutorial/shared/repos/0038/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/index.php.txt -O zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/index.php
    Check out the changes.
    steve@Desktop:~/swuk_tutorial$ git difftool zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/index.php

    4. Update System Javascript

    Make the following changes to the System webpage :-
    steve@Desktop:~/swuk_tutorial$ subl zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/script.js

    script.js

    //
    // File .......... script.js
    // Author ........ Steve Haywood
    // Website ....... http://www.spacewire.co.uk
    // Project ....... Zedboard Linux (SpaceWire UK Tutorial)
    // Date .......... 25 Mar 2026
    // Version ....... 10.0
    // History .......
    //   7.0 zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/uptime.js
    // Description ...
    //   Javascript for System Information & Operating System / Firmware Load.
    //
    
    /////////////
    // General //
    /////////////
    
    // Page load events
    window.addEventListener("load", (event) => {
      os_read_ids();
      fw_read_ids();
      st_kickoff();
    });
    
    // Get ID web elements
    function getIDs(name, count) {
      const elements = [];
      for (let i = 0; i < count; i++) {
        elements.push({
          txt: document.getElementById(`${name}_id_${i}`),
          img: document.getElementById(`${name}_id_status_${i}`),
        });
      }
      return elements;
    }
    
    ///////////////////////////////////////////////
    // Product Identification - Operating System //
    ///////////////////////////////////////////////
    
    // Web elements
    const os_ids  = getIDs("os", 6);
    const os_read = document.getElementById("os_read");
    // Web events
    os_read.addEventListener("click",  function() { os_read_ids(); });
    
    // Download OS information file & display result
    async function os_read_ids() {
      const response = await fetch("/project.txt?" + Date.now());
      if (response.status == 200) {
        const ids = await response.text();
        const fields = ids.split(/\r?\n/);
        for (let i = 0; i < 6; i++) {
          if (i < fields.length && fields[i] != "") {
            os_ids[i].img.src = "../share/green.gif?" + Date.now();
            os_ids[i].img.title = "Last file fetch successful";
            os_ids[i].txt.innerHTML = fields[i];
          } else {
            os_ids[i].img.src = "../share/red.gif?" + Date.now();
            os_ids[i].img.title = "Missing field information";
            os_ids[i].txt.innerHTML = "Unknown";
          }
        }
      } else {
        for (let i = 0; i < 6; i++) {
          os_ids[i].img.src = "../share/red.gif?" + Date.now();
          os_ids[i].img.title = "Last file fetch failed";
          os_ids[i].txt.innerHTML = "Unknown";
        }
      }
    }
    
    ///////////////////////////////////////
    // Product Identification - Firmware //
    ///////////////////////////////////////
    
    // Web elements
    const fw_ids  = getIDs("fw", 6);
    const fw_read = document.getElementById("fw_read");
    // Web events
    fw_read.addEventListener("click",  function() { fw_read_ids(); });
    
    // Peek all strings
    function fw_read_ids() {
      fw_read_id(c_id_description, 0);
      fw_read_id(c_id_company, 1);
      fw_read_id(c_id_author, 2);
      fw_read_id(c_id_version, 3);
      fw_read_id(c_id_timestamp, 4);
      fw_read_id(c_id_hash, 5);
    }
    
    // Peek string & display result
    function fw_read_id(offset, index) {
      const url = "/cgi-bin/peekstring?" + c_axi_identification + "&" + 4096 + "&" + offset + "&" + 128;
      const ajaxReq = new XMLHttpRequest();
      ajaxReq.open("GET", url, true);
      ajaxReq.onload = () => {
        if (ajaxReq.readyState === XMLHttpRequest.DONE && ajaxReq.status === 200) {
          const respText = ajaxReq.responseText;
          if (respText.substr(0,6) == "Error:") {
            fw_ids[index].img.src = "../share/red.gif?" + Date.now();;
            fw_ids[index].img.title = "Last peekstring failed : " + respText.substr(7);
          } else {
            fw_ids[index].img.src = "../share/green.gif?" + Date.now();;
            fw_ids[index].img.title = "Last peekstring successful";
            fw_ids[index].txt.innerHTML = respText;
          }
        }
      }
      ajaxReq.send(null);
    }
    
    //////////////////////////////////////////////
    // Component Reload - Operating System (PS) //
    //////////////////////////////////////////////
    
    // Web elements
    const os_select   = document.getElementById("os_select");
    const os_action   = document.getElementById("os_action");
    const os_progress = document.getElementById("os_progress");
    const os_count    = document.getElementById("os_count");
    const os_status   = document.getElementById("os_status");
    // Web events
    os_select.addEventListener("change", function() { os_reload(); });
    
    // Globals
    var os_timer;    // OS Timer
    var os_ping;     // Ping flag
    var os_terminal; // Terminal count
    var os_counter;  // Progress counter
    
    // A very crude ping
    function ping() {
      const xhr = new XMLHttpRequest();
      xhr.timeout = 500;
      xhr.open("GET", "", true);
      xhr.onreadystatechange = function() {
        if (xhr.readyState === XMLHttpRequest.DONE && xhr.status == 200) {
          os_ping = 1;
        }
      }
      xhr.send();
    }
    
    // Handle sequence of events for OS reload
    function os_ticker() {
      os_progress.value++;
      os_counter++;
      os_count.innerHTML = os_counter + "/" + os_progress.max + " s";
      if (os_counter == 30) {
        os_action.innerHTML = "Ping";
      } else if (os_counter == os_progress.max) {
        clearInterval(os_timer);
        os_action.innerHTML = "Done";
        os_status.src = "../share/red.gif?" + Date.now();
        os_status.title = "Reload OS timed out, check connection!";
        os_select.disabled = false;
        fw_select.disabled = false;
      } else if (os_counter == os_terminal - 1) {
        os_action.innerHTML = "Reload";
      } else if (os_counter == os_terminal) {
        clearInterval(os_timer);
        window.location.reload(true);
      } else if (os_terminal == -1 && os_counter > 30) {
        if (os_ping == 1) {
          os_action.innerHTML = "Done";
          os_progress.value = os_progress.max;
          os_status.src = "../share/green.gif?" + Date.now();;
          os_status.title = "Reload OS successful";
          os_terminal = os_counter + 2;
        } else {
          ping();
        }
      }
    }
    
    // Kick off OS reload
    function os_reload(obj) {
      if (os_select.value != "none") {
        os_select.disabled = true;
        fw_select.disabled = true;
        st_ref_once.disabled = true;
        st_ref_number.disabled = true;
        st_ref_range.disabled = true;
        st_ref_limit.disabled = true;
        st_align(0);
        st_kickoff();
        os_action.innerHTML = "Install";
        os_progress.value = 0;
        os_count.innerHTML = "0/" + os_progress.max + " s";
        os_status.src = "../share/amber.gif?" + Date.now();
        os_status.title = "Unknown!";
        os_ping = 0;
        os_terminal = -1;
        os_counter = 0;
        os_timer = setInterval("os_ticker()", 1000);
        const url = "/cgi-bin/reload_os.cgi?" + os_select.value;
        const ajaxReq = new XMLHttpRequest();
        ajaxReq.open("GET", url, true);
        ajaxReq.onload = () => {
          if (ajaxReq.readyState === XMLHttpRequest.DONE && ajaxReq.status === 200) {
            const respText = ajaxReq.responseText;
            if (respText == "OK") {
              os_action.innerHTML = "Reboot";
            } else {
              clearInterval(os_timer);
              os_action.innerHTML = "Done";
              os_status.src = "../share/red.gif?" + Date.now();
              os_status.title = respText;
              os_select.disabled = false;
              fw_select.disabled = false;
              st_ref_once.disabled = false;
              st_ref_number.disabled = false;
              st_ref_range.disabled = false;
              st_ref_limit.disabled = false;
            }
          }
        }
        ajaxReq.send(null);
      }
    }
    
    //////////////////////////////////////
    // Component Reload - Firmware (PL) //
    //////////////////////////////////////
    
    // Web elements
    const fw_select   = document.getElementById("fw_select");
    const fw_action   = document.getElementById("fw_action");
    const fw_progress = document.getElementById("fw_progress");
    const fw_status   = document.getElementById("fw_status");
    // Web events
    fw_select.addEventListener("change", function() { fw_reload(); });
    
    // Load PL Firmware
    function fw_reload() {
      if (fw_select.value != "none") {
        fw_select.disabled = true;
        fw_action.innerHTML = "Write";
        fw_progress.value = 0;
        fw_status.src = "../share/amber.gif?" + Date.now();
        fw_status.title = "Unknown!";
        const url = "/cgi-bin/loadfirmware?" + fw_select.value;
        const ajaxReq = new XMLHttpRequest();
        ajaxReq.open("GET", url, true);
        ajaxReq.onload = () => {
          if (ajaxReq.readyState === XMLHttpRequest.DONE && ajaxReq.status === 200) {
            var respText = ajaxReq.responseText;
            if (respText.substr(0,6) == "Error:") {
              fw_status.src = "../share/red.gif?" + Date.now();
              fw_status.title = "Last loadfirmware failed : " + respText.substr(7);
            } else {
              fw_status.src = "../share/green.gif?" + Date.now();
              fw_status.title = "Last loadfirmware successful";
            }
            fw_action.innerHTML = "Done";
            fw_progress.value = fw_progress.max;
            fw_select.disabled = false;
            fw_read_ids();
          }
        }
        ajaxReq.send(null);
      }
    }
    
    //////////////////////////////////
    // Operating System Information //
    //////////////////////////////////
    
    // Return item index from get_stats.cgi
    const cgi_sys_hostname = 0;
    const cgi_sys_time     = 1;
    const cgi_sys_uptime   = 2;
    const cgi_sys_seq      = 3;
    const cgi_cpu_model    = 4;
    const cgi_cpu_cores    = 5;
    const cgi_cpu_load     = 6;
    const cgi_cpu_idle     = 7;
    const cgi_cpu_total    = 8;
    const cgi_mem_total    = 9;
    const cgi_mem_used     = 10;
    const cgi_mem_free     = 11;
    const cgi_mem_buffer   = 12;
    const cgi_net_mac      = 13;
    const cgi_net_int_ip   = 14;
    const cgi_net_ext_ip   = 15;
    const cgi_net_time     = 16;
    const cgi_net_rx_bytes = 17;
    const cgi_net_tx_bytes = 18;
    const cgi_rootfs_total = 19;
    const cgi_rootfs_used  = 20;
    const cgi_rootfs_free  = 21;
    const cgi_sdcard_total = 22;
    const cgi_sdcard_used  = 23;
    const cgi_sdcard_free  = 24;
    
    // Web elements (statistics)
    const st_sys_hostname = document.getElementById("st_sys_hostname");
    const st_sys_time     = document.getElementById("st_sys_time");
    const st_sys_uptime   = document.getElementById("st_sys_uptime");
    const st_sys_seq_sent = document.getElementById("st_sys_seq_sent");
    const st_sys_seq_rcvd = document.getElementById("st_sys_seq_rcvd");
    const st_sys_seq_rtt  = document.getElementById("st_sys_seq_rtt");
    
    const st_cpu_model    = document.getElementById("st_cpu_model");
    const st_cpu_cores    = document.getElementById("st_cpu_cores");
    const st_cpu_load_1m  = document.getElementById("st_cpu_load_1m");
    const st_cpu_load_5m  = document.getElementById("st_cpu_load_5m");
    const st_cpu_load_15m = document.getElementById("st_cpu_load_15m");
    const st_cpu_util     = document.getElementById("st_cpu_util");
    
    const st_mem_txt_total  = document.getElementById("st_mem_txt_total");
    const st_mem_bar_total  = document.getElementById("st_mem_bar_total");
    const st_mem_txt_used   = document.getElementById("st_mem_txt_used");
    const st_mem_bar_used   = document.getElementById("st_mem_bar_used");
    const st_mem_txt_free   = document.getElementById("st_mem_txt_free");
    const st_mem_bar_free   = document.getElementById("st_mem_bar_free");
    const st_mem_txt_buffer = document.getElementById("st_mem_txt_buffer");
    const st_mem_bar_buffer = document.getElementById("st_mem_bar_buffer");
    
    const st_net_mac      = document.getElementById("st_net_mac");
    const st_net_int_ip   = document.getElementById("st_net_int_ip");
    const st_net_ext_ip   = document.getElementById("st_net_ext_ip");
    const st_net_rx_bytes = document.getElementById("st_net_rx_bytes");
    const st_net_tx_bytes = document.getElementById("st_net_tx_bytes");
    
    const st_rootfs_txt_total = document.getElementById("st_rootfs_txt_total");
    const st_rootfs_bar_total = document.getElementById("st_rootfs_bar_total");
    const st_rootfs_txt_used  = document.getElementById("st_rootfs_txt_used");
    const st_rootfs_bar_used  = document.getElementById("st_rootfs_bar_used");
    const st_rootfs_txt_free  = document.getElementById("st_rootfs_txt_free");
    const st_rootfs_bar_free  = document.getElementById("st_rootfs_bar_free");
    
    const st_sdcard_txt_total = document.getElementById("st_sdcard_txt_total");
    const st_sdcard_bar_total = document.getElementById("st_sdcard_bar_total");
    const st_sdcard_txt_used  = document.getElementById("st_sdcard_txt_used");
    const st_sdcard_bar_used  = document.getElementById("st_sdcard_bar_used");
    const st_sdcard_txt_free  = document.getElementById("st_sdcard_txt_free");
    const st_sdcard_bar_free  = document.getElementById("st_sdcard_bar_free");
    
    // Web elements (refresh)
    const st_ref_once   = document.getElementById("st_ref_once");
    const st_ref_number = document.getElementById("st_ref_number");
    const st_ref_range  = document.getElementById("st_ref_range");
    const st_ref_limit  = document.getElementById("st_ref_limit");
    
    // Web events (refresh)
    st_ref_once.addEventListener  ("click",  function() { st_statistics(); });
    st_ref_number.addEventListener("change", function() { st_align(st_ref_number.value); });
    st_ref_number.addEventListener("change", function() { st_kickoff(); });
    st_ref_range.addEventListener ("input",  function() { st_align(st_ref_range.value); });
    st_ref_range.addEventListener ("change", function() { st_kickoff(); });
    st_ref_limit.addEventListener ("change", function() { st_ref_limit_on(); });
    
    // Constants
    const st_limit_c = 1000; // Sensible limit value for refresh rate
    
    // Globals (general)
    var st_timer;          // Refresh timer
    var st_sequence   = 1; // Outgoing request sequence number
    var st_net_ptime  = 0; // Previous network timestamp
    var st_net_prx    = 0; // Previous network Rx bytes
    var st_net_ptx    = 0; // Previous network Tx bytes
    var st_cpu_pidle  = 0; // Previous CPU idle time
    var st_cpu_ptotal = 0; // Previous CPU total time
    
    // Globals (chart history)
    var cpu_chart    = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
    var eth_rx_chart = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
    var eth_tx_chart = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
    
    // Format Kilobytes into Kilobytes, Megabytes, Gigabytes & Terabytes (2 decimal places)
    function fmt_kbytes(value) {
      if      (value >= 1073741824) { value = (value / 1073741824).toFixed(2) + " Tb"; }
      else if (value >= 1048576)    { value = (value / 1048576).toFixed(2) + " Gb"; }
      else if (value >= 1024)       { value = (value / 1024).toFixed(2) + " Mb"; }
      else                          { value = (value / 1).toFixed(2) + " Kb"; }
      return value;
    }
    
    // Format uptime (fractions of a seconds) into days, hours, minutes & seconds
    function fmt_uptime(value) {
      value = Number(value);
      const d = Math.floor(value / (3600*24));
      const h = Math.floor(value % (3600*24) / 3600);
      const m = Math.floor(value % 3600 / 60);
      const s = Math.floor(value % 60);
      const days    = (d > 0) ? d + (d == 1 ? " day, " : " days, ") : "";
      const hours   = (d > 0 || h > 0) ? h + (h == 1 ? " hr, " : " hrs, ") : "";
      const minutes = (d > 0 || h > 0 || m > 0) ? m + (m == 1 ? " min " : " mins ") : "";
      const seconds = ((d > 0 || h > 0 || m > 0) ? " &amp; " : "") + s + (s == 1 ? " sec" : " secs");
      return days + hours + minutes + seconds;
    }
    
    // Format bytes per second (Bps) into Bits/Kilobits/Megabits & Gigabits per second
    function fmt_bps_bits(value) {
      if      (value >= 125000000) { value = (value / 125000000).toFixed() + " Gbps"; }
      else if (value >= 125000)    { value = (value / 125000).toFixed() + " Mbps"; }
      else if (value >= 125)       { value = (value / 125).toFixed() + " Kbps"; }
      else                         { value = (value / 1).toFixed() + " bps"; }
      return value;
    }
    
    // Format bytes per second (Bps) into Bytes/Kilobytes/Megabytes & Gigabytes per second
    function fmt_bps_bytes(value) {
      if      (value >= 1000000000) { value = (value / 1000000000).toFixed() + " GBps"; }
      else if (value >= 1000000)    { value = (value / 1000000).toFixed() + " MBps"; }
      else if (value >= 1000)       { value = (value / 1000).toFixed() + " KBps"; }
      else                          { value = (value / 1).toFixed() + " Bps"; }
      return value;
    }
    
    // Format milliseconds into milliseconds, seconds & minutes
    function fmt_ms(value) {
      if      (value >= 60000) { value = (value / 60000).toFixed() + " m"; }
      else if (value >= 1000)  { value = (value / 1000).toFixed() + " s"; }
      else                     { value = (value / 1).toFixed() + " ms"; }
      return value;
    }
    
    // Update the CPU chart with new data
    function update_cpu_chart(array, value, id) {
      array.shift();
      array.push(value);
      html = '';
      const min  = 0.0;
      const max  = 100.0;
      for (let i = 0; i < array.length; i++) {
        height = Math.round(99 * ((array[i] - min) / max));
        border = (array[i] == 0.0) ? 0 : 1;
        html += '<div class="bar" style="border-width:'+ border + 'px; height:' + height + 'px; left:' + (7 * i) + 'px"></div>';
      }
      document.getElementById(id).innerHTML = html;
    }
    
    // Update the Network Rx/Tx chart with new data
    function update_net_chart(array, value, id) {
      array.shift();
      array.push(value);
      var html ='';
      var max  = array[0];
      for (let i = 0; i < array.length; i++) {
        if (max < array[i]) { max = array[i]; }
      }
      for (let i = 0; i < array.length; i++) {
        height = Math.round( 46 * ((array[i] / max)) );
        border = (array[i] == 0.0) ? 0 : 1;
        html += '<div class="bar" style="border-width:'+ border + 'px; height:' + height + 'px; left:' + (7 * i) + 'px"></div>';
      }
      document.getElementById(id).innerHTML = html;
    }
    
    // Get statistics
    function st_statistics() {
      const url = "/cgi-bin/get_stats.cgi?" + st_sequence;
      const ajaxReq = new XMLHttpRequest();
      const start = Date.now();
      st_sys_seq_sent.innerHTML = st_sequence;
      st_sequence++;
      ajaxReq.open("GET", url, true);
      ajaxReq.onload = () => {
        if (ajaxReq.readyState === XMLHttpRequest.DONE && ajaxReq.status === 200) {
          st_sys_seq_rtt.innerHTML = fmt_ms(Date.now() - start);
          const respText = ajaxReq.responseText;
          fields = respText.split(/\r?\n/);
    
          // Update System table
          st_sys_hostname.innerHTML = fields[cgi_sys_hostname];              // CGI: Hostname
          st_sys_time.innerHTML     = fields[cgi_sys_time];                  // CGI: Date & Time
          st_sys_uptime.innerHTML   = fmt_uptime(fields[cgi_sys_uptime]);    // CGI: Uptime
          //                                                                 // JS: Sequence - Packet - Sent
          st_sys_seq_rcvd.innerHTML = fields[cgi_sys_seq];                   // CGI: Sequence - Packet - Received
          st_sys_seq_rcvd.style.color = (st_sequence - Number(st_sys_seq_rcvd.innerHTML) > 2) ? "red" : "black";
          //                                                                 // JS: Sequence - Turnaround
    
          // Update CPU table
          st_cpu_model.innerHTML = fields[cgi_cpu_model]; // CGI: Model
          st_cpu_cores.innerHTML = fields[cgi_cpu_cores]; // CGI: Cores
          const loads = fields[cgi_cpu_load].split(" ");  // CGI: Load
          st_cpu_load_1m.innerHTML  = loads[0];
          st_cpu_load_5m.innerHTML  = loads[1];
          st_cpu_load_15m.innerHTML = loads[2];
          // Calculate CPU usage                          // CGI & JS: Usage
          const st_cpu_idle   = fields[cgi_cpu_idle];
          const st_cpu_total  = fields[cgi_cpu_total];
          if (!isNaN(st_cpu_idle) && !isNaN(st_cpu_total)) {
            const diff_total = st_cpu_total - st_cpu_ptotal;
            if (st_cpu_pidle > 0 && diff_total > 0) {
              const diff_idle  = st_cpu_idle - st_cpu_pidle;
              const diff_usage = ((100*(diff_total-diff_idle)/diff_total));
              st_cpu_util.innerHTML = diff_usage.toFixed(2);
              update_cpu_chart(cpu_chart, diff_usage, "st_cpu_chart");
            }
            st_cpu_pidle  = st_cpu_idle;
            st_cpu_ptotal = st_cpu_total;
          }
    
          // Update Memory table
          // CGI: Total
          st_mem_bar_total.max        = fields[cgi_mem_total];
          st_mem_bar_total.value      = fields[cgi_mem_total];
          st_mem_pct_total.innerHTML  = (100*fields[cgi_mem_total]/fields[cgi_mem_total]).toFixed(2);
          st_mem_txt_total.innerHTML  = fmt_kbytes(fields[cgi_mem_total]);
          // CGI: Used
          st_mem_bar_used.max         = fields[cgi_mem_total];
          st_mem_bar_used.value       = fields[cgi_mem_used];
          st_mem_pct_used.innerHTML   = (100*fields[cgi_mem_used]/fields[cgi_mem_total]).toFixed(2);
          st_mem_txt_used.innerHTML   = fmt_kbytes(fields[cgi_mem_used]);
          // CGI: Free
          st_mem_bar_free.max         = fields[cgi_mem_total];
          st_mem_bar_free.value       = fields[cgi_mem_free];
          st_mem_pct_free.innerHTML   = (100*fields[cgi_mem_free]/fields[cgi_mem_total]).toFixed(2);
          st_mem_txt_free.innerHTML   = fmt_kbytes(fields[cgi_mem_free]);
          // CGI: Buffer/Cache
          st_mem_bar_buffer.max       = fields[cgi_mem_total];
          st_mem_bar_buffer.value     = fields[cgi_mem_buffer];
          st_mem_pct_buffer.innerHTML = (100*fields[cgi_mem_buffer]/fields[cgi_mem_total]).toFixed(2);
          st_mem_txt_buffer.innerHTML = fmt_kbytes(fields[cgi_mem_buffer]);
    
          // Update Network table
          st_net_mac.innerHTML   = fields[cgi_net_mac];     // CGI: MAC Address
          st_net_int_ip.innerHTML = fields[cgi_net_int_ip]; // CGI: Internal IP
          st_net_ext_ip.innerHTML = fields[cgi_net_ext_ip]; // CGI: External IP
          // Calculate Ethernet usage                       // CGI & JS: Speed
          const st_net_time = fields[cgi_net_time];
          const st_net_rx   = fields[cgi_net_rx_bytes];
          const st_net_tx   = fields[cgi_net_tx_bytes];
          if (!isNaN(st_net_time) && !isNaN(st_net_rx) && !isNaN(st_net_tx) && st_net_time != st_net_ptime) {
            const interval = st_net_time - st_net_ptime;
            if (st_net_ptime > 0 && interval > 0) {
              const rx_rate  = (st_net_rx-st_net_prx)/interval;
              const tx_rate  = (st_net_tx-st_net_ptx)/interval;
              st_net_rx_bytes.innerHTML = fmt_bps_bytes(rx_rate); // CGI & JS: Speed - Rx
              st_net_tx_bytes.innerHTML = fmt_bps_bytes(tx_rate); // CGI & JS: Speed - Tx
              update_net_chart(eth_rx_chart, rx_rate, "st_rx_chart");
              update_net_chart(eth_tx_chart, tx_rate, "st_tx_chart");
            }
            st_net_ptime = st_net_time;
            st_net_prx   = st_net_rx;
            st_net_ptx   = st_net_tx;
          }
    
          // Update Disks (rootfs) table
          // CGI: Total
          st_rootfs_bar_total.max       = fields[cgi_rootfs_total];
          st_rootfs_bar_total.value     = fields[cgi_rootfs_total];
          st_rootfs_pct_total.innerHTML = (100*fields[cgi_rootfs_total]/fields[cgi_rootfs_total]).toFixed(2);
          st_rootfs_txt_total.innerHTML = fmt_kbytes(fields[cgi_rootfs_total]);
          // CGI: Used
          st_rootfs_bar_used.max        = fields[cgi_rootfs_total];
          st_rootfs_bar_used.value      = fields[cgi_rootfs_used];
          st_rootfs_pct_used.innerHTML  = (100*fields[cgi_rootfs_used]/fields[cgi_rootfs_total]).toFixed(2);
          st_rootfs_txt_used.innerHTML  = fmt_kbytes(fields[cgi_rootfs_used]);
          // CGI: Free
          st_rootfs_bar_free.max        = fields[cgi_rootfs_total];
          st_rootfs_bar_free.value      = fields[cgi_rootfs_free];
          st_rootfs_pct_free.innerHTML  = (100*fields[cgi_rootfs_free]/fields[cgi_rootfs_total]).toFixed(2);
          st_rootfs_txt_free.innerHTML  = fmt_kbytes(fields[cgi_rootfs_free]);
    
          // Update Disks (mmcblk0p1) table
          // CGI: Total
          st_sdcard_bar_total.max       = fields[cgi_sdcard_total];
          st_sdcard_bar_total.value     = fields[cgi_sdcard_total];
          st_sdcard_pct_total.innerHTML = (100*fields[cgi_sdcard_total]/fields[cgi_sdcard_total]).toFixed(2);
          st_sdcard_txt_total.innerHTML = fmt_kbytes(fields[cgi_sdcard_total]);
          // CGI: Used
          st_sdcard_bar_used.max        = fields[cgi_sdcard_total];
          st_sdcard_bar_used.value      = fields[cgi_sdcard_used];
          st_sdcard_pct_used.innerHTML  = (100*fields[cgi_sdcard_used]/fields[cgi_sdcard_total]).toFixed(2);
          st_sdcard_txt_used.innerHTML  = fmt_kbytes(fields[cgi_sdcard_used]);
          // CGI: Free
          st_sdcard_bar_free.max        = fields[cgi_sdcard_total];
          st_sdcard_bar_free.value      = fields[cgi_sdcard_free];
          st_sdcard_pct_free.innerHTML  = (100*fields[cgi_sdcard_free]/fields[cgi_sdcard_total]).toFixed(2);
          st_sdcard_txt_free.innerHTML  = fmt_kbytes(fields[cgi_sdcard_free]);
        }
      }
      ajaxReq.send(null);
    }
    
    // Update statistics timer
    function st_kickoff() {
      clearInterval(st_timer);
      const interval = st_ref_range.value;
      if (interval > 0) {
        st_timer = setInterval("st_statistics()", interval);
      }
    }
    
    // Align slider & number values
    function st_align(value) {
      if (st_ref_limit.checked && value != 0 && value < st_limit_c) {
        st_ref_number.value = st_ref_range.value = st_limit_c;
      } else {
        st_ref_number.value = st_ref_range.value = value;
      }
    }
    
    // Apply limited on checkbox
    function st_ref_limit_on() {
      if (st_ref_limit.checked && st_ref_range.value != 0 && st_ref_range.value < st_limit_c) {
        st_align(st_limit_c);
        st_kickoff();
      }
    }
    
    Direct download available here :-
    steve@Desktop:~/swuk_tutorial$ wget https://spacewire.co.uk/tutorial/shared/repos/0038/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/script.js -O zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/script.js
    Check out the changes.
    steve@Desktop:~/swuk_tutorial$ git difftool zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/script.js

    5. Create a new Cascading Style sheet

    Create a new CSS to include the styles required for the System webpage :-
    steve@Desktop:~/swuk_tutorial$ subl zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/style.css

    style.css

    /*
     * File .......... style.css
     * Author ........ Steve Haywood
     * Website ....... http://www.spacewire.co.uk
     * Project ....... Zedboard Linux (SpaceWire UK Tutorial)
     * Date .......... 25 Mar 2026
     * Version ....... 1.0
     * Description ...
     *   Cascading Style Sheet for System Information & Operating System / Firmware Load.
     */
    
    .chart {
      position: relative;
      height: 103px;
      width: 108px;
      display: table-cell;
      vertical-align: bottom;
      font-size: 10px;
    }
    
    .bar {
      position: absolute;
      bottom: 0;
      display: inline-block;
      width: 4px;
      margin: 1px 2px;
      border: 1px #2377c7 solid;
      border-radius: 3px;
      background-color: #398ee7;
    }
    
    input:invalid + span::after {
      content: "✖";
      padding-left: 5px;
    }
    
    input:valid + span::after {
      content: "✓";
      padding-left: 5px;
    }
    
    Direct download available here :-
    steve@Desktop:~/swuk_tutorial$ wget https://spacewire.co.uk/tutorial/shared/repos/0038/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/style.css -O zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/style.css
    Check out the changes.
    steve@Desktop:~/swuk_tutorial$ git difftool zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/style.css
    #### Part 3 - Check everything is working as expected (beta) ####

    6. Patch Zedboard PetaLinux

    Patch the currently running Zedboard PetaLinux with the required files to add in the new System functionality.

    Recursively copy the entire website directory into the petalinux user account and then use sudo to add/overwrite the required files in the root owned /srv/www directory.
    steve@Desktop:~/swuk_tutorial$ swuk_ssh 192.168.2.87
    steve@Desktop:~/swuk_tutorial$ sshpass -p petalinux scp -r zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website petalinux@192.168.2.87:/home/petalinux
    steve@Desktop:~/swuk_tutorial$ sshpass -p petalinux ssh -t petalinux@192.168.2.87 "sudo sh -c 'cp website/files/cgi-bin/get_stats.cgi /srv/www/cgi-bin/get_stats.cgi; chmod +x /srv/www/cgi-bin/get_stats.cgi; cp website/files/system/index.php /srv/www/system/index.php; cp website/files/system/script.js /srv/www/system/script.js; cp website/files/system/style.css /srv/www/system/style.css'"
    Refresh the System page. Do a "Shift-Refresh" (Hard Reset) to clear the cache & reload the page (use Shift + Reload Button or Ctrl + Shift + R).

    Behold the new System webpage. Missing Image! Note: The System time is incorrect for the United Kingdom (it is one hour behind, the UK is currently on British Summer Time).

    7. Check new & updated System webpage elements

    Check the Webpage updates accordingly for the additional new features. Note these crude tests are designed to check the webpage NOT the performance of PetaLinux.

    System
    1. Check the (system) Time is incrementing correctly.
    2. Check the (system) Uptime is incrementing correctly (in long format; days, hours, minutes & seconds).
    3. Check the Sent sequence is incrementing by 1 every second.
    4. Check the Received sequence is also incrementing, just slightly behind the Sent sequence and in sync with it.
    5. Check the Round-Trip Time (RTT) is showing a range roughly between 310 and 340 ms.
    CPU
    1. Check the CPU usage is around 12-14% (main usage is this monitoring!).
    2. Take the Load (1m) upto around 0.40 and the CPU usage upto around 61%.
    3. Check the CPU chart tallies up with the CPU usage.
    petalinux:~$ for j in $(seq 1 1000000); do : ; done
    1. Wait for the Load (1m) to drop back to around 0.00 (takes a few minutes).
    2. Take the Load (1m) upto around 0.85 and the CPU usage upto 100%.
    3. Check the CPU chart tallies up with the CPU usage.
    petalinux:~$ for i in 1 2; do for j in $(seq 1 1000000); do : ; done & done
    1. Wait for the Load (1m) to drop back to around 0.00 (takes ages for some reason - need to look into this!).
    2. Take the Load (1m) upto around 2.48 and the CPU usage upto 100%.
    3. Check the graph tallies up with the CPU usage.
    petalinux:~$ for i in 1 2 3; do for j in $(seq 1 1000000); do : ; done & done
    Memory
    1. Check the Free field to see how much memory is available (~330 Mb).
    2. Use a large chuck of it, say 300 Mb.
    3. Check the Free field shows a lot lower available memory.
    4. Check the Used field shows a lot more used memory.
    5. Ctrl-C to stop the process.
    6. Check the Free and Used fields show things are back to normal.
    petalinux:~$ dd bs=300M if=/dev/zero of=/dev/null
    1. Check the Free field to see how much memory is available (~330 Mb).
    2. Allocate a large chuck of it, say 256 Mb.
    3. Fill the allocated space with a 255 Mb dummy file.
    4. Check the Free field shows a lot lower available memory.
    5. Check the Buffer/Cache field shows a lot more used memory.
    6. Delete the dummy file.
    7. Check the Free and Buffer/Cache fields show things are back to normal.
    petalinux:~$ mkdir /tmp/memtest
    petalinux:~$ sudo mount -o size=256M -t tmpfs memtest /tmp/memtest
    petalinux:~$ dd bs=1M if=/dev/zero of=/tmp/memtest/dummyfile count=255
    petalinux:~$ rm /tmp/memtest/dummyfile
    Network
    1. Create a largish file on the Linux Desktop.
    2. Copy the large file from the Desktop to PetaLinux using the Ethernet connection.
    3. Check the Network Speed (Rx) spikes up to around 20 MBps (megabytes per second).
    4. Copy the large file from PetaLinux to the Desktop using the Ethernet connection.
    5. Check the Network Speed (Tx) spikes up to around 18 MBps (megabytes per second).
    6. Copy the large file to & from PetaLinux & the Desktop simultaneously using the Ethernet connection.
    7. Check the Network Speed (Rx) spikes up to around 12 MBps & the Network Speed (Tx) spikes to around 12 MBps (this test isn't that great - could do with larger files!).
    8. Delete the 2 dummy files.
    steve@Desktop:~/swuk_tutorial$ dd bs=1M if=/dev/zero of=/tmp/file1 count=50
    steve@Desktop:~/swuk_tutorial$ sshpass -p petalinux scp /tmp/file1 petalinux@192.168.2.87:/home/petalinux/file1
    steve@Desktop:~/swuk_tutorial$ sshpass -p petalinux scp petalinux@192.168.2.87:/home/petalinux/file1 /tmp/file2
    steve@Desktop:~/swuk_tutorial$ sshpass -p petalinux scp petalinux@192.168.2.87:/home/petalinux/file1 /tmp/file2 & sshpass -p petalinux scp /tmp/file1 petalinux@192.168.2.87:/home/petalinux/file2
    petalinux:~$ rm file1 file2
    Disks (rootfs)
    1. Check the Free field to see how much disk space is available (~140 Mb).
    2. Create a large file that uses ~95% of it.
    3. Check the Webpage shows Used as almost full and Free as almost empty.
    4. Remove the large file.
    5. Check the Webpage shows Used and Free are back to normal.
    petalinux:~$ dd bs=1M if=/dev/zero of=large_file count=126
    126+0 records in
    126+0 records out
    petalinux:~$ rm large_file
    Disks (mmcblk0p1)
    1. Check the Free field to see how much disk space is available (~1.87 Gb for example).
    2. Create a large file (say 256 GB).
    3. Check the Webpage shows the Used and Free field have adjusted accordingly.
    4. Remove the large file.
    5. Check the Webpage shows Used and Free are back to normal.
    petalinux:~$ dd bs=1M if=/dev/zero of=/media/sd-mmcblk0p1/large_file count=256
    256+0 records in
    256+0 records out
    petalinux:~$ rm /media/sd-mmcblk0p1/large_file
    #### Part 4 - Make the changes permanent ####

    8. Bump version

    Change the version (or revision) number for this new development, this prevents ghost (post-release, same version) builds from appearing.
    steve@Desktop:~/swuk_tutorial$ sed -i 's/20.0/21.0/g' zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/project.txt
    Just for completeness :-

    project.txt

    Zedboard PetaLinux
    SpaceWire UK
    Steve Haywood
    21.0
    
    Direct download available here :-
    steve@Desktop:~/swuk_tutorial$ wget https://spacewire.co.uk/tutorial/shared/repos/0038/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/project.txt -O zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/project.txt
    Check out the changes.
    steve@Desktop:~/swuk_tutorial$ git difftool zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/project.txt

    9. Remove superfluous CGI script

    Remove the now unused/superseded uptime CGI script.
    steve@Desktop:~/swuk_tutorial$ rm zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/uptime.cgi
    Just for completeness :-

    10. Update Website BitBake recipe

    Update the website Bitbake recipe to include the following changes :- To achieve the above the BitBake recipe is going to be overhauled to be less cumbersome and more concise. Before this task can be undertaken the file permissions need to be examined. GIT only stores two types of permissions on files, executable and non-executable, which makes things less complicated.
    steve@Desktop:~/swuk_tutorial$ tree -p zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files
    zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files
    ├── [drwxrwxr-x]  cgi-bin
    │   ├── [-rwxrwxr-x]  get_stats.cgi
    │   ├── [-rw-rw-r--]  hello_world.php
    │   ├── [-rw-rw-r--]  phpliteadmin.config.php
    │   ├── [-rw-rw-r--]  phpliteadmin.php
    │   ├── [-rwxrwxr-x]  reload_os.cgi
    │   ├── [-rw-rw-r--]  sqlite_test.php
    │   └── [-rwxrwxr-x]  test-cgi
    ├── [drwxrwxr-x]  home
    │   └── [-rw-rw-r--]  index.php
    ├── [-rw-rw-r--]  index.php
    ├── [drwxrwxr-x]  peekpoke
    │   ├── [-rw-rw-r--]  index.php
    │   └── [-rw-rw-r--]  script.js
    ├── [-rw-rw-r--]  project.txt
    ├── [drwxrwxr-x]  share
    │   ├── [-rwxrwxr-x]  amber.gif
    │   ├── [-rw-rw-r--]  footer.php
    │   ├── [-rwxrwxr-x]  green.gif
    │   ├── [-rw-rw-r--]  header.php
    │   ├── [-rwxrwxr-x]  red.gif
    │   ├── [-rw-rw-r--]  script.js
    │   └── [-rw-rw-r--]  style.css
    ├── [drwxrwxr-x]  system
    │   ├── [-rw-rw-r--]  index.php
    │   └── [-rw-rw-r--]  script.js
    └── [drwxrwxr-x]  zedboard
        ├── [-rw-rw-r--]  btn_none.png
        ├── [-rw-rw-r--]  btn_off.png
        ├── [-rw-rw-r--]  btn_on.png
        ├── [-rw-rw-r--]  index.php
        ├── [-rw-rw-r--]  led_off.png
        ├── [-rw-rw-r--]  led_on.png
        ├── [-rw-rw-r--]  script.js
        ├── [-rw-rw-r--]  style.css
        ├── [-rw-rw-r--]  sw_none.png
        ├── [-rw-rw-r--]  sw_off.png
        ├── [-rw-rw-r--]  sw_on.png
        └── [-rw-rw-r--]  zedboard.png

    6 directories, 33 files
    Check get_stats.cgi, reload_os.cgi & test_cgi are all executable (they should be) and all other files are non-executable (some gif files are wrongly set!).

    Fix the git file permissions, not necessary but more clean!
    steve@Desktop:~/swuk_tutorial$ cd zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files$ chmod -x share/*.gif
    Create a symbolic link from a new www directory back to its parent. This will allow the BitBake recipe to install the entirety of the files directory using just a few lines of code.
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files$ ln -s . www
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files$ cd -
    Optimise the BitBake recipe to do a recursive install of all the items held inside the files directory.
    steve@Desktop:~/swuk_tutorial$ subl zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/website.bb

    website.bb

    #
    # File .......... website.bb
    # Author ........ Steve Haywood
    # Website ....... http://www.spacewire.co.uk
    # Project ....... Zedboard Linux (SpaceWire UK Tutorial)
    # Date .......... 25 Mar 2026
    # Version ....... 11.0
    # Description ...
    #   Application recipe for a dynamic (server & client side) website that is split
    # into separate sections (directories). Each section contains an index page
    # along with any associated files (sub-pages, scripts, styles, images, etc.).
    # There are also two shared areas for common files.
    #
    
    SUMMARY = "Simple website application"
    SECTION = "PETALINUX/apps"
    LICENSE = "MIT"
    LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
    
    SRC_URI = "file://www"
    
    FILES:${PN} += "/srv/www"
    
    S = "${WORKDIR}"
    
    do_install() {
      install -d ${D}/srv/www
      cp -r ${S}/www ${D}/srv
    }
    
    Direct download available here :-
    steve@Desktop:~/swuk_tutorial$ wget https://spacewire.co.uk/tutorial/shared/repos/0038/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/website.bb -O zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/website.bb
    Check out the changes.
    steve@Desktop:~/swuk_tutorial$ git difftool zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/website.bb
    #### Part 5 - Network time protocol & Timezone ####

    11. Clean up and correct the network time items

    Currently the time reported by PetaLinux is the Coordinated Universal Time (UTC). Not all that great for different parts of the world that reside in differing time zones.

    The PetaLinux time system will be overhauled as follows :-
    1. Remove all the various time related components (ntp, ntpdate, ntpq & sntp) from the PetaLinux BSP configuration file.
    2. Add back in the removed ntp component via the rootfs configuration utility.
    3. Add in the time zone component using the rootfs configuration utility.
    4. Set the correct time zone by using a BitBake append file.
    Action point 1

    Clean up the PetaLinux BSP configuration file.
    steve@Desktop:~/swuk_tutorial$ subl zedboard_linux/os/petalinux/project-spec/meta-user/conf/petalinuxbsp.conf

    petalinuxbsp.conf

    #
    # File .......... petalinuxbsp.conf
    # Author ........ Steve Haywood
    # Website ....... http://www.spacewire.co.uk
    # Project ....... Zedboard Linux (SpaceWire UK Tutorial)
    # Date .......... 25 Mar 2026
    # Version ....... 5.0
    # Description ...
    #   Updated to install the following :-
    # -- nano ...... Text editor
    # Updated to install a LAMP style stack :-
    # -- Apache .... HTTP server
    # -- SQLite .... SQL database engine
    # -- PHP ....... Scripting language
    #
    
    #User Configuration
    
    #OE_TERMINAL = "tmux"
    
    IMAGE_INSTALL:append = " nano"
    PACKAGECONFIG:append:pn-php = " apache2"
    IMAGE_INSTALL:append = " apache2 sqlite3 php-modphp"
    
    Direct download available here :-
    steve@Desktop:~/swuk_tutorial$ wget https://spacewire.co.uk/tutorial/shared/repos/0038/zedboard_linux/os/petalinux/project-spec/meta-user/conf/petalinuxbsp.conf -O zedboard_linux/os/petalinux/project-spec/meta-user/conf/petalinuxbsp.conf
    Check out the changes.
    steve@Desktop:~/swuk_tutorial$ git difftool zedboard_linux/os/petalinux/project-spec/meta-user/conf/petalinuxbsp.conf
    Action points 2 & 3

    Enable ntp & tzdata.
    steve@Desktop:~/swuk_tutorial$ cd zedboard_linux/os/petalinux
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ petalinux-config -c rootfs
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ cd -
    Select: Filesystem Packages ---> network ---> ntp ---> [*] ntp
    Then: Exit ---> Exit ---> Exit
    Select: Filesystem Packages ---> base ---> tzdata ---> [*] tzdata
    Then: Save ---> Ok ---> Exit ---> Exit ---> Exit ---> Exit ---> Exit

    Action point 4

    Create a new BitBake append file to set the timezone. Add the line: DEFAULT_TIMEZONE = "Europe/London". Obviously change for your own region!
    steve@Desktop:~/swuk_tutorial$ mkdir -p zedboard_linux/os/petalinux/project-spec/meta-user/recipes-extended/timezone
    steve@Desktop:~/swuk_tutorial$ subl zedboard_linux/os/petalinux/project-spec/meta-user/recipes-extended/timezone/tzdata%.bbappend

    tzdata%.bbappend

    DEFAULT_TIMEZONE = "Europe/London"
    
    Direct download available here :-
    steve@Desktop:~/swuk_tutorial$ wget https://spacewire.co.uk/tutorial/shared/repos/0038/zedboard_linux/os/petalinux/project-spec/meta-user/recipes-extended/timezone/tzdata%.bbappend -O zedboard_linux/os/petalinux/project-spec/meta-user/recipes-extended/timezone/tzdata%.bbappend
    Action point 4 (alternative)

    If the above isn't desired, the following can be executed at run-time or placed inside the boot-extras script. Obviously change for your own region!
    petalinux:~$ sudo ln -sf /usr/share/zoneinfo/Europe/London /etc/localtime
    #### Part 6 - Revision Control ####

    12. Commit new & updated files

    Check GIT status to make sure all is well and there are no spurious elements.
    steve@Desktop:~/swuk_tutorial$ git status -u
    On branch master
    Your branch is up to date with 'origin/master'.

    Changes not staged for commit:
      (use "git add/rm <file>..." to update what will be committed)
      (use "git restore <file>..." to discard changes in working directory)
       modified:   zedboard_linux/os/petalinux/.petalinux/metadata
       modified:   zedboard_linux/os/petalinux/project-spec/configs/.statistics
       modified:   zedboard_linux/os/petalinux/project-spec/configs/rootfs_config
       modified:   zedboard_linux/os/petalinux/project-spec/meta-user/conf/petalinuxbsp.conf
       deleted:    zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/uptime.cgi
       modified:   zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/project.txt
       modified:   zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/share/amber.gif
       modified:   zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/share/green.gif
       modified:   zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/share/red.gif
       modified:   zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/index.php
       modified:   zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/script.js
       modified:   zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/website.bb

    Untracked files:
      (use "git add <file>..." to include in what will be committed)
       zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/cgi-bin/get_stats.cgi
       zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/system/style.css
       zedboard_linux/os/petalinux/project-spec/meta-user/recipes-apps/website/files/www
       zedboard_linux/os/petalinux/project-spec/meta-user/recipes-extended/timezone/tzdata%.bbappend

    no changes added to commit (use "git add" and/or "git commit -a")
    Looks good! Commit the updates, create an annotated tag and push the commit & tag up to the remote repository.
    steve@Desktop:~/swuk_tutorial$ git add -A
    steve@Desktop:~/swuk_tutorial$ git commit -a -m "Updated the System webpage to include live data provided from PetaLinux."
    steve@Desktop:~/swuk_tutorial$ git push
    steve@Desktop:~/swuk_tutorial$ git tag -a my_zedboard_linux_v21.0 -m "PetaLinux, Peek/Poke, LED Runner, LAMP (Apache, SQLite, PHP & myLiteAdmin), Peek/Poke CGI, Load Firmware CGI, PL Access, Style Sheet, Register Bank, ID Strings, ADV5711 configuration, Reload OS & live System data with XSA from zedboard_video_tpg_hdmi v1.0"
    steve@Desktop:~/swuk_tutorial$ git push origin my_zedboard_linux_v21.0

    13. Create production release

    Create a potential production release for the PetaLinux (21.0) project using pure repository source. .

    (optional) Clear out all the superfluous files from the project area (non-tracked files).
    steve@Desktop:~/swuk_tutorial$ cd zedboard_linux/os/petalinux
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ git clean -fdx
    (optional) Clear out the transfer area.
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ rm -rf /tftpboot/*
    Double check GIT status.
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ git status -u
    On branch master
    Your branch is up-to-date with 'origin/master'.

    nothing to commit, working tree clean
    Build/rebuild PetaLinux.
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ swuk_petalinux-build
    Note: A fresh build of PetaLinux takes a long time to complete and can be prone to failure due to internet downloads. If the build process fails, firstly check the GIT status & difftool for any noddy file changes, restore these file changes and then execute the build command again until it finally completes.

    Package PetaLinux to produce the boot image BOOT.BIN, this will include the first stage boot loader zynq_fsbl.elf, the programmable logic system.bit, the Linux boot loader u-boot.elf and the device tree blob system.dtb.
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ petalinux-package --boot --force --fsbl images/linux/zynq_fsbl.elf --fpga images/linux/system.bit --uboot images/linux/u-boot.elf
    Archive the generated files for safe keeping. These files are not held in the repository as they can be recreated from source.
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ mkdir -p ~/Documents/swuk_tutorial/petalinux/v21.0
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ cp /tftpboot/{BOOT.BIN,boot.scr,image.ub} ~/Documents/swuk_tutorial/petalinux/v21.0
    #### Part 7 - Hardware Deployment ####

    14. Copy BOOT files to the SD-Card

    Upload the newly created PetaLinux Boot & OS files to the SD-Card.
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ swuk_ssh 192.168.2.87
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ sshpass -p petalinux scp -r ~/Documents/swuk_tutorial/petalinux/v21.0 petalinux@192.168.2.87:/media/sd-mmcblk0p1/petalinux

    15. Reload the Operating System using the Webserver

    Refresh the System page. Do a "Shift-Refresh" (Hard Reset) to clear the cache & reload the page (use Shift + Reload Button or Ctrl + Shift + R).

    Select v21.0 from the drop-down menu in the Reload Operating System section. Missing Image! After the reload sequence has finished...
    #### Part 8 - Check everything is working as expected ####

    16. Final checks

    The Operating System Information table should show Zedboard PetaLinux description along with a version of 21.0.

    The Firmware Information table should show Zedboard Video TPG HDMI description along with a version of 1.0.

    There should be no unsavoury comments after the version numbers! Missing Image! The Timestamp & Hash shown from GIT (1st entry) should marry up perfectly with the displayed Operating System Information.
    steve@Desktop:~/swuk_tutorial/zedboard_linux/os/petalinux$ git log -1
    commit 6828fde4cf93614fd9a662304fd11dbf56973564 (HEAD -> master, tag: zedboard_linux_v21.0, origin/master, origin/HEAD)
    Author: Steve Haywood <steve@spacewire.co.uk>
    Date:   Sat Apr 11 11:54:23 2026 +0000

        Updated the System webpage to include live data provided from PetaLinux.
    The System Time should show the correct time zone, in the UK that is, for example, Sat Apr 11 09:32:00 BST 2026

    The Operating System Information tables should be updating with live data at 1s intervals.

    Looks good!
    #### Part 9 - Final resulting build file(s) ####

    17. Resulting BOOT files

    The resulting BOOT files from the above build can be obtained from here: v21.0 (2025.2).