# import necessary libraries
from microbit import *
import gc
from machine import time_pulse_us
import neopixel
import music

# define here your Joy Car Mainboard Revision
joycar_rev = 1.3

# initialize I2C interface for the Joy Car Mainboard
i2c.init(freq=400000, sda=pin20, scl=pin19)

# initialize PWM controller
i2c.write(0x70, b'\x00\x01')
i2c.write(0x70, b'\xE8\xAA')

# the deceleration of a motor bias can be used to compensate for different motor speeds
biasR = 0  # deceleration of the right motor in percent
biasL = 0  # deceleration of the left motor in percent

# control motors using the PWM controller
# PWM0 and PWM1 for the left motor and PWM2 and PWM3 for the right motor
def drive(PWM0, PWM1, PWM2, PWM3):
    # The scale function is used to rescale the bias variables for
    # the calculation of the motor speed
    def scale(num, in_min, in_max, out_min, out_max):
        return (num - in_min) * (out_max - out_min) / (in_max - in_min) + out_min

    # Scaling of the deceleration value to the value in percent
    PWM0 = int(PWM0 * (scale(biasR, 0, 100, 100, 0) / 100))
    PWM1 = int(PWM1 * (scale(biasR, 0, 100, 100, 0) / 100))
    PWM2 = int(PWM2 * (scale(biasL, 0, 100, 100, 0) / 100))
    PWM3 = int(PWM3 * (scale(biasL, 0, 100, 100, 0) / 100))

    # transmit value for PWM channel (0-255) to PWM controller
    # 0x70 is the I2C address of the controller.
    # the byte with the PWM value is added to the byte for the channel
    i2c.write(0x70, b'\x02' + bytes([PWM0]))
    i2c.write(0x70, b'\x03' + bytes([PWM1]))
    i2c.write(0x70, b'\x04' + bytes([PWM2]))
    i2c.write(0x70, b'\x05' + bytes([PWM3]))

# get all sensor data
def fetchSensorData():
    # Since the zfill function is not included in micro:bit Micropython,
    # it must be inserted as a function
    def zfill(s, width):
        return '{:0>{w}}'.format(s, w=width)

    # Read hexadecimal data and convert to binary
    data = "{0:b}".format(ord(i2c.read(0x38, 1)))
    # fill in the data to 8 digits if necessary
    data = zfill(data, 8)
    # declare bol_data_dict as dictionary
    bol_data_dict = {}
    # Counter for the loop that enters the data from data into bol_data_dict
    bit_count = 7
    # Transfer the data from data to bol_data_dict
    for i in data:
        if i == "0":
            bol_data_dict[bit_count] = False
            bit_count -= 1
        else:
            bol_data_dict[bit_count] = True
            bit_count -= 1

    # after main board revision 1.3, the speed sensors are on separate pins
    if joycar_rev >= 1.3:
        bol_data_dict[8], bol_data_dict[9] = bol_data_dict[0], bol_data_dict[1]
        bol_data_dict[0] = bool(pin14.read_digital())
        bol_data_dict[1] = bool(pin15.read_digital())

    # bit 0 = SpeedLeft, bit 1 = SpeedRight, bit 2 = LineTrackerLeft,
    # bit 3 = LineTrackerMiddle, bit 4 = LineTrackerRight,
    # bit 5 = ObstclLeft, bit 6 = ObstclRight, bit 7 = free pin(7)
    # (bit 8 = free (pin0) bit 9 = free (pin1)) - just with revision 1.3 or newer
    return bol_data_dict

# define pins for ultrasonic sensor
trigger = pin8
echo = pin12

# initialize pins for ultrasonic sensor
trigger.write_digital(0)
echo.read_digital()

# method to calculate distance from ultrasonic sensor
def get_distance():
    # collect garbage
    gc.collect()
    # set short impulse onto the trigger pin
    trigger.write_digital(1)
    trigger.write_digital(0)
    # meassure time until echo pin is high
    duration = time_pulse_us(echo, 1)
    # calculate distance
    distance = ((duration / 1000000) * 34300) / 2
    # return the distance rounded to 2 decimal digits
    return round(distance, 2)

# setup pins for servomotor
pin1.set_analog_period(10)
pin13.set_analog_period(10)

