├── README.md ├── getparts.py ├── images ├── Untitled 1.png ├── Untitled 2.png ├── Untitled 3.png ├── Untitled 4.png ├── Untitled 5.png ├── Untitled 6.png ├── Untitled 7.png ├── Untitled.png ├── demo.gif └── ocr.jpg ├── tutorial.md └── webcam_example.py /README.md: -------------------------------------------------------------------------------- 1 | # What is getparts? 2 | 3 | Getparts is a Python3 tool takes a supplier barcode value as an input and returns component information. Currently works for most Digi-Key, Mouser, and some LCSC bags. 4 | 5 | ### Barcode Status 6 | | | 2D | 1D | 7 | |---------- |---- |---- | 8 | | Digi-Key | ✔ | ✔ | 9 | | Mouser | ✔ | ➖ | 10 | | LCSC | ✔ | ❌ | 11 | 12 | # Why? 13 | 14 | Nearly every electrical component ordered online will be packaged and marked with a barcode containing relevant part information. Supplier-specific API are starting to emerge, but I needed a single tool that would retrieve part info from a variety of suppliers with and without search API. 15 | 16 | # How? 17 | 18 | An example script is included that lets you use a webcam (laptop or usb webcam) to scan barcodes on component bags (Digikey, Mouser, and LCSC). This barcode value is fed into getparts which then tries a variety of techinques to retrieve the part information. Uses Python3. Works for 2D DataMatrix, QR, and 1D barcodes. 19 | 20 | - ![getparts.py](/getparts.py) handles all the API calls and refresh tokens (when necessary). 21 | - ![webcam_example.py](/webcam_example.py) is an example using a webcam. There are lots of debug messages to help others troubleshoot their own scripts. 22 | 23 | Some setup required. See 👇 for a guide setting up a Digikey/Mouser API and running the python script. 24 | ### ![📃 Step-by-step Tutorial](/tutorial.md) 25 | 26 | # In Action 27 | 28 |

29 | 30 | 31 |

