From b75d93116844521305fac5d0aa41b49a3c7399e0 Mon Sep 17 00:00:00 2001 From: Jordan Maxwell Date: Sat, 26 Aug 2023 18:38:53 -0500 Subject: [PATCH] Use pydBeacon instead of local beacon tools --- droiddepot/audio.py | 5 +- droiddepot/beacon.py | 235 ++++----------------------------------- droiddepot/connection.py | 5 +- droiddepot/hardware.py | 5 +- droiddepot/motor.py | 5 +- droiddepot/notify.py | 5 +- droiddepot/protocol.py | 3 + droiddepot/script.py | 51 +++++---- droiddepot/utils.py | 5 +- droiddepot/voice.py | 5 +- examples/react.py | 40 +++++++ requirements.txt | 3 +- setup.py | 9 +- 13 files changed, 122 insertions(+), 254 deletions(-) create mode 100644 examples/react.py diff --git a/droiddepot/audio.py b/droiddepot/audio.py index c820507..38fd9f8 100644 --- a/droiddepot/audio.py +++ b/droiddepot/audio.py @@ -1,10 +1,11 @@ """ +Copyright (c) Jordan Maxwell, All Rights Reserved. +See LICENSE file in the project root for full license information. + This module defines classes for controlling audio and LEDs for a droid. It contains three classes: 1. DroidAudioCommand: A collection of audio commands for a droid 2. DroidLedIdentifier: A collection of LED identifiers for a droid 3. DroidAudioController: Represents an audio controller for a Droid and has methods for controlling audio and LEDs - -This code is MIT licensed. """ from enum import IntEnum diff --git a/droiddepot/beacon.py b/droiddepot/beacon.py index 1bb47b6..0e02d23 100644 --- a/droiddepot/beacon.py +++ b/droiddepot/beacon.py @@ -1,225 +1,28 @@ """ -Module for working with SWGE Beacon data and emulating park beacons - -This code is MIT licensed. +Copyright (c) Jordan Maxwell, All Rights Reserved. +See LICENSE file in the project root for full license information. """ from droiddepot.utils import * -from droiddepot.protocol import DisneyBLEManufacturerId -from bleak import BleakScanner -from threading import Thread -import logging -import asyncio - -class DroidBeaconType(object): - """ - Consants representing the beacon types that SWGE droids respond to. - """ - - DroidIdentificationBeacon = 3 - ParkLocationBeacon = 10 class OfficialDroidBeaconLocations(object): """ Constants representing every official Walt Disney World and DisneyLand SWGE Droid Beacon """ - - DL_Marketplace = '0A040102A601' - DL_BehindDroidDepot = '0A040202A601' - DL_Resistence = '0A040302A601' - DL_FirstOrder = '0A040702A601' - DL_DroidDepot = '0A040318BA01' - DL_InFrontOfOgas = '0A0405FFA601' - - WDW_OutdoorsArea = '0A040102A601' - WDW_BehindDroidDepot = '0A040202A601' - WDW_Resistence = '0A040302A601' - WDW_DokOndars = '0A040602A601' - WDW_FirstOrder = '0A040702A601' - WDW_Marketplace = '0A040618BA01' - WDW_DroidDetector = '0A0405FFA601' - WDW_InFrontOfOgas = '0A0407FFA601' - -def get_beacon_header(beacon_type: int, data_length: int = 4) -> str: - """ - Returns the header used by a SWGE Beacon - """ - - return int_to_hex(beacon_type) + int_to_hex(data_length) - -def create_location_beacon_payload(script_id: int, reaction_interval: int, signal_strength: int, droid_paired: bool = True) -> str: - """ - Creates a Location beacon payload for getting an area based reaction out of a SWGE droid - """ - - if script_id < 1 or script_id > 7: - raise ValueError('Script ids outside of the range of 1-7 are not currently supported.') - - beacon_payload = get_beacon_header(DroidBeaconType.ParkLocationBeacon, 4) - beacon_payload += int_to_hex(script_id) - beacon_payload += int_to_hex(reaction_interval) - beacon_payload += dbm_to_hex(signal_strength) - beacon_payload += "01" if droid_paired else "00" - - return beacon_payload.upper() - -def decode_location_beacon_payload(payload: str) -> dict: - """ - Decodes a SWGE location beacon payload into its various parts - """ - - script_id = hex_to_int(payload[4:6]) - reaction_interval = hex_to_int(payload[6:8]) - signal_strength = hex_to_dbm(payload[8:10]) - droid_paired = bool(hex_to_int(payload[10:12])) - - return { 'script_id': script_id, 'reaction_interval': reaction_interval, 'signal_strength': signal_strength, 'droid_paired': droid_paired } - -def create_droid_beacon_payload(droid_paired: bool = True, affiliation_id: int = 1, personality_id: int = 1) -> str: - """ - Creates a droid beacon payload for a droid given its paired state, affilitation id, and personality id - """ - - header = get_beacon_header(DroidBeaconType.DroidIdentificationBeacon, 4) - droid_paired_byte = 0x80 + int(droid_paired) - affiliation_byte = (affiliation_id * 2) + 0x80 - personality_byte = personality_id - hex_string = f"{header}44{droid_paired_byte:02X}{affiliation_byte:02X}{personality_byte:02X}" - - return hex_string - -def decode_droid_beacon_payload(payload: str) -> dict: - """ - Decodes a SWGE droid beacon payload into its various parts - """ - - data_length = hex_to_int(payload[4:6]) - 0x40 - droid_paired = hex_to_int(payload[6:8]) - 0x80 - affiliation_id = int((hex_to_int(payload[8:10]) - 0x80) / 2) - personality_id = hex_to_int(payload[10:12]) - - return { 'data_length': data_length, 'droid_paired': droid_paired, 'affiliation_id': affiliation_id, 'personality_id': personality_id } - -class DroidReactionBeaconScanner(object): - """ - A class for scanning and decoding Disney BLE droid reaction beacons. - """ - - def __init__(self): - """ - Initializes a new instance of the DroidReactionBeaconScanner class. - """ - - self.__location_handlers = [] - self.__droid_handlers = [] - - self.__scan_loop = asyncio.new_event_loop() - self.__scan_thread = None - - def add_location_handler(self, handler: object) -> None: - """ - """ - - if handler not in self.__location_handlers: - self.__location_handlers.append(handler) - - def remove_location_handler(self, handler: object) -> None: - """ - """ - - if handler in self.__location_handlers: - self.__location_handlers.remove(handler) - - def add_droid_handler(self, handler: object) -> None: - """ - """ - - if handler not in self.__droid_handlers: - self.__droid_handlers.append(handler) - - def remove_droid_handler(self, handler: object) -> None: - """ - """ - - if handler in self.__droid_handlers: - self.__droid_handlers.remove(handler) - - async def __scan(self) -> None: - """ - Scans for nearby Disney BLE droid reaction beacons and decodes their data. - """ - - async with BleakScanner() as scanner: - await scanner.start() - - while True: - devices = scanner.discovered_devices_and_advertisement_data - visible_droids = {} - locations_in_range = {} - - for device_address in devices: - try: - # Check if the device is advertising with the expected manufacturer ID - device, data = devices[device_address] - if self.__is_droid_beacon(data.manufacturer_data): - beacon_payload = data.manufacturer_data[DisneyBLEManufacturerId.DroidManufacturerId] - beacon_payload = beacon_payload.hex() - - # Decode the beacon data based on type - beacon_type = hex_to_int(beacon_payload[:2]) - if beacon_type == DroidBeaconType.DroidIdentificationBeacon: - beacon_data = decode_droid_beacon_payload(beacon_payload) - visible_droids[device_address] = beacon_data - elif beacon_type == DroidBeaconType.ParkLocationBeacon: - beacon_data = decode_location_beacon_payload(beacon_payload) - if beacon_data['signal_strength'] >= data.rssi: - locations_in_range[device_address] = beacon_data - else: - logging.warning('Discovered unknown droid beacon type: %s' % beacon_type) - except Exception as e: - logging.error('An unexpected error occured processing bluetooth device: %s' % device_address) - logging.error(e, exc_info=True) - - for handler in self.__location_handlers: - await handler(locations_in_range) - - for handler in self.__droid_handlers: - await handler(visible_droids) - - await asyncio.sleep(2) - - def __start_scan_loop(self, loop: asyncio.AbstractEventLoop) -> None: - """ - Starts the beacon scanning event loop - """ - - asyncio.set_event_loop(loop) - loop.run_forever() - - def __is_droid_beacon(self, data: dict) -> bool: - """ - Checks if the given data contains data can a droid can react to. - """ - - return DisneyBLEManufacturerId.DroidManufacturerId in data - - def start(self) -> None: - """ - Starts the beacon scanner - """ - - if self.__scan_loop.is_running(): - return - - self.__scan_thread = Thread(target=self.__start_scan_loop, args=(self.__scan_loop,), daemon=True) - self.__scan_thread.start() - asyncio.run_coroutine_threadsafe(self.__scan(), self.__scan_loop) - - def stop(self) -> None: - """ - Stops the beacon scanner - """ - - if self.__scan_loop.is_running(): - self.__scan_loop.stop() - self.__scan_thread.join() \ No newline at end of file + + DL_Marketplace = '0A040102A601' + DL_BehindDroidDepot = '0A040202A601' + DL_Resistence = '0A040302A601' + DL_FirstOrder = '0A040702A601' + DL_DroidDepot = '0A040318BA01' + DL_InFrontOfOgas = '0A0405FFA601' + DL_MarketplaceEntertance = '0A040502A601' + + WDW_OutdoorsArea = '0A040102A601' + WDW_BehindDroidDepot = '0A040202A601' + WDW_Resistence = '0A040302A601' + WDW_DokOndars = '0A040602A601' + WDW_FirstOrder = '0A040702A601' + WDW_Marketplace = '0A040618BA01' + WDW_DroidDetector = '0A0405FFA601' + WDW_InFrontOfOgas = '0A0407FFA601' diff --git a/droiddepot/connection.py b/droiddepot/connection.py index c150f84..2d6e65f 100644 --- a/droiddepot/connection.py +++ b/droiddepot/connection.py @@ -1,11 +1,12 @@ """ +Copyright (c) Jordan Maxwell, All Rights Reserved. +See LICENSE file in the project root for full license information. + DroidConnection is a BLE class representing a connection to a SWGE DroidDepot droid. It includes methods for connecting, disconnecting, sending commands, and running scripts on the droid. It also includes instances of DroidAudioController, DroidMotorController, and DroidScriptEngine to manage the droid's audio, motor, and script functions. - -This class is licensed under MIT. """ import asyncio diff --git a/droiddepot/hardware.py b/droiddepot/hardware.py index fbd7aa2..5268aed 100644 --- a/droiddepot/hardware.py +++ b/droiddepot/hardware.py @@ -1,7 +1,8 @@ """ -This modules defines classes and helper functions for working with SWGE droid hardware. +Copyright (c) Jordan Maxwell, All Rights Reserved. +See LICENSE file in the project root for full license information. -This code is MIT licensed. +This modules defines classes and helper functions for working with SWGE droid hardware. """ DroidFirmwareVersion = '4b1001444411110100000000' diff --git a/droiddepot/motor.py b/droiddepot/motor.py index 3f9e7f9..b179699 100644 --- a/droiddepot/motor.py +++ b/droiddepot/motor.py @@ -1,7 +1,8 @@ """ -This module provides classes for controlling the motor functions of a SWGE DroidDepot droid. +Copyright (c) Jordan Maxwell, All Rights Reserved. +See LICENSE file in the project root for full license information. -The classes contained in this module are licensed under the MIT License. +This module provides classes for controlling the motor functions of a SWGE DroidDepot droid. """ from enum import IntEnum diff --git a/droiddepot/notify.py b/droiddepot/notify.py index e6b1c7c..a44a1e9 100644 --- a/droiddepot/notify.py +++ b/droiddepot/notify.py @@ -1,12 +1,13 @@ """ +Copyright (c) Jordan Maxwell, All Rights Reserved. +See LICENSE file in the project root for full license information. + This module provides classes for processing and handling notifications from a connected droid. It contains the DroidNotifyMessage class, which represents a notification message from the droid, and the DroidNotificationProcessor class, which handles incoming notifications from the droid and passes them to the appropriate handlers. The DroidNotificationProcessor class also includes methods for decoding incoming messages and verifying firmware versions. - -This class is licensed under MIT. """ import logging diff --git a/droiddepot/protocol.py b/droiddepot/protocol.py index a3786ca..272fb9b 100644 --- a/droiddepot/protocol.py +++ b/droiddepot/protocol.py @@ -1,4 +1,7 @@ """ +Copyright (c) Jordan Maxwell, All Rights Reserved. +See LICENSE file in the project root for full license information. + This module defines the DroidCommandId and DroidMultipurposeCommand classes used to define the constant command identifiers to communicate with a SWGE droid. """ diff --git a/droiddepot/script.py b/droiddepot/script.py index d28483a..0e2f329 100644 --- a/droiddepot/script.py +++ b/droiddepot/script.py @@ -1,18 +1,19 @@ """ +Copyright (c) Jordan Maxwell, All Rights Reserved. +See LICENSE file in the project root for full license information. + This module contains classes and functions for interacting with droid scripts. 1. DroidScripts: An enumeration containing constants representing available droid scripts. 2. DroidScriptActions: An enumeration containing constants representing available droid script actions. 3. DroidScriptEngine: A class that represents the droid script engine and provides methods for executing droid scripts. - -This code is MIT licensed. """ import asyncio import logging from datetime import datetime +from dbeacon import scanner, beacon from droiddepot.protocol import DroidCommandId -from droiddepot.beacon import DroidReactionBeaconScanner, decode_location_beacon_payload class DroidScripts(object): """ @@ -22,7 +23,7 @@ class DroidScripts(object): GeneralParkResponseScript = 1 DroidDepotParkResponseScript = 2 ResistenceParkResponseScript = 3 - UnknownParkResponseScript = 4 + UnknownParkResponseScript = 4 # Possibly related to smuggers run? OgasCantinaParkResponseScript = 5 DokOndarsParkResponseScript = 6 FirstOrderParkResponseScript = 7 @@ -58,8 +59,8 @@ def __init__(self, droid: object) -> None: self.droid = droid self.__location_reaction_tracker = {} - self.reaction_scanner = DroidReactionBeaconScanner() - self.reaction_scanner.add_location_handler(self.__perform_droid_location_reactions) + self.reaction_scanner = scanner.DBeaconScanner() + self.reaction_scanner.add_beacon_handler(10, self.__perform_location_reactions) async def send_script_command(self, script_id: int, script_action: int) -> None: """ @@ -93,17 +94,29 @@ async def execute_script(self, script_id: int) -> None: await self.send_script_command(script_id, DroidScriptActions.ExecuteScript) - async def execute_location_beacon_payload(self, payload: str) -> None: + async def execute_location_beacon(self, beacon: beacon.LocationBeacon) -> None: """ Executes a location beacon on the connected droid emulation what would happen if the droid encountered the beacon at a Disney park Args: - payload (str): Payload advertised by a park beacon + beacon (LocationBeacon): Location beacon to execute + """ + + await self.execute_script(beacon.location_id) + + async def execute_location_reaction(self, location_id: int) -> None: + """ + Executes a location reaction on the connected droid emulation what would happen + + Args: + location_id (int): Location id to execute """ - data = decode_location_beacon_payload(payload) - await self.execute_script(data['script_id']) + if location_id < 0 or location_id > 7: + raise ValueError("Invalid location id requested. Location ids must be between 0 and 7") + + await self.execute_script(location_id) async def open_script(self, script_id: int) -> None: """ @@ -147,40 +160,40 @@ def __calculate_reaction_time(self, interval: int) -> int: return interval - async def __perform_droid_location_reactions(self, locations: list) -> None: + async def __perform_location_reactions(self, beacons: list) -> None: """ Executes a script associated with each park location beacon that the droid enters. Args: - locations (list): A list of location beacon addresses to react to + locations (list): A list of beacons detected Returns: int: The calculated reaction time in seconds. """ # Verify we have at least one location to react to first. - if len(locations) == 0: + if len(beacons) == 0: return can_execute = True already_executed = False - for location_beacon_address in locations: + location_beacon_address = "Unknown" + + for location_beacon_info in beacons: try: - location_data = locations[location_beacon_address] + location_beacon_address, location_beacon = location_beacon_info # Check if we already reacted and if we have check if we are in a new reaction window if location_beacon_address in self.__location_reaction_tracker: last_execution = self.__location_reaction_tracker[location_beacon_address] time_since_last = (datetime.now() - last_execution).total_seconds() - can_execute = time_since_last >= self.__calculate_reaction_time(location_data['reaction_interval']) + can_execute = time_since_last >= self.__calculate_reaction_time(location_beacon.reaction_interval) # Attempt to execute the reaction if can_execute and already_executed == False: - await self.execute_script(location_data['script_id']) + await self.execute_location_reaction(location_beacon.location_id) self.__location_reaction_tracker[location_beacon_address] = datetime.now() already_executed = True - except ValueError: - logging.error('Failed to handle location beacon execution. Likely attempted to execute a dangerous script') except Exception as e: logging.error('An unexpected error occured processing a park location beacon: %s' % location_beacon_address) logging.error(e, exc_info=True) diff --git a/droiddepot/utils.py b/droiddepot/utils.py index f0707d8..39ac059 100644 --- a/droiddepot/utils.py +++ b/droiddepot/utils.py @@ -1,7 +1,8 @@ """ -Utility methods module for PyDroidDepot. +Copyright (c) Jordan Maxwell, All Rights Reserved. +See LICENSE file in the project root for full license information. -This code is MIT licensed. +Utility methods module for PyDroidDepot. """ def int_to_hex(num: int) -> str: diff --git a/droiddepot/voice.py b/droiddepot/voice.py index e99e190..d036c39 100644 --- a/droiddepot/voice.py +++ b/droiddepot/voice.py @@ -1,4 +1,7 @@ """ +Copyright (c) Jordan Maxwell, All Rights Reserved. +See LICENSE file in the project root for full license information. + This module provides a voice controller for SWGE droids that attempts to match the droid's configured affiliation to a tone of voice based on the available audio banks in the droid's memory. @@ -8,8 +11,6 @@ The `DroidVoiceTone` class defines identifiers for the possible tones of voice to use when speaking. The `talk_with_animation` method of the `DroidVoiceController` class sends a command to the droid to speak with a random animation and audio file based around the tone supplied when invoked. - -This code is MIT licensed. """ from droiddepot.hardware import DroidAffiliation, DroidAudioBankIdentifier diff --git a/examples/react.py b/examples/react.py new file mode 100644 index 0000000..1680d86 --- /dev/null +++ b/examples/react.py @@ -0,0 +1,40 @@ +""" +""" + +import sys +sys.path.insert(0, '../') + +from random import randrange +from droiddepot.connection import discover_droid, DroidCommandId +from droiddepot.script import DroidScripts +from time import sleep +from bleak import BleakError +import asyncio + +async def main() -> None: + """ + Main entry point into the example application + """ + + d = await discover_droid(retry=True) + try: + await d.connect(silent=True) + await d.audio_controller.set_volume(20) + d.script_engine.start_beacon_reactions() + + while d.droid.is_connected: + sleep(1) + + except OSError as err: + print(f"Discovery failed due to operating system: {err}") + except BleakError as err: + print(f"Discovery failed due to Bleak: {err}") + except KeyboardInterrupt as err: + pass + finally: + print("Shutting down.") + await d.disconnect() + +# Main entry point into the example application +if __name__ == "__main__": + asyncio.run(main()) diff --git a/requirements.txt b/requirements.txt index d7bb07c..fd69232 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -bleak==0.20.2 \ No newline at end of file +bleak==0.20.2 +pydBeacon==1.0.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 90bfb70..53ee23d 100644 --- a/setup.py +++ b/setup.py @@ -3,16 +3,17 @@ except ImportError: from distutils.core import setup -long_description = """ -Module for controlling droids built and purchased at the Droid Depot in Disney's Galaxys Edge over Bleutooth -""" +from pathlib import Path +repository_directory = Path(__file__).parent +long_description = (repository_directory / "README.md").read_text() setup( name='pyDroidDepot', description="Module for controlling droids built and purchased at the Droid Depot in Disney's Galaxys Edge", long_description=long_description, + long_description_content_type='text/markdown', license='MIT', - version='1.0.2', + version='1.0.3', author='Jordan Maxwell', maintainer='Jordan Maxwell', url='https://github.com/thetestgame/pyDroidDepot',