diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..5bb7116
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,11 @@
+FROM python:latest
+
+RUN apt-get update && apt-get install -y libgl1-mesa-glx ghostscript && apt-get clean && rm -rf /var/lib/apt/lists
+COPY . .
+#COPY config-server.yml config.yml
+RUN pip3 install -r requirements.txt
+
+CMD ["python3", "run.py"]
+EXPOSE 5000
+EXPOSE 8000
+EXPOSE 9000
diff --git a/banner_ivu_export.py b/banner_ivu_export.py
new file mode 100755
index 0000000..cb61bb5
--- /dev/null
+++ b/banner_ivu_export.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+
+import socket
+from datetime import datetime
+from time import sleep
+from util import fprint
+
+"""
+
+from: https://github.com/MoisesBrito31/ve_data_log/blob/main/serverContagem/VE/drive.py
+(no license)
+
+(partially) adapted to English language & iVu camera instead of classic VE by Cole Deck
+
+"""
+
+def gravaLog(ip="1",tipo="Evento", msg="", file="log_imagem.txt"):
+    # removed full logging
+    fprint(msg)
+
+class DriveImg():
+    HEADERSIZE = 100
+    ip = "192.168.0.1"
+    port = 32200
+    onLine = False
+
+    def __init__(self, ip, port, pasta = "media/"):
+        self.pasta = pasta
+        self.ip=ip
+        self.port = port
+        self.trans = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
+        self.trans.settimeout(5)
+        fprint("Trying to connect...")
+        try:
+            self.trans.connect((self.ip,self.port))
+            self.onLine = True
+            fprint("Camera Online")
+            #self.trans.close()
+        except:
+            self.onLine = False 
+            fprint("Offline")
+
+    def read_img(self):
+        resposta = 'Falha'
+        try:
+            if not self.onLine:
+                #print(f'tentando Conectar camera {self.ip}...')
+                gravaLog(ip=self.ip,msg=f'Trying to connect...')
+                sleep(2)
+                try:
+                    self.trans = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
+                    self.trans.connect((self.ip,self.PORT))
+                    self.onLine = True
+                    gravaLog(ip=self.ip,msg=f'Connection established.')
+                except:
+                    self.onLine = False
+                    self.trans.close()
+                    return resposta
+            ret = self.trans.recv(64)
+            try:
+                valida = str(ret[0:15].decode('UTF-8'))
+                #print(valida)
+                if valida.find("TC IMAGE")<0:
+                    self.onLine = False
+                    self.trans.close()
+                    sleep(2)
+                    gravaLog(ip=self.ip,tipo="Falha",msg=f'Unable to find TC IMAGE bookmark')
+                    return "Error"
+            except Exception as ex:
+                self.onLine = False
+                self.trans.close()
+                sleep(2)
+                gravaLog(ip=self.ip,tipo="Falha",msg=f'Error - {str(ex)}')
+                return "Error"
+            if ret:
+                frame = int.from_bytes(ret[24:27],"little")
+                isJpeg = int.from_bytes(ret[32:33],"little")
+                img_size = int.from_bytes(ret[20:23],"little")
+                data = self.trans.recv(5000)
+                while img_size>len(data) and ret:
+                    ret = self.trans.recv(10000)
+                    if ret:
+                        data = data+ret
+                        #print(f'{len(ret)}b dados recebidos, total de: {len(data)+64}b')
+                    else:
+                        gravaLog(ip=self.ip,tipo="Falha",msg="Unable to recieve the image")
+                        self.onLine = False
+                        return "Unable to recieve the image"
+                hoje = datetime.now()
+                idcam = self.ip.split('.')
+                """try:
+                    nomeFile = f'{hoje.day}{hoje.month}{hoje.year}-{idcam[3]}-{frame}'
+                    if isJpeg==1:
+                        file = open(f'{self.pasta}{nomeFile}.jpg','wb')
+                        nomeFile = f'{nomeFile}.jpg'
+                    else:
+                        file = open(f'{self.pasta}{nomeFile}.bmp','wb')
+                        nomeFile = f'{nomeFile}.bmp'
+                    file.write(data)
+                    file.close()
+                except Exception as ex:
+                    sleep(2)
+                    gravaLog(ip=self.ip,tipo="Falha",msg=f'Error - {str(ex)}')
+                    return "Falha" 
+                    """
+                if isJpeg==1:
+                    return "jpeg",data
+                else:
+                    return "bmp",data
+                
+        except Exception as ex:
+            gravaLog(ip=self.ip,tipo="Falha Generica",msg=f'Error - {str(ex)}')
+            #print(f'erro {str(ex)}')
+            self.onLine = False
+            self.trans.close()
+            sleep(2)
+            return resposta
+
+class DriveData():
+    HEADERSIZE = 100
+    ip = "192.168.0.1"
+    port = 32100
+    onLine = False
+
+    def __init__(self, ip, port):
+        gravaLog(ip=self.ip,msg=f'iniciou drive',file="log_data.txt")
+        self.ip=ip
+        self.port = port
+        self.trans = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
+        try:
+            self.trans.connect((self.ip,self.port))
+            self.onLine = True
+        except:
+            self.onLine = False 
+
+    def read_data(self):
+        resposta = 'falha'
+        try:
+            if not self.onLine:
+                #print(f'tentando Conectar...\n')
+                gravaLog(ip=self.ip,msg=f'tentando Conectar...',file="log_data.txt")
+                sleep(2)
+                try:
+                    self.trans = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
+                    self.trans.connect((self.ip,self.PORT))
+                    self.onLine = True
+                    gravaLog(ip=self.ip,msg=f'Conexão restabelecida...',file="log_data.txt")
+                except:
+                    self.onLine = False
+                    return resposta
+            resposta = self.trans.recv(self.HEADERSIZE).decode("utf-8")
+            resposta = str(resposta).split(',')
+            return resposta
+        except Exception as ex:
+            self.onLine = False 
+            gravaLog(ip=self.ip,tipo="Falha Generica",msg=f'erro {str(ex)}',file="log_data.txt")
+            sleep(2)
+            return resposta
+
+
+if __name__ == "__main__":
+    test = DriveImg("192.168.1.125", 32200)
+    x = 0
+    while x < 100:
+        x=x+1
+        imgtype, img = test.read_img()
\ No newline at end of file
diff --git a/config.yml b/config.yml
index 6c86f16..6356135 100644
--- a/config.yml
+++ b/config.yml
@@ -7,6 +7,12 @@ core:
 arm:
   ip: 192.168.1.145
 
+#cable_map:
+cameras:
+  banner:
+    ip: 192.168.1.125
+    port: 32200
+
 led: 
   fps: 90
   timeout: 0
@@ -26,13 +32,13 @@ led:
       ledstart: 288
       ledend: 431
       mode: rgb
-    - universe: 1
-      ip: 192.168.68.130
+    - universe: 4
+      ip: 192.168.5.40
       ledstart: 432
       ledend: 575
       mode: rgb
-    - universe: 4
-      ip: 192.168.68.131
+    - universe: 1
+      ip: 192.168.5.4
       ledstart: 576
       ledend: 719
       mode: rgb
diff --git a/database.py b/database.py
new file mode 100644
index 0000000..2befec2
--- /dev/null
+++ b/database.py
@@ -0,0 +1,140 @@
+"""This module contains functionality for interacting with a PostgreSQL database. It will automatically handle error
+conditions (i.e. missing columns) without terminating the entire program. Use the :py:class:`DBConnector` class to
+handle database interactions, either as a standalone object or in a context manager."""
+from __future__ import annotations
+
+import os
+import psycopg2
+from psycopg2 import DatabaseError, OperationalError
+from psycopg2.errors import UndefinedColumn
+
+DB_ADDRESS = os.getenv('DB_ADDRESS', 'localhost')
+DB_PORT = os.getenv('DB_PORT', 5432)
+DB_USER = os.getenv('DB_USER', 'postgres')
+DB_PASSWORD = os.getenv('DB_PASSWORD', '')
+DB_NAME = os.getenv('DB_NAME', 'postgres')
+DB_TABLE = os.getenv('DB_TABLE', 'cables')
+
+
+class DBConnector:
+    """Context managed database class. Use with statements to automatically open and close the database connection, like
+    so:
+
+    .. code-block:: python
+       with DBConnector() as db:
+           db.read()
+    """
+
+    def _db_start(self):
+        """Setup the database connection and cursor."""
+        try:
+            self.conn = psycopg2.connect(
+                f"host={DB_ADDRESS} port={DB_PORT} dbname={DB_NAME} user={DB_USER} password={DB_PASSWORD}")
+            self.cur = self.conn.cursor()
+        except OperationalError as e:
+            raise e
+
+    def _db_stop(self):
+        """Close the cursor and connection."""
+        self.cur.close()
+        self.conn.close()
+
+    def __init__(self):
+        self._db_start()
+
+    def __del__(self):
+        self._db_stop()
+
+    def __enter__(self):
+        self._db_start()
+
+    def __exit__(self):
+        self._db_stop()
+
+    def _get_cols(self) -> set[str]:
+        """Get the list of columns in the database.
+
+        :return: A list of column names."""
+        query = f"select COLUMN_NAME from information_schema.columns where table_name={DB_TABLE}"
+        rows = {x["COLUMN_NAME"] for x in self._query(query)}
+        return rows
+
+    def _column_parity(self, columns: list[str] | set[str]) -> set[str]:
+        """If the listed columns are not in the database, add them.
+
+        :param columns: The columns we expect are in the database.
+        :return: The list of columns in the database after querying."""
+        cols = set(columns)
+        existing = self._get_cols()
+        needs = cols.difference(existing.intersection(cols))
+        if len(needs) > 0:
+            query = f"ALTER TABLE {DB_TABLE} {', '.join([f'ADD COLUMN {c}' for c in needs])}"
+            self._query(query)
+            existing = self._get_cols()
+        return existing
+
+    def _query(self, sql) -> list[dict]:
+        """Basic function for running queries.
+
+        :param sql: SQL query as plaintext.
+        :return: Results of the query, or an empty list if none."""
+        result = []
+        try:
+            self.cur.execute(sql)
+            result = self._read_dict()
+        except DatabaseError as e:
+            print(f"ERROR {e.pgcode}: {e.pgerror}\n"
+                  f"Caused by query: {sql}")
+        finally:
+            return result
+
+    def _read_dict(self) -> list[dict]:
+        """Read the cursor as a list of dictionaries. psycopg2 defaults to using a list of tuples, so we want to convert
+        each row into a dictionary before we return it."""
+        cols = [i.name for i in self.cur.description]
+        results = []
+        for row in self.cur:
+            row_dict = {}
+            for i in range(0, len(row)):
+                if row[i]:
+                    row_dict = {**row_dict, cols[i]: row[i]}
+            results.append(row_dict)
+        return results
+
+    def read(self, **kwargs) -> list[dict]:
+        """Read rows from a database that match the specified filters.
+
+        :param kwargs: Column constraints; i.e. what value to filter by in what column.
+        :returns: A list of dictionaries of all matching rows, or an empty list if no match."""
+        args = []
+        for kw in kwargs.keys():
+            args.append(f"{kw} ILIKE {kwargs['kw']}")
+        query = f"SELECT * FROM {DB_TABLE}"
+        if len(args) > 0:
+            query += f" WHERE {' AND '.join(args)}"
+        return self._query(query)
+
+    def write(self, **kwargs) -> dict:
+        """Write a row to the database.
+
+        :param kwargs: Values to write for each database; specify each column separately!
+        :returns: The row you just added."""
+        self._column_parity(set(kwargs.keys()))
+        values = []
+        for val in kwargs.keys():
+            values.append(kwargs[val])
+        query = f"INSERT INTO {DB_TABLE} ({', '.join(kwargs.keys())}) VALUES ({', '.join(values)})"
+        self._query(query)
+        return kwargs
+
+    def write_all(self, items: list[dict]) -> list[dict]:
+        """Write multiple rows to the database.
+
+        :param items: Rows to write, as a list of dictionaries.
+        :returns: The rows that were added successfully."""
+        successes = []
+        for i in items:
+            res0 = self.write(**i)
+            if res0:
+                successes.append(res0)
+        return successes
diff --git a/get_specs.py b/get_specs.py
index 419421b..7a17f04 100755
--- a/get_specs.py
+++ b/get_specs.py
@@ -26,7 +26,7 @@ def check_internet(url='https://belden.com', timeout=5):
     
 
 
-def query_search(partnum):
+def query_search(partnum, source):
     """token_url = "https://www.belden.com/coveo/rest/token?t=" + str(int(time.time()))
     with requests.get(token_url) as r:
         out = json.loads(r.content)
@@ -49,15 +49,50 @@ def query_search(partnum):
     # Bash script uses some crazy json formatting that I could not figure out
     # Despite the fact that I wrote it
     # So I'll just leave it, becuase it works.
+    if source == "Belden":
+        command = ["./query-search.sh", partnum]
+        result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+        if result.returncode != 0: # error
+            fprint("No results found in search database for " + partnum + ". No hi-res part image available.", result.stderr)
+            return False
+        else:
+            data_out = json.loads(result.stdout)
+            return data_out
+    elif source == "Alphawire":
+        alphaurl = "https://www.alphawire.com//sxa/search/results/?l=en&s={4A774076-6068-460C-9CC6-A2D8E85E407F}&itemid={BF82F58C-EFD9-4D8B-AE3E-097DD12CF7DA}&sig=&autoFireSearch=true&productpartnumber=*" + partnum + "*&v={B22CD56D-AB95-4048-8AA1-5BBDF2F2D17F}&p=10&e=0&o=ProductPartNumber%2CAscending"
+        r = requests.get(url=alphaurl)
+        data = r.json()
+        output = dict()
+        #print(data)
+        try:
+            if data["Count"] > 0:
+                print(data["Results"][0]["Url"])
+                result = data["Results"][0]
+                if result["Url"].split("/")[-1] == partnum:
+                    #print(partnum)
+                    print(result["Html"])
+                    try:
+                        imgidx = result["Html"].index("<img src=") + 10
+                        imgidx2 = result["Html"].index("?", imgidx)
+                        output["image"] = result["Html"][imgidx:imgidx2]
+                        if output["image"].index("http") != 0:
+                            output["image"] = ""
+                            print("No cable image found.")
+                    except:
+                        print("No cable image found.")
 
-    command = ["./query-search.sh", partnum]
-    result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
-    if result.returncode != 0: # error
-        fprint("No results found in search database for " + partnum + ". No hi-res part image available.", result.stderr)
+                    dsidx = result["Html"].index("<a href=\"/disteAPI/") + 9
+                    dsidx2 = result["Html"].index(partnum, dsidx) + len(partnum)
+                    output["datasheet"] = "https://www.alphawire.com" + result["Html"][dsidx:dsidx2]
+                    #"test".index()
+                    print(output)
+                    return output
+
+
+        except:
+            return False
         return False
-    else:
-        data_out = json.loads(result.stdout)
-        return data_out
+
 
 def touch(path):
     with open(path, 'a'):
@@ -126,7 +161,7 @@ def get_multi(partnums):
                 sys.exit()
 
 
-        def _download_image(url, output_dir): # Download datasheet with known URL
+        def _download_image(url, output_dir): # Download image with known URL
             global bartext
 
             #fprint(url)
@@ -151,25 +186,31 @@ def get_multi(partnums):
                 os.remove(partnum + "/datasheet.pdf")
                 sys.exit()
 
-        def __use_cached_datasheet(partnum, path, output_dir):
+        def __use_cached_datasheet(partnum, path, output_dir, dstype):
             fprint("Using cached datasheet for " + partnum)
             bar.text = "Using cached datasheet for " + partnum
             bar(skipped=True)
             fprint("Parsing Datasheet contents of " + partnum)
             bar.text = "Parsing Datasheet contents of " + partnum + ".pdf..."
-            read_datasheet.parse(path, output_dir)
+            read_datasheet.parse(path, output_dir, partnum, dstype)
             bar(skipped=False)
 
-        def __downloaded_datasheet(partnum, path, output_dir):
+        def __downloaded_datasheet(partnum, path, output_dir, dstype):
             fprint("Downloaded " + path)
             bar.text = "Downloaded " + path
             bar(skipped=False)
             fprint("Parsing Datasheet contents of " + partnum)
             bar.text = "Parsing Datasheet contents of " + partnum + ".pdf..."
-            read_datasheet.parse(path, output_dir)
+            read_datasheet.parse(path, output_dir, partnum, dstype)
             bar(skipped=False)
 
-        for partnum in partnums:
+        for fullpartnum in partnums:
+            if fullpartnum[0:2] == "BL": # catalog.belden.com entry\
+                partnum = fullpartnum[2:]
+                dstype = "Belden"
+            elif fullpartnum[0:2] == "AW":
+                partnum = fullpartnum[2:]
+                dstype = "Alphawire"
             output_dir = "cables/" + partnum
             path = output_dir + "/datasheet.pdf"
             bartext = "Downloading files for part " + partnum
@@ -177,7 +218,7 @@ def get_multi(partnums):
             #
             if (not os.path.exists(output_dir + "/found_part_hires")) or not (os.path.exists(path) and os.path.getsize(path) > 1):
                 # Use query
-                search_result = query_search(partnum.replace(" ", ""))
+                search_result = query_search(partnum.replace(" ", ""), dstype)
                 # Try to use belden.com search
                 if search_result is not False:
                     # Download high resolution part image if available and needed
@@ -190,17 +231,17 @@ def get_multi(partnums):
 
                     # Download datasheet from provided URL if needed
                     if os.path.exists(path) and os.path.getsize(path) > 1:
-                        __use_cached_datasheet(partnum, path, output_dir)
+                        __use_cached_datasheet(partnum, path, output_dir, dstype)
 
                     elif _download_datasheet(search_result["datasheet"], output_dir) is not False:
-                        __downloaded_datasheet(partnum, path, output_dir)
+                        __downloaded_datasheet(partnum, path, output_dir, dstype)
                 
                 elif os.path.exists(path) and os.path.getsize(path) > 1:
-                    __use_cached_datasheet(partnum, path, output_dir)
+                    __use_cached_datasheet(partnum, path, output_dir, dstype)
                 
                 # If search fails, and we don't already have the datasheet, guess datasheet URL and skip the hires image download
                 elif _try_download_datasheet(partnum, output_dir) is not False:
-                    __downloaded_datasheet(partnum, path, output_dir)
+                    __downloaded_datasheet(partnum, path, output_dir, dstype)
 
                 # Failed to download with search or guess :(
                 else: 
@@ -213,7 +254,7 @@ def get_multi(partnums):
             # We already have a hi-res image and the datasheet - perfect!
             else:
                 fprint("Using cached hi-res part image for " + partnum)
-                __use_cached_datasheet(partnum, path, output_dir)
+                __use_cached_datasheet(partnum, path, output_dir, dstype)
     
     if len(failed) > 0:
         fprint("Failed to download:")
@@ -227,21 +268,22 @@ def get_multi(partnums):
 
 
 if __name__ == "__main__":
-    partnums = ["10GXS12", "RST 5L-RKT 5L-949", 
-"10GXS13",
-"10GXW12",
-"10GXW13",
-"2412",
-"2413",
-"OSP6AU",
-"FI4D024P9",
-"FISD012R9",
-"FDSD012A9",
-"FSSL024NG",
-"FISX006W0",
-"FISX00103",
-"C6D1100007"
+    partnums = ["BL7958A", "BL10GXS12", "BLRST 5L-RKT 5L-949", 
+"BL10GXS13",
+"BL10GXW12",
+"BL10GXW13",
+"BL2412",
+"BL2413",
+"BLOSP6AU",
+"BLFI4D024P9",
+"BLFISD012R9",
+"BLFDSD012A9",
+"BLFSSL024NG",
+"BLFISX006W0",
+"BLFISX00103",
+"BLC6D1100007"
     ]
     get_multi(partnums)
+    #query_search("3248", "Alphawire")
 
 
diff --git a/led_control.py b/led_control.py
index 4a55fff..75c7823 100755
--- a/led_control.py
+++ b/led_control.py
@@ -170,7 +170,7 @@ def init():
     data = list()
     for x in range(len(leds)):
         if leds_size[x] == 3:
-            data.append((50,50,255))
+            data.append((20,20,127))
         elif leds_size[x] == 4:
             data.append((50,50,255,0))
         else:
@@ -178,9 +178,9 @@ def init():
     sendall(data)
     #time.sleep(50000)    
     fprint("Running start-up test sequence...")
-    for y in range(1):
+    for y in range(100):
         for x in range(len(leds)):
-            setpixel(5,5,5,x)
+            setpixel(0,0,150,x)
         sendall(data)
         #time.sleep(2)
         #alloffsmooth()
@@ -290,7 +290,7 @@ def close():
     time.sleep(0.5)
     sender.stop()
 
-def mapimage(image, fps=30):
+def mapimage(image, fps=90):
     global start
     while uptime() - start < 1/fps:
         time.sleep(0.00001)
diff --git a/map.png b/map.png
index 6f73f78..52f3cba 100644
Binary files a/map.png and b/map.png differ
diff --git a/process_video.py b/process_video.py
new file mode 100755
index 0000000..42b020a
--- /dev/null
+++ b/process_video.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+
+import cv2
+import banner_ivu_export
+import numpy as np
+from util import fprint
+
+class qr_reader():
+    camera = None
+    def __init__(self, ip, port):
+        self.camera = banner_ivu_export.DriveImg(ip, port)
+
+    def read_qr(self, tries=1):
+        print("Trying " + str(tries) + " frames.")
+        for x in range(tries):
+            try:
+                imgtype, img = self.camera.read_img()
+                #fprint(imgtype)
+                image_array = np.frombuffer(img, np.uint8)
+                img = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
+                #cv2.imshow('Image', img)
+                #cv2.waitKey(1)
+                detect = cv2.QRCodeDetector()
+                value, points, straight_qrcode = detect.detectAndDecode(img)
+                return value
+            except:
+                continue
+        return False
+        
+
+class video_streamer():
+    camera = None
+    def __init__(self, ip, port):
+        self.camera = banner_ivu_export.DriveImg(ip, port)
+
+    def get_frame(self):
+        try:
+            return self.camera.read_img()
+        except:
+            return False
+
+if __name__ == "__main__":
+    test = qr_reader("192.168.1.125", 32200)
+    while True:
+        fprint(test.read_qr(5))
\ No newline at end of file
diff --git a/read_datasheet.py b/read_datasheet.py
index a919f8d..ef54a7c 100755
--- a/read_datasheet.py
+++ b/read_datasheet.py
@@ -9,8 +9,10 @@ from PIL import Image
 import io
 import json
 from util import fprint
+import uuid
+from util import run_cmd
 
-def parse(filename, output_dir):
+def parse(filename, output_dir, partnum, dstype):
 
     # Extract table data
 
@@ -22,6 +24,7 @@ def parse(filename, output_dir):
     page = reader.pages[0]
     table_list = {}
     for table in tables:
+        table.df.infer_objects(copy=False)
         table.df.replace('', np.nan, inplace=True)
         table.df.dropna(inplace=True, how="all")
         table.df.dropna(inplace=True, axis="columns", how="all")
@@ -137,44 +140,104 @@ def parse(filename, output_dir):
 
 
         # multi-page table check
-        if table_name.isdigit() and len(tables) > 1:
-            fprint(table_name)
-            fprint(previous_table)
-            
-            
-            
-            
-            main_key = previous_table
-            cont_key = table_name
-            fprint(tables)
-            if vertical == False:
-                main_keys = list(tables[main_key].keys())
-                for i, (cont_key, cont_values) in enumerate(tables[cont_key].items()):
-                    if i < len(main_keys):
-                        fprint(tables[main_key][main_keys[i]])
-                        tables[main_key][main_keys[i]] = (tables[main_key][main_keys[i]] + (cont_key,) + cont_values)
-
-                del tables[table_name]
-
-            else:
-                for key in tables[cont_key].keys():
-                    tables[main_key][key] = tables[cont_key][key]
-                del tables[table_name]
+        if dstype == "Belden":
+            if table_name.isdigit() and len(tables) > 1:
+                fprint(table_name)
+                fprint(previous_table)
+                
+                
+                
+                
+                main_key = previous_table
+                cont_key = table_name
+                fprint(tables)
+                if vertical == False:
+                    main_keys = list(tables[main_key].keys())
+                    for i, (cont_key, cont_values) in enumerate(tables[cont_key].items()):
+                        if i < len(main_keys):
+                            fprint(tables[main_key][main_keys[i]])
+                            tables[main_key][main_keys[i]] = (tables[main_key][main_keys[i]] + (cont_key,) + cont_values)
+    
+                    del tables[table_name]
+    
+                else:
+                    for key in tables[cont_key].keys():
+                        tables[main_key][key] = tables[cont_key][key]
+                    del tables[table_name]
 
         previous_table = table_name
     
+    # remove multi-line values that occasionally squeak through
+    def replace_newlines_in_dict(d):
+        for key, value in d.items():
+            if isinstance(value, str):
+                # Replace \n with " " if the value is a string
+                d[key] = value.replace('\n', ' ')
+            elif isinstance(value, dict):
+                # Recursively call the function if the value is another dictionary
+                replace_newlines_in_dict(value)
+        return d
+    
+    tables = replace_newlines_in_dict(tables)
 
-    fprint(tables)
-    with open(output_dir + "/tables.json", 'w') as json_file:
-        json.dump(tables, json_file)
+    # summary
+
+    output_table = dict()
+    output_table["partnum"] = partnum
+    id = str(uuid.uuid4())
+    output_table["id"] = id
+    #output_table["position"] = id
+    #output_table["brand"] = brand
+    output_table["fullspecs"] = tables
+    output_table["searchspecs"] = {"partnum": partnum, **flatten(tables)}
+    
+    output_table["searchspecs"]["id"] = id
+    
 
 
+    print(output_table)
+
+    run_cmd("rm " + output_dir + "/*.json") # not reliable!
+    with open(output_dir + "/" + output_table["searchspecs"]["id"] + ".json", 'w') as json_file:
+        json.dump(output_table["searchspecs"], json_file)
+
+    return output_table
 
 
+def flatten(tables):
+    def convert_to_number(s):
+        try:
+            # First, try converting to an integer.
+            return int(s)
+        except ValueError:
+            # If that fails, try converting to a float.
+            try:
+                return float(s)
+            except ValueError:
+                # If it fails again, return the original string.
+                return s
+    out = dict()
+    print("{")
+    for table in tables.keys():
+        for key in tables[table].keys():
+            if len(key) < 64:
+                keyname = key
+            else:
+                keyname = key[0:64]
 
-    return tables
+            fullkeyname = (table + ": " + keyname).replace(".","")
+            if type(tables[table][key]) is not tuple:
+                out[fullkeyname] = convert_to_number(tables[table][key])
+                print("\"" + keyname + "\":", "\"" + str(out[fullkeyname]) + "\",")
+            elif len(tables[table][key]) == 1:
+                out[fullkeyname] = convert_to_number(tables[table][key][0])
+                
+                print("\"" + keyname + "\":", "\"" + str(out[fullkeyname]) + "\",")
+
+    print("}")
+    return out
 
     
 
 if __name__ == "__main__":
-    parse("test2.pdf", "10GXS13")
\ No newline at end of file
+    parse("test2.pdf", "cables/10GXS13", "10GXS13")
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index f53fce0..68b28c7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,6 +5,7 @@ pypdf2==2.12.1
 alive-progress
 requests
 git+https://github.com/Byeongdulee/python-urx.git
+psycopg2-binary
 pyyaml
 Flask
 selenium
diff --git a/run.py b/run.py
index 287e4fc..e2bedef 100755
--- a/run.py
+++ b/run.py
@@ -21,6 +21,7 @@ import led_control
 import server
 import asyncio
 import json
+import process_video
 
 
 
@@ -34,6 +35,7 @@ vm_ready = False
 killme = None
 #pool = None
 serverproc = None
+camera = None
 
 to_server_queue = Queue()
 from_server_queue = Queue()
@@ -78,7 +80,7 @@ def start_server_socket():
     app.run(host='0.0.0.0', port=5000)"""
     global to_server_queue
     global from_server_queue
-    
+    fprint("Starting WebSocket server...")
     websocket_process = server.start_websocket_server(to_server_queue, from_server_queue)
     
     # Example
@@ -86,9 +88,10 @@ def start_server_socket():
     
 
     while True:
+        #print("HI")
         if not from_server_queue.empty():
-            message = from_server_queue.get()
-            fprint(f"Message from client: {message}")
+            client_id, message = from_server_queue.get()
+            fprint(f"Message from client {client_id}: {message}")
 
             # Message handler
             try:
@@ -141,6 +144,13 @@ def start_server_socket():
                             fprint("")
                         elif call == "request":
                             fprint("")
+                            if data["enabled"] == True:
+                                # todo : send this to client
+                                p = Process(target=run_cmd, args=("./keyboard-up.ps1",))
+                                p.start()
+                            elif data["enabled"] == False:
+                                p = Process(target=run_cmd, args=("./keyboard-down.ps1",))
+                                p.start()
 
                     case "machine_settings":
                         fprint("machine_settings message")
@@ -202,10 +212,10 @@ def setup_server(pool):
     global led_ready
     global arm_ready
     global serverproc
+    global camera
 
     pool.apply_async(ur5_control.init, (config["arm"]["ip"],), callback=arm_start_callback)
     pool.apply_async(led_control.init, callback=led_start_callback)