32 | 33 | # Detailed Barcode Status 34 | 35 | - [ ] Digikey: DK provides a fully functional barcode API for product and invoice info. 👍 good job DK! 36 | - ✔ 2D: specific API exists for invoice and product info queries. QR code is data matrix format.
Always starts with `[)>` 37 | - ✔ 1D: specific API exists for invoice and product info queries. QR code is data matrix format. 38 | - [ ] Mouser: There's a part number query API, but no native barcode searching. 39 | - ✔ 2D: PN is extracted from DataMatrix (always starts with `>[)>`) and searched using Mouser's API 40 | - ➖ 1D: Script can read the barcodes, but currently has no way of telling which barcode value correlates to which property because there are seperate 1D barcodes for `Cust PO`, `Line Items`, `Mouser P/N`, `MFG P/N`, `QTY`, `COO`, and `Invoice No`. 41 | - [ ] LCSC: No API. So we have to scrape. 42 | - ✔ 2D: Some of my LCSC bags have QR barcodes (1 in 10 I'd guess). The QR code contains: `productCode`, `orderNo`, `pickerNo`,`pickTime`, and `checkCode`. So far all the tool can do is search LCSC for the PN but the user needs to navigate the page and extract the info. Need to write a javascript web scraper. 43 | - ❌ 1D: String ~10 characters in length. Can't extract anything useful from these.

44 | Since the majority of LCSC bags don't (currently) have QR codes, I'm curious how feasible OCR on the LCSC PN will be. Using tesseract:
![](/images/ocr.jpg) it seems doable, but this will heavily depend on user environment. More to come. 45 | 46 | This tool was developed to aid inventory management via [InvenTree](https://inventree.github.io/) 47 | -------------------------------------------------------------------------------- /getparts.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Python3 3 | 4 | API tool for electronic component suppliers (digikey, mouser, LCSC) 5 | https://github.com/maholli/getparts 6 | M.Holliday 7 | ''' 8 | 9 | import requests 10 | import json, re 11 | import os.path 12 | from os import path 13 | from types import SimpleNamespace 14 | from requests_html import HTMLSession 15 | from bs4 import BeautifulSoup 16 | oauth_headers = { 17 | 'Content-Type': 'application/x-www-form-urlencoded' 18 | } 19 | oauth_body = { 20 | 'client_id': "", 21 | 'client_secret': "", 22 | 'grant_type': 'refresh_token', 23 | 'refresh_token': '' 24 | } 25 | digi_headers = { 26 | "X-DIGIKEY-Client-Id": "", 27 | 'authorization': "", 28 | 'accept': "application/json" 29 | } 30 | mouser_headers = { 31 | 'Content-Type': "application/json", 32 | 'accept': "application/json" 33 | } 34 | 35 | def printlevel(level,text): 36 | print('\t'*level+str(text)) 37 | 38 | class API: 39 | RECORDS_FILE = 'api_records_digi.txt' 40 | def __init__(self,cred,debug=False): 41 | self.DEBUG=debug 42 | self.digi2D="https://api.digikey.com/Barcoding/v3/Product2DBarcodes/".encode('utf-8') 43 | self.digi1D="https://api.digikey.com/Barcoding/v3/ProductBarcodes/".encode('utf-8') 44 | self.digiPN="https://api.digikey.com/Search/v3/Products/".encode('utf-8') 45 | self.mouserPN="https://api.mouser.com/api/v1/search/partnumber?apiKey=".encode('utf-8') 46 | self.barcode=SimpleNamespace() 47 | self.query=SimpleNamespace() 48 | self.query.suppliers={ 49 | 'digikey':{ 50 | '1D':lambda:requests.get(url=self.digi1D+self.barcode.barcode, headers=digi_headers), 51 | '2D':lambda:requests.get(url=self.digi2D+self.barcode.barcode, headers=digi_headers), 52 | 'pn':lambda:requests.get(url=self.digiPN+self.barcode.response['DigiKeyPartNumber'].encode(),headers=digi_headers), 53 | }, 54 | 'mouser':{ 55 | '1D':lambda:print('mouser1d'), 56 | '2D':lambda:requests.post(url=self.mouserPN+cred['mouser_key'].encode('utf-8'),data=self.barcode.mfpn.encode(), headers=mouser_headers), 57 | }, 58 | 'lcsc':{ 59 | '1D':lambda:print('lcsc1d'), 60 | '2D':lambda:lcsc.scrape(self.barcode.supplierPN), 61 | } 62 | } 63 | 64 | digi_headers['X-DIGIKEY-Client-Id']=cred['client_id'] 65 | oauth_body['client_id']=cred['client_id'] 66 | oauth_body['client_secret']=cred['client_secret'] 67 | self.setup_body = {k: cred[k] for k in ('code','client_id','client_secret')} 68 | printlevel(0,'Looking for local API records file in local directory...') 69 | if not path.exists('api_records_digi.txt'): 70 | printlevel(2,'No records found!') 71 | printlevel(2,'Running API setup...') 72 | if self.api_setup(): 73 | printlevel(2,'API setup successful') 74 | else: 75 | printlevel(2,'API setup unsuccessful!') 76 | printlevel(2,'Must repeat OAuth approval step and try again with new authorization code') 77 | printlevel(3,'USE: https://api.digikey.com/v1/oauth2/authorize?response_type=code&client_id='+cred['client_id']+'&redirect_uri='+'https://localhost\n\n') 78 | raise Exception('New OAuth code required') 79 | else: 80 | printlevel(1,'Records file found...') 81 | try: 82 | with open(self.RECORDS_FILE,'r') as apidata: 83 | for line in apidata: 84 | if line.startswith('#'): 85 | continue 86 | else: 87 | latest = json.loads(line) 88 | latest_refresh_token=latest['refresh_token'] 89 | latest_access_token=latest['access_token'] 90 | oauth_body['refresh_token']=latest['refresh_token'] 91 | digi_headers['authorization']= "Bearer "+latest['access_token'] 92 | except Exception as e: 93 | printlevel(1,'Error loading/saving credentials to file: ',e) 94 | def api_setup(self): 95 | AUTH_URL="https://api.digikey.com/v1/oauth2/token".encode('utf-8') 96 | self.setup_body['grant_type']="authorization_code" 97 | self.setup_body['redirect_uri']="https://localhost" 98 | try: 99 | printlevel(2,'Sending authorization request...') 100 | x = requests.post(url=AUTH_URL, data=self.setup_body, headers=oauth_headers) 101 | response=x.json() 102 | if self.DEBUG: print(json.dumps(response,indent=3,sort_keys=True)) 103 | except Exception as e: 104 | print('\tERROR during API setup - POST:',e) 105 | return False 106 | 107 | if 'ErrorMessage' not in response: 108 | printlevel(2,'Authorzation request successful') 109 | printlevel(2,'Creating new records file: api_records_digi.txt') 110 | try: 111 | printlevel(2,'Saving authorization credentials to file...') 112 | with open(self.RECORDS_FILE,'a') as apidata: 113 | apidata.write(json.dumps(response,sort_keys=True)) 114 | apidata.write('\n') 115 | return True 116 | except Exception as e: 117 | printlevel(2,'ERROR during API setup - SAVING step:',e) 118 | return False 119 | else: 120 | if 'Invalid authCode' in response['ErrorMessage']: 121 | printlevel(2,'ERROR during API setup - authorization code has expired') 122 | else: 123 | printlevel(2,'ERROR during API setup - Error message in POST') 124 | return False 125 | 126 | def refresh_token(self): 127 | print('Bearer token expired, attempting to refresh...') 128 | x = requests.post(url='https://api.digikey.com/v1/oauth2/token', data=oauth_body, headers=oauth_headers) 129 | data=x.json() 130 | print('New token:', data) 131 | with open(self.RECORDS_FILE,'a') as apidata: 132 | try: 133 | print('Before Refresh:') 134 | print('\t',oauth_body['refresh_token']) 135 | print('\t',digi_headers['authorization']) 136 | apidata.write(json.dumps(data,sort_keys=True)) 137 | apidata.write('\n') 138 | oauth_body['refresh_token']=data['refresh_token'] 139 | digi_headers['authorization']= "Bearer "+data['access_token'] 140 | print('After Refresh:') 141 | print('\t',oauth_body['refresh_token']) 142 | print('\t',digi_headers['authorization']) 143 | print('Updated Records File: {}\n'.format(self.RECORDS_FILE)) 144 | return True 145 | except Exception as e: 146 | print('Refresh Error:',e) 147 | return False 148 | 149 | def search(self,scan,product_info=False): 150 | # Determine barcode type and supplier 151 | self.barcode.barcode=scan.data 152 | if self.DEBUG: print(scan) 153 | try: 154 | if 'QRCODE' in scan.type: 155 | self.barcode.type='2D' 156 | self.barcode.supplier='lcsc' 157 | supPN=re.split(r",",self.barcode.barcode.decode()) 158 | self.barcode.supplierPN=supPN[1][12:] 159 | elif 'CODE128' in scan.type: 160 | self.barcode.type='1D' 161 | if self.barcode.barcode.decode().isdecimal(): 162 | if len(self.barcode.barcode) > 10: 163 | self.barcode.supplier='digikey' 164 | else: 165 | print("Short barcode... possibly Mouser Line Item or QTY: ".format(self.barcode.barcode.decode())) 166 | else: 167 | self.barcode.supplier='mouser' 168 | else: 169 | print('Unknown supplier') 170 | except AttributeError: 171 | self.barcode.type='2D' 172 | if b'>[)>' in self.barcode.barcode: 173 | self.barcode.supplier='mouser' 174 | mfgpart=re.split(r"",self.barcode.barcode.decode()) 175 | print('mfgpart:',mfgpart) 176 | if '1P' in mfgpart[3]: 177 | self.barcode.mfpn="{\"SearchByPartRequest\": {\"mouserPartNumber\": \""+mfgpart[3][2:]+"\",}}" 178 | else: 179 | self.barcode.supplier='digikey' 180 | 181 | # make supplier-specific API query 182 | try: 183 | r=self.query.suppliers[self.barcode.supplier][self.barcode.type]() 184 | self.barcode.response=r.json() 185 | if 'ErrorMessage' in self.barcode.response: 186 | if 'Bearer token expired' in self.barcode.response['ErrorMessage']: 187 | if self.refresh_token(): 188 | r=self.query.suppliers[self.barcode.supplier][self.barcode.type]() 189 | self.barcode.response=r.json() 190 | else: 191 | print('Fatal error during token refresh ') 192 | return 193 | if product_info: 194 | self.barcode.type='pn' 195 | try: 196 | r=self.query.suppliers[self.barcode.supplier][self.barcode.type]() 197 | self.barcode.response.update(r.json()) 198 | except Exception as e: 199 | print('Error during Product Info request:',e) 200 | print(json.dumps(self.barcode.response,indent=3,sort_keys=True)) 201 | return self.barcode 202 | except Exception as e: 203 | print('Error during API request:',e) 204 | if self.DEBUG:print('Attributes: {}'.format(self.barcode)) 205 | return 206 | class lcscdata: 207 | def __init__(self,val): 208 | self.value=val 209 | def json(self): 210 | return self.value 211 | class lcsc: 212 | def scrape(pn): 213 | lcscPN=pn 214 | # create an HTML Session object 215 | session = HTMLSession() 216 | # Use the object above to connect to needed webpage 217 | r1 = session.get("https://lcsc.com/search?q="+lcscPN) 218 | # Run any JavaScript code on webpage 219 | r1.html.render() 220 | # Find absolute link for product page 221 | a=r1.html.find('body > div#lcsc.push-body > div#global_search.contianer > div.table-content > section > div > div#product_table_list > div.product-list-area.table-area > table') 222 | links=a[0].absolute_links 223 | for link in links: 224 | if lcscPN+'.html' in link: 225 | product_page=link 226 | # Load product page 227 | direct=session.get(product_page) 228 | soup = BeautifulSoup(direct.html.html, "lxml") 229 | # Find correct product table 230 | table=soup.find('table', attrs={'class':'products-specifications'}) # 2nd table 231 | table_body = table.find('tbody') 232 | rows = table_body.find_all('tr') 233 | result=lcscdata({}) 234 | for row in rows: 235 | cols = row.find_all('td') 236 | cols = [ele.text.strip() for ele in cols] 237 | line=[ele for ele in cols if ele] 238 | try: 239 | result.value.update({line[0]:line[1]}) 240 | except: 241 | pass 242 | return result -------------------------------------------------------------------------------- /images/Untitled 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maholli/getparts/3ab3795f78a840833ae0ea2a5d6d821212678c60/images/Untitled 1.png -------------------------------------------------------------------------------- /images/Untitled 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maholli/getparts/3ab3795f78a840833ae0ea2a5d6d821212678c60/images/Untitled 2.png -------------------------------------------------------------------------------- /images/Untitled 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maholli/getparts/3ab3795f78a840833ae0ea2a5d6d821212678c60/images/Untitled 3.png -------------------------------------------------------------------------------- /images/Untitled 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maholli/getparts/3ab3795f78a840833ae0ea2a5d6d821212678c60/images/Untitled 4.png -------------------------------------------------------------------------------- /images/Untitled 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maholli/getparts/3ab3795f78a840833ae0ea2a5d6d821212678c60/images/Untitled 5.png -------------------------------------------------------------------------------- /images/Untitled 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maholli/getparts/3ab3795f78a840833ae0ea2a5d6d821212678c60/images/Untitled 6.png -------------------------------------------------------------------------------- /images/Untitled 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maholli/getparts/3ab3795f78a840833ae0ea2a5d6d821212678c60/images/Untitled 7.png -------------------------------------------------------------------------------- /images/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maholli/getparts/3ab3795f78a840833ae0ea2a5d6d821212678c60/images/Untitled.png -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maholli/getparts/3ab3795f78a840833ae0ea2a5d6d821212678c60/images/demo.gif -------------------------------------------------------------------------------- /images/ocr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maholli/getparts/3ab3795f78a840833ae0ea2a5d6d821212678c60/images/ocr.jpg -------------------------------------------------------------------------------- /tutorial.md: -------------------------------------------------------------------------------- 1 | # How To 2 | 3 | 1. Register/login for digikey API: [https://developer.digikey.com/](https://developer.digikey.com/) 4 | 2. Go to organizations tab: 5 | 6 | ![images/Untitled.png](images/Untitled.png) 7 | 8 | (we're going to skip making a sandbox version of the app) 9 | 10 | 3. Create new organization 11 | 4. Click Production Apps next to your new organization 12 | 13 | ![images/Untitled%201.png](images/Untitled%201.png) 14 | 15 | 5. Create Production App 16 | 6. Fill in the details, paying special attention to the following: 17 | - **Name** - doesn't matter 18 | - **OAuth Callback** - [`https://localhost`](https://localhost/) 19 | - **Description** - doesn't matter 20 | - Select at least the following: 21 | 22 | ![images/Untitled%202.png](images/Untitled%202.png) 23 | 24 | 7. Click add production app 25 | 8. Next screen should look like what's shown below. Click the app name (circled) 26 | 27 | ![images/Untitled%203.png](images/Untitled%203.png) 28 | 29 | 9. Open the `barcode_scan.py` file in a text editor and copy and paste the your Client ID (`BBB`) and Client Secret (`CCC`) from your app credentials into their respective fields: 30 | 31 | (note: you wont have a value for `code` yet) 32 | 33 | app_credentials= { 34 | 'code': 'AAA', 35 | 'client_id': "BBB", 36 | 'client_secret': "CCC", 37 | 'mouser_key': "DDD" 38 | } 39 | 40 | 10. Now ensure you have the necessary python3 libraries: 41 | 42 | Windows Powershell, Linux, & MacOS: 43 | 44 | pip3 install pyzbar, pylibmtx, opencv-python 45 | 46 | and if you want to use a webcam to scan barcodes: 47 | 48 | pip3 install opencv-python 49 | 50 | (note: your system may use `pip` instead of `pip3`. Either way, just make sure you're installing python3 packages) 51 | 52 | 11. Save and run the `barcode_scan.py` file using python3 and you will get an error message. 53 | 54 | Windows Powershell: 55 | 56 | PS C:\Users\USER\DEMO_DIRECTORY> python barcode_scan.py 57 | 58 | Linux/MacOS 59 | 60 | USER@DESKTOP:~/DEMO_DIRECTORY$ python barcode_scan.py 61 | 62 | **The error message should look similar to the one below, otherwise, troubleshoot the errors and try again.** 63 | 64 | ![images/Untitled%204.png](images/Untitled%204.png) 65 | 66 | 12. With a similar error message as shown above, proceed by copying the URL underlined in red. 67 | 13. Open a new tab in your browser and paste the previously copied URL 68 | 14. You should be directed to a page that looks like this (you may need to log in). Click "Allow" 69 | 70 | ![images/Untitled%205.png](images/Untitled%205.png) 71 | 72 | 15. Paste the copied code into the `barcode_scan.py` file replacing the `AAA` term: 73 | 74 | app_credentials= { 75 | 'code': 'AAA', 76 | 'client_id': "BBB", 77 | 'client_secret': "CCC", 78 | 'mouser_key': "DDD" 79 | } 80 | 81 | 16. After updating the 3 `app_credential` fields, save the `barcode_scan.py` file and run again. 82 | 83 | (note: the OAuth code is only good for 60 seconds. If you get an error saying the code expired, just navigate to the URL again and repeat the process) 84 | 85 | 17. A the first successful `barcode_scan.py` should look like this when starting: 86 | 87 | ![images/Untitled%206.png](images/Untitled%206.png) 88 | 89 | Running subsequent `barcode_scan.py` (after the first successful startup) will look like this: 90 | 91 | (you shouldn't need to do the OAuth code any longer) 92 | 93 | ![images/Untitled%207.png](images/Untitled%207.png) 94 | 95 | 18. If you have a webcam the example script will try and connect. 96 | 97 | 19. To return detailed product information, change `product_info=False` to `product_info=True`. 98 | 99 | ## Mouser Setup 100 | 101 | Mouser's API is easier to set up but much more limited in its capability. 102 | 103 | 1. Navigate to https://www.mouser.com/MyMouser/MouserSearchApplication.aspx and register for an API account 104 | 105 | 2. You'll recieve an email shortly containing an "API Key." 106 | 107 | 3. Enter the API key into the `DDD` field in barcode_scan.py: 108 | 109 | app_credentials= { 110 | 'code': 'AAA', 111 | 'client_id': "BBB", 112 | 'client_secret': "CCC", 113 | 'mouser_key': "DDD" 114 | } 115 | 116 | -------------------------------------------------------------------------------- /webcam_example.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Python3 3 | 4 | Example barcode scanner for electronic component suppliers (digikey, mouser, LCSC) 5 | https://github.com/maholli/getparts 6 | M.Holliday 7 | ''' 8 | 9 | from pyzbar import pyzbar 10 | from pylibdmtx import pylibdmtx 11 | import time 12 | import cv2, codecs 13 | import numpy as np 14 | import getparts 15 | import os.path 16 | from os import path 17 | 18 | app_credentials= { 19 | 'code': 'AAA', 20 | 'client_id': "BBB", 21 | 'client_secret': "CCC", 22 | 'mouser_key': "DDD" 23 | } 24 | 25 | # initialize barcode_api with our API credentials 26 | api = getparts.API(app_credentials,debug=False) 27 | state='Searching' 28 | states={ 29 | 'Searching':(0,0,255), 30 | 'Found':(0,255,0), 31 | 'Duplicate':(0,165,255), 32 | } 33 | 34 | # File to save barcodes 35 | barcodefile='barcodes.txt' 36 | found = set() 37 | poly=np.array([[[0,0],[0,0],[0,0],[0,0]]],np.int32) 38 | 39 | # initialize the video stream and allow the camera sensor to warm up 40 | print("Starting video stream...") 41 | vs=cv2.VideoCapture(0) 42 | if vs is None or not vs.isOpened(): 43 | raise TypeError('Error starting video stream\n\n') 44 | 45 | while True: 46 | code=False 47 | # read frame from webcam 48 | _,frame2 = vs.read() 49 | # check for a data matrix barcode 50 | barcodes = pylibdmtx.decode(frame2,timeout=10) 51 | if barcodes: 52 | code=True 53 | # if no data matrix, check for any other barcodes 54 | else: 55 | barcodes=pyzbar.decode(frame2) 56 | if barcodes: 57 | code=True 58 | if code: 59 | for item in barcodes: 60 | barcodeData = item.data 61 | # find and draw barcode outline 62 | try: 63 | pts=[] 64 | [pts.append([i.x,i.y]) for i in item.polygon] 65 | poly=np.array([pts],np.int32) 66 | cv2.polylines(frame2, [poly], True, (0,0,255),2) 67 | except AttributeError: 68 | # data matrix 69 | (x, y, w, h) = item.rect 70 | cv2.rectangle(frame2,(x,y),(x+w, y+h),(0,0,255),2) 71 | # if we haven't seen this barcode this session, add it to our list 72 | if barcodeData not in found: 73 | state='Found' 74 | found.add(barcodeData) 75 | print(barcodeData.decode()) 76 | # query the barcode_api.py for barcode 77 | result = api.search(item,product_info=False) 78 | with codecs.open(barcodefile,'a', encoding='latin-1') as file: 79 | file.write('{}\n'.format(codecs.decode(barcodeData,'latin-1'))) 80 | file.flush() 81 | else: 82 | state='Duplicate' 83 | code=False 84 | else: 85 | state='Searching' 86 | # update the video stream window 87 | cv2.putText(frame2,str(state),(10,10),cv2.FONT_HERSHEY_SIMPLEX,0.5,states[state],2,cv2.LINE_AA) 88 | cv2.imshow("Barcode Scanner", frame2) 89 | key = cv2.waitKey(1) & 0xFF 90 | 91 | # if the `q` key was pressed, break from the loop 92 | if key == ord("q"): 93 | break 94 | 95 | print("Cleaning up...") 96 | cv2.destroyAllWindows() 97 | --------------------------------------------------------------------------------