Using Go and Fyne for your next embedded software development

by | Dec 22, 2022 | Uncategorized | 0 comments

As professional developers, we have for a long time chosen to rely on C and C++ for embedded software. Some maker might pick node.js or python, but today I will make the case that you should consider Go.

Go (also better known as Golang when using Google search) is a programming language developed at Google in 2007. It is a statically-typed, compiled language that is designed to be simple, efficient, and easy to learn. Go is similar to C and C++ in that it is a compiled, low-level language, but it is also influenced by higher-level languages and will not feel foreign to Python developers. Go has a simple syntax and a lightweight standard library, making it an excellent choice for building web servers, command-line tools, and other applications that require performance. In addition to its performance and simplicity, Go also has a strong focus on code readability and maintainability, making it a great choice for software engineers looking to build reliable as a team.

The later statement of readability and maintainability has lead to something that is quite often overlooked, but you can easily understand a foreign code base and your module dependencies. The entire ecosystem has scaled this readability and provides easy to use components with a large, well indexed documentation out of the box. Additionally, a number of built-in features and tools helps with code organization and maintenance, such as go test for unit and integration tests, gofmt for formatting code, go vet for detecting potential problems, gocyclo to keep track of your code complexity, supply chain license checker or services providing up to date static check of your code. This gives a strong base to build any kind of application.

Today consumers expect that your new devices are connected. This means that devices need to have their own web server. You might also need to provide an API to enable third party ecosystem. This can be done very easily in Go as you can see with the example below of a website serving Linux lmsensors data over a REST API that gets announced on the local network:

//go:embed index.html
var indexPage []byte

func serveIndexPageHandler(w http.ResponseWriter, r *http.Request) {
    w.Write(indexPage)
}

func main() {
    http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
        sensors, err := gosensors.NewFromSystem()
        if err != nil {
            log.Println(err)
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            return
        }
        fmt.Fprint(w, sensors.JSON())
    })
    http.HandleFunc("/", serveIndexPageHandler)

    server, err := zeroconf.Register("GoIoTSensor", "_go_iot_sensor._tcp", "local.", 8080, nil, nil)
    if err != nil {
        panic(err)
    }
    defer server.Shutdown()

    log.Println("Server running on port 8080")
    err = http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}

This only covers a small portion of the IoT needs – you might need a graphical user interface that runs locally, but also an application for iOS and Android. If you were to pick Go and Fyne, all of those requirements would actually be building on each others. A Fyne application can be compiled without change to iOS and Android. Yes, the same application run on all major mobile operating system. We created a demo of those possibility a while ago named Nomad, have a look! What run on the screen of your IoT device, could run out of the box on phones and desktop! As an example, below is a graphical application that will connect to the server we just wrote using that REST API and will run on all the platforms supported by Fyne:

func main() {
    remoteSensors, err := findSensorWithZeroConf()
    if err != nil {
        panic(err)
    }

    a := app.New()
    w := a.NewWindow("IoT go sensors!")
    w.Resize(fyne.NewSize(200, 600))

    hello := widget.NewLabel("List of sensors found on " + remoteSensors.Instance + ":")

    sensorsValue := []string{}

    list := widget.NewList(
        func() int {
            return len(sensorsValue)
        },
        func() fyne.CanvasObject {
            return container.NewHBox(widget.NewLabel("Template Object"))
        },
        func(id widget.ListItemID, item fyne.CanvasObject) {
            item.(*fyne.Container).Objects[0].(*widget.Label).SetText(sensorsValue[id])
        },
    )

    go func() {
        for {
            time.Sleep(1 * time.Second)
            sensors, err := getSensorsValueFromZeroConf(remoteSensors)
            if err == nil {
                sensorsValue = stringsFromSensors(sensors)
                list.Refresh()
            }
        }
    }()

    w.SetContent(container.NewBorder(hello, nil, nil, nil, list))

    w.ShowAndRun()
}

Third party developers would love to have a command line tool to more easily get that JSON without being bothered with any details to use it in scripts, so let’s add a command line argument to do so easily:

...

func main() {
    jsonPtr := flag.Bool("json", false, "get the json content of a remote sensor.")
    flag.Parse()

    remoteSensors, err := findSensorWithZeroConf()
    if err != nil {
        panic(err)
    }

    if *jsonPtr {
        sensors, err := getSensorsValueFromZeroConf(remoteSensors)
        if err != nil {
            panic(err)
        }
        fmt.Println(sensors.JSON())
        return
    }

    a := app.New()
    w := a.NewWindow("IoT go sensors!")
    w.Resize(fyne.NewSize(200, 600))

...

Usually, by the time you have an iOS and an Android application, there is no budget/time/energy left to do any of those. As you see, picking Go might make this possible even for a small team and can enable a lot more features than your usual approach! This is making things even more true as everyone working on this code can easily contribute new features from end to end. Your team skills can now cover the entire work from backend to frontend to IoT services.

One of the other interesting feature of Go for embedded devices is that by default it builds a single binary with the ability to statically link all dependencies. And the resulting binaries are actually quite small. Maybe a few megabytes each when using Go main compiler and only a few kilo bytes when using tiny-go. This enables a new way to look at your firmware update. Instead of doing full system update, you can now plan to do individual services update using go self update module. You can now easily update your web and API service without risking a full system update. Things can even be set up with an overlay so that you can rollback easily to the version that came with the firmware. Imagine being able to roll out update at the same speed as you update a website. This also applies to the graphical user interface that you could provide on your device, as an example I have added self update support to our example with just the following lines using Geoffrey our continuous delivery tool for Fyne applications:

func selfManage(a fyne.App, w fyne.Window) {
    publicKey := ed25519.PublicKey{6, 198, 210, 230, 188, 44, 158, 38, 216, 148, 255, 131, 42, 143, 18, 108, 121, 208, 126, 63, 57, 34, 6, 167, 117, 9, 169, 130, 41, 55, 26, 46}

    // The public key above matches the signature of the below file served by our CDN
    httpSource := selfupdate.NewHTTPSource(nil, "https://geoffrey-artefacts.fynelabs.com/self-update/b5/b5e4483c-40c7-4c8b-a506-72d0a8d1a38c/{{.OS}}-{{.Arch}}/{{.Executable}}{{.Ext}}")

    config := fyneselfupdate.NewConfigWithTimeout(a, w, time.Minute, httpSource, selfupdate.Schedule{FetchOnStart: true, Interval: time.Hour * 12}, publicKey)

    _, err := selfupdate.Manage(config)
    if err != nil {
        log.Println("Error while setting up update manager: ", err)
        return
    }
}

func main() {
   ...

   w.SetContent(container.NewBorder(hello, nil, nil, nil, list))

   if _, ok := a.(desktop.App); ok {
           selfManage(a, w)
   }

   w.ShowAndRun()
}

Let’s finish this demo by adding some tests for the web server:

func TestRootIndexContent(t *testing.T) {
    req, err := http.NewRequest("GET", "/", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(serveIndexPageHandler)

    handler.ServeHTTP(rr, req)

    indexPage, err := os.ReadFile("index.html")
    assert.NoError(t, err)

    assert.Equal(t, http.StatusOK, rr.Code)
    assert.Equal(t, indexPage, rr.Body.Bytes())
}

I have pushed all the code of this demo into a git repository with github actions to give a quick overview of what is possible. The goal here is to keep things simple and we might later provide a different and more comprehensive demo to show more capabilities of Fyne on embedded devices.

As you can see reading this code is pretty straight forward for someone with past experience in developing embedded software or even just experience in C, C++ or Python. I could have added a demo around integrating a database with SQLite or BoltDB, but I am sure there isn’t really much difficulty there and it would just make this blog longer. I hope that with all this information, next time you start a new embedded software project or just plan to add a new feature, even if you do not have much Go experience, you will investigate using Go and consider maybe doing your project with it. Personally, I would, but I might be biased now!

0 Comments

Leave a Reply

%d bloggers like this: