init
fce8d181
5 file(s) · +376 −0
| 1 | + | /pi-widget |
|
| 2 | + | /Makefile |
| 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 | + | ) |
| 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= |
| 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> |
| 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 | + | } |