init fce8d181
Steve · 2024-08-25 15:06 5 file(s) · +376 −0
.gitignore (added) +2 −0
1 +
/pi-widget
2 +
/Makefile
go.mod (added) +12 −0
1 +
module github.com/stevedylandev/pi-widget
2 +
3 +
go 1.22.6
4 +
5 +
require (
6 +
	github.com/go-ole/go-ole v1.2.6 // indirect
7 +
	github.com/shirou/gopsutil v3.21.11+incompatible // indirect
8 +
	github.com/tklauser/go-sysconf v0.3.14 // indirect
9 +
	github.com/tklauser/numcpus v0.8.0 // indirect
10 +
	github.com/yusufpapurcu/wmi v1.2.4 // indirect
11 +
	golang.org/x/sys v0.24.0 // indirect
12 +
)
go.sum (added) +13 −0
1 +
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
2 +
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
3 +
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
4 +
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
5 +
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
6 +
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
7 +
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
8 +
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
9 +
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
10 +
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
11 +
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
12 +
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
13 +
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
index.html (added) +174 −0
1 +
<!doctype html>
2 +
<html>
3 +
  <head>
4 +
    <title>Steve's Pi</title>
5 +
    <meta charset="utf-8" />
6 +
    <meta
7 +
      name="description"
8 +
      content="A peek into Steve's Raspberry Pi and the services it runs"
9 +
    />
10 +
    <meta name="viewport" content="width=device-width, initial-scale=1" />
11 +
12 +
    <meta property="og:type" content="website" />
13 +
    <meta property="og:title" content="Steve's Pi" />
14 +
    <meta
15 +
      property="og:description"
16 +
      content="A peek into Steve's Raspberry Pi and the services it runs"
17 +
    />
18 +
    <meta property="og:url" content="https://pi.stevedylan.dev" />
19 +
    <meta property="og:site_name" content="Steve's Pi" />
20 +
    <meta
21 +
      property="og:image"
22 +
      content="https://stevedylan.dev/social-card.png"
23 +
    />
24 +
    <meta property="og:image:width" content="1200" />
25 +
    <meta property="og:image:height" content="630" />
26 +
27 +
    <link rel="icon" href="https://stevedylan.dev/favicon.ico" sizes="any" />
28 +
    <link
29 +
      rel="icon"
30 +
      href="https://stevedylan.dev/icon.svg"
31 +
      type="image/svg+xml"
32 +
    />
33 +
    <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
34 +
    <style>
35 +
      @font-face {
36 +
          font-family: 'CommitMono';
37 +
          src: url('https://stevedylan.dev/CommitMono-400-Regular.otf') format('opentype'),
38 +
          font-weight: normal;
39 +
          font-style: normal;
40 +
      }
41 +
         html {
42 +
             padding: 0;
43 +
             margin: 0 1rem 0 1rem;
44 +
             box-sizing: border-box;
45 +
             background: #121212;
46 +
             color: #C1C1C1;
47 +
             font-family: 'CommitMono', sans-serif;
48 +
         }
49 +
         body {
50 +
             display: flex;
51 +
             justify-content: center;
52 +
             align-items: center;
53 +
             flex-direction: column;
54 +
             min-height: 90vh;
55 +
             max-width: 500px;
56 +
             margin: auto;
57 +
         }
58 +
         .stats-container {
59 +
         	display: flex;
60 +
          	flex-direction: column;
61 +
           	justify-content: flex-start;
62 +
         }
63 +
         .ipfs-div {
64 +
         	display: flex;
65 +
             justify-content: flex-start;
66 +
             align-items: center;
67 +
          	gap: 0.5rem;
68 +
         }
69 +
         p {
70 +
         	padding: 0;
71 +
          	margin: 0;
72 +
         }
73 +
         a {
74 +
             color:#C1C1C1;
75 +
         }
76 +
    </style>
77 +
  </head>
78 +
  <body>
79 +
    <div class="stats-container">
80 +
      <h1>Steve's Pi</h1>
81 +
      <p>
82 +
        Welcome to a live feed of my Raspberry Pi! It sits on my desk and runs
83 +
        multiple small services such as
84 +
        <a href="https://ipfs.io" target="_blank">IPFS</a> and
85 +
        <a href="https://radicle.xyz" target="_blank">Radicle</a>.
86 +
      </p>
87 +
      <div class="ipfs-div">
88 +
        <svg
89 +
          xmlns="http://www.w3.org/2000/svg"
90 +
          width="24"
91 +
          height="24"
92 +
          viewBox="0 0 32 32"
93 +
        >
94 +
          <path
95 +
            fill="#888888"
96 +
            d="m28.504 8.136l-12-7a1 1 0 0 0-1.008 0l-12 7A1 1 0 0 0 3 9v14a1 1 0 0 0 .496.864l12 7a1 1 0 0 0 1.008 0l12-7A1 1 0 0 0 29 23V9a1 1 0 0 0-.496-.864M16 3.158L26.016 9L16 14.842L5.984 9ZM5 10.74l10 5.833V28.26L5 22.426Zm12 17.52V16.574l10-5.833v11.685Z"
