Aus Linux-Magazin 03/2025

Go-Programmierung: Animierte Figuren zeigen den Netzwerkdurchsatz

© anatoliygleb / 123RF.com

Statt mit Balkengrafiken zeigt Mike Schilli die Auslastung seiner Internetanbindung mit Zeichentrickfiguren an. Eine Einführung in die Sprite-Technik von 2D-Spielen gibt es obendrauf.

Der Messwert der Bits pro Sekunde, die gerade über die Internetanbindung rauschen, gibt Aufschluss darüber, ob das Heimnetzwerk im grünen Bereich operiert oder gerade jemand im Haushalt unbotmäßig viel Bandbreite beansprucht.

Fließt der gesamte Datenverkehr durch einen zentralen Router wie meine PfSense-Appliance, lassen sich die flitzenden Bits einfach über mehrere Sekunden gemittelt zählen, zum Beispiel mit dem nützlichen Programm Vnstat. Auf dem PfSense-Router installiere ich das Werkzeug wie im FreeBSD-Ökosystem üblich als normales Paket mittels »pkg install vnstat«.

Das Kommando »service vnstat start« startet den Dämon, der laufend über den Durchsatz Buch führt und die Messwerte in einer eigenen Binärdatenbank ablegt. Nach etwas Vorlaufzeit kann ich dann abfragen, wie viele Bits in den letzten paar Minuten, Stunden, Tagen, Wochen, Monaten oder Jahren geflossen sind.

Abbildung 1 zeigt die in Echtzeit erfolgende Ausgabe des Tools auf der Kommandozeile als Antwort auf eine Anfrage nach dem Durchfluss in beide Richtungen. Im Beispiel wählt der Parameter »-i igb0« das WAN-Interface des Routers aus, »-tr« verlangt nach dessen Realtime-Auslastung.

Abbildung 1: Das Vnstat-Kommando auf der Firewall zeigt die Auslastung in Mbit/s an.

Abbildung 1: Das Vnstat-Kommando auf der Firewall zeigt die Auslastung in Mbit/s an.

Normalerweise lauscht das Tool fünf Sekunden, bis es das Ergebnis gemittelt ausgibt. Der zusätzliche unbenannte Parameter »2« verkürzt die Zeitspanne auf zwei Sekunden. Die gemessenen Werte für »rx« (Receive, Download) und »tx« (Transmit, Upload) gibt das Werkzeug in »bit/s«, »kbit/s« oder »Mbit/s« aus, je nachdem, in welcher Größenordnung sich der Messwert bewegt.

Nun könnte ein Go-Programm diese Werte regelmäßig abholen und auf andere Art und Weise anzeigen. Vnstat bringt bereits das Rüstzeug für interessante Statistiken (Abbildung 2) mit, die aber etwas dröge wirken. Wie wäre es stattdessen mit zwei Comicfiguren als Läufer, von denen der für den Download von rechts nach links und der für den Upload in der Gegenrichtung marschiert, mit einer Geschwindigkeit, die dem gemessenen Durchflusswert entspricht?

Abbildung 2: Die normale, wenig attraktive Tabellenausgabe von Vnstat.

Abbildung 2: Die normale, wenig attraktive Tabellenausgabe von Vnstat.

Abbildung 3 zeigt die fertige Applikation. Das Go-Programm stellt die Läufer mithilfe des Fyne-Frameworks dynamisch dar. Dabei bewegen sie sich nicht nur von links nach rechts, sondern schlackern im Laufschritt auch mit den Gliedmaßen. Das klappt mit schnell überladenen Einzelbildern wie bei einem Zeichentrickfilm – dazu später mehr.

Abbildung 3: Mithilfe von Go und Fyne lassen sich Down- und Upload sehr viel dynamischer illustrieren.

Abbildung 3: Mithilfe von Go und Fyne lassen sich Down- und Upload sehr viel dynamischer illustrieren.

Sicher ohne Passwort

Damit sich das Go-Programm ohne Angabe des Passworts auf dem Admin-Account des Routers einloggen und dort Vnstat in einer Shell aufrufen darf, muss auf dem FreeBSD-System ein SSH-Daemon laufen und auf einem eingestellten Port lauschen. Dieser Dienst ist in der Voreinstellung deaktiviert, aber unter System | Advanced finden sich auf der Pfsense-Weboberfläche im unteren Bereich die zur Aktivierung notwendigen Schalter (Abbildung 4).

Abbildung 4: Mit wenigen Settings aktivieren Sie auf PfSense SSH für Shell-Kommandos.

Abbildung 4: Mit wenigen Settings aktivieren Sie auf PfSense SSH für Shell-Kommandos.

Mit der Option Public Key Only akzeptiert der Daemon aus Sicherheitsgründen kein Passwort zum Login als User admin, sondern nur einen öffentlichen Schlüssel. Der gehört mit den passenden Dateirechten nach ».ssh/authorized_keys/« im User-Verzeichnis auf dem Router und genehmigt fürderhin dem Go-Programm freien Zugang auf die Shell. Das Verwenden des Ports 8022 anstelle des Standardports 22 ist ein zusätzliches Gimmick. Sie müssen den korrekten Port dann aber später beim Aufbau der SSH-Verbindung mit der Vnstat-Option »-p« gesondert angeben.

Daumenkino

Wie in einem Zeichentrickfilm kommt Bewegung in die Animation, indem Einzelbilder wie in einem Daumenkino durchrattern. Wie viel Muskelschmalz Trickfilme vor dem Durchbruch mit Computergrafik benötigten, davon konnte ich mich neulich im Academy Museum of Motion Pictures in Los Angeles überzeugen.

Jeder einzelne Frame zeigt die Figur in einem Zustand, der nur leicht von dem im vorherigen Einzelbild abweicht. Bewegt der nächste Frame die Figur in dieselbe Richtung weiter, entsteht bei mehreren Bildern pro Sekunde die Illusion einer animierten Comicfigur.

Damit nun das Programm nicht Dutzende Dateien mit Einzelbildern von der Platte lesen muss, legt man üblicherweise alle Frames als Kacheln auf einem sogenannten Sprite Sheet ab. So braucht die Software nur eine einzige Datei einzulesen. Sie sucht sich alle benötigten Frames heraus, indem sie entsprechend der bekannten Abstände unter Angabe der X/Y-Koordinaten die Kacheln entsprechend ihrer gleichfalls bekannten Höhe und Breite ausschneidet.

Kein Picasso

Künstlerisch Begabte zeichnen sich ihre eigenen Sprites. Wer kein Picasso ist, lädt lieber frei verfügbare Bildchen herunter. Die Abstände der Frames vom Rand des Sprite Sheets sowie voneinander in X- und Y-Richtung lassen sich mit einem Foto-Editor wie Gimp leicht als Pixelwerte ermitteln (Abbildung 5). Das Animationsprogramm (Listing 1) liest später die heruntergeladene PNG-Datei ein, dekodiert die komprimierten Daten und speichert dann die Bildpixel in einer Struktur des Typs »image.Image« aus der Go-Standardbibliothek.

Abbildung 5: Die Abstände zwischen den Einzelbildern dienen zum Ausschneiden. Quelle: topvectors / 123RF.com

Abbildung 5: Die Abstände zwischen den Einzelbildern dienen zum Ausschneiden. Quelle: topvectors / 123RF.com

Listing 1

sprite.go

package main
import (
  "image"
  "image/draw"
  "image/png"
  "os"
)
type Sprite struct {
  xOff, yOff    int
  width, height int
  xPad, yPad    int
  columns       int
  reversed      bool
}
func NewSprite(reversed bool) *Sprite {
  return &Sprite{
    xOff: 313, yOff: 67,
    width: 205, height: 258,
    xPad: 27, yPad: 39,
    columns:  5,
    reversed: reversed,
  }
}
func (s *Sprite) Icons(file string) ([]image.Image, error) {
  icons := []image.Image{}
  img, err := loadPNG(file)
  if err != nil {
    return icons, err
  }
  for i := 0; i < 10; i++ {
    icon := s.extractIcon(img, i)
    if s.reversed {
      icon = flipH(icon)
    }
    icons = append(icons, icon)
  }
  return icons, nil
}
func loadPNG(path string) (image.Image, error) {
  file, err := os.Open(path)
  if err != nil {
    return nil, err
  }
  defer file.Close()
  img, err := png.Decode(file)
  if err != nil {
    return nil, err
  }
  return img, nil
}
func (s *Sprite) extractIcon(sheet image.Image, idx int) image.Image {
  col := idx % s.columns
  row := idx / s.columns
  x := s.xOff + col*(s.width+s.xPad)
  y := s.yOff + row*(s.height+s.yPad)
  iconRect := image.Rect(x, y, x+s.width, y+s.height)
  icon := image.NewRGBA(iconRect)
  draw.Draw(icon, iconRect, sheet, image.Point{x, y}, draw.Src)
  return icon
}
func flipH(src image.Image) image.Image {
  bounds := src.Bounds()
  width := bounds.Dx()
  height := bounds.Dy()
  dst := image.NewRGBA(bounds)
  for y := 0; y < height; y++ {
    for x := 0; x < width; x++ {
      flippedX := bounds.Max.X - 1 - x
      dst.Set(flippedX, bounds.Min.Y+y, src.At(bounds.Min.X+x, bounds.Min.Y+y))
    }
  }
  return dst
}

