# Copyright (C) 2020 Daniel Richardson richardson.daniel@hotmail.co.uk # # This file is part of stockTicker project for justinodunn. # # stockTicker can not be copied and/or distributed without the express # permission of Daniel Richardson import sys, select import os import threading from PIL import Image, ImageDraw, ImageFont import time import csv import requests from rgbmatrix import RGBMatrix, RGBMatrixOptions import finnhub def getInput(Block=False): if Block or select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []): msg = sys.stdin.read(1) #sys.stdin.flush() else: msg = '' return msg class StockTicker(): def __init__(self): #Define global resources self.symbols = [] self.ApiKey = "c24qddqad3ickpckgg80" self.SandboxApiKey = "sandbox_c24qddqad3ickpckgg8g" self.greenORred = (255, 255, 255) #self.blank = Image.open('logos/blank.png') self.blank = Image.new('RGB', (0, 32)) self.running = True self.brightness = 1.0 self.delay = 0.02 # Configuration for the matrix options = RGBMatrixOptions() options.rows = 64 options.cols = 64 options.chain_length = 1 options.parallel = 1 options.hardware_mapping = 'adafruit-hat' # If you have an Adafruit HAT: 'adafruit-hat' options.gpio_slowdown = 3 self.matrix = RGBMatrix(options = options) self.finnhubClient = finnhub.Client(api_key=self.ApiKey) def openImage(self, image_file): image = Image.open(image_file) # Make image fit our screen. #image.thumbnail((self.matrix.width, self.matrix.height), Image.ANTIALIAS) image = image.convert('RGB') return image def setImage(self, image, offset_x = 0, offset_y = 0, unsafe=False): if (image.mode != "RGB"): raise Exception("Currently, only RGB mode is supported for SetImage(). Please create images with mode 'RGB' or convert first with image = image.convert('RGB'). Pull requests to support more modes natively are also welcome :)") if unsafe: #In unsafe mode we directly acceshow to send commands to running python processs the underlying PIL image array #in cython, which is considered unsafe pointer accecss, #however it's super fast and seems to work fine #https://groups.google.com/forum/#!topic/cython-users/Dc1ft5W6KM4 img_width, img_height = image.size print('args', img_width, img_height, offset_x, offset_y, image) self.matrix.SetPixelsPillow(offset_x, offset_y, img_width, img_height, image) else: # First implementation of a SetImage(). OPTIMIZE_ME: A more native # implementation that directly reads the buffer and calls the underlying # C functions can certainly be faster. img_width, img_height = image.size pixels = image.load() for x in range(max(0, -offset_x), min(img_width, self.matrix.width - offset_x)): for y in range(max(0, -offset_y), min(img_height, self.matrix.height - offset_y)): (r, g, b) = pixels[x, y] self.matrix.SetPixel(x + offset_x, y + offset_y, r*self.brightness, g*self.brightness, b*self.brightness) def scrollImage(self, image_file, offset_x = 0, offset_y = 0): image = self.openImage(image_file) img_width, img_height = image.size print(offset_x, offset_y) while offset_x > -img_width: offset_x -= 1 self.setImage(image, offset_x = offset_x, offset_y = offset_y) time.sleep(delay) def scrollImageTransition(self, image_files, offset_x = 0, offset_y = 0): # use two image files and switch between them with a seemless transition current_img = 1 #while True: if current_img == 1: image = self.openImage(image_files[0]) elif current_img == 2: image = self.openImage(image_files[1]) img_width, img_height = image.size while offset_x > -img_width: offset_x -= 1 self.setImage(image, offset_x = offset_x, offset_y = offset_y) time.sleep(self.delay) try: msg = getInput() if msg == 'K': self.resetMatrix() break elif msg == 's': stock_ticker.delay = 0.04 elif msg == 'm': stock_ticker.delay = 0.02 elif msg == 'f': stock_ticker.delay = 0.01 elif msg in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']: self.brightness = min(1.0, float(msg)/10 + 0.1) except KeyboardInterrupt: sys.stdout.flush() pass if current_img == 1: current_img = 2 elif current_img == 2: current_img = 1 offset_x = 0 #Get the logo from file that is the same as ticker name def getLogo(self, Ticker): try: Logo = Image.open('logos/' + Ticker + '.png') except: print('Could not find logo for: ' + Ticker + ' using default') Logo = Image.open('logos/default.png') return Logo #Using change between min and day price give appropriate arrow #and set the overall change colour def getArrow(self, CHANGE): self.greenORred if(CHANGE>0): Arrow = Image.open('logos/up.png') self.greenORred = (0, 255, 0) return Arrow, CHANGE Arrow = Image.open('logos/down.png') self.greenORred = (255, 0, 0) CHANGE = (CHANGE * -1) return Arrow, CHANGE def get_text_dimensions(self, text_string, font): canvas = Image.new('RGB', (100,100)) draw = ImageDraw.Draw(canvas) monospace = font text = text_string white = (255,255,255) draw.text((10, 10), text, font=monospace, fill=white) bbox = canvas.getbbox() width = bbox[2]-bbox[0] height = bbox[3]-bbox[1] return width,height #Draw Ticker, current and change onto one image def textToImage(self, TICKER, CURRENT, CHANGE, ARROW): font = ImageFont.load("./fonts/10x20.pil") img = Image.new('RGB', (150, 32)) d = ImageDraw.Draw(img) d.text((4, 0), TICKER, fill=(255, 255, 255), font=font) d.text((4, 16), CURRENT, fill=self.greenORred, font=font) text_width_current, text_height = self.get_text_dimensions(CURRENT, font) img.paste(ARROW, ((text_width_current + 9),18)) d.text(((text_width_current+29), 16), CHANGE, fill=self.greenORred, font=font) text_width_change, text_height = self.get_text_dimensions(CHANGE, font) newWidth = (text_width_current+29) + (text_width_change) img.crop((0,0,newWidth,32)) return img #Stitch the logo & prices picture into one image def stitchImage(self, image_list): widths, heights = zip(*(i.size for i in image_list)) total_width = sum(widths) max_height = max(heights) new_im = Image.new('RGB', (total_width, max_height)) x_offset = 0 for im in image_list: new_im.paste(im, (x_offset,0)) x_offset += im.size[0] print('width:', im.size[0]) return new_im #Get current prices and day prices for the ticker then format the response into an array def getStockPrices(self, symbols): apiCalledError = False stock_info = [] try: quotes = [self.finnhubClient.quote(symbol) for symbol in symbols] current_prices = [quote['c'] for quote in quotes] opening_prices = [quote['o'] for quote in quotes] for i, symbol in enumerate(symbols): stock_info.append([symbol, current_prices[i], opening_prices[i]]) except Exception as e: print("Could not fetch data - API CALLS REACHED? - Will display old image") print(e) apiCalledError = True return stock_info, apiCalledError def resetMatrix(self): for x in range(self.matrix.width): for y in range(self.matrix.height): self.matrix.SetPixel(x , y , 0,0,0) #Connect all the pieces togeather creating 1 long final stock image def getFullStockImage(self): image_list = [] start = time.time() stock_info, apiCalledError = self.getStockPrices(self.symbols) if (apiCalledError == False): for i in range(len(stock_info)): change = float(stock_info[i][1])-float(stock_info[i][2]) #TEXT ticker = stock_info[i][0] #TEXT current = '%.2f' % float(stock_info[i][1]) #TEXT logo = self.getLogo(stock_info[i][0]) arrow, change = self.getArrow(change) change = '%.2f' % change midFrame = self.textToImage(ticker, current, change, arrow) #IMAGE THE TEXT stitchedStock = self.stitchImage([logo,midFrame]) image_list.append(self.blank) image_list.append(stitchedStock) print('elapsed time loop ended: ' + str((time.time()-start))) if (apiCalledError == False): finalDisplayImage = self.stitchImage(image_list) finalDisplayImage.save('final.ppm') #Send the final stitched image to the display for set amount of time def displayMatrix(self): #os.system("sudo ./demo -D1 final.ppm -t " + str(displayTime) +" -m "+ str(speedDisplay) +" --led-gpio-mapping=adafruit-hat --led-rows=32 --led-cols=256") #os.system("sudo ./demo -D1 final.ppm -t " + str(self.displayTime) +" -m "+ str(self.speedDisplay) +" --led-gpio-mapping=adafruit-hat --led-rows=64 --led-cols=64 --led-slowdown-gpio=4 ") self.scrollImageTransition(['final.ppm', 'final.ppm'], offset_x = 0, offset_y = 0) #Retrieve symbols from the CSV file def getSymbols(self): self.symbols = [] CSV = csv.reader(open('csv/tickers.csv', 'r')) for row in CSV: print(row) self.symbols.append(row[0]) print(self.symbols) #Main run definition called by server def runStockTicker(self, runtime, delay, speedtime): self.getSymbols() self.GetfullStockImage() self.keySwapper += 1 self.running = True while (True): if (self.running == True): #th = threading.Thread(target=self.displayMatrix) #th.start() self.displayMatrix() time.sleep((int(runtime) - int(delay))) self.getfullStockImage() #th.join() else: break; #Change running to false stopping refresh at next checkpoint def stopStockTicker(self): self.keySwapper = 0 self.running = False print('MATRIX DISPLAY STOP CALLED') if __name__ == '__main__': #print(sys.stdin.readlines()) stock_ticker = StockTicker() stock_ticker.getSymbols() #stock_ticker.getFullStockImage() #stock_ticker.displayMatrix() while True: msg = getInput() if msg == 'S': stock_ticker.getFullStockImage() stock_ticker.displayMatrix() elif msg == 's': stock_ticker.delay = 0.04 elif msg == 'm': stock_ticker.delay = 0.02 elif msg == 'f': stock_ticker.delay = 0.01 elif msg in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']: stock_ticker.brightness = min(1.0, float(msg)/10 + 0.1)