code.py
  1  # SPDX-FileCopyrightText: 2020 Jeff Epler for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  # pylint: disable=redefined-outer-name,no-self-use,broad-except,try-except-raise,too-many-branches,too-many-statements,unused-import
  6  
  7  import gc
  8  import time
  9  
 10  from adafruit_display_text.label import Label
 11  from adafruit_hid.keyboard import Keyboard
 12  from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
 13  from jepler_udecimal import Decimal, getcontext, localcontext
 14  import jepler_udecimal.utrig  # Needed for trig functions in Decimal
 15  import board
 16  import digitalio
 17  import displayio
 18  import framebufferio
 19  import microcontroller
 20  import sharpdisplay
 21  import terminalio
 22  
 23  try:
 24      import usb_hid
 25  except ImportError:
 26      usb_hid = None
 27  
 28  # Initialize the display, cleaning up after a display from the previous
 29  # run if necessary
 30  displayio.release_displays()
 31  framebuffer = sharpdisplay.SharpMemoryFramebuffer(board.SPI(), board.RX, 400, 240)
 32  display = framebufferio.FramebufferDisplay(framebuffer, auto_refresh=False)
 33  
 34  def extraprec(add=8, num=0, den=1):
 35      def inner(fn):
 36          def wrapper(*args, **kw):
 37              with localcontext() as ctx:
 38                  ctx.prec = ctx.prec + add + (ctx.prec * num + den - 1) // den
 39                  result = fn(*args, **kw)
 40              return +result
 41          return wrapper
 42      return inner
 43  
 44  class AngleConvert:
 45      def __init__(self):
 46          self.state = 0
 47  
 48      def next_state(self):
 49          self.state = (self.state + 1) % 3
 50  
 51      def __str__(self):
 52          return "DRG"[self.state]
 53  
 54      @property
 55      def factor(self):
 56          return [360, None, 400][self.state]
 57  
 58      def from_user(self, x):
 59          factor = self.factor
 60          if factor is None:
 61              return x
 62          x = x.remainder_near(factor)
 63          pi_4 = Decimal("1.0").atan()
 64          return x * pi_4 * 8 / factor
 65  
 66      def to_user(self, x):
 67          factor = self.factor
 68          if factor is None:
 69              return x
 70          pi_4 = Decimal("1.0").atan()
 71          return x * factor / pi_4 / 8
 72  
 73      @extraprec(num=1)
 74      def cos(self, x):
 75          return self.from_user(x).cos()
 76  
 77      @extraprec(num=1)
 78      def sin(self, x):
 79          return self.from_user(x).sin()
 80  
 81      @extraprec(num=1)
 82      def tan(self, x):
 83          return self.from_user(x).tan()
 84  
 85      @extraprec(num=1)
 86      def acos(self, x):
 87          return self.to_user(x.acos())
 88  
 89      @extraprec(num=1)
 90      def asin(self, x):
 91          return self.to_user(x.asin())
 92  
 93      @extraprec(num=1)
 94      def atan(self, x):
 95          return self.to_user(x.atan())
 96  
 97  getcontext().prec = 14
 98  getcontext().Emax = 99
 99  getcontext().Emin = -99
