Skip to content

Phase 1 - Code a UGV

Discover the basics of software architecture on a microcontroller compared to a single-board computer or standard computer. This phase will introduce a first software iteration to remotely control the UGV from a simple web page.

The code provides the following features:

  • connection to a pre-defined wireless network
  • serve a web page to drive the rover (forward, backward, left, right, stop)
  • detecting minerals of interests (optional)

This first phase will provide an usable UGV and exposing a series of flaws.

Workspace

Architecture

The below table shows the hardware difference between:

  • a microcontroller like the Raspberry Pico Wireless
  • a single board computer like the Raspberry Pi 4B
  • a standard computer specification overview
Summary Pico W Raspberry Pi 4B Computer
CPU 2x133Mhz 4x1.8Ghz ARMv8 up 5Ghz / 24+ core
RAM 256KB from 1GB to 8GB up 64 GB
Disk 2MB SD card (up to 2TB) HDD (multi TB)
Power from 90 (no wifi) up to 260-370mA (wifi) 540 to 1280mA from 1400 to +4500mA
Autonomy on 4AA between 20 to 50 hours max 2 hours n/a
Operating System none Linux or Windows for ARM Linux, BSD, Windows

Each solution has its own target:

  • a microcontroller is perfect for embedded systems on battery
  • a single board controller targets kiosk or edge computer solutions
  • a standard computer provides the horse power for multitasking with productivity or even gaming

Last is the operating system consideration. While a SBC or a computer would have an operating system and compatible software to address the user needs, a microcontroller doesn't have per say an operating system. The Raspberry Pico has the following options:

  • a MicroPython firmware capable of interpreting Python code (See Pico Getting Started)
  • a Pico SDK to develop in C/C++/ASM and build the firmware (See Pico SDK)

While this is the two official ways of developing on a Raspberry Pico, other compiled languages like Rust could also be leverage to build a firmware. Check the video from Low Level Learning.

Software

The phase 1 source code is available here.

By loading the below code, the microcontroller will connect to a local wireless network (to be configured) and provide a web server and basic HTML page to control the UGV' s movement (forward, backward, left and right).

What does the code in a linear fashion:

  • loads the necessary modules/libraries
  • define a timestamp function using the ISO8601 specs
  • get the MAC and build version (currently static)
  • establish a wireless connection with a given SSID and password
  • define the motors' PINs and movement
  • create a network socket on port 80 and bind it to the IP leased by DHCP
  • define a webControl function to link the motors' movement with web form buttons
  • define a webServer function handling the buttons' actions towards the motors' movement
  • define a continuous loop on the required functions with catching a keyboard exception (for debugging)
# Copyright 2022 Rom Adams (https://github.com/romdalf) at Red Hat Inc. 
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import utime  
import ujson
import network
import ubinascii
import machine
from machine import Pin
import socket

# getting a ISO8601 timestamp format
def timestamp():
    current_time = utime.localtime()
    time = '{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}Z'.format(
        current_time[0], current_time[1], current_time[2],
        current_time[3], current_time[4], current_time[5])
    return time

# print device and software info
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
mac = ubinascii.hexlify(network.WLAN().config('mac'),':').decode()
print(f"{timestamp()} - WRCR: {mac} v0.1 DEV RELEASE")

# network connection ####################################################
networkName = ''
networkPassword= ''

# connecting to network 
def connecting():
    if not wlan.isconnected():
        print(f"{timestamp()} - WLAN connecting to network...")
        wlan.connect(networkName, networkPassword)
        print(f"{timestamp()} - WLAN waiting for connection...")
        while not wlan.isconnected():
            pass 
    ip = wlan.ifconfig()[0]
    print(f"{timestamp()} - WLAN connected on {networkName} with IP {ip}")
    return ip

# motor setup
motorOneFW = Pin(18, Pin.OUT)
motorOneBW = Pin(19, Pin.OUT)
motorTwoFW = Pin(20, Pin.OUT)
motorTwoBW = Pin(21, Pin.OUT)

# define movements
def moveStop():
    motorOneFW.value(0)
    motorOneBW.value(0)
    motorTwoFW.value(0)
    motorTwoBW.value(0)

def moveForward():
    motorOneFW.value(0)
    motorOneBW.value(1)
    motorTwoFW.value(0)
    motorTwoBW.value(1)
    utime.sleep_ms(250)
    moveStop()

def moveBackward():
    motorOneFW.value(1)
    motorOneBW.value(0)
    motorTwoFW.value(1)
    motorTwoBW.value(0)
    utime.sleep_ms(250)
    moveStop()

def moveLeft():
    motorOneFW.value(1)
    motorOneBW.value(0)
    motorTwoFW.value(0)
    motorTwoBW.value(1)
    utime.sleep_ms(250)
    moveStop()

def moveRight():
    motorOneFW.value(0)
    motorOneBW.value(1)
    motorTwoFW.value(1)
    motorTwoBW.value(0)
    utime.sleep_ms(250)
    moveStop()

# create a network socket
socketPort = 80
def networkSocket(ip):
    address = (ip, socketPort)
    connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        print(f"{timestamp()} - Try to start a Network Socket...")
        connection.bind(address)
    except OSError:
        old = connection.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)
        print(f"{timestamp()} - Socket state {old} already in use!")
        connection.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR, 1)
        new = connection.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)
        print(f"{timestamp()} -Socket state {new}")
        connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        connection.bind(address)
    print(f"{timestamp()} - Socket started on {ip} on port {socketPort}")
    connection.listen(1)
    return connection

def webControl():
    html = f"""
            <!DOCTYPE html>
            <html>
            <head>
            <title>WRCR</title>
            </head>
            <center><b>
            <form action="./forward">
            <input type="submit" value="Forward" style="height:120px; width:120px" />
            </form>
            <table><tr>
            <td><form action="./left">
            <input type="submit" value="Left" style="height:120px; width:120px" />
            </form></td>
            <td><form action="./stop">
            <input type="submit" value="Stop" style="height:120px; width:120px" />
            </form></td>
            <td><form action="./right">
            <input type="submit" value="Right" style="height:120px; width:120px" />
            </form></td>
            </tr></table>
            <form action="./backward">
            <input type="submit" value="Backward" style="height:120px; width:120px" />
            </form>
            </body>
            </html>
            """
    return str(html)

def webServer(connection):
    print(f"{timestamp()} - webServer started with webControl as Index")
    while True:
        status = Pin("LED", Pin.OUT)
        status.toggle()
        client = connection.accept()[0]
        request = client.recv(1024)
        request = str(request)
        try:
            request = request.split()[1]
        except IndexError:
            print(f"{timestamp()} - webServer failed due to IndexError!")
            pass
        if request == '/forward?':
            print(f"{timestamp()} - webControl called for moveForward")
            moveForward()
        elif request == '/backward?':
            print(f"{timestamp()} - webControl called for moveBackward")
            moveBackward()
        elif request == '/right?':
            print(f"{timestamp()} - webControl called for moveRight")
            moveRight()
        elif request == '/left?':
            print(f"{timestamp()} - webControl called for moveLeft")
            moveLeft()
        elif request == '/stop?':
            print(f"{timestamp()} - webControl called for moveStop")
            moveStop()
        html = webControl()
        client.send(html)
        client.close()

try:
    ip = connecting()
    connection = networkSocket(ip)
    webServer(connection)
except KeyboardInterrupt:
    machine.reset()

Limitations

While this first software phase allows to discover the basics of microcontroller's usage and development via MicroPython along with making it alive, there are some limitations to this approach:

  • agile development; the entire software stack is deployed on the device itself and can only be modified by reattaching the device to a workstation and upload the code. While it might not be an issue when showcasing the UGV within a closed by location, when the UGV are deployed kilometers away, this will become a real device management issue.
  • security; anyone scouting for web page resources could takeover the UGV's control and also potential discover the network details providing compromised credentials to the network.
  • scalability; this software stack is fine when prototyping and showcasing the capabilities of the UGV but will not be scalable if there is dozen or hundreds of UGVs to control.
  • observability; this software stack can only provide debugging capabilities when connected to a workstation.