Skip to content

Instantly share code, notes, and snippets.

@willwade
Created October 10, 2022 11:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save willwade/6fa3629f13aef2c289ed999d6b8247d8 to your computer and use it in GitHub Desktop.
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
'''
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