Ein Beispiel: Der zweite Frame aus der zweiten Reihe trägt die Indexnummer 6, da die Indizes bei null starten und pro Reihe fünf Frames im Sprite liegen. Seine linke obere Ecke liegt auf der X-Koordinate »xOff + width + xPad« sowie auf der Y-Koordinate »yOff + height + yPad«. Der Konstruktor »NewSprite()« ab Zeile 15 in Listing 1 definiert dazu die Koordinaten und Abmessungen der Einzelbilder.

Im Parameter »reversed« gibt der Aufrufer noch ein Flag mit, das anzeigt, ob das extrahierte Icon später nach rechts oder links laufen soll. In letzterem Fall spiegelt die Funktion »flipH()« ab Zeile 61 alle Icons nach dem Einlesen horizontal, stellt sie also seitenverkehrt dar.

Die Funktion »extractIcon()« ab Zeile 51 extrahiert einzelne Icons mit ab 0 laufenden Indexnummern in »idx«. Das Sprite Sheet enthält die zehn Bildchen in zwei Reihen zu je fünf Icons (Abbildung 6). Davon ausgehend rechnet die Funktion zunächst anhand der Indexnummer mittels Integer-Division und Modulo-Operation die Reihe und die Spalte des gesuchten Teilbilds aus. So ist zum Beispiel das Icon mit dem Index 8 das vorletzte in der zweiten Reihe, mit »row=1« und »col=3« (die Indizes beginnen bei 0).

Abbildung 6: Das Sprite Sheet enth&auml;lt die Einzelbilder der Animation in zwei Reihen. Aus Platzgr&uuml;nden sind hier nur acht Einzelbilder zu sehen. Quelle: topvectors / 123RF.com

Abbildung 6: Das Sprite Sheet enthält die Einzelbilder der Animation in zwei Reihen. Aus Platzgründen sind hier nur acht Einzelbilder zu sehen. Quelle: topvectors / 123RF.com

Schrullig

Die in Zeile 58 von Listing 1 verwendete »Draw()«-Funktion aus dem Go-Image-Paket hat eine kleine Schrulle: Nach dem Ausschneiden eines Einzelbilds beginnen dessen Koordinaten nicht notwendigerweise bei (0,0). Stattdessen behält die Variable mit dem Teilbild eine Referenz auf das Gesamtbild und setzt in seinen Koordinaten einen (X,Y)-Offset zur eigentlichen linken oberen Ecke des Icons.

Das muss die Funktion »flipH()« ab Zeile 61 berücksichtigen. Nähme sie beim Spiegeln an, die X- und Y-Koordinaten begännen bei 0, käme ein falscher Ausschnitt heraus. Der richtige Ansatz: Eine Doppelschleife ab Zeile 66 iteriert zuerst gemäß der Einzelbildhöhe über alle Pixelzeilen und dann gemäß der Bildbreite über alle Spalten. Dann vertauscht der Code in jeder Bildzeile die Pixel an gegenüberliegenden X-Werten. Dabei beachtet er die mit »Bounds()« aus dem Originalbild extrahierten X- und Y-Offsets, die nicht notwendigerweise mit den Indizes der For-Schleife übereinstimmen.

Zeichentrick

Die zehn ausgeschnittenen Einzelbilder muss das GUI-Framework Fyne so schnell hintereinander anzeigen, dass die Illusion einer Bewegung entsteht.

Listing 2 definiert dazu ab Zeile 8 die Struktur »Flicker«. Sie speichert die Frames als Array und hält in »Reverse« fest, ob der Sprinter nach rechts oder nach links läuft. Die Funktion »LoadSprite()« lädt die Frames mittels »Icons()« (Listing 1) aus der Datei mit dem Sprite Sheet. Damit Fyne die Frames anzeigen kann, importiert »NewImageFromImage« die Image-Objekte in Fyne. Zeile 28 von Listing 2 hängt zum späteren Gebrauch jeden neuen Frame ans Ende des Arrays »Frames« in der Instanzstruktur an.

Listing 2

flicker.go

package main
import (
  "time"
  "fyne.io/fyne/v2"
  "fyne.io/fyne/v2/canvas"
  "fyne.io/fyne/v2/container"
)
type Flicker struct {
  Frames   []*canvas.Image
  Reversed bool
}
func NewFlicker(reversed bool) *Flicker {
  return &Flicker{
    Reversed: reversed,
  }
}
func (f *Flicker) LoadSprite(spriteFile string) error {
  s := NewSprite(f.Reversed)
  icons, err := s.Icons(spriteFile)
  if err != nil {
    return err
  }
  for i, img := range icons {
    icon := s.extractIcon(img, i)
    canvasImage := canvas.NewImageFromImage(icon)
    canvasImage.FillMode = canvas.ImageFillContain
    canvasImage.SetMinSize(fyne.NewSize(100, 100))
    f.Frames = append(f.Frames, canvasImage)
  }
  return nil
}
func (f *Flicker) Animate() (*fyne.Container, chan float64) {
  ch := make(chan float64)
  con := container.NewMax(f.Frames[0])
  speed := 0.0
  count := 0.0
  go func() {
    for {
      select {
      case speed = <-ch:
        speed = limiter(speed)
      case <-time.After(100 * time.Millisecond):
        count += speed / MaxSpeed * 2
        frame := f.Frames[(int(count) % len(f.Frames))]
        con.RemoveAll()
        con.Add(frame)
        frame.Refresh()
      }
    }
  }()
  return con, ch
}

Der Trickfilm beginnt mit »Animate()« ab Zeile 32 zu laufen. Die Startgeschwindigkeit »speed« des Läufers steht am Anfang auf »0.0«. Sie kann im Laufe des Programms bis auf maximal »100.0« steigen. Die ab Zeile 37 nebenläufig gestartete Goroutine betritt eine Endlosschleife mit Select-Anweisung, die im Normalfall darauf wartet, dass in Zeile 42 der Trickfilm-Timer alle 100 Millisekunden abläuft.

Zeile 43 erhöht entsprechend der eingestellten Geschwindigkeit »speed« im Verhältnis zur Maximalgeschwindigkeit den Zähler »count« dergestalt, dass bei Vollgas die Anzeige gleich zwei Frames vorspult. Das alte Einzelbild wird dem Fyne-Container »con« nun entzogen und stattdessen das neue mit »Add()« untergeschoben. Ein »Refresh()« aktualisiert die Anzeige. Das Ganze erfolgt zehnmal pro Sekunde, sodass eine flüssige Bewegung entsteht.

Läufer läuft

Der Läufer bewegt nicht nur seine Gliedmaßen, sondern wandert in seinem Fyne-Container von links nach rechts (Upload) beziehungsweise in der Gegenrichtung (Download). Listing 3 kapselt den Code zum Voranschubsen des Läufers in objektorientierter Manier.

Listing 3

mover.go

package main
import (
  "time"
  "fyne.io/fyne/v2"
  "fyne.io/fyne/v2/container"
)
type Mover struct {
  Reverse bool
}
func NewMover(reverse bool) *Mover {
  return &Mover{
    Reverse: reverse,
  }
}
func (m *Mover) Animate(obj fyne.CanvasObject) (*fyne.Container, chan float64) {
  con := container.NewWithoutLayout(obj)
  speed := MinSpeed
  ch := make(chan float64)
  direction := 1.0
  if m.Reverse {
    direction = -1.0
  }
  go func() {
    pos := float32(0)
    obj.Hide()
    for {
      select {
      case speed = <-ch:
        speed = limiter(speed)
        if !obj.Visible() {
          if m.Reverse {
            pos = con.Size().Width - obj.Size().Width
          }
          obj.Show()
        }
      case <-time.After(10 * time.Millisecond):
        pos += float32(speed * direction / MaxSpeed)
      }
      if m.Reverse {
        if pos < -obj.Size().Width {
          pos = con.Size().Width
        }
      } else {
        if pos > con.Size().Width {
          pos = -obj.Size().Width
        }
      }
      obj.Move(fyne.NewPos(pos, (con.Size().Height-obj.Size().Height)/2))
      con.Refresh()
    }
  }()
  return con, ch
}
const MaxSpeed = 100.0
const MinSpeed = 0.0
func limiter(speed float64) float64 {
  if speed > MaxSpeed {
    return MaxSpeed
  } else if speed < MinSpeed {
    return MinSpeed
  }
  return speed
}

Der Konstruktor »NewMover« nimmt das Flag »reverse« entgegen, das angibt, ob die nächste Fahrt vorwärts oder rückwärts geht. Die Funktion »Animate()« liefert ähnlich wie in Listing 1 zwei Parameter zurück: einen Fyne-Container, in dem der Avatar sich bewegt, und einen Channel, durch den der Aufrufer im laufenden Betrieb die Geschwindigkeit der Animation beeinflussen darf. Schiebt später der Aufrufer einen neuen Fließkommawert in den Channel, liest ihn eine nebenläufige Goroutine ab Zeile 23 im »case« der Select-Anweisung in Zeile 28 aus dem Channel. Sie setzt die lokale und per Closure weiter bestehende Variable »speed« auf den neuen Wert und treibt den Läufer an.

Läuft der Trickfilm-Timer in Zeile 36 nach 100 Millisekunden ab, erhält die Position »pos« des zu bewegenden Grafikobjekts »obj« einen neuen Wert. Der ergibt sich aus der in der Zwischenzeit mit der Geschwindigkeit »speed« zurückgelegten Strecke und der Laufrichtung in »direction«.

Hinaus und hinein

Was am linken Containerrand passiert, definiert Zeile 40 für den Rückwärtslauf. In diesem Fall steht »pos« weit im Negativen, das bewegte Objekt ist bereits in seiner gesamten Breite über die linke Containergrenze hinausgelaufen. Zeile 41 lässt es deswegen an der rechten Containergrenze langsam wieder erscheinen.

Den umgekehrten Fall im Vorauslauf prüft Zeile 44. Sie lässt das bewegte Objekt am linken Rand wieder auftauchen, sobald es rechts über die Containergrenze gerauscht ist. Dass Fyne negative Koordinaten klaglos verarbeitet und das verschobene Objekt einfach nicht oder nur teilweise darstellt, hilft hier definitiv.

Die Funktion »limiter« ab Zeile 56 sorgt dafür, dass einerseits die Geschwindigkeitsbegrenzung auf »100.0« eingehalten wird und andererseits keine negativen Geschwindigkeiten durch den Channel gelangen. Die Konstanten »MaxSpeed« und »MinSpeed« gelten übrigens nicht nur in Listing 2, sondern in allen fünf Listings, da alle im Paket main operieren.

Radarmessung

Woher weiß nun die GUI, wie schnell die Bits durch die ISP-Leitung flitzen? Wie eingangs erwähnt, läuft dazu auf der Firewall ein Vnstat-Prozess, der eifrig misst und mitschreibt. Für den aktuellen Wert muss sich die GUI per SSH auf der Firewall einloggen und den Vnstat-Befehl absetzen. Listing 4 übernimmt diese Aufgabe in Go.

Listing 4

vnstat.go

package main
import (
  "fmt"
  "github.com/dustin/go-humanize"
  "math"
  "os/exec"
  "regexp"
)
func vnstat() (float64, float64, error) {
  rx := float64(0)
  tx := float64(0)
  cmd := exec.Command("ssh", "-p", "8022", "admin@192.168.0.1", "vnstat", "-i",
    "igb0", "-tr", "2")
  output, err := cmd.Output()
  if err != nil {
    return rx, tx, err
  }
  rateRex := regexp.MustCompile(`(?m)^\s+([rt]x)\s+([\d.]+)\s+(\S+)`)
  matches := rateRex.FindAllStringSubmatch(string(output), 2)
  for _, match := range matches {
    if match[1] == "rx" {
      rx, err = toBits(match[2], match[3])
    } else if match[1] == "tx" {
      tx, err = toBits(match[2], match[3])
    } else {
      return rx, tx, fmt.Errorf("Unknown entry %s", match[1])
    }
    if err != nil {
      return rx, tx, err
    }
  }
  return rx, tx, nil
}
func toBits(str string, unit string) (float64, error) {
  s := str + string(unit[0])
  i, err := humanize.ParseBytes(s)
  return float64(i), err
}
func toBitRate(bps float64) string {
  return humanize.Bytes(uint64(bps)) + "it/sec"
}
func speedFromRate(x float64) float64 {
  return math.Sqrt(x / 1000.0)
}

Zeile 12 zeigt das Kommando, das sich mittels SSH mit der IP-Adresse der Firewall auf dem eingestellten Port verbindet. Der reguläre Ausdruck ab Zeile 20 fieselt anschließend die eingangs gezeigte Rückgabe des Tools (Abbildung 1) auseinander. Er extrahiert die zwei Werte »rx« und »tx«, die jeweils als Fließkommazahl mit Einheit vorliegen, also zum Beispiel »1.3 Mbit/s«.

Die Funktion »toBits()« ab Zeile 36 macht daraus unter Verwendung des Pakets humanize von Github maschinenlesbare Bits pro Sekunde. Umgekehrt wandelt »toBitRate()« ab Zeile 41 einen Bitwert wieder in einen menschenlesbaren String zurück, den später die GUI zur Anzeige nutzt.

Skalierung

Der Läufer bewegt sich mit einer virtuellen Geschwindigkeit zwischen 0 (Stillstand) und 100 (voller Spurt) vorwärts, abhängig davon, wie viele Bits pro Sekunde durch die Leitung flitzen.

Allerdings bewegt sich die Bitrate im laufenden Betrieb durch mehrere Dimensionen. Ist kaum etwas los, dümpelt sie vielleicht bei 1 kbit/s dahin, bei Volllast fließt mit 10 Mbit/s schon mal das 10 000-fache. Damit der Läufer im Leerlaufbetrieb nicht ganz stehen bleibt, soll er bei 1 kbit/s mit Tempo 1 bummeln. Bei der Volllast von 10 Mbit/s spurtet er mit Tempo 100 auf der Y-Achse (Abbildung 7).

Abbildung 7: L&auml;ufergeschwindigkeit je nach Bit-Durchsatz.

Abbildung 7: Läufergeschwindigkeit je nach Bit-Durchsatz.

Eine passende Mapping-Funktion für derartige Zahlenbereiche, die sich über mehrere Dimensionen erstrecken, lässt sich nur schwer linear beschreiben – da muss normalerweise ein Logarithmus ran. Der dämpft allerdings das Drama des athletischen Wettkampfs: Wenn der Läufer bei einer 1000-fachen Bitrate nur vier Mal schneller läuft, sieht das wenig glaubwürdig aus.

Stattdessen nutzt Listing 4 in Zeile 45 die Quadratwurzelfunktion »Sqrt()« aus dem Mathe-Paket von Go, die dem Lauftempo größere Schwankungen verschreibt. Teilt Zeile 45 den X-Wert durch tausend und zieht dann die Quadratwurzel, deckt die Konvertierung die gesuchte Verteilung von Abbildung 7 relativ gut ab.

Showtime!

Nun muss das Hauptprogramm (Listing 5) nur noch alle Komponenten vereinen und auf den Bildschirm bringen. Zuvor kommt in der Hilfsfunktion »mkPanel()« ab Zeile 13 zusammen, was für eine Verbindungsrichtung zusammengehört: das Daumenkino eines rasenden Läufers (»NewFlicker()«), dessen Animationscontainer »avaCon« und der Update-Channel »avaCh«.

Den Container packt »NewMover()« in ein Rechteck, das sich über die Funktion »Animate()« im Takt der Bitrate in Verbindungsrichtung bewegt. Zu guter Letzt spendiert Zeile 33 dem Panel noch eine Digitalanzeige der Up- und Download-Geschwindigkeit als Text im »meter«-Widget.

Listing 5

marathon.go

package main
import (
  "os"
  "time"
  "fyne.io/fyne/v2"
  "fyne.io/fyne/v2/app"
  "fyne.io/fyne/v2/canvas"
  "fyne.io/fyne/v2/container"
  "fyne.io/fyne/v2/theme"
  "fyne.io/fyne/v2/widget"
)
const SpriteFile = "sprite.png"
func mkPanel(isDownload bool) (*fyne.Container, func(float64)) {
  ava := NewFlicker(isDownload)
  err := ava.LoadSprite(SpriteFile)
  if err != nil {
    panic(err)
  }
  avaCon, avaCh := ava.Animate()
  avaCon.Resize(fyne.NewSize(100, 100))
  mv := NewMover(isDownload)
  mvCon, mvCh := mv.Animate(avaCon)
  meter := widget.NewLabel("")
  throttle := func(v float64) {
    meter.Text = toBitRate(v)
    meter.Refresh()
    avaCh <- speedFromRate(v)
    mvCh <- speedFromRate(v)
  }
  panel := container.NewVBox(meter, mvCon)
  return panel, throttle
}
func main() {
  myApp := app.New()
  myWindow := myApp.NewWindow("Bandwidth Marathon")
  down, downUpdate := mkPanel(true)
  up, upUpdate := mkPanel(false)
  border := canvas.NewRectangle(theme.DisabledColor())
  dual := container.NewVBox(down, up)
  all := container.NewMax(border, dual)
  myWindow.SetContent(all)
  myWindow.Resize(fyne.NewSize(float32(800), float32(300)))
  go func() {
    for {
      rx, tx, err := vnstat()
      if err != nil {
        panic(err)
      }
      upUpdate(tx)
      downUpdate(rx)
      time.Sleep(3 * time.Second)
    }
  }()
  myWindow.Canvas().SetOnTypedKey(
    func(ev *fyne.KeyEvent) {
      os.Exit(0)
    })
  myWindow.ShowAndRun()
}

Pumpt man in Go einen Wert in einen Channel, darf ihn immer nur ein Empfänger abholen; lauschen mehrere, bekommt ihn derjenige, der am schnellsten zugreift. Ändert sich aber die vom Tool gemessene Bitrate, wollen zwei Channels bedient werden: der des Daumenkinos und der des bewegten Rechtecks. Die Lösung bringt die ab Zeile 24 definierte Funktion »throttle()«, die der Code innerhalb der Funktion »mkPanel()« definiert.

Am Ende gelangt die Funktion wie ein ganz normaler Wert an den Aufrufer zurück. Der kann sie später aufrufen und ihr den neuen Messwert überreichen. Unter der Haube gibt ihn die Funktion dann an die zwei Channels weiter und frischt gleich noch die Digitalanzeige in »meter« auf. Praktisch, wenn eine Programmiersprache Funktionen wie ganz normale Variablen behandelt.

Das Hauptprogramm »main()« ab Zeile 33 muss nun noch eine neue Fyne-Applikation aufspannen und dem Hauptfenster die beiden neu erschaffenen Panels zum Layouten überreichen. Ein Container vom Typ »VBox« ordnet sie übereinander an. Die nebenläufige Goroutine ab Zeile 43 tritt anschließend in eine Endlosschleife ein, die mit »vnstat()« die neuesten Messwerte »rx« und »tx« von der Firewall abholt und sie den beiden Funktionen »upUpdate()« und »downUpdate()« für die jeweilige Übertragungsrichtung zur Anzeige übergibt. Nach drei Sekunden Pause geht es weiter in die nächste Runde.

Bevor »ShowAndRun()« in Zeile 58 auf Nimmerwiedersehen in die Haupt-Event-Schleife des Fyne-Frameworks eintritt, sorgt der Callback »SetOnTypedKey()« in Zeile 54 noch dafür, dass die GUI-Applikation sich sauber zusammenfaltet, falls der User irgendeine Taste drückt.

Aus allen fünf Listings, die es im selben Verzeichnis erwartet, baut die bekannte Befehlssequenz aus Listing 6 das Binary »marathon«. Vor dem ersten Aufruf muss es per Public Key einen SSH-Zugang auf den Router mit installierten und laufenden Vnstat erhalten – dann startet der Sprint. (uba)

Listing 6

build.sh

$ go mod init marathon
$ go mod tidy
$ go build

Der Autor

Michael Schilli arbeitet als Software Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mailto:mschilli@perlmeister.com beantwortet er gern Fragen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 8 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben