2021-06-09 16:11:48 +00:00
|
|
|
# scale_log.py Extension to micro-gui providing the ScaleLog class
|
|
|
|
|
|
|
|
# Released under the MIT License (MIT). See LICENSE.
|
|
|
|
# Copyright (c) 2021 Peter Hinch
|
|
|
|
|
|
|
|
# A logarithmic Scale which responds to user input
|
|
|
|
# Usage:
|
|
|
|
# from gui.widgets.scale_log import ScaleLog
|
|
|
|
|
|
|
|
|
|
|
|
import uasyncio as asyncio
|
|
|
|
from time import ticks_ms, ticks_diff
|
|
|
|
from math import log10
|
|
|
|
|
|
|
|
from gui.core.ugui import LinearIO, display
|
|
|
|
from hardware_setup import ssd # Display driver for Writer
|
|
|
|
from gui.core.writer import Writer
|
2021-06-11 14:33:05 +00:00
|
|
|
from gui.core.colors import *
|
2021-06-09 16:11:48 +00:00
|
|
|
|
|
|
|
# Null function
|
|
|
|
dolittle = lambda *_ : None
|
|
|
|
|
|
|
|
|
|
|
|
# Start value is 1.0. User applies scaling to value and ticks callback.
|
|
|
|
class ScaleLog(LinearIO):
|
2021-07-04 17:21:37 +00:00
|
|
|
encoder_rate = 5
|
2021-06-09 16:11:48 +00:00
|
|
|
def __init__(self, writer, row, col, *,
|
|
|
|
decades=5, height=0, width=160,
|
|
|
|
bdcolor=None, fgcolor=None, bgcolor=None,
|
2021-06-16 13:15:06 +00:00
|
|
|
pointercolor=None, fontcolor=None, prcolor=None,
|
2021-06-09 16:11:48 +00:00
|
|
|
legendcb=None, tickcb=None,
|
|
|
|
callback=dolittle, args=[],
|
|
|
|
value=1.0, delta=0.01, active=False):
|
|
|
|
# For correct text rendering inside control must explicitly set bgcolor
|
|
|
|
bgcolor = BLACK if bgcolor is None else bgcolor
|
|
|
|
if decades < 3:
|
|
|
|
raise ValueError('decades must be >= 3')
|
|
|
|
self.mval = 10**decades # Max value
|
|
|
|
self.tickcb = tickcb
|
|
|
|
self.delta = delta # Min multiplier = 1 + delta
|
|
|
|
def lcb(f):
|
|
|
|
return '{:<1.0f}'.format(f)
|
|
|
|
self.legendcb = legendcb if legendcb is not None else lcb
|
|
|
|
text_ht = writer.height
|
|
|
|
ctrl_ht = 12 # Minimum height for ticks
|
|
|
|
min_ht = text_ht + 8 # Ht of text and gap between text and ticks
|
|
|
|
if height < min_ht + ctrl_ht:
|
|
|
|
height = min_ht + ctrl_ht # min workable height
|
|
|
|
else:
|
|
|
|
ctrl_ht = height - min_ht # adjust ticks for greater height
|
|
|
|
width &= 0xfffe # Make divisible by 2: avoid 1 pixel pointer offset
|
2021-06-22 08:01:40 +00:00
|
|
|
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, self._constrain(value), active, prcolor)
|
|
|
|
super()._set_callbacks(callback, args)
|
2021-06-09 16:11:48 +00:00
|
|
|
self.fontcolor = fontcolor if fontcolor is not None else self.fgcolor
|
|
|
|
|
|
|
|
self.x0 = col + 2
|
|
|
|
self.x1 = col + self.width - 2
|
|
|
|
self.y0 = row + 2
|
|
|
|
self.y1 = row + self.height - 2
|
|
|
|
self.ptrcolor = pointercolor if pointercolor is not None else self.fgcolor
|
|
|
|
# Define tick dimensions
|
|
|
|
ytop = self.y0 + text_ht + 2 # Top of scale graphic (2 pixel gap)
|
|
|
|
ycl = ytop + (self.y1 - ytop) // 2 # Centre line
|
|
|
|
self.sdl = round(ctrl_ht * 1 / 3) # Length of small tick.
|
|
|
|
self.sdy0 = ycl - self.sdl // 2
|
|
|
|
self.mdl = round(ctrl_ht * 2 / 3) # Medium tick
|
|
|
|
self.mdy0 = ycl - self.mdl // 2
|
|
|
|
self.ldl = ctrl_ht # Large tick
|
|
|
|
self.ldy0 = ycl - self.ldl // 2
|
|
|
|
self.dw = (self.x1 - self.x0) // 2 # Pixel width of a decade
|
|
|
|
self.draw = True # Ensure a redraw on next refresh
|
2021-06-22 08:01:40 +00:00
|
|
|
# Run callback (e.g. to set dynamic colors)
|
|
|
|
self.callback(self, *self.args)
|
2021-06-09 16:11:48 +00:00
|
|
|
|
|
|
|
# Pre calculated log10(x) for x in range(1, 10)
|
|
|
|
def show(self, logs=(0.0, 0.3010, 0.4771, 0.6021, 0.6990, 0.7782, 0.8451, 0.9031, 0.9542)):
|
|
|
|
#start = ticks_ms()
|
|
|
|
x0: int = self.x0 # Internal rectangle occupied by scale and text
|
|
|
|
x1: int = self.x1
|
|
|
|
y0: int = self.y0
|
|
|
|
y1: int = self.y1
|
|
|
|
xc: int = x0 + (x1 - x0) // 2 # x location of pointer
|
|
|
|
dw: int = self.dw # Width of a decade in pixels
|
|
|
|
wri = self.writer
|
|
|
|
if super().show():
|
|
|
|
vc = self._value # Current value, corresponds to centre of display
|
|
|
|
d = int(log10(vc)) - 1 # 10**d is start of a decade guaranteed to be outside display
|
|
|
|
vs = max(10 ** d, 1.0) # vs: start value of current decade
|
2021-06-11 14:33:05 +00:00
|
|
|
txtcolor = GREY if self.greyed_out() else self.fontcolor
|
2021-06-09 16:11:48 +00:00
|
|
|
while True: # For each decade until we run out of space
|
|
|
|
done = True # Assume completion
|
|
|
|
xs: float = xc - dw * log10(vc / vs) # x location of start of scale
|
|
|
|
tick: int
|
|
|
|
q: float
|
|
|
|
# log10 ~38us on Pi Pico
|
|
|
|
for tick, q in enumerate(logs):
|
|
|
|
vt: float = vs * (1 + tick) # Value of current tick
|
|
|
|
x: int = round(xs + q * dw) # x location of current tick
|
|
|
|
if x >= x1:
|
|
|
|
break # All visible ticks drawn
|
|
|
|
elif x > x0: # Tick is visible
|
|
|
|
if not tick:
|
|
|
|
txt = self.legendcb(vt)
|
|
|
|
tlen = wri.stringlen(txt)
|
|
|
|
Writer.set_textpos(ssd, y0, min(x, x1 - tlen))
|
2021-06-11 14:33:05 +00:00
|
|
|
wri.setcolor(txtcolor, self.bgcolor)
|
2021-06-09 16:11:48 +00:00
|
|
|
wri.printstring(txt)
|
|
|
|
ys = self.ldy0 # Large tick
|
|
|
|
yl = self.ldl
|
|
|
|
elif tick == 4:
|
|
|
|
ys = self.mdy0
|
|
|
|
yl = self.mdl
|
|
|
|
else:
|
|
|
|
ys = self.sdy0
|
|
|
|
yl = self.sdl
|
|
|
|
if self.tickcb is None:
|
|
|
|
color = self.fgcolor
|
|
|
|
else:
|
|
|
|
color = self.tickcb(vt, self.fgcolor)
|
|
|
|
display.vline(x, ys, yl, color) # Draw tick
|
|
|
|
if (not tick) and (vt > 0.999 * self.mval):
|
|
|
|
break # Drawn last tick at end of data
|
|
|
|
else:
|
|
|
|
vs *= 10 # More to do. Next decade.
|
|
|
|
done = False
|
|
|
|
if done:
|
|
|
|
break
|
|
|
|
|
|
|
|
display.vline(xc, y0, y1 - y0, self.ptrcolor) # Draw pointer
|
|
|
|
#print(ticks_diff(ticks_ms(), start)) 75-95ms on Pyboard D depending on calbacks
|
|
|
|
|
|
|
|
def _constrain(self, v):
|
|
|
|
return min(max(v, 1.0), self.mval)
|
|
|
|
|
|
|
|
def value(self, val=None): # User method to get or set value
|
|
|
|
if val is not None:
|
|
|
|
v = self._constrain(val)
|
|
|
|
if self._value is None or v != self._value:
|
|
|
|
self._value = v
|
|
|
|
self.draw = True # Ensure a redraw on next refresh
|
|
|
|
self.callback(self, *self.args)
|
|
|
|
return self._value
|
|
|
|
|
|
|
|
|
2021-07-04 17:21:37 +00:00
|
|
|
# Adjust widget's value. Args: button pressed, amount of increment
|
|
|
|
def do_adj(self, button, val):
|
|
|
|
if isinstance(button, int): # Using an encoder
|
2022-02-06 12:05:38 +00:00
|
|
|
delta = self.delta * self.encoder_rate * 0.1 if self.precision() else self.delta * self.encoder_rate
|
2021-07-04 17:21:37 +00:00
|
|
|
self.value(self.value() * (1 + delta)**val)
|
2021-07-08 12:18:05 +00:00
|
|
|
else: # val == 1 or -1
|
|
|
|
asyncio.create_task(self.btnhan(button, val))
|
2021-07-04 17:21:37 +00:00
|
|
|
|
2021-06-09 16:11:48 +00:00
|
|
|
async def btnhan(self, button, up):
|
|
|
|
up = up == 1
|
2022-02-06 12:05:38 +00:00
|
|
|
if self.precision():
|
2021-06-16 13:15:06 +00:00
|
|
|
delta = self.delta * 0.1
|
|
|
|
maxdelta = self.delta
|
|
|
|
else:
|
|
|
|
delta = self.delta
|
|
|
|
maxdelta = 0.64
|
2021-06-09 16:11:48 +00:00
|
|
|
smul= (1 + delta) if up else (1 / (1 + delta))
|
|
|
|
self.value(self.value() * smul)
|
|
|
|
t = ticks_ms()
|
2022-02-06 12:05:38 +00:00
|
|
|
while button():
|
2021-06-09 16:11:48 +00:00
|
|
|
await asyncio.sleep_ms(0) # Quit fast on button release
|
|
|
|
if ticks_diff(ticks_ms(), t) > 500: # Button was held down
|
|
|
|
delta = min(maxdelta, delta * 2)
|
|
|
|
smul = (1 + delta) if up else (1 / (1 + delta))
|
|
|
|
self.value(self.value() * smul)
|
|
|
|
t = ticks_ms()
|