-    #pool.apply_async(camera_control.init, callback=camera_start_callback)
     #pool.apply_async(sensor_control.init, callback=sensor_start_callback)
     serverproc = Process(target=start_server_socket)
     serverproc.start()
@@ -225,10 +235,11 @@ def setup_server(pool):
 
     if camera_ready is False:
         fprint("waiting for " + "Camera initilization" + " to complete...", sendqueue=to_server_queue)
-        while camera_ready is False:
-            sleep(0.1)
+        camera = process_video.qr_reader(config["cameras"]["banner"]["ip"], config["cameras"]["banner"]["port"])
+
     fprint("Camera initialized.", sendqueue=to_server_queue)
 
+    arm_ready = True
     if arm_ready is False:
         fprint("waiting for " + "UR5 initilization" + " to complete...", sendqueue=to_server_queue)
         while arm_ready is False:
@@ -251,6 +262,9 @@ def mainloop_server(pool):
         killall()
     counter = counter + 1
 
+    fprint("Looking for QR code...")
+    print(camera.read_qr(30))
+
 def run_loading_app():
     
     app = Flask(__name__)
diff --git a/server.py b/server.py
index 63ff41c..17dab98 100755
--- a/server.py
+++ b/server.py
@@ -50,32 +50,37 @@ def run_server():
 import asyncio
 import websockets
 from multiprocessing import Process, Queue
