diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 016cfec..f9c734e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -81,7 +81,7 @@ jobs: make BINARY_NAME=display1306-${{ matrix.suffix }}${{ matrix.ext }} build - name: Upload binary - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: display1306-${{ matrix.suffix }} path: display1306-${{ matrix.suffix }}${{ matrix.ext }} diff --git a/Makefile b/Makefile index d97b83a..84fe3b1 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ BINARY_NAME ?= display1306 # Build the display1306 binary build: - go build -C v2 -o ../$(BINARY_NAME) ./cmd/display1306 + go build -o ../$(BINARY_NAME) ./cmd/display1306 # Run tests test: diff --git a/v2/cmd/display1306/main.go b/cmd/display1306/main.go similarity index 100% rename from v2/cmd/display1306/main.go rename to cmd/display1306/main.go diff --git a/display/display.go b/display/display.go index 9b5aeda..7ac5807 100644 --- a/display/display.go +++ b/display/display.go @@ -92,14 +92,22 @@ func (d *Display) Close() error { return nil } -func (d *Display) Clear() error { +func (d *Display) ClearLines() error { if !d.initialized { return fmt.Errorf("driver has not been initialized") } for i := range d.buffer { d.buffer[i] = "" } - return d.Update() + return nil +} + +func (d *Display) ClearScreen() error { + img := image1bit.NewVerticalLSB(d.driver.Bounds()) + if err := d.driver.Draw(d.driver.Bounds(), img, image.Point{}); err != nil { + return fmt.Errorf("failed to draw on display: %w", err) + } + return nil } func (d *Display) PrintLine(line uint, text string) error { @@ -144,7 +152,7 @@ func (d *Display) Update() error { } for i, textLine := range d.buffer { - screen.Dot = fixed.P(0, d.lineHeight*(1+i) - d.font.Metrics().Descent.Round()) + screen.Dot = fixed.P(0, d.lineHeight*(1+i)-d.font.Metrics().Descent.Round()) screen.DrawString(textLine) } if err := d.driver.Draw(d.driver.Bounds(), img, image.Point{}); err != nil { diff --git a/display/display_test.go b/display/display_test.go index 33bba8d..f562f7b 100644 --- a/display/display_test.go +++ b/display/display_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/larsks/display1306/display/fakedriver" + "github.com/larsks/display1306/v2/display/fakedriver" "golang.org/x/image/font" "golang.org/x/image/font/basicfont" ) @@ -318,7 +318,7 @@ func TestDisplay_Close(t *testing.T) { } } -func TestDisplay_Clear(t *testing.T) { +func TestDisplay_ClearLines(t *testing.T) { mock := NewTrackedFakeSSD1306() display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() assertNoError(t, err) @@ -333,7 +333,7 @@ func TestDisplay_Clear(t *testing.T) { display.buffer[1] = "Test line 2" // Clear the display - display.Clear() //nolint:errcheck + display.ClearLines() //nolint:errcheck // Verify all buffer lines are empty for i, line := range display.buffer { @@ -457,7 +457,7 @@ func TestDisplay_PrintLines(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Clear buffer before each test - display.Clear() //nolint:errcheck + display.ClearLines() //nolint:errcheck err := display.PrintLines(tt.line, tt.text) @@ -565,7 +565,7 @@ func TestDisplay_MethodsFailWithoutInit(t *testing.T) { { name: "Clear fails without init", operation: func() error { - return display.Clear() + return display.ClearLines() }, errorSubstr: "driver has not been initialized", }, @@ -639,7 +639,7 @@ func TestDisplay_Integration(t *testing.T) { } // Clear and verify - display.Clear() //nolint:errcheck + display.ClearLines() //nolint:errcheck for i, line := range display.buffer { if line != "" { t.Errorf("Expected buffer[%d] to be empty after clear, got %q", i, line) diff --git a/display/fakedriver/fakedriver.go b/display/fakedriver/fakedriver.go index e2774dd..fc6246b 100644 --- a/display/fakedriver/fakedriver.go +++ b/display/fakedriver/fakedriver.go @@ -11,6 +11,8 @@ import ( "image/png" "log" "net/http" + "os" + "strconv" "sync" "periph.io/x/devices/v3/ssd1306/image1bit" @@ -20,26 +22,55 @@ import ( var displayTemplate embed.FS type FakeSSD1306 struct { - bounds image.Rectangle - mutex sync.Mutex - buffer *image.RGBA - server *http.Server - port string - clients map[chan string]bool - waitMode bool - startChan chan bool - started bool + bounds image.Rectangle + mutex sync.Mutex + buffer *image.RGBA + server *http.Server + listenAddress string + port uint + clients map[chan string]bool + waitMode bool + startChan chan bool + started bool +} + +func getEnvWithDefault(name, defval string) string { + val := os.Getenv(name) + if val == "" { + return defval + } + return val } func NewFakeSSD1306() *FakeSSD1306 { + listenAddress := getEnvWithDefault("FAKESSD1306_LISTEN_ADDRESS", "127.0.0.1") + portStr := getEnvWithDefault("FAKESSD1306_PORT", "8080") + + port, err := strconv.ParseUint(portStr, 10, 32) + if err != nil { + port = 8080 + log.Printf("invalid port %s: using default port %d", portStr, port) + } + return &FakeSSD1306{ - bounds: image.Rect(0, 0, 128, 64), - port: "8080", - clients: make(map[chan string]bool), - startChan: make(chan bool, 1), + bounds: image.Rect(0, 0, 128, 64), + listenAddress: listenAddress, + port: uint(port), + clients: make(map[chan string]bool), + startChan: make(chan bool, 1), } } +func (f *FakeSSD1306) WithPort(port uint) *FakeSSD1306 { + f.port = port + return f +} + +func (f *FakeSSD1306) WithListenAddress(addr string) *FakeSSD1306 { + f.listenAddress = addr + return f +} + func (d *FakeSSD1306) SetWaitMode(waitMode bool) { d.waitMode = waitMode } @@ -76,13 +107,13 @@ func (d *FakeSSD1306) Open() error { mux.HandleFunc("/start", d.handleStart) d.server = &http.Server{ - Addr: ":" + d.port, + Addr: fmt.Sprintf("%s:%d", d.listenAddress, d.port), Handler: mux, } // Start server in a goroutine go func() { - log.Printf("SSD1306 Display Simulator running at http://localhost:%s", d.port) + log.Printf("SSD1306 Display Simulator running at http://localhost:%d", d.port) if err := d.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Printf("HTTP server error: %v", err) } diff --git a/go.mod b/go.mod index 401eca8..1996653 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,12 @@ -module github.com/larsks/display1306 +module github.com/larsks/display1306/v2 go 1.23.4 require ( + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 + github.com/spf13/pflag v1.0.7 golang.org/x/image v0.28.0 periph.io/x/conn/v3 v3.7.2 periph.io/x/devices/v3 v3.7.4 periph.io/x/host/v3 v3.8.5 ) - -require ( - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/spf13/pflag v1.0.6 -) diff --git a/go.sum b/go.sum index 71f97b8..a07d56a 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= periph.io/x/conn/v3 v3.7.2 h1:qt9dE6XGP5ljbFnCKRJ9OOCoiOyBGlw7JZgoi72zZ1s= diff --git a/v2/display/display.go b/v2/display/display.go deleted file mode 100644 index 7ac5807..0000000 --- a/v2/display/display.go +++ /dev/null @@ -1,215 +0,0 @@ -package display - -import ( - "fmt" - "image" - "image/color" - _ "image/gif" - _ "image/jpeg" - _ "image/png" - "os" - - _ "golang.org/x/image/bmp" - "golang.org/x/image/font" - "golang.org/x/image/font/basicfont" - "golang.org/x/image/math/fixed" - "periph.io/x/devices/v3/ssd1306/image1bit" -) - -const ( - DEFAULT_MAX_LINES uint = 5 -) - -type ( - Display struct { - busName string - driver SSD1306 - lines uint - buffer []string - font font.Face - lineHeight int - initialized bool - } -) - -func NewDisplay() *Display { - return &Display{ - lines: DEFAULT_MAX_LINES, - } -} - -func (d *Display) WithLines(lines uint) *Display { - d.lines = lines - return d -} - -func (d *Display) WithBusName(busName string) *Display { - d.busName = busName - return d -} - -func (d *Display) WithDriver(driver SSD1306) *Display { - d.driver = driver - return d -} - -func (d *Display) WithFont(f font.Face) *Display { - d.font = f - d.lineHeight = f.Metrics().Height.Ceil() - return d -} - -func (d *Display) Build() (*Display, error) { - if d.font == nil { - f := basicfont.Face7x13 - lineHeight := f.Metrics().Height.Ceil() - d.font = f - d.lineHeight = lineHeight - } - return d, nil -} - -func (d *Display) Init() error { - d.buffer = make([]string, d.lines) - - if d.driver == nil { - d.driver = NewRealSSD1306(d.busName) - } - - if err := d.driver.Open(); err != nil { - return fmt.Errorf("failed to initialize device: %w", err) - } - - d.initialized = true - - return nil -} - -func (d *Display) Close() error { - if d.initialized { - return d.driver.Close() - } - return nil -} - -func (d *Display) ClearLines() error { - if !d.initialized { - return fmt.Errorf("driver has not been initialized") - } - for i := range d.buffer { - d.buffer[i] = "" - } - return nil -} - -func (d *Display) ClearScreen() error { - img := image1bit.NewVerticalLSB(d.driver.Bounds()) - if err := d.driver.Draw(d.driver.Bounds(), img, image.Point{}); err != nil { - return fmt.Errorf("failed to draw on display: %w", err) - } - return nil -} - -func (d *Display) PrintLine(line uint, text string) error { - if !d.initialized { - return fmt.Errorf("driver has not been initialized") - } - - if int(line) >= len(d.buffer) { - return fmt.Errorf("request to draw on line %d but display only has %d lines", line, len(d.buffer)) - } - - d.buffer[line] = text - return nil -} - -func (d *Display) PrintLines(line uint, text []string) error { - if !d.initialized { - return fmt.Errorf("driver has not been initialized") - } - - if int(line)+len(text) > int(d.lines) { - return fmt.Errorf("text requires more than %d lines", len(d.buffer)) - } - - for i := range text { - d.buffer[int(line)+i] = text[i] - } - - return nil -} - -func (d *Display) Update() error { - if !d.initialized { - return fmt.Errorf("driver has not been initialized") - } - - img := image1bit.NewVerticalLSB(d.driver.Bounds()) - screen := font.Drawer{ - Dst: img, - Src: &image.Uniform{image1bit.On}, - Face: d.font, - } - - for i, textLine := range d.buffer { - screen.Dot = fixed.P(0, d.lineHeight*(1+i)-d.font.Metrics().Descent.Round()) - screen.DrawString(textLine) - } - if err := d.driver.Draw(d.driver.Bounds(), img, image.Point{}); err != nil { - return fmt.Errorf("failed to draw on display: %w", err) - } - - return nil -} - -func (d *Display) SetFont(f font.Face) { - d.font = f - d.lineHeight = f.Metrics().Height.Ceil() -} - -func (d *Display) ShowImage(img image.Image) error { - if !d.initialized { - return fmt.Errorf("driver has not been initialized") - } - - bounds := d.driver.Bounds() - displayImg := image1bit.NewVerticalLSB(bounds) - - imgBounds := img.Bounds() - for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - srcX := imgBounds.Min.X + x - srcY := imgBounds.Min.Y + y - if srcX < imgBounds.Max.X && srcY < imgBounds.Max.Y { - c := img.At(srcX, srcY) - gray := color.GrayModel.Convert(c).(color.Gray) - if gray.Y > 128 { - displayImg.Set(x, y, image1bit.On) - } else { - displayImg.Set(x, y, image1bit.Off) - } - } - } - } - - if err := d.driver.Draw(bounds, displayImg, image.Point{}); err != nil { - return fmt.Errorf("failed to draw image on display: %w", err) - } - - return nil -} - -func (d *Display) ShowImageFromFile(filename string) error { - file, err := os.Open(filename) - if err != nil { - return fmt.Errorf("failed to open image file: %w", err) - } - defer file.Close() //nolint:errcheck - - img, _, err := image.Decode(file) - if err != nil { - return fmt.Errorf("failed to decode image: %w", err) - } - - return d.ShowImage(img) -} diff --git a/v2/display/display_test.go b/v2/display/display_test.go deleted file mode 100644 index f562f7b..0000000 --- a/v2/display/display_test.go +++ /dev/null @@ -1,899 +0,0 @@ -package display - -import ( - "fmt" - "image" - "image/color" - "strings" - "testing" - - "github.com/larsks/display1306/v2/display/fakedriver" - "golang.org/x/image/font" - "golang.org/x/image/font/basicfont" -) - -// Call represents a method call on the mock -type Call struct { - Method string - Args []interface{} -} - -// Enhanced FakeSSD1306 for testing with call tracking -type TrackedFakeSSD1306 struct { - *fakedriver.FakeSSD1306 - Calls []Call - ErrorOnOpen bool - ErrorOnClose bool - ErrorOnDraw bool -} - -func NewTrackedFakeSSD1306() *TrackedFakeSSD1306 { - return &TrackedFakeSSD1306{ - FakeSSD1306: fakedriver.NewFakeSSD1306(), - Calls: make([]Call, 0), - } -} - -func (t *TrackedFakeSSD1306) Open() error { - t.Calls = append(t.Calls, Call{Method: "Open", Args: nil}) - if t.ErrorOnOpen { - return fmt.Errorf("mock open error") - } - return nil -} - -func (t *TrackedFakeSSD1306) Close() error { - t.Calls = append(t.Calls, Call{Method: "Close", Args: nil}) - if t.ErrorOnClose { - return fmt.Errorf("mock close error") - } - return nil -} - -func (t *TrackedFakeSSD1306) Bounds() image.Rectangle { - t.Calls = append(t.Calls, Call{Method: "Bounds", Args: nil}) - return t.FakeSSD1306.Bounds() -} - -func (t *TrackedFakeSSD1306) Draw(r image.Rectangle, src image.Image, sp image.Point) error { - t.Calls = append(t.Calls, Call{Method: "Draw", Args: []interface{}{r, src, sp}}) - if t.ErrorOnDraw { - return fmt.Errorf("mock draw error") - } - return nil -} - -// Test helper functions -func (t *TrackedFakeSSD1306) WasCalled(method string) bool { - for _, call := range t.Calls { - if call.Method == method { - return true - } - } - return false -} - -func (t *TrackedFakeSSD1306) CallCount(method string) int { - count := 0 - for _, call := range t.Calls { - if call.Method == method { - count++ - } - } - return count -} - -func (t *TrackedFakeSSD1306) LastDrawArgs() (image.Rectangle, image.Image, image.Point) { - for i := len(t.Calls) - 1; i >= 0; i-- { - if t.Calls[i].Method == "Draw" && len(t.Calls[i].Args) == 3 { - return t.Calls[i].Args[0].(image.Rectangle), - t.Calls[i].Args[1].(image.Image), - t.Calls[i].Args[2].(image.Point) - } - } - return image.Rectangle{}, nil, image.Point{} -} - -// Test assertion helpers -func assertNoError(t *testing.T, err error) { - t.Helper() - if err != nil { - t.Errorf("Expected no error but got: %v", err) - } -} - -func assertError(t *testing.T, err error, expectedSubstr string) { - t.Helper() - if err == nil { - t.Error("Expected error but got none") - } else if expectedSubstr != "" && !strings.Contains(err.Error(), expectedSubstr) { - t.Errorf("Expected error to contain %q, got %q", expectedSubstr, err.Error()) - } -} - -func assertMethodCalled(t *testing.T, mock *TrackedFakeSSD1306, method string) { - t.Helper() - if !mock.WasCalled(method) { - t.Errorf("Expected %s to be called", method) - } -} - -func TestNewDisplay(t *testing.T) { - tests := []struct { - name string - busName string - dev SSD1306 - wantDev bool - }{ - { - name: "with provided fake device", - busName: "/dev/i2c-0", - dev: fakedriver.NewFakeSSD1306(), - wantDev: true, - }, - { - name: "with nil device creates real device", - busName: "/dev/i2c-1", - dev: nil, - wantDev: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - display, err := NewDisplay().WithBusName(tt.busName).WithDriver(tt.dev).Build() - assertNoError(t, err) - _ = display.Init() - - if display == nil { - t.Fatal("NewDisplay returned nil") - } - - if display.driver == nil && tt.wantDev { - t.Error("Expected device to be set") - } - - if display.lines != DEFAULT_MAX_LINES { - t.Errorf("Expected lines to be %d, got %d", DEFAULT_MAX_LINES, display.lines) - } - - if display.font == nil { - t.Error("Expected font to be set") - } - - if display.lineHeight <= 0 { - t.Error("Expected lineHeight to be positive") - } - }) - } -} - -func TestDisplay_WithFont(t *testing.T) { - tests := []struct { - name string - font font.Face - expectHeight int - }{ - { - name: "with basicfont Face7x13", - font: basicfont.Face7x13, - expectHeight: basicfont.Face7x13.Metrics().Height.Ceil(), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - display := NewDisplay().WithFont(tt.font) - - if display.font != tt.font { - t.Error("Expected custom font to be set") - } - - if display.lineHeight != tt.expectHeight { - t.Errorf("Expected lineHeight to be %d, got %d", tt.expectHeight, display.lineHeight) - } - }) - } -} - -func TestDisplay_Build_WithDefaultFont(t *testing.T) { - display := NewDisplay() - built, err := display.Build() - assertNoError(t, err) - - // Should have default font set - if built.font == nil { - t.Error("Expected default font to be set") - } - - expectedHeight := basicfont.Face7x13.Metrics().Height.Ceil() - if built.lineHeight != expectedHeight { - t.Errorf("Expected lineHeight to be %d, got %d", expectedHeight, built.lineHeight) - } -} - -func TestDisplay_Build_WithCustomFont(t *testing.T) { - customFont := basicfont.Face7x13 - display := NewDisplay().WithFont(customFont) - built, err := display.Build() - assertNoError(t, err) - - // Should preserve custom font - if built.font != customFont { - t.Error("Expected custom font to be preserved") - } - - expectedHeight := customFont.Metrics().Height.Ceil() - if built.lineHeight != expectedHeight { - t.Errorf("Expected lineHeight to be %d, got %d", expectedHeight, built.lineHeight) - } -} - -func TestDisplay_Init(t *testing.T) { - tests := []struct { - name string - setupMock func(*TrackedFakeSSD1306) - wantError bool - errorSubstr string - }{ - { - name: "successful init", - setupMock: func(mock *TrackedFakeSSD1306) { - // No errors - }, - wantError: false, - }, - { - name: "device open error", - setupMock: func(mock *TrackedFakeSSD1306) { - mock.ErrorOnOpen = true - }, - wantError: true, - errorSubstr: "failed to initialize device", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mock := NewTrackedFakeSSD1306() - tt.setupMock(mock) - - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - err = display.Init() - - if tt.wantError { - assertError(t, err, tt.errorSubstr) - } else { - assertNoError(t, err) - assertMethodCalled(t, mock, "Open") - - if len(display.buffer) != int(display.lines) { - t.Errorf("Expected buffer length to be %d, got %d", display.lines, len(display.buffer)) - } - } - }) - } -} - -func TestDisplay_Close(t *testing.T) { - tests := []struct { - name string - shouldError bool - wantError bool - }{ - { - name: "successful close", - shouldError: false, - wantError: false, - }, - { - name: "close with error", - shouldError: true, - wantError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mock := NewTrackedFakeSSD1306() - mock.ErrorOnClose = tt.shouldError - - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - err = display.Init() - assertNoError(t, err) - - err = display.Close() - - if tt.wantError { - assertError(t, err, "") - } else { - assertNoError(t, err) - } - - assertMethodCalled(t, mock, "Close") - }) - } -} - -func TestDisplay_ClearLines(t *testing.T) { - mock := NewTrackedFakeSSD1306() - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - // Initialize the display to set up the buffer - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize display: %v", err) - } - - // Add some text to the buffer - display.buffer[0] = "Test line 1" - display.buffer[1] = "Test line 2" - - // Clear the display - display.ClearLines() //nolint:errcheck - - // Verify all buffer lines are empty - for i, line := range display.buffer { - if line != "" { - t.Errorf("Expected buffer[%d] to be empty, got %q", i, line) - } - } -} - -func TestDisplay_PrintLine(t *testing.T) { - mock := NewTrackedFakeSSD1306() - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - // Initialize the display - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize display: %v", err) - } - - tests := []struct { - name string - line uint - text string - wantError bool - }{ - { - name: "valid line", - line: 0, - text: "Hello World", - wantError: false, - }, - { - name: "last valid line", - line: DEFAULT_MAX_LINES - 1, - text: "Last line", - wantError: false, - }, - { - name: "line out of bounds", - line: DEFAULT_MAX_LINES, - text: "Should fail", - wantError: true, - }, - { - name: "line far out of bounds", - line: 100, - text: "Should fail", - wantError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := display.PrintLine(tt.line, tt.text) - - if tt.wantError { - if err == nil { - t.Error("Expected error but got none") - } - } else { - if err != nil { - t.Errorf("Expected no error but got: %v", err) - } - - if int(tt.line) < len(display.buffer) && display.buffer[tt.line] != tt.text { - t.Errorf("Expected buffer[%d] to be %q, got %q", tt.line, tt.text, display.buffer[tt.line]) - } - } - }) - } -} - -func TestDisplay_PrintLines(t *testing.T) { - mock := NewTrackedFakeSSD1306() - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - // Initialize the display - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize display: %v", err) - } - - tests := []struct { - name string - line uint - text []string - wantError bool - }{ - { - name: "valid lines", - line: 0, - text: []string{"Line 1", "Line 2", "Line 3"}, - wantError: false, - }, - { - name: "single line", - line: 2, - text: []string{"Single line"}, - wantError: false, - }, - { - name: "fills remaining space", - line: 3, - text: []string{"Line 4", "Line 5"}, - wantError: false, - }, - { - name: "overflow", - line: 0, - text: []string{"1", "2", "3", "4", "5", "6"}, // 6 lines but only 5 available - wantError: true, - }, - { - name: "overflow from middle", - line: 3, - text: []string{"Line 4", "Line 5", "Line 6"}, // 3 lines starting at line 3 = 6 total - wantError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Clear buffer before each test - display.ClearLines() //nolint:errcheck - - err := display.PrintLines(tt.line, tt.text) - - if tt.wantError { - if err == nil { - t.Error("Expected error but got none") - } - } else { - if err != nil { - t.Errorf("Expected no error but got: %v", err) - } - - // Verify the text was written to the correct positions - for i, expectedText := range tt.text { - bufferIndex := int(tt.line) + i - if bufferIndex < len(display.buffer) { - if display.buffer[bufferIndex] != expectedText { - t.Errorf("Expected buffer[%d] to be %q, got %q", bufferIndex, expectedText, display.buffer[bufferIndex]) - } - } - } - } - }) - } -} - -func TestDisplay_Update(t *testing.T) { - tests := []struct { - name string - setupBuffer func(*Display) - mockShouldErr bool - wantError bool - errorSubstr string - wantDrawCalled bool - }{ - { - name: "successful update", - setupBuffer: func(d *Display) { - d.buffer[0] = "Line 1" - d.buffer[1] = "Line 2" - }, - wantDrawCalled: true, - wantError: false, - }, - { - name: "draw error", - setupBuffer: func(d *Display) { - d.buffer[0] = "Test" - }, - mockShouldErr: true, - wantError: true, - errorSubstr: "failed to draw on display", - wantDrawCalled: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mock := NewTrackedFakeSSD1306() - mock.ErrorOnDraw = tt.mockShouldErr - - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - // Initialize and setup buffer - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize display: %v", err) - } - - if tt.setupBuffer != nil { - tt.setupBuffer(display) - } - - err = display.Update() - - if tt.wantError { - assertError(t, err, tt.errorSubstr) - } else { - assertNoError(t, err) - } - - if tt.wantDrawCalled { - assertMethodCalled(t, mock, "Draw") - expectedBounds := mock.Bounds() - drawRect, _, _ := mock.LastDrawArgs() - if drawRect != expectedBounds { - t.Errorf("Expected draw rect to be %v, got %v", expectedBounds, drawRect) - } - } - }) - } -} - -func TestDisplay_MethodsFailWithoutInit(t *testing.T) { - mock := NewTrackedFakeSSD1306() - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - // Test that all methods fail when Init() hasn't been called - tests := []struct { - name string - operation func() error - errorSubstr string - }{ - { - name: "Clear fails without init", - operation: func() error { - return display.ClearLines() - }, - errorSubstr: "driver has not been initialized", - }, - { - name: "PrintLine fails without init", - operation: func() error { - return display.PrintLine(0, "test") - }, - errorSubstr: "driver has not been initialized", - }, - { - name: "PrintLines fails without init", - operation: func() error { - return display.PrintLines(0, []string{"test"}) - }, - errorSubstr: "driver has not been initialized", - }, - { - name: "Update fails without init", - operation: func() error { - return display.Update() - }, - errorSubstr: "driver has not been initialized", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.operation() - assertError(t, err, tt.errorSubstr) - }) - } - - // Verify that no driver methods were called - if len(mock.Calls) > 0 { - t.Errorf("Expected no driver methods to be called, but got: %v", mock.Calls) - } -} - -func TestDisplay_Integration(t *testing.T) { - // Integration test that exercises the full workflow - mock := NewTrackedFakeSSD1306() - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - // Initialize - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize: %v", err) - } - - // Add some text - if err := display.PrintLine(0, "Hello World"); err != nil { - t.Fatalf("Failed to print line: %v", err) - } - - if err := display.PrintLines(1, []string{"Line 2", "Line 3"}); err != nil { - t.Fatalf("Failed to print lines: %v", err) - } - - // Update display - if err := display.Update(); err != nil { - t.Fatalf("Failed to update: %v", err) - } - - // Verify all expected calls were made - assertMethodCalled(t, mock, "Open") - assertMethodCalled(t, mock, "Draw") - - if mock.CallCount("Draw") != 1 { - t.Errorf("Expected Draw to be called once, got %d times", mock.CallCount("Draw")) - } - - // Clear and verify - display.ClearLines() //nolint:errcheck - for i, line := range display.buffer { - if line != "" { - t.Errorf("Expected buffer[%d] to be empty after clear, got %q", i, line) - } - } - - // Close - if err := display.Close(); err != nil { - t.Fatalf("Failed to close: %v", err) - } - - assertMethodCalled(t, mock, "Close") -} - -func TestDisplay_FontIntegration(t *testing.T) { - // Integration test that verifies font handling throughout the workflow - mock := NewTrackedFakeSSD1306() - - // Test with custom font - customFont := basicfont.Face7x13 - display, err := NewDisplay(). - WithBusName("/dev/i2c-0"). - WithDriver(mock). - WithFont(customFont). - Build() - assertNoError(t, err) - - // Verify font is set correctly - if display.font != customFont { - t.Error("Expected custom font to be preserved") - } - - expectedHeight := customFont.Metrics().Height.Ceil() - if display.lineHeight != expectedHeight { - t.Errorf("Expected lineHeight to be %d, got %d", expectedHeight, display.lineHeight) - } - - // Initialize and use the display - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize: %v", err) - } - - if err := display.PrintLine(0, "Font Test"); err != nil { - t.Fatalf("Failed to print line: %v", err) - } - - if err := display.Update(); err != nil { - t.Fatalf("Failed to update: %v", err) - } - - // Verify draw was called - assertMethodCalled(t, mock, "Draw") -} - -// TestImage is a helper type for creating test images -type TestImage struct { - bounds image.Rectangle - pixels map[image.Point]color.Color -} - -func NewTestImage(width, height int) *TestImage { - return &TestImage{ - bounds: image.Rect(0, 0, width, height), - pixels: make(map[image.Point]color.Color), - } -} - -func (t *TestImage) ColorModel() color.Model { - return color.RGBAModel -} - -func (t *TestImage) Bounds() image.Rectangle { - return t.bounds -} - -func (t *TestImage) At(x, y int) color.Color { - if c, ok := t.pixels[image.Point{x, y}]; ok { - return c - } - return color.RGBA{0, 0, 0, 255} // Default to black -} - -func (t *TestImage) Set(x, y int, c color.Color) { - t.pixels[image.Point{x, y}] = c -} - -func TestDisplay_ShowImage_SmallImage(t *testing.T) { - mock := NewTrackedFakeSSD1306() - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize display: %v", err) - } - - // Create a small test image (smaller than display bounds) - displayBounds := mock.Bounds() - smallImage := NewTestImage(displayBounds.Dx()/2, displayBounds.Dy()/2) - - // Set some white pixels - smallImage.Set(0, 0, color.RGBA{255, 255, 255, 255}) - smallImage.Set(1, 1, color.RGBA{255, 255, 255, 255}) - - err = display.ShowImage(smallImage) - assertNoError(t, err) - assertMethodCalled(t, mock, "Draw") -} - -func TestDisplay_ShowImage_LargeImageCropped(t *testing.T) { - mock := NewTrackedFakeSSD1306() - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize display: %v", err) - } - - // Create a large test image (larger than display bounds) - displayBounds := mock.Bounds() - largeImage := NewTestImage(displayBounds.Dx()*2, displayBounds.Dy()*2) - - // Set different colored pixels in different quadrants - // Upper left (should be visible after cropping) - largeImage.Set(0, 0, color.RGBA{255, 255, 255, 255}) // White - largeImage.Set(1, 0, color.RGBA{255, 255, 255, 255}) // White - - // Upper right (should be cropped out) - largeImage.Set(displayBounds.Dx()+10, 0, color.RGBA{255, 0, 0, 255}) // Red - - // Lower left (should be cropped out) - largeImage.Set(0, displayBounds.Dy()+10, color.RGBA{0, 255, 0, 255}) // Green - - // This should not return an error anymore - large images should be cropped - err = display.ShowImage(largeImage) - assertNoError(t, err) - assertMethodCalled(t, mock, "Draw") -} - -func TestDisplay_ShowImage_ExactSizeImage(t *testing.T) { - mock := NewTrackedFakeSSD1306() - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize display: %v", err) - } - - // Create an image exactly the size of the display - displayBounds := mock.Bounds() - exactImage := NewTestImage(displayBounds.Dx(), displayBounds.Dy()) - - // Fill with a pattern - for y := 0; y < displayBounds.Dy(); y++ { - for x := 0; x < displayBounds.Dx(); x++ { - if (x+y)%2 == 0 { - exactImage.Set(x, y, color.RGBA{255, 255, 255, 255}) // White - } - } - } - - err = display.ShowImage(exactImage) - assertNoError(t, err) - assertMethodCalled(t, mock, "Draw") -} - -func TestDisplay_ShowImage_WithoutInit(t *testing.T) { - mock := NewTrackedFakeSSD1306() - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - // Don't initialize the display - testImage := NewTestImage(10, 10) - - err = display.ShowImage(testImage) - assertError(t, err, "driver has not been initialized") - - // Verify no draw was called - if mock.WasCalled("Draw") { - t.Error("Expected Draw not to be called when display not initialized") - } -} - -func TestDisplay_ShowImage_DrawError(t *testing.T) { - mock := NewTrackedFakeSSD1306() - mock.ErrorOnDraw = true - - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize display: %v", err) - } - - testImage := NewTestImage(10, 10) - - err = display.ShowImage(testImage) - assertError(t, err, "failed to draw image on display") - assertMethodCalled(t, mock, "Draw") -} - -func TestDisplay_ShowImageFromFile_Integration(t *testing.T) { - // This test just verifies that ShowImageFromFile calls ShowImage - // We can't easily test file operations without creating temporary files - mock := NewTrackedFakeSSD1306() - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize display: %v", err) - } - - // Test with non-existent file should return appropriate error - err = display.ShowImageFromFile("/nonexistent/file.png") - assertError(t, err, "failed to open image file") - - // Verify no draw was called due to file error - if mock.WasCalled("Draw") { - t.Error("Expected Draw not to be called when file cannot be opened") - } -} - -func TestDisplay_SetFont(t *testing.T) { - mock := NewTrackedFakeSSD1306() - display, err := NewDisplay().WithBusName("/dev/i2c-0").WithDriver(mock).Build() - assertNoError(t, err) - - // Initialize the display - if err := display.Init(); err != nil { - t.Fatalf("Failed to initialize display: %v", err) - } - - // Set a new font (using the same font for simplicity, but this demonstrates the method works) - newFont := basicfont.Face7x13 - display.SetFont(newFont) - - // Verify the font was changed - if display.font != newFont { - t.Error("Expected font to be updated") - } - - // Verify line height was recalculated - expectedHeight := newFont.Metrics().Height.Ceil() - if display.lineHeight != expectedHeight { - t.Errorf("Expected lineHeight to be %d, got %d", expectedHeight, display.lineHeight) - } - - // Verify we can still use the display normally after changing font - if err := display.PrintLine(0, "Test with new font"); err != nil { - t.Fatalf("Failed to print line after font change: %v", err) - } - - if err := display.Update(); err != nil { - t.Fatalf("Failed to update after font change: %v", err) - } - - assertMethodCalled(t, mock, "Draw") -} diff --git a/v2/display/fakedriver/display.html b/v2/display/fakedriver/display.html deleted file mode 100644 index 9793435..0000000 --- a/v2/display/fakedriver/display.html +++ /dev/null @@ -1,142 +0,0 @@ - - -
-128x64 OLED Display (4x scaled)
-Connecting to real-time updates...
- -Waiting for connection...
-