18 Commits

Author SHA1 Message Date
e9d30b63a5 flake.lock: bump 2025-06-07 15:13:32 +02:00
0844843e59 remove all the image related things from dibbler service 2025-05-17 20:05:35 +02:00
70677f7f79 db: handle database.url_file 2025-05-17 19:19:10 +02:00
4a4f0e6947 module.nix: config -> settings 2025-05-05 14:52:28 +02:00
a4d10ad0c7 Merge pull request 'Seed test data' () from seed_test into master
Reviewed-on: 
Reviewed-by: Oystein Kristoffer Tveit <oysteikt@pvv.ntnu.no>
2025-03-30 21:45:37 +02:00
a654baba11 ruff format 2025-03-30 21:44:37 +02:00
e69d04dcd0 mock script, og mock data. 2025-03-29 22:48:30 +01:00
b2a6384f31 la tilbake uv, en project manager 2025-03-29 22:46:30 +01:00
4f89765070 ignorer bifiler fra hatchling 2025-03-29 22:42:36 +01:00
914e5b4e50 fjerner __pyachce__, fra repo tracking 2025-03-29 22:37:11 +01:00
de20bad7dd remove conf.py 2025-03-19 18:47:23 +01:00
4bab5e7e21 treewide: fix brother-ql usage 2025-03-19 18:47:16 +01:00
b85a6535fe shell.nix: add python with all packages 2025-03-19 18:14:42 +01:00
22a09b4177 README: add more information 2025-03-19 18:06:40 +01:00
c39b15d1a8 .envrc: init 2025-03-19 17:50:48 +01:00
122ac2ab18 treewide: update everything nix 2025-03-19 17:50:14 +01:00
28228beccd pyproject.toml: remove invalid license
This license field was added without any of the earlier contributors
consent on accident. It is not valid
2025-03-17 21:03:55 +01:00
8a6a0c12ba Merge pull request from Programvareverkstedet/restructure-project
Restructure project
2023-09-02 21:18:04 +02:00
22 changed files with 320 additions and 148 deletions

1
.envrc Normal file

@ -0,0 +1 @@
use flake

5
.gitignore vendored

@ -1,8 +1,9 @@
result
result-*
**/__pycache__
dibbler.egg-info
dist
test.db
.ruff_cache
.ruff_cache

@ -2,13 +2,31 @@
EDB-system for PVVVV
## Hva er dette?
Dibbler er et system laget av PVVere for PVVere for å byttelåne både matvarer og godis.
Det er designet for en gammeldags VT terminal, og er laget for å være enkelt både å bruke og å hacke på.
Programmet er skrevet i Python, og bruker en sql database for å lagre data.
Samlespleiseboden er satt opp slik at folk kjøper inn varer, og får dibblerkreditt, og så kan man bruke
denne kreditten til å kjøpe ut andre varer. Det er ikke noen form for authentisering, så hele systemet er basert på tillit.
Det er anbefalt å koble en barkodeleser til systemet for å gjøre det enklere å både legge til og kjøpe varer.
## Kom i gang
Installer python, og lag og aktiver et venv. Installer så avhengighetene med `pip install`.
Deretter kan du kjøre programmet med
```console
python -m dibbler -c example-config.ini create-db
python -m dibbler -c example-config.ini loop
```
## Nix
### Hvordan kjøre
`nix run github:Prograrmvarverkstedet/dibbler`
### Bygge nytt image
### Bygge nytt image
For å bygge et image trenger du en builder som takler å bygge for arkitekturen du skal lage et image for.
@ -16,16 +34,16 @@ For å bygge et image trenger du en builder som takler å bygge for arkitekturen
Flaket exposer en modul som autologger inn med en bruker som automatisk kjører dibbler, og setter opp et minimalistisk miljø.
Før du bygger imaget burde du endre conf.py lokalt til å inneholde instillingene dine. **NB: Denne kommer til å ligge i nix storen.**
Før du bygger imaget burde du kopiere og endre `example-config.ini` lokalt til å inneholde instillingene dine. **NB: Denne kommer til å ligge i nix storen, ikke si noe her som du ikke vil at moren din skal høre.**
Du kan også endre hvilken conf.py som blir brukt direkte i pakken eller i modulen.
Du kan også endre hvilken config-fil som blir brukt direkte i pakken eller i modulen.
Se eksempelet for hvordan skrot er satt opp i flake.nix
Se eksempelet for hvordan skrot er satt opp i `flake.nix` og `nix/skrott.nix`
### Bygge image for skrot
Skrot har et image definert i flake.nix:
1. endre conf.py
1. endre `example-config.ini`
2. `nix build .#images.skrot`
3. ???
4. non-profit
4. non-profit

13
conf.py

@ -1,13 +0,0 @@
db_url = "postgresql://robertem@127.0.0.1/pvvvv"
quit_allowed = True
stop_allowed = False
show_tracebacks = True
input_encoding = "utf8"
low_credit_warning_limit = -100
user_recent_transaction_limit = 100
# See https://pypi.org/project/brother_ql/ for label types
# Set rotate to False for endless labels
label_type = "62"
label_rotate = False

@ -1,4 +0,0 @@
{ pkgs ? import <nixos-unstable> { } }:
{
dibbler = pkgs.callPackage ./nix/dibbler.nix { };
}

@ -1,7 +1,16 @@
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from dibbler.conf import config
engine = create_engine(config.get("database", "url"))
if (url := config.get("database", "url")) is not None:
database_url = url
elif (url_file := config.get("database", "url_file")) is not None:
with Path(url_file).open() as file:
database_url = file.read().strip()
engine = create_engine(database_url)
Session = sessionmaker(bind=engine)

@ -2,7 +2,7 @@ import os
from PIL import ImageFont
from barcode.writer import ImageWriter, mm2px
from brother_ql.devicedependent import label_type_specs
from brother_ql.labels import ALL_LABELS
def px2mm(px, dpi=300):
@ -12,14 +12,15 @@ def px2mm(px, dpi=300):
class BrotherLabelWriter(ImageWriter):
def __init__(self, typ="62", max_height=350, rot=False, text=None):
super(BrotherLabelWriter, self).__init__()
assert typ in label_type_specs
label = next([l for l in ALL_LABELS if l.identifier == typ])
assert label is not None
self.rot = rot
if self.rot:
self._h, self._w = label_type_specs[typ]["dots_printable"]
self._h, self._w = label.dots_printable
if self._w == 0 or self._w > max_height:
self._w = min(max_height, self._h / 2)
else:
self._w, self._h = label_type_specs[typ]["dots_printable"]
self._w, self._h = label.dots_printable
if self._h == 0 or self._h > max_height:
self._h = min(max_height, self._w / 2)
self._xo = 0.0

@ -2,9 +2,10 @@ import os
import datetime
import barcode
from brother_ql import BrotherQLRaster, create_label
from brother_ql.brother_ql_create import create_label
from brother_ql.raster import BrotherQLRaster
from brother_ql.backends import backend_factory
from brother_ql.devicedependent import label_type_specs
from brother_ql.labels import ALL_LABELS
from PIL import Image, ImageDraw, ImageFont
from .barcode_helpers import BrotherLabelWriter
@ -17,10 +18,11 @@ def print_name_label(
label_type="62",
printer_type="QL-700",
):
label = next([l for l in ALL_LABELS if l.identifier == label_type])
if not rotate:
width, height = label_type_specs[label_type]["dots_printable"]
width, height = label.dots_printable
else:
height, width = label_type_specs[label_type]["dots_printable"]
height, width = label.dots_printable
font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ChopinScript.ttf")
fs = 2000

@ -76,12 +76,8 @@ class Database:
personDatoVerdi = defaultdict(list) # dict->array
personUkedagVerdi = defaultdict(list)
# for global
personPosTransactions = (
{}
) # personPosTransactions[trygvrad] == 100 #trygvrad har lagt 100kr i boksen
personNegTransactions = (
{}
) # personNegTransactions[trygvrad» == 70 #trygvrad har tatt 70kr fra boksen
personPosTransactions = {} # personPosTransactions[trygvrad] == 100 #trygvrad har lagt 100kr i boksen
personNegTransactions = {} # personNegTransactions[trygvrad» == 70 #trygvrad har tatt 70kr fra boksen
globalVareAntall = {} # globalVareAntall[Oreo] == 3
globalVareVerdi = {} # globalVareVerdi[Oreo] == 30 #[kr]
globalPersonAntall = {} # globalPersonAntall[trygvrad] == 3

@ -20,6 +20,7 @@ subparsers = parser.add_subparsers(
subparsers.add_parser("loop", help="Run the dibbler loop")
subparsers.add_parser("create-db", help="Create the database")
subparsers.add_parser("slabbedasker", help="Find out who is slabbedasker")
subparsers.add_parser("seed-data", help="Fill with mock data")
def main():
@ -41,6 +42,11 @@ def main():
slabbedasker.main()
elif args.subcommand == "seed-data":
import dibbler.subcommands.seed_test_data as seed_test_data
seed_test_data.main()
if __name__ == "__main__":
main()

@ -180,7 +180,7 @@ When finished, write an empty line to confirm the purchase.\n"""
print(f"User {t.user.name}'s credit is now {t.user.credit:d} kr")
if t.user.credit < config.getint("limits", "low_credit_warning_limit"):
print(
f'USER {t.user.name} HAS LOWER CREDIT THAN {config.getint("limits", "low_credit_warning_limit"):d},',
f"USER {t.user.name} HAS LOWER CREDIT THAN {config.getint('limits', 'low_credit_warning_limit'):d},",
"AND SHOULD CONSIDER PUTTING SOME MONEY IN THE BOX.",
)

@ -1,11 +1,11 @@
__all__ = [
'Base',
'Product',
'Purchase',
'PurchaseEntry',
'Transaction',
'User',
'UserProducts',
"Base",
"Product",
"Purchase",
"PurchaseEntry",
"Transaction",
"User",
"UserProducts",
]
from .Base import Base

@ -0,0 +1,48 @@
import json
from dibbler.db import Session
from pathlib import Path
from dibbler.models.Product import Product
from dibbler.models.User import User
JSON_FILE = Path(__file__).parent.parent.parent / "mock_data.json"
def clear_db(session):
session.query(Product).delete()
session.query(User).delete()
session.commit()
def main():
session = Session()
clear_db(session)
product_items = []
user_items = []
with open(JSON_FILE) as f:
json_obj = json.load(f)
for product in json_obj["products"]:
product_item = Product(
bar_code=product["bar_code"],
name=product["name"],
price=product["price"],
stock=product["stock"],
)
product_items.append(product_item)
for user in json_obj["users"]:
user_item = User(
name=user["name"],
card=user["card"],
rfid=user["rfid"],
credit=user["credit"],
)
user_items.append(user_item)
session.add_all(product_items)
session.add_all(user_items)
session.commit()

@ -5,8 +5,8 @@ show_tracebacks = true
input_encoding = 'utf8'
[database]
; url = postgresql://robertem@127.0.0.1/pvvvv
url = sqlite:///test.db
# url = "postgresql://robertem@127.0.0.1/pvvvv"
url = "sqlite:///test.db"
[limits]
low_credit_warning_limit = -100

23
flake.lock generated

@ -5,31 +5,32 @@
"systems": "systems"
},
"locked": {
"lastModified": 1692799911,
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
"id": "flake-utils",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1693145325,
"narHash": "sha256-Gat9xskErH1zOcLjYMhSDBo0JTBZKfGS0xJlIRnj6Rc=",
"lastModified": 1749143949,
"narHash": "sha256-QuUtALJpVrPnPeozlUG/y+oIMSLdptHxb3GK6cpSVhA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "cddebdb60de376c1bdb7a4e6ee3d98355453fe56",
"rev": "d3d2d80a2191a73d1e86456a751b83aa13085d7d",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {

@ -1,77 +1,65 @@
{
description = "Dibbler samspleisebod";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system: let
outputs = { self, nixpkgs, flake-utils }: let
inherit (nixpkgs) lib;
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems = f: lib.genAttrs systems (system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
packages = {
in f system pkgs);
in {
packages = forAllSystems (system: pkgs: {
default = self.packages.${system}.dibbler;
dibbler = pkgs.callPackage ./nix/dibbler.nix {
python3Packages = pkgs.python311Packages;
python3Packages = pkgs.python312Packages;
};
};
skrot = self.nixosConfigurations.skrot.config.system.build.sdImage;
});
apps = {
apps = forAllSystems (system: pkgs: {
default = self.apps.${system}.dibbler;
dibbler = flake-utils.lib.mkApp {
drv = self.packages.${system}.dibbler;
};
};
});
devShells = {
default = self.devShells.${system}.dibbler;
dibbler = pkgs.mkShell {
packages = with pkgs; [
python311Packages.black
ruff
];
overlays = {
default = self.overlays.dibbler;
dibbler = final: prev: {
inherit (self.packages.${prev.system}) dibbler;
};
};
})
//
devShells = forAllSystems (system: pkgs: {
default = self.devShells.${system}.dibbler;
dibbler = pkgs.callPackage ./nix/shell.nix {
python = pkgs.python312;
};
});
{
# Note: using the module requires that you have applied the
# overlay first
# Note: using the module requires that you have applied the overlay first
nixosModules.default = import ./nix/module.nix;
images.skrot = self.nixosConfigurations.skrot.config.system.build.sdImage;
nixosConfigurations.skrot = nixpkgs.lib.nixosSystem {
nixosConfigurations.skrot = nixpkgs.lib.nixosSystem (rec {
system = "aarch64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ self.overlays.dibbler ];
};
modules = [
(nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64.nix")
self.nixosModules.default
({...}: {
system.stateVersion = "22.05";
networking = {
hostName = "skrot";
domain = "pvv.ntnu.no";
nameservers = [ "129.241.0.200" "129.241.0.201" ];
defaultGateway = "129.241.210.129";
interfaces.eth0 = {
useDHCP = false;
ipv4.addresses = [{
address = "129.241.210.235";
prefixLength = 25;
}];
};
};
# services.resolved.enable = true;
# systemd.network.enable = true;
# systemd.network.networks."30-network" = {
# matchConfig.Name = "*";
# DHCP = "no";
# address = [ "129.241.210.235/25" ];
# gateway = [ "129.241.210.129" ];
# };
})
./nix/skrott.nix
];
};
});
};
}

76
mock_data.json Normal file

@ -0,0 +1,76 @@
{
"products": [
{
"product_id": 1,
"bar_code": "1234567890123",
"name": "Wireless Mouse",
"price": 2999,
"stock": 150,
"hidden": false
},
{
"product_id": 2,
"bar_code": "9876543210987",
"name": "Mechanical Keyboard",
"price": 5999,
"stock": 75,
"hidden": false
},
{
"product_id": 3,
"bar_code": "1112223334445",
"name": "Gaming Monitor",
"price": 19999,
"stock": 20,
"hidden": false
},
{
"product_id": 4,
"bar_code": "5556667778889",
"name": "USB-C Docking Station",
"price": 8999,
"stock": 50,
"hidden": true
},
{
"product_id": 5,
"bar_code": "4445556667771",
"name": "Noise Cancelling Headphones",
"price": 12999,
"stock": 30,
"hidden": true
}
],
"users": [
{
"name": "Albert",
"credit": 42069,
"card": "NTU12345678",
"rfid": "a1b2c3d4e5"
},
{
"name": "lorem",
"credit": 2000,
"card": "9876543210",
"rfid": "f6e7d8c9b0"
},
{
"name": "ibsum",
"credit": 1000,
"card": "11122233",
"rfid": ""
},
{
"name": "dave",
"credit": 7500,
"card": "NTU56789012",
"rfid": "1234abcd5678"
},
{
"name": "eve",
"credit": 3000,
"card": null,
"rfid": "deadbeef1234"
}
]
}

@ -4,11 +4,15 @@
}:
python3Packages.buildPythonApplication {
pname = "dibbler";
version = "unstable-2021-09-07";
version = "unstable";
src = lib.cleanSource ../.;
format = "pyproject";
# brother-ql is breaky breaky
# https://github.com/NixOS/nixpkgs/issues/285234
dontCheckRuntimeDeps = true;
nativeBuildInputs = with python3Packages; [ setuptools ];
propagatedBuildInputs = with python3Packages; [
brother-ql

@ -1,16 +1,31 @@
{ config, pkgs, lib, ... }: let
cfg = config.services.dibbler;
format = pkgs.formats.ini { };
in {
options.services.dibbler = {
enable = lib.mkEnableOption "dibbler, the little kiosk computer";
package = lib.mkPackageOption pkgs "dibbler" { };
config = lib.mkOption {
default = ../conf.py;
settings = lib.mkOption {
description = "Configuration for dibbler";
default = { };
type = lib.types.submodule {
freeformType = format.type;
};
};
};
config = let
screen = "${pkgs.screen}/bin/screen";
in {
in lib.mkIf cfg.enable {
services.dibbler.settings = lib.pipe ../example-config.ini [
builtins.readFile
builtins.fromTOML
(lib.mapAttrsRecursive (_: lib.mkDefault))
];
boot = {
consoleLogLevel = 0;
enableContainers = false;
@ -23,7 +38,7 @@ in {
group = "dibbler";
extraGroups = [ "lp" ];
isNormalUser = true;
shell = ((pkgs.writeShellScriptBin "login-shell" "${screen} -x dibbler") // {shellPath = "/bin/login-shell";});
shell = (pkgs.writeShellScriptBin "login-shell" "${screen} -x dibbler") // {shellPath = "/bin/login-shell";};
};
};
@ -32,7 +47,9 @@ in {
wantedBy = [ "default.target" ];
serviceConfig = {
ExecStartPre = "-${screen} -X -S dibbler kill";
ExecStart = "${screen} -dmS dibbler -O -l ${cfg.package}/bin/dibbler --config ${cfg.config} loop";
ExecStart = let
config = format.generate "dibbler-config.ini" cfg.settings;
in "${screen} -dmS dibbler -O -l ${cfg.package}/bin/dibbler --config ${config} loop";
ExecStartPost = "${screen} -X -S dibbler width 42 80";
User = "dibbler";
Group = "dibbler";
@ -55,30 +72,6 @@ in {
serviceConfig.Restart = "always"; # restart when session is closed
};
services = {
openssh = {
enable = true;
permitRootLogin = "yes";
};
getty.autologinUser = lib.mkForce "dibbler";
udisks2.enable = false;
};
networking.firewall.logRefusedConnections = false;
console.keyMap = "no";
programs.command-not-found.enable = false;
i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
environment.noXlibs = true;
documentation = {
info.enable = false;
man.enable = false;
};
security = {
polkit.enable = lib.mkForce false;
audit.enable = false;
};
services.getty.autologinUser = lib.mkForce "dibbler";
};
}

20
nix/shell.nix Normal file

@ -0,0 +1,20 @@
{
mkShell,
python,
ruff,
uv,
}:
mkShell {
packages = [
ruff
uv
(python.withPackages (ps: with ps; [
brother-ql
matplotlib
psycopg2
python-barcode
sqlalchemy
]))
];
}

27
nix/skrott.nix Normal file

@ -0,0 +1,27 @@
{...}: {
system.stateVersion = "25.05";
services.dibbler.enable = true;
networking = {
hostName = "skrot";
domain = "pvv.ntnu.no";
nameservers = [ "129.241.0.200" "129.241.0.201" ];
defaultGateway = "129.241.210.129";
interfaces.eth0 = {
useDHCP = false;
ipv4.addresses = [{
address = "129.241.210.235";
prefixLength = 25;
}];
};
};
# services.resolved.enable = true;
# systemd.network.enable = true;
# systemd.network.networks."30-network" = {
# matchConfig.Name = "*";
# DHCP = "no";
# address = [ "129.241.210.235/25" ];
# gateway = [ "129.241.210.129" ];
# };
}

@ -8,7 +8,6 @@ authors = []
description = "EDB-system for PVV"
readme = "README.md"
requires-python = ">=3.11"
license = {text = "BSD-3-Clause"}
classifiers = [
"Programming Language :: Python :: 3",
]
@ -32,4 +31,3 @@ line-length = 100
[tool.ruff]
line-length = 100