├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go ├── odroidhc4-display.service ├── when-new.jpeg └── with-burn-in.jpeg /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | odroidhc4-display 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Chandler Swift 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # odroidhc4-display 2 | 3 | **Warning!** These displays do suffer from burn-in over time. I'm unable to find 4 | a spec for the rated lifetime of these, but it was noticeable after a few months 5 | of continuous use, and has grown pretty severe over a roughly 16 months of use. 6 | See images below. 7 | 8 | odroidhc4-display is a package to provide basic helpful output for the 9 | [OLED display on the ODROID-HC4](https://wiki.odroid.com/odroid-hc4/application_note/oled). 10 | It's intended to be a no-dependencies-required download-and-run alternative for 11 | the `odroid-homecloud-display` (the base install requires pillow, which has some 12 | native dependencies; the install of the examples requires pygame and all its 13 | dependencies as well). 14 | 15 | ### Installing 16 | ```sh 17 | # from source 18 | git clone git@github.com:ChandlerSwift/odroidhc4-display.git 19 | cd odroidhc4-display 20 | go build # if not built elsewhere; you can also cross-compile with GOOS=linux GOARCH=arm64 21 | sudo cp odroidhc4-display /usr/bin/ 22 | sudo cp odroidhc4-display.service /etc/systemd/system/ 23 | sudo systemctl daemon-reload 24 | sudo systemctl enable --now odroidhc4-display 25 | 26 | # or prebuilt 27 | sudo curl -Lo /usr/bin/odroidhc4-display $(curl -s https://api.github.com/repos/ChandlerSwift/odroidhc4-display/releases/latest | jq -r ".assets[0].browser_download_url") 28 | sudo chmod +x /usr/bin/odroidhc4-display 29 | sudo curl -Lo /etc/systemd/system/odroidhc4-display.service https://raw.githubusercontent.com/ChandlerSwift/odroidhc4-display/main/odroidhc4-display.service 30 | sudo systemctl daemon-reload 31 | sudo systemctl enable --now odroidhc4-display 32 | ``` 33 | ### Sample output 34 | Output includes: 35 | * Date and time 36 | * IP address, if any 37 | * CPU usage, 1 and 5 minute load averages 38 | * Memory usage 39 | * Disk usage of largest attached partition (this is assumed to be the disk pool) 40 | 41 | ``` 42 | May 2 3:47:42 PM 43 | IP: 192.168.1.168 44 | CPU: 0% (0.1 0.1) 45 | MEM: 175M/3.7G 46 | /mnt: 957G/3.9T 47 | ``` 48 | 49 | ![sample output](when-new.jpeg) 50 | 51 | Displaying a fully white background after about 16 months of use: 52 | ![with burn-in](with-burn-in.jpeg) 53 | 54 |
55 | Go code to display a fully-white screen 56 | 57 | ```go 58 | package main 59 | 60 | import ( 61 | "image" 62 | "log" 63 | 64 | "periph.io/x/periph/conn/i2c/i2creg" 65 | "periph.io/x/periph/devices/ssd1306" 66 | "periph.io/x/periph/devices/ssd1306/image1bit" 67 | "periph.io/x/periph/host" 68 | ) 69 | 70 | func main() { 71 | // Make sure periph is initialized. 72 | if _, err := host.Init(); err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | // Use i2creg I²C bus registry to find the first available I²C bus. 77 | b, err := i2creg.Open("") 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | defer b.Close() 82 | 83 | dev, err := ssd1306.NewI2C(b, &ssd1306.Opts{ 84 | W: 128, 85 | H: 64, 86 | Rotated: true, 87 | Sequential: false, 88 | SwapTopBottom: false, 89 | }) 90 | if err != nil { 91 | log.Fatalf("failed to initialize ssd1306: %v", err) 92 | } 93 | 94 | img := &image.Uniform{image1bit.On} 95 | 96 | if err := dev.Draw(dev.Bounds(), img, image.Point{}); err != nil { 97 | log.Fatal(err) 98 | } 99 | 100 | } 101 | ``` 102 | 103 |
104 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chandlerswift/odroidhc4-display 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/shirou/gopsutil/v3 v3.21.4 7 | golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb 8 | periph.io/x/periph v3.6.7+incompatible 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= 2 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= 6 | github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/shirou/gopsutil/v3 v3.21.4 h1:XB/+p+kVnyYLuPHCfa99lxz2aJyvVhnyd+FxZqH/k7M= 10 | github.com/shirou/gopsutil/v3 v3.21.4/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0cv/18ZqVczw= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 13 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/tklauser/go-sysconf v0.3.4 h1:HT8SVixZd3IzLdfs/xlpq0jeSfTX57g1v6wB1EuzV7M= 15 | github.com/tklauser/go-sysconf v0.3.4/go.mod h1:Cl2c8ZRWfHD5IrfHo9VN+FX9kCFjIOyVklgXycLB6ek= 16 | github.com/tklauser/numcpus v0.2.1 h1:ct88eFm+Q7m2ZfXJdan1xYoXKlmwsfP+k88q05KvlZc= 17 | github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1gBkr4QyP8= 18 | golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= 19 | golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 20 | golang.org/x/sys v0.0.0-20210217105451-b926d437f341 h1:2/QtM1mL37YmcsT8HaDNHDgTqqFVw+zr8UzMiBVLzYU= 21 | golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 22 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 25 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | periph.io/x/periph v3.6.7+incompatible h1:ZfRdHbcxVekgSJZmxp3873YpxNdWs6wg7waDCF7GB18= 27 | periph.io/x/periph v3.6.7+incompatible/go.mod h1:EWr+FCIU2dBWz5/wSWeiIUJTriYv9v2j2ENBmgYyy7Y= 28 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "log" 7 | "net" 8 | "time" 9 | 10 | "golang.org/x/image/font" 11 | "golang.org/x/image/font/basicfont" 12 | "golang.org/x/image/math/fixed" 13 | 14 | "github.com/shirou/gopsutil/v3/cpu" 15 | "github.com/shirou/gopsutil/v3/disk" 16 | "github.com/shirou/gopsutil/v3/load" 17 | "github.com/shirou/gopsutil/v3/mem" 18 | "periph.io/x/periph/conn/i2c/i2creg" 19 | "periph.io/x/periph/devices/ssd1306" 20 | "periph.io/x/periph/devices/ssd1306/image1bit" 21 | "periph.io/x/periph/host" 22 | ) 23 | 24 | // based on https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/ 25 | func formatSize(size uint64, unit uint64) string { 26 | if size < unit { 27 | return fmt.Sprintf("%dB", size) 28 | } 29 | div, suffix := unit, 0 30 | for n := size / unit; n >= unit; n /= unit { 31 | div *= unit 32 | suffix++ 33 | } 34 | value := float64(size) / float64(div) 35 | var fmtStr string 36 | if value >= 100 { 37 | fmtStr = "%.f%c" 38 | } else { 39 | fmtStr = "%.1f%c" 40 | } 41 | 42 | return fmt.Sprintf(fmtStr, 43 | value, "kMGTP"[suffix]) 44 | } 45 | 46 | // TODO: error handling 47 | func getMemString() string { 48 | v, _ := mem.VirtualMemory() 49 | 50 | return fmt.Sprintf("MEM: %v/%v", formatSize(v.Used, 1024), formatSize(v.Total, 1024)) 51 | } 52 | 53 | func getCPUString() string { 54 | v, _ := cpu.Percent(0, false) 55 | l, _ := load.Avg() 56 | // Unfortunately, the screen just isn't wide enough to include Load15 57 | return fmt.Sprintf("CPU: %.f%% (%.1f %.1f)", v[0], l.Load1, l.Load5) 58 | } 59 | 60 | // getHDDString returns data about the biggest mounted partition. 61 | func getHDDString() string { 62 | partitions, _ := disk.Partitions(false) 63 | biggestDiskSize := uint64(0) 64 | biggestDiskUsed := uint64(0) 65 | biggestDiskName := "" 66 | for _, partition := range partitions { 67 | d, _ := disk.Usage(partition.Mountpoint) 68 | if d.Total > biggestDiskSize { 69 | biggestDiskName = partition.Mountpoint 70 | biggestDiskUsed = d.Used 71 | biggestDiskSize = d.Total 72 | } 73 | } 74 | return fmt.Sprintf("%v: %v/%v", biggestDiskName, formatSize(biggestDiskUsed, 1000), formatSize(biggestDiskSize, 1000)) 75 | } 76 | 77 | func getIPAddrString() string { 78 | // https://stackoverflow.com/a/37382208/3814663 79 | // Note that since this is UDP, no connection is actually established. 80 | conn, err := net.Dial("udp", "8.8.8.8:80") 81 | if err != nil { 82 | return "IP: Network down" 83 | } 84 | defer conn.Close() 85 | 86 | localAddr := conn.LocalAddr().(*net.UDPAddr) 87 | 88 | return fmt.Sprintf("IP: %v", localAddr.IP) 89 | } 90 | 91 | func main() { 92 | // Make sure periph is initialized. 93 | if _, err := host.Init(); err != nil { 94 | log.Fatal(err) 95 | } 96 | 97 | // Use i2creg I²C bus registry to find the first available I²C bus. 98 | b, err := i2creg.Open("") 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | defer b.Close() 103 | 104 | dev, err := ssd1306.NewI2C(b, &ssd1306.Opts{ 105 | W: 128, 106 | H: 64, 107 | Rotated: true, 108 | Sequential: false, 109 | SwapTopBottom: false, 110 | }) 111 | if err != nil { 112 | log.Fatalf("failed to initialize ssd1306: %v", err) 113 | } 114 | 115 | f := basicfont.Face7x13 116 | drawer := font.Drawer{ 117 | Src: &image.Uniform{image1bit.On}, 118 | Face: f, 119 | Dot: fixed.P(0, f.Height), 120 | } 121 | 122 | ticker := time.NewTicker(time.Second) 123 | defer ticker.Stop() 124 | for { 125 | t := <-ticker.C 126 | 127 | lines := []string{ 128 | t.Format("Jan 2 3:04:05 PM"), 129 | getIPAddrString(), 130 | getCPUString(), 131 | getMemString(), 132 | getHDDString(), 133 | } 134 | 135 | img := image1bit.NewVerticalLSB(dev.Bounds()) // reset canvas per frame 136 | drawer.Dst = img 137 | for i, s := range lines { 138 | drawer.Dot = fixed.P(0, (f.Height-1)*(i+1)) 139 | drawer.DrawString(s) 140 | } 141 | 142 | if err := dev.Draw(dev.Bounds(), img, image.Point{}); err != nil { 143 | log.Fatal(err) 144 | } 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /odroidhc4-display.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ODROID-HC4 Display daemon 3 | [Service] 4 | ExecStart=/usr/bin/odroidhc4-display 5 | [Install] 6 | WantedBy=multi-user.target 7 | -------------------------------------------------------------------------------- /when-new.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChandlerSwift/odroidhc4-display/342497215001984878e9ded11c4cfc7d6f6d6d09/when-new.jpeg -------------------------------------------------------------------------------- /with-burn-in.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChandlerSwift/odroidhc4-display/342497215001984878e9ded11c4cfc7d6f6d6d09/with-burn-in.jpeg --------------------------------------------------------------------------------