From fa02ecc079f3c45c1a11491cbe4a1075f6b1125f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=A1clav=20Jel=C3=ADnek?= <jelinva4@fel.cvut.cz>
Date: Fri, 7 Mar 2025 22:24:55 +0100
Subject: [PATCH] Optimize sending data to the display

---
 lib/cube/sh1106.py | 92 ++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 80 insertions(+), 12 deletions(-)

diff --git a/lib/cube/sh1106.py b/lib/cube/sh1106.py
index 0f90463..1945dac 100644
--- a/lib/cube/sh1106.py
+++ b/lib/cube/sh1106.py
@@ -44,6 +44,7 @@ _SET_SEG_REMAP       = const(0xa0)
 _LOW_COLUMN_ADDRESS  = const(0x00)
 _HIGH_COLUMN_ADDRESS = const(0x10)
 _SET_PAGE_ADDRESS    = const(0xB0)
+_SET_OFFSET          = const(0xD3)
 
 # Function to generate n equidistant points in range <a,b>
 def linspace(a, b, n):    
@@ -67,6 +68,8 @@ class SH1106(framebuf.FrameBuffer):
         self.pages = self.height // 8
         self.bufsize = self.pages * self.width
         self.renderbuf = bytearray(self.bufsize)
+        self.pages_to_update = 0
+
         if self.rotate90:
             self.displaybuf = bytearray(self.bufsize)
             # HMSB is required to keep the bit order in the render buffer
@@ -94,6 +97,7 @@ class SH1106(framebuf.FrameBuffer):
     def init_display(self):
         self.reset()
         self.fill(0)
+        self.show()
         self.poweron()
         # rotate90 requires a call to flip() for setting up.
         self.flip(self.flip_en)
@@ -125,18 +129,37 @@ class SH1106(framebuf.FrameBuffer):
     def invert(self, invert):
         self.write_cmd(_SET_NORM_INV | (invert & 1))
 
-    def show(self):
+    def show(self, full_update = False):
         # self.* lookups in loops take significant time (~4fps).
         (w, p, db, rb) = (self.width, self.pages,
                           self.displaybuf, self.renderbuf)
         if self.rotate90:
             for i in range(self.bufsize):
                 db[w * (i % p) + (i // p)] = rb[i]
-        for page in range(self.height // 8):
-            self.write_cmd(_SET_PAGE_ADDRESS | page)
-            self.write_cmd(_LOW_COLUMN_ADDRESS | 2)
-            self.write_cmd(_HIGH_COLUMN_ADDRESS | 0)
-            self.write_data(db[(w*page):(w*page+w)])
+        if full_update:
+            pages_to_update = (1 << self.pages) - 1
+        else:
+            pages_to_update = self.pages_to_update
+        #print("Updating pages: {:08b}".format(pages_to_update))
+        for page in range(self.pages):
+            if (pages_to_update & (1 << page)):
+                self.write_cmd(_SET_PAGE_ADDRESS | page)
+                self.write_cmd(_LOW_COLUMN_ADDRESS | 2)
+                self.write_cmd(_HIGH_COLUMN_ADDRESS | 0)
+                self.write_data(db[(w*page):(w*page+w)])
+        self.pages_to_update = 0
+
+    def register_updates(self, y0, y1=None):
+        # this function takes the top and optional bottom address of the changes made
+        # and updates the pages_to_change list with any changed pages
+        # that are not yet on the list
+        start_page = max(0, y0 // 8)
+        end_page = max(0, y1 // 8) if y1 is not None else start_page
+        # rearrange start_page and end_page if coordinates were given from bottom to top
+        if start_page > end_page:
+            start_page, end_page = end_page, start_page
+        for page in range(start_page, end_page+1):
+            self.pages_to_update |= 1 << page
 
     def reset(self, res):
         if res is not None:
@@ -147,6 +170,51 @@ class SH1106(framebuf.FrameBuffer):
             res(1)
             time.sleep_ms(20)
             
+    def pixel(self, x, y, color=None):
+        if color is None:
+            return super().pixel(x, y)
+        else:
+            super().pixel(x, y , color)
+            page = y // 8
+            self.pages_to_update |= 1 << page
+
+    def text(self, text, x, y, color=1):
+        super().text(text, x, y, color)
+        self.register_updates(y, y+7)
+
+    def line(self, x0, y0, x1, y1, color):
+        super().line(x0, y0, x1, y1, color)
+        self.register_updates(y0, y1)
+
+    def hline(self, x, y, w, color):
+        super().hline(x, y, w, color)
+        self.register_updates(y)
+
+    def vline(self, x, y, h, color):
+        super().vline(x, y, h, color)
+        self.register_updates(y, y+h-1)
+
+    def fill(self, color):
+        super().fill(color)
+        self.pages_to_update = (1 << self.pages) - 1
+
+    def blit(self, fbuf, x, y, key=-1, palette=None):
+        super().blit(fbuf, x, y, key, palette)
+        self.register_updates(y, y+self.height)
+
+    def scroll(self, x, y):
+        # my understanding is that scroll() does a full screen change
+        super().scroll(x, y)
+        self.pages_to_update =  (1 << self.pages) - 1
+
+    def fill_rect(self, x, y, w, h, color):
+        super().fill_rect(x, y, w, h, color)
+        self.register_updates(y, y+h-1)
+
+    def rect(self, x, y, w, h, color):
+        super().rect(x, y, w, h, color)
+        self.register_updates(y, y+h-1)
+
     def draw_bar_chart_v(self, current_val, x, y, w, h, low_lim=0, high_lim=100, no_of_ticks=5, label= None, redraw=False):      
         if(redraw):
             self.init_display()
@@ -167,7 +235,7 @@ class SH1106(framebuf.FrameBuffer):
         self.fill_rect(x, y - h, w, h - level,  0);
         self.rect(x, y - h, w, h, 1);
         self.fill_rect(x, y - level, w,  level, 1);
-        self.show()
+        self.show(True)
         return redraw
         
     def draw_bar_chart_h(self, current_val, x, y, w, h, low_lim=0, high_lim=100, no_of_ticks=5, label= None, redraw=False):      
@@ -190,7 +258,7 @@ class SH1106(framebuf.FrameBuffer):
         self.fill_rect(x + level, y - h, w - level, h,  0);
         self.rect(x, y - h, w,  h, 1);
         self.fill_rect(x, y - h, level,  h, 1);
-        self.show()
+        self.show(True)
         return redraw
         
     def draw_dial(self, curval, cx, cy, r, loval, hival, no_of_steps, sa, label, Redraw):         
@@ -267,7 +335,7 @@ class SH1106(framebuf.FrameBuffer):
         self.ply = ly
         self.prx = rx
         self.pry = ry     
-        self.show()
+        self.show(True)
         return Redraw
     
     def continuous_graph(self, x, y, gx, gy, w, h, xlo, xhi, ylo, yhi, label, Redraw):
@@ -314,7 +382,7 @@ class SH1106(framebuf.FrameBuffer):
             self.data_points = 0
         # up until now print sends data to a video buffer NOT the screen
         # this call sends the data to the screen
-        self.show();        
+        self.show(True);        
         return Redraw
 
     def fill_circle(self, x0, y0, radius, color):
@@ -338,7 +406,7 @@ class SH1106(framebuf.FrameBuffer):
             self.vline(int(x0 + y), int(y0 - x), int(2*x + 1), color)
             self.vline(int(x0 - x), int(y0 - y), int(2*y + 1), color)
             self.vline(int(x0 - y), int(y0 - x), int(2*x + 1), color)
-        self.show()
+        self.show(True)
             
     def fill_triangle(self, x0, y0, x1, y1, x2, y2, color):
         # Filled triangle drawing function.  Will draw a filled triangle around
@@ -406,7 +474,7 @@ class SH1106(framebuf.FrameBuffer):
                 a, b = b, a
             self.hline(a, y, b-a+1, color)
             y += 1
-        self.show()
+        self.show(True)
 
     def draw_arrow(self, x: int, y: int, dir: int, c: int = 1):
         j_max = 2
-- 
GitLab