Repository: martinohanlon/BlueDot Branch: master Commit: 208163eeeae2 Files: 148 Total size: 374.3 KB Directory structure: gitextract_s_m_z04y/ ├── .github/ │ ├── FUNDING.yml │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── PRIVACYPOLICY.MD ├── README.rst ├── bluedot/ │ ├── __init__.py │ ├── app.py │ ├── btcomm.py │ ├── colors.py │ ├── constants.py │ ├── dot.py │ ├── exceptions.py │ ├── interactions.py │ ├── mock.py │ ├── threads.py │ └── utils.py ├── clients/ │ ├── android/ │ │ ├── .gitignore │ │ ├── README.rst │ │ ├── app/ │ │ │ ├── .gitignore │ │ │ ├── build.gradle │ │ │ ├── proguard-rules.pro │ │ │ ├── release/ │ │ │ │ ├── app-release.aab │ │ │ │ ├── app-release.apk │ │ │ │ └── output-metadata.json │ │ │ └── src/ │ │ │ ├── androidTest/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── stuffaboutcode/ │ │ │ │ └── bluedot/ │ │ │ │ └── ExampleInstrumentedTest.java │ │ │ ├── main/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── stuffaboutcode/ │ │ │ │ │ ├── bluedot/ │ │ │ │ │ │ ├── BluetoothChatService.java │ │ │ │ │ │ ├── Button.java │ │ │ │ │ │ ├── Constants.java │ │ │ │ │ │ ├── Devices.java │ │ │ │ │ │ ├── DynamicMatrix.java │ │ │ │ │ │ └── SettingsActivity.java │ │ │ │ │ └── logger/ │ │ │ │ │ ├── Log.java │ │ │ │ │ ├── LogFragment.java │ │ │ │ │ ├── LogNode.java │ │ │ │ │ ├── LogView.java │ │ │ │ │ ├── LogWrapper.java │ │ │ │ │ └── MessageOnlyLogFilter.java │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── round_button.xml │ │ │ │ ├── layout/ │ │ │ │ │ ├── activity_button.xml │ │ │ │ │ ├── activity_devices.xml │ │ │ │ │ └── settings_activity.xml │ │ │ │ ├── menu/ │ │ │ │ │ └── settings_menu.xml │ │ │ │ ├── values/ │ │ │ │ │ ├── arrays.xml │ │ │ │ │ ├── attrs.xml │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── dimens.xml │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ └── xml/ │ │ │ │ └── root_preferences.xml │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── stuffaboutcode/ │ │ │ └── bluedot/ │ │ │ └── ExampleUnitTest.java │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ └── settings.gradle │ └── python/ │ └── README.rst ├── docs/ │ ├── Makefile │ ├── bluedotandroidapp.rst │ ├── bluedotpythonapp.rst │ ├── btcommapi.rst │ ├── build.rst │ ├── changelog.rst │ ├── conf.py │ ├── dotapi.rst │ ├── examples/ │ │ ├── bt_enumerate.py │ │ ├── bt_pair_button.py │ │ ├── bt_pairing.py │ │ ├── camera.py │ │ ├── dpad.py │ │ ├── dpad_layout.py │ │ ├── goodbye_world.py │ │ ├── hello_event.py │ │ ├── hello_world.py │ │ ├── joypad.py │ │ ├── led1.py │ │ ├── led2.py │ │ ├── led3.py │ │ ├── looks_border.py │ │ ├── looks_color.py │ │ ├── looks_square.py │ │ ├── looks_visible.py │ │ ├── many_buttons.py │ │ ├── many_buttons_random_colors.py │ │ ├── mock_app.py │ │ ├── mock_script.py │ │ ├── multiple_dots.py │ │ ├── robot1.py │ │ ├── robot2.py │ │ ├── robot3.py │ │ ├── rotation.py │ │ ├── shout_hello.py │ │ ├── slider_centre.py │ │ ├── slider_dimmer.py │ │ ├── slider_left_right.py │ │ ├── swipe1.py │ │ ├── swipe2.py │ │ ├── swipe_direction.py │ │ ├── swipe_speed_angle.py │ │ ├── two_buttons.py │ │ ├── two_buttons_gap.py │ │ └── two_buttons_two_events.py │ ├── gettingstarted.rst │ ├── index.rst │ ├── mockapi.rst │ ├── pairpiandroid.rst │ ├── pairpipi.rst │ ├── protocol.rst │ └── recipes.rst ├── examples/ │ ├── adapter_details.py │ ├── click_wheel.py │ ├── client_connects.py │ ├── client_debug.py │ ├── color_changer.py │ ├── connect_multiple_apps.py │ ├── dot_changer.py │ ├── dot_debug.py │ ├── dot_single_button_debug.py │ ├── double_press.py │ ├── dpad.py │ ├── dpad_layout.py │ ├── hello_world.py │ ├── joypad.py │ ├── many_buttons.py │ ├── matrix_of_dots.py │ ├── mock_app_debug.py │ ├── mock_btcomm_debug.py │ ├── mock_debug.py │ ├── remote_camera.py │ ├── server_debug.py │ ├── simple_robot.py │ ├── simple_variable_robot.py │ ├── slider_centre.py │ ├── slider_horizontal.py │ ├── slider_led_dimmer.py │ ├── source_robot.py │ ├── swipe_debug.py │ ├── threaded_callbacks.py │ ├── two_buttons.py │ └── wait_for_events.py ├── setup.py └── tests/ ├── test_blue_dot.py ├── test_bluetooth_adapter.py └── test_bluetooth_comms.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [martinohanlon] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Having trouble with BlueDot? title: '' labels: '' assignees: '' --- Fill in the details or delete as appropriate :) **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error or Run this program ```python ``` **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **System (please complete the following information):** - OS: [e.g. Raspbian] - Version [e.g. Buster] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Got an idea for BlueDot? title: '' labels: '' assignees: '' --- **Describe the feature you'd like** A clear and concise description of what you want to happen. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ *.py[cdo] pythonhosted/ .pytest_cache/ # Editor detritus *.vim *.swp tags .vscode .idea # Packaging detritus *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg # Installer logs pip-log.txt # Unit test / coverage reports coverage .coverage .tox .cache # Generated documentation docs/_build ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Martin O'Hanlon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: PRIVACYPOLICY.MD ================================================ # BlueDot Privacy Policy The BlueDot Python library and Android app do not store your personal data. That is it! There is nowhere to store your data, no database, no server. Even if Rick Astley asked [really really nicely](https://www.youtube.com/watch?v=dQw4w9WgXcQ) to see your data it couldn't be given it to him. For the purposes of utmost transparency... The android app will store 2 items of non-personal data, [preferences for using the default port and a preferred port](https://github.com/martinohanlon/BlueDot/blob/master/clients/android/app/src/main/java/com/stuffaboutcode/bluedot/SettingsActivity.java) (if you change them from the default) in a [shared preferences file](https://developer.android.com/training/data-storage/shared-preferences). ================================================ FILE: README.rst ================================================ Blue Dot ======== |pypibadge| |docsbadge| .. raw:: html Blue Dot allows you to control your Raspberry Pi projects wirelessly - it's a Bluetooth remote and zero boiler plate (super simple to use :) Python library. |bluedotfeature| |bluedotapp| |bluedotmanybuttons| |bluedotpython| Created by `Martin O'Hanlon`_ (`@martinohanlon`_, `stuffaboutco.de`_). Getting Started --------------- `Install and usage`_ is really simple: 1. Install the Python library:: sudo pip3 install bluedot 2. Get the `Android Blue Dot app`_ or use the `Python Blue Dot app`_ 3. Pair your Raspberry Pi 4. Write some code:: from bluedot import BlueDot bd = BlueDot() bd.wait_for_press() print("You pressed the blue dot!") 5. Press the Blue Dot See the `getting started`_ guide to 'get started'! More ---- Blue Dot is more than just one `button_`. You can create as many buttons as you want and change their appearance to create your own controller. |bluedotjoypad| Every `button`_ is also a `joystick`_. You can tell if a button was pressed in the middle, on the top, bottom, left or right. You can easily create a `BlueDot controlled Robot`_. Why be restricted by such vague positions like top and bottom though: you can get the exact (x, y) position or even the angle and distance from centre where the button was pressed. Its not all about when the button was pressed either - pressed, released or moved they all work. A button can be any colour, square, given give or hidden! You can press it, `slide it`_, `swipe it`_, `rotate it`_ - one blue circle can do a lot! Even more --------- The `online documentation`_ describes how to use Blue Dot and the `Python library`_ including `Recipes`_ and ideas. Status ------ Production - under active development. Be sure to raise an `issue`_ if you have a feature request or experience problems. .. _Martin O'Hanlon: https://github.com/martinohanlon .. _stuffaboutco.de: http://stuffaboutco.de .. _@martinohanlon: https://twitter.com/martinohanlon .. _getting started: http://bluedot.readthedocs.io/en/latest/gettingstarted.html .. _Install and usage: http://bluedot.readthedocs.io/en/latest/gettingstarted.html .. _online documentation: http://bluedot.readthedocs.io/en/latest/ .. _Python library: http://bluedot.readthedocs.io/en/latest/dotapi.html .. _examples: https://github.com/martinohanlon/BlueDot/tree/master/examples .. _Recipes: http://bluedot.readthedocs.io/en/latest/recipes.html .. _Android Blue Dot app: http://play.google.com/store/apps/details?id=com.stuffaboutcode.bluedot .. _Python Blue Dot app: http://bluedot.readthedocs.io/en/latest/bluedotpythonapp.html .. _issue: https://github.com/martinohanlon/bluedot/issues .. _BlueDot controlled Robot: https://youtu.be/eW9oEPySF58 .. _joystick: http://bluedot.readthedocs.io/en/latest/recipes.html#joystick .. _button: http://bluedot.readthedocs.io/en/latest/recipes.html#button .. _slide it: http://bluedot.readthedocs.io/en/latest/recipes.html#slider .. _swipe it: http://bluedot.readthedocs.io/en/latest/recipes.html#swiping .. _rotate it: http://bluedot.readthedocs.io/en/latest/recipes.html#rotating .. |bluedotapp| image:: https://raw.githubusercontent.com/martinohanlon/BlueDot/master/docs/images/bluedotandroid_small.png :height: 270 px :width: 144 px :scale: 100 % :alt: blue dot app .. |bluedotpython| image:: https://raw.githubusercontent.com/martinohanlon/BlueDot/master/docs/images/bluedotpython.png :height: 247 px :width: 294 px :scale: 100 % :alt: blue dot python app .. |bluedotjoypad| image:: https://raw.githubusercontent.com/martinohanlon/BlueDot/master/docs/images/layout_joypad_smaller.png :height: 147 px :width: 294 px :scale: 100 % :alt: blue dot app as a joy pad controller .. |bluedotmanybuttons| image:: https://raw.githubusercontent.com/martinohanlon/BlueDot/master/docs/images/layout_many_buttons_smaller.png :height: 270 px :width: 144 px :scale: 100 % :alt: blue dot app with 10 buttons in a 2x5 grid .. |bluedotfeature| image:: https://raw.githubusercontent.com/martinohanlon/BlueDot/master/docs/images/blue_dot_feature_small.png :height: 247 px :width: 506 px :scale: 100 % :alt: blue dot feature .. |pypibadge| image:: https://badge.fury.io/py/bluedot.svg :target: https://badge.fury.io/py/bluedot :alt: Latest Version .. |docsbadge| image:: https://readthedocs.org/projects/bluedot/badge/ :target: https://readthedocs.org/projects/bluedot/ :alt: Docs ================================================ FILE: bluedot/__init__.py ================================================ from .dot import BlueDot, BlueDotButton from .interactions import BlueDotInteraction, BlueDotPosition, BlueDotRotation, BlueDotSwipe from .colors import COLORS from .mock import MockBlueDot ================================================ FILE: bluedot/app.py ================================================ import os os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1' from argparse import ArgumentParser import pygame import sys from .btcomm import BluetoothAdapter, BluetoothClient from .constants import PROTOCOL_VERSION from .colors import BLUE, GRAY43, GRAY86, RED, parse_color DEFAULTSIZE = (320, 240) BORDER = 7 FONT = "monospace" FONTSIZE = 18 FONTPAD = 3 CLIENT_NAME = "Blue Dot Python app" BORDER_THICKNESS = 0.025 class BlueDotClient: def __init__(self, device, server, port, fullscreen, width, height): self._device = device self._server = server self._port = 1 if port is None else port self._fullscreen = fullscreen #init pygame pygame.init() #load font self._font = pygame.font.SysFont(FONT, FONTSIZE) #setup the screen #set the screen caption pygame.display.set_caption("Blue Dot") #create the screen screenflags = 0 if fullscreen: screenflags = pygame.FULLSCREEN if width == None and height == None: display_info = pygame.display.Info() width = display_info.current_w height = display_info.current_h if width == None: width = DEFAULTSIZE[0] if height == None: height = DEFAULTSIZE[1] self._screen = pygame.display.set_mode((width, height), screenflags) self._width = width self._height = height self._run() pygame.quit() def _run(self): # has a server been specified? If so connected directly if self._server: button_screen = ButtonScreen(self._screen, self._font, self._device, self._server, self._port, self._width, self._height) button_screen.run() else: # start the devices screen devices_screen = DevicesScreen(self._screen, self._font, self._device, self._port, self._width, self._height) devices_screen.run() class BlueDotScreen: def __init__(self, screen, font, width, height): self.screen = screen self.font = font self.width = width self.height = height # setup screen attributes self.frame_rect = pygame.Rect(BORDER, BORDER, self.width - (BORDER * 2) - FONTSIZE - FONTPAD, self.height - (BORDER * 2)) self.close_rect = pygame.Rect(self.width - FONTSIZE - FONTPAD - BORDER, BORDER, FONTSIZE + FONTPAD, FONTSIZE + FONTPAD) self.draw_screen() def draw_screen(self): # set the screen background self.screen.fill(GRAY86.rgb) self.draw_close_button() def draw_close_button(self): # draw close button pygame.draw.rect(self.screen, BLUE.rgb, self.close_rect, 2) pygame.draw.line(self.screen, BLUE.rgb, (self.close_rect[0], self.close_rect[1]), (self.close_rect[0] + self.close_rect[2], self.close_rect[1] + self.close_rect[3]), 1) pygame.draw.line(self.screen, BLUE.rgb, (self.close_rect[0], self.close_rect[1] + self.close_rect[3]), (self.close_rect[0] + self.close_rect[2], self.close_rect[1]), 1) def draw_error(self, e): message = "Error: {}".format(e) print(message) self.draw_status_message(message, colour = RED.rgb) def draw_status_message(self, message, colour = BLUE.rgb): self.screen.fill(GRAY86.rgb, self.frame_rect) self.draw_close_button() self.draw_text(message, colour, self.frame_rect.height / 2, border = True, border_pad = FONTPAD) pygame.display.update() def draw_text(self, text, colour, start_y, antiaalias=False, background=None, border=False, border_width=1, border_pad=0): rect = pygame.Rect(self.frame_rect) y = rect.top + start_y + border_pad lineSpacing = -2 # get the height of the font fontHeight = self.font.size("Tg")[1] while text: i = 1 # determine if the row of text will be outside our area if y + fontHeight > rect.bottom: break # determine maximum width of line while self.font.size(text[:i])[0] < (rect.width - (border_pad * 2)) and i < len(text): i += 1 # if we've wrapped the text, then adjust the wrap to the last word if i < len(text): i = text.rfind(" ", 0, i) + 1 # render the line and blit it to the surface if background: image = self.font.render(text[:i], 1, colour, background) image.set_colorkey(background) else: image = self.font.render(text[:i], antiaalias, colour) self.screen.blit(image, (rect.left + border_pad, y)) y += fontHeight + lineSpacing + border_pad # remove the text we just blitted text = text[i:] #return the rect the text was drawn in rect.top = rect.top + start_y rect.height = y - start_y if border: pygame.draw.rect(self.screen, colour, rect, border_width) return rect class DevicesScreen(BlueDotScreen): def __init__(self, screen, font, device, port, width, height): self.bt_adapter = BluetoothAdapter(device = device) self.port = port super(DevicesScreen, self).__init__(screen, font, width, height) # self.draw_screen() def draw_screen(self): self.device_rects = [] super(DevicesScreen, self).draw_screen() #title title_rect = self.draw_text("Connect", RED.rgb, 0) y = title_rect.bottom for d in self.bt_adapter.paired_devices: device_rect = self.draw_text("{} ({})".format(d[1], d[0]), BLUE.rgb, y, border = True, border_pad = FONTPAD) self.device_rects.append(pygame.Rect(device_rect)) y = device_rect.bottom def run(self): clock = pygame.time.Clock() running = True while running: clock.tick(50) ev = pygame.event.get() for event in ev: if event.type == pygame.MOUSEBUTTONDOWN: pos = pygame.mouse.get_pos() #has a device been clicked? for d in range(len(self.device_rects)): if self.device_rects[d].collidepoint(pos): # show the button self.draw_status_message("Connecting") button_screen = ButtonScreen(self.screen, self.font, self.bt_adapter.device, self.bt_adapter.paired_devices[d][0], self.port, self.width, self.height) button_screen.run() #redraw the screen self.draw_screen() if self.close_rect.collidepoint(pos): running = False if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False if event.type == pygame.QUIT: running = False pygame.display.update() class ButtonScreen(BlueDotScreen): def __init__(self, screen, font, device, server, port, width, height): self.device = device self.server = server self.port = port self._data_buffer = "" self.last_x = 0 self.last_y = 0 self._colour = BLUE self._border = False self._square = False self._visible = True self._pressed = False super(ButtonScreen, self).__init__(screen, font, width, height) def draw_screen(self): super(ButtonScreen, self).draw_screen() # work out dot position self.dot_centre = (int(self.frame_rect.top + (self.frame_rect.width / 2)), int(self.frame_rect.left + (self.frame_rect.height / 2))) if self.frame_rect.width > self.frame_rect.height: self.dot_rect = pygame.Rect(self.frame_rect.left + int((self.frame_rect.width - self.frame_rect.height) / 2), self.frame_rect.top, self.frame_rect.height, self.frame_rect.height) self.dot_radius = int(self.dot_rect.height / 2) else: self.dot_rect = pygame.Rect(self.frame_rect.left, self.frame_rect.top + int((self.frame_rect.height - self.frame_rect.width) / 2), self.frame_rect.width, self.frame_rect.width) self.dot_radius = int(self.dot_rect.width / 2) self.border_width = max(int(self.dot_rect.width * BORDER_THICKNESS), 1) self.border_height = max(int(self.dot_rect.height * BORDER_THICKNESS), 1) self._draw_dot() def _draw_dot(self): # clear the dot pygame.draw.rect( self.screen, GRAY86.rgb, ( self.dot_rect.left - self.border_width, self.dot_rect.top - self.border_height, self.dot_rect.width + (self.border_width * 2), self.dot_rect.height + (self.border_height * 2), ) ) colour = self._colour if not self._pressed else self._colour.get_adjusted_color(0.85) # draw the dot if self._square: if self._visible: pygame.draw.rect(self.screen, colour.rgb, self.dot_rect) if self._border: pygame.draw.rect(self.screen, GRAY43.rgb, self.dot_rect, max(int(self.dot_radius * BORDER_THICKNESS), 1)) else: if self._visible: pygame.draw.ellipse(self.screen, colour.rgb, self.dot_rect) if self._border: pygame.draw.ellipse(self.screen, GRAY43.rgb, self.dot_rect, max(int(self.dot_radius * BORDER_THICKNESS), 1)) def _process(self, op, pos): if self.bt_client.connected: x = (pos[0] - self.dot_centre[0]) / float(self.dot_radius) x = round(x, 4) y = ((pos[1] - self.dot_centre[1]) / float(self.dot_radius)) * -1 y = round(y, 4) message = "{},0,0,{},{}\n".format(op, x, y) if op == 2: if x != self.last_x or y != self.last_y: self._send_message(message) else: self._send_message(message) self.last_x = x self.last_y = y else: self.draw_error("Blue Dot not connected") def _send_protocol_version(self): if self.bt_client.connected: self._send_message("3,{},{}\n".format(PROTOCOL_VERSION, CLIENT_NAME)) def _send_message(self, message): try: self.bt_client.send(message) except: e = str(sys.exc_info()[1]) self.draw_error(e) def _data_received(self, data): # add the data received to the buffer self._data_buffer += data # get any full commands ended by \n last_command = self._data_buffer.rfind("\n") if last_command != -1: commands = self._data_buffer[:last_command].split("\n") # remove the processed commands from the buffer self._data_buffer = self._data_buffer[last_command + 1:] self._process_commands(commands) def _process_commands(self, commands): for command in commands: params = command.split(",") invalid_command = False if len(params) == 7: if params[0] == "4": # currently the python blue dot client only supports 1 button if params[5] != "1" or params[6] != "1": print("Error - The BlueDot python client only supports a single button.") self._colour = parse_color(params[1]) self._square = True if params[2] == "1" else False self._border = True if params[3] == "1" else False self._visible = True if params[4] == "1" else False self._draw_dot() elif params[0] == "5": if params[5] == "0" and params[6] == "0": self._colour = parse_color(params[1]) self._square = True if params[2] == "1" else False self._border = True if params[3] == "1" else False self._visible = True if params[4] == "1" else False self._draw_dot() else: invalid_command = True if invalid_command: print("Error - Invalid message received '{}'".format(command)) def run(self): self._connect() self._send_protocol_version() clock = pygame.time.Clock() pygame.event.clear() self._pressed = False running = True while running: clock.tick(50) ev = pygame.event.get() for event in ev: # handle mouse if event.type == pygame.MOUSEBUTTONDOWN or event.type == pygame.MOUSEBUTTONUP or (event.type == pygame.MOUSEMOTION and self._pressed): pos = pygame.mouse.get_pos() #circle clicked? if self.dot_rect.collidepoint(pos): if event.type == pygame.MOUSEBUTTONDOWN: self._pressed = True self._draw_dot() self._process(1, pos) elif event.type == pygame.MOUSEBUTTONUP: self._pressed = False self._draw_dot() self._process(0, pos) elif event.type == pygame.MOUSEMOTION: self._process(2, pos) #close clicked? if self.close_rect.collidepoint(pos): running = False if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False if event.type == pygame.QUIT: running = False pygame.display.update() self.bt_client.disconnect() def _connect(self): self.bt_client = BluetoothClient(self.server, self._data_received, port = self.port, device = self.device, auto_connect = False) try: self.bt_client.connect() except: e = str(sys.exc_info()[1]) self.draw_error(e) def main(): #read command line options parser = ArgumentParser(description="Blue Dot Python App") parser.add_argument("--device", help="The name of the bluetooth device to use (default is hci0)") parser.add_argument("--server", help="The name or mac address of the bluedot server") parser.add_argument("--port", help="The port number to use when connecting (default is 1)", type=int) parser.add_argument("--fullscreen", help="Fullscreen app", action="store_true") parser.add_argument("--width", type=int, help="A custom screen width (default is {})".format(DEFAULTSIZE[0])) parser.add_argument("--height", type=int, help="A customer screen height (default is {})".format(DEFAULTSIZE[1])) args = parser.parse_args() #start the blue dot client blue_dot_client = BlueDotClient(args.device, args.server, args.port, args.fullscreen, args.width, args.height) if __name__ == "__main__": main() ================================================ FILE: bluedot/btcomm.py ================================================ from __future__ import unicode_literals import socket import sys import errno from .utils import ( register_spp, get_mac, get_adapter_powered_status, get_adapter_discoverable_status, get_adapter_pairable_status, get_paired_devices, device_pairable, device_discoverable, device_powered, ) from .threads import WrapThread BLUETOOTH_TIMEOUT = 0.01 class BluetoothAdapter: """ Represents and allows interaction with a Bluetooth Adapter. The following example will get the Bluetooth adapter, print its powered status and any paired devices:: a = BluetoothAdapter() print("Powered = {}".format(a.powered)) print(a.paired_devices) :param str device: The Bluetooth device to be used, the default is "hci0", if your device only has 1 Bluetooth adapter this shouldn't need to be changed. """ def __init__(self, device = "hci0"): self._device = device self._address = get_mac(self._device) self._pairing_thread = None @property def device(self): """ The Bluetooth device name. This defaults to "hci0". """ return self._device @property def address(self): """ The `MAC address`_ of the Bluetooth adapter. .. _MAC address: https://en.wikipedia.org/wiki/MAC_address """ return self._address @property def powered(self): """ Set to ``True`` to power on the Bluetooth adapter. Depending on how Bluetooth has been powered down, you may need to use :command:`rfkill` to unblock Bluetooth to give permission to bluez to power on Bluetooth:: sudo rfkill unblock bluetooth """ return get_adapter_powered_status(self._device) @powered.setter def powered(self, value): device_powered(self._device, value) @property def discoverable(self): """ Set to ``True`` to make the Bluetooth adapter discoverable. """ return get_adapter_discoverable_status(self._device) @discoverable.setter def discoverable(self, value): device_discoverable(self._device, value) @property def pairable(self): """ Set to ``True`` to make the Bluetooth adapter pairable. """ return get_adapter_pairable_status(self._device) @pairable.setter def pairable(self, value): device_pairable(self._device, value) @property def paired_devices(self): """ Returns a sequence of devices paired with this adapater :code:`[(mac_address, name), (mac_address, name), ...]`:: a = BluetoothAdapter() devices = a.paired_devices for d in devices: device_address = d[0] device_name = d[1] """ return get_paired_devices(self._device) def allow_pairing(self, timeout = 60): """ Put the adapter into discoverable and pairable mode. :param int timeout: The time in seconds the adapter will remain pairable. If set to ``None`` the device will be discoverable and pairable indefinetly. """ #if a pairing thread is already running, stop it and restart if self._pairing_thread: if self._pairing_thread.is_alive: self._pairing_thread.stop() #make the adapter pairable self.pairable = True self.discoverable = True if timeout != None: #start the pairing thread self._pairing_thread = WrapThread(target=self._expire_pairing, args=(timeout, )) self._pairing_thread.start() def _expire_pairing(self, timeout): #wait till the timeout or the thread is stopped self._pairing_thread.stopping.wait(timeout) self.discoverable = False self.pairable = False class BluetoothServer: """ Creates a Bluetooth server which will allow connections and accept incoming RFCOMM serial data. When data is received by the server it is passed to a callback function which must be specified at initiation. The following example will create a Bluetooth server which will wait for a connection and print any data it receives and send it back to the client:: from bluedot.btcomm import BluetoothServer from signal import pause def data_received(data): print(data) s.send(data) s = BluetoothServer(data_received) pause() :param data_received_callback: A function reference should be passed, this function will be called when data is received by the server. The function should accept a single parameter which when called will hold the data received. Set to ``None`` if received data is not required. :param bool auto_start: If ``True`` (the default), the Bluetooth server will be automatically started on initialisation, if ``False``, the method ``start`` will need to be called before connections will be accepted. :param str device: The Bluetooth device the server should use, the default is "hci0", if your device only has 1 Bluetooth adapter this shouldn't need to be changed. :param int port: The Bluetooth port the server should use, the default is 1. :param str encoding: The encoding standard to be used when sending and receiving byte data. The default is "utf-8". If set to ``None`` no encoding is done and byte data types should be used. :param bool power_up_device: If ``True``, the Bluetooth device will be powered up (if required) when the server starts. The default is ``False``. Depending on how Bluetooth has been powered down, you may need to use :command:`rfkill` to unblock Bluetooth to give permission to bluez to power on Bluetooth:: sudo rfkill unblock bluetooth :param when_client_connects: A function reference which will be called when a client connects. If ``None`` (the default), no notification will be given when a client connects :param when_client_disconnects: A function reference which will be called when a client disconnects. If ``None`` (the default), no notification will be given when a client disconnects """ def __init__(self, data_received_callback, auto_start = True, device = "hci0", port = 1, encoding = "utf-8", power_up_device = False, when_client_connects = None, when_client_disconnects = None): self._setup_adapter(device) self._data_received_callback = data_received_callback self._port = port self._encoding = encoding self._power_up_device = power_up_device self._when_client_connects = when_client_connects self._when_client_disconnects = when_client_disconnects self._running = False self._client_connected = False self._server_sock = None self._client_info = None self._client_sock = None self._conn_thread = None if auto_start: self.start() @property def device(self): """ The Bluetooth device the server is using. This defaults to "hci0". """ return self.adapter.device @property def adapter(self): """ A :class:`BluetoothAdapter` object which represents the Bluetooth device the server is using. """ return self._adapter @property def port(self): """ The port the server is using. This defaults to 1. """ return self._port @property def encoding(self): """ The encoding standard the server is using. This defaults to "utf-8". """ return self._encoding @property def running(self): """ Returns a ``True`` if the server is running. """ return self._running @property def server_address(self): """ The `MAC address`_ of the device the server is using. .. _MAC address: https://en.wikipedia.org/wiki/MAC_address """ return self.adapter.address @property def client_address(self): """ The `MAC address`_ of the client connected to the server. Returns ``None`` if no client is connected. .. _MAC address: https://en.wikipedia.org/wiki/MAC_address """ if self._client_info: return self._client_info[0] else: return None @property def client_connected(self): """ Returns ``True`` if a client is connected. """ return self._client_connected @property def data_received_callback(self): """ Sets or returns the function which is called when data is received by the server. The function should accept a single parameter which when called will hold the data received. Set to ``None`` if received data is not required. """ return self._data_received_callback @data_received_callback.setter def data_received_callback(self, value): self._data_received_callback = value @property def when_client_connects(self): """ Sets or returns the function which is called when a client connects. """ return self._when_client_connects @when_client_connects.setter def when_client_connects(self, value): self._when_client_connects = value @property def when_client_disconnects(self): """ Sets or returns the function which is called when a client disconnects. """ return self._when_client_disconnects @when_client_disconnects.setter def when_client_disconnects(self, value): self._when_client_disconnects = value def start(self): """ Starts the Bluetooth server if its not already running. The server needs to be started before connections can be made. """ if not self._running: if self._power_up_device: self.adapter.powered = True if not self.adapter.powered: raise Exception("Bluetooth device {} is turned off".format(self.adapter.device)) #register the serial port profile with Bluetooth register_spp(self._port) #start Bluetooth server #open the Bluetooth socket self._server_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) self._server_sock.settimeout(BLUETOOTH_TIMEOUT) try: self._server_sock.bind((self.server_address, self.port)) except (socket.error, OSError) as e: if e.errno == errno.EADDRINUSE: print("Bluetooth address {} is already in use - is the server already running?".format(self.server_address)) raise e self._server_sock.listen(1) #wait for client connection self._conn_thread = WrapThread(target=self._wait_for_connection) self._conn_thread.start() self._running = True def stop(self): """ Stops the Bluetooth server if its running. """ if self._running: if self._conn_thread: self._conn_thread.stop() self._conn_thread = None def send(self, data): """ Send data to a connected Bluetooth client :param str data: The data to be sent. """ # print(data) if self._client_connected: if self._encoding is not None: data = data.encode(self._encoding) try: self._send_data(data) except IOError as e: self._handle_bt_error(e) def _send_data(self, data): """ Send raw data to the client. :param bytes data: The data to be sent. """ self._client_sock.sendall(data) def disconnect_client(self): """ Disconnects the client if connected. Returns `True` if a client was disconnected. """ if self._client_connected: self._client_connected = False # call the callback if self.when_client_disconnects: WrapThread(target=self.when_client_disconnects).start() return True else: return False def _setup_adapter(self, device): self._adapter = BluetoothAdapter(device) def _wait_for_connection(self): #keep going until the server is stopped while not self._conn_thread.stopping.is_set(): #wait for connection self._client_connected = False while not self._conn_thread.stopping.is_set(): try: # accept() will timeout after BLUETOOTH_TIMEOUT seconds self._client_sock, self._client_info = self._server_sock.accept() self._client_connected = True break except socket.timeout as e: self._handle_bt_error(e) #did a client connect? if self._client_connected: #call the call back if self.when_client_connects: WrapThread(target=self.when_client_connects).start() #read data self._read() #server has been stopped self._server_sock.close() self._server_sock = None self._running = False def _read(self): #read until the server is stopped or the client disconnects while self._client_connected: #read data from Bluetooth socket try: data = self._client_sock.recv(1024, socket.MSG_DONTWAIT) except IOError as e: self._handle_bt_error(e) data = b"" if data: if self._data_received_callback: if self._encoding: data = data.decode(self._encoding) self.data_received_callback(data) if self._conn_thread.stopping.wait(BLUETOOTH_TIMEOUT): break #close the client socket self._client_sock.close() self._client_sock = None self._client_info = None self._client_connected = False def _handle_bt_error(self, bt_error): assert isinstance(bt_error, IOError) #'timed out' is caused by the wait_for_connection loop if isinstance(bt_error, socket.timeout): pass #'resource unavailable' is when data cannot be read because there is nothing in the buffer elif bt_error.errno == errno.EAGAIN: pass #'connection reset' is caused when the client disconnects elif bt_error.errno == errno.ECONNRESET: self.disconnect_client() #'conection timeout' is caused when the server can no longer connect to read from the client # (perhaps the client has gone out of range) elif bt_error.errno == errno.ETIMEDOUT: self.disconnect_client() else: raise bt_error class BluetoothClient(): """ Creates a Bluetooth client which can send data to a server using RFCOMM Serial Data. The following example will create a Bluetooth client which will connect to a paired device called "raspberrypi", send "helloworld" and print any data is receives:: from bluedot.btcomm import BluetoothClient from signal import pause def data_received(data): print(data) c = BluetoothClient("raspberrypi", data_received) c.send("helloworld") pause() :param str server: The server name ("raspberrypi") or server MAC address ("11:11:11:11:11:11") to connect to. The server must be a paired device. :param data_received_callback: A function reference should be passed, this function will be called when data is received by the client. The function should accept a single parameter which when called will hold the data received. Set to ``None`` if data received is not required. :param int port: The Bluetooth port the client should use, the default is 1. :param str device: The Bluetooth device to be used, the default is "hci0", if your device only has 1 Bluetooth adapter this shouldn't need to be changed. :param str encoding: The encoding standard to be used when sending and receiving byte data. The default is "utf-8". If set to ``None`` no encoding is done and byte data types should be used. :param bool power_up_device: If ``True``, the Bluetooth device will be powered up (if required) when the server starts. The default is ``False``. Depending on how Bluetooth has been powered down, you may need to use :command:`rfkill` to unblock Bluetooth to give permission to Bluez to power on Bluetooth:: sudo rfkill unblock bluetooth :param bool auto_connect: If ``True`` (the default), the Bluetooth client will automatically try to connect to the server at initialisation, if ``False``, the :meth:`connect` method will need to be called. """ def __init__(self, server, data_received_callback, port = 1, device = "hci0", encoding = "utf-8", power_up_device = False, auto_connect = True): self._server = server self._data_received_callback = data_received_callback self._port = port self._power_up_device = power_up_device self._encoding = encoding self._setup_adapter(device) self._connected = False self._client_sock = None self._conn_thread = None if auto_connect: self.connect() @property def device(self): """ The Bluetooth device the client is using. This defaults to "hci0". """ return self.adapter.device @property def server(self): """ The server name ("raspberrypi") or server `MAC address`_ ("11:11:11:11:11:11") to connect to. .. _MAC address: https://en.wikipedia.org/wiki/MAC_address """ return self._server @property def port(self): """ The port the client is using. This defaults to 1. """ return self._port @property def adapter(self): """ A :class:`BluetoothAdapter` object which represents the Bluetooth device the client is using. """ return self._adapter @property def encoding(self): """ The encoding standard the client is using. The default is "utf-8". """ return self._encoding @property def client_address(self): """ The MAC address of the device being used. """ return self.adapter.address @property def connected(self): """ Returns ``True`` when connected. """ return self._connected @property def data_received_callback(self): """ Sets or returns the function which is called when data is received by the client. The function should accept a single parameter which when called will hold the data received. Set to ``None`` if data received is not required. """ return self._data_received_callback @data_received_callback.setter def data_received_callback(self, value): self._data_received_callback = value def connect(self): """ Connect to a Bluetooth server. """ if not self._connected: if self._power_up_device: self.adapter.powered = True if not self.adapter.powered: raise Exception("Bluetooth device {} is turned off".format(self.adapter.device)) #try and find the server name or MAC address in the paired devices list server_mac = None for device in self.adapter.paired_devices: if self._server == device[0] or self._server == device[1]: server_mac = device[0] break if server_mac == None: raise Exception("Server {} not found in paired devices".format(self._server)) #create a socket self._client_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) self._client_sock.bind((self.adapter.address, self._port)) self._client_sock.connect((server_mac, self._port)) self._connected = True self._conn_thread = WrapThread(target=self._read) self._conn_thread.start() def disconnect(self): """ Disconnect from a Bluetooth server. """ if self._connected: #stop the connection thread if self._conn_thread: self._conn_thread.stop() self._conn_thread = None #close the socket try: self._client_sock.close() finally: self._client_sock = None self._connected = False def send(self, data): """ Send data to a Bluetooth server. :param str data: The data to be sent. """ if self._connected: if self._encoding is not None: data = data.encode(self._encoding) try: self._send_data(data) except IOError as e: self._handle_bt_error(e) def _send_data(self, data): """ Send raw data to the client. :param bytes data: The data to be sent. """ self._client_sock.sendall(data) def _read(self): #read until the client is stopped or the client disconnects while self._connected: #read data from Bluetooth socket try: data = self._client_sock.recv(1024, socket.MSG_DONTWAIT) except IOError as e: self._handle_bt_error(e) data = b"" if data: #print("received [%s]" % data) if self._data_received_callback: if self._encoding: data = data.decode(self._encoding) self.data_received_callback(data) if self._conn_thread.stopping.wait(BLUETOOTH_TIMEOUT): break def _setup_adapter(self, device): self._adapter = BluetoothAdapter(device) def _handle_bt_error(self, bt_error): assert isinstance(bt_error, IOError) #'resource unavailable' is when data cannot be read because there is nothing in the buffer if bt_error.errno == errno.EAGAIN: pass #'connection reset' is caused when the client disconnects elif bt_error.errno == errno.ECONNRESET: self._connected = False #'conection timeout' is caused when the server can no longer connect to read from the client # (perhaps the client has gone out of range) elif bt_error.errno == errno.ETIMEDOUT: self._connected = False else: raise bt_error ================================================ FILE: bluedot/colors.py ================================================ # color codes obtained from https://www.webucator.com/blog/2015/03/python-color-constants-module/ class Color: """ Represents a color within bluedot. Used to change the color of the dot. Color objects are immutable. :param int red: The red value of the color `0 - 255`. Default is `255`. :param int green: The green value of the color `0 - 255`. Default is `255`. :param int green: The blue value of the color `0 - 255`. Default is `255`. :param int green: The alpha value of the color `0 - 255`. `0` is transparent. Default is `255`. """ def __init__(self, red = 255, green = 255, blue = 255, alpha = 255): self._red = red self._green = green self._blue = blue self._alpha = alpha @property def red(self): """ Returns the red value of the color. """ return self._red @property def green(self): """ Returns the green value of the color. """ return self._green @property def blue(self): """ Returns the blue value of the color. """ return self._blue @property def alpha(self): """ Returns the alpha value of the color. """ return self._alpha @property def rgb(self): """ Returns a tuple of `(red, green, blue)` values. """ return (self._red, self._green, self._blue) @property def rgba(self): """ Returns a tuple of `(red, green, blue, alpha)` values. """ return (self._red, self._green, self._blue, self._alpha) @property def str_rgb(self): """ Returns a string of red, green, blue hex values in the format `#rrggbb`. """ return '#%02x%02x%02x' % (self._red, self._green, self._blue) @property def str_rgba(self): """ Returns a string of red, green, blue, alpha hex values in the format `#rrggbbaa`. """ return '#%02x%02x%02x%02x' % (self._red, self._green, self._blue, self._alpha) @property def str_argb(self): """ Returns a string of alpha, red, green, blue hex values in the format `#aarrggbb`. """ return '#%02x%02x%02x%02x' % (self._alpha, self._red, self._green, self._blue) def get_adjusted_color(self, factor): """ Returns a new Color object based on this Color adjusted by a factor :param float factor: The value to adjust this color by. """ return Color(self.red * factor, self.green * factor, self.blue * factor) def __eq__(self, other): other = parse_color(other) return self._red == other._red and self._green == other._green and self._blue == other._blue and self._alpha == other._alpha def __str__(self): return self.str_rgba ALICEBLUE = Color(240, 248, 255) ANTIQUEWHITE = Color(250, 235, 215) ANTIQUEWHITE1 = Color(255, 239, 219) ANTIQUEWHITE2 = Color(238, 223, 204) ANTIQUEWHITE3 = Color(205, 192, 176) ANTIQUEWHITE4 = Color(139, 131, 120) AQUA = Color(0, 255, 255) AQUAMARINE1 = Color(127, 255, 212) AQUAMARINE2 = Color(118, 238, 198) AQUAMARINE3 = Color(102, 205, 170) AQUAMARINE4 = Color(69, 139, 116) AZURE1 = Color(240, 255, 255) AZURE2 = Color(224, 238, 238) AZURE3 = Color(193, 205, 205) AZURE4 = Color(131, 139, 139) BANANA = Color(227, 207, 87) BEIGE = Color(245, 245, 220) BISQUE1 = Color(255, 228, 196) BISQUE2 = Color(238, 213, 183) BISQUE3 = Color(205, 183, 158) BISQUE4 = Color(139, 125, 107) BLACK = Color(0, 0, 0) BLANCHEDALMOND = Color(255, 235, 205) BLUE = Color(0, 0, 255) BLUE2 = Color(0, 0, 238) BLUE3 = Color(0, 0, 205) BLUE4 = Color(0, 0, 139) BLUEVIOLET = Color(138, 43, 226) BRICK = Color(156, 102, 31) BROWN = Color(165, 42, 42) BROWN1 = Color(255, 64, 64) BROWN2 = Color(238, 59, 59) BROWN3 = Color(205, 51, 51) BROWN4 = Color(139, 35, 35) BURLYWOOD = Color(222, 184, 135) BURLYWOOD1 = Color(255, 211, 155) BURLYWOOD2 = Color(238, 197, 145) BURLYWOOD3 = Color(205, 170, 125) BURLYWOOD4 = Color(139, 115, 85) BURNTSIENNA = Color(138, 54, 15) BURNTUMBER = Color(138, 51, 36) CADETBLUE = Color(95, 158, 160) CADETBLUE1 = Color(152, 245, 255) CADETBLUE2 = Color(142, 229, 238) CADETBLUE3 = Color(122, 197, 205) CADETBLUE4 = Color(83, 134, 139) CADMIUMORANGE = Color(255, 97, 3) CADMIUMYELLOW = Color(255, 153, 18) CARROT = Color(237, 145, 33) CHARTREUSE1 = Color(127, 255, 0) CHARTREUSE2 = Color(118, 238, 0) CHARTREUSE3 = Color(102, 205, 0) CHARTREUSE4 = Color(69, 139, 0) CHOCOLATE = Color(210, 105, 30) CHOCOLATE1 = Color(255, 127, 36) CHOCOLATE2 = Color(238, 118, 33) CHOCOLATE3 = Color(205, 102, 29) CHOCOLATE4 = Color(139, 69, 19) COBALT = Color(61, 89, 171) COBALTGREEN = Color(61, 145, 64) COLDGREY = Color(128, 138, 135) CORAL = Color(255, 127, 80) CORAL1 = Color(255, 114, 86) CORAL2 = Color(238, 106, 80) CORAL3 = Color(205, 91, 69) CORAL4 = Color(139, 62, 47) CORNFLOWERBLUE = Color(100, 149, 237) CORNSILK1 = Color(255, 248, 220) CORNSILK2 = Color(238, 232, 205) CORNSILK3 = Color(205, 200, 177) CORNSILK4 = Color(139, 136, 120) CRIMSON = Color(220, 20, 60) CYAN1 = Color(0, 238, 238) CYAN2 = Color(0, 205, 205) CYAN3 = Color(0, 139, 139) DARKGOLDENROD = Color(184, 134, 11) DARKGOLDENROD1 = Color(255, 185, 15) DARKGOLDENROD2 = Color(238, 173, 14) DARKGOLDENROD3 = Color(205, 149, 12) DARKGOLDENROD4 = Color(139, 101, 8) DARKGRAY = Color(169, 169, 169) DARKGREEN = Color(0, 100, 0) DARKKHAKI = Color(189, 183, 107) DARKOLIVEGREEN = Color(85, 107, 47) DARKOLIVEGREEN1 = Color(202, 255, 112) DARKOLIVEGREEN2 = Color(188, 238, 104) DARKOLIVEGREEN3 = Color(162, 205, 90) DARKOLIVEGREEN4 = Color(110, 139, 61) DARKORANGE = Color(255, 140, 0) DARKORANGE1 = Color(255, 127, 0) DARKORANGE2 = Color(238, 118, 0) DARKORANGE3 = Color(205, 102, 0) DARKORANGE4 = Color(139, 69, 0) DARKORCHID = Color(153, 50, 204) DARKORCHID1 = Color(191, 62, 255) DARKORCHID2 = Color(178, 58, 238) DARKORCHID3 = Color(154, 50, 205) DARKORCHID4 = Color(104, 34, 139) DARKSALMON = Color(233, 150, 122) DARKSEAGREEN = Color(143, 188, 143) DARKSEAGREEN1 = Color(193, 255, 193) DARKSEAGREEN2 = Color(180, 238, 180) DARKSEAGREEN3 = Color(155, 205, 155) DARKSEAGREEN4 = Color(105, 139, 105) DARKSLATEBLUE = Color(72, 61, 139) DARKSLATEGRAY = Color(47, 79, 79) DARKSLATEGRAY1 = Color(151, 255, 255) DARKSLATEGRAY2 = Color(141, 238, 238) DARKSLATEGRAY3 = Color(121, 205, 205) DARKSLATEGRAY4 = Color(82, 139, 139) DARKTURQUOISE = Color(0, 206, 209) DARKVIOLET = Color(148, 0, 211) DEEPPINK1 = Color(255, 20, 147) DEEPPINK2 = Color(238, 18, 137) DEEPPINK3 = Color(205, 16, 118) DEEPPINK4 = Color(139, 10, 80) DEEPSKYBLUE1 = Color(0, 191, 255) DEEPSKYBLUE2 = Color(0, 178, 238) DEEPSKYBLUE3 = Color(0, 154, 205) DEEPSKYBLUE4 = Color(0, 104, 139) DIMGRAY = Color(105, 105, 105) DODGERBLUE1 = Color(30, 144, 255) DODGERBLUE2 = Color(28, 134, 238) DODGERBLUE3 = Color(24, 116, 205) DODGERBLUE4 = Color(16, 78, 139) EGGSHELL = Color(252, 230, 201) EMERALDGREEN = Color(0, 201, 87) FIREBRICK = Color(178, 34, 34) FIREBRICK1 = Color(255, 48, 48) FIREBRICK2 = Color(238, 44, 44) FIREBRICK3 = Color(205, 38, 38) FIREBRICK4 = Color(139, 26, 26) FLESH = Color(255, 125, 64) FLORALWHITE = Color(255, 250, 240) FORESTGREEN = Color(34, 139, 34) GAINSBORO = Color(220, 220, 220) GHOSTWHITE = Color(248, 248, 255) GOLD1 = Color(255, 215, 0) GOLD2 = Color(238, 201, 0) GOLD3 = Color(205, 173, 0) GOLD4 = Color(139, 117, 0) GOLDENROD = Color(218, 165, 32) GOLDENROD1 = Color(255, 193, 37) GOLDENROD2 = Color(238, 180, 34) GOLDENROD3 = Color(205, 155, 29) GOLDENROD4 = Color(139, 105, 20) GRAY = Color(128, 128, 128) GRAY1 = Color(3, 3, 3) GRAY10 = Color(26, 26, 26) GRAY11 = Color(28, 28, 28) GRAY12 = Color(31, 31, 31) GRAY13 = Color(33, 33, 33) GRAY14 = Color(36, 36, 36) GRAY15 = Color(38, 38, 38) GRAY16 = Color(41, 41, 41) GRAY17 = Color(43, 43, 43) GRAY18 = Color(46, 46, 46) GRAY19 = Color(48, 48, 48) GRAY2 = Color(5, 5, 5) GRAY20 = Color(51, 51, 51) GRAY21 = Color(54, 54, 54) GRAY22 = Color(56, 56, 56) GRAY23 = Color(59, 59, 59) GRAY24 = Color(61, 61, 61) GRAY25 = Color(64, 64, 64) GRAY26 = Color(66, 66, 66) GRAY27 = Color(69, 69, 69) GRAY28 = Color(71, 71, 71) GRAY29 = Color(74, 74, 74) GRAY3 = Color(8, 8, 8) GRAY30 = Color(77, 77, 77) GRAY31 = Color(79, 79, 79) GRAY32 = Color(82, 82, 82) GRAY33 = Color(84, 84, 84) GRAY34 = Color(87, 87, 87) GRAY35 = Color(89, 89, 89) GRAY36 = Color(92, 92, 92) GRAY37 = Color(94, 94, 94) GRAY38 = Color(97, 97, 97) GRAY39 = Color(99, 99, 99) GRAY4 = Color(10, 10, 10) GRAY40 = Color(102, 102, 102) GRAY42 = Color(107, 107, 107) GRAY43 = Color(110, 110, 110) GRAY44 = Color(112, 112, 112) GRAY45 = Color(115, 115, 115) GRAY46 = Color(117, 117, 117) GRAY47 = Color(120, 120, 120) GRAY48 = Color(122, 122, 122) GRAY49 = Color(125, 125, 125) GRAY5 = Color(13, 13, 13) GRAY50 = Color(127, 127, 127) GRAY51 = Color(130, 130, 130) GRAY52 = Color(133, 133, 133) GRAY53 = Color(135, 135, 135) GRAY54 = Color(138, 138, 138) GRAY55 = Color(140, 140, 140) GRAY56 = Color(143, 143, 143) GRAY57 = Color(145, 145, 145) GRAY58 = Color(148, 148, 148) GRAY59 = Color(150, 150, 150) GRAY6 = Color(15, 15, 15) GRAY60 = Color(153, 153, 153) GRAY61 = Color(156, 156, 156) GRAY62 = Color(158, 158, 158) GRAY63 = Color(161, 161, 161) GRAY64 = Color(163, 163, 163) GRAY65 = Color(166, 166, 166) GRAY66 = Color(168, 168, 168) GRAY67 = Color(171, 171, 171) GRAY68 = Color(173, 173, 173) GRAY69 = Color(176, 176, 176) GRAY7 = Color(18, 18, 18) GRAY70 = Color(179, 179, 179) GRAY71 = Color(181, 181, 181) GRAY72 = Color(184, 184, 184) GRAY73 = Color(186, 186, 186) GRAY74 = Color(189, 189, 189) GRAY75 = Color(191, 191, 191) GRAY76 = Color(194, 194, 194) GRAY77 = Color(196, 196, 196) GRAY78 = Color(199, 199, 199) GRAY79 = Color(201, 201, 201) GRAY8 = Color(20, 20, 20) GRAY80 = Color(204, 204, 204) GRAY81 = Color(207, 207, 207) GRAY82 = Color(209, 209, 209) GRAY83 = Color(212, 212, 212) GRAY84 = Color(214, 214, 214) GRAY85 = Color(217, 217, 217) GRAY86 = Color(219, 219, 219) GRAY87 = Color(222, 222, 222) GRAY88 = Color(224, 224, 224) GRAY89 = Color(227, 227, 227) GRAY9 = Color(23, 23, 23) GRAY90 = Color(229, 229, 229) GRAY91 = Color(232, 232, 232) GRAY92 = Color(235, 235, 235) GRAY93 = Color(237, 237, 237) GRAY94 = Color(240, 240, 240) GRAY95 = Color(242, 242, 242) GRAY97 = Color(247, 247, 247) GRAY98 = Color(250, 250, 250) GRAY99 = Color(252, 252, 252) GREEN = Color(0, 128, 0) GREEN1 = Color(0, 255, 0) GREEN2 = Color(0, 238, 0) GREEN3 = Color(0, 205, 0) GREEN4 = Color(0, 139, 0) GREENYELLOW = Color(173, 255, 47) HONEYDEW1 = Color(240, 255, 240) HONEYDEW2 = Color(224, 238, 224) HONEYDEW3 = Color(193, 205, 193) HONEYDEW4 = Color(131, 139, 131) HOTPINK = Color(255, 105, 180) HOTPINK1 = Color(255, 110, 180) HOTPINK2 = Color(238, 106, 167) HOTPINK3 = Color(205, 96, 144) HOTPINK4 = Color(139, 58, 98) INDIANRED = Color(176, 23, 31) INDIANRED = Color(205, 92, 92) INDIANRED1 = Color(255, 106, 106) INDIANRED2 = Color(238, 99, 99) INDIANRED3 = Color(205, 85, 85) INDIANRED4 = Color(139, 58, 58) INDIGO = Color(75, 0, 130) IVORY1 = Color(255, 255, 240) IVORY2 = Color(238, 238, 224) IVORY3 = Color(205, 205, 193) IVORY4 = Color(139, 139, 131) IVORYBLACK = Color(41, 36, 33) KHAKI = Color(240, 230, 140) KHAKI1 = Color(255, 246, 143) KHAKI2 = Color(238, 230, 133) KHAKI3 = Color(205, 198, 115) KHAKI4 = Color(139, 134, 78) LAVENDER = Color(230, 230, 250) LAVENDERBLUSH1 = Color(255, 240, 245) LAVENDERBLUSH2 = Color(238, 224, 229) LAVENDERBLUSH3 = Color(205, 193, 197) LAVENDERBLUSH4 = Color(139, 131, 134) LAWNGREEN = Color(124, 252, 0) LEMONCHIFFON1 = Color(255, 250, 205) LEMONCHIFFON2 = Color(238, 233, 191) LEMONCHIFFON3 = Color(205, 201, 165) LEMONCHIFFON4 = Color(139, 137, 112) LIGHTBLUE = Color(173, 216, 230) LIGHTBLUE1 = Color(191, 239, 255) LIGHTBLUE2 = Color(178, 223, 238) LIGHTBLUE3 = Color(154, 192, 205) LIGHTBLUE4 = Color(104, 131, 139) LIGHTCORAL = Color(240, 128, 128) LIGHTCYAN1 = Color(224, 255, 255) LIGHTCYAN2 = Color(209, 238, 238) LIGHTCYAN3 = Color(180, 205, 205) LIGHTCYAN4 = Color(122, 139, 139) LIGHTGOLDENROD1 = Color(255, 236, 139) LIGHTGOLDENROD2 = Color(238, 220, 130) LIGHTGOLDENROD3 = Color(205, 190, 112) LIGHTGOLDENROD4 = Color(139, 129, 76) LIGHTGOLDENRODYELLOW = Color(250, 250, 210) LIGHTGREY = Color(211, 211, 211) LIGHTPINK = Color(255, 182, 193) LIGHTPINK1 = Color(255, 174, 185) LIGHTPINK2 = Color(238, 162, 173) LIGHTPINK3 = Color(205, 140, 149) LIGHTPINK4 = Color(139, 95, 101) LIGHTSALMON1 = Color(255, 160, 122) LIGHTSALMON2 = Color(238, 149, 114) LIGHTSALMON3 = Color(205, 129, 98) LIGHTSALMON4 = Color(139, 87, 66) LIGHTSEAGREEN = Color(32, 178, 170) LIGHTSKYBLUE = Color(135, 206, 250) LIGHTSKYBLUE1 = Color(176, 226, 255) LIGHTSKYBLUE2 = Color(164, 211, 238) LIGHTSKYBLUE3 = Color(141, 182, 205) LIGHTSKYBLUE4 = Color(96, 123, 139) LIGHTSLATEBLUE = Color(132, 112, 255) LIGHTSLATEGRAY = Color(119, 136, 153) LIGHTSTEELBLUE = Color(176, 196, 222) LIGHTSTEELBLUE1 = Color(202, 225, 255) LIGHTSTEELBLUE2 = Color(188, 210, 238) LIGHTSTEELBLUE3 = Color(162, 181, 205) LIGHTSTEELBLUE4 = Color(110, 123, 139) LIGHTYELLOW1 = Color(255, 255, 224) LIGHTYELLOW2 = Color(238, 238, 209) LIGHTYELLOW3 = Color(205, 205, 180) LIGHTYELLOW4 = Color(139, 139, 122) LIMEGREEN = Color(50, 205, 50) LINEN = Color(250, 240, 230) MAGENTA = Color(255, 0, 255) MAGENTA2 = Color(238, 0, 238) MAGENTA3 = Color(205, 0, 205) MAGENTA4 = Color(139, 0, 139) MANGANESEBLUE = Color(3, 168, 158) MAROON = Color(128, 0, 0) MAROON1 = Color(255, 52, 179) MAROON2 = Color(238, 48, 167) MAROON3 = Color(205, 41, 144) MAROON4 = Color(139, 28, 98) MEDIUMORCHID = Color(186, 85, 211) MEDIUMORCHID1 = Color(224, 102, 255) MEDIUMORCHID2 = Color(209, 95, 238) MEDIUMORCHID3 = Color(180, 82, 205) MEDIUMORCHID4 = Color(122, 55, 139) MEDIUMPURPLE = Color(147, 112, 219) MEDIUMPURPLE1 = Color(171, 130, 255) MEDIUMPURPLE2 = Color(159, 121, 238) MEDIUMPURPLE3 = Color(137, 104, 205) MEDIUMPURPLE4 = Color(93, 71, 139) MEDIUMSEAGREEN = Color(60, 179, 113) MEDIUMSLATEBLUE = Color(123, 104, 238) MEDIUMSPRINGGREEN = Color(0, 250, 154) MEDIUMTURQUOISE = Color(72, 209, 204) MEDIUMVIOLETRED = Color(199, 21, 133) MELON = Color(227, 168, 105) MIDNIGHTBLUE = Color(25, 25, 112) MINT = Color(189, 252, 201) MINTCREAM = Color(245, 255, 250) MISTYROSE1 = Color(255, 228, 225) MISTYROSE2 = Color(238, 213, 210) MISTYROSE3 = Color(205, 183, 181) MISTYROSE4 = Color(139, 125, 123) MOCCASIN = Color(255, 228, 181) NAVAJOWHITE1 = Color(255, 222, 173) NAVAJOWHITE2 = Color(238, 207, 161) NAVAJOWHITE3 = Color(205, 179, 139) NAVAJOWHITE4 = Color(139, 121, 94) NAVY = Color(0, 0, 128) OLDLACE = Color(253, 245, 230) OLIVE = Color(128, 128, 0) OLIVEDRAB = Color(107, 142, 35) OLIVEDRAB1 = Color(192, 255, 62) OLIVEDRAB2 = Color(179, 238, 58) OLIVEDRAB3 = Color(154, 205, 50) OLIVEDRAB4 = Color(105, 139, 34) ORANGE = Color(255, 128, 0) ORANGE1 = Color(255, 165, 0) ORANGE2 = Color(238, 154, 0) ORANGE3 = Color(205, 133, 0) ORANGE4 = Color(139, 90, 0) ORANGERED1 = Color(255, 69, 0) ORANGERED2 = Color(238, 64, 0) ORANGERED3 = Color(205, 55, 0) ORANGERED4 = Color(139, 37, 0) ORCHID = Color(218, 112, 214) ORCHID1 = Color(255, 131, 250) ORCHID2 = Color(238, 122, 233) ORCHID3 = Color(205, 105, 201) ORCHID4 = Color(139, 71, 137) PALEGOLDENROD = Color(238, 232, 170) PALEGREEN = Color(152, 251, 152) PALEGREEN1 = Color(154, 255, 154) PALEGREEN2 = Color(144, 238, 144) PALEGREEN3 = Color(124, 205, 124) PALEGREEN4 = Color(84, 139, 84) PALETURQUOISE1 = Color(187, 255, 255) PALETURQUOISE2 = Color(174, 238, 238) PALETURQUOISE3 = Color(150, 205, 205) PALETURQUOISE4 = Color(102, 139, 139) PALEVIOLETRED = Color(219, 112, 147) PALEVIOLETRED1 = Color(255, 130, 171) PALEVIOLETRED2 = Color(238, 121, 159) PALEVIOLETRED3 = Color(205, 104, 137) PALEVIOLETRED4 = Color(139, 71, 93) PAPAYAWHIP = Color(255, 239, 213) PEACHPUFF1 = Color(255, 218, 185) PEACHPUFF2 = Color(238, 203, 173) PEACHPUFF3 = Color(205, 175, 149) PEACHPUFF4 = Color(139, 119, 101) PEACOCK = Color(51, 161, 201) PINK = Color(255, 192, 203) PINK1 = Color(255, 181, 197) PINK2 = Color(238, 169, 184) PINK3 = Color(205, 145, 158) PINK4 = Color(139, 99, 108) PLUM = Color(221, 160, 221) PLUM1 = Color(255, 187, 255) PLUM2 = Color(238, 174, 238) PLUM3 = Color(205, 150, 205) PLUM4 = Color(139, 102, 139) POWDERBLUE = Color(176, 224, 230) PURPLE = Color(128, 0, 128) PURPLE1 = Color(155, 48, 255) PURPLE2 = Color(145, 44, 238) PURPLE3 = Color(125, 38, 205) PURPLE4 = Color(85, 26, 139) RASPBERRY = Color(135, 38, 87) RAWSIENNA = Color(199, 97, 20) RED = Color(255, 0, 0) RED1 = Color(238, 0, 0) RED2 = Color(205, 0, 0) RED3 = Color(139, 0, 0) RED4 = Color(125, 0, 0) ROSYBROWN = Color(188, 143, 143) ROSYBROWN1 = Color(255, 193, 193) ROSYBROWN2 = Color(238, 180, 180) ROSYBROWN3 = Color(205, 155, 155) ROSYBROWN4 = Color(139, 105, 105) ROYALBLUE = Color(65, 105, 225) ROYALBLUE1 = Color(72, 118, 255) ROYALBLUE2 = Color(67, 110, 238) ROYALBLUE3 = Color(58, 95, 205) ROYALBLUE4 = Color(39, 64, 139) SALMON = Color(250, 128, 114) SALMON1 = Color(255, 140, 105) SALMON2 = Color(238, 130, 98) SALMON3 = Color(205, 112, 84) SALMON4 = Color(139, 76, 57) SANDYBROWN = Color(244, 164, 96) SAPGREEN = Color(48, 128, 20) SEAGREEN1 = Color(84, 255, 159) SEAGREEN2 = Color(78, 238, 148) SEAGREEN3 = Color(67, 205, 128) SEAGREEN4 = Color(46, 139, 87) SEASHELL1 = Color(255, 245, 238) SEASHELL2 = Color(238, 229, 222) SEASHELL3 = Color(205, 197, 191) SEASHELL4 = Color(139, 134, 130) SEPIA = Color(94, 38, 18) SGIBEET = Color(142, 56, 142) SGIBRIGHTGRAY = Color(197, 193, 170) SGICHARTREUSE = Color(113, 198, 113) SGIDARKGRAY = Color(85, 85, 85) SGIGRAY12 = Color(30, 30, 30) SGIGRAY16 = Color(40, 40, 40) SGIGRAY32 = Color(81, 81, 81) SGIGRAY36 = Color(91, 91, 91) SGIGRAY52 = Color(132, 132, 132) SGIGRAY56 = Color(142, 142, 142) SGIGRAY72 = Color(183, 183, 183) SGIGRAY76 = Color(193, 193, 193) SGIGRAY92 = Color(234, 234, 234) SGIGRAY96 = Color(244, 244, 244) SGILIGHTBLUE = Color(125, 158, 192) SGILIGHTGRAY = Color(170, 170, 170) SGIOLIVEDRAB = Color(142, 142, 56) SGISALMON = Color(198, 113, 113) SGISLATEBLUE = Color(113, 113, 198) SGITEAL = Color(56, 142, 142) SIENNA = Color(160, 82, 45) SIENNA1 = Color(255, 130, 71) SIENNA2 = Color(238, 121, 66) SIENNA3 = Color(205, 104, 57) SIENNA4 = Color(139, 71, 38) SILVER = Color(192, 192, 192) SKYBLUE = Color(135, 206, 235) SKYBLUE1 = Color(135, 206, 255) SKYBLUE2 = Color(126, 192, 238) SKYBLUE3 = Color(108, 166, 205) SKYBLUE4 = Color(74, 112, 139) SLATEBLUE = Color(106, 90, 205) SLATEBLUE1 = Color(131, 111, 255) SLATEBLUE2 = Color(122, 103, 238) SLATEBLUE3 = Color(105, 89, 205) SLATEBLUE4 = Color(71, 60, 139) SLATEGRAY = Color(112, 128, 144) SLATEGRAY1 = Color(198, 226, 255) SLATEGRAY2 = Color(185, 211, 238) SLATEGRAY3 = Color(159, 182, 205) SLATEGRAY4 = Color(108, 123, 139) SNOW1 = Color(255, 250, 250) SNOW2 = Color(238, 233, 233) SNOW3 = Color(205, 201, 201) SNOW4 = Color(139, 137, 137) SPRINGGREEN = Color(0, 255, 127) SPRINGGREEN1 = Color(0, 238, 118) SPRINGGREEN2 = Color(0, 205, 102) SPRINGGREEN3 = Color(0, 139, 69) STEELBLUE = Color(70, 130, 180) STEELBLUE1 = Color(99, 184, 255) STEELBLUE2 = Color(92, 172, 238) STEELBLUE3 = Color(79, 148, 205) STEELBLUE4 = Color(54, 100, 139) TAN = Color(210, 180, 140) TAN1 = Color(255, 165, 79) TAN2 = Color(238, 154, 73) TAN3 = Color(205, 133, 63) TAN4 = Color(139, 90, 43) TEAL = Color(0, 128, 128) THISTLE = Color(216, 191, 216) THISTLE1 = Color(255, 225, 255) THISTLE2 = Color(238, 210, 238) THISTLE3 = Color(205, 181, 205) THISTLE4 = Color(139, 123, 139) TOMATO1 = Color(255, 99, 71) TOMATO2 = Color(238, 92, 66) TOMATO3 = Color(205, 79, 57) TOMATO4 = Color(139, 54, 38) TURQUOISE = Color(64, 224, 208) TURQUOISE1 = Color(0, 245, 255) TURQUOISE2 = Color(0, 229, 238) TURQUOISE3 = Color(0, 197, 205) TURQUOISE4 = Color(0, 134, 139) TURQUOISEBLUE = Color(0, 199, 140) VIOLET = Color(238, 130, 238) VIOLETRED = Color(208, 32, 144) VIOLETRED1 = Color(255, 62, 150) VIOLETRED2 = Color(238, 58, 140) VIOLETRED3 = Color(205, 50, 120) VIOLETRED4 = Color(139, 34, 82) WARMGREY = Color(128, 128, 105) WHEAT = Color(245, 222, 179) WHEAT1 = Color(255, 231, 186) WHEAT2 = Color(238, 216, 174) WHEAT3 = Color(205, 186, 150) WHEAT4 = Color(139, 126, 102) WHITE = Color(255, 255, 255) WHITESMOKE = Color(245, 245, 245) WHITESMOKE = Color(245, 245, 245) YELLOW = Color(255, 255, 0) YELLOW1 = Color(255, 255, 0) YELLOW2 = Color(238, 238, 0) YELLOW3 = Color(205, 205, 0) YELLOW4 = Color(139, 139, 0) COLORS = {} # Add colors to colors dictionary COLORS["aliceblue"] = ALICEBLUE COLORS["antiquewhite"] = ANTIQUEWHITE COLORS["antiquewhite1"] = ANTIQUEWHITE1 COLORS["antiquewhite2"] = ANTIQUEWHITE2 COLORS["antiquewhite3"] = ANTIQUEWHITE3 COLORS["antiquewhite4"] = ANTIQUEWHITE4 COLORS["aqua"] = AQUA COLORS["aquamarine"] = AQUAMARINE1 COLORS["aquamarine1"] = AQUAMARINE1 COLORS["aquamarine2"] = AQUAMARINE2 COLORS["aquamarine3"] = AQUAMARINE3 COLORS["aquamarine4"] = AQUAMARINE4 COLORS["azure"] = AZURE1 COLORS["azure1"] = AZURE1 COLORS["azure2"] = AZURE2 COLORS["azure3"] = AZURE3 COLORS["azure4"] = AZURE4 COLORS["banana"] = BANANA COLORS["beige"] = BEIGE COLORS["bisque"] = BISQUE1 COLORS["bisque1"] = BISQUE1 COLORS["bisque2"] = BISQUE2 COLORS["bisque3"] = BISQUE3 COLORS["bisque4"] = BISQUE4 COLORS["black"] = BLACK COLORS["blanchedalmond"] = BLANCHEDALMOND COLORS["blue"] = BLUE COLORS["blue1"] = BLUE COLORS["blue2"] = BLUE2 COLORS["blue3"] = BLUE3 COLORS["blue4"] = BLUE4 COLORS["blueviolet"] = BLUEVIOLET COLORS["brick"] = BRICK COLORS["brown"] = BROWN COLORS["brown1"] = BROWN1 COLORS["brown2"] = BROWN2 COLORS["brown3"] = BROWN3 COLORS["brown4"] = BROWN4 COLORS["burlywood"] = BURLYWOOD COLORS["burlywood1"] = BURLYWOOD1 COLORS["burlywood2"] = BURLYWOOD2 COLORS["burlywood3"] = BURLYWOOD3 COLORS["burlywood4"] = BURLYWOOD4 COLORS["burntsienna"] = BURNTSIENNA COLORS["burntumber"] = BURNTUMBER COLORS["cadetblue"] = CADETBLUE COLORS["cadetblue1"] = CADETBLUE1 COLORS["cadetblue2"] = CADETBLUE2 COLORS["cadetblue3"] = CADETBLUE3 COLORS["cadetblue4"] = CADETBLUE4 COLORS["cadmiumorange"] = CADMIUMORANGE COLORS["cadmiumyellow"] = CADMIUMYELLOW COLORS["carrot"] = CARROT COLORS["chartreuse"] = CHARTREUSE1 COLORS["chartreuse1"] = CHARTREUSE1 COLORS["chartreuse2"] = CHARTREUSE2 COLORS["chartreuse3"] = CHARTREUSE3 COLORS["chartreuse4"] = CHARTREUSE4 COLORS["chocolate"] = CHOCOLATE COLORS["chocolate1"] = CHOCOLATE1 COLORS["chocolate2"] = CHOCOLATE2 COLORS["chocolate3"] = CHOCOLATE3 COLORS["chocolate4"] = CHOCOLATE4 COLORS["cobalt"] = COBALT COLORS["cobaltgreen"] = COBALTGREEN COLORS["coldgrey"] = COLDGREY COLORS["coral"] = CORAL COLORS["coral1"] = CORAL1 COLORS["coral2"] = CORAL2 COLORS["coral3"] = CORAL3 COLORS["coral4"] = CORAL4 COLORS["cornflowerblue"] = CORNFLOWERBLUE COLORS["cornsilk"] = CORNSILK1 COLORS["cornsilk1"] = CORNSILK1 COLORS["cornsilk2"] = CORNSILK2 COLORS["cornsilk3"] = CORNSILK3 COLORS["cornsilk4"] = CORNSILK4 COLORS["crimson"] = CRIMSON COLORS["cyan"] = CYAN1 COLORS["cyan1"] = CYAN1 COLORS["cyan2"] = CYAN2 COLORS["cyan3"] = CYAN3 COLORS["darkgoldenrod"] = DARKGOLDENROD COLORS["darkgoldenrod1"] = DARKGOLDENROD1 COLORS["darkgoldenrod2"] = DARKGOLDENROD2 COLORS["darkgoldenrod3"] = DARKGOLDENROD3 COLORS["darkgoldenrod4"] = DARKGOLDENROD4 COLORS["darkgray"] = DARKGRAY COLORS["darkgreen"] = DARKGREEN COLORS["darkkhaki"] = DARKKHAKI COLORS["darkolivegreen"] = DARKOLIVEGREEN COLORS["darkolivegreen1"] = DARKOLIVEGREEN1 COLORS["darkolivegreen2"] = DARKOLIVEGREEN2 COLORS["darkolivegreen3"] = DARKOLIVEGREEN3 COLORS["darkolivegreen4"] = DARKOLIVEGREEN4 COLORS["darkorange"] = DARKORANGE COLORS["darkorange1"] = DARKORANGE1 COLORS["darkorange2"] = DARKORANGE2 COLORS["darkorange3"] = DARKORANGE3 COLORS["darkorange4"] = DARKORANGE4 COLORS["darkorchid"] = DARKORCHID COLORS["darkorchid1"] = DARKORCHID1 COLORS["darkorchid2"] = DARKORCHID2 COLORS["darkorchid3"] = DARKORCHID3 COLORS["darkorchid4"] = DARKORCHID4 COLORS["darksalmon"] = DARKSALMON COLORS["darkseagreen"] = DARKSEAGREEN COLORS["darkseagreen1"] = DARKSEAGREEN1 COLORS["darkseagreen2"] = DARKSEAGREEN2 COLORS["darkseagreen3"] = DARKSEAGREEN3 COLORS["darkseagreen4"] = DARKSEAGREEN4 COLORS["darkslateblue"] = DARKSLATEBLUE COLORS["darkslategray"] = DARKSLATEGRAY COLORS["darkslategray1"] = DARKSLATEGRAY1 COLORS["darkslategray2"] = DARKSLATEGRAY2 COLORS["darkslategray3"] = DARKSLATEGRAY3 COLORS["darkslategray4"] = DARKSLATEGRAY4 COLORS["darkturquoise"] = DARKTURQUOISE COLORS["darkviolet"] = DARKVIOLET COLORS["deeppink"] = DEEPPINK1 COLORS["deeppink1"] = DEEPPINK1 COLORS["deeppink2"] = DEEPPINK2 COLORS["deeppink3"] = DEEPPINK3 COLORS["deeppink4"] = DEEPPINK4 COLORS["deepskyblue"] = DEEPSKYBLUE1 COLORS["deepskyblue1"] = DEEPSKYBLUE1 COLORS["deepskyblue2"] = DEEPSKYBLUE2 COLORS["deepskyblue3"] = DEEPSKYBLUE3 COLORS["deepskyblue4"] = DEEPSKYBLUE4 COLORS["dimgray"] = DIMGRAY COLORS["dodgerblue"] = DODGERBLUE1 COLORS["dodgerblue1"] = DODGERBLUE1 COLORS["dodgerblue2"] = DODGERBLUE2 COLORS["dodgerblue3"] = DODGERBLUE3 COLORS["dodgerblue4"] = DODGERBLUE4 COLORS["eggshell"] = EGGSHELL COLORS["emeraldgreen"] = EMERALDGREEN COLORS["firebrick"] = FIREBRICK COLORS["firebrick1"] = FIREBRICK1 COLORS["firebrick2"] = FIREBRICK2 COLORS["firebrick3"] = FIREBRICK3 COLORS["firebrick4"] = FIREBRICK4 COLORS["flesh"] = FLESH COLORS["floralwhite"] = FLORALWHITE COLORS["forestgreen"] = FORESTGREEN COLORS["gainsboro"] = GAINSBORO COLORS["ghostwhite"] = GHOSTWHITE COLORS["gold"] = GOLD1 COLORS["gold1"] = GOLD1 COLORS["gold2"] = GOLD2 COLORS["gold3"] = GOLD3 COLORS["gold4"] = GOLD4 COLORS["goldenrod"] = GOLDENROD COLORS["goldenrod1"] = GOLDENROD1 COLORS["goldenrod2"] = GOLDENROD2 COLORS["goldenrod3"] = GOLDENROD3 COLORS["goldenrod4"] = GOLDENROD4 COLORS["gray"] = GRAY COLORS["gray1"] = GRAY1 COLORS["gray10"] = GRAY10 COLORS["gray11"] = GRAY11 COLORS["gray12"] = GRAY12 COLORS["gray13"] = GRAY13 COLORS["gray14"] = GRAY14 COLORS["gray15"] = GRAY15 COLORS["gray16"] = GRAY16 COLORS["gray17"] = GRAY17 COLORS["gray18"] = GRAY18 COLORS["gray19"] = GRAY19 COLORS["gray2"] = GRAY2 COLORS["gray20"] = GRAY20 COLORS["gray21"] = GRAY21 COLORS["gray22"] = GRAY22 COLORS["gray23"] = GRAY23 COLORS["gray24"] = GRAY24 COLORS["gray25"] = GRAY25 COLORS["gray26"] = GRAY26 COLORS["gray27"] = GRAY27 COLORS["gray28"] = GRAY28 COLORS["gray29"] = GRAY29 COLORS["gray3"] = GRAY3 COLORS["gray30"] = GRAY30 COLORS["gray31"] = GRAY31 COLORS["gray32"] = GRAY32 COLORS["gray33"] = GRAY33 COLORS["gray34"] = GRAY34 COLORS["gray35"] = GRAY35 COLORS["gray36"] = GRAY36 COLORS["gray37"] = GRAY37 COLORS["gray38"] = GRAY38 COLORS["gray39"] = GRAY39 COLORS["gray4"] = GRAY4 COLORS["gray40"] = GRAY40 COLORS["gray42"] = GRAY42 COLORS["gray43"] = GRAY43 COLORS["gray44"] = GRAY44 COLORS["gray45"] = GRAY45 COLORS["gray46"] = GRAY46 COLORS["gray47"] = GRAY47 COLORS["gray48"] = GRAY48 COLORS["gray49"] = GRAY49 COLORS["gray5"] = GRAY5 COLORS["gray50"] = GRAY50 COLORS["gray51"] = GRAY51 COLORS["gray52"] = GRAY52 COLORS["gray53"] = GRAY53 COLORS["gray54"] = GRAY54 COLORS["gray55"] = GRAY55 COLORS["gray56"] = GRAY56 COLORS["gray57"] = GRAY57 COLORS["gray58"] = GRAY58 COLORS["gray59"] = GRAY59 COLORS["gray6"] = GRAY6 COLORS["gray60"] = GRAY60 COLORS["gray61"] = GRAY61 COLORS["gray62"] = GRAY62 COLORS["gray63"] = GRAY63 COLORS["gray64"] = GRAY64 COLORS["gray65"] = GRAY65 COLORS["gray66"] = GRAY66 COLORS["gray67"] = GRAY67 COLORS["gray68"] = GRAY68 COLORS["gray69"] = GRAY69 COLORS["gray7"] = GRAY7 COLORS["gray70"] = GRAY70 COLORS["gray71"] = GRAY71 COLORS["gray72"] = GRAY72 COLORS["gray73"] = GRAY73 COLORS["gray74"] = GRAY74 COLORS["gray75"] = GRAY75 COLORS["gray76"] = GRAY76 COLORS["gray77"] = GRAY77 COLORS["gray78"] = GRAY78 COLORS["gray79"] = GRAY79 COLORS["gray8"] = GRAY8 COLORS["gray80"] = GRAY80 COLORS["gray81"] = GRAY81 COLORS["gray82"] = GRAY82 COLORS["gray83"] = GRAY83 COLORS["gray84"] = GRAY84 COLORS["gray85"] = GRAY85 COLORS["gray86"] = GRAY86 COLORS["gray87"] = GRAY87 COLORS["gray88"] = GRAY88 COLORS["gray89"] = GRAY89 COLORS["gray9"] = GRAY9 COLORS["gray90"] = GRAY90 COLORS["gray91"] = GRAY91 COLORS["gray92"] = GRAY92 COLORS["gray93"] = GRAY93 COLORS["gray94"] = GRAY94 COLORS["gray95"] = GRAY95 COLORS["gray97"] = GRAY97 COLORS["gray98"] = GRAY98 COLORS["gray99"] = GRAY99 COLORS["green"] = GREEN COLORS["green1"] = GREEN1 COLORS["green2"] = GREEN2 COLORS["green3"] = GREEN3 COLORS["green4"] = GREEN4 COLORS["greenyellow"] = GREENYELLOW COLORS["honeydew"] = HONEYDEW1 COLORS["honeydew1"] = HONEYDEW1 COLORS["honeydew2"] = HONEYDEW2 COLORS["honeydew3"] = HONEYDEW3 COLORS["honeydew4"] = HONEYDEW4 COLORS["hotpink"] = HOTPINK COLORS["hotpink1"] = HOTPINK1 COLORS["hotpink2"] = HOTPINK2 COLORS["hotpink3"] = HOTPINK3 COLORS["hotpink4"] = HOTPINK4 COLORS["indianred"] = INDIANRED COLORS["indianred"] = INDIANRED COLORS["indianred1"] = INDIANRED1 COLORS["indianred2"] = INDIANRED2 COLORS["indianred3"] = INDIANRED3 COLORS["indianred4"] = INDIANRED4 COLORS["indigo"] = INDIGO COLORS["ivory"] = IVORY1 COLORS["ivory1"] = IVORY1 COLORS["ivory2"] = IVORY2 COLORS["ivory3"] = IVORY3 COLORS["ivory4"] = IVORY4 COLORS["ivoryblack"] = IVORYBLACK COLORS["khaki"] = KHAKI COLORS["khaki1"] = KHAKI1 COLORS["khaki2"] = KHAKI2 COLORS["khaki3"] = KHAKI3 COLORS["khaki4"] = KHAKI4 COLORS["lavender"] = LAVENDER COLORS["lavenderblush"] = LAVENDERBLUSH1 COLORS["lavenderblush1"] = LAVENDERBLUSH1 COLORS["lavenderblush2"] = LAVENDERBLUSH2 COLORS["lavenderblush3"] = LAVENDERBLUSH3 COLORS["lavenderblush4"] = LAVENDERBLUSH4 COLORS["lawngreen"] = LAWNGREEN COLORS["lemonchiffon"] = LEMONCHIFFON1 COLORS["lemonchiffon1"] = LEMONCHIFFON1 COLORS["lemonchiffon2"] = LEMONCHIFFON2 COLORS["lemonchiffon3"] = LEMONCHIFFON3 COLORS["lemonchiffon4"] = LEMONCHIFFON4 COLORS["lightblue"] = LIGHTBLUE COLORS["lightblue1"] = LIGHTBLUE1 COLORS["lightblue2"] = LIGHTBLUE2 COLORS["lightblue3"] = LIGHTBLUE3 COLORS["lightblue4"] = LIGHTBLUE4 COLORS["lightcoral"] = LIGHTCORAL COLORS["lightcyan"] = LIGHTCYAN1 COLORS["lightcyan1"] = LIGHTCYAN1 COLORS["lightcyan2"] = LIGHTCYAN2 COLORS["lightcyan3"] = LIGHTCYAN3 COLORS["lightcyan4"] = LIGHTCYAN4 COLORS["lightgoldenrod"] = LIGHTGOLDENROD1 COLORS["lightgoldenrod1"] = LIGHTGOLDENROD1 COLORS["lightgoldenrod2"] = LIGHTGOLDENROD2 COLORS["lightgoldenrod3"] = LIGHTGOLDENROD3 COLORS["lightgoldenrod4"] = LIGHTGOLDENROD4 COLORS["lightgoldenrodyellow"] = LIGHTGOLDENRODYELLOW COLORS["lightgrey"] = LIGHTGREY COLORS["lightpink"] = LIGHTPINK COLORS["lightpink1"] = LIGHTPINK1 COLORS["lightpink2"] = LIGHTPINK2 COLORS["lightpink3"] = LIGHTPINK3 COLORS["lightpink4"] = LIGHTPINK4 COLORS["lightsalmon"] = LIGHTSALMON1 COLORS["lightsalmon1"] = LIGHTSALMON1 COLORS["lightsalmon2"] = LIGHTSALMON2 COLORS["lightsalmon3"] = LIGHTSALMON3 COLORS["lightsalmon4"] = LIGHTSALMON4 COLORS["lightseagreen"] = LIGHTSEAGREEN COLORS["lightskyblue"] = LIGHTSKYBLUE COLORS["lightskyblue1"] = LIGHTSKYBLUE1 COLORS["lightskyblue2"] = LIGHTSKYBLUE2 COLORS["lightskyblue3"] = LIGHTSKYBLUE3 COLORS["lightskyblue4"] = LIGHTSKYBLUE4 COLORS["lightslateblue"] = LIGHTSLATEBLUE COLORS["lightslategray"] = LIGHTSLATEGRAY COLORS["lightsteelblue"] = LIGHTSTEELBLUE COLORS["lightsteelblue1"] = LIGHTSTEELBLUE1 COLORS["lightsteelblue2"] = LIGHTSTEELBLUE2 COLORS["lightsteelblue3"] = LIGHTSTEELBLUE3 COLORS["lightsteelblue4"] = LIGHTSTEELBLUE4 COLORS["lightyellow"] = LIGHTYELLOW1 COLORS["lightyellow1"] = LIGHTYELLOW1 COLORS["lightyellow2"] = LIGHTYELLOW2 COLORS["lightyellow3"] = LIGHTYELLOW3 COLORS["lightyellow4"] = LIGHTYELLOW4 COLORS["limegreen"] = LIMEGREEN COLORS["linen"] = LINEN COLORS["magenta"] = MAGENTA COLORS["magenta1"] = MAGENTA2 COLORS["magenta2"] = MAGENTA2 COLORS["magenta3"] = MAGENTA3 COLORS["magenta4"] = MAGENTA4 COLORS["manganeseblue"] = MANGANESEBLUE COLORS["maroon"] = MAROON COLORS["maroon1"] = MAROON1 COLORS["maroon2"] = MAROON2 COLORS["maroon3"] = MAROON3 COLORS["maroon4"] = MAROON4 COLORS["mediumorchid"] = MEDIUMORCHID COLORS["mediumorchid1"] = MEDIUMORCHID1 COLORS["mediumorchid2"] = MEDIUMORCHID2 COLORS["mediumorchid3"] = MEDIUMORCHID3 COLORS["mediumorchid4"] = MEDIUMORCHID4 COLORS["mediumpurple"] = MEDIUMPURPLE COLORS["mediumpurple1"] = MEDIUMPURPLE1 COLORS["mediumpurple2"] = MEDIUMPURPLE2 COLORS["mediumpurple3"] = MEDIUMPURPLE3 COLORS["mediumpurple4"] = MEDIUMPURPLE4 COLORS["mediumseagreen"] = MEDIUMSEAGREEN COLORS["mediumslateblue"] = MEDIUMSLATEBLUE COLORS["mediumspringgreen"] = MEDIUMSPRINGGREEN COLORS["mediumturquoise"] = MEDIUMTURQUOISE COLORS["mediumvioletred"] = MEDIUMVIOLETRED COLORS["melon"] = MELON COLORS["midnightblue"] = MIDNIGHTBLUE COLORS["mint"] = MINT COLORS["mintcream"] = MINTCREAM COLORS["mistyrose"] = MISTYROSE1 COLORS["mistyrose1"] = MISTYROSE1 COLORS["mistyrose2"] = MISTYROSE2 COLORS["mistyrose3"] = MISTYROSE3 COLORS["mistyrose4"] = MISTYROSE4 COLORS["moccasin"] = MOCCASIN COLORS["navajowhite"] = NAVAJOWHITE1 COLORS["navajowhite1"] = NAVAJOWHITE1 COLORS["navajowhite2"] = NAVAJOWHITE2 COLORS["navajowhite3"] = NAVAJOWHITE3 COLORS["navajowhite4"] = NAVAJOWHITE4 COLORS["navy"] = NAVY COLORS["oldlace"] = OLDLACE COLORS["olive"] = OLIVE COLORS["olivedrab"] = OLIVEDRAB COLORS["olivedrab1"] = OLIVEDRAB1 COLORS["olivedrab2"] = OLIVEDRAB2 COLORS["olivedrab3"] = OLIVEDRAB3 COLORS["olivedrab4"] = OLIVEDRAB4 COLORS["orange"] = ORANGE COLORS["orange1"] = ORANGE1 COLORS["orange2"] = ORANGE2 COLORS["orange3"] = ORANGE3 COLORS["orange4"] = ORANGE4 COLORS["orangered"] = ORANGERED1 COLORS["orangered1"] = ORANGERED1 COLORS["orangered2"] = ORANGERED2 COLORS["orangered3"] = ORANGERED3 COLORS["orangered4"] = ORANGERED4 COLORS["orchid"] = ORCHID COLORS["orchid1"] = ORCHID1 COLORS["orchid2"] = ORCHID2 COLORS["orchid3"] = ORCHID3 COLORS["orchid4"] = ORCHID4 COLORS["palegoldenrod"] = PALEGOLDENROD COLORS["palegreen"] = PALEGREEN COLORS["palegreen1"] = PALEGREEN1 COLORS["palegreen2"] = PALEGREEN2 COLORS["palegreen3"] = PALEGREEN3 COLORS["palegreen4"] = PALEGREEN4 COLORS["paleturquoise1"] = PALETURQUOISE1 COLORS["paleturquoise2"] = PALETURQUOISE2 COLORS["paleturquoise3"] = PALETURQUOISE3 COLORS["paleturquoise4"] = PALETURQUOISE4 COLORS["palevioletred"] = PALEVIOLETRED COLORS["palevioletred1"] = PALEVIOLETRED1 COLORS["palevioletred2"] = PALEVIOLETRED2 COLORS["palevioletred3"] = PALEVIOLETRED3 COLORS["palevioletred4"] = PALEVIOLETRED4 COLORS["papayawhip"] = PAPAYAWHIP COLORS["peachpuff1"] = PEACHPUFF1 COLORS["peachpuff2"] = PEACHPUFF2 COLORS["peachpuff3"] = PEACHPUFF3 COLORS["peachpuff4"] = PEACHPUFF4 COLORS["peacock"] = PEACOCK COLORS["pink"] = PINK COLORS["pink1"] = PINK1 COLORS["pink2"] = PINK2 COLORS["pink3"] = PINK3 COLORS["pink4"] = PINK4 COLORS["plum"] = PLUM COLORS["plum1"] = PLUM1 COLORS["plum2"] = PLUM2 COLORS["plum3"] = PLUM3 COLORS["plum4"] = PLUM4 COLORS["powderblue"] = POWDERBLUE COLORS["purple"] = PURPLE COLORS["purple1"] = PURPLE1 COLORS["purple2"] = PURPLE2 COLORS["purple3"] = PURPLE3 COLORS["purple4"] = PURPLE4 COLORS["raspberry"] = RASPBERRY COLORS["rawsienna"] = RAWSIENNA COLORS["red"] = RED COLORS["red1"] = RED1 COLORS["red2"] = RED2 COLORS["red3"] = RED3 COLORS["red4"] = RED4 COLORS["rosybrown"] = ROSYBROWN COLORS["rosybrown1"] = ROSYBROWN1 COLORS["rosybrown2"] = ROSYBROWN2 COLORS["rosybrown3"] = ROSYBROWN3 COLORS["rosybrown4"] = ROSYBROWN4 COLORS["royalblue"] = ROYALBLUE COLORS["royalblue1"] = ROYALBLUE1 COLORS["royalblue2"] = ROYALBLUE2 COLORS["royalblue3"] = ROYALBLUE3 COLORS["royalblue4"] = ROYALBLUE4 COLORS["salmon"] = SALMON COLORS["salmon1"] = SALMON1 COLORS["salmon2"] = SALMON2 COLORS["salmon3"] = SALMON3 COLORS["salmon4"] = SALMON4 COLORS["sandybrown"] = SANDYBROWN COLORS["sapgreen"] = SAPGREEN COLORS["seagreen1"] = SEAGREEN1 COLORS["seagreen2"] = SEAGREEN2 COLORS["seagreen3"] = SEAGREEN3 COLORS["seagreen4"] = SEAGREEN4 COLORS["seashell1"] = SEASHELL1 COLORS["seashell2"] = SEASHELL2 COLORS["seashell3"] = SEASHELL3 COLORS["seashell4"] = SEASHELL4 COLORS["sepia"] = SEPIA COLORS["sgibeet"] = SGIBEET COLORS["sgibrightgray"] = SGIBRIGHTGRAY COLORS["sgichartreuse"] = SGICHARTREUSE COLORS["sgidarkgray"] = SGIDARKGRAY COLORS["sgigray12"] = SGIGRAY12 COLORS["sgigray16"] = SGIGRAY16 COLORS["sgigray32"] = SGIGRAY32 COLORS["sgigray36"] = SGIGRAY36 COLORS["sgigray52"] = SGIGRAY52 COLORS["sgigray56"] = SGIGRAY56 COLORS["sgigray72"] = SGIGRAY72 COLORS["sgigray76"] = SGIGRAY76 COLORS["sgigray92"] = SGIGRAY92 COLORS["sgigray96"] = SGIGRAY96 COLORS["sgilightblue"] = SGILIGHTBLUE COLORS["sgilightgray"] = SGILIGHTGRAY COLORS["sgiolivedrab"] = SGIOLIVEDRAB COLORS["sgisalmon"] = SGISALMON COLORS["sgislateblue"] = SGISLATEBLUE COLORS["sgiteal"] = SGITEAL COLORS["sienna"] = SIENNA COLORS["sienna1"] = SIENNA1 COLORS["sienna2"] = SIENNA2 COLORS["sienna3"] = SIENNA3 COLORS["sienna4"] = SIENNA4 COLORS["silver"] = SILVER COLORS["skyblue"] = SKYBLUE COLORS["skyblue1"] = SKYBLUE1 COLORS["skyblue2"] = SKYBLUE2 COLORS["skyblue3"] = SKYBLUE3 COLORS["skyblue4"] = SKYBLUE4 COLORS["slateblue"] = SLATEBLUE COLORS["slateblue1"] = SLATEBLUE1 COLORS["slateblue2"] = SLATEBLUE2 COLORS["slateblue3"] = SLATEBLUE3 COLORS["slateblue4"] = SLATEBLUE4 COLORS["slategray"] = SLATEGRAY COLORS["slategray1"] = SLATEGRAY1 COLORS["slategray2"] = SLATEGRAY2 COLORS["slategray3"] = SLATEGRAY3 COLORS["slategray4"] = SLATEGRAY4 COLORS["snow"] = SNOW1 COLORS["snow1"] = SNOW1 COLORS["snow2"] = SNOW2 COLORS["snow3"] = SNOW3 COLORS["snow4"] = SNOW4 COLORS["springgreen"] = SPRINGGREEN COLORS["springgreen1"] = SPRINGGREEN1 COLORS["springgreen2"] = SPRINGGREEN2 COLORS["springgreen3"] = SPRINGGREEN3 COLORS["steelblue"] = STEELBLUE COLORS["steelblue1"] = STEELBLUE1 COLORS["steelblue2"] = STEELBLUE2 COLORS["steelblue3"] = STEELBLUE3 COLORS["steelblue4"] = STEELBLUE4 COLORS["tan"] = TAN COLORS["tan1"] = TAN1 COLORS["tan2"] = TAN2 COLORS["tan3"] = TAN3 COLORS["tan4"] = TAN4 COLORS["teal"] = TEAL COLORS["thistle"] = THISTLE COLORS["thistle1"] = THISTLE1 COLORS["thistle2"] = THISTLE2 COLORS["thistle3"] = THISTLE3 COLORS["thistle4"] = THISTLE4 COLORS["tomato"] = TOMATO1 COLORS["tomato1"] = TOMATO1 COLORS["tomato2"] = TOMATO2 COLORS["tomato3"] = TOMATO3 COLORS["tomato4"] = TOMATO4 COLORS["turquoise"] = TURQUOISE COLORS["turquoise1"] = TURQUOISE1 COLORS["turquoise2"] = TURQUOISE2 COLORS["turquoise3"] = TURQUOISE3 COLORS["turquoise4"] = TURQUOISE4 COLORS["turquoiseblue"] = TURQUOISEBLUE COLORS["violet"] = VIOLET COLORS["violetred"] = VIOLETRED COLORS["violetred1"] = VIOLETRED1 COLORS["violetred2"] = VIOLETRED2 COLORS["violetred3"] = VIOLETRED3 COLORS["violetred4"] = VIOLETRED4 COLORS["warmgrey"] = WARMGREY COLORS["wheat"] = WHEAT COLORS["wheat1"] = WHEAT1 COLORS["wheat2"] = WHEAT2 COLORS["wheat3"] = WHEAT3 COLORS["wheat4"] = WHEAT4 COLORS["white"] = WHITE COLORS["whitesmoke"] = WHITESMOKE COLORS["whitesmoke"] = WHITESMOKE COLORS["yellow"] = YELLOW COLORS["yellow1"] = YELLOW1 COLORS["yellow2"] = YELLOW2 COLORS["yellow3"] = YELLOW3 COLORS["yellow4"] = YELLOW4 def parse_color(value): if value is not None: # is it a Color object? if isinstance(value, Color): return value # is the color a string elif isinstance(value, str): # strip the color of white space value = value.strip() # if it starts with a # check it is a valid color if value[0] == "#": # check its format if len(value) != 7 and len(value) != 9: raise ValueError("{} is not a valid # color, it must be in the format #rrggbb or #rrggbbaa".format(value)) else: # add the alpha if required if len(value) == 7: value = value + "ff" hex_values = (value[1:3], value[3:5], value[5:7], value[7:9]) # check hex values are between 00 and ff int_values = [] for hex_color in hex_values: try: int_color = int(hex_color, 16) int_values.append(int_color) except: raise ValueError("{} is not a valid value, it must be hex 00 - ff".format(hex_color)) if not (0 <= int_color <= 255): raise ValueError("{} is not a valid color value, it must be 00 - ff".format(hex_color)) return Color(red=int_values[0], green=int_values[1], blue=int_values[2], alpha=int_values[3]) else: # does the color exist in the dictionary of colors if value.lower() in COLORS: return COLORS[value.lower()] else: raise ValueError("'{}' is not a valid color value.") # if the color is not a string or a Color object, maybe its a list or tuple - try and convert it else: # get the number of colors and check it is iterable try: no_of_colors = len(value) except: raise ValueError("A color must be a Color object, string or list of values (red, green, blue) or (alpha, red, green, blue)") if not (3 <= no_of_colors <= 4): raise ValueError("A color must contain 3 or 4 values (red, green, blue) or (red, green, blue, alpha)") # check the color values are between 0 and 255 for c in value: if not (0 <= c <= 255): raise ValueError("{} is not a valid color value, it must be 0 - 255") if no_of_colors == 3: return Color(red=value[0], green=value[1], blue=value[2]) else: return Color(red=value[0], green=value[1], blue=value[2], alpha=value[3]) ================================================ FILE: bluedot/constants.py ================================================ PROTOCOL_VERSION = 2 CHECK_PROTOCOL_TIMEOUT = 2 ================================================ FILE: bluedot/dot.py ================================================ from __future__ import division import sys import warnings from time import sleep, time from threading import Event from inspect import getfullargspec from .btcomm import BluetoothServer from .threads import WrapThread from .constants import PROTOCOL_VERSION, CHECK_PROTOCOL_TIMEOUT from .interactions import BlueDotInteraction, BlueDotPosition, BlueDotRotation, BlueDotSwipe from .colors import parse_color, BLUE from .exceptions import ButtonDoesNotExist class Dot: """ The internal base class for the implementation of a "button" or "buttons". """ def __init__(self, color, square, border, visible): self._color = color self._square = square self._border = border self._visible = visible self._is_pressed_event = Event() self._is_released_event = Event() self._is_moved_event = Event() self._is_swiped_event = Event() self._is_double_pressed_event = Event() self._when_pressed = None self._when_pressed_background = False self._when_double_pressed = None self._when_double_pressed_background = False self._when_released = None self._when_released_background = False self._when_moved = None self._when_moved_background = False self._when_swiped = None self._when_swiped_background = False self._when_rotated = None self._when_rotated_background = False self._is_pressed = False self._position = None self._double_press_time = 0.3 self._rotation_segments = 8 @property def is_pressed(self): """ Returns ``True`` if the button is pressed (or held). """ return self._is_pressed @property def value(self): """ Returns a 1 if ``.is_pressed``, 0 if not. """ return 1 if self.is_pressed else 0 @property def values(self): """ Returns an infinite generator constantly yielding the current value. """ while True: yield self.value @property def position(self): """ Returns an instance of :class:`BlueDotPosition` representing the current or last position the button was pressed, held or released. .. note:: If the button is released (and inactive), :attr:`position` will return the position where it was released, until it is pressed again. If the button has never been pressed :attr:`position` will return ``None``. """ return self._position @property def when_pressed(self): """ Sets or returns the function which is called when the button is pressed. The function should accept 0 or 1 parameters, if the function accepts 1 parameter an instance of :class:`BlueDotPosition` will be returned representing where the button was pressed. The following example will print a message to the screen when the button is pressed:: from bluedot import BlueDot def dot_was_pressed(): print("The button was pressed") bd = BlueDot() bd.when_pressed = dot_was_pressed This example shows how the position of where the button was pressed can be obtained:: from bluedot import BlueDot def dot_was_pressed(pos): print("The button was pressed at pos x={} y={}".format(pos.x, pos.y)) bd = BlueDot() bd.when_pressed = dot_was_pressed The function will be run in the same thread and block, to run in a separate thread use `set_when_pressed(function, background=True)` """ return self._when_pressed @when_pressed.setter def when_pressed(self, value): self.set_when_pressed(value) def set_when_pressed(self, callback, background=False): """ Sets the function which is called when the button is pressed. :param Callable callback: The function to call, setting to `None` will stop the callback. :param bool background: If set to `True` the function will be run in a separate thread and it will return immediately. The default is `False`. """ self._when_pressed = callback self._when_pressed_background = background @property def when_double_pressed(self): """ Sets or returns the function which is called when the button is double pressed. The function should accept 0 or 1 parameters, if the function accepts 1 parameter an instance of :class:`BlueDotPosition` will be returned representing where the button was pressed the second time. The function will be run in the same thread and block, to run in a separate thread use `set_when_double_pressed(function, background=True)` .. note:: The double press event is fired before the 2nd press event e.g. events would be appear in the order, pressed, released, double pressed, pressed. """ return self._when_double_pressed @when_double_pressed.setter def when_double_pressed(self, value): self.set_when_double_pressed(value) def set_when_double_pressed(self, callback, background=False): """ Sets the function which is called when the button is double pressed. :param Callable callback: The function to call, setting to `None` will stop the callback. :param bool background: If set to `True` the function will be run in a separate thread and it will return immediately. The default is `False`. """ self._when_double_pressed = callback self._when_double_pressed_background = background @property def double_press_time(self): """ Sets or returns the time threshold in seconds for a double press. Defaults to 0.3. """ return self._double_press_time @double_press_time.setter def double_press_time(self, value): self._double_press_time = value @property def when_released(self): """ Sets or returns the function which is called when the button is released. The function should accept 0 or 1 parameters, if the function accepts 1 parameter an instance of :class:`BlueDotPosition` will be returned representing where the button was held when it was released. The function will be run in the same thread and block, to run in a separate thread use `set_when_released(function, background=True)` """ return self._when_released @when_released.setter def when_released(self, value): self.set_when_released(value) def set_when_released(self, callback, background=False): """ Sets the function which is called when the button is released. :param Callable callback: The function to call, setting to `None` will stop the callback. :param bool background: If set to `True` the function will be run in a separate thread and it will return immediately. The default is `False`. """ self._when_released = callback self._when_released_background = background @property def when_moved(self): """ Sets or returns the function which is called when the position the button is pressed is moved. The function should accept 0 or 1 parameters, if the function accepts 1 parameter an instance of :class:`BlueDotPosition` will be returned representing the new position of where the Blue Dot is held. The function will be run in the same thread and block, to run in a separate thread use `set_when_moved(function, background=True)` """ return self._when_moved @when_moved.setter def when_moved(self, value): self.set_when_moved(value) def set_when_moved(self, callback, background=False): """ Sets the function which is called when the position the button is pressed is moved. :param Callable callback: The function to call, setting to `None` will stop the callback. :param bool background: If set to `True` the function will be run in a separate thread and it will return immediately. The default is `False`. """ self._when_moved = callback self._when_moved_background = background @property def when_swiped(self): """ Sets or returns the function which is called when the button is swiped. The function should accept 0 or 1 parameters, if the function accepts 1 parameter an instance of :class:`BlueDotSwipe` will be returned representing the how the button was swiped. The function will be run in the same thread and block, to run in a separate thread use `set_when_swiped(function, background=True)` """ return self._when_swiped @when_swiped.setter def when_swiped(self, value): self.set_when_swiped(value) def set_when_swiped(self, callback, background=False): """ Sets the function which is called when the position the button is swiped. :param Callable callback: The function to call, setting to `None` will stop the callback. :param bool background: If set to `True` the function will be run in a separate thread and it will return immediately. The default is `False`. """ self._when_swiped = callback self._when_swiped_background = background @property def rotation_segments(self): """ Sets or returns the number of virtual segments the button is split into for rotating. Defaults to 8. """ return self._rotation_segments @rotation_segments.setter def rotation_segments(self, value): self._rotation_segments = value @property def when_rotated(self): """ Sets or returns the function which is called when the button is rotated (like an iPod clock wheel). The function should accept 0 or 1 parameters, if the function accepts 1 parameter an instance of :class:`BlueDotRotation` will be returned representing how the button was rotated. The function will be run in the same thread and block, to run in a separate thread use `set_when_rotated(function, background=True)` """ return self._when_rotated @when_rotated.setter def when_rotated(self, value): self.set_when_rotated(value) def set_when_rotated(self, callback, background=False): """ Sets the function which is called when the position the button is rotated (like an iPod clock wheel). :param Callable callback: The function to call, setting to `None` will stop the callback. :param bool background: If set to `True` the function will be run in a separate thread and it will return immediately. The default is `False`. """ self._when_rotated = callback self._when_rotated_background = background @property def color(self): """ Sets or returns the color of the dot. Defaults to BLUE. An instance of :class:`.colors.Color` is returned. Value can be set as a :class:`.colors.Color` object, a hex color value in the format `#rrggbb` or `#rrggbbaa`, a tuple of `(red, green, blue)` or `(red, green, blue, alpha)` values between `0` & `255` or a text description of the color, e.g. "red". A dictionary of available colors can be obtained from `bluedot.COLORS`. """ return self._color @color.setter def color(self, value): self._color = parse_color(value) @property def square(self): """ When set to `True` the 'dot' is made square. Default is `False`. """ return self._square @square.setter def square(self, value): self._square = value @property def border(self): """ When set to `True` adds a border to the dot. Default is `False`. """ return self._border @border.setter def border(self, value): self._border = value @property def visible(self): """ When set to `False` the dot will be hidden. Default is `True`. .. note:: Events (press, release, moved) are still sent from the dot when it is not visible. """ return self._visible @visible.setter def visible(self, value): self._visible = value def wait_for_press(self, timeout = None): """ Waits until a Blue Dot is pressed. Returns ``True`` if the button was pressed. :param float timeout: Number of seconds to wait for a Blue Dot to be pressed, if ``None`` (the default), it will wait indefinetly. """ return self._is_pressed_event.wait(timeout) def wait_for_double_press(self, timeout = None): """ Waits until a Blue Dot is double pressed. Returns ``True`` if the button was double pressed. :param float timeout: Number of seconds to wait for a Blue Dot to be double pressed, if ``None`` (the default), it will wait indefinetly. """ return self._is_double_pressed_event.wait(timeout) def wait_for_release(self, timeout = None): """ Waits until a Blue Dot is released. Returns ``True`` if the button was released. :param float timeout: Number of seconds to wait for a Blue Dot to be released, if ``None`` (the default), it will wait indefinetly. """ return self._is_released_event.wait(timeout) def wait_for_move(self, timeout = None): """ Waits until the position where the button is pressed is moved. Returns ``True`` if the position pressed on the button was moved. :param float timeout: Number of seconds to wait for the position that the button is pressed to move, if ``None`` (the default), it will wait indefinetly. """ return self._is_moved_event.wait(timeout) def wait_for_swipe(self, timeout = None): """ Waits until the button is swiped. Returns ``True`` if the button was swiped. :param float timeout: Number of seconds to wait for the button to be swiped, if ``None`` (the default), it will wait indefinetly. """ return self._is_swiped_event.wait(timeout) def press(self, position): """ Processes any "pressed" events associated with this dot. :param BlueDotPosition position: The BlueDotPosition where the dot was pressed. """ self._position = position self._is_pressed = True self._is_pressed_event.set() self._is_pressed_event.clear() self._process_callback(self.when_pressed, position, self._when_pressed_background) def release(self, position): """ Processes any "released" events associated with this dot. :param BlueDotPosition position: The BlueDotPosition where the Dot was pressed. """ self._position = position self._is_pressed = False self._is_released_event.set() self._is_released_event.clear() self._process_callback(self.when_released, position, self._when_released_background) def move(self, position): """ Processes any "released" events associated with this dot. :param BlueDotPosition position: The BlueDotPosition where the Dot was pressed. """ self._is_moved_event.set() self._is_moved_event.clear() self._process_callback(self.when_moved, position, self._when_moved_background) def double_press(self, position): """ Processes any "double press" events associated with this dot. :param BlueDotPosition position: The BlueDotPosition where the Dot was pressed. """ self._is_double_pressed_event.set() self._is_double_pressed_event.clear() self._process_callback(self.when_double_pressed, position, self._when_double_pressed_background) def swipe(self, swipe): """ Processes any "swipe" events associated with this dot. :param BlueDotSwipe swipe: The BlueDotSwipe representing how the dot was swiped. """ self._is_swiped_event.set() self._is_swiped_event.clear() self._process_callback(self.when_swiped, swipe, self._when_swiped_background) def rotate(self, rotation): """ Processes any "rotation" events associated with this dot. :param BlueDotRotation rotation: The BlueDotRotation representing how the dot was rotated. """ # print("rotating - when_rotated {}") self._process_callback(self.when_rotated, rotation, self._when_rotated_background) def _process_callback(self, callback, arg, background): if callback: args_expected = getfullargspec(callback).args no_args_expected = len(args_expected) if len(args_expected) > 0: # if someone names the first arg of a class function to something # other than self, this will fail! or if they name the first argument # of a non class function to self this will fail! if args_expected[0] == "self": no_args_expected -= 1 if no_args_expected == 0: call_back_t = WrapThread(target=callback) else: call_back_t = WrapThread(target=callback, args=(arg, )) call_back_t.start() # if this callback is not running in the background wait for it if not background: call_back_t.join() class BlueDotButton(Dot): """ Represents a single button on the button client applications. It keeps tracks of when and where the button has been pressed and processes any events. This class is intended for use via :class:`BlueDot` and should not be instantiated "manually". A button can be interacted with individually via :class:`BlueDot` by stating its position in the grid e.g. :: from bluedot import BlueDot bd = BlueDot() first_button = bd[0,0].wait_for_press first_button.wait_for_press() print("The first button was pressed") :param BlueDot bd: The BlueDot object this button belongs too. :param int col: The column position for this button in the grid. :param int col: The row position for this button in the grid. :param string color The color of the button. Can be set as a :class:`.colors.Color` object, a hex color value in the format `#rrggbb` or `#rrggbbaa`, a tuple of `(red, green, blue)` or `(red, green, blue, alpha)` values between `0` & `255` or a text description of the color, e.g. "red". A dictionary of available colors can be obtained from `bluedot.COLORS`. :param bool square: When set to `True` the button is made square. :param bool border: When set to `True` adds a border to the button. :param bool visible: When set to `False` the button will be hidden. """ def __init__(self, bd, col, row, color, square, border, visible): self._bd = bd self.col = col self.row = row self._interaction = None # setup the "dot" super().__init__(color, square, border, visible) @property def color(self): return super(BlueDotButton, self.__class__).color.fget(self) @color.setter def color(self, value): super(BlueDotButton, self.__class__).color.fset(self, value) self._send_config() @property def square(self): return super(BlueDotButton, self.__class__).square.fget(self) @square.setter def square(self, value): super(BlueDotButton, self.__class__).square.fset(self, value) self._send_config() @property def border(self): return super(BlueDotButton, self.__class__).border.fget(self) @border.setter def border(self, value): super(BlueDotButton, self.__class__).border.fset(self, value) self._send_config() @property def visible(self): return super(BlueDotButton, self.__class__).visible.fget(self) @visible.setter def visible(self, value): super(BlueDotButton, self.__class__).visible.fset(self, value) self._send_config() @property def modified(self): """ Returns `True` if the button's appearance has been modified [is different] from the default. """ return not ( self.color == self._bd.color and self.visible == self._bd.visible and self.border == self._bd.border and self.square == self._bd.square ) @property def interaction(self): """ Returns an instance of :class:`BlueDotInteraction` representing the current or last interaction with the button. .. note:: If the button is released (and inactive), :attr:`interaction` will return the interaction when it was released, until it is pressed again. If the button has never been pressed :attr:`interaction` will return ``None``. """ return self._interaction def press(self, position): """ Processes any "pressed" events associated with this button. :param BlueDotPosition position: The BlueDotPosition where the dot was pressed. """ super().press(position) # create new interaction self._interaction = BlueDotInteraction(position) def release(self, position): """ Processes any "released" events associated with this button. :param BlueDotPosition position: The BlueDotPosition where the Dot was pressed. """ super().release(position) self._interaction.released(position) def move(self, position): """ Processes any "released" events associated with this button. :param BlueDotPosition position: The BlueDotPosition where the Dot was pressed. """ super().move(position) self._interaction.moved(position) def is_double_press(self, position): """ Returns True if the position passed represents a double press. i.e. The last interaction was the button was to release it, and the time to press is less than the double_press_time. :param BlueDotPosition position: The BlueDotPosition where the Dot was pressed. """ double_press = False #was there a previous interaction if self._interaction: # was the previous interaction complete (i.e. had it been released) if not self._interaction.active: # was it less than the time threshold (0.3 seconds) if self._interaction.duration < self._double_press_time: #was the dot pressed again in less than the threshold if time() - self._interaction.released_position.time < self._double_press_time: double_press = True return double_press def get_swipe(self): """ Returns an instance of :class:`BlueDotSwipe` if the last interaction with the button was a swipe. Returns `None` if the button was not swiped. """ swipe = BlueDotSwipe(self.interaction) if swipe.valid: return swipe def get_rotation(self): """ Returns an instance of :class:`BlueDotRotation` if the last interaction with the button was a rotation. Returns `None` if the button was not rotated. """ # only bother checking to see if its a rotation if `when_rotated` # as been set. Performance thang! if self.when_rotated or self._bd.when_rotated: rotation = BlueDotRotation(self._interaction, self._rotation_segments) if rotation.valid: return rotation def _build_config_msg(self): return "5,{},{},{},{},{},{}\n".format( self.color, int(self.square), int(self.border), int(self.visible), self.col, self.row ) def _send_config(self): if self._bd.is_connected: self._bd._server.send(self._build_config_msg()) class BlueDot(Dot): """ Interacts with a Blue Dot client application, communicating when and where a button has been pressed, released or held. This class starts an instance of :class:`.btcomm.BluetoothServer` which manages the connection with the Blue Dot client. This class is intended for use with a Blue Dot client application. The following example will print a message when the Blue Dot button is pressed:: from bluedot import BlueDot bd = BlueDot() bd.wait_for_press() print("The button was pressed") Multiple buttons can be created, by changing the number of columns and rows. Each button can be referenced using its [col, row]:: bd = BlueDot(cols=2, rows=2) bd[0,0].wait_for_press() print("Top left button pressed") bd[1,1].wait_for_press() print("Bottom right button pressed") :param str device: The Bluetooth device the server should use, the default is "hci0", if your device only has 1 Bluetooth adapter this shouldn't need to be changed. :param int port: The Bluetooth port the server should use, the default is 1, and under normal use this should never need to change. :param bool auto_start_server: If ``True`` (the default), the Bluetooth server will be automatically started on initialisation; if ``False``, the method :meth:`start` will need to be called before connections will be accepted. :param bool power_up_device: If ``True``, the Bluetooth device will be powered up (if required) when the server starts. The default is ``False``. Depending on how Bluetooth has been powered down, you may need to use :command:`rfkill` to unblock Bluetooth to give permission to bluez to power on Bluetooth:: sudo rfkill unblock bluetooth :param bool print_messages: If ``True`` (the default), server status messages will be printed stating when the server has started and when clients connect / disconnect. :param int cols: The number of columns in the grid of buttons. Defaults to ``1``. :param int rows: The number of rows in the grid of buttons. Defaults to ``1``. """ def __init__(self, device = "hci0", port = 1, auto_start_server = True, power_up_device = False, print_messages = True, cols = 1, rows = 1): self._data_buffer = "" self._device = device self._port = port self._power_up_device = power_up_device self._print_messages = print_messages self._check_protocol_event = Event() self._is_connected_event = Event() self._when_client_connects = None self._when_client_connects_background = False self._when_client_disconnects = None self._when_client_disconnects_background = False # setup the main "dot" super().__init__(BLUE, False, False, True) # setup the grid self._buttons = {} self.resize(cols, rows) self._create_server() if auto_start_server: self.start() @property def buttons(self): """ A list of :class:`BlueDotButton` objects in the "grid". """ return self._buttons.values() @property def cols(self): """ Sets or returns the number of columns in the grid of buttons. """ return self._cols @cols.setter def cols(self, value): self.resize(value, self._rows) @property def rows(self): """ Sets or returns the number of rows in the grid of buttons. """ return self._rows @rows.setter def rows(self, value): self.resize(self._cols, value) @property def device(self): """ The Bluetooth device the server is using. This defaults to "hci0". """ return self._device @property def port(self): """ The port the server is using. This defaults to 1. """ return self._port @property def server(self): """ The :class:`.btcomm.BluetoothServer` instance that is being used to communicate with clients. """ return self._server @property def adapter(self): """ The :class:`.btcomm.BluetoothAdapter` instance that is being used. """ return self._server.adapter @property def paired_devices(self): """ Returns a sequence of devices paired with this adapter :code:`[(mac_address, name), (mac_address, name), ...]`:: bd = BlueDot() devices = bd.paired_devices for d in devices: device_address = d[0] device_name = d[1] """ return self._server.adapter.paired_devices @property def print_messages(self): """ When set to ``True`` messages relating to the status of the Bluetooth server will be printed. """ return self._print_messages @print_messages.setter def print_messages(self, value): self._print_messages = value @property def running(self): """ Returns a ``True`` if the server is running. """ return self._server.running @property def is_connected(self): """ Returns ``True`` if a Blue Dot client is connected. """ return self._is_connected_event.is_set() @property def is_pressed(self): """ Returns ``True`` if the button is pressed (or held). .. note:: If there are multiple buttons, if any button is pressed, `True` will be returned. """ for button in self.buttons: if button._is_pressed: return True return False @property def interaction(self): """ Returns an instance of :class:`BlueDotInteraction` representing the current or last interaction with the Blue Dot. .. note:: If the Blue Dot is released (and inactive), :attr:`interaction` will return the interaction when it was released, until it is pressed again. If the Blue Dot has never been pressed :attr:`interaction` will return ``None``. If there are multiple buttons, the interaction will only be returned for button [0,0] .. deprecated:: 2.0.0 """ return self._get_button((0,0)).interaction @property def rotation_segments(self): """ Sets or returns the number of virtual segments the button is split into for rotating. Defaults to 8. .. note:: If there are multiple buttons in the grid, the 'default' value will be returned and when set all buttons will be updated. """ return super(BlueDot, self.__class__).rotation_segments.fget(self) @rotation_segments.setter def rotation_segments(self, value): super(BlueDot, self.__class__).rotation_segments.fset(self, value) for button in self.buttons: button.rotation_segments = value @property def double_press_time(self): """ Sets or returns the time threshold in seconds for a double press. Defaults to 0.3. .. note:: If there are multiple buttons in the grid, the 'default' value will be returned and when set all buttons will be updated. """ return super(BlueDot, self.__class__).double_press_time.fget(self) @double_press_time.setter def double_press_time(self, value): super(BlueDot, self.__class__).double_press_time.fset(self, value) for button in self.buttons: button.double_press_time = value @property def color(self): """ Sets or returns the color of the button. Defaults to BLUE. An instance of :class:`.colors.Color` is returned. Value can be set as a :class:`.colors.Color` object, a hex color value in the format `#rrggbb` or `#rrggbbaa`, a tuple of `(red, green, blue)` or `(red, green, blue, alpha)` values between `0` & `255` or a text description of the color, e.g. "red". A dictionary of available colors can be obtained from `bluedot.COLORS`. .. note:: If there are multiple buttons in the grid, the 'default' value will be returned and when set all buttons will be updated. """ return super(BlueDot, self.__class__).color.fget(self) @color.setter def color(self, value): super(BlueDot, self.__class__).color.fset(self, value) for button in self.buttons: button.color = value @property def square(self): """ When set to `True` the 'dot' is made square. Default is `False`. .. note:: If there are multiple buttons in the grid, the 'default' value will be returned and when set all buttons will be updated. """ return super(BlueDot, self.__class__).square.fget(self) @square.setter def square(self, value): super(BlueDot, self.__class__).square.fset(self, value) for button in self.buttons: button.square = value @property def border(self): """ When set to `True` adds a border to the dot. Default is `False`. .. note:: If there are multiple buttons in the grid, the 'default' value will be returned and when set all buttons will be updated. """ return super(BlueDot, self.__class__).border.fget(self) @border.setter def border(self, value): super(BlueDot, self.__class__).border.fset(self, value) for button in self.buttons: button.border = value @property def visible(self): """ When set to `False` the dot will be hidden. Default is `True`. .. note:: Events (press, release, moved) are still sent from the dot when it is not visible. If there are multiple buttons in the grid, the 'default' value will be returned and when set all buttons will be updated. """ return super(BlueDot, self.__class__).visible.fget(self) @visible.setter def visible(self, value): super(BlueDot, self.__class__).visible.fset(self, value) for button in self.buttons: button.visible = value @property def when_client_connects(self): """ Sets or returns the function which is called when a Blue Dot application connects. The function will be run in the same thread and block, to run in a separate thread use `set_when_client_connects(function, background=True)` """ return self._when_client_connects @when_client_connects.setter def when_client_connects(self, value): self.set_when_client_connects(value) def set_when_client_connects(self, callback, background=False): """ Sets the function which is called when a Blue Dot connects. :param Callable callback: The function to call, setting to `None` will stop the callback. :param bool background: If set to `True` the function will be run in a separate thread and it will return immediately. The default is `False`. """ self._when_client_connects = callback self._when_client_connects_background = background @property def when_client_disconnects(self): """ Sets or returns the function which is called when a Blue Dot disconnects. The function will be run in the same thread and block, to run in a separate thread use `set_when_client_disconnects(function, background=True)` """ return self._when_client_disconnects @when_client_disconnects.setter def when_client_disconnects(self, value): self.set_when_client_disconnects(value) def set_when_client_disconnects(self, callback, background=False): """ Sets the function which is called when a Blue Dot disconnects. :param Callable callback: The function to call, setting to `None` will stop the callback. :param bool background: If set to `True` the function will be run in a separate thread and it will return immediately. The default is `False`. """ self._when_client_disconnects = callback self._when_client_disconnects_background = background def wait_for_connection(self, timeout = None): """ Waits until a Blue Dot client connects. Returns ``True`` if a client connects. :param float timeout: Number of seconds to wait for a wait connections, if ``None`` (the default), it will wait indefinetly for a connection from a Blue Dot client. """ return self._is_connected_event.wait(timeout) def start(self): """ Start the :class:`.btcomm.BluetoothServer` if it is not already running. By default the server is started at initialisation. """ self._server.start() self._print_message("Server started {}".format(self.server.server_address)) self._print_message("Waiting for connection") def _create_server(self): self._server = BluetoothServer( self._data_received, when_client_connects = self._client_connected, when_client_disconnects = self._client_disconnected, device = self.device, port = self.port, power_up_device = self._power_up_device, auto_start = False) def stop(self): """ Stop the Bluetooth server. """ self._server.stop() def allow_pairing(self, timeout = 60): """ Allow a Bluetooth device to pair with your Raspberry Pi by putting the adapter into discoverable and pairable mode. :param int timeout: The time in seconds the adapter will remain pairable. If set to ``None`` the device will be discoverable and pairable indefinetly. """ self.server.adapter.allow_pairing(timeout = timeout) def resize(self, cols, rows): """ Resizes the grid of buttons. :param int cols: The number of columns in the grid of buttons. :param int rows: The number of rows in the grid of buttons. .. note:: Existing buttons will retain their state (color, border, etc) when resized. New buttons will be created with the default values set by the :class:`BlueDot`. """ self._cols = cols self._rows = rows # create new buttons new_buttons = {} for c in range(cols): for r in range(rows): # if button already exist, reuse it if (c,r) in self._buttons.keys(): new_buttons[c,r] = self._buttons[c,r] else: new_buttons[c,r] = BlueDotButton(self, c, r, self._color, self._square, self._border, self._visible) self._buttons = new_buttons self._send_bluedot_config() def _get_button(self, key): try: return self._buttons[key] except KeyError: raise ButtonDoesNotExist("The button `{}` does not exist".format(key)) def _client_connected(self): self._is_connected_event.set() self._print_message("Client connected {}".format(self.server.client_address)) self._send_bluedot_config() if self.when_client_connects: self._process_callback(self.when_client_connects, None, self._when_client_connects_background) # wait for the protocol version to be checked. if not self._check_protocol_event.wait(CHECK_PROTOCOL_TIMEOUT): self._print_message("Protocol version not received from client - do you need to update the client to the latest version?") self._server.disconnect_client() def _client_disconnected(self): self._is_connected_event.clear() self._check_protocol_event.clear() self._print_message("Client disconnected") if self.when_client_disconnects: self._process_callback(self.when_client_disconnects, None, self._when_client_disconnects_background) def _data_received(self, data): #add the data received to the buffer self._data_buffer += data #get any full commands ended by \n last_command = self._data_buffer.rfind("\n") if last_command != -1: commands = self._data_buffer[:last_command].split("\n") #remove the processed commands from the buffer self._data_buffer = self._data_buffer[last_command + 1:] self._process_commands(commands) def _process_commands(self, commands): for command in commands: # debug - print each command # print(command) operation = command.split(",")[0] params = command.split(",")[1:] # dot change operation? if operation in ["0", "1", "2"]: position = None try: button, position = self._parse_interaction_msg(operation, params) self._position = position except ValueError: # warn about the occasional corrupt command warnings.warn("Data received which could not be parsed.\n{}".format(command)) except ButtonDoesNotExist: # data received for a button which could not be found warnings.warn("Data received for a button which does not exist.\n{}".format(command)) else: # dot released if operation == "0": self._process_release(button, position) # dot pressed elif operation == "1": self._process_press(button, position) # dot pressed position moved elif operation == "2": self._process_move(button, position) # protocol check elif operation == "3": self._check_protocol_version(params[0], params[1]) else: # operation not identified... warnings.warn("Data received for an unknown operation.\n{}".format(command)) def _parse_interaction_msg(self, operation, params): """ Parses an interaction (press, move, release) message and returns the component parts """ # parse message col = int(params[0]) row = int(params[1]) position = BlueDotPosition(col, row, params[2], params[3]) button = self._get_button((col, row)) return button, position def _process_press(self, button, position): # was the button double pressed? if button.is_double_press(position): self.double_press(position) button.double_press(position) # set the blue dot and button as pressed self.press(position) button.press(position) def _process_move(self, button, position): # set the blue dot as moved self.move(position) # set the button as moved button.move(position) # was it a rotation rotation = button.get_rotation() if rotation is not None: self.rotate(rotation) button.rotate(rotation) def _process_release(self, button, position): # set the blue dot as released self.release(position) # set the button as released button.release(position) # was it a swipe? swipe = button.get_swipe() if swipe is not None: self.swipe(swipe) button.swipe(swipe) def _check_protocol_version(self, protocol_version, client_name): try: version_no = int(protocol_version) except ValueError: raise ValueError("protocol version number must be numeric, received {}.".format(protocol_version)) self._check_protocol_event.set() if version_no != PROTOCOL_VERSION: msg = "Client '{}' was using protocol version {}, bluedot python library is using version {}. " if version_no > PROTOCOL_VERSION: msg += "Update the bluedot python library, using 'sudo pip3 --upgrade install bluedot'." msg = msg.format(client_name, protocol_version, PROTOCOL_VERSION) else: msg += "Update the {}." msg = msg.format(client_name, protocol_version, PROTOCOL_VERSION, client_name) self._server.disconnect_client() print(msg) # called whenever the BlueDot configuration is changed or a client connects def _send_bluedot_config(self): if self.is_connected: self._server.send( "4,{},{},{},{},{},{}\n".format( self._color.str_rgba, int(self._square), int(self._border), int(self._visible), self._cols, self._rows ) ) # send the configuration for the individual buttons button_config_msg = "" for button in self.buttons: if button.modified: button_config_msg += button._build_config_msg() if button_config_msg != "": self._server.send(button_config_msg) def _print_message(self, message): if self.print_messages: print(message) def __getitem__(self, key): return self._get_button(key) ================================================ FILE: bluedot/exceptions.py ================================================ class ButtonDoesNotExist(Exception): pass ================================================ FILE: bluedot/interactions.py ================================================ from time import time from math import atan2, degrees, hypot class BlueDotPosition: """ Represents a position of where the blue dot is pressed, released or held. :param float x: The x position of the Blue Dot, 0 being centre, -1 being far left and 1 being far right. :param float y: The y position of the Blue Dot, 0 being centre, -1 being at the bottom and 1 being at the top. """ def __init__(self, col, row, x, y): self._time = time() self._col = int(col) self._row = int(row) self._x = self._clamped(float(x)) self._y = self._clamped(float(y)) self._angle = None self._distance = None def _clamped(self, v): return max(-1, min(1, v)) @property def col(self): """ The column. """ return self._col @property def row(self): """ The row. """ return self._row @property def x(self): """ The x position of the Blue Dot, 0 being centre, -1 being far left and 1 being far right. """ return self._x @property def y(self): """ The y position of the Blue Dot, 0 being centre, -1 being at the bottom and 1 being at the top. """ return self._y @property def angle(self): """ The angle from centre of where the Blue Dot is pressed, held or released. 0 degrees is up, 0..180 degrees clockwise, -180..0 degrees anti-clockwise. """ if self._angle is None: self._angle = degrees(atan2(self.x, self.y)) return self._angle @property def distance(self): """ The distance from centre of where the Blue Dot is pressed, held or released. The radius of the Blue Dot is 1. """ if self._distance is None: self._distance = self._clamped(hypot(self.x, self.y)) return self._distance @property def middle(self): """ Returns ``True`` if the Blue Dot is pressed, held or released in the middle. """ return self.distance <= 0.5 @property def top(self): """ Returns ``True`` if the Blue Dot is pressed, held or released at the top. """ return self.distance > 0.5 and (-45 < self.angle <= 45) @property def right(self): """ Returns ``True`` if the Blue Dot is pressed, held or released on the right. """ return self.distance > 0.5 and (45 < self.angle <= 135) @property def bottom(self): """ Returns ``True`` if the Blue Dot is pressed, held or released at the bottom. """ return self.distance > 0.5 and (self.angle > 135 or self.angle <= -135) @property def left(self): """ Returns ``True`` if the Blue Dot is pressed, held or released on the left. """ return self.distance > 0.5 and (-135 < self.angle <= -45) @property def time(self): """ The time the blue dot was at this position. .. note:: This is the time the message was received from the Blue Dot app, not the time it was sent. """ return self._time def __str__(self): return "BlueDotPosition - col={}, row={}, x={}, y={}".format( self.col, self.row, self.x, self.y ) class BlueDotInteraction: """ Represents an interaction with the Blue Dot, from when it was pressed to when it was released. A :class:`BlueDotInteraction` can be active or inactive, i.e. it is active because the Blue Dot has not been released, or inactive because the Blue Dot was released and the interaction finished. :param BlueDotPosition pressed_position: The BlueDotPosition when the Blue Dot was pressed. """ def __init__(self, pressed_position): self._active = True self._positions = [] self._positions.append(pressed_position) @property def active(self): """ Returns ``True`` if the interaction is still active, i.e. the Blue Dot hasnt been released. """ return self._active @property def positions(self): """ A sequence of :class:`BlueDotPosition` instances for all the positions which make up this interaction. The first position is where the Blue Dot was pressed, the last is where the Blue Dot was released, all position in between are where the position Blue Dot changed (i.e. moved) when it was held down. """ return self._positions @property def pressed_position(self): """ Returns the position when the Blue Dot was pressed i.e. where the interaction started. """ return self._positions[0] @property def released_position(self): """ Returns the position when the Blue Dot was released i.e. where the interaction ended. If the interaction is still active it returns ``None``. """ return self._positions[-1] if not self.active else None @property def current_position(self): """ Returns the current position for the interaction. If the interaction is inactive, it will return the position when the Blue Dot was released. """ return self._positions[-1] @property def previous_position(self): """ Returns the previous position for the interaction. If the interaction contains only 1 position, None will be returned. """ return self._positions[-2] if len(self._positions) > 1 else None @property def duration(self): """ Returns the duration in seconds of the interaction, i.e. the amount time between when the Blue Dot was pressed and now or when it was released. """ if self.active: return time() - self.pressed_position.time else: return self.released_position.time - self.pressed_position.time @property def distance(self): """ Returns the total distance of the Blue Dot interaction """ dist = 0 for i in range(1, len(self._positions)): p1 = self._positions[i-1] p2 = self._positions[i] dist += hypot(p2.x - p1.x, p2.y - p1.y) return dist def moved(self, moved_position): """ Adds an additional position to the interaction, called when the position the Blue Dot is pressed moves. """ if self._active: self._positions.append(moved_position) def released(self, released_position): """ Called when the Blue Dot is released and completes a Blue Dot interaction :param BlueDotPosition released_position: The BlueDotPosition when the Blue Dot was released. """ self._active = False self._positions.append(released_position) class BlueDotSwipe: """ Represents a Blue Dot swipe interaction. A :class:`BlueDotSwipe` can be valid or invalid based on whether the Blue Dot interaction was a swipe or not. :param BlueDotInteraction interaction: The BlueDotInteraction object to be used to determine whether the interaction was a swipe. """ def __init__(self, interaction): self._interaction = interaction self._col = interaction.current_position.col self._col = interaction.current_position.col self._speed_threshold = 2 self._angle = None self._distance = None self._valid = self._is_valid_swipe() def _is_valid_swipe(self): #the validity of a swipe is based on the speed of the interaction, # so a short fast swipe is valid as well as a long slow swipe #self._speed = self.distance / self.interaction.duration self._speed = self.distance / self.interaction.duration if not self.interaction.active and self._speed > self._speed_threshold: return True else: return False @property def col(self): """ The column. """ return self.interaction.current_position.col @property def row(self): """ The row. """ return self.interaction.current_position.row @property def interaction(self): """ The :class:`BlueDotInteraction` object relating to this swipe. """ return self._interaction @property def valid(self): """ Returns ``True`` if the Blue Dot interaction is a swipe. """ return self._valid @property def distance(self): """ Returns the distance of the swipe (i.e. the distance between the pressed and released positions) """ # should this be the total length of the swipe. All the points? It might be slow to calculate if self._distance == None: self._distance = hypot( self.interaction.released_position.x - self.interaction.pressed_position.x, self.interaction.released_position.y - self.interaction.pressed_position.y) return self._distance @property def angle(self): """ Returns the angle of the swipe (i.e. the angle between the pressed and released positions) """ if self._angle == None: self._angle = degrees(atan2( self.interaction.released_position.x - self.interaction.pressed_position.x, self.interaction.released_position.y - self.interaction.pressed_position.y)) return self._angle @property def speed(self): """ Returns the speed of the swipe in Blue Dot radius / second. """ return self._speed @property def up(self): """ Returns ``True`` if the Blue Dot was swiped up. """ return self.valid and (-45 < self.angle <= 45) @property def down(self): """ Returns ``True`` if the Blue Dot was swiped down. """ return self.valid and (self.angle > 135 or self.angle <= -135) @property def left(self): """ Returns ``True`` if the Blue Dot was swiped left. """ return self.valid and (-135 < self.angle <= -45) @property def right(self): """ Returns ``True`` if the Blue Dot was swiped right. """ return self.valid and (45 < self.angle <= 135) @property def direction(self): """ Returns the direction ("up", "down", "left", "right") of the swipe. If the swipe is not valid `None` is returned. """ if self.up: return "up" elif self.down: return "down" elif self.right: return "right" elif self.left: return "left" else: return None def __str__(self): return "BlueDotSwipe - col={}, row={}, direction={}".format( self.col, self.row, self.direction ) class BlueDotRotation: def __init__(self, interaction, no_of_segments): """ Represents a Blue Dot rotation. A :class:`BlueDotRotation` can be valid or invalid based on whether the Blue Dot interaction was a rotation or not. :param BlueDotInteraction interaction: The object to be used to determine whether the interaction was a rotation. """ self._interaction = interaction self._value = 0 self._clockwise = False self._anti_clockwise = False self._previous_segment = 0 self._current_segment = 0 prev_pos = interaction.previous_position pos = interaction.current_position # was there a previous position (i.e. the interaction has more than 2 positions) if prev_pos != None: # were both positions in the 'outer circle' if prev_pos.distance > 0.5 and pos.distance > 0.5: # what segments are the positions in deg_per_seg = (360 / no_of_segments) self._previous_segment = int((prev_pos.angle + 180) / deg_per_seg) + 1 self._current_segment = int((pos.angle + 180) / deg_per_seg) + 1 # were the positions in different segments if self._previous_segment != self._current_segment: # calculate the rotation diff = self._previous_segment - self._current_segment if diff != 0: if diff == -1: self._value = 1 elif diff == 1: self._value = -1 elif diff == (no_of_segments - 1): self._value = 1 elif diff == (1 - no_of_segments): self._value = -1 @property def col(self): """ The column. """ return self.interaction.current_position.col @property def row(self): """ The row. """ return self.interaction.current_position.row @property def valid(self): """ Returns ``True`` if the Blue Dot was rotated. """ return self._value != 0 @property def interaction(self): """ The :class:`BlueDotInteraction` object relating to this rotation. """ return self._interaction @property def value(self): """ Returns 0 if the Blue Dot wasn't rotated, -1 if rotated anti-clockwise and 1 if rotated clockwise. """ return self._value @property def anti_clockwise(self): """ Returns ``True`` if the Blue Dot was rotated anti-clockwise. """ return self._value == -1 @property def clockwise(self): """ Returns ``True`` if the Blue Dot was rotated clockwise. """ return self._value == 1 def __str__(self): return "BlueDotRotation - col={}, row={}, value={}".format( self.col, self.row, self.value ) ================================================ FILE: bluedot/mock.py ================================================ from .btcomm import BluetoothServer, BluetoothClient, BluetoothAdapter from .dot import BlueDot from .threads import WrapThread from .constants import PROTOCOL_VERSION CLIENT_NAME = "Mock client" class MockBluetoothAdapter(BluetoothAdapter): def __init__(self, device = "mock0", address = "00:00:00:00:00:00"): self._device = device self._address = address self._powered = True self._discoverable = False self._pairable = False self._pairing_thread = None @property def powered(self): return self._powered @powered.setter def powered(self, value): self._powered = value @property def discoverable(self): return self._discoverable @discoverable.setter def discoverable(self, value): self._discoverable = value @property def pairable(self): return self._pairable @pairable.setter def pairable(self, value): self._pairable = value @property def paired_devices(self): return [["01:01:01:01:01:01", "mock_device_1"], ["02:02:02:02:02:02", "mock_device_2"]] class MockBluetoothServer(BluetoothServer): """ :class:`MockBluetoothServer` inherits from :class:`~.btcomm.BluetoothServer` but overrides ``__init__``, :meth:`start` , :meth:`stop` and :meth:`send_raw` to create a :class:`MockBluetoothServer` which can be used for testing and debugging. """ def __init__(self, data_received_callback, auto_start = True, device = "mock0", port = 1, encoding = "utf-8", power_up_device = False, when_client_connects = None, when_client_disconnects = None): super(MockBluetoothServer, self).__init__( data_received_callback, auto_start, device, port, encoding, power_up_device, when_client_connects, when_client_disconnects) self._mock_client = None def start(self): self._running = True def stop(self): self._running = False def mock_client_connected(self, mock_client = None): """ Simulates a client connected to the :class:`~.btcomm.BluetoothServer`. :param MockBluetoothClient mock_client: The mock client to interact with, defaults to `None`. If `None`, client address is set to '99:99:99:99:99:99' """ self._mock_client = mock_client if not self._client_connected: if self._mock_client is None: client_address = "99:99:99:99:99:99" else: client_address = self._mock_client.adapter.address self._client_connected = True self._client_info = (client_address, self.port) #call the call back if self.when_client_connects: WrapThread(target=self.when_client_connects).start() def mock_client_disconnected(self): """ Simulates a client disconnecting from the :class:`~.btcomm.BluetoothServer`. """ if self._client_connected: self._client_connected = False self._client_info = None if self._when_client_disconnects: WrapThread(target=self.when_client_disconnects).start() def mock_client_sending_data(self, data): """ Simulates a client sending data to the :class:`~.btcomm.BluetoothServer`. """ if self._client_connected: self._data_received_callback(data) def _send_data(self, data): if self._mock_client is not None: # call the data received callback if self._encoding: data = data.decode(self._encoding) self._mock_client.mock_server_sending_data(data) def _setup_adapter(self, device): self._adapter = MockBluetoothAdapter(device) class MockBluetoothClient(BluetoothClient): """ :class:`MockBluetoothClient` inherits from :class:`~.btcomm.BluetoothClient` but overrides ``__init__``, :meth:`connect` and :meth:`send_raw` to create a :class:`MockBluetoothServer` which can be used for testing and debugging. Note - the `server` parameter should be an instance of :class:`MockBluetoothServer`. """ def __init__(self, server, data_received_callback, port = 1, device = "mock1", encoding = "utf-8", power_up_device = False, auto_connect = True): super(MockBluetoothClient, self).__init__( server, data_received_callback, port, device, encoding, power_up_device, auto_connect) def connect(self): """ Connect to a Bluetooth server. """ self._server.mock_client_connected(self) self._connected = True def disconnect(self): """ Disconnect from a Bluetooth server. """ self._server.mock_client_disconnected() self._connected = False def mock_server_sending_data(self, data): """ Simulates a server sending data to the :class:`~.btcomm.BluetoothClient`. """ if self._connected: self._data_received_callback(data) def _send_data(self, data): # send data to the server # call the data received callback if self._encoding: data = data.decode(self._encoding) self._server.mock_client_sending_data(data) def _setup_adapter(self, device): self._adapter = MockBluetoothAdapter(device, address = "11:11:11:11:11:11") class MockBlueDot(BlueDot): """ :class:`MockBlueDot` inherits from :class:`BlueDot` but overrides :meth:`_create_server`, to create a :class:`~.mock.MockBluetoothServer` which can be used for testing and debugging. """ def _create_server(self): self._server = MockBluetoothServer( self._data_received, when_client_connects = self._client_connected, when_client_disconnects = self._client_disconnected, device = self.device, port = self.port, power_up_device = self._power_up_device, auto_start = False) def mock_client_connected(self): """ Simulates a client connecting to the Blue Dot. :param string client_address: The mock client mac address, defaults to '11:11:11:11:11:11' """ self._server.mock_client_connected() # send protocol version to server self._server.mock_client_sending_data("3,{},{}\n".format(PROTOCOL_VERSION, CLIENT_NAME)) def mock_client_disconnected(self): """ Simulates a client disconnecting from the Blue Dot. """ self._server.mock_client_disconnected() def mock_blue_dot_pressed(self, col, row, x, y): """ Simulates the Blue Dot being pressed. :param int col: The column position of the button :param int row: The row position of the button :param int x: The x position where the button was pressed :param int y: The y position where the button was pressed """ self._server.mock_client_sending_data("1,{},{},{},{}\n".format(col, row, x, y)) def mock_blue_dot_released(self, col, row, x, y): """ Simulates the Blue Dot being released. :param int col: The column position of the button :param int row: The row position of the button :param int x: The x position where the button was released :param int y: The y position where the button was released """ self._server.mock_client_sending_data("0,{},{},{},{}\n".format(col, row, x, y)) def mock_blue_dot_moved(self, col, row, x, y): """ Simulates the Blue Dot being moved. :param int col: The column position of the button :param int row: The row position of the button :param int x: The x position where the button was moved too :param int y: The y position where the button was moved too """ self._server.mock_client_sending_data("2,{},{},{},{}\n".format(col, row, x, y)) def launch_mock_app(self): """ Launches a mock Blue Dot app. The mock app reacts to mouse clicks and movement and calls the mock blue dot methods to simulates presses. This is useful for testing, allowing you to interact with Blue Dot without having to script mock functions. The mock app uses pygame which will need to be installed. """ self._mock_app_thread = WrapThread(target=self._launch_mock_app) self._mock_app_thread.start() def _launch_mock_app(self): # imported here, so pygame is only a pre-requisite for the mock app from .app import BlueDotClient, ButtonScreen class MockBlueDotClient(BlueDotClient): def _run(self): button_screen = MockButtonScreen(self._screen, self._font, self._device, self._server, self._port, self._width, self._height) button_screen.run() class MockButtonScreen(ButtonScreen): def _connect(self): self.bt_client = MockBluetoothClient(self.server, self._data_received, device = self.device, auto_connect = True) MockBlueDotClient("mock2", self._server, self._port, None, None, None) ================================================ FILE: bluedot/threads.py ================================================ import atexit from threading import Thread, Event _THREADS = set() def _shutdown(): while _THREADS: for t in _THREADS.copy(): t.stop() atexit.register(_shutdown) class WrapThread(Thread): def __init__(self, group=None, target=None, name=None, args=(), kwargs={}): super(WrapThread, self).__init__(group, target, name, args, kwargs) self.stopping = Event() self.daemon = True def start(self): self.stopping.clear() _THREADS.add(self) super(WrapThread, self).start() def stop(self): self.stopping.set() self.join() def join(self): super(WrapThread, self).join() _THREADS.discard(self) ================================================ FILE: bluedot/utils.py ================================================ from __future__ import unicode_literals import dbus import time import sys SERVICE_NAME = "org.bluez" ADAPTER_INTERFACE = SERVICE_NAME + ".Adapter1" DEVICE_INTERFACE = SERVICE_NAME + ".Device1" PROFILE_MANAGER = SERVICE_NAME + ".ProfileManager1" def get_managed_objects(): bus = dbus.SystemBus() manager = dbus.Interface(bus.get_object(SERVICE_NAME, "/"), "org.freedesktop.DBus.ObjectManager") return manager.GetManagedObjects() def find_adapter(pattern=None): return find_adapter_in_objects(get_managed_objects(), pattern) def find_adapter_in_objects(objects, pattern=None): bus = dbus.SystemBus() for path, ifaces in objects.items(): adapter = ifaces.get(ADAPTER_INTERFACE) if adapter is None: continue if not pattern or pattern == adapter["Address"] or path.endswith(pattern): obj = bus.get_object(SERVICE_NAME, path) return dbus.Interface(obj, ADAPTER_INTERFACE) raise Exception("Bluetooth adapter {} not found".format(pattern)) def get_adapter_property(device_name, prop): bus = dbus.SystemBus() adapter_path = find_adapter(device_name).object_path adapter = dbus.Interface(bus.get_object(SERVICE_NAME, adapter_path),"org.freedesktop.DBus.Properties") return adapter.Get(ADAPTER_INTERFACE, prop) def get_mac(device_name): return get_adapter_property(device_name, "Address") def get_adapter_powered_status(device_name): powered = get_adapter_property(device_name, "Powered") return bool(powered) def get_adapter_discoverable_status(device_name): discoverable = get_adapter_property(device_name, "Discoverable") return bool(discoverable) def get_adapter_pairable_status(device_name): pairable = get_adapter_property(device_name, "Pairable") return bool(pairable) def get_paired_devices(device_name): paired_devices = [] bus = dbus.SystemBus() adapter_path = find_adapter(device_name).object_path om = dbus.Interface(bus.get_object(SERVICE_NAME, "/"), "org.freedesktop.DBus.ObjectManager") objects = om.GetManagedObjects() for path, interfaces in objects.items(): if DEVICE_INTERFACE not in interfaces: continue properties = interfaces[DEVICE_INTERFACE] if properties["Adapter"] != adapter_path: continue paired_devices.append((str(properties["Address"]), str(properties["Alias"]))) return paired_devices def device_discoverable(device_name, discoverable): bus = dbus.SystemBus() adapter_path = find_adapter(device_name).object_path adapter = dbus.Interface(bus.get_object(SERVICE_NAME, adapter_path),"org.freedesktop.DBus.Properties") if discoverable: value = dbus.Boolean(1) else: value = dbus.Boolean(0) adapter.Set(ADAPTER_INTERFACE, "Discoverable", value) def device_pairable(device_name, pairable): bus = dbus.SystemBus() adapter_path = find_adapter(device_name).object_path adapter = dbus.Interface(bus.get_object(SERVICE_NAME, adapter_path),"org.freedesktop.DBus.Properties") if pairable: value = dbus.Boolean(1) else: value = dbus.Boolean(0) adapter.Set(ADAPTER_INTERFACE, "Pairable", value) def device_powered(device_name, powered): bus = dbus.SystemBus() adapter_path = find_adapter(device_name).object_path adapter = dbus.Interface(bus.get_object(SERVICE_NAME, adapter_path),"org.freedesktop.DBus.Properties") if powered: value = dbus.Boolean(1) else: value = dbus.Boolean(0) adapter.Set(ADAPTER_INTERFACE, "Powered", value) def register_spp(port): service_record = """ """.format(port) bus = dbus.SystemBus() manager = dbus.Interface(bus.get_object(SERVICE_NAME, "/org/bluez"), PROFILE_MANAGER) path = "/bluez" uuid = "00001101-0000-1000-8000-00805f9b34fb" opts = { # "AutoConnect" : True, "ServiceRecord" : service_record } try: manager.RegisterProfile(path, uuid, opts) except dbus.exceptions.DBusException as e: #the spp profile has already been registered, ignore if str(e) != "org.bluez.Error.AlreadyExists: Already Exists": raise(e) ================================================ FILE: clients/android/.gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures .externalNativeBuild ================================================ FILE: clients/android/README.rst ================================================ Blue Dot Android App ==================== The `Blue Dot app`_ is available to download from the Google Play store. Please leave a rating and review if you find Blue Dot useful :) |bluedotapp| |bluedotappdevices| Start ----- 1. Download the `Blue Dot app`_ from the Google Play store. 2. If you havent already done so, pair your raspberry pi as described in the `getting started`_ guide 3. Run the Blue Dot app |bluedotappicon| 4. Select your Raspberry Pi from the paired devices list |bluedotappdevices| 5. Press the Dot |bluedotapp| .. _Blue Dot app: http://play.google.com/store/apps/details?id=com.stuffaboutcode.bluedot .. _getting started: http://bluedot.readthedocs.io/en/latest/gettingstarted.html .. |bluedotapp| image:: https://raw.githubusercontent.com/martinohanlon/BlueDot/master/docs/images/bluedotandroid_small.png :height: 247 px :width: 144 px :scale: 100 % :alt: blue dot app .. |bluedotappdevices| image:: https://raw.githubusercontent.com/martinohanlon/BlueDot/master/docs/images/bluedotandroiddevices_small.png :height: 246 px :width: 144 px :scale: 100 % :alt: blue dot app devices list .. |bluedotappicon| image:: https://raw.githubusercontent.com/martinohanlon/BlueDot/master/docs/images/bluedotandroidicon.png :height: 143 px :width: 127 px :scale: 100 % :alt: blue dot icon ================================================ FILE: clients/android/app/.gitignore ================================================ /build ================================================ FILE: clients/android/app/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 33 defaultConfig { applicationId "com.stuffaboutcode.bluedot" minSdkVersion 14 targetSdkVersion 33 versionCode 10 versionName "2.2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.preference:preference:1.2.0' androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { exclude group: 'com.android.support', module: 'support-annotations' }) implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' testImplementation 'junit:junit:4.12' } configurations { all { exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx' } } ================================================ FILE: clients/android/app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in C:\Users\Mart\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: clients/android/app/release/output-metadata.json ================================================ { "version": 3, "artifactType": { "type": "APK", "kind": "Directory" }, "applicationId": "com.stuffaboutcode.bluedot", "variantName": "release", "elements": [ { "type": "SINGLE", "filters": [], "attributes": [], "versionCode": 10, "versionName": "2.2.1", "outputFile": "app-release.apk" } ], "elementType": "File" } ================================================ FILE: clients/android/app/src/androidTest/java/com/stuffaboutcode/bluedot/ExampleInstrumentedTest.java ================================================ package com.stuffaboutcode.bluedot; import android.content.Context; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumentation test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() throws Exception { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); assertEquals("com.stuffaboutcode.bluedot", appContext.getPackageName()); } } ================================================ FILE: clients/android/app/src/main/AndroidManifest.xml ================================================ /> /> ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/bluedot/BluetoothChatService.java ================================================ /* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.stuffaboutcode.bluedot; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Message; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.util.UUID; import java.lang.reflect.Method; import com.stuffaboutcode.logger.Log; /** * This class does all the work for setting up and managing Bluetooth * connections with other devices. It has a thread that listens for * incoming connections, a thread for connecting with a device, and a * thread for performing data transmissions when connected. */ public class BluetoothChatService { // Debugging private static final String TAG = "BluetoothChatService"; // Name for the SDP record when creating server socket private static final String NAME_SECURE = "BluetoothChatSecure"; private static final String NAME_INSECURE = "BluetoothChatInsecure"; // Unique UUID for this application private static final UUID MY_UUID_SECURE = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"); private static final UUID MY_UUID_INSECURE = UUID.fromString("8ce255c0-200a-11e0-ac64-0800200c9a66"); // Member fields private final BluetoothAdapter mAdapter; private final Handler mHandler; private AcceptThread mSecureAcceptThread; private AcceptThread mInsecureAcceptThread; private ConnectThread mConnectThread; private ConnectedThread mConnectedThread; private int mState; private int mNewState; // Constants that indicate the current connection state public static final int STATE_NONE = 0; // we're doing nothing public static final int STATE_LISTEN = 1; // now listening for incoming connections public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection public static final int STATE_CONNECTED = 3; // now connected to a remote device /** * Constructor. Prepares a new BluetoothChat session. * * @param context The UI Activity Context * @param handler A Handler to send messages back to the UI Activity */ public BluetoothChatService(Context context, Handler handler) { mAdapter = BluetoothAdapter.getDefaultAdapter(); mState = STATE_NONE; mNewState = mState; mHandler = handler; } /** * Update UI title according to the current state of the chat connection */ private synchronized void updateUserInterfaceTitle() { mState = getState(); Log.d(TAG, "updateUserInterfaceTitle() " + mNewState + " -> " + mState); mNewState = mState; // Give the new state to the Handler so the UI Activity can update mHandler.obtainMessage(Constants.MESSAGE_STATE_CHANGE, mNewState, -1).sendToTarget(); } /** * Return the current connection state. */ public synchronized int getState() { return mState; } /** * Start the chat service. Specifically start AcceptThread to begin a * session in listening (server) mode. Called by the Activity onResume() */ public synchronized void start() { Log.d(TAG, "start"); // Cancel any thread attempting to make a connection if (mConnectThread != null) { mConnectThread.cancel(); mConnectThread = null; } // Cancel any thread currently running a connection if (mConnectedThread != null) { mConnectedThread.cancel(); mConnectedThread = null; } // Start the thread to listen on a BluetoothServerSocket if (mSecureAcceptThread == null) { mSecureAcceptThread = new AcceptThread(true); mSecureAcceptThread.start(); } if (mInsecureAcceptThread == null) { mInsecureAcceptThread = new AcceptThread(false); mInsecureAcceptThread.start(); } // Update UI title updateUserInterfaceTitle(); } /** * Start the ConnectThread to initiate a connection to a remote device. * * @param device The BluetoothDevice to connect * @param secure Socket Security type - Secure (true) , Insecure (false) */ public synchronized void connect(BluetoothDevice device, int port, boolean secure) { Log.d(TAG, "connect to: " + device); // Cancel any thread attempting to make a connection if (mState == STATE_CONNECTING) { if (mConnectThread != null) { mConnectThread.cancel(); mConnectThread = null; } } // Cancel any thread currently running a connection if (mConnectedThread != null) { mConnectedThread.cancel(); mConnectedThread = null; } // Start the thread to connect with the given device mConnectThread = new ConnectThread(device, port, secure); mConnectThread.start(); // Update UI title updateUserInterfaceTitle(); } /** * Start the ConnectedThread to begin managing a Bluetooth connection * * @param socket The BluetoothSocket on which the connection was made * @param device The BluetoothDevice that has been connected */ public synchronized void connected(BluetoothSocket socket, BluetoothDevice device, final String socketType) { Log.d(TAG, "connected, Socket Type:" + socketType); // Cancel the thread that completed the connection if (mConnectThread != null) { mConnectThread.cancel(); mConnectThread = null; } // Cancel any thread currently running a connection if (mConnectedThread != null) { mConnectedThread.cancel(); mConnectedThread = null; } // Cancel the accept thread because we only want to connect to one device if (mSecureAcceptThread != null) { mSecureAcceptThread.cancel(); mSecureAcceptThread = null; } if (mInsecureAcceptThread != null) { mInsecureAcceptThread.cancel(); mInsecureAcceptThread = null; } // Start the thread to manage the connection and perform transmissions mConnectedThread = new ConnectedThread(socket, socketType); mConnectedThread.start(); // Send the name of the connected device back to the UI Activity Message msg = mHandler.obtainMessage(Constants.MESSAGE_DEVICE_NAME); Bundle bundle = new Bundle(); bundle.putString(Constants.DEVICE_NAME, device.getName()); msg.setData(bundle); mHandler.sendMessage(msg); // Update UI title updateUserInterfaceTitle(); } /** * Stop all threads */ public synchronized void stop() { Log.d(TAG, "stop"); if (mConnectThread != null) { mConnectThread.cancel(); mConnectThread = null; } if (mConnectedThread != null) { mConnectedThread.cancel(); mConnectedThread = null; } if (mSecureAcceptThread != null) { mSecureAcceptThread.cancel(); mSecureAcceptThread = null; } if (mInsecureAcceptThread != null) { mInsecureAcceptThread.cancel(); mInsecureAcceptThread = null; } mState = STATE_NONE; // Update UI title updateUserInterfaceTitle(); } /** * Write to the ConnectedThread in an unsynchronized manner * * @param out The bytes to write * @see ConnectedThread#write(byte[]) */ public void write(byte[] out) { // Create temporary object ConnectedThread r; // Synchronize a copy of the ConnectedThread synchronized (this) { if (mState != STATE_CONNECTED) return; r = mConnectedThread; } // Perform the write unsynchronized r.write(out); } /** * Indicate that the connection attempt failed and notify the UI Activity. */ private void connectionFailed() { // Send a failure message back to the Activity Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST); Bundle bundle = new Bundle(); bundle.putString(Constants.TOAST, "Unable to connect"); msg.setData(bundle); mHandler.sendMessage(msg); mState = STATE_NONE; // Update UI title updateUserInterfaceTitle(); // Start the service over to restart listening mode BluetoothChatService.this.start(); } /** * Indicate that the connection was lost and notify the UI Activity. */ private void connectionLost() { // Send a failure message back to the Activity Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST); Bundle bundle = new Bundle(); bundle.putString(Constants.TOAST, "Connection lost"); msg.setData(bundle); mHandler.sendMessage(msg); mState = STATE_NONE; // Update UI title updateUserInterfaceTitle(); // Start the service over to restart listening mode //BluetoothChatService.this.start(); } /** * This thread runs while listening for incoming connections. It behaves * like a server-side client. It runs until a connection is accepted * (or until cancelled). */ private class AcceptThread extends Thread { // The local server socket private final BluetoothServerSocket mmServerSocket; private String mSocketType; public AcceptThread(boolean secure) { BluetoothServerSocket tmp = null; mSocketType = secure ? "Secure" : "Insecure"; // Create a new listening server socket try { if (secure) { tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME_SECURE, MY_UUID_SECURE); } else { tmp = mAdapter.listenUsingInsecureRfcommWithServiceRecord( NAME_INSECURE, MY_UUID_INSECURE); } } catch (IOException e) { Log.e(TAG, "Socket Type: " + mSocketType + "listen() failed", e); } mmServerSocket = tmp; mState = STATE_LISTEN; } public void run() { Log.d(TAG, "Socket Type: " + mSocketType + "BEGIN mAcceptThread" + this); setName("AcceptThread" + mSocketType); BluetoothSocket socket = null; // Listen to the server socket if we're not connected while (mState != STATE_CONNECTED) { try { // This is a blocking call and will only return on a // successful connection or an exception socket = mmServerSocket.accept(); } catch (IOException e) { Log.e(TAG, "Socket Type: " + mSocketType + "accept() failed", e); break; } // If a connection was accepted if (socket != null) { synchronized (BluetoothChatService.this) { switch (mState) { case STATE_LISTEN: case STATE_CONNECTING: // Situation normal. Start the connected thread. connected(socket, socket.getRemoteDevice(), mSocketType); break; case STATE_NONE: case STATE_CONNECTED: // Either not ready or already connected. Terminate new socket. try { socket.close(); } catch (IOException e) { Log.e(TAG, "Could not close unwanted socket", e); } break; } } } } Log.i(TAG, "END mAcceptThread, socket Type: " + mSocketType); } public void cancel() { Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this); try { mmServerSocket.close(); } catch (IOException e) { Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e); } } } /** * This thread runs while attempting to make an outgoing connection * with a device. It runs straight through; the connection either * succeeds or fails. */ private class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; private String mSocketType; public ConnectThread(BluetoothDevice device, int port, boolean secure) { mmDevice = device; BluetoothSocket tmp = null; mSocketType = secure ? "Secure" : "Insecure"; mSocketType += (Integer.toString(port)); // Get a BluetoothSocket for a connection with the // given BluetoothDevice // if the port is 0 use the service record to connect, otherwise connect direct try { if (secure) { if (port == 0) { tmp = device.createRfcommSocketToServiceRecord(MY_UUID_SECURE); } else { Method createRfcommSocket = device.getClass().getMethod("createRfcommSocket", new Class[]{int.class}); tmp = (BluetoothSocket) createRfcommSocket.invoke(device, port); } } else { if (port == 0) { tmp = device.createInsecureRfcommSocketToServiceRecord(MY_UUID_INSECURE); } else { Method createInsecureRfcommSocket = device.getClass().getMethod("createInsecureRfcommSocket", new Class[]{int.class}); tmp = (BluetoothSocket) createInsecureRfcommSocket.invoke(device, port); } } } catch (IOException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e); } mmSocket = tmp; mState = STATE_CONNECTING; } public void run() { Log.i(TAG, "BEGIN mConnectThread SocketType:" + mSocketType); setName("ConnectThread" + mSocketType); // Always cancel discovery because it will slow down a connection mAdapter.cancelDiscovery(); // Make a connection to the BluetoothSocket try { if (mmSocket != null) { // This is a blocking call and will only return on a // successful connection or an exception mmSocket.connect(); } else { // This shouldn't happen, but was reported as an ANR so sometimes it must! connectionFailed(); return; } } catch (IOException e) { // Close the socket try { mmSocket.close(); } catch (IOException e2) { Log.e(TAG, "unable to close() " + mSocketType + " socket during connection failure", e2); } connectionFailed(); return; } // Reset the ConnectThread because we're done synchronized (BluetoothChatService.this) { mConnectThread = null; } // Start the connected thread connected(mmSocket, mmDevice, mSocketType); } public void cancel() { try { if (mmSocket != null) { mmSocket.close(); } } catch (IOException e) { Log.e(TAG, "close() of connect " + mSocketType + " socket failed", e); } } } /** * This thread runs during a connection with a remote device. * It handles all incoming and outgoing transmissions. */ private class ConnectedThread extends Thread { private final BluetoothSocket mmSocket; private final InputStream mmInStream; private final OutputStream mmOutStream; public ConnectedThread(BluetoothSocket socket, String socketType) { Log.d(TAG, "create ConnectedThread: " + socketType); mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; // Get the BluetoothSocket input and output streams try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { Log.e(TAG, "temp sockets not created", e); } mmInStream = tmpIn; mmOutStream = tmpOut; mState = STATE_CONNECTED; } public void run() { Log.i(TAG, "BEGIN mConnectedThread"); //byte[] buffer = new byte[1024]; //int bytes; // Keep listening to the InputStream while connected while (mState == STATE_CONNECTED) { try { byte[] buffer = new byte[1024]; int bytes; // Read from the InputStream bytes = mmInStream.read(buffer); // Send the obtained bytes to the UI Activity mHandler.obtainMessage(Constants.MESSAGE_READ, bytes, -1, buffer) .sendToTarget(); } catch (IOException e) { Log.e(TAG, "Disconnected", e); connectionLost(); break; } } } /** * Write to the connected OutStream. * * @param buffer The bytes to write */ public void write(byte[] buffer) { try { mmOutStream.write(buffer); // Share the sent message back to the UI Activity mHandler.obtainMessage(Constants.MESSAGE_WRITE, -1, -1, buffer) .sendToTarget(); } catch (IOException e) { Log.e(TAG, "Exception during write", e); } } public void cancel() { try { mmSocket.close(); } catch (IOException e) { Log.e(TAG, "close() of connect socket failed", e); } } } } ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/bluedot/Button.java ================================================ package com.stuffaboutcode.bluedot; import android.graphics.Color; import android.os.Handler; import android.os.Message; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.PreferenceManager; import android.content.SharedPreferences; import android.os.Bundle; import android.content.Intent; import android.view.MotionEvent; import android.view.View; import android.widget.TextView; import android.widget.Toast; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.app.ProgressDialog; import com.stuffaboutcode.logger.Log; public class Button extends AppCompatActivity { private String mConnectedDeviceName = null; private StringBuffer mOutStringBuffer; private StringBuffer mInStringBuffer; private BluetoothAdapter mBluetoothAdapter = null; private BluetoothChatService mChatService = null; String address = null; String deviceName = null; private ProgressDialog progress; private double last_x = 0; private double last_y = 0; private DynamicMatrix matrix; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_button); Intent newint = getIntent(); deviceName = newint.getStringExtra(Devices.EXTRA_NAME); address = newint.getStringExtra(Devices.EXTRA_ADDRESS); // Get the bluetooth port number from preferences SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); int port_number = 1; // if the default port is not used, get the port if (!sharedPreferences.getBoolean("default_port", true)) { String port_value = sharedPreferences.getString("port", "0"); port_number = Integer.parseInt(port_value); } // Get local Bluetooth adapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); // If the adapter is null, then Bluetooth is not supported if (mBluetoothAdapter == null) { Toast.makeText(getApplicationContext(), "Bluetooth is not available", Toast.LENGTH_LONG).show(); this.finish(); } // Initialize the BluetoothChatService to perform bluetooth connections mChatService = new BluetoothChatService(this, mHandler); // Initialize the buffer for outgoing messages mOutStringBuffer = new StringBuffer(""); // Initialize the buffer for incoming messages mInStringBuffer = new StringBuffer(""); // Get the BluetoothDevice object BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); // Attempt to connect to the device mChatService.connect(device, port_number,true); matrix = findViewById(R.id.matrix); // Once connected setup the listener matrix.setOnUseListener(new DynamicMatrix.DynamicMatrixListener() { @Override public void onPress(DynamicMatrix.MatrixCell cell, int pointerId, float actual_x, float actual_y) { double x = calcX(cell, actual_x); double y = calcY(cell, actual_y); send(buildMessage("1", cell.getCol(), cell.getRow(), x, y)); last_x = x; last_y = y; } @Override public void onMove(DynamicMatrix.MatrixCell cell, int pointerId, float actual_x, float actual_y) { double x = calcX(cell, actual_x); double y = calcY(cell, actual_y); if ((x != last_x) || (y != last_y)) { send(buildMessage("2", cell.getCol(), cell.getRow(), x, y)); last_x = x; last_y = y; } } @Override public void onRelease(DynamicMatrix.MatrixCell cell, int pointerId, float actual_x, float actual_y) { double x = calcX(cell, actual_x); double y = calcY(cell, actual_y); send(buildMessage("0", cell.getCol(), cell.getRow(), x, y)); last_x = x; last_y = y; } }); } private double calcX(DynamicMatrix.MatrixCell cell, float actual_x) { double relative_x = actual_x - cell.getBounds().left; relative_x = (relative_x - (cell.getWidth() / 2)) / (cell.getWidth() / 2); return (double)Math.round(relative_x * 10000d) / 10000d; } private double calcY(DynamicMatrix.MatrixCell cell, float actual_y) { double relative_y = actual_y - cell.getBounds().top; relative_y = (relative_y - (cell.getHeight() / 2)) / (cell.getHeight() / 2) * -1; return (double)Math.round(relative_y * 10000d) / 10000d; } private double calcX(View roundButton, MotionEvent event) { double x = (event.getX() - (roundButton.getWidth() / 2)) / (roundButton.getWidth() / 2); x = (double)Math.round(x * 10000d) / 10000d; return x; } private double calcY(View roundButton, MotionEvent event) { double y = (event.getY() - (roundButton.getHeight() / 2)) / (roundButton.getHeight() /2) * -1; y = (double)Math.round(y * 10000d) / 10000d; return y; } private String buildMessage(String operation, int col, int row, double x, double y) { return (operation + "," + String.valueOf(col) + "," + String.valueOf(row) + "," + String.valueOf(x) + "," + String.valueOf(y) + "\n"); } public void send(String message) { // Check that we're actually connected before trying anything if (mChatService.getState() != BluetoothChatService.STATE_CONNECTED) { Toast.makeText(this, "cant send message - not connected", Toast.LENGTH_SHORT).show(); return; } // Check that there's actually something to send if (message.length() > 0) { // Get the message bytes and tell the BluetoothChatService to write byte[] send = message.getBytes(); mChatService.write(send); // Reset out string buffer to zero and clear the edit text field mOutStringBuffer.setLength(0); } } private void disconnect() { if (mChatService != null) { mChatService.stop(); }; finish(); } private void msg(String message) { TextView statusView = (TextView)findViewById(R.id.status); statusView.setText(message); } private void parseData(String data) { //msg(data); // add the message to the buffer mInStringBuffer.append(data); // debug - log data and buffer //Log.d("data", data); //Log.d("mInStringBuffer", mInStringBuffer.toString()); //msg(data.toString()); // find any complete messages String[] messages = mInStringBuffer.toString().split("\\n"); int noOfMessages = messages.length; // does the last message end in a \n, if not its incomplete and should be ignored if (!mInStringBuffer.toString().endsWith("\n")) { noOfMessages = noOfMessages - 1; } // clean the data buffer of any processed messages if (mInStringBuffer.lastIndexOf("\n") > -1) mInStringBuffer.delete(0, mInStringBuffer.lastIndexOf("\n") + 1); // process messages for (int messageNo = 0; messageNo < noOfMessages; messageNo++) { processMessage(messages[messageNo]); } } private void processMessage(String message) { // Debug // msg(message); String parameters[] = message.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)"); boolean invalid = false; // Check the message if (parameters.length > 0) { switch (parameters[0]) { case "4": invalid = processSetMatrixMessage(parameters); break; case "5": invalid = processSetCellMessage(parameters); break; default: invalid = true; } } if (invalid) { msg("Error - Invalid message received '" + message +"'"); } } private boolean processSetMatrixMessage(String parameters[]) { // "4,[color],[square],[border],[visible],[cols],[rows]" boolean invalid = false; // check length if (parameters.length == 7) { // cols matrix.setCols(Integer.parseInt(parameters[5])); // rows matrix.setRows(Integer.parseInt(parameters[6])); //color String color = convertColor(parameters[1]); if (!color.equals("")) { matrix.setColor(Color.parseColor(color)); } else { invalid = true; } matrix.setSquare(parameters[2].equals("1")); matrix.setBorder(parameters[3].equals("1")); matrix.setVisible(parameters[4].equals("1")); matrix.update(); } else { invalid = true; } return invalid; } private boolean processSetCellMessage(String parameters[]) { // "5,[color],[square],[border],[visible],[col],[row]" boolean invalid = false; int col; int row; // check length if (parameters.length == 7) { // get the col and row col = Integer.parseInt(parameters[5]); row = Integer.parseInt(parameters[6]); // get the cell DynamicMatrix.MatrixCell cell = matrix.getCell(col, row); String color = convertColor(parameters[1]); if (!color.equals("")) { cell.setColor(Color.parseColor(color)); } else { invalid = true; } cell.setSquare(parameters[2].equals("1")); cell.setBorder(parameters[3].equals("1")); cell.setVisible(parameters[4].equals("1")); matrix.update(); } else { invalid = true; } return invalid; } private String convertColor(String color) { // convert color from #rrggbbaa to #aarrggbb String new_color; try { new_color = color.substring(0, 1) + color.substring(7, 9) + color.substring(1, 7); } catch (Exception i) { // return an empty string if the color is invalid new_color = ""; } return new_color; } private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case Constants.MESSAGE_STATE_CHANGE: switch (msg.arg1) { case BluetoothChatService.STATE_CONNECTED: Log.d("status","connected"); msg("Connected to " + deviceName); matrix.setVisibility(View.VISIBLE); // send the protocol version to the server send("3," + Constants.PROTOCOL_VERSION + "," + Constants.CLIENT_NAME + "\n"); break; case BluetoothChatService.STATE_CONNECTING: Log.d("status","connecting"); msg("Connecting to " + deviceName); matrix.setVisibility(View.INVISIBLE); break; case BluetoothChatService.STATE_LISTEN: case BluetoothChatService.STATE_NONE: Log.d("status","not connected"); msg("Not connected"); disconnect(); break; } break; case Constants.MESSAGE_WRITE: byte[] writeBuf = (byte[]) msg.obj; // construct a string from the buffer String writeMessage = new String(writeBuf); break; case Constants.MESSAGE_READ: byte[] readBuf = (byte[]) msg.obj; // construct a string from the valid bytes in the buffer String readData = new String(readBuf, 0, msg.arg1); // message received parseData(readData); break; case Constants.MESSAGE_DEVICE_NAME: // save the connected device's name mConnectedDeviceName = msg.getData().getString(Constants.DEVICE_NAME); if (null != this) { Toast.makeText(getApplicationContext(), "Connected to " + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); } break; case Constants.MESSAGE_TOAST: if (null != this) { Toast.makeText(getApplicationContext(), msg.getData().getString(Constants.TOAST), Toast.LENGTH_SHORT).show(); } break; } } }; @Override public void onBackPressed() { disconnect(); } } ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/bluedot/Constants.java ================================================ /* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.stuffaboutcode.bluedot; /** * Defines several constants used between {@link BluetoothChatService} and the UI. */ public interface Constants { // Message types sent from the BluetoothChatService Handler public static final int MESSAGE_STATE_CHANGE = 1; public static final int MESSAGE_READ = 2; public static final int MESSAGE_WRITE = 3; public static final int MESSAGE_DEVICE_NAME = 4; public static final int MESSAGE_TOAST = 5; // Key names received from the BluetoothChatService Handler public static final String DEVICE_NAME = "device_name"; public static final String TOAST = "toast"; public static final float BORDER_THICKNESS = (float)0.025; public static final String PROTOCOL_VERSION = "2"; public static final String CLIENT_NAME = "Blue Dot Android app"; } ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/bluedot/Devices.java ================================================ package com.stuffaboutcode.bluedot; import androidx.appcompat.app.AppCompatActivity; import android.content.pm.PackageManager; import android.os.Bundle; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.widget.ImageButton; import android.widget.Toast; import android.content.Intent; import android.widget.ArrayAdapter; import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; import android.view.View; import android.net.Uri; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import androidx.core.app.ActivityCompat; import androidx.preference.PreferenceManager; import android.content.SharedPreferences; import android.Manifest; import java.util.Set; import java.util.ArrayList; public class Devices extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener { ListView devicelist; ImageButton infoButton; private BluetoothAdapter myBluetooth = null; private Set pairedDevices; public static String EXTRA_ADDRESS = "device_address"; public static String EXTRA_NAME = "device_name"; private static String[] PERMISSIONS = { Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT, }; private void checkPermissions(){ int permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT); if (permission != PackageManager.PERMISSION_GRANTED) { // We don't have permission so prompt the user ActivityCompat.requestPermissions( this, PERMISSIONS, 1 ); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_devices); devicelist = (ListView)findViewById(R.id.listView); checkPermissions(); //if the device has bluetooth myBluetooth = BluetoothAdapter.getDefaultAdapter(); if(myBluetooth == null) { Toast.makeText( getApplicationContext(), "Bluetooth Device Not Available", Toast.LENGTH_LONG).show(); //finish apk this.finish(); System.exit(0); } else if(!myBluetooth.isEnabled()) { //Ask to the user turn the bluetooth on Intent turnBTon = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(turnBTon,1); } SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); sharedPreferences.registerOnSharedPreferenceChangeListener(this); } @Override protected void onResume() { super.onResume(); setConnectMsg(); pairedDevicesList(); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater menuInflater = getMenuInflater(); menuInflater.inflate(R.menu.settings_menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.settings) { Intent intent = new Intent(Devices.this, SettingsActivity.class); startActivity(intent); return true; } else if (id == R.id.help) { Uri uri = Uri.parse("https://bluedot.readthedocs.io"); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); return true; } return super.onOptionsItemSelected(item); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals("port")) { setConnectMsg(sharedPreferences); } } private void setConnectMsg() { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); setConnectMsg(sharedPreferences); } private void setConnectMsg(SharedPreferences sharedPreferences) { String message = "Connect"; Boolean default_port = sharedPreferences.getBoolean("default_port", true); String port_value = sharedPreferences.getString("port", "1"); if (!default_port) { message = message + " on port " + port_value; } TextView connectView = findViewById(R.id.connect); connectView.setText(message); } private void pairedDevicesList() { pairedDevices = myBluetooth.getBondedDevices(); ArrayList list = new ArrayList(); if (pairedDevices.size()>0) { // create a list of paired bluetooth devices for(BluetoothDevice bt : pairedDevices) { list.add(bt.getName() + "\n" + bt.getAddress()); //Get the device's name and the address } } else { Toast.makeText( getApplicationContext(), "No Paired Bluetooth Devices Found.", Toast.LENGTH_LONG).show(); } final ArrayAdapter adapter = new ArrayAdapter(this,android.R.layout.simple_list_item_1, list); devicelist.setAdapter(adapter); devicelist.setOnItemClickListener(myListClickListener); //Method called when the device from the list is clicked } private AdapterView.OnItemClickListener myListClickListener = new AdapterView.OnItemClickListener() { public void onItemClick (AdapterView av, View v, int arg2, long arg3) { // Get the device MAC address, the last 17 chars in the View String info = ((TextView) v).getText().toString(); String deviceName = info.split("\n")[0]; String address = info.split("\n")[1]; // Make an intent to start next activity. Intent i = new Intent(Devices.this, Button.class); //Change the activity. i.putExtra(EXTRA_NAME, deviceName); i.putExtra(EXTRA_ADDRESS, address); startActivity(i); } }; } ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/bluedot/DynamicMatrix.java ================================================ package com.stuffaboutcode.bluedot; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import java.util.ArrayList; import java.util.HashMap; class DynamicMatrix extends View { private ArrayList> mCells; private int mCols, mRows; private Paint mTextPaint, mCellPaint, mBorderPaint, mLinePaint; private float mTextHeight; private int mWidth, mHeight; int mMatrixWidth, mMatrixHeight; int mCellSize; private Context mContext; private RectF mMatrixBounds = new RectF(); private HashMap pointers = new HashMap(); public interface DynamicMatrixListener { public void onPress(MatrixCell cell, int pointerId, float actual_x, float actual_y); public void onMove(MatrixCell cell, int pointerId, float actual_x, float actual_y); public void onRelease(MatrixCell cell, int pointerId, float actual_x, float actual_y); } private DynamicMatrixListener listener; public DynamicMatrix(Context context, AttributeSet attrs) { super(context, attrs); listener = null; mContext = context; TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.DynamicMatrix, 0, 0); try { mCols = a.getInteger(R.styleable.DynamicMatrix_cols, 0); mRows = a.getInteger(R.styleable.DynamicMatrix_rows, 0); } finally { a.recycle(); } init(); } public void setOnUseListener(DynamicMatrixListener listener) { this.listener = listener; } // initialise the matrix private void init() { mCellPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCellPaint.setStyle(Paint.Style.FILL); mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBorderPaint.setStyle(Paint.Style.STROKE); mBorderPaint.setStrokeWidth(5); mBorderPaint.setColor(mContext.getResources().getColor(R.color.darkgrey)); setupMatrix(); } // when the size changes, re-create the matrix @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); int xpad = (getPaddingLeft() + getPaddingRight()); int ypad = (getPaddingTop() + getPaddingBottom()); mWidth = w - xpad; mHeight = h - ypad; sizeMatrix(); } // setup matrix is called on init or when the number of rows or cols changes // and creates a default matrix with a zero size, its not sized until onSizeChanged // is called private void setupMatrix() { mMatrixWidth = 0; mMatrixHeight = 0; mCellSize = 0; mMatrixBounds = new RectF(0 ,0,0,0); // create the cells mCells = new ArrayList>(); int color = mContext.getResources().getColor(R.color.defaultCellColor); for(int c = 0; c < getCols(); c++) { mCells.add(new ArrayList()); for (int r = 0; r < getRows(); r++) { mCells.get(c).add(new MatrixCell( c, r, new RectF(0,0,0,0), true, false, color, false)); } } } // called when the screen size of the matrix needs to change private void sizeMatrix() { // calc potential size of matrix int left = 0, top = 0; float borderWidth; // find out how big each cell can be if ((mWidth / getCols()) < (mHeight / getRows())) { borderWidth = (mWidth * Constants.BORDER_THICKNESS); mCellSize = (int)((mWidth - borderWidth) / getCols()); mMatrixWidth = mWidth; mMatrixHeight = getRows() * mCellSize; top = (mHeight - mMatrixHeight) / 2; left = (int)(borderWidth / 2); } else { borderWidth = (mHeight * Constants.BORDER_THICKNESS); mCellSize = (int)((mHeight - borderWidth) / getRows()); mMatrixWidth = getCols() * mCellSize; mMatrixHeight = mHeight; top = (int)(borderWidth / 2); left = (mWidth - mMatrixWidth) / 2; } // set the bounds for the matrix mMatrixBounds = new RectF( left, top, left + mMatrixWidth, top + mMatrixHeight); // set the bound for the cells for(int c = 0; c < getCols(); c++) { for (int r = 0; r < getRows(); r++) { mCells.get(c).get(r).setBounds(sizeCell(c, r)); } } // set line thickness mBorderPaint.setStrokeWidth((float)Math.max(1, mCellSize * Constants.BORDER_THICKNESS)); } private RectF sizeCell(int c, int r) { return new RectF( (int) mMatrixBounds.left + (c * mCellSize), (int) mMatrixBounds.top + (r * mCellSize), (int) mMatrixBounds.left + (c * mCellSize) + mCellSize, (int) mMatrixBounds.top + (r * mCellSize) + mCellSize); } // called when the control needs to be drawn protected void onDraw(Canvas canvas) { super.onDraw(canvas); // draw matrix for (ArrayList row : mCells) { for (MatrixCell cell : row ) { if (cell.getVisible()) mCellPaint.setColor(cell.getColor()); else mCellPaint.setColor(Color.TRANSPARENT); if (cell.getBorder()) { if (cell.getSquare()) { canvas.drawRect(cell.getInnerBounds(), mCellPaint); canvas.drawRect(cell.getBounds(), mBorderPaint); } else { canvas.drawOval(cell.getInnerBounds(), mCellPaint); canvas.drawOval(cell.getBounds(), mBorderPaint); } } else { if (cell.getSquare()) { canvas.drawRect(cell.getBounds(), mCellPaint); } else { canvas.drawOval(cell.getBounds(), mCellPaint); } } } } // fancy animation stuff, but its a bit weird /*for (Integer pointerId : pointers.keySet()){ MatrixPointer pointer = pointers.get(pointerId); float x = pointer.getX(); float y = pointer.getY(); RectF cellBounds = pointer.getPressedCell().getBounds(); float highlightWidth = cellBounds.width() * 0.1f; //draw a line from the centre of the cell to the position mLinePaint.setColor(pointer.getPressedCell().getMovedColor()); canvas.drawLine(cellBounds.centerX(), cellBounds.centerY(), x, y, mLinePaint); // is pointer inside the pressed cell? //if (cellBounds.contains(x,y)){ // RectF selectedRect = new RectF(x - highlightWidth, y - highlightWidth, x + highlightWidth, y + highlightWidth); // mCellPaint.setColor(pointer.getPressedCell().getMovedColor()); // canvas.drawRect(selectedRect, mCellPaint); //} }*/ } // manage the touch events @Override public boolean onTouchEvent(MotionEvent event) { int pointerIndex, pointerId; MatrixPointer pointer; MatrixCell cell; float x, y; switch(event.getActionMasked()) { case MotionEvent.ACTION_DOWN: // do the perform click performClick(); case MotionEvent.ACTION_POINTER_DOWN: // TODO have a look at the acceleration bit pointerIndex = event.getActionIndex(); x = event.getX(pointerIndex); y = event.getY(pointerIndex); // was it inside the matrix? if (mMatrixBounds.contains(x, y)) { cell = findCellFromXY(x, y); if (cell != null) { // if this cell isnt already pressed, press it if (!cell.getPressed()) { pointerId = event.getPointerId(pointerIndex); pointers.put(pointerId, new MatrixPointer(pointerId, x, y, cell)); cell.press(); if (listener != null) listener.onPress(cell, pointerId, x, y); } } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: pointerIndex = event.getActionIndex(); x = event.getX(pointerIndex); y = event.getY(pointerIndex); pointerId = event.getPointerId(pointerIndex); pointer = pointers.get(pointerId); if (pointer != null) { cell = pointer.getPressedCell(); cell.release(); pointers.remove(pointerId); if (listener != null) listener.onRelease(cell, pointerId, x, y); } break; case MotionEvent.ACTION_MOVE: int numPointers = event.getPointerCount(); for (pointerIndex = 0; pointerIndex < numPointers; pointerIndex++) { //pointerIndex = event.getActionIndex(); x = event.getX(pointerIndex); y = event.getY(pointerIndex); // was it inside the matrix? if (mMatrixBounds.contains(x, y)) { // was it inside the pressed cell? pointerId = event.getPointerId(pointerIndex); pointer = pointers.get(pointerId); if (pointer != null) { // has this pointer moved? if (pointer.getX() != x && pointer.getY() != y) { pointer.move(x, y); // removed - no need to update the cell view when it is moved // pointer.getPressedCell().moved(); if (listener != null) listener.onMove(pointer.getPressedCell(), pointerId, x, y); } } } } break; } return true; } // finds the cell on the matrix from the xy private MatrixCell findCellFromXY(float x, float y) { /*for (ArrayList row : mCells) { for (MatrixCell cell : row) { if (cell.getBounds().contains(x, y)) { return cell } } }*/ int col = (int)(x - mMatrixBounds.left) / mCellSize; int row = (int)(y - mMatrixBounds.top) / mCellSize; if (col < mCols && row < mRows) { return mCells.get(col).get(row); } else { return null; } } @Override public boolean performClick() { super.performClick(); return true; } // updates the matrix, must be called after each update to the matrix to display the changes public void update() { invalidate(); requestLayout(); } // getters and setters public ArrayList> getCells() { return mCells; } public MatrixCell getCell(int col, int row) { return mCells.get(col).get(row); } public void setSize(int cols, int rows) { cols = Math.max(1, cols); rows = Math.max(1, rows); mCols = cols; mRows = rows; setupMatrix(); sizeMatrix(); } public void setColor(int color) { for (ArrayList row : mCells) { for (MatrixCell cell : row ) { cell.setColor(color); } } } public void setVisible(boolean value) { for (ArrayList row : mCells) { for (MatrixCell cell : row ) { cell.setVisible(value); } } } public void setBorder(boolean value) { for (ArrayList row : mCells) { for (MatrixCell cell : row ) { cell.setBorder(value); } } } public void setSquare(boolean value) { for (ArrayList row : mCells) { for (MatrixCell cell : row ) { cell.setSquare(value); } } } public int getCols() { return mCols; } public void setCols(int value) { setSize(value, getRows()); } public int getRows() { return mRows; } public void setRows(int value) { setSize(getCols(), value); } // internal classes // keeps track of a single pointer on the matrix private class MatrixPointer { private int mPointedId; private float mX, mY; private MatrixCell mPressedCell; private MatrixPointer(int pointerId, float x, float y, MatrixCell pressedCell) { mPointedId = pointerId; mX = x; mY = y; mPressedCell = pressedCell; } private void move(float x, float y) { mX = x; mY = y; } private MatrixCell getPressedCell() { return mPressedCell; } private float getX() { return mX; } private float getY() { return mY; } } // represents a cell on the matrix, used to keep track of the state public class MatrixCell { private int mRow, mCol, mCurrentColor, mReleasedColor, mPressedColor, mMovedColor; private RectF mBounds; private boolean mBorder, mPressed, mVisible, mSquare; private MatrixCell(int col, int row, RectF bounds, boolean visible, boolean border, int color, boolean square) { mCol = col; mRow = row; mBounds = bounds; mBorder = border; mPressed = false; mVisible = visible; mSquare = square; updateColors(color); } /*public int getRow() { return mRow; } public int getCol() { return mCol; }*/ public RectF getBounds() { return mBounds; } private void setBounds(RectF value) { mBounds = value; } public RectF getInnerBounds() { float border = (mCellSize * Constants.BORDER_THICKNESS) / 2; return new RectF( mBounds.left + border, mBounds.top + border, mBounds.right - border, mBounds.bottom - border); } public int getColor() { return mCurrentColor; } public int getMovedColor() { return mMovedColor; } public void setColor(int value) { updateColors(value); } public boolean getBorder() { return mBorder; } public void setBorder(boolean value) { mBorder = value; } public boolean getVisible() { return mVisible; } public void setVisible(boolean value) { mVisible = value; } public boolean getSquare() { return mSquare; } public void setSquare(boolean value) { mSquare = value; } public int getCol() { return mCol; } public int getRow() { return mRow; } public float getWidth() { return mBounds.right - mBounds.left; } public float getHeight() { return mBounds.bottom - mBounds.top; } public boolean getPressed() { return mPressed; } // called when the cell is pressed private void press() { mCurrentColor = mPressedColor; mPressed = true; invalidate(); requestLayout(); } // called when the cell is released private void release() { mCurrentColor = mReleasedColor; mPressed = false; invalidate(); requestLayout(); } // called when the cell is moved private void moved() { invalidate(); requestLayout(); } // manages the colors in the cell private void updateColors(int color) { mReleasedColor = color; mPressedColor = manipulateColor(color, 0.85f); mMovedColor = manipulateColor(color, 0.7f); if (mPressed) { mCurrentColor = mPressedColor; } else { mCurrentColor = mReleasedColor; } } // manipulates a color private int manipulateColor(int color, float factor) { int a = Color.alpha(color); int r = Math.round(Color.red(color) * factor); int g = Math.round(Color.green(color) * factor); int b = Math.round(Color.blue(color) * factor); return Color.argb(a, Math.min(r,255), Math.min(g,255), Math.min(b,255)); } } } ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/bluedot/SettingsActivity.java ================================================ package com.stuffaboutcode.bluedot; import android.os.Bundle; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.Preference; import androidx.preference.ListPreference; import androidx.preference.SwitchPreferenceCompat; import android.content.SharedPreferences; public class SettingsActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.settings_activity); getSupportFragmentManager() .beginTransaction() .replace(R.id.settings, new SettingsFragment()) .commit(); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } public static class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener{ @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.root_preferences, rootKey); setupPreferences(); } @Override public void onResume() { super.onResume(); getPreferenceScreen().getSharedPreferences() .registerOnSharedPreferenceChangeListener(this); } @Override public void onPause() { super.onPause(); getPreferenceScreen().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(this); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,String key) { setupPreferences(); } private void setupPreferences() { Preference default_port = findPreference("default_port"); SwitchPreferenceCompat default_port_switch = (SwitchPreferenceCompat)default_port; Preference port = findPreference("port"); ListPreference port_list = (ListPreference) port; if (default_port_switch != null) { if (port_list != null) { // disable the port if the default_port is enabled if (default_port_switch.isChecked()) port.setVisible(false); else port.setVisible(true); // set the port summary port.setSummary(port_list.getEntry()); } } } } } ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/logger/Log.java ================================================ /* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.stuffaboutcode.logger; /** * Helper class for a list (or tree) of LoggerNodes. * *

When this is set as the head of the list, * an instance of it can function as a drop-in replacement for {@link android.util.Log}. * Most of the methods in this class server only to map a method call in Log to its equivalent * in LogNode.

*/ public class Log { // Grabbing the native values from Android's native logging facilities, // to make for easy migration and interop. public static final int NONE = -1; public static final int VERBOSE = android.util.Log.VERBOSE; public static final int DEBUG = android.util.Log.DEBUG; public static final int INFO = android.util.Log.INFO; public static final int WARN = android.util.Log.WARN; public static final int ERROR = android.util.Log.ERROR; public static final int ASSERT = android.util.Log.ASSERT; // Stores the beginning of the LogNode topology. private static LogNode mLogNode; /** * Returns the next LogNode in the linked list. */ public static LogNode getLogNode() { return mLogNode; } /** * Sets the LogNode data will be sent to. */ public static void setLogNode(LogNode node) { mLogNode = node; } /** * Instructs the LogNode to print the log data provided. Other LogNodes can * be chained to the end of the LogNode as desired. * * @param priority Log level of the data being logged. Verbose, Error, etc. * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ public static void println(int priority, String tag, String msg, Throwable tr) { if (mLogNode != null) { mLogNode.println(priority, tag, msg, tr); } } /** * Instructs the LogNode to print the log data provided. Other LogNodes can * be chained to the end of the LogNode as desired. * * @param priority Log level of the data being logged. Verbose, Error, etc. * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. The actual message to be logged. */ public static void println(int priority, String tag, String msg) { println(priority, tag, msg, null); } /** * Prints a message at VERBOSE priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ public static void v(String tag, String msg, Throwable tr) { println(VERBOSE, tag, msg, tr); } /** * Prints a message at VERBOSE priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. */ public static void v(String tag, String msg) { v(tag, msg, null); } /** * Prints a message at DEBUG priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ public static void d(String tag, String msg, Throwable tr) { println(DEBUG, tag, msg, tr); } /** * Prints a message at DEBUG priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. */ public static void d(String tag, String msg) { d(tag, msg, null); } /** * Prints a message at INFO priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ public static void i(String tag, String msg, Throwable tr) { println(INFO, tag, msg, tr); } /** * Prints a message at INFO priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. */ public static void i(String tag, String msg) { i(tag, msg, null); } /** * Prints a message at WARN priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ public static void w(String tag, String msg, Throwable tr) { println(WARN, tag, msg, tr); } /** * Prints a message at WARN priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. */ public static void w(String tag, String msg) { w(tag, msg, null); } /** * Prints a message at WARN priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ public static void w(String tag, Throwable tr) { w(tag, null, tr); } /** * Prints a message at ERROR priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ public static void e(String tag, String msg, Throwable tr) { println(ERROR, tag, msg, tr); } /** * Prints a message at ERROR priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. */ public static void e(String tag, String msg) { e(tag, msg, null); } /** * Prints a message at ASSERT priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ public static void wtf(String tag, String msg, Throwable tr) { println(ASSERT, tag, msg, tr); } /** * Prints a message at ASSERT priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. */ public static void wtf(String tag, String msg) { wtf(tag, msg, null); } /** * Prints a message at ASSERT priority. * * @param tag Tag for for the log data. Can be used to organize log statements. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ public static void wtf(String tag, Throwable tr) { wtf(tag, null, tr); } } ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/logger/LogFragment.java ================================================ /* * Copyright 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Copyright 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.stuffaboutcode.logger; import android.graphics.Typeface; import android.os.Bundle; import androidx.fragment.app.Fragment; import android.text.Editable; import android.text.TextWatcher; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ScrollView; /** * Simple fraggment which contains a LogView and uses is to output log data it receives * through the LogNode interface. */ public class LogFragment extends Fragment { private LogView mLogView; private ScrollView mScrollView; public LogFragment() {} public View inflateViews() { mScrollView = new ScrollView(getActivity()); ViewGroup.LayoutParams scrollParams = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); mScrollView.setLayoutParams(scrollParams); mLogView = new LogView(getActivity()); ViewGroup.LayoutParams logParams = new ViewGroup.LayoutParams(scrollParams); logParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; mLogView.setLayoutParams(logParams); mLogView.setClickable(true); mLogView.setFocusable(true); mLogView.setTypeface(Typeface.MONOSPACE); // Want to set padding as 16 dips, setPadding takes pixels. Hooray math! int paddingDips = 16; double scale = getResources().getDisplayMetrics().density; int paddingPixels = (int) ((paddingDips * (scale)) + .5); mLogView.setPadding(paddingPixels, paddingPixels, paddingPixels, paddingPixels); mLogView.setCompoundDrawablePadding(paddingPixels); mLogView.setGravity(Gravity.BOTTOM); mLogView.setTextAppearance(getActivity(), android.R.style.TextAppearance_Holo_Medium); mScrollView.addView(mLogView); return mScrollView; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View result = inflateViews(); mLogView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} @Override public void afterTextChanged(Editable s) { mScrollView.fullScroll(ScrollView.FOCUS_DOWN); } }); return result; } public LogView getLogView() { return mLogView; } } ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/logger/LogNode.java ================================================ /* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.stuffaboutcode.logger; /** * Basic interface for a logging system that can output to one or more targets. * Note that in addition to classes that will output these logs in some format, * one can also implement this interface over a filter and insert that in the chain, * such that no targets further down see certain data, or see manipulated forms of the data. * You could, for instance, write a "ToHtmlLoggerNode" that just converted all the log data * it received to HTML and sent it along to the next node in the chain, without printing it * anywhere. */ public interface LogNode { /** * Instructs first LogNode in the list to print the log data provided. * @param priority Log level of the data being logged. Verbose, Error, etc. * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. The actual message to be logged. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ public void println(int priority, String tag, String msg, Throwable tr); } ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/logger/LogView.java ================================================ /* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.stuffaboutcode.logger; import android.app.Activity; import android.content.Context; import android.util.*; import android.widget.TextView; /** Simple TextView which is used to output log data received through the LogNode interface. */ public class LogView extends TextView implements LogNode { public LogView(Context context) { super(context); } public LogView(Context context, AttributeSet attrs) { super(context, attrs); } public LogView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } /** * Formats the log data and prints it out to the LogView. * @param priority Log level of the data being logged. Verbose, Error, etc. * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. The actual message to be logged. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ @Override public void println(int priority, String tag, String msg, Throwable tr) { String priorityStr = null; // For the purposes of this View, we want to print the priority as readable text. switch(priority) { case android.util.Log.VERBOSE: priorityStr = "VERBOSE"; break; case android.util.Log.DEBUG: priorityStr = "DEBUG"; break; case android.util.Log.INFO: priorityStr = "INFO"; break; case android.util.Log.WARN: priorityStr = "WARN"; break; case android.util.Log.ERROR: priorityStr = "ERROR"; break; case android.util.Log.ASSERT: priorityStr = "ASSERT"; break; default: break; } // Handily, the Log class has a facility for converting a stack trace into a usable string. String exceptionStr = null; if (tr != null) { exceptionStr = android.util.Log.getStackTraceString(tr); } // Take the priority, tag, message, and exception, and concatenate as necessary // into one usable line of text. final StringBuilder outputBuilder = new StringBuilder(); String delimiter = "\t"; appendIfNotNull(outputBuilder, priorityStr, delimiter); appendIfNotNull(outputBuilder, tag, delimiter); appendIfNotNull(outputBuilder, msg, delimiter); appendIfNotNull(outputBuilder, exceptionStr, delimiter); // In case this was originally called from an AsyncTask or some other off-UI thread, // make sure the update occurs within the UI thread. ((Activity) getContext()).runOnUiThread( (new Thread(new Runnable() { @Override public void run() { // Display the text we just generated within the LogView. appendToLog(outputBuilder.toString()); } }))); if (mNext != null) { mNext.println(priority, tag, msg, tr); } } public LogNode getNext() { return mNext; } public void setNext(LogNode node) { mNext = node; } /** Takes a string and adds to it, with a separator, if the bit to be added isn't null. Since * the logger takes so many arguments that might be null, this method helps cut out some of the * agonizing tedium of writing the same 3 lines over and over. * @param source StringBuilder containing the text to append to. * @param addStr The String to append * @param delimiter The String to separate the source and appended strings. A tab or comma, * for instance. * @return The fully concatenated String as a StringBuilder */ private StringBuilder appendIfNotNull(StringBuilder source, String addStr, String delimiter) { if (addStr != null) { if (addStr.length() == 0) { delimiter = ""; } return source.append(addStr).append(delimiter); } return source; } // The next LogNode in the chain. LogNode mNext; /** Outputs the string as a new line of log data in the LogView. */ public void appendToLog(String s) { append("\n" + s); } } ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/logger/LogWrapper.java ================================================ /* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.stuffaboutcode.logger; import android.util.Log; /** * Helper class which wraps Android's native Log utility in the Logger interface. This way * normal DDMS output can be one of the many targets receiving and outputting logs simultaneously. */ public class LogWrapper implements LogNode { // For piping: The next node to receive Log data after this one has done its work. private LogNode mNext; /** * Returns the next LogNode in the linked list. */ public LogNode getNext() { return mNext; } /** * Sets the LogNode data will be sent to.. */ public void setNext(LogNode node) { mNext = node; } /** * Prints data out to the console using Android's native log mechanism. * @param priority Log level of the data being logged. Verbose, Error, etc. * @param tag Tag for for the log data. Can be used to organize log statements. * @param msg The actual message to be logged. The actual message to be logged. * @param tr If an exception was thrown, this can be sent along for the logging facilities * to extract and print useful information. */ @Override public void println(int priority, String tag, String msg, Throwable tr) { // There actually are log methods that don't take a msg parameter. For now, // if that's the case, just convert null to the empty string and move on. String useMsg = msg; if (useMsg == null) { useMsg = ""; } // If an exeption was provided, convert that exception to a usable string and attach // it to the end of the msg method. if (tr != null) { msg += "\n" + Log.getStackTraceString(tr); } // This is functionally identical to Log.x(tag, useMsg); // For instance, if priority were Log.VERBOSE, this would be the same as Log.v(tag, useMsg) Log.println(priority, tag, useMsg); // If this isn't the last node in the chain, move things along. if (mNext != null) { mNext.println(priority, tag, msg, tr); } } } ================================================ FILE: clients/android/app/src/main/java/com/stuffaboutcode/logger/MessageOnlyLogFilter.java ================================================ /* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.stuffaboutcode.logger; /** * Simple {@link LogNode} filter, removes everything except the message. * Useful for situations like on-screen log output where you don't want a lot of metadata displayed, * just easy-to-read message updates as they're happening. */ public class MessageOnlyLogFilter implements LogNode { LogNode mNext; /** * Takes the "next" LogNode as a parameter, to simplify chaining. * * @param next The next LogNode in the pipeline. */ public MessageOnlyLogFilter(LogNode next) { mNext = next; } public MessageOnlyLogFilter() { } @Override public void println(int priority, String tag, String msg, Throwable tr) { if (mNext != null) { getNext().println(Log.NONE, null, msg, null); } } /** * Returns the next LogNode in the chain. */ public LogNode getNext() { return mNext; } /** * Sets the LogNode data will be sent to.. */ public void setNext(LogNode node) { mNext = node; } } ================================================ FILE: clients/android/app/src/main/res/drawable/round_button.xml ================================================ ================================================ FILE: clients/android/app/src/main/res/layout/activity_button.xml ================================================ ================================================ FILE: clients/android/app/src/main/res/layout/activity_devices.xml ================================================ ================================================ FILE: clients/android/app/src/main/res/layout/settings_activity.xml ================================================ ================================================ FILE: clients/android/app/src/main/res/menu/settings_menu.xml ================================================ ================================================ FILE: clients/android/app/src/main/res/values/arrays.xml ================================================ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ================================================ FILE: clients/android/app/src/main/res/values/attrs.xml ================================================ ================================================ FILE: clients/android/app/src/main/res/values/colors.xml ================================================ #3F51B5 #303F9F #FF4081 #FFFFFF #FF0000 #0000FF #6d6d6e #0000FF ================================================ FILE: clients/android/app/src/main/res/values/dimens.xml ================================================ 16dp ================================================ FILE: clients/android/app/src/main/res/values/strings.xml ================================================ BlueButton Button Settings Messages Sync Your signature Default reply action Sync email periodically Download incoming attachments Automatically download attachments for incoming emails Only download attachments when manually requested ================================================ FILE: clients/android/app/src/main/res/values/styles.xml ================================================