# method to change position to servo motors
def servo(channel, position):
    # method to scale from 0-180 (°) to 100-200 (us)
    def scale(num, in_min, in_max, out_min, out_max):
        # Rückgabe des auf eine ganze Zahl gerundeten Werts
        return (round((num - in_min) * (out_max - out_min) /
                (in_max - in_min) + out_min))
    # check if position is in range
    if position < 0 and position > 180:
        return "position not in range"
    # send position to selected channel
    if channel == 1:
        pin1.write_analog(scale(position, 0, 180, 100, 200))
    elif channel == 2:
        pin13.write_analog(scale(position, 0, 180, 100, 200))

# define object for the lights
np = neopixel.NeoPixel(pin0, 8)

# --- LEDs ---
# set all LEDs to white
for x in range(0,8):
    np[x] = (255, 255, 255)
np.show()
# print prompt to check LED state
print("Does all the LEDs work? y/n")
ans = input()
# save data
if ans == "y" or ans == "Y":
    led_state = True
elif ans == "n" or ans == "N":
    led_state = False
else:
    led_state = None
# deactivate all LEDs again
for x in range(0,8):
    np[x] = (0, 0, 0)
np.show()

# --- Obstacle sensors ---
# save current time
time = running_time()
# print prompt to check left obstacle sensor state
print("Hold something in front of the left obstacle sensor.")
left_obstacle_state = False
# loop limited on 5 seconds to prevent infinite loop if sensor does not work
while running_time()-time < 5000:
    # fetch sensor data
    sensor_data = fetchSensorData()
    # if something is recognized by sensor -> end loop and set state true
    if sensor_data[5] is True:
        left_obstacle_state = True
        break
if left_obstacle_state is True:
    left_obstacle_state = False
    # save current time
    time = running_time()
    # loop limited on 5 seconds to prevent infinite loop if sensor does not work
    while running_time()-time < 5000:
        # fetch sensor data
        sensor_data = fetchSensorData()
        # if something is recognized by sensor -> end loop and set state true
        if sensor_data[5] is False:
            left_obstacle_state = True
            break
# save current time
time = running_time()
# print prompt to check right obstacle sensor state
print("Hold something in front of the right obstacle sensor.")
right_obstacle_state = False
# loop limited on 5 seconds to prevent infinite loop if sensor does not work
while running_time()-time < 5000:
    # fetch sensor data
    sensor_data = fetchSensorData()
    # if something is recognized by sensor -> end loop and set state true
    if sensor_data[6] is False:
        right_obstacle_state = True
        break
if right_obstacle_state is True:
    right_obstacle_state = False
    # save current time
    time = running_time()
    # loop limited on 5 seconds to prevent infinite loop if sensor does not work
    while running_time()-time < 5000:
        # fetch sensor data
        sensor_data = fetchSensorData()
        # if something is recognized by sensor -> end loop and set state true
        if sensor_data[6] is False:
            right_obstacle_state = True
            break

# --- Line sensors ---
print("Lie the Joy Car on the side")
# save current time
time = running_time()
# variables for
all_line_state = False
left_line_state = False
middle_line_state = False
right_line_state = False
while running_time()-time < 10000:
    # fetch sensor data
    sensor_data = fetchSensorData()
    if sensor_data[2] is True and sensor_data[3] is True and sensor_data[4] is True:
        all_line_state = True
        break
if all_line_state is True:
    # print prompt to check left line sensor state
    print("Hold something in front of the left line sensor.")
    # save current time
    time = running_time()
    # loop limited on 5 seconds to prevent infinite loop if sensor does not work
    while running_time()-time < 5000:
        # fetch sensor data
        sensor_data = fetchSensorData()
        # if something is recognized by sensor -> end loop and set state true
        if sensor_data[2] is False:
            left_line_state = True
            break
    # print prompt to check left line sensor state
    print("Hold something in front of the middle line sensor.")
    # save current time
    time = running_time()
    # loop limited on 5 seconds to prevent infinite loop if sensor does not work
    while running_time()-time < 5000:
        # fetch sensor data
        sensor_data = fetchSensorData()
        # if something is recognized by sensor -> end loop and set state true
        if sensor_data[3] is False:
            middle_line_state = True
            break
    # print prompt to check left line sensor state
    print("Hold something in front of the right line sensor.")
    # save current time
    time = running_time()
    # loop limited on 5 seconds to prevent infinite loop if sensor does not work
    while running_time()-time < 5000:
        # fetch sensor data
        sensor_data = fetchSensorData()
        # if something is recognized by sensor -> end loop and set state true
        if sensor_data[4] is False:
            right_line_state = True
            break
print("Put the JoyCar back to normal position.")

# --- Speed sensors ---
# print prompt to check left speed sensor state
print("Rotate the left wheel.")
# save current time
time = running_time()
left_speed_state = False
# loop limited on 5 seconds to prevent infinite loop if sensor does not work
while running_time()-time < 5000:
    # fetch sensor data
    sensor_data = fetchSensorData()
    # if something is recognized by sensor -> end loop and set state true
    if sensor_data[0] is True:
        left_speed_state = True
        break
if left_speed_state is True:
    left_speed_state = False
    # save current time
    time = running_time()
    # loop limited on 5 seconds to prevent infinite loop if sensor does not work
    while running_time()-time < 5000:
        # fetch sensor data
        sensor_data = fetchSensorData()
        # if something is recognized by sensor -> end loop and set state true
        if sensor_data[0] is False:
            left_speed_state = True
            break
# print prompt to check right speed sensor state
print("Rotate the right wheel.")
# save current time
time = running_time()
right_speed_state = False
# loop limited on 5 seconds to prevent infinite loop if sensor does not work
while running_time()-time < 5000:
    # fetch sensor data
    sensor_data = fetchSensorData()
    # if something is recognized by sensor -> end loop and set state true
    if sensor_data[1] is True:
        right_speed_state = True
        break
if right_speed_state is True:
    right_speed_state = False
    # save current time
    time = running_time()
    # loop limited on 5 seconds to prevent infinite loop if sensor does not work
    while running_time()-time < 5000:
        # fetch sensor data
        sensor_data = fetchSensorData()
        # if something is recognized by sensor -> end loop and set state true
        if sensor_data[1] is False:
            right_speed_state = True
            break

# --- Motors ---
# print prompt to check motor state
print("Does the left wheel rotate? (Y/N)")
drive(0, 0, 0, 255)
ans = input()
# save data
if ans == "y" or ans == "Y":
    left_motor_state = True
elif ans == "n" or ans == "N":
    left_motor_state = False
else:
    left_motor_state = None
# print prompt to check motor state
print("Does the right wheel rotate? (Y/N)")
drive(0, 255, 0, 0)
ans = input()
# save data
if ans == "y" or ans == "Y":
    right_motor_state = True
elif ans == "n" or ans == "N":
    right_motor_state = False
else:
    right_motor_state = None
drive(0, 0, 0, 0)

# --- Servomotor ---
print("Does the servo on channel 1 move? (Y/N)")
servo(1, 0)
sleep(500)
servo(1, 180)
sleep(500)
servo(1, 90)
ans = input()
# save data
if ans == "y" or ans == "Y":
    servo_state = True
elif ans == "n" or ans == "N":
    servo_state = False
else:
    servo_state = None

# --- ADC ---
if pin2.read_analog() != 0:
    adc_state = True
else:
    adc_state = False

# --- Buzzer ---
music.pitch(440, 500, pin=pin16)
sleep(1000)
print("Did you hear a noise from the buzzer? (Y/N)")
ans = input()
if ans == "y" or ans == "Y":
    buzzer_state = True
elif ans == "n" or ans == "N":
    buzzer_state = False
else:
    buzzer_state = None

# --- print all ---
print("LEDs:", led_state)
print("Left obstacle sensor:", left_obstacle_state)
print("Right obstacle sensor:", right_obstacle_state)
print("Left line sensor:", left_line_state)
print("Middle line sensor:", middle_line_state)
print("Right line sensor:", right_line_state)
print("Left speed sensor:", left_speed_state)
print("Right speed sensor:", right_speed_state)
print("Left motor:", left_motor_state)
print("Right motor:", right_motor_state)
print("Servomotor:", servo_state)
print("ADC:", adc_state)
print("Buzzer:", buzzer_state)
