diff --git a/pwnagotchi/bettercap.py b/pwnagotchi/bettercap.py index def188021..16ce07ba4 100644 --- a/pwnagotchi/bettercap.py +++ b/pwnagotchi/bettercap.py @@ -29,7 +29,7 @@ def decode(r, verbose_errors=True): err = "error %d: %s" % (r.status_code, r.text.strip()) if verbose_errors: logging.info(err) - raise Exception(err) + #raise Exception(err) return r.text diff --git a/pwnagotchi/cli.py b/pwnagotchi/cli.py index b2b1bb124..8470ba07d 100644 --- a/pwnagotchi/cli.py +++ b/pwnagotchi/cli.py @@ -48,7 +48,8 @@ def do_auto_mode(agent): logging.info("entering auto mode ...") agent.mode = 'auto' - agent.last_session.parse(agent.view(), args.skip_session) # show stats in AUTO + if agent.config().get('STATS_ON_AUTO', False): + agent.last_session.parse(agent.view(), args.skip_session) # show stats in AUTO agent.start() while True: diff --git a/pwnagotchi/log.py b/pwnagotchi/log.py index bd55ff6c3..4ea046b10 100644 --- a/pwnagotchi/log.py +++ b/pwnagotchi/log.py @@ -2,11 +2,15 @@ import time import re import os +import sys import logging +import logging.handlers +import syslog import shutil import gzip import warnings from datetime import datetime +from dateutil import parser as date_parser from pwnagotchi.voice import Voice from pwnagotchi.mesh.peer import Peer @@ -30,6 +34,7 @@ def __init__(self, config): self.config = config self.voice = Voice(lang=config['main']['lang']) self.path = config['main']['log']['path'] + self.log_type = config['main']['log']['type'].lower() self.last_session = [] self.last_session_id = '' self.last_saved_session_id = '' @@ -64,10 +69,19 @@ def save_session_id(self): self.last_saved_session_id = self.last_session_id def _parse_datetime(self, dt): - dt = dt.split('.')[0] - dt = dt.split(',')[0] - dt = datetime.strptime(dt.split('.')[0], '%Y-%m-%d %H:%M:%S') - return time.mktime(dt.timetuple()) + try: + return time.mktime(date_parser.parse(dt)) + except ValueError as ve: + logging.debug("%s: %s %s" % (dt,ve, sys.exc_info())) + dt = dt.split('.')[0] + dt = dt.split(',')[0] + try: + dt = datetime.strptime(dt.split('.')[0], '%Y-%m-%d %H:%M:%S') + return time.mktime(dt.timetuple()) + except Exception as e: + # not a date + logging.debug("%s: %s" % (sys.exc_info(), e)) + return time.time() def _parse_stats(self): self.duration = '' @@ -91,7 +105,6 @@ def _parse_stats(self): parts = line.split(']') if len(parts) < 2: continue - try: line_timestamp = parts[0].strip('[') line = ']'.join(parts[1:]) @@ -173,6 +186,8 @@ def _parse_stats(self): def parse(self, ui, skip=False): if skip: logging.debug("skipping parsing of the last session logs ...") + elif self.log_type in ['syslog', 'console']: + logging.info("skipping last session stats with %s logging" % self.log_type) else: logging.debug("reading last session logs ...") @@ -220,41 +235,54 @@ def setup_logging(args, config): filenameDebug = cfg['path-debug'] #global formatter + formatter = logging.Formatter(cfg.get("format", "[%(asctime)s] [%(levelname)s] [%(threadName)s] : %(message)s")) + print("Format: %s" % cfg.get("format","")) + formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] [%(threadName)s] : %(message)s") + logger = logging.getLogger() + + dbg = args.debug or cfg.get('debug', False) for handler in logger.handlers: - handler.setLevel(logging.DEBUG if args.debug else logging.INFO) + handler.setLevel(logging.DEBUG if dbg else logging.INFO) handler.setFormatter(formatter) - - - logger.setLevel(logging.DEBUG if args.debug else logging.INFO) - if filename: - # since python default log rotation might break session data in different files, - # we need to do log rotation ourselves - log_rotation(filename, cfg) - log_rotation(filenameDebug, cfg) - - - # File handler for logging all normal messages - file_handler = logging.FileHandler(filename) #creates new - file_handler.setLevel(logging.INFO) - file_handler.setFormatter(formatter) - logger.addHandler(file_handler) - - # File handler for logging all debug messages - file_handler = logging.FileHandler(filenameDebug) #creates new - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(formatter) - logger.addHandler(file_handler) - - # Console handler for logging debug messages if args.debug is true else just log normal - #console_handler = logging.StreamHandler() #creates new - #console_handler.setLevel(logging.DEBUG if args.debug else logging.INFO) - #console_handler.setFormatter(formatter) - #logger.addHandler(console_handler) + logger.setLevel(logging.DEBUG if dbg else logging.INFO) + log_type = cfg.get("type", "files").strip().lower() # files, syslog or console + + if log_type == "syslog": + syslog_handler = logging.handlers.SysLogHandler(address='/dev/log', facility=syslog.LOG_USER) + syslog_handler.setLevel(logging.DEBUG if dbg else logging.INFO) + syslog_handler.setFormatter(formatter) + logger.addHandler(syslog_handler) + + elif log_type == "console": + # Console handler for logging debug messages if args.debug is true else just log normal + console_handler = logging.StreamHandler() #creates new + console_handler.setLevel(logging.DEBUG if dbg else logging.INFO) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + else: # default is using file and debug file + if filename: + # since python default log rotation might break session data in different files, + # we need to do log rotation ourselves + log_rotation(filename, cfg) + # File handler for logging all normal messages + file_handler = logging.FileHandler(filename) #creates new + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + if filenameDebug: + log_rotation(filenameDebug, cfg) + # File handler for logging all debug messages + file_handler = logging.FileHandler(filenameDebug) #creates new + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) if not args.debug: # disable scapy and tensorflow logging diff --git a/pwnagotchi/plugins/__init__.py b/pwnagotchi/plugins/__init__.py index 767982606..ea2ce28b1 100644 --- a/pwnagotchi/plugins/__init__.py +++ b/pwnagotchi/plugins/__init__.py @@ -47,7 +47,8 @@ def __init__(self, plugin_name): def __del__(self): self.keep_going = False - self._worker_thread.join() + if self._worker_thread: + self._worker_thread.join() if self.load_handler: self.load_handler.join() diff --git a/pwnagotchi/plugins/default/pwncrack.py b/pwnagotchi/plugins/default/pwncrack.py index 64ef5893a..b182c5ce2 100644 --- a/pwnagotchi/plugins/default/pwncrack.py +++ b/pwnagotchi/plugins/default/pwncrack.py @@ -15,12 +15,12 @@ class UploadConvertPlugin(Plugin): def __init__(self): self.server_url = 'http://pwncrack.org/upload_handshake' # Leave this as is self.potfile_url = 'http://pwncrack.org/download_potfile_script' # Leave this as is - self.timewait = 600 + self.timewait = 30 self.last_run_time = 0 self.options = dict() def on_loaded(self): - logging.info('[pwncrack] loading') + logging.info('[pwncrack] loading@@') def on_config_changed(self, config): self.handshake_dir = config["bettercap"].get("handshakes") @@ -28,14 +28,19 @@ def on_config_changed(self, config): self.whitelist = config["main"].get("whitelist", []) self.combined_file = os.path.join(self.handshake_dir, 'combined.hc22000') self.potfile_path = os.path.join(self.handshake_dir, 'cracked.pwncrack.potfile') + self.last_upload_path = os.path.join(self.handshake_dir, '.pwncrack_last_up') def on_internet_available(self, agent): current_time = time.time() remaining_wait_time = self.timewait - (current_time - self.last_run_time) if remaining_wait_time > 0: - logging.debug(f"[pwncrack] Waiting {remaining_wait_time:.1f} more seconds before next run.") + logging.info(f"[pwncrack] Waiting {remaining_wait_time:.1f} more seconds before next run.") return self.last_run_time = current_time + + if self.key == "": + logging.warn("PWNCrack enabled, but no api key specified. Add a key to config.toml") + return logging.info(f"[pwncrack] Running upload process. Key: {self.key}, waiting: {self.timewait} seconds.") try: self._convert_and_upload() @@ -43,10 +48,17 @@ def on_internet_available(self, agent): except Exception as e: logging.error(f"[pwncrack] Error occurred during upload process: {e}", exc_info=True) + def on_ready(self, agent): + self.on_internet_available(agent) + def _convert_and_upload(self): # Convert all .pcap files to .hc22000, excluding files matching whitelist items + last_up_time = os.path.getmtime(self.last_upload_path) if os.path.isfile(self.last_upload_path) else 0 + pcap_files = [f for f in os.listdir(self.handshake_dir) - if f.endswith('.pcap') and not any(item in f for item in self.whitelist)] + if f.endswith('.pcap') and os.path.getmtime(os.path.join(self.handshake_dir,f)) > last_up_time and not any(item in f for item in self.whitelist)] + logging.info("Doing %s -> %s" % (self.whitelist, pcap_files)) + if pcap_files: for pcap_file in pcap_files: subprocess.run(['hcxpcapngtool', '-o', self.combined_file, os.path.join(self.handshake_dir, pcap_file)]) @@ -60,9 +72,10 @@ def _convert_and_upload(self): files = {'handshake': file} data = {'key': self.key} response = requests.post(self.server_url, files=files, data=data) - - # Log the response - logging.info(f"[pwncrack] Upload response: {response.json()}") + # Log the response + logging.info(f"[pwncrack] Upload response({response.status_code}): {response.json()}") + with open(self.last_upload_path, 'w') as fout: + fout.write("\n".join(pcap_files)) os.remove(self.combined_file) # Remove the combined.hc22000 file else: logging.info("[pwncrack] No .pcap files found to convert (or all files are whitelisted).") @@ -74,8 +87,7 @@ def _download_potfile(self): file.write(response.text) logging.info(f"[pwncrack] Potfile downloaded to {self.potfile_path}") else: - logging.error(f"[pwncrack] Failed to download potfile: {response.status_code}") - logging.error(f"[pwncrack] {response.json()}") # Log the error message from the server + logging.error(f"[pwncrack] Failed to download potfile ({response.status_code}): {response.json()}") def on_unload(self, ui): logging.info('[pwncrack] unloading') diff --git a/pwnagotchi/ui/components.py b/pwnagotchi/ui/components.py index 8537cddf8..900b49a34 100644 --- a/pwnagotchi/ui/components.py +++ b/pwnagotchi/ui/components.py @@ -1,15 +1,56 @@ +import logging from PIL import Image, ImageOps from textwrap import TextWrapper class Widget(object): - def __init__(self, xy, color=0): + def __init__(self, xy, color=0, bgcolor="white", click_url=None): self.xy = xy self.color = color + self.bgcolor = bgcolor + self.click_url = click_url + + def set_click_url(self, url): + self.click_url = url + + def get_click_url(self): + return self.click_url def draw(self, canvas, drawer): raise Exception("not implemented") + def setColor(self, color): + self.color = color + + def setBackground(self, color): + self.bgcolor = color + + def get_text_box(self, text, font): + w = 0 + h = 0 + for li in text.split("\n"): + li = li.strip() + met = font.getmetrics() + bbox = font.getbbox(li) + w = max(w, bbox[2]) + h += met[0] + met[1] + return (0, 0, w, h) + + def get_bb(self): + if len(self.xy) == 4: + return self.xy + elif len(self.xy) == 2: # upper left + if hasattr(self, 'image') and self.image: + w,h = self.image.size + return (self.xy[0], self.xy[1], self.xy[0]+w, self.xy[1]+h) + elif hasattr(self, 'value') and hasattr(self, 'font'): + # split value into lines and figure it out + bb = self.get_text_box(self.value, self.font) + return (self.xy[0], self.xy[1], self.xy[0] + bb[2], self.xy[1] + bb[3]) + else: + bb = self.get_text_box(self.value) + return (self.xy[0], self.xy[1], self.xy[0] + bb[2], self.xy[1] + bb[3]) + # canvas.paste: https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.paste # takes mask variable, to identify color system. (not used for pwnagotchi yet) # Pwn should use "1" since its mainly black or white displays. @@ -44,14 +85,20 @@ def draw(self, canvas, drawer): class Text(Widget): - def __init__(self, value="", position=(0, 0), font=None, color=0, wrap=False, max_length=0, png=False): - super().__init__(position, color) + def __init__(self, value="", position=(0, 0), font=None, color=0, bgcolor="white", wrap=False, max_length=0, png=False, scale=1, colorize=True): + super().__init__(position, color, bgcolor) self.value = value self.font = font self.wrap = wrap self.max_length = max_length self.wrapper = TextWrapper(width=self.max_length, replace_whitespace=False) if wrap else None self.png = png + self.scale = scale + self.colorize = colorize + + self.image = None + self.offsets = (0,0) + self.last_file = None def draw(self, canvas, drawer): if self.value is not None: @@ -62,20 +109,45 @@ def draw(self, canvas, drawer): text = self.value drawer.text(self.xy, text, font=self.font, fill=self.color) else: - self.image = Image.open(self.value) - self.image = self.image.convert('RGBA') - self.pixels = self.image.load() - for y in range(self.image.size[1]): - for x in range(self.image.size[0]): - if self.pixels[x,y][3] < 255: # check alpha - self.pixels[x,y] = (255, 255, 255, 255) - if self.color == 255: - self._image = ImageOps.colorize(self.image.convert('L'), black = "white", white = "black") - else: - self._image = self.image - self.image = self._image.convert('1') - canvas.paste(self.image, self.xy) - + ox, oy = self.offsets + try: + if self.value != self.last_file: + image = Image.open(self.value) + image = image.convert('RGBA') + pixels = image.load() + for y in range(image.size[1]): + for x in range(image.size[0]): + if pixels[x,y][3] < 255: # check alpha + pixels[x,y] = (255, 255, 255, 255) + self.raw_image = image.copy() + else: + logging.debug("Not reloading same image") + image = self.raw_image.copy() + + if self.colorize: + logging.debug("Colorizing %s from (%s, %s)" % (self.value, self.color, self.bgcolor)) + image = ImageOps.colorize(image.convert('L'), black = self.color, white = self.bgcolor) + if len(self.xy) > 2: + iw,ih = image.size + bw,bh = (self.xy[2]-self.xy[0], self.xy[3]-self.xy[1]) + sc = min(float(bw/iw), float(bh/ih)) + nw = int(iw * sc) + nh = int(ih * sc) + ox = int((bw-nw)/2) + oy = int((bh-nh)/2) + image = image.resize((nw,nh), Image.NEAREST) + self.offsets = [ox,oy] + logging.debug("Offsets %s" % (self.offsets)) + elif self.scale != 1.0: + new_w = int(image.size[0]*self.scale) + new_h = int(image.size[1]*self.scale) + image = image.resize((new_w, new_h), Image.NEAREST) + self.image = image.convert(canvas.mode) + self.last_file = self.value + except Exception as e: + logging.exception("%s: %s" % (self.value, e)) + if self.image: + canvas.paste(self.image, (self.xy[0]+self.offsets[0], self.xy[1]+self.offsets[1])) class LabeledValue(Widget): def __init__(self, label, value="", position=(0, 0), label_font=None, text_font=None, color=0, label_spacing=5): @@ -93,3 +165,14 @@ def draw(self, canvas, drawer): pos = self.xy drawer.text(pos, self.label, font=self.label_font, fill=self.color) drawer.text((pos[0] + self.label_spacing + 5 * len(self.label), pos[1]), self.value, font=self.text_font, fill=self.color) + + def get_bb(self): + w = self.label_spacing + h = 0 + bb = self.get_text_box(self.label, self.label_font) + w += bb[2] + h = bb[3] + + bb = self.get_text_box(self.value, self.text_font) + w += bb[2] + h = max(h, bb[3]) diff --git a/pwnagotchi/ui/display.py b/pwnagotchi/ui/display.py index cb29a6b88..534c79009 100644 --- a/pwnagotchi/ui/display.py +++ b/pwnagotchi/ui/display.py @@ -257,6 +257,9 @@ def is_spotpear24inch(self): def is_spotpear154lcd(self): return self._implementation.name == 'spotpear154lcd' + def is_ST7789(self): + return self._implementation.name == 'st7789' + def is_displayhatmini(self): return self._implementation.name == 'displayhatmini' @@ -319,7 +322,7 @@ def clear(self): def image(self): img = None if self._canvas is not None: - img = self._canvas if self._rotation == 0 else self._canvas.rotate(-self._rotation) + img = self._canvas if self._rotation == 0 else self._canvas.rotate(-self._rotation, expand=True) return img def _render_thread(self): @@ -338,7 +341,7 @@ def _on_view_rendered(self, img): logging.error("%s" % e) if self._enabled: - self._canvas = (img if self._rotation == 0 else img.rotate(self._rotation)) + self._canvas = (img if self._rotation == 0 else img.rotate(self._rotation, expand=True)) if self._implementation is not None: self._canvas_next = self._canvas self._canvas_next_event.set() diff --git a/pwnagotchi/ui/hw/__init__.py b/pwnagotchi/ui/hw/__init__.py index f6b9ddad9..2010a7529 100644 --- a/pwnagotchi/ui/hw/__init__.py +++ b/pwnagotchi/ui/hw/__init__.py @@ -1,4 +1,7 @@ +import logging + def display_for(config): + logging.warn("\t\t\t%s" % (config['ui']['display'])) # config has been normalized already in utils.load_config if config['ui']['display']['type'] == 'inky': from pwnagotchi.ui.hw.inky import Inky @@ -92,6 +95,14 @@ def display_for(config): from pwnagotchi.ui.hw.spotpear154lcd import Spotpear154lcd return Spotpear154lcd(config) + elif config['ui']['display']['type'] == 'st7789': + try: + from pwnagotchi.ui.hw.st7789 import st7789_display + + return st7789_display(config) + except Exception as e: + logging.exception(e) + elif config['ui']['display']['type'] == 'displayhatmini': from pwnagotchi.ui.hw.displayhatmini import DisplayHatMini return DisplayHatMini(config) diff --git a/pwnagotchi/ui/hw/displayhatmini.py b/pwnagotchi/ui/hw/displayhatmini.py index ba9cbb8af..6c3ccd789 100644 --- a/pwnagotchi/ui/hw/displayhatmini.py +++ b/pwnagotchi/ui/hw/displayhatmini.py @@ -1,11 +1,14 @@ import logging import pwnagotchi.ui.fonts as fonts -from pwnagotchi.ui.hw.base import DisplayImpl +from pwnagotchi.ui.hw.st7789 import st7789_display -class DisplayHatMini(DisplayImpl): +class DisplayHatMini(st7789_display): def __init__(self, config): super(DisplayHatMini, self).__init__(config, 'displayhatmini') self._display = None + self.defaults['cs'] = 1 + self.defaults['dc'] = 9 + self.defaults['bl'] = 13 def layout(self): fonts.setup(12, 10, 12, 70, 25, 9) @@ -28,18 +31,3 @@ def layout(self): 'max': 20 } return self._layout - - def initialize(self): - logging.info("Initializing Display Hat Mini") - logging.info("Available pins for GPIO Buttons A/B/X/Y: 5, 6, 16, 24") - logging.info("Available pins for RGB Led: 17, 27, 22") - logging.info("Backlight pin available on GPIO 13") - logging.info("I2C bus available on stemma QT and Breakout Garden headers") - from pwnagotchi.ui.hw.libs.pimoroni.displayhatmini.ST7789 import ST7789 - self._display = ST7789(0,1,9,13) - - def render(self, canvas): - self._display.display(canvas) - - def clear(self): - self._display.clear() diff --git a/pwnagotchi/ui/hw/libs/ST7789.py b/pwnagotchi/ui/hw/libs/ST7789.py new file mode 100644 index 000000000..26ebc7c89 --- /dev/null +++ b/pwnagotchi/ui/hw/libs/ST7789.py @@ -0,0 +1,385 @@ +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola +# +# 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. +import numbers +import time +import numpy as np + +import spidev +import RPi.GPIO as GPIO + +import logging + + +__version__ = '0.0.4' + +BG_SPI_CS_BACK = 0 +BG_SPI_CS_FRONT = 1 + +SPI_CLOCK_HZ = 16000000 + +ST7789_NOP = 0x00 +ST7789_SWRESET = 0x01 +ST7789_RDDID = 0x04 +ST7789_RDDST = 0x09 + +ST7789_SLPIN = 0x10 +ST7789_SLPOUT = 0x11 +ST7789_PTLON = 0x12 +ST7789_NORON = 0x13 + +ST7789_INVOFF = 0x20 +ST7789_INVON = 0x21 +ST7789_DISPOFF = 0x28 +ST7789_DISPON = 0x29 + +ST7789_CASET = 0x2A +ST7789_RASET = 0x2B +ST7789_RAMWR = 0x2C +ST7789_RAMRD = 0x2E + +ST7789_PTLAR = 0x30 +ST7789_MADCTL = 0x36 +ST7789_COLMOD = 0x3A + +ST7789_FRMCTR1 = 0xB1 +ST7789_FRMCTR2 = 0xB2 +ST7789_FRMCTR3 = 0xB3 +ST7789_INVCTR = 0xB4 +ST7789_DISSET5 = 0xB6 + +ST7789_GCTRL = 0xB7 +ST7789_GTADJ = 0xB8 +ST7789_VCOMS = 0xBB + +ST7789_LCMCTRL = 0xC0 +ST7789_IDSET = 0xC1 +ST7789_VDVVRHEN = 0xC2 +ST7789_VRHS = 0xC3 +ST7789_VDVS = 0xC4 +ST7789_VMCTR1 = 0xC5 +ST7789_FRCTRL2 = 0xC6 +ST7789_CABCCTRL = 0xC7 + +ST7789_RDID1 = 0xDA +ST7789_RDID2 = 0xDB +ST7789_RDID3 = 0xDC +ST7789_RDID4 = 0xDD + +ST7789_GMCTRP1 = 0xE0 +ST7789_GMCTRN1 = 0xE1 + +ST7789_PWCTR6 = 0xFC + + +class ST7789(object): + """Representation of an ST7789 TFT LCD.""" + + def __init__(self, port, cs, dc, backlight, rst=None, width=240, + height=240, rotation=90, invert=True, spi_speed_hz=60 * 1000 * 1000, + offset_left=0, + offset_top=0, + backlight_pwm=0): + """Create an instance of the display using SPI communication. + + Must provide the GPIO pin number for the D/C pin and the SPI driver. + + Can optionally provide the GPIO pin number for the reset pin as the rst parameter. + + :param port: SPI port number + :param cs: SPI chip-select number (0 or 1 for BCM + :param backlight: Pin for controlling backlight + :param rst: Reset pin for ST7789 + :param width: Width of display connected to ST7789 + :param height: Height of display connected to ST7789 + :param rotation: Rotation of display connected to ST7789 + :param invert: Invert display + :param backlight_pwm: 0 = off, >0 == pwm frequency + :param spi_speed_hz: SPI speed (in Hz) + + """ + if rotation not in [0, 90, 180, 270]: + raise ValueError("Invalid rotation {}".format(rotation)) + + #if width != height and rotation in [90, 270]: + # raise ValueError("Invalid rotation {} for {}x{} resolution".format(rotation, width, height)) + + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + + self._spi = spidev.SpiDev(port, cs) + self._spi.mode = 0 + self._spi.lsbfirst = False + self._spi.max_speed_hz = spi_speed_hz + + self._dc = dc + self._rst = rst + self._width = width + self._height = height + self._rotation = rotation + self._invert = invert + + self._offset_left = offset_left + self._offset_top = offset_top + + # Set DC as output. + GPIO.setup(dc, GPIO.OUT) + + # Setup backlight as output (if provided). + self._backlight = backlight + self._brightness = 0 + if backlight is not None: + try: + GPIO.setup(backlight, GPIO.OUT) + if backlight_pwm: + from RPi.GPIO import PWM + self._bl_pwm = PWM(backlight, backlight_pwm) + self._brightness = 100 + self._bl_pwm.start(self._brightness) + else: + self._backlight_pwm = None + GPIO.output(backlight, GPIO.LOW) + time.sleep(0.1) + GPIO.output(backlight, GPIO.HIGH) + self._brightness = 100 + except Exception as e: + logging.exception(e) + # Setup reset as output (if provided). + if rst is not None: + GPIO.setup(self._rst, GPIO.OUT) + self.reset() + self._init() + + def send(self, data, is_data=True, chunk_size=4096): + """Write a byte or array of bytes to the display. Is_data parameter + controls if byte should be interpreted as display data (True) or command + data (False). Chunk_size is an optional size of bytes to write in a + single SPI transaction, with a default of 4096. + """ + # Set DC low for command, high for data. + GPIO.output(self._dc, is_data) + # Convert scalar argument to list so either can be passed as parameter. + if isinstance(data, numbers.Number): + data = [data & 0xFF] + # Write data a chunk at a time. + for start in range(0, len(data), chunk_size): + end = min(start + chunk_size, len(data)) + self._spi.xfer(data[start:end]) + + def set_backlight(self, value): + """Set the backlight on/off.""" + if self._backlight is not None: + if self._bl_pwm: + self._bl_pwm.ChangeDutyCycle(int(value * 100)) + else: + if value < 1: + value = GPIO.HIGH if value > 0.3 else GPIO.LOW + GPIO.output(self._backlight, value) + self._brightness = value + + def get_backlight(self): + if self._backlight is not None: + return self._brightness + + @property + def width(self): + return self._width if self._rotation == 0 or self._rotation == 180 else self._height + + @property + def height(self): + return self._height if self._rotation == 0 or self._rotation == 180 else self._width + + def command(self, data): + """Write a byte or array of bytes to the display as command data.""" + self.send(data, False) + + def data(self, data): + """Write a byte or array of bytes to the display as display data.""" + self.send(data, True) + + def reset(self): + """Reset the display, if reset pin is connected.""" + if self._rst is not None: + GPIO.output(self._rst, 1) + time.sleep(0.500) + GPIO.output(self._rst, 0) + time.sleep(0.500) + GPIO.output(self._rst, 1) + time.sleep(0.500) + + def _init(self): + # Initialize the display. + + self.command(ST7789_SWRESET) # Software reset + time.sleep(0.150) # delay 150 ms + + self.command(ST7789_MADCTL) + self.data(0x70) + + self.command(ST7789_FRMCTR2) # Frame rate ctrl - idle mode + self.data(0x0C) + self.data(0x0C) + self.data(0x00) + self.data(0x33) + self.data(0x33) + + self.command(ST7789_COLMOD) + self.data(0x05) + + self.command(ST7789_GCTRL) + self.data(0x14) + + self.command(ST7789_VCOMS) + self.data(0x37) + + self.command(ST7789_LCMCTRL) # Power control + self.data(0x2C) + + self.command(ST7789_VDVVRHEN) # Power control + self.data(0x01) + + self.command(ST7789_VRHS) # Power control + self.data(0x12) + + self.command(ST7789_VDVS) # Power control + self.data(0x20) + + self.command(0xD0) + self.data(0xA4) + self.data(0xA1) + + self.command(ST7789_FRCTRL2) + self.data(0x0F) + + self.command(ST7789_GMCTRP1) # Set Gamma + self.data(0xD0) + self.data(0x04) + self.data(0x0D) + self.data(0x11) + self.data(0x13) + self.data(0x2B) + self.data(0x3F) + self.data(0x54) + self.data(0x4C) + self.data(0x18) + self.data(0x0D) + self.data(0x0B) + self.data(0x1F) + self.data(0x23) + + self.command(ST7789_GMCTRN1) # Set Gamma + self.data(0xD0) + self.data(0x04) + self.data(0x0C) + self.data(0x11) + self.data(0x13) + self.data(0x2C) + self.data(0x3F) + self.data(0x44) + self.data(0x51) + self.data(0x2F) + self.data(0x1F) + self.data(0x1F) + self.data(0x20) + self.data(0x23) + + if self._invert: + self.command(ST7789_INVON) # Invert display + else: + self.command(ST7789_INVOFF) # Don't invert display + + self.command(ST7789_SLPOUT) + + self.command(ST7789_DISPON) # Display on + time.sleep(0.100) # 100 ms + + def begin(self): + """Set up the display + + Deprecated. Included in __init__. + + """ + pass + + def set_window(self, x0=0, y0=0, x1=None, y1=None): + """Set the pixel address window for proceeding drawing commands. x0 and + x1 should define the minimum and maximum x pixel bounds. y0 and y1 + should define the minimum and maximum y pixel bound. If no parameters + are specified the default will be to update the entire display from 0,0 + to width-1,height-1. + """ + if x1 is None: + x1 = self._width - 1 + + if y1 is None: + y1 = self._height - 1 + + y0 += self._offset_top + y1 += self._offset_top + + x0 += self._offset_left + x1 += self._offset_left + + self.command(ST7789_CASET) # Column addr set + self.data(x0 >> 8) + self.data(x0 & 0xFF) # XSTART + self.data(x1 >> 8) + self.data(x1 & 0xFF) # XEND + self.command(ST7789_RASET) # Row addr set + self.data(y0 >> 8) + self.data(y0 & 0xFF) # YSTART + self.data(y1 >> 8) + self.data(y1 & 0xFF) # YEND + self.command(ST7789_RAMWR) # write to RAM + + def display(self, image): + """Write the provided image to the hardware. + + :param image: Should be RGB format and the same dimensions as the display hardware. + + """ + # Set address bounds to entire display. + self.set_window() + + # Convert image to 16bit RGB565 format and + # flatten into bytes. + pixelbytes = self.image_to_data(image, self._rotation) + + # Write data to hardware. + for i in range(0, len(pixelbytes), 4096): + self.data(pixelbytes[i:i + 4096]) + + def image_to_data(self, image, rotation=0): + if not isinstance(image, np.ndarray): + image = np.array(image.convert('RGB')) + + # Rotate the image + pb = np.rot90(image, rotation // 90).astype('uint16') + + # Mask and shift the 888 RGB into 565 RGB + red = (pb[..., [0]] & 0xf8) << 8 + green = (pb[..., [1]] & 0xfc) << 3 + blue = (pb[..., [2]] & 0xf8) >> 3 + + # Stick 'em together + result = red | green | blue + + # Output the raw bytes + return result.byteswap().tobytes() diff --git a/pwnagotchi/ui/hw/minipitft.py b/pwnagotchi/ui/hw/minipitft.py index 9f859f1d6..83764f511 100644 --- a/pwnagotchi/ui/hw/minipitft.py +++ b/pwnagotchi/ui/hw/minipitft.py @@ -7,12 +7,15 @@ import logging import pwnagotchi.ui.fonts as fonts -from pwnagotchi.ui.hw.base import DisplayImpl +from pwnagotchi.ui.hw.st7789 import st7789_display - -class MiniPitft(DisplayImpl): +class MiniPitft(st7789_display): def __init__(self, config): super(MiniPitft, self).__init__(config, 'minipitft') + self.defaults['dc'] = 25 + self.defaults['bl'] = 22 + self.defaults['width'] = 240 + self.defaults['height'] = 240 def layout(self): fonts.setup(10, 9, 10, 35, 25, 9) @@ -37,16 +40,3 @@ def layout(self): return self._layout - def initialize(self): - logging.info("Initializing Adafruit Mini Pi Tft 240x240") - logging.info("Available pins for GPIO Buttons: 23, 24") - logging.info("Backlight pin available on GPIO 22") - logging.info("I2C bus available on stemma QT header") - from pwnagotchi.ui.hw.libs.adafruit.minipitft.ST7789 import ST7789 - self._display = ST7789(0,0,25,22) - - def render(self, canvas): - self._display.display(canvas) - - def clear(self): - self._display.clear() \ No newline at end of file diff --git a/pwnagotchi/ui/hw/minipitft2.py b/pwnagotchi/ui/hw/minipitft2.py index bf9a5c718..f3baacef2 100644 --- a/pwnagotchi/ui/hw/minipitft2.py +++ b/pwnagotchi/ui/hw/minipitft2.py @@ -7,12 +7,15 @@ import logging import pwnagotchi.ui.fonts as fonts -from pwnagotchi.ui.hw.base import DisplayImpl +from pwnagotchi.ui.hw.st7789 import st7789_display - -class MiniPitft2(DisplayImpl): +class MiniPitft2(st7789_display): def __init__(self, config): super(MiniPitft2, self).__init__(config, 'minipitft2') + self.defaults['dc'] = 25 + self.defaults['bl'] = 22 + self.defaults['width'] = 240 + self.defaults['height'] = 135 def layout(self): fonts.setup(10, 9, 10, 35, 25, 9) @@ -36,17 +39,3 @@ def layout(self): } return self._layout - - def initialize(self): - logging.info("initializing Adafruit Mini Pi Tft 135x240") - logging.info("Available pins for GPIO Buttons: 23, 24") - logging.info("Backlight pin available on GPIO 22") - logging.info("I2C bus available on stemma QT header") - from pwnagotchi.ui.hw.libs.adafruit.minipitft2.ST7789 import ST7789 - self._display = ST7789(0,0,25,22) - - def render(self, canvas): - self._display.display(canvas) - - def clear(self): - self._display.clear() \ No newline at end of file diff --git a/pwnagotchi/ui/hw/st7789.py b/pwnagotchi/ui/hw/st7789.py new file mode 100644 index 000000000..9f3f32032 --- /dev/null +++ b/pwnagotchi/ui/hw/st7789.py @@ -0,0 +1,90 @@ +import logging +import pwnagotchi.ui.fonts as fonts +from pwnagotchi.ui.hw.base import DisplayImpl + +class st7789_display(DisplayImpl): + def __init__(self, config, name='st7789'): + super(st7789_display, self).__init__(config, 'st7789') + self._display = None + logging.warn("Loaded st7789 display") + self.defaults = {'spi_port':0, + 'cs': 0, + 'dc': 25, + 'bl': 22, + 'rst': None, + 'rotation':180, + 'width':320, + 'height':240, + 'invert': True, + 'spi_hz':60*1000*1000, + 'offset_left':0, + 'offset_top':0, + 'backlight_pwm_steps':0} + + def layout(self): + fonts.setup(12, 10, 12, 70, 25, 9) + w = self.config.get('width', 320) + h = self.config.get('height', 240) + self._layout['width'] = w + self._layout['height'] = h + self._layout['face'] = (35, 50) + self._layout['name'] = (5, 20) + self._layout['channel'] = (0, 0) + self._layout['aps'] = (40, 0) + self._layout['uptime'] = (w-80, 0) + self._layout['line1'] = [0, 14, w, 14] + self._layout['line2'] = [0, h-20, w, h-20] + self._layout['friend_face'] = (0, 130) + self._layout['friend_name'] = (40, 135) + self._layout['shakes'] = (0, h-20) + self._layout['mode'] = (w-40, h-20) + self._layout['status'] = { + 'pos': (80, 160), + 'font': fonts.status_font(fonts.Medium), + 'max': 20 + } + return self._layout + + def initialize(self): + try: + from pwnagotchi.ui.hw.libs.ST7789 import ST7789 + cfg = self.config.get("st7789", {}) + logging.warn("Initializing ST7789 based display: %s %s" % (type(self).__name__, cfg)) + + # pull everything from config with reasonable defaults (adafruit minipitft) + spi_port = cfg.get("spi_port", self.defaults['spi_port']) + cs = cfg.get("cs", self.defaults['cs']) + dc = cfg.get("dc", self.defaults['dc']) + bl = cfg.get("backlight", self.defaults['bl']) + rst = cfg.get("rst", self.defaults['rst']) + w = self.config.get("width", self.defaults['width']) + h = self.config.get("height", self.defaults['height']) + rotation = cfg.get("rotation", self.defaults['rotation']) + invert = cfg.get("invert", self.defaults['invert']) + spi_hz = cfg.get("spi_hz", self.defaults['spi_hz']) + of_l = cfg.get("offset_left", self.defaults['offset_left']) + of_t = cfg.get("offset_top", self.defaults['offset_top']) + bl_pwm = cfg.get("backlight_pwm_steps", self.defaults['backlight_pwm_steps']) + + logging.warn("Setting up with %s, %s, %s, %s" % ((spi_port, cs, dc, bl), w, h, spi_hz)) + self._display = ST7789(spi_port, cs, dc, bl, rst=rst, + width=w, height=h, rotation=rotation, invert=invert, + spi_speed_hz=spi_hz, + offset_left=of_l, offset_top=of_t, + backlight_pwm=bl_pwm) + except Exception as e: + logging.exception(e) + + def render(self, canvas): + self._display.display(canvas) + + def clear(self): + self._display.clear() + + def set_backlight(self, value): + if self._display: + self._display.set_backlight(value) + + def get_backlight(self): + if self._display: + return self._display.get_backlight(value) diff --git a/pwnagotchi/ui/hw/waveshareoledlcd.py b/pwnagotchi/ui/hw/waveshareoledlcd.py index 60e82e69e..da0d2c2a7 100644 --- a/pwnagotchi/ui/hw/waveshareoledlcd.py +++ b/pwnagotchi/ui/hw/waveshareoledlcd.py @@ -17,12 +17,14 @@ import logging import pwnagotchi.ui.fonts as fonts -from pwnagotchi.ui.hw.base import DisplayImpl +from pwnagotchi.ui.hw.st7789 import st7789_display -class Waveshareoledlcd(DisplayImpl): +class Waveshareoledlcd(st7789_display): def __init__(self, config): super(Waveshareoledlcd, self).__init__(config, 'waveshareoledlcd') + self.defaults['dc'] = 22 + self.defaults['bl'] = 18 def layout(self): fonts.setup(12, 10, 12, 70, 25, 9) @@ -47,14 +49,3 @@ def layout(self): return self._layout - def initialize(self): - logging.info("initializing Waveshare OLED/LCD hat") - logging.info("Available pins for GPIO Buttons K1/K2/K3/K4: 4, 17, 23, 24") - from pwnagotchi.ui.hw.libs.waveshare.oled.oledlcd.ST7789 import ST7789 - self._display = ST7789(0,0,22,18) - - def render(self, canvas): - self._display.display(canvas) - - def clear(self): - self._display.clear() \ No newline at end of file diff --git a/pwnagotchi/ui/state.py b/pwnagotchi/ui/state.py index 3c1a543cb..81fe7deb9 100644 --- a/pwnagotchi/ui/state.py +++ b/pwnagotchi/ui/state.py @@ -1,3 +1,4 @@ +import logging from threading import Lock @@ -31,6 +32,23 @@ def get(self, key): with self._lock: return self._state[key].value if key in self._state else None + def get_map_actions(self): + actions = [] + + # get UI element actions + for key, lv in self._state.items(): + try: + link = lv.get_click_url() + if link: + bb = lv.get_bb() + shape = 'rect' + actions.append((shape, ','.join(map(str,bb)), key, link)) + except Exception as e: + logging.exception("Error getting action for %s: %s" % (key, e)) + + actions.reverse() + return actions + def reset(self): with self._lock: self._changes = {} diff --git a/pwnagotchi/ui/view.py b/pwnagotchi/ui/view.py index 0c5ad5dae..edbf7e752 100644 --- a/pwnagotchi/ui/view.py +++ b/pwnagotchi/ui/view.py @@ -37,6 +37,14 @@ def __init__(self, config, impl, state=None): self._black = 0x00 self._white = 0xFF + if 'bgcolor' in config['ui']: + self._white = config['ui']['bgcolor'] + WHITE = config['ui']['bgcolor'] + + if 'fgcolor' in config['ui']: + self._black = config['ui']['fgcolor'] + BLACK = config['ui']['fgcolor'] + # setup faces from the configuration in case the user customized them faces.load_from_config(config['ui']['faces']) @@ -44,13 +52,21 @@ def __init__(self, config, impl, state=None): self._render_cbs = [] self._config = config self._canvas = None + self._web_canvas = None + self._next_png_time = 0 self._frozen = False self._lock = Lock() self._voice = Voice(lang=config['main']['lang']) self._implementation = impl self._layout = impl.layout() - self._width = self._layout['width'] - self._height = self._layout['height'] + self._rotation = config['ui']['display'].get('rotation',0) + if (self._rotation/90)%2 == 0: + self._width = self._layout['width'] + self._height = self._layout['height'] + else: + self._width = self._layout['height'] + self._height = self._layout['width'] + self._state = State(state={ 'channel': LabeledValue(color=BLACK, label='CH', value='00', position=self._layout['channel'], label_font=fonts.Bold, @@ -68,7 +84,9 @@ def __init__(self, config, impl, state=None): 'face': Text(value=faces.SLEEP, position=(config['ui']['faces']['position_x'], config['ui']['faces']['position_y']), - color=BLACK, font=fonts.Huge, png=config['ui']['faces']['png']), + color=self._black, bgcolor=self._white, font=fonts.Huge, png=config['ui']['faces']['png'], + scale=config['ui']['faces'].get('scale', 1), + colorize=config['ui']['faces'].get('colorize', True)), # 'friend_face': Text(value=None, position=self._layout['friend_face'], font=fonts.Bold, color=BLACK), 'friend_name': Text(value=None, position=self._layout['friend_face'], font=fonts.BoldSmall, color=BLACK), @@ -97,9 +115,8 @@ def __init__(self, config, impl, state=None): plugins.on('ui_setup', self) if config['ui']['fps'] > 0.0: - threading.Thread(target=self._refresh_handler, args=(), name="UI Handler", daemon=True).start() - self._ignore_changes = () + threading.Thread(target=self._refresh_handler, args=(), name="UI Handler", daemon=True).start() else: logging.warning("ui.fps is 0, the display will only update for major changes") self._ignore_changes = ('uptime', 'name') @@ -113,7 +130,7 @@ def has_element(self, key): self._state.has_element(key) def add_element(self, key, elem): - if self.invert is 1 and elem.color: + if self.invert == 1 and elem.color: if elem.color == 0xff: elem.color = 0x00 elif elem.color == 0x00: @@ -387,18 +404,27 @@ def update(self, force=False, new_data={}): state = self._state changes = state.changes(ignore=self._ignore_changes) if force or len(changes): - self._canvas = Image.new('1', (self._width, self._height), self._white) + colormode = self._config.get('ui', {}).get('colormode', '1') + self._canvas = Image.new(colormode, (self._width, self._height), self._white) drawer = ImageDraw.Draw(self._canvas) - + drawer.font_mode = '1' + drawer.fontmode = '1' + plugins.on('ui_update', self) for key, lv in state.items(): # lv is a ui element lv.draw(self._canvas, drawer) - web.update_frame(self._canvas) + self._web_canvas = self._canvas.copy() + if time.time() > self._next_png_time: + self._next_png_time = time.time() + self._config['ui'].get('png_seconds', 0) + web.update_frame(self._canvas) for cb in self._render_cbs: cb(self._canvas) self._state.reset() + + def get_current_image(self): + return self._web_canvas.copy() diff --git a/pwnagotchi/ui/web/handler.py b/pwnagotchi/ui/web/handler.py index 6e75cb027..eeab57622 100644 --- a/pwnagotchi/ui/web/handler.py +++ b/pwnagotchi/ui/web/handler.py @@ -25,6 +25,9 @@ from flask import redirect from flask import render_template, render_template_string +from pwnagotchi.utils import pointInBox + +from io import BytesIO class Handler: def __init__(self, config, agent, app): @@ -34,6 +37,7 @@ def __init__(self, config, agent, app): self._app.add_url_rule('/', 'index', self.with_auth(self.index)) self._app.add_url_rule('/ui', 'ui', self.with_auth(self.ui)) + self._app.add_url_rule('/clickui/', 'clickui', self.with_auth(self.clickui)) self._app.add_url_rule('/shutdown', 'shutdown', self.with_auth(self.shutdown), methods=['POST']) self._app.add_url_rule('/reboot', 'reboot', self.with_auth(self.reboot), methods=['POST']) @@ -64,14 +68,11 @@ def _check_creds(self, u, p): def with_auth(self, f): @wraps(f) def wrapper(*args, **kwargs): - if not self._config['auth']: - return f(*args, **kwargs) - else: - auth = request.authorization - if not auth or not auth.username or not auth.password or not self._check_creds(auth.username, - auth.password): - return Response('Unauthorized', 401, {'WWW-Authenticate': 'Basic realm="Unauthorized"'}) - return f(*args, **kwargs) + auth = request.authorization + if not auth or not auth.username or not auth.password or not self._check_creds(auth.username, + auth.password): + return Response('Unauthorized', 401, {'WWW-Authenticate': 'Basic realm="Unauthorized"'}) + return f(*args, **kwargs) return wrapper @@ -81,6 +82,30 @@ def index(self): other_mode='AUTO' if self._agent.mode == 'manual' else 'MANU', fingerprint=self._agent.fingerprint()) + def clickui(self, coords): + try: + logging.warn("WEBHOOK %s: %s, %s" % (request.path, request.query_string.decode(), ",".join([f"{key}={value}" for key, value in request.args.items()]))) + x,y = list(map(int,request.query_string.decode().split(","))) + logging.warn("Split: %s" % (coords)) + w,h = list(map(int,coords.split("x"))) + rw = self._agent._view.width() + rh = self._agent._view.height() + ex = int(rw * x / w) + ey = int(rh * y / h) + logging.warning("Effective click: %f, %f" % (ex, ey)) + for (shape, coords, key, link) in self._agent._view._state.get_map_actions(): + bbox = list(map(int,coords.split(','))) + if pointInBox((ex,ey), bbox): + try: + logging.info("%s -> %s" % (key, link)) + return redirect(link) + + except Exception as e: + logging.exception(e) + except Exception as e: + logging.exception(e) + return "OK", 204 + def inbox(self): page = request.args.get("p", default=1, type=int) inbox = { @@ -235,4 +260,10 @@ def restart(self): # serve the PNG file with the display image def ui(self): with web.frame_lock: - return send_file(web.frame_path, mimetype='image/png') + if self._agent._view and self._agent._view._web_canvas: + imgIO = BytesIO() + self._agent._view._web_canvas.save(imgIO, 'PNG') + imgIO.seek(0) + return send_file(imgIO, mimetype='image/png') + else: + return send_file(web.frame_path, mimetype='image/png') diff --git a/pwnagotchi/ui/web/templates/index.html b/pwnagotchi/ui/web/templates/index.html index 475a97a45..1bf08822d 100644 --- a/pwnagotchi/ui/web/templates/index.html +++ b/pwnagotchi/ui/web/templates/index.html @@ -8,15 +8,28 @@ {% block script %} window.onload = function() { var image = document.getElementById("ui"); + var link = document.getElementById("touch_ui_link"); + link.href = "/clickui/" + image.offsetWidth+ "x" + image.offsetHeight; + function updateImage() { image.src = image.src.split("?")[0] + "?" + new Date().getTime(); + link.href = "/clickui/" + image.offsetWidth+ "x" + image.offsetHeight; } setInterval(updateImage, 1000); } + +window.onresize = function() { + var img = document.getElementById("ui"); + if (img.naturalWidth == 0) { + return + } + var link = document.getElementById("touch_ui_link"); + link.href = "/clickui/" + img.offsetWidth+ "x" + img.offsetHeight; +} {% endblock %} {% block content %} - +
  • diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index 727b28ce5..dbfb02b4e 100644 --- a/pwnagotchi/utils.py +++ b/pwnagotchi/utils.py @@ -305,6 +305,9 @@ def load_toml_file(filename): elif config['ui']['display']['type'] in ('tftbonnet'): config['ui']['display']['type'] = 'tftbonnet' + elif config['ui']['display']['type'] in ('st7789', 'ST7789'): + config['ui']['display']['type'] = 'st7789' + elif config['ui']['display']['type'] in ('waveshareoledlcd'): config['ui']['display']['type'] = 'waveshareoledlcd' @@ -531,6 +534,18 @@ def iface_channels(ifname): pass return channels +# is this point(x,y) in box (x1, y1, x2, y2), x2>x1, y2>y1 +def pointInBox(point, box): + try: + logging.debug("is %s in %s" % (repr(point), repr(box))) + if len(box) == 4: + return (point[0] >= box[0] and point[0] <= box[2] and point[1] >= box[1] and point[1] <= box[3]) + else: + logging.error("Unknown box size. return False") + return False + except Exception as e: + logging.exception("Point: %s, Box: %s, error: %s" % (point, box, repr(e))) + return False class WifiInfo(Enum): """