97 +
          />
98 +
        </svg>
99 +
        <h3>IPFS Node</h3>
100 +
      </div>
101 +
      <p>RepoSize: <span id="repoSize">-</span></p>
102 +
      <p>StorageMax: <span id="storageMax">-</span></p>
103 +
      <p>Objects: <span id="objects">-</span></p>
104 +
      <p>Bandwidth In: <span id="rateIn">-</span></p>
105 +
      <p>Bandwidth Out: <span id="rateOut">-</span></p>
106 +
      <p>Total Data In: <span id="totalIn">-</span></p>
107 +
      <p>Total Data Out: <span id="totalOut">-</span></p>
108 +
    </div>
109 +
    <script>
110 +
      function formatBytes(bytes, decimals = 2) {
111 +
        if (bytes === 0) return "0 Bytes";
112 +
113 +
        const k = 1024;
114 +
        const dm = decimals < 0 ? 0 : decimals;
115 +
        const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
116 +
117 +
        const i = Math.floor(Math.log(bytes) / Math.log(k));
118 +
119 +
        return (
120 +
          parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]
121 +
        );
122 +
      }
123 +
124 +
      function formatBitrate(bits) {
125 +
        if (bits < 1000) {
126 +
          return bits.toFixed(2) + " bps";
127 +
        } else if (bits < 1000000) {
128 +
          return (bits / 1000).toFixed(2) + " Kbps";
129 +
        } else {
130 +
          return (bits / 1000000).toFixed(2) + " Mbps";
131 +
        }
132 +
      }
133 +
134 +
      const evtSource = new EventSource("/events");
135 +
136 +
      evtSource.onopen = function (event) {
137 +
        console.log("SSE connection opened");
138 +
      };
139 +
140 +
      evtSource.onerror = function (event) {
141 +
        console.error("SSE connection error:", event);
142 +
      };
143 +
144 +
      evtSource.onmessage = function (event) {
145 +
        console.log("Received data:", event.data);
146 +
        try {
147 +
          const data = JSON.parse(event.data);
148 +
          document.getElementById("repoSize").textContent = formatBytes(
149 +
            data.RepoSize,
150 +
          );
151 +
          document.getElementById("storageMax").textContent = formatBytes(
152 +
            data.StorageMax,
153 +
          );
154 +
          document.getElementById("objects").textContent =
155 +
            data.NumObjects.toLocaleString();
156 +
          document.getElementById("rateIn").textContent = formatBitrate(
157 +
            data.RateIn,
158 +
          );
159 +
          document.getElementById("rateOut").textContent = formatBitrate(
160 +
            data.RateOut,
161 +
          );
162 +
          document.getElementById("totalIn").textContent = formatBytes(
163 +
            data.TotalIn,
164 +
          );
165 +
          document.getElementById("totalOut").textContent = formatBytes(
166 +
            data.TotalOut,
167 +
          );
168 +
        } catch (error) {
169 +
          console.error("Error parsing or updating data:", error);
170 +
        }
171 +
      };
172 +
    </script>
173 +
  </body>
174 +
</html>
main.go (added) +175 −0
1 +
package main
2 +
3 +
import (
4 +
	_ "embed"
5 +
	"encoding/json"
6 +
	"fmt"
7 +
	"io/ioutil"
8 +
	"log"
9 +
	"net/http"
10 +
	"time"
11 +
12 +
	"github.com/shirou/gopsutil/cpu"
13 +
	"github.com/shirou/gopsutil/mem"
14 +
)
15 +
16 +
type SystemStats struct {
17 +
	CPUUsage    float64 `json:"cpuUsage"`
18 +
	MemoryUsage float64 `json:"memoryUsage"`
19 +
}
20 +
21 +
type IPFSRepoStats struct {
22 +
	RepoSize   int64  `json:"RepoSize"`
23 +
	StorageMax int64  `json:"StorageMax"`
24 +
	NumObjects int    `json:"NumObjects"`
25 +
	RepoPath   string `json:"RepoPath"`
26 +
	Version    string `json:"Version"`
27 +
}
28 +
29 +
type BandwidthStats struct {
30 +
	RateIn   float64 `json:"RateIn"`
31 +
	RateOut  float64 `json:"RateOut"`
32 +
	TotalIn  int64   `json:"TotalIn"`
33 +
	TotalOut int64   `json:"TotalOut"`
34 +
}
35 +
36 +
type CombinedStats struct {
37 +
	IPFSRepoStats
38 +
	BandwidthStats
39 +
}
40 +
41 +
func main() {
42 +
	http.HandleFunc("/", serveHTML)
43 +
	http.HandleFunc("/events", handleSSE)
44 +
	fmt.Println("Server is running on http://localhost:4321")
45 +
	log.Fatal(http.ListenAndServe(":4321", nil))
46 +
}
47 +
48 +
//go:embed index.html
49 +
var indexHTML string
50 +
51 +
func serveHTML(w http.ResponseWriter, r *http.Request) {
52 +
	w.Header().Set("Content-Type", "text/html")
53 +
	w.Write([]byte(indexHTML))
54 +
}
55 +
56 +
func handleSSE(w http.ResponseWriter, r *http.Request) {
57 +
	w.Header().Set("Content-Type", "text/event-stream")
58 +
	w.Header().Set("Cache-Control", "no-cache")
59 +
	w.Header().Set("Connection", "keep-alive")
60 +
61 +
	log.Println("SSE connection established")
62 +
63 +
	for {
64 +
		combinedStats, err := getIpfsStats()
65 +
		if err != nil {
66 +
			log.Printf("Error getting IPFS Stats: %v", err)
67 +
			time.Sleep(1 * time.Second)
68 +
			continue
69 +
		}
70 +
71 +
		data, err := json.Marshal(combinedStats)
72 +
		if err != nil {
73 +
			log.Printf("Error marshaling IPFS stats: %v", err)
74 +
			time.Sleep(1 * time.Second)
75 +
			continue
76 +
		}
77 +
78 +
		_, err = fmt.Fprintf(w, "data: %s\n\n", data)
79 +
		if err != nil {
80 +
			log.Printf("Error writing to response: %v", err)
81 +
			return
82 +
		}
83 +
		w.(http.Flusher).Flush()
84 +
85 +
		time.Sleep(1 * time.Second)
86 +
	}
87 +
}
88 +
89 +
func getIpfsStats() (CombinedStats, error) {
90 +
	repoStats, err := getIpfsRepoStat()
91 +
	if err != nil {
92 +
		return CombinedStats{}, err
93 +
	}
94 +
95 +
	bwStats, err := getBandwidthStats()
96 +
	if err != nil {
97 +
		return CombinedStats{}, err
98 +
	}
99 +
100 +
	return CombinedStats{
101 +
		IPFSRepoStats:  repoStats,
102 +
		BandwidthStats: bwStats,
103 +
	}, nil
104 +
}
105 +
106 +
func getIpfsRepoStat() (IPFSRepoStats, error) {
107 +
	client := &http.Client{}
108 +
	req, err := http.NewRequest("POST", "http://127.0.0.1:5001/api/v0/repo/stat", nil)
109 +
	if err != nil {
110 +
		return IPFSRepoStats{}, fmt.Errorf("error creating request: %v", err)
111 +
	}
112 +
113 +
	resp, err := client.Do(req)
114 +
	if err != nil {
115 +
		return IPFSRepoStats{}, fmt.Errorf("error fetching IPFS Repo Stats: %v", err)
116 +
	}
117 +
	defer resp.Body.Close()
118 +
119 +
	body, err := ioutil.ReadAll(resp.Body)
120 +
	if err != nil {
121 +
		return IPFSRepoStats{}, fmt.Errorf("error reading response body: %v", err)
122 +
	}
123 +
124 +
	var stats IPFSRepoStats
125 +
	err = json.Unmarshal(body, &stats)
126 +
	if err != nil {
127 +
		return IPFSRepoStats{}, fmt.Errorf("error unmarshaling JSON: %v", err)
128 +
	}
129 +
130 +
	return stats, nil
131 +
}
132 +
133 +
func getBandwidthStats() (BandwidthStats, error) {
134 +
	client := &http.Client{}
135 +
	req, err := http.NewRequest("POST", "http://127.0.0.1:5001/api/v0/stats/bw", nil)
136 +
	if err != nil {
137 +
		return BandwidthStats{}, fmt.Errorf("error creating request: %v", err)
138 +
	}
139 +
140 +
	resp, err := client.Do(req)
141 +
	if err != nil {
142 +
		return BandwidthStats{}, fmt.Errorf("error fetching Bandwidth Stats: %v", err)
143 +
	}
144 +
	defer resp.Body.Close()
145 +
146 +
	body, err := ioutil.ReadAll(resp.Body)
147 +
	if err != nil {
148 +
		return BandwidthStats{}, fmt.Errorf("error reading response body: %v", err)
149 +
	}
150 +
151 +
	var stats BandwidthStats
152 +
	err = json.Unmarshal(body, &stats)
153 +
	if err != nil {
154 +
		return BandwidthStats{}, fmt.Errorf("error unmarshaling JSON: %v", err)
155 +
	}
156 +
157 +
	return stats, nil
158 +
}
159 +
160 +
func getSystemStats() (SystemStats, error) {
161 +
	v, err := mem.VirtualMemory()
162 +
	if err != nil {
163 +
		return SystemStats{}, err
164 +
	}
165 +
166 +
	c, err := cpu.Percent(0, false)
167 +
	if err != nil {
168 +
		return SystemStats{}, err
169 +
	}
170 +
171 +
	return SystemStats{
172 +
		CPUUsage:    c[0],
173 +
		MemoryUsage: v.UsedPercent,
174 +
	}, nil
175 +
}