├── 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 | -  handles all the API calls and refresh tokens (when necessary).
21 | -  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 | ### 
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:
 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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------