+from util import fprint
+import uuid
 
-connected_clients = set()
+connected_clients = {}
 
 async def handler(websocket, path, to_server_queue, from_server_queue):
     # Register websocket connection
-    connected_clients.add(websocket)
+    client_id = str(uuid.uuid4())
+    connected_clients[client_id] = websocket
     try:
         # Handle incoming messages
         async for message in websocket:
             #print(f"Received message: {message}")
-            from_server_queue.put(message)  # Put received message into from_server_queue
+            print(client_id)
+            from_server_queue.put((client_id, message))
     finally:
         # Unregister websocket connection
-        connected_clients.remove(websocket)
+        if client_id in connected_clients:
+            del connected_clients[client_id]
+            print(f"Client {client_id} connection closed")
 
 async def send_messages(to_server_queue):
     while True:
         if not to_server_queue.empty():
-            message = to_server_queue.get()
-            if connected_clients:  # Check if there are any connected clients
-                #await asyncio.wait([client.send(message) for client in connected_clients])
-                #await [client.send(message) for client in connected_clients]
-                for client in connected_clients:
+            client_id, message = to_server_queue.get()
+            if client_id in connected_clients:  # Send message to specific client
+                await connected_clients[client_id].send(message)
+            elif len(connected_clients) > 0:  # Broadcast message to all clients
+                for client in connected_clients.values():
                     await client.send(message)
-                
-        await asyncio.sleep(0.1)  # Prevent the loop from running too fast
+        await asyncio.sleep(0.001)
 
 def websocket_server(to_server_queue, from_server_queue):
     start_server = websockets.serve(lambda ws, path: handler(ws, path, to_server_queue, from_server_queue), "localhost", 9000)
diff --git a/util.py b/util.py
index 77b9c91..b957305 100755
--- a/util.py
+++ b/util.py
@@ -62,7 +62,7 @@ def fprint(msg, settings = None, sendqueue = None):
         
         print(logMsg)
         if (sendqueue is not None):
-            sendqueue.put("{ \"type\": \"log\", \"call\":\"send\", \"data\": \"" + logMsg + "\" }")
+            sendqueue.put(("*", "{ \"type\": \"log\", \"call\":\"send\", \"data\": \"" + logMsg + "\" }"))
         if (settings is not None):
             tmpList = settings["logMsg"]
             tmpList.append(logMsg)
@@ -111,9 +111,9 @@ def run_cmd(cmd):
         return completed
 
 def setup_child(sets=None):
-    #if not getattr(sys, "frozen", False):
-    #    sys.stdout = Logger(filename=find_data_file("output.log"))
-    #    sys.stderr = Logger(filename=find_data_file("output.log"))
+    if not getattr(sys, "frozen", False):
+        sys.stdout = Logger(filename=find_data_file("output.log"))
+        sys.stderr = Logger(filename=find_data_file("output.log"))
     if sets is not None:
         settings = sets
 
@@ -123,7 +123,7 @@ class Logger(object):
         self.terminal = sys.stdout
 
     def write(self, message):
-        self.log.write(message)
+        #self.log.write(message)
         #close(filename)
         #self.log = open(filename, "a")
         try: