Infineon TeamJakob Valtl
Published © MIT

3D Magnetic Sensor 2 Go kit: The Ultimate 3D Magnetic Finger

Explore tactile sensing with the 3D Magnetic Finger, turning touch and deformation into real-time sensor data.

IntermediateProtip1 hour215
3D Magnetic Sensor 2 Go kit: The Ultimate 3D Magnetic Finger

Story

Read more

Code

MagneticFingerGUI.py

Python
import time
import math
import serial
import serial.tools.list_ports
import numpy as np

#  Common Configuration (change these once) 
DEAD_ZONE = 3
BAUD_RATE = 115200
SERIAL_TIMEOUT = 0.02       # 20 ms  more than enough for up to 5 pixels --> angepasst 

MAX_SENSOR_COUNT = 4
CONFIGURATIONS = [0x80, 0x80, 0x80, 0x98, 0x20]  # M109

ACTIVE = 0b1111  # binary of xyzt; unused dimensions output zero

CMD_READ = 0x48
CMD_ADDRESSING = 0x4F
CMD_CONF = 0x7A

import tkinter as tk


def init_serial():
    """Open the serial port and run the shared sensor init sequence."""
    ser = serial.Serial(find_port(), BAUD_RATE, timeout=SERIAL_TIMEOUT)

    ser.write((CMD_ADDRESSING,))
    ser.read(5)
    time.sleep(0.01)

    ser.write((CMD_CONF, *CONFIGURATIONS))
    ser.read(len(CONFIGURATIONS) + 1)
    time.sleep(0.001)
    return ser


#  GUI mode constants & function 
CANVAS_SIZE = 600
SENSOR_SPACING = 200
SCALE_FACTOR = 0.2
MOVE_SCALE = 8.0

BG_COLOR = '#1e1e2e'
GRID_COLOR = '#3a3a5c'
DOT_COLORS = ['#ff6b6b', '#51cf66', '#339af0', '#fcc419']
CIRCLE_COLORS = ['#ff8787', '#69db7c', '#5cb8ff', '#ffe066']
TEXT_COLOR = '#e0e0e0'
LABEL_COLOR_DIM = '#a0a0c0'
SENSOR_LABELS = ['Sensor 1', 'Sensor 2', 'Sensor 3', 'Sensor 4']
FONT_FAMILY = 'Segoe UI'

SENSOR_OFFSETS = [
    (-SENSOR_SPACING // 2,  SENSOR_SPACING // 2),
    ( SENSOR_SPACING // 2,  SENSOR_SPACING // 2),
    (-SENSOR_SPACING // 2, -SENSOR_SPACING // 2),
    ( SENSOR_SPACING // 2, -SENSOR_SPACING // 2),
]

# Per-sensor target values at rest (in raw units, before sign flip).
# After calibration, x and y read ~0 and z reads ~CALIBRATION_Z_TARGET.
CALIBRATION_SAMPLES = 100
CALIBRATION_Z_TARGET = 50


def calibrate_offsets(ser, num_samples=CALIBRATION_SAMPLES, max_sensors=4):
    """Collect num_samples frames and compute per-sensor [x, y, z] offsets so that
    after subtraction the sensor reads [0, 0, CALIBRATION_Z_TARGET] at rest.
    Make sure no magnet is near the board during this step."""
    sums = np.zeros((max_sensors, 3), dtype=np.float64)
    counts = np.zeros(max_sensors, dtype=np.int64)
    collected = 0
    print(f"Calibrating: collecting {num_samples} samples (keep magnet away)...")
    while collected < num_samples:
        ser.write((CMD_READ,))
        ser.read(1)
        rx_bytes = ser.read((ACTIVE.bit_count() + 1) * MAX_SENSOR_COUNT)
        sensor_count = len(rx_bytes) / (ACTIVE.bit_count() + 1)
        if int(sensor_count) != sensor_count or sensor_count == 0:
            continue
        sensor_count = min(int(sensor_count), max_sensors)
        for i in range(sensor_count):
            br = computeBr(rx_bytes[(ACTIVE.bit_count() + 1) * i:], ACTIVE)
            sums[i] += br[:3]
            counts[i] += 1
        collected += 1
        if collected % 100 == 0:
            print(f"  ...{collected}/{num_samples}")
    offsets = []
    target = np.array([0, 0, CALIBRATION_Z_TARGET], dtype=np.float64)
    for i in range(max_sensors):
        if counts[i] > 0:
            mean = sums[i] / counts[i]
            offsets.append([int(round(v)) for v in (mean - target)])
        else:
            offsets.append([0, 0, 0])
    print("Calibration complete. Offsets per sensor (x, y, z):")
    for i, o in enumerate(offsets):
        print(f"  Sensor {i}: [{o[0]}, {o[1]}, {o[2]}]")
    return offsets


def run_gui():
    ser = init_serial()
    bug_fix_offsets = calibrate_offsets(ser)

    cx_center = CANVAS_SIZE // 2
    cy_center = CANVAS_SIZE // 2

    root = tk.Tk()
    root.title("Magnet Distance Monitor")
    root.configure(bg=BG_COLOR)
    root.resizable(False, False)

    title_frame = tk.Frame(root, bg=BG_COLOR)
    title_frame.pack(pady=(12, 0))
    tk.Label(title_frame, text="Magnet Distance Monitor",
             font=(FONT_FAMILY, 16, 'bold'), fg=TEXT_COLOR, bg=BG_COLOR).pack()
    tk.Label(title_frame, text="Live sensor proximity view",
             font=(FONT_FAMILY, 9), fg=LABEL_COLOR_DIM, bg=BG_COLOR).pack()

    # Button frame for recalibration
    button_frame = tk.Frame(root, bg=BG_COLOR)
    button_frame.pack(pady=(8, 8))
    
    status_label = tk.Label(button_frame, text="Ready", 
                           font=(FONT_FAMILY, 8), fg='#69db7c', bg=BG_COLOR)
    status_label.pack(side=tk.LEFT, padx=(0, 8))
    
    def recalibrate():
        """Recalibrate the sensor offsets."""
        nonlocal bug_fix_offsets
        status_label.config(text="Calibrating...", fg='#fcc419')
        root.update()
        try:
            bug_fix_offsets = calibrate_offsets(ser)
            status_label.config(text="Calibration complete", fg='#69db7c')
        except Exception as e:
            print(f"Calibration error: {e!r}")
            status_label.config(text="Calibration failed", fg='#ff6b6b')
        root.after(2000, lambda: status_label.config(text="Ready", fg='#69db7c'))
    
    recalibrate_btn = tk.Button(button_frame, text="Recalibrate Offsets",
                               command=recalibrate,
                               font=(FONT_FAMILY, 9),
                               bg='#339af0', fg=TEXT_COLOR,
                               activebackground='#4c8fff', activeforeground=TEXT_COLOR,
                               relief=tk.FLAT, padx=10, pady=4)
    recalibrate_btn.pack(side=tk.LEFT)

    canvas = tk.Canvas(root, width=CANVAS_SIZE, height=CANVAS_SIZE,
                       bg=BG_COLOR, highlightthickness=0)
    canvas.pack(padx=16, pady=(8, 16))

    positions = [(cx_center + ox, cy_center + oy) for ox, oy in SENSOR_OFFSETS]
    for a, b in [(0, 1), (1, 3), (3, 2), (2, 0)]:
        canvas.create_line(*positions[a], *positions[b],
                           fill=GRID_COLOR, width=1, dash=(6, 4))

    dots, labels, glows, circles = [], [], [], []
    for i, (ox, oy) in enumerate(SENSOR_OFFSETS):
        cx = cx_center + ox
        cy = cy_center + oy
        glow = canvas.create_oval(cx, cy, cx, cy,
                                  outline=CIRCLE_COLORS[i], width=6, stipple='gray25')
        circle = canvas.create_oval(cx, cy, cx, cy,
                                    outline=CIRCLE_COLORS[i], width=2)
        dot = canvas.create_oval(cx - 5, cy - 5, cx + 5, cy + 5,
                                 fill=DOT_COLORS[i], outline='')
        label = canvas.create_text(cx, cy - 30, text=SENSOR_LABELS[i],
                                   font=(FONT_FAMILY, 14, "bold"), fill=LABEL_COLOR_DIM)
        dots.append(dot)
        labels.append(label)
        glows.append(glow)
        circles.append(circle)

    def update():
        try:
            ser.write((CMD_READ,))
            ser.read(1)
            # print(ser.read(1).hex())
            rx_bytes = ser.read((ACTIVE.bit_count() + 1) * MAX_SENSOR_COUNT)
            ser.read(10)
            # print(rx_bytes.hex())
            sensor_count = len(rx_bytes) / (ACTIVE.bit_count() + 1)
            if int(sensor_count) != sensor_count or sensor_count == 0:
                root.after(50, update)
                return
            sensor_count = min(int(sensor_count), 4)

            for i in range(sensor_count):
                br = computeBr(rx_bytes[(ACTIVE.bit_count() + 1) * i:], ACTIVE)
                br[:3] = br[:3] - np.array(bug_fix_offsets[i], dtype=br.dtype)
                # if (i==1 or i==2):
                    # br *= -1
                x, y, z = -br[0], -br[1], br[2]
                # Dead zone: ignore tiny x/y noise (|v| <= 1 -> 0)
                # print(i, x, y, z)
                if abs(x) <= DEAD_ZONE:
                    x = 0
                else:
                    x = math.copysign(abs(x) - DEAD_ZONE, x)                
                if abs(y) <= DEAD_ZONE:
                    y = 0
                else:
                    y = math.copysign(abs(y) - DEAD_ZONE, y)
                # print(i, x, y, z)
                magnitude = math.sqrt(x**2 + y**2 + z**2)
                display_r = max(magnitude * SCALE_FACTOR, 1)

                dx = math.copysign(math.sqrt(abs(x)), x) * MOVE_SCALE
                dy = math.copysign(math.sqrt(abs(y)), y) * MOVE_SCALE

                ox, oy = SENSOR_OFFSETS[i]
                cx = cx_center + ox + dx
                cy = cy_center + oy + dy
                canvas.coords(dots[i], cx - 5, cy - 5, cx + 5, cy + 5)
                canvas.coords(labels[i], cx, cy - display_r - 30)
                glow_r = display_r + 6
                canvas.coords(glows[i], cx - glow_r, cy - glow_r, cx + glow_r, cy + glow_r)
                canvas.coords(circles[i], cx - display_r, cy - display_r, cx + display_r, cy + display_r)
        except Exception as e:
            print(f"update() error: {e!r}")

        root.after(50, update)

    def on_close():
        ser.close()
        root.destroy()

    root.protocol("WM_DELETE_WINDOW", on_close)
    root.after(100, update)
    root.mainloop()
# 


def find_port():
    ports = serial.tools.list_ports.comports()
    hardware = 'USB-SERIAL CH340'
    manufacturer = 'Cypress'
    result = [port.device for port in ports if (port.description[:16] == hardware or port.manufacturer == manufacturer)]
    if len(result) != 1:
        exit('Make sure one USB-Serial CH340 is connected')
    return result[0]

def twos_complement(value, bits):
    if value & (1 << (bits - 1)):
        value -= 1 << bits
    return value

def computeBr(rx_Bytes, active): # active is binary of xyzt, non used will output zero for vector dimensions
    rx_Bytes = bytearray(rx_Bytes)
    if not (active & 0b1000):
        rx_Bytes.insert(0, 0)
    if not (active & 0b0100):
        rx_Bytes.insert(1, 0)
    if not (active & 0b0010):
        rx_Bytes.insert(2, 0)
    if not (active & 0b0001):
        rx_Bytes.insert(3, 0)
    x = int(rx_Bytes[0]<<2 | ((rx_Bytes[4] & 0b11000000) >> 6))
    y = int(rx_Bytes[1]<<2 | ((rx_Bytes[4] & 0b00110000) >> 4))
    z = int(rx_Bytes[2]<<2 | ((rx_Bytes[4] & 0b00001100) >> 2))
    t = int(rx_Bytes[3])
    c = int((rx_Bytes[4] & 0b00000011))
    result = [x, y, z]
    result = [twos_complement(r, 10) for r in result]
    result.append(twos_complement(t, 8))
    result.append(c)
    return np.array(result)

if __name__ == '__main__':
    run_gui()

Credits

Infineon Team
143 projects • 429 followers
Hands-on projects, tutorials, and code for sensors, MCUs, connectivity, security, power, and IoT. Follow for new builds and ideas.
Jakob Valtl
1 project • 0 followers

Comments