Compare commits
16 Commits
f87025c5a6
...
f75a1939a0
Author | SHA1 | Date |
---|---|---|
King Kévin | f75a1939a0 | |
King Kévin | b6f11afe3f | |
King Kévin | 199ac09b69 | |
King Kévin | 55234f0d7b | |
King Kévin | b6cc7ef060 | |
King Kévin | d530af9677 | |
King Kévin | f1a02d8fae | |
King Kévin | 3395d36c3c | |
King Kévin | cd601ede89 | |
King Kévin | dfe0f5c924 | |
King Kévin | a60e3063d1 | |
King Kévin | 149b9c9676 | |
King Kévin | ceeb101130 | |
King Kévin | feaee52376 | |
King Kévin | 4d891f17e7 | |
King Kévin | b02832b8d3 |
69
README.md
69
README.md
|
@ -7,6 +7,8 @@ Once the service is installed, there is a single page to search, add, delete, or
|
|||
The only aspect not handled by the web frontend is the attachment deletion.
|
||||
Just remove the files directly on the server under `public/attachments/<part_name>/`.
|
||||
|
||||
To add a part, start with the fresh home page without element selected, file the fields (at least the name), and click the update/add button.
|
||||
You can then find it using the search box.
|
||||
To import an LCSC part, simply go to the `/import/lcsc/Cxxxx` page and the part will be added to the database.
|
||||
|
||||
goals
|
||||
|
@ -39,7 +41,7 @@ components
|
|||
|
||||
Part information is:
|
||||
|
||||
- stored in a MariaDB/mySQL database
|
||||
- stored in a sqlite3 database
|
||||
- retrieved using a sinatra HTTP/REST server
|
||||
- displayed using minimal HTML+JS pages
|
||||
|
||||
|
@ -49,25 +51,64 @@ installation
|
|||
install software dependencies (here for Arch linux):
|
||||
|
||||
~~~
|
||||
sudo pacman -S mariadb
|
||||
sudo pacman -S ruby-sinatra ruby-webrick
|
||||
pikaur -S ruby-mysql2
|
||||
sudo pacman -S sqlite ruby ruby-rake
|
||||
gem install json sqlite3 sinatra puma
|
||||
~~~
|
||||
|
||||
configure database
|
||||
create database
|
||||
|
||||
~~~
|
||||
sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql
|
||||
sudo mysql
|
||||
CREATE DATABASE partdb;
|
||||
CREATE USER 'partdb'@localhost IDENTIFIED BY 'password';
|
||||
GRANT ALL PRIVILEGES ON partdb.* TO 'partdb'@localhost;
|
||||
exit
|
||||
mysql --user=partdb --password=password --database=partdb < schema.sql
|
||||
sqlite partdb.db < schema.sql
|
||||
~~~
|
||||
|
||||
put the database access information in `credentials.json` and run `server.rb`.
|
||||
now just run `server.rb`.
|
||||
now go to http://localhost:4245 and you are ready to use it.
|
||||
the port is defined in `server.rb`.
|
||||
the port and database file is defined in `server.rb`.
|
||||
|
||||
use a proxy web server to handle the authentication, TLS, compression, ...
|
||||
|
||||
KiCAD
|
||||
=====
|
||||
|
||||
KiCAD can have a [KiCad database library](https://docs.kicad.org/master/en/eeschema/eeschema.html#database_libraries).
|
||||
Running the `kicad_lib.rb` script will create tables (actually views) for each partdb category to be used by KiCAD.
|
||||
The views are added without the `partdb.db` database file itself.
|
||||
The script will also create the `partdb.kicad_dbl` library file.
|
||||
This is the file to be used by KiCAD to add the database library.
|
||||
But first you also need to install and configure ODBC so KiCAD can actually access the database:
|
||||
|
||||
~~~
|
||||
# install ODBC
|
||||
sudo pacman -S unixodbc
|
||||
# install OBDC sqlite driver
|
||||
pikaur -S sqliteodbc
|
||||
# fix ODBC sqlite configuration
|
||||
sudo sed -i 's|/usr/lib64/libsqlite3odbc.so|/usr/lib/libsqlite3odbc.so|g' /etc/odbcinst.ini
|
||||
sudo sed -i 's|/usrl/lib64/libsqlite3odbc.so|/usr/lib/libsqlite3odbc.so|g' /etc/odbcinst.ini
|
||||
sudo sed -i 's|/usr/lib64/libsqliteodbc.so|/usr/lib/libsqlite3odbc.so|g' /etc/odbcinst.ini
|
||||
sudo sed -i 's|/usrl/lib64/libsqliteodbc.so|/usr/lib/libsqlite3odbc.so|g' /etc/odbcinst.ini
|
||||
# add database
|
||||
cat << EOF | sudo tee -a /etc/odbc.ini
|
||||
[partdb]
|
||||
Description=electronic parts database
|
||||
Driver=SQLite
|
||||
Database=<path to partdb.db>
|
||||
Timeout=2000
|
||||
EOF
|
||||
# ensure it works
|
||||
isql partdb
|
||||
SELECT * FROM part LIMIT 1;
|
||||
exit
|
||||
~~~
|
||||
|
||||
service
|
||||
=======
|
||||
|
||||
For convenience you can install the systemD service.
|
||||
Be sure to adjust the path to the partdb script.
|
||||
|
||||
~~~
|
||||
mkdir -p ~/.local/share/systemd/user/
|
||||
cp partdb.service ~/.local/share/systemd/user/
|
||||
systemctl --user start partdb.service
|
||||
~~~
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"username": "partdb",
|
||||
"password": "password",
|
||||
"database": "partdb"
|
||||
}
|
32
kicad_lib.rb
32
kicad_lib.rb
|
@ -7,15 +7,14 @@ https://docs.kicad.org/master/en/eeschema/eeschema.html#database_libraries
|
|||
this script creates views from the partdb table so to create a database for KiCAD
|
||||
if wil also output the KiCad database library file
|
||||
=end
|
||||
require 'mysql2'
|
||||
require 'sqlite3'
|
||||
require 'json'
|
||||
|
||||
CREDENTIALS = "credentials.json"
|
||||
raise "database information #{CREDENTIALS} do not exist" unless File.file? CREDENTIALS
|
||||
credentials = {}
|
||||
JSON.parse(IO.read(CREDENTIALS)).each {|key,value| credentials[key.to_sym] = value}
|
||||
Mysql2::Client.default_query_options.merge!(:as => :hash)
|
||||
db = Mysql2::Client.new(credentials)
|
||||
# database file
|
||||
DB_PATH = "partdb.db"
|
||||
raise "DB file #{DB_PATH} does not exist" unless File.file? DB_PATH
|
||||
db = SQLite3::Database.new(DB_PATH)
|
||||
db.results_as_hash = true
|
||||
KICAD_FILE = "partdb.kicad_dbl"
|
||||
DEBUG = false # print debug information
|
||||
|
||||
|
@ -25,7 +24,8 @@ prop2view = [{property: "kicad_symbol"}, {property: "kicad_footprint"}, {propert
|
|||
prop2view.each do |prop|
|
||||
puts prop[:property]
|
||||
prop[:table] = "part_" + prop[:property].downcase.gsub(/[- \/]/,"_")
|
||||
view = "CREATE OR REPLACE VIEW #{prop[:table]} AS SELECT properties.part AS part, properties.value AS #{prop[:property]} FROM properties JOIN property ON property.id = properties.property WHERE property.name = '#{prop[:property]}'"
|
||||
db.query("DROP VIEW IF EXISTS #{prop[:table]}")
|
||||
view = "CREATE VIEW #{prop[:table]} AS SELECT properties.part AS part, properties.value AS #{prop[:property]} FROM properties JOIN property ON property.id = properties.property WHERE property.name = '#{prop[:property]}'"
|
||||
puts view if DEBUG
|
||||
db.query(view)
|
||||
end
|
||||
|
@ -37,7 +37,8 @@ dist2view = [{property: "LCSC"}, {property: "JLCPCB"}, {property: "DigiKey"}]
|
|||
dist2view.each do |prop|
|
||||
puts prop[:property]
|
||||
prop[:table] = "part_" + prop[:property].downcase.gsub(/[- \/]/,"_")
|
||||
view = "CREATE OR REPLACE VIEW #{prop[:table]} AS SELECT distribution.part AS part, distribution.sku AS #{prop[:property]} FROM distribution JOIN distributor ON distributor.id = distribution.distributor WHERE distributor.name = '#{prop[:property]}'"
|
||||
db.query("DROP VIEW IF EXISTS #{prop[:table]}")
|
||||
view = "CREATE VIEW #{prop[:table]} AS SELECT distribution.part AS part, distribution.sku AS #{prop[:property]} FROM distribution JOIN distributor ON distributor.id = distribution.distributor WHERE distributor.name = '#{prop[:property]}'"
|
||||
puts view if DEBUG
|
||||
db.query(view)
|
||||
end
|
||||
|
@ -57,12 +58,13 @@ categories.uniq!
|
|||
categories.each do |category|
|
||||
puts category
|
||||
table = "kicad_lib_" + category.downcase.gsub(/[- \/]/,"_")
|
||||
view = "CREATE OR REPLACE VIEW #{table} AS "
|
||||
view += "SELECT part.id, part.name, "
|
||||
prop2view.each do |prop|
|
||||
db.query("DROP VIEW IF EXISTS #{table}")
|
||||
view = "CREATE VIEW #{table} AS "
|
||||
view += "SELECT part.id, part.name, part.description, "
|
||||
(prop2view + dist2view).each do |prop|
|
||||
view += "COALESCE(#{prop[:table]}1.#{prop[:property]},#{prop[:table]}2.#{prop[:property]}) AS #{prop[:property]}, "
|
||||
end
|
||||
view += "part.description FROM part "
|
||||
view += "COALESCE(part.datasheet,parent.datasheet) AS datasheet FROM part "
|
||||
view += "LEFT JOIN part AS parent ON parent.id = part.family "
|
||||
(prop2view + dist2view).each do |prop|
|
||||
view += "LEFT JOIN #{prop[:table]} AS #{prop[:table]}1 ON #{prop[:table]}1.part = part.id "
|
||||
|
@ -76,6 +78,8 @@ categories.each do |category|
|
|||
library = {name: category, table: table, key: "name", symbols: "kicad_symbol", footprints: "kicad_footprint"}
|
||||
library[:properties] = {description: "description"}
|
||||
library[:fields] = []
|
||||
library[:fields] << {name: "Description", column: "description", visible_on_add: false, visible_in_chooser: true, show_name: false, inherit_properties: true}
|
||||
library[:fields] << {name: "Datasheet", column: "datasheet", visible_on_add: false, visible_in_chooser: true, show_name: false, inherit_properties: true}
|
||||
(prop2view + dist2view).each do |prop|
|
||||
next if prop[:property].start_with? "kicad_"
|
||||
library[:fields] << {name: prop[:property], column: prop[:property], visible_on_add: false, visible_in_chooser: true, show_name: false, inherit_properties: true}
|
||||
|
@ -85,5 +89,5 @@ end
|
|||
|
||||
puts "writing KiCad database library file to #{KICAD_FILE}"
|
||||
File.open(KICAD_FILE, "w") do |file|
|
||||
file.write kicad_dbl.to_json
|
||||
file.write JSON.pretty_generate(kicad_dbl)
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
// the last search query
|
||||
var last_search = null;
|
||||
var next_search = null;
|
||||
// the collection of parts
|
||||
var parts = null;
|
||||
// last selected part
|
||||
|
@ -21,23 +22,35 @@ function search()
|
|||
{
|
||||
const terms = document.getElementById('terms');
|
||||
if (terms && terms.value && terms.value.length >= 3) {
|
||||
} else {
|
||||
return;
|
||||
const current_search = '/search?terms=' + terms.value;
|
||||
do_search(current_search);
|
||||
}
|
||||
}
|
||||
|
||||
last_search = '/search?terms=' + terms.value;
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', last_search, true);
|
||||
xhr.onload = function() {
|
||||
if (decodeURI(this.responseURL).endsWith(last_search)) {
|
||||
parts = JSON.parse(this.response);
|
||||
results();
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
console.log("search call failed");
|
||||
};
|
||||
xhr.send();
|
||||
function do_search(current_search)
|
||||
{
|
||||
if (null == last_search) { // no request running
|
||||
last_search = current_search;
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', last_search, true);
|
||||
xhr.onload = function() {
|
||||
if (decodeURI(this.responseURL).endsWith(last_search)) {
|
||||
parts = JSON.parse(this.response);
|
||||
results();
|
||||
}
|
||||
last_search = null;
|
||||
if (next_search) {
|
||||
do_search(next_search);
|
||||
next_search = null;
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
console.log("search call failed");
|
||||
};
|
||||
xhr.send();
|
||||
} else {
|
||||
next_search = current_search; // save for next search
|
||||
}
|
||||
}
|
||||
|
||||
function results()
|
||||
|
|
18
schema.sql
18
schema.sql
|
@ -1,9 +1,9 @@
|
|||
-- enable foreign key support in sqlite
|
||||
--PRAGMA foreign_keys = ON;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- part manufacturer
|
||||
CREATE TABLE IF NOT EXISTS manufacturer (
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY, -- index
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- index
|
||||
name TEXT NOT NULL UNIQUE, -- manufacturer (expanded version, without legal form)
|
||||
nick TEXT, -- manufacturer nickname
|
||||
partof INTEGER, -- if the manufacturer has been acquired or is part of another
|
||||
|
@ -14,7 +14,7 @@ CREATE TABLE IF NOT EXISTS manufacturer (
|
|||
|
||||
-- part distributor
|
||||
CREATE TABLE IF NOT EXISTS distributor (
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY, -- index
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- index
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
homepage TEXT, -- URL to home page
|
||||
product_page TEXT -- URL to product page (%s is replace by sku)
|
||||
|
@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS distributor (
|
|||
|
||||
-- the part itself
|
||||
CREATE TABLE IF NOT EXISTS part (
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY, -- index
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- index
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT, -- a short (searchable) description
|
||||
details TEXT, -- even more part details than just in the description
|
||||
|
@ -48,7 +48,7 @@ CREATE TABLE IF NOT EXISTS assembly (
|
|||
|
||||
-- a part at a distributor
|
||||
CREATE TABLE IF NOT EXISTS distribution (
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY, -- index
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- index
|
||||
part INTEGER NOT NULL, -- the part
|
||||
distributor INTEGER NOT NULL, -- the part distributor
|
||||
sku TEXT NOT NULL, -- distributor part number
|
||||
|
@ -58,13 +58,13 @@ CREATE TABLE IF NOT EXISTS distribution (
|
|||
|
||||
-- part property
|
||||
CREATE TABLE IF NOT EXISTS property (
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY, -- index
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- index
|
||||
name TEXT NOT NULL -- property name (min V, ...)
|
||||
);
|
||||
|
||||
-- property value
|
||||
CREATE TABLE IF NOT EXISTS properties (
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY, -- index
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- index
|
||||
part INTEGER NOT NULL, -- the part
|
||||
property INTEGER NOT NULL,
|
||||
value TEXT,
|
||||
|
@ -74,14 +74,14 @@ CREATE TABLE IF NOT EXISTS properties (
|
|||
|
||||
-- part location
|
||||
CREATE TABLE IF NOT EXISTS location (
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY, -- index
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- index
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
container TEXT -- container type
|
||||
);
|
||||
|
||||
-- local stock
|
||||
CREATE TABLE IF NOT EXISTS inventory (
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY, -- index
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- index
|
||||
part INTEGER NOT NULL,
|
||||
location INTEGER,
|
||||
quantity INTEGER NOT NULL,
|
||||
|
|
21
server.rb
21
server.rb
|
@ -8,23 +8,22 @@ Copyright (C) 2023 King Kévin <kingkevin@cuvoodoo.info>
|
|||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
to install sinatra
|
||||
sudo pacman -S ruby-sinatra ruby-webrick
|
||||
pikaur -S ruby-mysql2
|
||||
gem install sinatra puma
|
||||
=end
|
||||
require 'set'
|
||||
require 'mysql2'
|
||||
require 'sqlite3'
|
||||
require 'json'
|
||||
require 'sinatra'
|
||||
require 'uri'
|
||||
require 'net/http'
|
||||
require 'cgi'
|
||||
|
||||
# allow dumping crashes in browser
|
||||
DEBUG = false
|
||||
# maximum number of parts returned
|
||||
PARTS_LIMIT = 100
|
||||
# credentials for database
|
||||
CREDENTIALS = "credentials.json"
|
||||
# database file
|
||||
DB_PATH = "partdb.db"
|
||||
raise "DB file #{DB_PATH} does not exist" unless File.file? DB_PATH
|
||||
# folder name for served pages
|
||||
PUBLIC = "public"
|
||||
# folder name for part attachments (in PUBLIC)
|
||||
|
@ -32,8 +31,6 @@ ATTACHMENTS = "attachments"
|
|||
# port for this service
|
||||
PORT = 4245
|
||||
|
||||
raise "database information #{CREDENTIALS} do not exist" unless File.file? CREDENTIALS
|
||||
|
||||
# open server
|
||||
configure do
|
||||
if DEBUG then
|
||||
|
@ -63,10 +60,8 @@ before do
|
|||
# all replies are only JSON
|
||||
content_type 'application/json'
|
||||
# open database
|
||||
credentials = {}
|
||||
JSON.parse(IO.read(CREDENTIALS)).each {|key,value| credentials[key.to_sym] = value}
|
||||
Mysql2::Client.default_query_options.merge!(:as => :hash)
|
||||
@db = Mysql2::Client.new(credentials)
|
||||
@db = SQLite3::Database.new(DB_PATH)
|
||||
@db.results_as_hash = true
|
||||
end
|
||||
|
||||
after do
|
||||
|
@ -314,6 +309,8 @@ def add_part(part)
|
|||
field = "location"
|
||||
part[field] = nil if part[field] and 0 == part[field].length
|
||||
if part[field] then
|
||||
delete = @db.prepare("DELETE FROM inventory WHERE part = ?")
|
||||
delete.execute(part["id"])
|
||||
statement = @db.prepare("SELECT id FROM #{field} WHERE LOWER(name) = ?")
|
||||
ref = statement.execute(part[field].downcase).to_a[0]
|
||||
unless ref then
|
||||
|
|
Loading…
Reference in New Issue