100  
101  def get_pin(x):
102      if isinstance(x, microcontroller.Pin):
103          return digitalio.DigitalInOut(x)
104      return x
105  
106  class MatrixKeypadBase:
107      def __init__(self, row_pins, col_pins):
108          self.row_pins = [get_pin(p) for p in row_pins]
109          self.col_pins = [get_pin(p) for p in col_pins]
110          self.old_state = set()
111          self.state = set()
112  
113          for r in self.row_pins:
114              r.switch_to_input(digitalio.Pull.UP)
115          for c in self.col_pins:
116              c.switch_to_output(False)
117  
118      def scan(self):
119          self.old_state = self.state
120          state = set()
121          for c, cp in enumerate(self.col_pins):
122              cp.switch_to_output(False)
123              for r, rp in enumerate(self.row_pins):
124                  if not rp.value:
125                      state.add((r, c))
126              cp.switch_to_input()
127          self.state = state
128          return state
129  
130      def rising(self):
131          old_state = self.old_state
132          new_state = self.state
133  
134          return new_state - old_state
135  
136  class LayerSelect:
137      def __init__(self, idx=1, next_layer=None):
138          self.idx = idx
139          self.next_layer = next_layer or self
140  
141  LL0 = LayerSelect(0)
142  LL1 = LayerSelect(1)
143  LS1 = LayerSelect(1, LL0)
144  
145  class MatrixKeypad:
146      def __init__(self, row_pins, col_pins, layers):
147          self.base = MatrixKeypadBase(row_pins, col_pins)
148          self.layers = layers
149          self.layer = LL0
150          self.pending = []
151  
152      def getch(self):
153          if not self.pending:
154              self.base.scan()
155              for r, c in self.base.rising():
156                  op = self.layers[self.layer.idx][r][c]
157                  if isinstance(op, LayerSelect):
158                      self.layer = op
159                  else:
160                      self.pending.extend(op)
161                      self.layer = self.layer.next_layer
162  
163          if self.pending:
164              return self.pending.pop(0)
165  
166          return None
167  
168  col_pins = (board.D10, board.D9, board.D6, board.TX)
169  row_pins = (board.A0, board.A1, board.A2, board.A3, board.A4, board.A5)
170  
171  BS = '\x7f'
172  CR = '\n'
173  
174  layers = (
175      (
176          ('^', 'l', 'r', LS1),
177          ('s', 'c', 't', '/'),
178          ('7', '8', '9', '*'),
179          ('4', '5', '6', '-'),
180          ('1', '2', '3', '+'),
181          ('0', '.',  BS,  CR)
182      ),
183  
184      (
185          ('v', 'L', 'R', LL0),
186          ('S', 'C', 'T', 'N'),
187          ( '',  '',  '',  ''),
188          ( '',  '',  '', 'n'),
189          ( '',  '',  '',  ''),
190          ('=', '@',  BS, '~')
191      ),
192  )
193  
194  
195  class Impl:
196      def __init__(self):
197          # incoming keypad
198          self.keypad = MatrixKeypad(row_pins, col_pins, layers)
199  
200          # outgoing keypresses
201          self.keyboard = None
202          self.keyboard_layout = None
203  
204          g = displayio.Group()
205  
206          self.labels = labels = []
207          labels.append(Label(terminalio.FONT, scale=2, color=0))
208          labels.append(Label(terminalio.FONT, scale=3, color=0))
209          labels.append(Label(terminalio.FONT, scale=3, color=0))
210          labels.append(Label(terminalio.FONT, scale=3, color=0))
211          labels.append(Label(terminalio.FONT, scale=3, color=0))
212          labels.append(Label(terminalio.FONT, scale=3, color=0))
213  
214          for li in labels:
215              g.append(li)
216  
217          bitmap = displayio.Bitmap((display.width + 126)//127, (display.height + 126)//127, 1)
218          palette = displayio.Palette(1)
219          palette[0] = 0xffffff
220  
221          tile_grid = displayio.TileGrid(bitmap, pixel_shader=palette)
222          bg = displayio.Group(scale=127)
223          bg.append(tile_grid)
224  
225          g.insert(0, bg)
226  
227          display.show(g)
228  
229      def getch(self):
230          while True:
231              time.sleep(.02)
232              c = self.keypad.getch()
233              if c is not None:
234                  return c
235  
236      def setline(self, i, text):
237          li = self.labels[i]
238          text = text[:31] or " "
239          if text == li.text:
240              return
241          li.text = text
242          li.anchor_point = (0,0)
243          li.anchored_position = (1, max(1, 41 * i - 7) + 6)
244  
245      def refresh(self):
246          pass
247  
248      def paste(self, text):
249          if self.keyboard is None:
250              if usb_hid:
251                  self.keyboard = Keyboard(usb_hid.devices)
252                  self.keyboard_layout = KeyboardLayoutUS(self.keyboard)
253              else:
254                  return
255  
256          if self.keyboard_layout is None:
257              raise ValueError("USB HID not available")
258          text = str(text)
259          self.keyboard_layout.write(text)
260          raise RuntimeError("Pasted")
261  
262      def start_redraw(self):
263          display.auto_refresh = False
264  
265      def end_redraw(self):
266          display.auto_refresh = True
267  
268      def end(self):
269          pass
270  impl = Impl()
271  
272  stack = []
273  entry = []
274  
275  def do_op(arity, fun):
276      if arity > len(stack):
277          return "underflow"
278      res = fun(*stack[-arity:][::-1])
279      del stack[-arity:]
280      if isinstance(res, list):
281          stack.extend(res)
282      elif res is not None:
283          stack.append(res)
284      return None
285  angleconvert = AngleConvert()
286  
287  def roll():
288      stack[:] = stack[1:] + stack[:1]
289  
290  def rroll():
291      stack[:] = stack[-1:] + stack[:-1]
292  
293  def swap():
294      stack[-2:] = [stack[-1], stack[-2]]
295  
296  ops = {
297      '\'': (1, lambda x: -x),
298      '\\': (2, lambda x, y: x/y),
299      '#': (2, lambda x, y: y**(1/x)),
300      '*': (2, lambda x, y: y*x),
301      '+': (2, lambda x, y: y+x),
302      '-': (2, lambda x, y: y-x),
303      '/': (2, lambda x, y: y/x),
304      '^': (2, lambda x, y: y**x),
305      'v': (2, lambda x, y: y**(1/x)),
306      '_': (2, lambda x, y: x-y),
307      '@': angleconvert.next_state,
308      'C': (1, angleconvert.acos),
309      'c': (1, angleconvert.cos),
310      'L': (1, Decimal.exp),
311      'l': (1, Decimal.ln),
312      'q': (1, lambda x: x**.5),
313      'r': roll,
314      'R': rroll,
315      'S': (1, angleconvert.asin),
316      's': (1, angleconvert.sin),
317      '~': swap,
318      'T': (1, angleconvert.atan),
319      't': (1, angleconvert.tan),
320      'n': (1, lambda x: -x),
321      'N': (1, lambda x: 1/x),
322      '=': (1, impl.paste)
323  }
324  
325  def pstack(msg):
326      impl.setline(0, f'[{angleconvert}] {msg}')
327  
328      for i, reg in enumerate("TZYX"):
329          if len(stack) > 3-i:
330              val = stack[-4+i]
331          else:
332              val = ""
333          impl.setline(1+i, f"{reg} {val}")
334  
335  def loop():
336      impl.start_redraw()
337      pstack(f'{gc.mem_free()} RPN bytes free')
338      impl.setline(5, "> " + "".join(entry) + "_")
339      impl.refresh()
340      impl.end_redraw()
341  
342      while True:
343          do_pstack = False
344          do_pentry = False
345          message = ''
346  
347  
348          c = impl.getch()
349          if c in '\x7f\x08':
350              if entry:
351                  entry.pop()
352                  do_pentry = True
353              elif stack:
354                  stack.pop()
355                  do_pstack = True
356          if c == '\x1b':
357              del entry[:]
358              do_pentry = True
359          elif c in '0123456789.eE':
360              if c == '.' and '.' in entry:
361                  c = 'e'
362              entry.append(c)
363              do_pentry = True
364          elif c == '\x04':
365              break
366          elif c in ' \n':
367              if entry:
368                  try:
369                      stack.append(Decimal("".join(entry)))
370                  except Exception as e:
371                      message = str(e)
372                  del entry[:]
373              elif c == '\n' and stack:
374                  stack.append(stack[-1])
375              do_pstack = True
376          elif c in ops:
377              if entry:
378                  try:
379                      stack.append(Decimal("".join(entry)))
380                  except Exception as e:
381                      message = str(e)
382                  del entry[:]
383              op = ops.get(c)
384              try:
385                  if callable(op):
386                      message = op() or ''
387                  else:
388                      message = do_op(*op) or ''
389              except (KeyboardInterrupt, SystemExit):
390                  raise
391              except Exception as e:
392                  message = str(e)
393              do_pstack = True
394  
395          impl.start_redraw()
396  
397          if do_pstack:
398              pstack(message)
399              do_pentry = True
400  
401          if do_pentry:
402              impl.setline(5, "> " + "".join(entry) + "_")
403  
404          if do_pentry or do_pstack:
405              impl.refresh()
406  
407          impl.end_redraw()
408  
409  try:
410      loop()
411  finally:
412      impl.end()