song_title = "Aschenbrödel Thema"
song_ticks = [281543696318464,0,4294967296,0,17592723046400,0,4294967296,0,2267742863360,0,4294967296,0,281543696318464,0,4294967296,0,17592722915328,0,4294967296,0,2267742863360,0,4294967296,0,281543696318464,0,2147483648,0,8796227764224,0,2147483648,0,618475814912,0,2147483648,0,281543696711680,0,2147483648,0,8796227239936,0,2147483648,0,618475814912,0,2147483648,0,281543697235968,0,4294967296,0,17592722915328,0,4294967296,0,2267743780864,0,4294967296,0,281543696318464,0,4294967296,0,17592722915328,0,4294967296,0,2267743780864,0,4294967296,0,70385925095424,0,536870912,0,2199061004288,0,536870912,0,154623016960,0,536870912,0,70385928241152,0,536870912,0,17592219598848,0,536870912,0,8813272891392,0,536870912,0,70385924571136,0,2147483648,0,134217728,0,2147483648,0,70385924177920,0,2147483648,0,8813273415680,0,2147483648,0,134217728,0,2147483648,0,70385925095424,0,2147483648,0,2216207319040,0,536870912,0,33554432,0,536870912,0,2216203124736,0,536870912,0,70385924046848,0,536870912,0,17592190238720,0,536870912,0,8813272891392,0,536870912,0,2267742863360,0,4294967296,0,536870912,0,4294967296,0,2267742765056,0,4294967296,0,70385924177920,0,2147483648,0,17592219598848,0,2147483648,0,8813277609984,0,33554432,0,2267760558080,0,4294967296,0,536870912,0,16777216,0,2199560126464,0,4294967296,0,17660905521152,0,536870912,0,8800387989504,0,16777216,0,2199560126464,0,4294967296,0,301334922264576]
model_name = "20-c"
resolution = 4

# =====================================================

models = { # (Linienabstand in mm, Liste mit MIDI-Nummern von tief nach hoch bzw. von rechts nach links)
"20-c"  : (3.0, [60,62,64,65,67,69,71,72,74,76,77,79,81,83,84,86,88,89,91,93]),
"30-c"  : (2.0, [53,55,60,62,64,65,67,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,91,93]),
"15-gis": (2.0, [68,70,72,73,75,77,79,80,82,84,85,87,89,91,92]),
}

seitschub_mm, midi_liste = models[model_name]
model_bits = [108 - midi for midi in midi_liste]

# =====================================================

import motor
import force_sensor
from hub import port, sound, button
import runloop
from math import pi

MOTOR_VERTICAL = port.B # Stift hoch und runter
MOTOR_LATERAL  = port.C # Seitliche Bewegung des Stiftes
SENSOR_START   = port.D # Kraftsensor zum Feintuning
MOTOR_VORSCHUB = port.E # Vorschub des Papierstreifens
SENSOR_PENTEST = port.F # Stifttest

beat_width = 8.0 # Breite einer Zaehlzeit in mm
durchmesser = 43.0 # Durchmesser der Antriebswalze in mm
vorschub_mm = beat_width / resolution # mm (r=16 -> v=0.5, r=8 -> v=1.0, r=4 -> v=2.0, r=2 -> v=4.0, r=1 -> v=8.0)
ritzel_klein = 8
ritzel_gross = 40
vorschub_pro_grad = pi * durchmesser / 360 / (ritzel_gross / ritzel_klein)
vorschub_grad = vorschub_mm / vorschub_pro_grad

abstand_zahn = 3.2 # mm
seitschub_grad = seitschub_mm / abstand_zahn * 360

stift_ab = 20 # in Grad
stift_auf = 320 # in Grad
stift_velo = 300
stift_haltezeit = 250 # ms

# =====================================================

def signum(x):
    return (x > 0) - (x < 0)

def to_bitlist_64(value):
    bits = []
    for i in range(63, -1, -1):
        bits.append((value >> i) & 1)
    return bits

def optimal_order(current, targets): # Optimierung (minimaler Weg)
    targets = sorted(targets)
    left = [x for x in targets if x < current]
    right = [x for x in targets if x >= current]
    route1 = left[::-1] + right # 1. Möglichkeit: erst nach links
    route2 = right + left[::-1] # 2. Möglichkeit:erst nach rechts
    def distance(route):
        total = abs(current - route[0])
        for i in range(len(route) - 1):
            total += abs(route[i] - route[i+1])
        return total
    return min([route1, route2], key=distance)

# ---------------------------
class myMotor: # absolute Motorsteuerung, stets bezogen auf den Ausgangswert, Motor darf nicht weiter als 359 Grad drehen!!!
# ---------------------------

    def __init__(self, portNum, delta, velocity):
        self.IST = 0 # speichern des tatsächlich gedrehten Winkels
        self.SOLL = 0 # aktueller Zielwert des Winkels
        self.portNum = portNum # merken
        self.delta = delta
        self.velocity = velocity
        self.startposition = motor.absolute_position(self.portNum) # absolute Startposition des Motors merken [−180°,+180°]
        self.pos_full = 0 # aktuelle glo
        self.position = 0
        self.cycle = 0

    async def step(self, vorzeichen):
        rotation = vorzeichen * self.delta
        self.SOLL += rotation
        self.drehung = round(self.SOLL - self.IST) # Abstand zum letzten IST-Wert drehen
        await motor.run_for_degrees(self.portNum, self.drehung, self.velocity, stop=motor.HOLD)
        pos_half_test = motor.absolute_position(self.portNum) # absolute Position des Motors lesen [−180°,+180°]
        pos_full_test = (pos_half_test - self.startposition) % 360 # aktuelle Position in der Form [0°,360°] geprüft!!!
        if rotation > 0:
            self.position += 1 # ein Vorschub rum
            if pos_full_test < self.pos_full:
                self.cycle += 1 # eine Runde rum
        if rotation < 0:
            self.position -= 1 # ein Vorschub rum
            if pos_full_test > self.pos_full:
                self.cycle -= 1 # eine Runde rum
        self.IST = 360 * self.cycle + pos_full_test
        self.pos_full = pos_full_test # merken
        await runloop.sleep_ms(50)

# ---------------------------
async def move_stift():
# ---------------------------

    await motor.run_to_absolute_position(MOTOR_VERTICAL, stift_ab, stift_velo, direction=motor.SHORTEST_PATH, stop=motor.BRAKE)
    await sound.beep(300, stift_haltezeit)
    await motor.run_to_absolute_position(MOTOR_VERTICAL, stift_auf, stift_velo, direction=motor.SHORTEST_PATH, stop=motor.BRAKE)

# ---------------------------
async def main():
# ---------------------------

    # 1. Stift in die Ausgangslage bringen
    await motor.run_to_absolute_position(MOTOR_VERTICAL, stift_auf, stift_velo, direction=motor.SHORTEST_PATH, stop=motor.BRAKE)

    # 2. Feineinstellungen per Kraftsensoren und Tasten am Hub
    while not force_sensor.force(SENSOR_START):
        if button.pressed(button.RIGHT):
            await motor.run_for_degrees(MOTOR_VORSCHUB, 5, 200, stop=motor.HOLD)
            await runloop.sleep_ms(500)
        if button.pressed(button.LEFT):
            await motor.run_for_degrees(MOTOR_VORSCHUB, -20, 200, stop=motor.HOLD)
            await runloop.sleep_ms(500)
        if force_sensor.force(SENSOR_PENTEST):
            await move_stift()
            await runloop.sleep_ms(800)
        await runloop.sleep_ms(20)
    await sound.beep(600, 1000)

    # 3. Motoren für Steuerung initialisieren
    motor_vorschub = myMotor(MOTOR_VORSCHUB, vorschub_grad, 300)
    motor_stiftpos = myMotor(MOTOR_LATERAL, seitschub_grad, 1000)

    # 4. Motor von der Testposition einen Takt weiter fahren = Anfang des Liedes
    for durchlauf in range(resolution):
        await motor_vorschub.step(+1) # Vorschub immer positiv

    # 5. Lied plotten
    for tick in song_ticks:
        if tick != 0:
            bitlist = to_bitlist_64(tick)
            bitlist.reverse()
            bitlist_filtered = [bitlist[index] for index in model_bits]
            positions = [i for i, bit in enumerate(bitlist_filtered) if bit == 1] # Positionen zum Lochen in aufsteigender Reihenfolge
            positions_opt = optimal_order(motor_stiftpos.position, positions) # umsortieren -> optimale Reihenfolge (minimaler Weg)
            for zielpos in positions_opt: # Zielpositionen abarbeiten
                delta = zielpos - motor_stiftpos.position
                anzahl = abs(delta) # Schritte zum aktuellen Ziel
                vorzeichen = signum(delta) # Richtung zum aktuellen Ziel
                for durchlauf in range(anzahl):
                    await motor_stiftpos.step(vorzeichen) # kürzester Weg
                await move_stift()
        await motor_vorschub.step(+1) # Vorschub immer positiv
    for durchlauf in range(motor_stiftpos.position): # Am Ende Stift wieder nach rechts fahren
        await motor_stiftpos.step(-1) # Zurück zur Ausgangsposition
    await sound.beep(600, 1000)
    await runloop.sleep_ms(100)

# ---------------------------

runloop.run(main())
raise SystemExit
