Created
October 10, 2022 11:26
-
-
Save willwade/6fa3629f13aef2c289ed999d6b8247d8 to your computer and use it in GitHub Desktop.
Logs changes to a AAC software - to log when someone is using it or not
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
Originally from matt.oppenheim@gmail.com for his https://github.com/mattoppenheim/microbit_activity_indicator | |
Hacked by Will - just logs if someone is writing or not. Used as a tool to detect how long someone is using their Aid for | |
Of course doesn't log if its person or someone helping | |
''' | |
from logging.handlers import RotatingFileHandler | |
import click | |
from datetime import datetime | |
import logging | |
import sys | |
# ImageGrab is windows only | |
try: | |
from PIL import ImageGrab, ImageStat | |
except ImportError: | |
print('you need to install pillow\npip install pillow') | |
sys.exit() | |
import time | |
try: | |
import win32gui | |
except ModuleNotFoundError: | |
print('you need to install win32gui\npip install pywin32') | |
pass | |
# use SetProcessDPIAware() in case 100% scaling is not set in windows 8 | |
try: | |
from ctypes import windll | |
except ImportError: | |
print('need to run this on Windows') | |
pass | |
user32 = windll.user32 | |
user32.SetProcessDPIAware() | |
# number of changed pixels to cause an activity trigger | |
LIMIT = 3 | |
FRACTION = 0.2 | |
# titles of windows to look for activity in | |
COM_SOFTWARE = ['grid', 'communicator'] | |
# extra windows that are not visible can be created by e.g. Grid 3 | |
IGNORE = ['grid 3.exe', 'users'] | |
TIMEOUT = 0.1 | |
logging.basicConfig( | |
handlers=[RotatingFileHandler( | |
'./AACMessageWriting_log.log', maxBytes=100000, backupCount=10)], | |
level=logging.INFO, | |
format='%(asctime)s.%(msecs)03d %(message)s', | |
datefmt='%Y-%m-%dT%H:%M:%S') | |
def singleton(cls, *args): | |
''' Singleton pattern. ''' | |
instances = {} | |
def getinstance(*args): | |
if cls not in instances: | |
instances[cls] = cls(*args) | |
return instances[cls] | |
return getinstance | |
@singleton | |
def check_fraction(fraction): | |
''' Check that the fraction is >0 and <1. ''' | |
if not 0.01 < fraction < 0.99: | |
logging.info('Fraction needs to be between 0.01 and 0.99') | |
sys.exit() | |
def count_black_pixels(image): | |
''' Count the number of black pixels in <image>. ''' | |
black = 0 | |
for pixel in image.getdata(): | |
if pixel == (0, 0, 0): | |
black += 1 | |
return black | |
def find_window_handle(com_software=COM_SOFTWARE, ignore=IGNORE): | |
''' Find the window for communication software. ''' | |
toplist, winlist = [], [] | |
def _enum_cb(window_handle, results): | |
winlist.append((window_handle, win32gui.GetWindowText(window_handle))) | |
win32gui.EnumWindows(_enum_cb, toplist) | |
for sware in com_software: | |
# winlist is a list of tuples (window_id, window title) | |
logging.debug('items in ignore: {}'.format( | |
[item.lower() for item in ignore])) | |
for window_handle, title in winlist: | |
#logging.debug('window_handle: {}, title: {}'.format(window_handle, title)) | |
if sware in title.lower() and not any(x in title.lower() for x in ignore): | |
# logging.debug('found title: {}'.format(title)) | |
return window_handle | |
logging.info( | |
'no communications software found for {}'.format(com_software)) | |
time.sleep(0.5) | |
def format_list(in_list): | |
str_format = len(in_list) * ' {:.2f}' | |
return str_format.format(*in_list) | |
def get_time(): | |
''' Return a formatted time string. ''' | |
return datetime.now().strftime('%H:%M:%S') | |
def get_window_top(fraction, software=COM_SOFTWARE): | |
''' Find the top of the window containing target software. ''' | |
try: | |
window_handle = find_window_handle(software) | |
except TypeError: | |
('no communications software found for {}'.format(software)) | |
return None | |
if window_handle is None: | |
return | |
# window_rect is (left, top, right, bottom) with top left as origin | |
window_rect = win32gui.GetWindowRect(window_handle) | |
# grab top fraction of image to reduce processing time | |
window_top = (window_rect[0], window_rect[1], window_rect[2], window_rect[1] + | |
int((window_rect[3]-window_rect[1])*fraction)) | |
return window_top | |
def num_new_black_pixels(window_top): | |
''' Check software for a change. ''' | |
try: | |
img = ImageGrab.grab(window_top) | |
except OSError as e: | |
logging.debug('OSError: {}'.format(e)) | |
return None | |
try: | |
new_black = count_black_pixels(img) | |
logging.debug('{} new_black: {}'.format(get_time(), new_black)) | |
except ZeroDivisionError as e: | |
logging.debug('ZeroDivisionError: {}'.format(e)) | |
return None | |
if new_black: | |
return new_black | |
else: | |
return None | |
def check_limit(new_black, old_black, limit): | |
''' Have we exceeded the threshold for new black pixels? ''' | |
logging.debug('new_black: {}, old_black: {}, limit: {}'.format( | |
new_black, old_black, limit)) | |
if abs(new_black - old_black) > limit: | |
logging.debug('Change detected') | |
return True | |
return False | |
@click.command() | |
@click.option('--limit', default=LIMIT, | |
help='Number of changed pixels to trigger event. Default is {}.' | |
.format(LIMIT)) | |
@click.option('--fraction', default=FRACTION, | |
help='Fraction of screen, from the top, to monitor. Default is {}.' | |
.format(FRACTION)) | |
def main(limit, fraction): | |
check_fraction(fraction) | |
logging.debug('limit={} fraction={}\n'.format(limit, fraction)) | |
old_black = 0 | |
while True: | |
time.sleep(0.2) | |
# look for the top fraction of a window running the target software | |
window_top = get_window_top(fraction) | |
if window_top is None: | |
continue | |
# count black pixels in top fraction of target window | |
new_black = num_new_black_pixels(window_top) | |
logging.debug('new_black: {}'.format(new_black)) | |
if new_black is None: | |
continue | |
is_limit_exceeded = check_limit(new_black, old_black, limit) | |
if is_limit_exceeded: | |
logging.info('Thrshold exceeded') | |
old_black = new_black | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment