Compare commits

...

16 Commits

6 changed files with 119 additions and 71 deletions

View File

@ -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
~~~

View File

@ -1,7 +0,0 @@
{
"host": "localhost",
"port": 3306,
"username": "partdb",
"password": "password",
"database": "partdb"
}

View File

@ -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

View File

@ -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()

View File

@ -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,

View File

@ -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