review my pull request. 
this files has changed:
RPi/fake_smbus.py
dro.py
test_model.py
todo.txt

test_model.py passes all tests, here is the result:
´´´bash
smbus2 not available; using fake smbus2.py
........
----------------------------------------------------------------------
Ran 8 tests in 0.000s

OK
´´´

here is the diff between my version of RPi/fake_smbus.py and main:
´´´diff
+import tkinter as tk
+
+encoders = []
+class SMBus:
+    def __init__(self, bus):
+        print(f"fake SMBus initialized on bus {bus}")
+        self.bus = bus
+
+
+    def read_i2c_block_data(self, addr, cmd, length):
+        data = []
+        for encoder in encoders:
+            pos_as_byte_array = encoder.get_position().to_bytes(2, 'little', signed=True)
+            data.append(pos_as_byte_array[0])
+            data.append(pos_as_byte_array[1])
+
+        #print(f"SMBus data set to: {data}")
+        return data
+
+def createTestEncoder():
+    encoder = TestEncoder()
+    encoders.append(encoder)
+    return encoder
+
+class TestEncoder:
+    """ represent a test encoder controlled by keyboard input """
+    def __init__(self):
+        self._position = 0
+
+    def _rotate_cw(self, event=None):
+        self._position += 1
+
+    def _rotate_ccw(self, event=None):
+        self._position -= 1
+
+    def get_position(self) -> int:
+        return self._position
´´´

---

here is the diff between my version of dro.py and main:
´´´diff
diff --git a/dro.py b/dro.py
index 5071466..6e3b919 100755
--- a/dro.py
+++ b/dro.py
@@ -1,396 +1,370 @@
 #/usr/bin/python3
-import math
-import encoder
-import threading
-import time
 import tkinter as tk
 from tkinter import simpledialog
 from enum import IntEnum
-import RPi.GPIO as GPIO
 import atexit
 import subprocess
 import getopt
 import sys
 import shutil
+import logging
+
+logger = logging.getLogger(__name__)
+
+# Prefer system-installed `smbus2`, otherwise fall back to local `smbus2.py`.
+try:
+    import smbus2
+except ImportError:
+    import RPi.fake_smbus as smbus2  # type: ignore
+    logger.warning('smbus2 not available; using fake smbus2.py')

 class XMode(IntEnum):
     RADIUS = 0
     DIAMETER = 1

 class Updated(IntEnum):
     POS_X = 0
     POS_Z = 1
     X_MODE = 2
     FULLSCREEN = 3

-X_DIRECTION_PIN = 13 #gpio 23
-X_CLOCK_PIN = 11 #gpio 24
-Z_DIRECTION_PIN = 18 #gpio 17
-Z_CLOCK_PIN = 16 #gpio 27
-
 class Axis:
     """ represent one axis in the model """
-    def __init__(self, name):
-        self._position = 0
-        self._scale = 1
+    def __init__(self, name: str):
+        self._position = 0 # raw position from encoder
+        self._scale = 1.0
+        self._offset = 0.0
         self._name = name

-    def set_position(self, pos):
+    def get_raw_position(self):
+        """Return the uncompensated raw position (no offset applied)."""
+        return self._position
+
+    def set_position(self, pos: int):
         self._position = pos

-    def get_position(self):
-        return self._position
+    def set_offset(self, offset: float):
+        self._offset = offset

-    def get_position_as_string(self):
-        return str("{:.2f}".format(self._position))
+    def get_position(self):
+        return self._position + self._offset

-    def set_scale(self, scale):
-        self._scale  = scale
+    def set_scale(self, scale: float):
+        self._scale = scale

-    def update_position(self, direction):
-        self._position += direction * self._scale
+    def get_scale(self) -> float:
+        """Return the scale factor (mm per step)."""
+        return self._scale


 class Model:
     """ represent the DRO model """
     def __init__(self, fullscreen = False):
         self._observers = []
         self._x_mode = XMode.RADIUS # denote x axis mode (radius/diameter)

         self._axis = {}
         self._axis['x'] = Axis('x')
         self._axis['z'] = Axis('z')

         self._fullscreen = fullscreen

     def attatch(self, o):
         self._observers.append(o)

     def notify(self, updated = None):
         for o in self._observers:
             o.update(updated)

-    def set_position(self, pos, axis):
+    def steps_to_position(self, axis, steps):
         if axis in self._axis:
-            factor = 0.5 if axis == 'x' and self.get_x_mode() == XMode.DIAMETER else 1
-            self._axis[axis].set_position(factor*pos)
+            return steps * self._axis[axis].get_scale()
+        return 0

-            self.notify(self._string_to_updated_axis(axis))
+    def set_position(self, axis, pos):
+        """ set position in mm. Always radius for x axis """
+        if axis in self._axis:
+            if abs(self._axis[axis].get_position() - pos) >= 0.005:
+                self._axis[axis].set_position(pos)
+                self.notify(self._string_to_updated_axis(axis))

-    def get_position_as_string(self, axis):
+    def set_offset(self, axis, offset):
+        """ set offset in mm. Always radius for x axis """
         if axis in self._axis:
-            return str("{:.2f}".format(self.get_position(axis)))
-        return None
+            self._axis[axis].set_offset(offset)
+            self.notify(self._string_to_updated_axis(axis))

     def get_position(self, axis):
         if axis in self._axis:
-            factor = 2 if axis == 'x' and self.get_x_mode() == XMode.DIAMETER else 1
-            return factor*self._axis[axis].get_position()
+            return self._axis[axis].get_position()
         return None

-    def update_position(self, direction, axis):
-        """ update position of given axis and notify observers """
+    def get_position_uncompensated(self, axis):
+        """ get position without offset applied """
         if axis in self._axis:
-            self._axis[axis].update_position(direction)
-
-            self.notify(self._string_to_updated_axis(axis))
+            return self._axis[axis].get_raw_position()
+        return None

     def get_x_mode(self):
         return self._x_mode

     def set_toggle_x_mode(self):
         if self._x_mode == XMode.RADIUS:
             self._x_mode = XMode.DIAMETER
-
         elif self._x_mode == XMode.DIAMETER:
             self._x_mode = XMode.RADIUS

         self.notify(Updated.X_MODE)

     def set_scale(self, axis, scale):
         if axis in self._axis:
             self._axis[axis].set_scale(scale)

     def set_fullscreen(self, v):
         self._fullscreen = v
         self.notify(Updated.FULLSCREEN)

     def is_fullscreen(self):
         return self._fullscreen

     def _string_to_updated_axis(self, axis_str):
         if axis_str == 'x':
             return Updated.POS_X
         if axis_str == 'z':
             return Updated.POS_Z

         return None

 class Controller:
     """ represent the controller in the MVC pattern """
     def __init__(self, model, test_mode = False):
         self._model = model
         self._test_mode = test_mode

-    def handle_x_position_update(self, dir):
-        self._model.update_position(dir, 'x')
+        self._bus = smbus2.SMBus(1)
+        self._address = 0x08              # Arduino I2C Address
+
+    def handle_x_position_update(self, steps):
+        self._model.set_position('x', self._model.steps_to_position('x', steps))

-    def handle_z_position_update(self, dir):
-        self._model.update_position(dir, 'z')
+    def handle_z_position_update(self, steps):
+        self._model.set_position('z', self._model.steps_to_position('z', steps))

     def handle_btn_x0_press(self):
-        self._model.set_position(0, 'x')
+        self._model.set_offset('x', -self._model.get_position_uncompensated('x'))

     def handle_btn_z0_press(self):
-        self._model.set_position(0, 'z')
+        self._model.set_offset('z', -self._model.get_position_uncompensated('z'))

     def handle_btn_toggle_x_mode(self):
         self._model.set_toggle_x_mode()

     def hanlde_btn_x(self):
         title = 'current radius' if self._model.get_x_mode() == XMode.RADIUS else 'current diameter'
         pos = simpledialog.askfloat('x', title, initialvalue=0.0, minvalue=-4000.0, maxvalue=4000.0)
+
+        if self._model.get_x_mode() == XMode.DIAMETER and pos is not None:
+            pos = pos / 2.0
+
         if pos is not None:
-            self._model.set_position(pos, 'x')
+            self._model.set_offset('x', pos-self._model.get_position_uncompensated('x'))

     def hanlde_btn_z(self):
         pos = simpledialog.askfloat('z', 'current z position', initialvalue=0.0, minvalue=-4000.0, maxvalue=4000.0)
         if pos is not None:
-            self._model.set_position(pos, 'z')
+            self._model.set_offset('z', pos-self._model.get_position_uncompensated('z'))

     def toggle_fullscreen(self, event=None):
         if self._model.is_fullscreen() == True:
             self._model.set_fullscreen(False)
         else:
             self._model.set_fullscreen(True)

     def end_fullscreen(self, event=None):
         self._model.set_fullscreen(False)

     def handle_btn_calc(self):
         calculator_cmd = shutil.which('galculator')

         if calculator_cmd != None:
             try:
                 subprocess.Popen(calculator_cmd)
             except:
                 print("can't start calculator")
         else:
             print("can't find calculator")

     def shutdown(self):
         if not self._test_mode:
             subprocess.call('sudo shutdown -h now', shell=True)
         else:
             print('will not shutdown in test mode')

+    def poll_i2c_data(self):
+        """ poll I2C data from Arduino """
+        try:
+            data = self._bus.read_i2c_block_data(self._address, 0, 4)
+            #self.handle_x_position_update(int(data[0] + (data[1] << 8)))
+            self.handle_x_position_update(int.from_bytes(bytes(data[0:2]), 'little', signed=True))
+            self.handle_z_position_update(int.from_bytes(bytes(data[2:4]), 'little', signed=True))
+        except Exception as e:
+            logger.error(f"I2C read error: {e}")
+
 class View:
     """ represent the view in the MVC pattern """
     def __init__(self, model, test_mode = False):
         self._model = model
         self._model.attatch(self)
         self._controller = Controller(model, test_mode)
-        self.x_dirty = False
-        self.z_dirty = False
-
-        # use tkinter's after() loop on the main thread instead of a background thread
-        # This avoids cross-thread UI calls and reduces context switching overhead.

         self.window = tk.Tk()
         self.window.resizable(False, False)
         #self.window.attributes('-zoomed', zoomed)  # This just maximizes it so we can see the window. It's nothing to do with fullscreen.
         self.window.attributes("-fullscreen", self._model.is_fullscreen())

         w_frame = tk.Frame(self.window)
         w_frame.grid(row=0, column=0, sticky='n')
         e_frame = tk.Frame(self.window)
         e_frame.grid(row=0, column=1, sticky='n')

         # x frame
         x_frame = tk.Frame(w_frame, relief=tk.GROOVE, borderwidth=3)
         x_frame.grid(row=0, column=0, padx=5, pady=5)
         # use a StringVar for quicker updates from the main thread
-        self.x_var = tk.StringVar(value=self._model.get_position_as_string('x'))
+        self.x_var = tk.StringVar(value=self._position_to_string(self._model.get_position('x')))
         self.x_label = tk.Label(x_frame, textvariable=self.x_var, anchor="e", font=("Helvetica", 80), fg="green", bg="black", width=6)
         self.x_label.grid(row=0, column=0, padx=5, pady=5)
         btn_set_x = tk.Button(master=x_frame, text="X", padx=5, pady=5, font=("Helvetica", 80), command=self._controller.hanlde_btn_x)
         btn_set_x.grid(row=0, column=1, padx=5, pady=5, sticky="nw")
         btn_set_x0 = tk.Button(master=x_frame, text="X_0", padx=5, pady=5, font=("Helvetica", 80), command=self._controller.handle_btn_x0_press)
         btn_set_x0.grid(row=0, column=2, padx=5, pady=5, sticky="nw")

         # Z frame
         z_frame = tk.Frame(w_frame, relief=tk.GROOVE, borderwidth=3)
         z_frame.grid(row=1, column=0, padx=5, pady=5)#, sticky="nw")
-        self.z_var = tk.StringVar(value=self._model.get_position_as_string('z'))
+        self.z_var = tk.StringVar(value=self._position_to_string(self._model.get_position('z')))
         self.z_label = tk.Label(z_frame, textvariable=self.z_var, anchor="e", font=("Helvetica", 80), fg="green", bg="black", width=6)
         self.z_label.grid(row=0, column=0, padx=5, pady=5)
         btn_set_z = tk.Button(master=z_frame, text="Z", padx=5, pady=5, font=("Helvetica", 80), command=self._controller.hanlde_btn_z)
         btn_set_z.grid(row=0, column=1, padx=5, pady=5, sticky="nw")
         btn_set_z0 = tk.Button(master=z_frame, text="Z_0", padx=5, pady=5, font=("Helvetica", 80), command=self._controller.handle_btn_z0_press)
         btn_set_z0.grid(row=0, column=2, padx=5, pady=5, sticky="nw")

         # status frame
         status_frame = tk.Frame(w_frame, relief=tk.GROOVE, borderwidth=3)
         status_frame.grid(row=3, column=0, padx=5, pady=5, sticky='e')
         self.x_mode_var = tk.StringVar(value=str(self._mode_to_text()))
         self.x_mode_label = tk.Label(status_frame, textvariable=self.x_mode_var, font=("Helvetica", 80), fg="red")
         self.x_mode_label.grid(row=0, column=0, padx=5, pady=5, sticky="e")

         # button frame
         btn_frame = tk.Frame(e_frame, relief=tk.GROOVE, borderwidth=3)
         btn_frame.grid(row=0, column=0, padx=5, pady=5)

         btn_rd = tk.Button(master=btn_frame, text="r/D", padx=5, pady=5, font=("Helvetica", 80), width=4, command=self._controller.handle_btn_toggle_x_mode)
         btn_rd.grid(row=1, column=0, padx=5, pady=5, sticky="nw")

         btn_calc = tk.Button(master=btn_frame, text="Calc", padx=5, pady=5, font=("Helvetica", 80), width=4, command=self._controller.handle_btn_calc)
         btn_calc.grid(row=2, column=0, padx=5, pady=15, sticky="nw")

         #btn_enter = tk.Button(master=btn_frame, text="TBD", padx=5, pady=5, font=("Helvetica", 80), width=4)#, command=self._controller.horizontal_end)
         self.photo = tk.PhotoImage(file="power.png")
         btn_off = tk.Button(master=btn_frame, image=self.photo, command=self._controller.shutdown)
         btn_off.grid(row=3, column=0, padx=5, pady=15, sticky="nw")

         # bind keyboar inputs
         self.window.bind("<F11>", self._controller.toggle_fullscreen)
         self.window.bind("<Escape>", self._controller.end_fullscreen)

         # schedule the GUI update loop on the main thread
-        self._last_x = self._model.get_position('x')
-        self._last_z = self._model.get_position('z')
         self._update_interval_ms = 100  # update at 10Hz by default
-        self.window.after(self._update_interval_ms, self._update_view)
+        self.window.after(self._update_interval_ms, self._poll_input)
         self.window.update()
-        #self.update()

     def _mode_to_text(self):
         if self._model.get_x_mode() == XMode.DIAMETER:
             return 'D'
         if self._model.get_x_mode() == XMode.RADIUS:
             return 'r'

     def _position_to_string(self, pos):
         return str("{:.2f}".format(pos))

     def start(self):
         self.window.mainloop()

     def update_x(self):
         """ update x position in view (main thread) """
-        self.x_dirty = False
         pos = self._model.get_position('x')
-        # avoid frequent updates when change is negligible
-        if abs(pos - self._last_x) >= 0.005:
-            self._last_x = pos
-            self.x_var.set(self._position_to_string(pos))
+        if self._model.get_x_mode() == XMode.DIAMETER:
+            pos = pos * 2.0
+        self.x_var.set(self._position_to_string(pos))

     def update_z(self):
         """ update z position in view (main thread) """
-        self.z_dirty = False
         pos = self._model.get_position('z')
-        if abs(pos - self._last_z) >= 0.005:
-            self._last_z = pos
-            self.z_var.set(self._position_to_string(pos))
+        self.z_var.set(self._position_to_string(pos))

     def update(self, updated = None):
         """ called by model when something has changed """
         if updated == Updated.POS_X:
-            self.x_dirty = True
+            self.update_x()
         if updated == Updated.POS_Z:
-            self.z_dirty = True
+            self.update_z()
         if updated == Updated.X_MODE:
             # main thread will pick this up through the scheduled update
             self.x_mode_var.set(str(self._mode_to_text()))
             self.update_x()
         if updated == Updated.FULLSCREEN:
             self.window.attributes("-fullscreen", self._model.is_fullscreen())

-    def _update_view(self):
+    def _poll_input(self):
         """ scheduled function (runs on tkinter main loop) to update the view."""
         try:
-            if self.x_dirty:
-                self.update_x()
-            if self.z_dirty:
-                self.update_z()
+            # request data
+            self._controller.poll_i2c_data()
+
         finally:
             # re-schedule
-            self.window.after(self._update_interval_ms, self._update_view)
-
-@atexit.register
-def cleanup():
-    GPIO.cleanup(X_CLOCK_PIN)
-    GPIO.cleanup(X_DIRECTION_PIN)
-    GPIO.cleanup(Z_CLOCK_PIN)
-    GPIO.cleanup(Z_DIRECTION_PIN)
-    print("done")
-
-def test(arg):
-    print(arg)
-
-
-class TestEncoder:
-    """ represent a test encoder controlled by keyboard input """
-    def __init__(self, view, event_cbk, inc_key = 'a', dec_key = 'b'):
-
-        view.window.bind(inc_key, self._rotate_cw)
-        view.window.bind(dec_key, self._rotate_ccw)
-
-        self._event_cbk = event_cbk
-
-    def _rotate_cw(self, event=None):
-        self._event_cbk(encoder.Direction.CW)
-
-    def _rotate_ccw(self, event=None):
-        self._event_cbk(encoder.Direction.CCW)
+            self.window.after(self._update_interval_ms, self._poll_input)

 def main():
     _zoomed = False
     _test = False

     try:
         opts, args = getopt.getopt(sys.argv[1:], "", ["zoomed", "test"])
     except getopt.GetoptError as err:
         # print help information and exit:
         print(err) # will print something like "option -a not recognized"
         sys.exit(2)

     for o, a in opts:
-        print(o)
-        print(a)
         if o == "--zoomed":
             _zoomed = True
         elif o == "--test":
             _test = True
         else:
             assert False, "unhandled option"

     model = Model(_zoomed)
     view = View(model, _test)

     model.set_scale('x', -2.5/200) #negative to flip direction
     model.set_scale('z', 90/1000)

-    GPIO.setwarnings(False)       ## Turn off warnings
-    GPIO.setmode(GPIO.BOARD)      ## Use BOARD pin numbering
-    GPIO.setup(X_DIRECTION_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
-    GPIO.setup(Z_DIRECTION_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
-    GPIO.setup(X_CLOCK_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
-    GPIO.setup(Z_CLOCK_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
-
     if _test:
-        x_test_encoder = TestEncoder(view, view._controller.handle_x_position_update, 'a', 'z')
-        z_test_encoder = TestEncoder(view, view._controller.handle_z_position_update, 's', 'x')
-
-    x_encoder = encoder.Encoder(X_CLOCK_PIN, X_DIRECTION_PIN, view._controller.handle_x_position_update)
-    z_encoder = encoder.Encoder(Z_CLOCK_PIN, Z_DIRECTION_PIN, view._controller.handle_z_position_update)
-
-    GPIO.add_event_detect(X_CLOCK_PIN, GPIO.RISING)
-    GPIO.add_event_detect(Z_CLOCK_PIN, GPIO.RISING)
-
-    GPIO.add_event_callback(X_CLOCK_PIN, x_encoder.update)
-    GPIO.add_event_callback(Z_CLOCK_PIN, z_encoder.update)
+        try:
+            x_test_encoder = smbus2.createTestEncoder()
+            view.window.bind('a', x_test_encoder._rotate_cw)
+            view.window.bind('z', x_test_encoder._rotate_ccw)
+            z_test_encoder = smbus2.createTestEncoder()
+            view.window.bind('s', z_test_encoder._rotate_cw)
+            view.window.bind('x', z_test_encoder._rotate_ccw)
+        except:
+            logger.error("couldn't create test encoders...")

     view.start()

 if __name__ == '__main__':
     main()
´´´

---

here is the diff between my version of test_model.py and main:
´´´diff
diff --git a/test_model.py b/test_model.py
index dae5311..91e21b5 100644
--- a/test_model.py
+++ b/test_model.py
@@ -1,95 +1,93 @@
 import unittest
 import dro
-import encoder

 class Observer:
     def __init__(self):
         self.reset()
         self.nbr_of_calls = 0
         self.has_been_called = False

     def update(self, updated = None):
         self.nbr_of_calls += 1
         self.has_been_called = True
         self.updated = updated

     def reset(self):
         self.nbr_of_calls = 0
         self.has_been_called = False

 class TestModel(unittest.TestCase):
     #def check_pos(self, actual, expected):
     #    self.assertTrue(int(self.model.get_position_as_string('x')) == 1)

     def setUp(self):
         self.model = dro.Model()

     def test_init(self):
         self.assertEqual(self.model.get_position('x'), 0)
         self.assertEqual(self.model.get_position('z'), 0)

-    def test_notify_observers(self):
-        o = Observer()
-        self.model.attatch(o)
-
-        self.assertFalse(o.has_been_called)
-        self.model.update_position(encoder.Direction.CW, 'x')
-        self.assertTrue(o.has_been_called)
-        self.assertTrue(int(self.model.get_position('x')) == 1)
-        self.assertTrue(o.updated == dro.Updated.POS_X)
-
-        o.reset()
-
-        self.model.update_position(encoder.Direction.CW, 'z')
-        self.assertTrue(int(self.model.get_position('z')) == 1)
-        self.assertTrue(o.has_been_called)
-        self.assertTrue(int(self.model.get_position('z')) == 1)
-        self.assertTrue(o.updated == dro.Updated.POS_Z)
-
     def test_wrong_axis(self):
         o = Observer()
         self.model.attatch(o)
-        self.model.set_position(1, 'apa')
+        self.model.set_position('apa', 1)

         self.assertFalse(o.has_been_called)

     def test_set_pos(self):
         o = Observer()
         self.model.attatch(o)

-        self.model.set_position(1, 'x')
+        self.model.set_position('x', 1)
         self.assertTrue(o.updated == dro.Updated.POS_X)
-        self.model.set_position(-11, 'z')
+        self.model.set_position('z', -11)
         self.assertTrue(o.updated == dro.Updated.POS_Z)

         self.assertEqual(o.nbr_of_calls, 2)
         self.assertEqual(int(self.model.get_position('x')), 1)
         self.assertEqual(int(self.model.get_position('z')), -11)

     def test_get_pos_as_string(self):
-        self.model.set_position(-11.49, 'x')
+        self.model.set_position('x', -11.49)

         self.assertEqual(self.model.get_position('x'), -11.49)

     def test_toogle_x_mode(self):
         o = Observer()
         self.model.attatch(o)

-        self.model.set_position(-11.49, 'x')
+        self.model.set_position('x', -11.49)
         self.model.set_toggle_x_mode()
-        self.assertEqual(self.model.get_position('x'), -22.98)
+        self.assertEqual(self.model.get_position('x'), -11.49)
         self.assertTrue(o.updated == dro.Updated.X_MODE)

-    def test_set_diam(self):
-        if self.model.get_x_mode() == dro.XMode.RADIUS:
-            self.model.set_toggle_x_mode()
-        self.assertEqual(self.model.get_x_mode(), dro.XMode.DIAMETER)
-
-        self.model.set_position(10, 'x')
-        self.assertEqual(self.model.get_position('x'), 10)
-        self.model.set_toggle_x_mode()
-        self.assertEqual(self.model.get_position('x'), 5)
+    def test_set_position_no_notify_if_unchanged(self):
+        o = Observer()
+        self.model.attatch(o)
+
+        self.model.set_position('x', 10)
+        self.assertEqual(o.nbr_of_calls, 1)
+
+        self.model.set_position('x', 10)
+        self.assertEqual(o.nbr_of_calls, 1)  # No change, no notify
+
+        self.model.set_position('x', 20)
+        self.assertEqual(o.nbr_of_calls, 2)  # Changed, should notify
+
+    def test_set_position_no_notify_if_small_change(self):
+        o = Observer()
+        self.model.attatch(o)
+
+        self.model.set_position('x', 10)
+        self.assertEqual(o.nbr_of_calls, 1)
+
+        self.model.set_position('x', 10+0.003)
+        self.assertEqual(o.nbr_of_calls, 1)  # No change, no notify

+    def test_steps_to_position(self):
+        self.model.set_scale('x', 2.5/200)  # 200 steps per unit
+        pos = self.model.steps_to_position('x', 500)
+        self.assertEqual(pos, 6.25)  # 200 * 1.5 = 300 steps

 if __name__ == '__main__':
    unittest.main()
´´´

---

here is the diff between my version of todo.txt and main:
´´´diff
diff --git a/todo.txt b/todo.txt
index cf193f0..fd78b5e 100644
--- a/todo.txt
+++ b/todo.txt
@@ -1,46 +1,50 @@
-askfloat funkar inte med alltid??????

-tester för r/D och ser/read position
+askfloat funkar inte med alltid??????

+KLAR i2c:
+   handle_btn_x0_press måste sätta en offset i axis som gör att offset+position blir 0
+   self._model.set_offset('x', -get_position)
+   motsvarande för hanlde_btn_x
+
 KLART
 desktop-filen pekar på ett shell-script som startar dro.py
 dro.desktop autostartar inte???
 men går att starta via file-explorer?????

 KLART
 stänga av med shutdown utan lösen:
    sudo visudo
    user_name ALL=(ALL) NOPASSWD: /sbin/poweroff, /sbin/reboot, /sbin/shutdown

 KLART
 stänga av med fysisk knapp:
    "adding this to /boot/config.txt:
       dtoverlay=gpio-shutdown

 KLART avstängningsknapp
    knapp som kortsluter pin 5 (GPIO3, grön) och jord
    och
    "adding this to /boot/config.txt:
    dtoverlay=gpio-shutdown
    "
    och
    https://askubuntu.com/questions/168879/shutdown-from-terminal-without-entering-password

    SW
    knapp till funktion/command:
    subprocess.call('sudo shutdown -h now', shell=True)

 utöka test.py
     *funkar det med olika ISR? eller måste det vara en central?
     VERKAR FUNKA

     lägg till z också...
     GJODE ETT TEST MED z_clock_pin på gpio13 FUNKAR...

      HW funkar inte
         testa enligt c:\Users\johan\Documents\verkstad\emco compact 5\bob\my-bob.drawio
         // 1. 4.7kOhm motstånd pull up, inte pull up i HW...
         // verkar bli för liten spänning
         eller
         2. inget motstånd, pull up i HW... FUNKAR...
            det blir 3.22V HIGH på GPIO
´´´
