1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
  2. Anuncie Aqui
    Anuncie aqui você Também: fdantas@4each.com.br

[Python] Bluetooth low energy with calliope mini

Discussão em 'Python' iniciado por Stack, Setembro 7, 2025.

  1. Stack

    Stack Membro Participativo

    I would like to create a Bluetooth remote control for my Calliope Mini v2. The Calliope works with BLE (Bluetooth Low Energy), and I wrote the code for it in Python using Makecode. I used a BLE extension for this. Here is the code:

    def on_bluetooth_connected():
    global verbunden
    basic.show_icon(IconNames.YES)
    basic.set_led_color(0x00ff00)
    basic.pause(100)
    verbunden = 1
    bluetooth.on_bluetooth_connected(on_bluetooth_connected)

    def on_bluetooth_disconnected():
    global verbunden
    basic.show_icon(IconNames.NO)
    basic.set_led_color(0xff0000)
    basic.pause(100)
    verbunden = 0
    basic.pause(1000)
    basic.show_leds("""
    . . # # .
    # . # . #
    . # # # .
    # . # . #
    . . # # .
    """)
    basic.set_led_color(0x0000ff)
    bluetooth.on_bluetooth_disconnected(on_bluetooth_disconnected)

    def on_uart_data_received():
    global nachricht
    nachricht = bluetooth.uart_read_until(serial.delimiters(Delimiters.NEW_LINE))
    basic.show_string("nachricht")
    if nachricht == "F":
    basic.show_icon(IconNames.ARROW_SOUTH)
    calliBot2.motor(C2Motor.BEIDE, C2Dir.VORWAERTS, 100)
    elif nachricht == "S":
    basic.show_icon(IconNames.SQUARE)
    basic.pause(50)
    basic.show_icon(IconNames.SMALL_SQUARE)
    calliBot2.motor_stop(C2Motor.BEIDE, C2Stop.FREI)
    else:
    basic.show_string("unknown")
    bluetooth.on_uart_data_received(serial.delimiters(Delimiters.NEW_LINE),
    on_uart_data_received)

    nachricht = ""
    verbunden = 0
    basic.pause(2000)
    basic.show_leds("""
    # # # # #
    # # # # #
    # # # # #
    # # # # #
    # # # # #
    """)
    bluetooth.start_uart_service()
    bluetooth.set_transmit_power(7)
    basic.pause(1000)
    basic.show_leds("""
    . . # # .
    # . # . #
    . # # # .
    # . # . #
    . . # # .
    """)
    basic.set_led_color(0x0000ff)

    def on_forever():
    global nachricht
    nachricht = bluetooth.uart_read_until(serial.delimiters(Delimiters.NEW_LINE))
    basic.show_string("nachricht")
    if nachricht == "F":
    basic.show_icon(IconNames.ARROW_SOUTH)
    calliBot2.motor(C2Motor.BEIDE, C2Dir.VORWAERTS, 100)
    elif nachricht == "S":
    basic.show_icon(IconNames.SQUARE)
    basic.pause(50)
    basic.show_icon(IconNames.SMALL_SQUARE)
    calliBot2.motor_stop(C2Motor.BEIDE, C2Stop.FREI)
    else:
    basic.show_string("unknown")
    basic.forever(on_forever)



    I wrote the code for the remote control using Pycharm and Cloude, as I am not very familiar with the bleak and kivy extensions. Here is the code:

    import threading
    import time
    from bleak import BleakClient, BleakScanner
    from kivy.app import App
    from kivy.uix.boxlayout import BoxLayout
    from kivy.uix.gridlayout import GridLayout
    from kivy.uix.button import Button
    from kivy.uix.label import Label
    from kivy.config import Config
    from kivy.clock import Clock
    import asyncio

    # Mobile format settings (Pixel 4a)
    Config.set("graphics", "width", "393")
    Config.set("graphics", "height", "851")
    Config.set("graphics", "resizable", False)

    # Calliope UART UUIDs (standardized)
    UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
    UART_TX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" # App -> Calliope
    UART_RX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" # Calliope -> App


    class CalliopeApp(App):
    def __init__(self):
    super().__init__()
    # Bluetooth variables
    self.client = None
    self.connected = False
    self.bluetooth_thread = None
    self.bluetooth_loop = None
    self.calliope_mac = None
    # Store TX Characteristic for direct access
    self.tx_characteristic = None

    def build(self):
    main_layout = BoxLayout(orientation="vertical", spacing=10, padding=20)

    # Title
    title = Label(
    text="Calliope calli:bot Remote Control",
    size_hint_y=None,
    height=80,
    font_size=20,
    bold=True
    )
    main_layout.add_widget(title)

    # Bluetooth Connect/Disconnect Buttons
    bluetooth_layout = BoxLayout(
    size_hint_y=None,
    height=60,
    spacing=10
    )

    # Scan Button (Blue)
    self.scan_btn = Button(
    text="Search Calliope",
    font_size=16,
    bold=True,
    background_color=[0.2, 0.2, 0.8, 1]
    )
    self.scan_btn.bind(on_press=self.scan_for_calliope)

    # Connect Button (Green)
    self.connect_btn = Button(
    text="Connect",
    font_size=16,
    bold=True,
    background_color=[0.2, 0.8, 0.2, 1],
    disabled=True
    )
    self.connect_btn.bind(on_press=self.connect_to_calliope)

    # Disconnect Button (Yellow)
    self.disconnect_btn = Button(
    text="Disconnect",
    font_size=16,
    bold=True,
    background_color=[0.8, 0.8, 0.2, 1],
    disabled=True
    )
    self.disconnect_btn.bind(on_press=self.disconnect_from_calliope)

    bluetooth_layout.add_widget(self.scan_btn)
    bluetooth_layout.add_widget(self.connect_btn)
    bluetooth_layout.add_widget(self.disconnect_btn)
    main_layout.add_widget(bluetooth_layout)

    # Status display
    self.status_label = Label(
    text="Step 1: Press 'Search Calliope'",
    size_hint_y=None,
    height=120,
    font_size=14,
    bold=True,
    text_size=(350, None),
    halign="center",
    valign="middle"
    )
    main_layout.add_widget(self.status_label)

    # Control Buttons for calli:bot
    control_layout = GridLayout(
    cols=3,
    size_hint_y=None,
    height=400,
    spacing=10
    )

    # First row: [ ] [↑] [ ]
    control_layout.add_widget(Label())
    self.forward_btn = Button(
    text="Forward\n",
    font_size=16,
    bold=True,
    disabled=True
    )
    self.forward_btn.bind(on_press=self.send_forward)
    control_layout.add_widget(self.forward_btn)
    control_layout.add_widget(Label())

    # Second row: [←] [→]
    self.left_btn = Button(
    text="Left\n↺",
    font_size=16,
    bold=True,
    disabled=True
    )
    self.left_btn.bind(on_press=self.send_left)
    control_layout.add_widget(self.left_btn)

    self.stop_btn = Button(
    text="STOP\n⏹️",
    background_color=[1, 0.2, 0.2, 1],
    font_size=18,
    bold=True,
    disabled=True
    )
    self.stop_btn.bind(on_press=self.send_stop)
    control_layout.add_widget(self.stop_btn)

    self.right_btn = Button(
    text="Right\n↻",
    font_size=16,
    bold=True,
    disabled=True
    )
    self.right_btn.bind(on_press=self.send_right)
    control_layout.add_widget(self.right_btn)

    # Third row: [ ] [↓] [ ]
    control_layout.add_widget(Label())
    self.backward_btn = Button(
    text="Backward\n",
    font_size=16,
    bold=True,
    disabled=True
    )
    self.backward_btn.bind(on_press=self.send_backward)
    control_layout.add_widget(self.backward_btn)
    control_layout.add_widget(Label())

    main_layout.add_widget(control_layout)

    # Debug info
    debug_label = Label(
    text="For MakeCode: Expects F/B/L/R/S commands",
    size_hint_y=None,
    height=40,
    font_size=12
    )
    main_layout.add_widget(debug_label)

    return main_layout

    def scan_for_calliope(self, instance):
    """Bluetooth scan for Calliope devices"""
    self.update_status(" Searching for Calliope devices...")
    print("Starting Bluetooth scan...")

    scan_thread = threading.Thread(
    target=self.run_bluetooth_scan,
    daemon=True
    )
    scan_thread.start()

    def run_bluetooth_scan(self):
    """Bluetooth scan in separate thread"""
    try:
    scan_loop = asyncio.new_event_loop()
    asyncio.set_event_loop(scan_loop)
    scan_loop.run_until_complete(self._scan_for_devices())
    except Exception as e:
    print(f"Scan error: {e}")
    Clock.schedule_once(
    lambda dt: self.update_status(f"❌ Scan error: {str(e)}")
    )

    async def _scan_for_devices(self):
    """Bluetooth scan with improved Calliope detection"""
    try:
    print(" Starting 15-second Bluetooth scan...")
    Clock.schedule_once(
    lambda dt: self.update_status(" Scanning 15 seconds for Calliope...")
    )

    # Longer scan for better detection
    devices = await BleakScanner.discover(timeout=15.0)
    print(f" {len(devices)} Bluetooth devices found")

    calliope_devices = []
    for device in devices:
    name = device.name or "Unknown"
    mac = device.address
    print(f" Device: {name} ({mac})")

    # Extended Calliope detection
    calliope_patterns = [
    "calliope", "Calliope", "CALLIOPE",
    "BBC micro:bit", "micro:bit",
    "tivat", "mini" # From your log
    ]

    for pattern in calliope_patterns:
    if pattern.lower() in name.lower():
    calliope_devices.append((name, mac))
    print(f" ✅ Calliope detected: {name}")
    break

    if not calliope_devices:
    error_msg = f"❌ No Calliope detected from {len(devices)} devices"
    print(error_msg)
    Clock.schedule_once(
    lambda dt: self.update_status(
    f"{error_msg}\n\n"
    "Make sure:\n"
    "• Calliope is turned on\n"
    "• Bluetooth program is running\n"
    "• Within range (< 10m)"
    )
    )
    return

    # Use first found Calliope
    chosen_name, chosen_mac = calliope_devices[0]
    self.calliope_mac = chosen_mac

    success_msg = f"✅ Calliope found: {chosen_name}"
    print(success_msg)
    Clock.schedule_once(
    lambda dt: self.update_status(
    f"{success_msg}\n({chosen_mac})\n\nStep 2: Press 'Connect'"
    )
    )

    # Update buttons
    Clock.schedule_once(lambda dt: setattr(self.connect_btn, 'disabled', False))
    Clock.schedule_once(lambda dt: setattr(self.scan_btn, 'disabled', True))

    except Exception as e:
    error_msg = f"Scan failed: {str(e)}"
    print(f"❌ {error_msg}")
    Clock.schedule_once(
    lambda dt: self.update_status(f"❌ {error_msg}")
    )

    def connect_to_calliope(self, instance):
    """Establish connection"""
    if not self.calliope_mac:
    self.update_status("❌ First use 'Search Calliope'!")
    return

    self.update_status(" Establishing connection...")
    print(f"Connecting to Calliope: {self.calliope_mac}")

    self.bluetooth_thread = threading.Thread(
    target=self.run_bluetooth_connection,
    daemon=True
    )
    self.bluetooth_thread.start()

    def run_bluetooth_connection(self):
    """Connection establishment in separate thread"""
    try:
    self.bluetooth_loop = asyncio.new_event_loop()
    asyncio.set_event_loop(self.bluetooth_loop)
    self.bluetooth_loop.run_until_complete(self._connect_with_fixes())
    except Exception as e:
    print(f"Connection thread error: {e}")
    Clock.schedule_once(
    lambda dt: self.update_status(f"❌ Connection failed: {str(e)}")
    )

    async def _connect_with_fixes(self):
    """Connection with improved Windows support"""
    MAX_ATTEMPTS = 3

    for attempt in range(1, MAX_ATTEMPTS + 1):
    try:
    print(f" Connection attempt {attempt}/{MAX_ATTEMPTS}")
    Clock.schedule_once(
    lambda dt: self.update_status(
    f" Connection attempt {attempt}/{MAX_ATTEMPTS}"
    )
    )

    # STEP 1: Create client
    self.client = BleakClient(
    self.calliope_mac,
    timeout=30.0 # Longer timeout
    )

    # STEP 2: Connect with retry
    connected = False
    for connect_try in range(3):
    try:
    await self.client.connect()
    connected = True
    break
    except Exception as e:
    print(f" Connect attempt {connect_try + 1}: {e}")
    if connect_try < 2:
    await asyncio.sleep(3.0)

    if not connected:
    raise Exception("Connection failed after multiple attempts")

    print("✅ Basic connection established")

    # STEP 3: Load services with wait time
    Clock.schedule_once(
    lambda dt: self.update_status(" Loading Bluetooth services...")
    )

    await asyncio.sleep(3.0) # Important wait time for Windows
    services = self.client.services

    if not services:
    raise Exception("No services found")

    # STEP 4: Find UART service
    print(" Searching UART service...")
    uart_service = None
    service_count = 0

    for service in services:
    service_count += 1
    service_uuid = str(service.uuid).upper()
    print(f" Service {service_count}: {service_uuid}")

    if "6E400001" in service_uuid:
    uart_service = service
    print(f"✅ UART service found!")
    break

    if not uart_service:
    available = [str(s.uuid) for s in services]
    raise Exception(f"UART service not found. Available services: {available}")

    # STEP 5: Find and store TX Characteristic
    print(" Searching TX characteristic...")
    tx_char = None

    for char in uart_service.characteristics:
    char_uuid = str(char.uuid).upper()
    print(f" Characteristic: {char_uuid}")

    if "6E400002" in char_uuid:
    tx_char = char
    self.tx_characteristic = char # For later direct access
    print("✅ TX characteristic found and stored")
    break

    if not tx_char:
    raise Exception("TX characteristic not found")

    # STEP 6: Connection test with corrected formats
    print(" Testing communication with various formats...")
    await self._test_communication()

    # STEP 7: Success!
    self.connected = True
    success_msg = " Successfully connected!\nRemote control ready for calli:bot"

    print(success_msg)
    Clock.schedule_once(lambda dt: self.update_status(success_msg))
    Clock.schedule_once(lambda dt: self.update_button_states())

    return # Successful!

    except Exception as e:
    error_msg = f"Attempt {attempt} failed: {str(e)}"
    print(f"❌ {error_msg}")

    # Cleanup
    if self.client:
    try:
    await self.client.disconnect()
    except:
    pass
    self.client = None
    self.tx_characteristic = None

    if attempt == MAX_ATTEMPTS:
    final_error = (
    f"❌ All {MAX_ATTEMPTS} attempts failed\n\n"
    "Troubleshooting:\n"
    "• Restart Calliope\n"
    "• Restart Windows Bluetooth\n"
    "• Get closer to Calliope\n"
    "• Close other Bluetooth apps"
    )
    Clock.schedule_once(lambda dt: self.update_status(final_error))
    else:
    Clock.schedule_once(
    lambda dt: self.update_status(f"⏳ Waiting before attempt {attempt + 1}...")
    )
    await asyncio.sleep(5.0)

    async def _test_communication(self):
    """Test various communication formats for Calliope mini"""
    print(" Testing communication with various formats...")

    # These formats often work with micro:bit/Calliope
    test_formats = [
    b"test\r\n", # Windows line ending
    b"test\n", # Unix line ending
    b"test", # Without line ending
    "test".encode('utf-8'), # UTF-8 without line ending
    ]

    for i, data in enumerate(test_formats):
    try:
    print(f" Test format {i + 1}: {repr(data)}")
    await self.client.write_gatt_char(
    self.tx_characteristic,
    data,
    response=False # Important for micro:bit/Calliope
    )
    await asyncio.sleep(0.2) # Short pause between tests
    print(f" ✅ Format {i + 1} sent")
    except Exception as e:
    print(f" ❌ Format {i + 1} failed: {e}")

    def disconnect_from_calliope(self, instance):
    """Disconnect connection"""
    self.update_status(" Disconnecting...")
    disconnect_thread = threading.Thread(target=self.run_bluetooth_disconnect, daemon=True)
    disconnect_thread.start()

    def run_bluetooth_disconnect(self):
    """Bluetooth disconnection"""
    try:
    if not self.bluetooth_loop:
    self.bluetooth_loop = asyncio.new_event_loop()
    asyncio.set_event_loop(self.bluetooth_loop)
    self.bluetooth_loop.run_until_complete(self._disconnect_bluetooth())
    except Exception as e:
    print(f"Disconnect error: {e}")

    async def _disconnect_bluetooth(self):
    """Clean disconnection"""
    try:
    if self.client and self.connected:
    await self.client.disconnect()
    self.connected = False
    self.client = None
    self.tx_characteristic = None

    Clock.schedule_once(lambda dt: setattr(self.scan_btn, 'disabled', False))
    Clock.schedule_once(lambda dt: setattr(self.connect_btn, 'disabled', True))
    Clock.schedule_once(lambda dt: self.update_status(" Disconnected. New search possible."))
    Clock.schedule_once(lambda dt: self.update_button_states())
    print("Successfully disconnected")
    except Exception as e:
    print(f"Disconnect error: {e}")

    def update_status(self, message):
    """Update status text"""
    self.status_label.text = message

    def update_button_states(self):
    """Button states depending on connection"""
    control_buttons = [
    self.forward_btn, self.backward_btn,
    self.left_btn, self.right_btn, self.stop_btn
    ]

    for button in control_buttons:
    button.disabled = not self.connected

    self.disconnect_btn.disabled = not self.connected

    # Control commands for calli:bot
    def send_forward(self, instance):
    self.send_command("F")

    def send_backward(self, instance):
    self.send_command("B")

    def send_left(self, instance):
    self.send_command("L")

    def send_right(self, instance):
    self.send_command("R")

    def send_stop(self, instance):
    self.send_command("S")

    def send_command(self, command):
    """Send command to Calliope"""
    if not self.connected or not self.client or not self.tx_characteristic:
    self.update_status("❌ Not connected!")
    return

    print(f" Sending command: {command}")
    self.update_status(f" Sending: {command}")

    # Send command in separate thread
    send_thread = threading.Thread(
    target=self.run_bluetooth_send,
    args=(command,),
    daemon=True
    )
    send_thread.start()

    def run_bluetooth_send(self, command):
    """Improved Bluetooth sending for Calliope mini"""
    try:
    # Create event loop for send thread
    if not self.bluetooth_loop or self.bluetooth_loop.is_closed():
    print(" Creating new event loop for sending...")
    send_loop = asyncio.new_event_loop()
    asyncio.set_event_loop(send_loop)
    else:
    send_loop = self.bluetooth_loop

    # Test various formats - optimized for micro:bit/Calliope
    send_formats = [
    # Format 1: Command only (common with micro:bit)
    command.encode('utf-8'),

    # Format 2: With Carriage Return + Newline (Windows)
    f"{command}\r\n".encode('utf-8'),

    # Format 3: Only with Newline (Unix)
    f"{command}\n".encode('utf-8'),

    # Format 4: With Carriage Return
    f"{command}\r".encode('utf-8'),

    # Format 5: As single byte (if only one character expected)
    bytes([ord(command)]) if len(command) == 1 else command.encode('utf-8'),
    ]

    success = False
    last_error = None

    for i, data in enumerate(send_formats):
    try:
    print(f" Testing send format {i + 1}: {repr(data)}")

    # Send with various methods
    send_loop.run_until_complete(self._async_send_optimized(data))

    success = True
    print(f"✅ Successfully sent with format {i + 1}: {command}")
    Clock.schedule_once(
    lambda dt: self.update_status(f"✅ Sent: {command}")
    )
    break

    except Exception as format_error:
    last_error = format_error
    print(f"❌ Format {i + 1} failed: {format_error}")

    # Short pause between attempts
    time.sleep(0.1)
    continue

    if not success:
    error_msg = f"All send formats failed. Last error: {last_error}"
    print(f"❌ {error_msg}")
    Clock.schedule_once(
    lambda dt: self.update_status(f"❌ Send error: {str(last_error)}")
    )

    except Exception as e:
    error_msg = str(e)
    print(f"❌ Send thread error: {error_msg}")
    Clock.schedule_once(
    lambda dt: self.update_status(f"❌ Send error: {error_msg}")
    )

    async def _async_send_optimized(self, data):
    """Optimized asynchronous sending for Calliope mini"""
    try:
    print(f" Sending via BLE: {repr(data)}")

    # Method 1: Direct write with response=False (standard for micro:bit)
    try:
    await self.client.write_gatt_char(
    self.tx_characteristic,
    data,
    response=False # Important: Don't expect response
    )
    print("✅ Method 1: Direct without response - successful")

    # Small pause after sending (important for micro:bit)
    await asyncio.sleep(0.05)
    return

    except Exception as e1:
    print(f"⚠️ Method 1 failed: {e1}")

    # Method 2: Try with response=True
    try:
    await self.client.write_gatt_char(
    self.tx_characteristic,
    data,
    response=True
    )
    print("✅ Method 2: With response - successful")
    await asyncio.sleep(0.05)
    return

    except Exception as e2:
    print(f"⚠️ Method 2 failed: {e2}")

    # Method 3: Via UUID instead of Characteristic object
    try:
    await self.client.write_gatt_char(
    UART_TX_CHAR_UUID,
    data,
    response=False
    )
    print("✅ Method 3: Via UUID - successful")
    await asyncio.sleep(0.05)
    return

    except Exception as e3:
    print(f"⚠️ Method 3 failed: {e3}")
    raise e3 # Last attempt, raise error

    except Exception as e:
    print(f"❌ All send methods failed: {e}")
    raise

    def on_stop(self):
    """Clean app shutdown"""
    print("App is shutting down...")
    if self.connected and self.client:
    try:
    if self.bluetooth_loop and self.bluetooth_loop.is_running():
    # Send stop command before disconnection
    future = asyncio.run_coroutine_threadsafe(
    self._async_send_optimized(b"S"),
    self.bluetooth_loop
    )
    future.result(timeout=1.0)

    # Disconnect
    future = asyncio.run_coroutine_threadsafe(
    self.client.disconnect(),
    self.bluetooth_loop
    )
    future.result(timeout=2.0)
    except:
    pass


    # Start app
    if __name__ == "__main__":
    print(" Starting Calliope calli:bot Remote Control...")
    print(" Optimized for Calliope mini v2 BLE communication")
    app = CalliopeApp()
    app.run()


    It already works to the extent that the Calliope connects to the computer. Both the computer and the Calliope confirm this. However, the Calliope does not receive any messages, even though the remote control program does not detect any problems. The program on the Calliope does not even recognize that anything has been sent.

    I first searched online but couldn't find anything on the topic. ChatGPT didn't help either. However, Claude was able to help me get the computer to connect to the microcontroller at all. But nothing worked with sending messages, and the AI just kept saying it was due to incorrect transmission formats. The general exchange of data has to work, though, because there is an app that loads programs onto the Calliope via BLE. I hope someone here can help me, and I thank you in advance.

    Continue reading...

Compartilhe esta Página