# SPDX-License-Identifier: LGPL-2.1-or-later
# adapted from https://github.com/bluez/bluez/blob/master/test/example-advertisement
#----------------------------------------------------------------
# BleuIO (for BleuIO UB serial device)  module for a standalone python-3.6 or later AirPlay Service-Discovery Bluetooth LE beacon for UxPlay 
# (c)  F. Duncanh, March 2026


# **** This implementation requires a blueio dongle https://bleuio.com/bluetooth-low-energy-usb-ssd005.php
# This device has a self-contained bluetooth LE stack packaged as a usb serial modem.
# It is needed on macOS because macOS does not permit users to send manufacturer-specific BLE advertisements
# with its native BlueTooth stack.    It works also on linux and windows.

import time
import os
import ipaddress

try:
    import serial
    from serial.tools import list_ports
except ImportError as e:
    print(f'ImportError: {e}, failed to import required serial port support')
    print(f'install pyserial')
    raise SystemExit(1)

#global variables
advertised_port = None
advertised_address = None
serial_port = None
advertisement_parameters = None
airplay_advertisement = None

# --- Serial Communication Helper Functions ---
def send_at_command(serial_port, command):
    # Sends an AT command and reads the response.
    serial_port.write(f"{command}\r\n".encode('utf-8'))
    time.sleep(0.1) # Give the dongle a moment to respond
    response = ""
    while serial_port.in_waiting:
        response += serial_port.readline().decode('utf-8')
    response_without_empty_lines = os.linesep.join(
        [line for line in response.splitlines() if line]
    )
    return response_without_empty_lines

#check AdvInterval
def check_adv_intrvl(min, max):
    if not (100 <= min):
        raise ValueError('advmin was smaller than 100 msecs')
    if not (max >= min):
        raise ValueError('advmax  was smaller than advmin')
    if not (max <= 10240):
        raise ValueError('advmax was larger than 10240 msecs')

from typing import Literal
def setup_beacon(ipv4_str: str, port: int, advmin: int, advmax: int, index: Literal[None]) ->bool:
    global advertised_port
    global advertised_address
    global airplay_advertisement
    global advertisement_parameters   
    if index is not None:
        raise ValuError('uxplay_beacon_module_BleuIO called with value of index: not None')
    check_adv_intrvl(advmin, advmax)
    #  set up advertising message:
    assert port > 0
    assert port <= 65535
    ipv4_address = ipaddress.ip_address(ipv4_str)
    port_bytes = port.to_bytes(2, 'big')
    data = bytearray([0xff, 0x4c, 0x00]) # ( 3 bytes) type manufacturer_specific 0xff, manufacturer id Apple 0x004c
    data.extend(bytearray([0x09, 0x08, 0x13, 0x30])) #  (4 bytes) Apple Data Unit type 9 (Airplay),  Apple data length 8, Apple flags 0001 0011, seed 30
    data.extend(bytearray(ipv4_address.packed))  # (4 bytes) ipv4 address
    data.extend(port_bytes) # (2 bytes) port
    length = len(data)   # 13 bytes                                                                                                                                 
    adv_data = bytearray([length])   # first byte of message data unit is length of meaningful data that follows (0x0d = 13)
    adv_data.extend(data)
    airplay_advertisement = ':'.join(format(b,'02x') for b in adv_data)
    advertisement_parameters = "0;" + str(advmin) + ";" + str(advmax) + ";0;"  # non-connectable mode, min ad internal, max ad interval, time = unlimited
    advertised_address = ipv4_str
    advertised_port = port
    return True

def beacon_on() ->bool:
    global advertised_port
    ser = None
    try:
        print(f'Connecting to BleuIO dongle on {serial_port} ....')
        with serial.Serial(serial_port, 115200, timeout = 1) as ser:
            print(f'Connection established')
            #Start advertising
            response = send_at_command(ser, "AT+ADVDATA=" +  airplay_advertisement)
            #print(response)
            response = send_at_command(ser, "AT+ADVSTART=" + advertisement_parameters)
            #print(f'{response}')
            print(f'AirPlay Service Discovery advertising started, port = {advertised_port} ip address = {advertised_address}')
    except serial.SerialException as e:
        print(f"beacon_on: Serial port error: {e}")
        raise SystemExit(1)
        advertised_port = None
    except Exception as e:
        print(f"beacon_on: An unexpected error occurred: {e}")
        advertised_port = None
    finally:
        if ser is not None:
            ser.close()
        return advertised_port
    
def beacon_off():
    global advertisement_parameters
    global airplay_advertisement
    global advertised_port
    global advertised_address
    ser = None
     # Stop advertising
    try:
        with serial.Serial(serial_port, 115200, timeout = 1) as ser:
            response = send_at_command(ser, "AT+ADVSTOP")
            #print(f'{response}')
            print(f'AirPlay Service-Discovery beacon advertisement stopped')
            airplay_advertisement = None
            advertised_Port = None
            advertised_address = None
            advertisement_parameters = None
    except serial.SerialException as e:
        print(f"beacon_off: Serial port error: {e}")
    except Exception as e:
        print(f"beacon_off: An unexpected error occurred: {e}")
    finally:
        if ser is not None:
            ser.close()

from typing import Optional
def find_device(serial_port_in: Optional[str]) ->Optional[str]:
    global serial_port
    serial_ports = list(list_ports.comports())
    serial_port_found = False
    serial_port = None
    TARGET_VID = '0x2DCF'   # used by BleuIO and BleuIO Pro
    target_vid = int(TARGET_VID,16)

    if serial_port_in is not None:
        for p in serial_ports:
            if getattr(p, 'vid', None) == target_vid or TARGET_VID in p.hwid:
                if p.device == serial_port_in:
                    serial_port = serial_port_in
                    break

    if serial_port is None:
        count = 0
        for p in serial_ports:
            if getattr(p, 'vid', None) == target_vid or TARGET_VID in p.hwid:
                count+=1
                if count == 1:
                    serial_port = p.device
                    print(f'=== detected BlueuIO {count}. port: {p.device} desc: {p.description} hwid: {p.hwid}')
                if count>1:
                    print(f'warning: {count} BleueIO devices were found, the first found will be used')
                    print(f'(to override this choice, specify "--device =..." in optional arguments)')

    if serial_port is None:
        return serial_port
    
    #test access to serial_port
    try:
        with serial.Serial(serial_port, 115200, timeout = 1) as ser:
            send_at_command(ser, "AT")
            ser.close()
    except Exception as e:
        print(f"beacon_on: Serial port error: {e}")
        text='''
  The user does not have sufficient privileges to access this serial port:
  On Linux, the user should be added to the "dialout" or "uucp" group
  On BSD systems, the necesary group is usually the "dialer" group.
  The correct group can be found using '''
        print(text, f'"ls -l {serial_port}"')
        raise SystemExit(1)
    return serial_port

print(f'Imported uxplay_beacon_module_BleuIO')
