Skip to content

Latest commit

 

History

381 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Foundation - Truck Scale Management System

Foundation is a web-based truck scale management application for weighing inbound and outbound trucks, tracking transactions, and generating reports. It runs on ASP.NET Core 10 with a SQLite database and supports touchscreen kiosk terminals with remote ticket printing via Raspberry Pi.


Table of Contents


Features

  • Real-Time Scale Display — Live weight readings from connected scales with motion/error status
  • Multiple Scales — Named site scales (grain-management style): operators pick the scale on the weigh forms, each kiosk is mapped to a scale, and every ticket records which scale captured each weighment
  • Weigh In / Weigh Out — Record inbound and outbound truck weights with automatic net weight calculation
  • Inbound & Completed Trucks — Track trucks currently on-site and view completed transactions
  • Reports — Date-range filtering, group by (Customer, Carrier, Commodity, etc.), export to Excel and PDF
  • Scheduled Email Reports — Excel workbooks of what was weighed, emailed daily, weekly, or monthly to any number of recipients; filter by customer, commodity, and location, and optionally split each commodity onto its own worksheet
  • Load Emails — Email a message per load as it's weighed out, filtered by customer, commodity, both, or neither
  • Master Data Tables — Manage Customers, Carriers, Trucks, Commodities, Locations, and Destinations (tabs follow field visibility; dropdown custom fields get their own tab)
  • Custom Fields — Admin-defined ticket fields (text, dropdown, integer, decimal with min/max) that appear on the weigh forms, grids, kiosk prompts, and printed tickets — placeable anywhere in the ticket designer
  • Field Ordering — Standard and custom fields share one sort order that drives the weigh forms, with the two form columns kept balanced automatically
  • Kiosk Mode — Touchscreen-optimized interface for unattended scale houses (1280x800 resolution)
  • Cards — A card carries a load's details, so nobody enters them at the scale. The loader operator issues it with the details on it; the driver presents it at a kiosk card reader or keys its number in on the kiosk keypad, and weighs in and out without answering prompts. Cards deactivate when the load closes, or recycle for the next trip
  • Retained Tare — A truck's empty weight is stored on weigh-out and reused, so a return trip closes in one weighment. When a truck has a stored tare, the kiosk and the phone page tell the driver what it is and let them either finish on it or reset it — a reset clears the stored weight and keeps the ticket open so the real weigh-out captures a fresh one. Whether a driver may reset is gated per source (kiosk, phone, prox card) in Setup; with a gate off that source applies the stored tare automatically instead. Tares expire overnight by default, and the office can view, edit, or clear them on the Retained Tare page
  • Gate & Light Control — A Raspberry Pi (Gate Controller Service) opens a gate or turns on a light when a ticket completes on the scale it serves, releasing once the truck drives off or a timeout expires. Per-scale, and entirely optional
  • On-Scale Detection — Optional photo-eyes at each end of the deck, wired to two Pi inputs. While either beam is broken the truck is hanging off the platform, so the kiosk, phone page and weigh forms show NOT ON SCALE and refuse to capture. A scale with no detectors wired behaves exactly as before
  • Remote Printing — Print tickets to thermal printers via Raspberry Pi print agents over SignalR
  • Print Rules — Turn the kiosk's inbound or outbound ticket off, and print or skip any ticket by what is on it (customer, carrier, truck, commodity, location, destination, a custom field), at the kiosk, the office, or both. Tickets print bold, with the net weight larger
  • Audit Trail — Every change to tickets, tables, cards and settings, with the old and new value, the user (when login is on) and where it came from — office, which kiosk, phone, API. Each ticket has a History button
  • Shared Grid Layouts — A Manager or Admin arranges the Trucks in Yard and Completed columns once and every user sees that layout, with a Reset button
  • Ticket Designer — Edit ticket layouts with the built-in DevExpress Report Designer
  • Driver Signature Capture — Operator-device overlay or a remote signature-pad tablet (opened by scanning a QR code on the Setup page)
  • Bilingual (English / Spanish) — Off by default; one checkbox in Setup turns it on. The driver-facing screens — kiosk, the phone page, the signature pad and the ticket views — then render in either language, with a site default, an on-screen EN / ES button per device, and a kiosk Pi pinnable to one language at install time. Office and admin pages are English
  • User Login & Roles — Optional login with User, Manager, and Admin roles
  • Customizable — Themes, custom icons, configurable kiosk prompts, and editable ticket templates; Setup changes auto-save
  • Demo Mode — Built-in scale simulator for testing without hardware, one independent simulator per defined scale

Deployment Guides

Pick the path that matches where the app will run. Each guide is self-contained — follow it end to end.

Target When to use it Guide
Debian cloud server (Vultr, etc.) Internet-facing site with a real domain and HTTPS. Required if you need access from outside the LAN or want Let's Encrypt SSL. docs/deploy-vultr.md
Raspberry Pi on the LAN (HTTP) Single weigh station, operators on the same local network, no domain or certificate. Reachable at http://truckscale.local. docs/deploy-pi.md
Windows server or PC The office already runs Windows and you would rather not add a Linux box. Unzip the release package and run INSTALL-WEB.bat as administrator: a Windows service on port 5110, or INSTALL-WEB.bat -Port 80 to reach it as http://<pc-name>/ with no port (see Port 80). docs/deploy-windows.md
Raspberry Pi kiosk display A second Pi (per kiosk display) wired to the scale-house TV. Boots straight into Chromium pointed at <server>/Kiosk with a watchdog that restarts the browser on outage. Bootstrap is a one-shot paste into Raspberry Pi Connect; install.sh prompts for the kiosk PIN, service-id, and printer-id and assembles the full URL. RaspberryPiKiosk/README.md
Raspberry Pi branding Cosmetic, run on any Pi in the fleet. Replaces the boot splash and desktop with Michelli artwork, quiets the boot, and rotates the screen. One-shot — run once, reboot once, done. Pi_Branding/README.md

After the app is running, see Server Management for updates and routine ops, and Configuration for app settings.

Field commissioning tip: to configure a headless Pi's network from a phone (tech access point + browser-based Wi-Fi/ethernet setup and connectivity test), install pi-network-setup on the Pi alongside the app.

DeskPi Lite hardware note: the extra USB ports on a DeskPi Lite adapter board are dead out of the box — the Pi 4's USB-C port they hang off is left in peripheral mode. One line in /boot/firmware/config.txt fixes it: DeskPi Lite: Enabling the Extra USB Ports. Applies to any Pi role, not just the web app.

Pi access to private repos (GitHub App token)

Deploy Pis clone private GTMichelli-Dev repos (pi-network-setup, camera-capture-service, qb-sync-service) over plain HTTPS using a GitHub App installation token instead of PATs or SSH keys. A one-time bootstrap per Pi installs a git credential helper that mints short-lived tokens from the App's private key — after that, git clone / git pull of any org repo just works on the box.

The scripts and the walkthrough live in pi-git-auth, their own public repo, so every service repo points at one copy — and so a Pi that cannot authenticate to anything yet can still fetch the installer.

From Raspberry Pi Connect (no SSH, no file copy — the usual field case):

curl -fsSL -o /tmp/gh-auth.sh https://raw.githubusercontent.com/GTMichelli-Dev/pi-git-auth/main/scripts/pi-connect-github-auth.sh
bash /tmp/gh-auth.sh </dev/tty

It asks for the Installation ID and takes the PEM as a paste, stripping the ^[[201~ bracketed-paste markers the Connect web shell appends to the last line — which would otherwise corrupt the key. Have the .pem open in a text editor before you start. The step-by-step path avoids the paste problem entirely with a nano step, for when you want to watch each part happen.

From a workstation with SSH access, or on a Pi with curl but no git, take the release tarball:

curl -fsSL -o pga.tar.gz https://github.com/GTMichelli-Dev/pi-git-auth/releases/latest/download/pi-git-auth.tar.gz
mkdir -p /tmp/pga && tar -xzf pga.tar.gz -C /tmp/pga
sudo bash /tmp/pga/setup-pi-github-app.sh --install-id <ID> --pem /path/to/michelli-app.pem
shred -u /path/to/michelli-app.pem   # if you copied it onto the Pi

Take the tarball rather than a single script — setup-pi-github-app.sh installs the two helpers from alongside itself and stops if they are not there.

Either way, verify against a private repo — the public ones answer without auth and so prove nothing:

git ls-remote https://github.com/GTMichelli-Dev/pi-network-setup.git HEAD

Full walkthrough — including creating the GitHub App the first time (org settings → Developer settings → GitHub Apps, Contents: Read-only) — in the pi-git-auth README.


Deploy Script Reference

All scripts are in the deploy/ folder:

Server (Debian x64)

Script Windows Description
deploy/publish.sh deploy/publish.bat Builds the web app for Linux and creates a deployment tarball
deploy/install.sh Installs on the server (Nginx, SSL, systemd service)
deploy/deploy.sh deploy/deploy.bat One-step deploy: builds, uploads, and installs remotely
deploy/deploy-all.sh deploy/deploy-all.bat Deploys every site in a manifest, in one pass

deploy.sh options:

./deploy/deploy.sh <user@host> [options]

Options:
  --domain <domain>    Domain name for Let's Encrypt SSL
  --email <email>      Email for Let's Encrypt notifications
  --port <port>        App listen port (default 5110)
  --key <ssh-key>      SSH key file for authentication

Examples:

# With Let's Encrypt HTTPS (recommended for production)
bash deploy/deploy.sh admin@149.28.xxx.xxx --domain scale.yourcompany.com --email admin@yourcompany.com

# With SSH key instead of password
bash deploy/deploy.sh admin@149.28.xxx.xxx --domain scale.yourcompany.com --email admin@yourcompany.com --key ~/.ssh/id_rsa

# Self-signed cert (LAN only, no domain needed)
bash deploy/deploy.sh admin@192.168.1.100

Multiple Sites at Once

deploy.sh handles one site. Running it across a fleet means retyping the same email for each, and a full rebuild per site for byte-identical output. deploy-all reads a manifest, builds once, and deploys to every site listed.

Copy deploy/sites.example.txt to deploy/sites.txt and list one site per line. A site's domain defaults to the host part of user@host, which is what you want when a box is reached at the name it serves:

admin@northsite.yourcompany.com
admin@southsite.yourcompany.com

# Domain differs from the SSH host — deploying to an IP, serving a name:
admin@149.28.xxx.xxx        westsite.yourcompany.com

# Non-default app port:
admin@eastsite.yourcompany.com   eastsite.yourcompany.com   8080

sites.txt is gitignored — it names live customer hosts. Only the example ships.

First time only. SSH prompts twice per site (once to upload, once to install), so a ten-site fleet is twenty password prompts. Install your public key on every host once and they stop for good:

bash deploy/deploy-all.sh --setup-keys

You're asked for each host's password one time. There is deliberately no way to pass deploy-all a password — a file holding it would become the one secret that unlocks every site in the fleet.

Then deploy the fleet:

bash deploy/deploy-all.sh --email admin@yourcompany.com

deploy-all.sh options:

Options:
  --sites <file>       Site manifest (default: deploy/sites.txt)
  --email <email>      Let's Encrypt email, shared by every site
  --key <ssh-key>      SSH key file, used for every site
  --port <port>        Default app port for sites that don't name one
  --setup-keys         Install your SSH public key on every host, then exit
  --skip-build         Reuse the existing tarball instead of publishing fresh
  --dry-run            Print what would be deployed and exit

A failed site is reported and the run continues, so one unreachable box doesn't strand the rest undeployed. The summary lists what succeeded and what didn't, and the script exits non-zero if anything failed.

--rebuild-db is deliberately not forwarded by deploy-all. It destroys the database, and a fleet-wide wipe should never be one flag away. Use deploy.sh for that, one site at a time, on purpose.

Raspberry Pi Full Stack (web + scale reader + print service + tech AP)

One command deploys or updates everything on a scale-house Pi — the web app (built locally, pushed over SSH) plus the Scale Reader Service, Web Print Service, and pi-network-setup tech access point (cloned and built on the Pi; requires the GitHub App bootstrap once per Pi):

bash deploy/deploy-pi-all.sh admin@<pi-ip>

Defaults: services point at http://127.0.0.1 (same-Pi web app) and the printer queue is TicketPrinter. Options: --server-url, --printer-name, --key, and --skip-web / --skip-scale / --skip-print / --skip-net to deploy a subset. Re-running is safe — every installer preserves its database/settings.

Raspberry Pi Web App (arm64, Kestrel on port 80)

For internal-network Pi installs of the full web app — Kestrel serves plain HTTP directly on port 80, no nginx and no SSL:

Script Description
deploy/publish-pi-web.sh Builds the web app for Raspberry Pi (arm64) and creates a tarball
deploy/install-pi-web.sh Installs on the Pi (systemd service, no nginx)
deploy/deploy-pi-web.sh One-step deploy: builds, uploads, and installs remotely

deploy-pi-web.sh options:

./deploy/deploy-pi-web.sh <user@host> [options]

Options:
  --port <port>        App listen port (default 80)
  --key <ssh-key>      SSH key file
  --rebuild-db         Delete and recreate the database (WARNING: deletes all data)

Example:

bash deploy/deploy-pi-web.sh admin@192.168.1.60

To commission the Pi's network connection in the field without a monitor, see pi-network-setup.

RFID Card Reader Service

For presenting cards at a reader (cards can also be keyed in, with no reader at all). Installs on the machine the RS-232 card reader is plugged into — usually the same Pi that runs the kiosk or the scale reader. Run it on that machine:

git clone https://github.com/GTMichelli-Dev/foundation.git /tmp/fnd
bash /tmp/fnd/RfidReaderService/deploy/install.sh https://your-server \
  --service-id kiosk-1 --local /tmp/fnd/RfidReaderService

The --local flag builds from the monorepo checkout. Once the service has its own repo (see RfidReaderService/REPO-SETUP.md) the shorter form in RfidReaderService/README.md applies instead.

Option Default Notes
--service-id default Kiosks map to readers as serviceId:readerId.
--port 5230 Local REST/Swagger port.
--install-dir /opt/rfid-reader-service

Re-run to update; the reader configuration is preserved. Configure the reader from Setup → Options → Readers in the web app.

Gate Controller Service (arm64)

Drives a gate relay and/or a light from a Pi's GPIO header when a ticket completes. Shipped as gate-controller-linux-arm64.tar.gz on each release:

curl -fsSL -o gate.tar.gz https://github.com/GTMichelli-Dev/foundation/releases/latest/download/gate-controller-linux-arm64.tar.gz
mkdir -p /tmp/gcs && tar -xzf gate.tar.gz -C /tmp/gcs
bash /tmp/gcs/install.sh https://scale.yourcompany.com --service-id north-gate

Re-run to update; the gate configuration is preserved. Add the gates at http://<pi>:5240/, then set each Scale's Gate to <service-id>:<gate-id> on the Scale page. Full details in GateControllerService/README.md.

Raspberry Pi Print Agent (arm64)

Script Description
deploy/publish-pi.sh Builds the print agent for Raspberry Pi (arm64)
deploy/install-pi.sh Installs print agent with CUPS on the Pi
deploy/deploy-pi.sh One-step deploy to a Pi

deploy-pi.sh options:

./deploy/deploy-pi.sh <user@host> [options]

Options:
  --server <url>       Foundation server URL (e.g. https://scale.yourcompany.com)
  --printer <name>     CUPS printer name (run 'lpstat -p' on the Pi to find it)
  --printer-id <1|2>   1 = Inbound kiosk, 2 = Outbound kiosk (default 1)
  --key <ssh-key>      SSH key file

Example:

bash deploy/deploy-pi.sh pi@192.168.1.50 --server https://scale.yourcompany.com --printer Zebra_LP2844 --printer-id 1

Raspberry Pi Kiosk Display (arm64)

Unlike the print agent, the kiosk has no separate publish/deploy scripts — the operator clones the repo on the kiosk Pi itself (via Raspberry Pi Connect) and runs install.sh interactively. See RaspberryPiKiosk/README.md for the full bootstrap walkthrough.

Script Description
RaspberryPiKiosk/install.sh One-time setup. Prompts for Server URL, Kiosk PIN, Service ID, Printer ID; verifies connectivity; installs Chromium + curl + unclutter; registers the watchdog autostart entry; suppresses gnome-keyring popups
RaspberryPiKiosk/kiosk-loop.sh The watchdog. Launches Chromium in --kiosk mode at the assembled URL and restarts it after UNREACHABLE_THRESHOLD seconds of server outage
RaspberryPiKiosk/kiosk-stop Pause the kiosk (writes STOP flag, kills Chromium, loop stays alive)
RaspberryPiKiosk/kiosk-start Resume after a pause
RaspberryPiKiosk/uninstall.sh Remove the autostart entry

Run on the kiosk Pi:

cd ~/foundation/RaspberryPiKiosk
./install.sh
sudo reboot

install.sh prompts for five values (all but the first are optional):

Prompt Becomes URL parameter Default on re-run
Server URL base URL last value used
Kiosk PIN ?pin=… (required when User Login is on) last value used
Service ID ?service-id=… (Browser or blank for browser-print) last value used
Printer ID ?printer-id=… (e.g. Zebra_LP2844) last value used
Language ?lang=… (en / es; blank follows the Setup default) last value used

Raspberry Pi Branding (arm64)

Brands a Pi as a Michelli appliance: Michelli boot splash on light grey, a quiet boot with no rainbow square or kernel text, a light-grey desktop with the logo centred and no icons, the Michelli "M" as the taskbar menu button, and optional screen rotation. It is cosmetic and independent of the app — run it on a web-app Pi, a kiosk Pi, or both, before or after the app is installed.

Ships from its own repo, vendored here as the Pi_Branding submodule and pinned to a release tag, so a branding change never lands on the fleet unnoticed.

Script Description
Pi_Branding/setup-kiosk-display.sh One-shot provisioning. Clones the stock pix Plymouth theme into its own michelli theme, installs the splash / desktop / menu artwork, strips the boot noise, and writes the kanshi + cmdline rotation

Run on the Pi (interactive — prompts for display and rotation):

curl -fsSL https://raw.githubusercontent.com/GTMichelli-Dev/Pi_Branding/v1.0.0/setup-kiosk-display.sh | sudo bash
sudo reboot

Two prompts: the display (auto-detected where possible; usually DSI-2 on the Touch Display 2) and the rotation (Normal / Right / Inverted / Left — one answer sets the desktop and the boot splash together).

Fleet rollout — pass the arguments and nothing is asked. Touch Display 2, rotated right:

curl -fsSL https://raw.githubusercontent.com/GTMichelli-Dev/Pi_Branding/v1.0.0/setup-kiosk-display.sh | sudo bash -s -- "" michelli 270 DSI-2 right_side_up

Arguments in order: logo, theme, rotate, output, panel_orientation, boot_mode, menu_icon, icon_theme, desktop_logo; "" keeps the default. "Right" is 90 degrees clockwise — the desktop expresses it as kanshi transform 270, the splash as right_side_up.

Pi with no internet — take the release tarball, which carries the artwork alongside the script. The artwork arguments default to raw.githubusercontent URLs even when the script runs from a checkout, so pass the local files explicitly or it still reaches out:

curl -fsSL -o pb.tar.gz https://github.com/GTMichelli-Dev/Pi_Branding/archive/refs/tags/v1.0.0.tar.gz
tar xzf pb.tar.gz && cd Pi_Branding-1.0.0
sudo bash setup-kiosk-display.sh ./Michelli-Logo.png michelli "" "" "" 720x1280M@60D ./Michelli-Menu.png PiXtrix ./Michelli-Desktop.png
sudo reboot

The three empty arguments leave rotation and display to the prompts; fill them in to skip those too.

From a checkout of this repo on the Pi, the submodule is already there:

cd ~/foundation && git submodule update --init Pi_Branding
sudo bash Pi_Branding/setup-kiosk-display.sh
sudo reboot

That form fetches the artwork over the network; add the argument list above to use the copies in the submodule instead.

The script is idempotent and does not run on every boot — re-run it to restore the branding after an OS or icon-theme update overwrites it. Edited boot files are backed up beside the original as *.bak-<timestamp> and replaced theme icons as *.orig; Pi_Branding/README.md documents the full revert.

↑ Back to top


Server Management

After deployment, use these commands on the server:

# Check if the app is running
sudo systemctl status foundation

# View live logs
sudo journalctl -u foundation -f

# Restart the app
sudo systemctl restart foundation

# Stop the app
sudo systemctl stop foundation

Updating to a New Version

When the project has been updated (new features, bug fixes, etc.), follow these steps to deploy the latest version to your server:

1. Pull the latest code on your local development machine:

cd foundation
git pull

2. Rebuild and deploy to the server:

Windows (Command Prompt):

deploy\publish.bat
deploy\deploy.bat admin@149.28.xxx.xxx --domain yourDNSName.scaledata.net --email admin@yourcompany.com

Linux / Mac / Git Bash:

bash deploy/publish.sh
bash deploy/deploy.sh admin@149.28.xxx.xxx --domain yourDNSName.scaledata.net --email admin@yourcompany.com

Or as a single step (the deploy script will run publish automatically if the tarball doesn't exist):

Windows:

del deploy\foundation-deploy.tar.gz
deploy\deploy.bat admin@149.28.xxx.xxx --domain yourDNSName.scaledata.net --email admin@yourcompany.com

Linux / Mac / Git Bash:

rm -f deploy/foundation-deploy.tar.gz
bash deploy/deploy.sh admin@149.28.xxx.xxx --domain yourDNSName.scaledata.net --email admin@yourcompany.com

Updating several sites at once? Use deploy-all instead — it builds once and walks a site manifest, rather than rebuilding per site:

Windows:

deploy\deploy-all.bat --email admin@yourcompany.com

Linux / Mac / Git Bash:

bash deploy/deploy-all.sh --email admin@yourcompany.com

What's preserved during updates:

  • Your database (Foundation.db) — all transactions, master data, and settings
  • Custom ticket templates in the Reports/ folder
  • Nginx configuration and SSL certificates

What gets replaced:

  • Application binaries and static files
  • The systemd service file

3. Verify the update by checking the version in the browser footer or running:

ssh admin@149.28.xxx.xxx 'sudo systemctl status foundation'

Updating the Pi Print Agent

cd foundation
git pull
bash deploy/publish-pi.sh
bash deploy/deploy-pi.sh pi@192.168.1.50 --server https://yourDNSName.scaledata.net --printer Zebra_LP2844 --printer-id 1

The Pi agent's appsettings.json (ServerUrl, PrinterName, PrinterId) is preserved during updates.

File Locations on the Server

Path Contents
/opt/foundation/ Application files
/opt/foundation/Foundation.db SQLite database (preserved on updates)
/opt/foundation/Reports/ Custom ticket templates (preserved on updates)
/opt/foundation/App_Data/keys Data Protection key ring (preserved on updates) — decrypts the saved SMTP password
/etc/systemd/system/foundation.service Systemd service file
/etc/nginx/sites-available/default Nginx reverse proxy config
/etc/letsencrypt/ SSL certificates (auto-renewed)

↑ Back to top


Configuration

Application Settings

Edit /opt/foundation/appsettings.json on the server:

{
  "ShowResetDatabase": false,
  "DatabaseProvider": "SQLite",
  "ConnectionStrings": {
    "SQLite": "Data Source=Foundation.db"
  }
}
Setting Description
ShowResetDatabase Show/hide the database reset buttons on the Setup page (true for testing, false for production)
DatabaseProvider SQLite (default) or MariaDB
MaxScales Maximum number of site scales that can be defined on the Scale page (default 4)

After editing, restart the service:

sudo systemctl restart foundation

Setup Page

Navigate to Setup in the web interface to configure. Changes auto-save — there is no Save button; a "Saved ✓" indicator confirms each change (text fields save when you leave them). Changing the theme reloads the page so it applies immediately.

  • Company & Ticket — Header lines (company name, address, phone), ticket numbering
  • System — Demo mode, kiosk count (0/1/2), login mode, language, theme, custom icon, driver signature capture (the Remote Signature Pad option shows a QR code — scan it with the tablet to open the pad, no typing)
  • Fields — Show/hide the standard fields, set the sort order for standard and custom fields (one shared scale, so a custom field can slot between built-ins), and manage custom fields (text, dropdown, integer/decimal with min/max/precision, required, show-on-ticket, kiosk prompting)
  • Locations — Physical yards/facilities; with two or more, operators pick theirs in the navbar
  • Email — SMTP server, scheduled reports, and per-load emails (see Email Reports). This tab saves through its own API, so its edits are independent of the auto-save above
  • Kiosk Prompts — Which fields to show on the kiosk touchscreen
  • Printing — Which tickets print by themselves, print rules, and bold ticket text (see Automatic Printing & Print Rules). The rules list saves through its own API, like the Email tab
  • Ticket Designers — Edit the layout of printed tickets

Email Reports

Setup → Email configures outgoing mail and two kinds of automatic email. Defaults target SMTP2GO (mail.smtp2go.com, port 2525, STARTTLS) — use the SMTP username and password from the SMTP2GO account, not the site login — but any relay works. Send Test proves the settings before you trust a schedule to them.

The SMTP password is encrypted with ASP.NET Data Protection before it is stored; the key ring lives in App_Data/keys next to the app and is never committed. The password is never sent back to the browser — the box shows "Saved — type to replace" and only overwrites when retyped. Moving the database to a different server without App_Data/keys means re-entering the password.

Deploys preserve it: install.sh excludes App_Data/ from the rsync --delete sweep that replaces the app files, so the saved password survives an update. Lose that directory and the operator has to retype the SMTP password after every deploy.

Scheduled Reports attach an .xlsx of what was weighed:

  • Daily / Weekly / Monthly at a time you choose, in the timezone set on the System tab. Each run covers the last completed period — yesterday, the seven days ending that morning, or the previous calendar month — so every load lands in exactly one report.
  • Recipients — any number of To and CC addresses per schedule, and any number of schedules, so one site can send an all-loads summary to the office and a customer-filtered copy to that customer.
  • Filters — customer, commodity, and location. Empty means everything. The location filter matches through the scale that weighed the load, so tickets with no scale recorded are left out.
  • Separate worksheet per commodity — adds a Summary sheet of per-commodity totals followed by one sheet per commodity. Off puts every load on a single sheet.
  • Columns follow the fields enabled on the Fields tab (a hidden field never appears in a report), plus net weight in pounds and each commodity's own reporting unit — bushels, tons, CWT, whatever is set on Tables → Commodities. Gross/Tare are optional.
  • Preview downloads the workbook without emailing anyone; Send mails it immediately without disturbing the schedule's normal cadence.

Individual Load Emails send one message per load as soon as it's weighed out, with the details in the message body (no attachment). Filter by customer, by commodity, by both, or by neither; several rules can match the same load and each sends its own email. Loads are picked up by a sweep that runs every minute, so a send can never slow down or fail a weighment, and a site that is offline retries when its uplink returns rather than losing the email.

Finding the Serial Port

The Scale Reader and RFID Card Reader services each need to be told which port their hardware is on. Run this on the machine the cable is plugged into:

Platform Command
Pi / Linux bash scripts/list-serial-ports.sh
Windows .\scripts\list-serial-ports.ps1

Both list the ports with the adapter behind each one — vendor, model and serial number — so two identical USB-serial cables can be told apart. Add --plain (Linux) or -Plain (Windows) for bare port names in a script. The Pi copy can be run without checking out the repo there:

ssh admin@<pi-ip> 'bash -s' < scripts/list-serial-ports.sh

Each service also serves its own list at /api/serialports once installed — http://localhost:5220/api/serialports (scale reader) and http://localhost:5230/api/serialports (RFID reader), from the machine itself.

On a Pi, configure the by-id name, not ttyUSB0. The kernel hands out ttyUSB numbers in enumeration order, so adding a second adapter — or just replugging this one — can silently move the scale to ttyUSB1. The names under /dev/serial/by-id are built from the adapter's own vendor, model and serial number, so they always point at the same physical cable:

ls -l /dev/serial/by-id/
lrwxrwxrwx 1 root root 13 Aug 24 20:00 usb-FTDI_FT232R_USB_UART_A9U6VOHB-if00-port0 -> ../../ttyUSB0

That scale is on ttyUSB0 today. Configure the full /dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A9U6VOHB-if00-port0 path as the port instead and it stays correct across reboots and replugs. The reader services offer these names in the setup page's port suggestions — sorted above the /dev/tty* entries — so on a Pi the first suggestion is the safe one.

Windows needs no equivalent: it pins a COM number to the adapter's serial number in the registry, so that cable keeps COM3 wherever it is plugged in.

If a port doesn't show up:

  • Linuxdmesg | tail straight after plugging in says whether the kernel bound a driver. If the port is listed but won't open, the login needs the dialout group (sudo usermod -aG dialout $USER, then log out and back in) — the script flags this for you. The Pi's own GPIO UART (/dev/ttyAMA0) also needs enable_uart=1 and the serial console released via raspi-config.
  • Windows — an adapter with no driver sits under Other devices in Device Manager and never gets a COM number. Adapters that are installed but unplugged keep their number reserved and reappear on the list when plugged back in.

Scales (multi-scale)

The Scales page (System → Options → Scales) manages the named site scales:

  • Each scale has a name (what operators see and what tickets record), a hardware feed (a scale reported by a connected Scale Reader Service), an order, and an active flag. A scale with no hardware feed is driven by the per-scale simulator in Demo Mode.
  • The weigh forms show a scale picker when more than one scale is active; the choice is remembered per browser. The dashboard's live weight display follows the same selection.
  • Kiosks are mapped to a scale in the Launch Kiosk dialog (or via ?scale-id=<id> in the kiosk URL); each kiosk reads and records weights from its mapped scale.
  • Every ticket stores the scale name per weighment (In Scale / Out Scale). Manually entered weights record no scale and are marked manual — see Manual Weights.
  • Each scale can have its own inbound and outbound ticket printers (optional). Auto-printed tickets go to the capturing scale's printer; scales without one use the site-wide defaults from the Printers page. Explicit kiosk printer choices (Launch Kiosk dialog) still take precedence.
  • In Demo Mode each scale has its own independent simulator — the simulator panels (header bar, Get Weight dialog, kiosk) drive whichever scale is selected.
  • Each scale has a position (latitude/longitude, decimal degrees). Set… in its Position column opens a dialog: type the coordinates, click the map (drag the pin to fine-tune), or stand on the deck with a phone or laptop and use Use This Device's Location. Map opens the saved position on OpenStreetMap. The map needs internet; typing the coordinates always works. The phone app uses the position to decide which scale a phone is standing at — see below.

Phone App Location (Require Location on Mobile)

With Setup → Mobile → Require Location on Mobile on (the default), the driver's phone app (/Mobile) only weighs on the scale the phone is standing at:

  • The phone must allow the page to use its location. Until it does, the page explains how and offers Try Again — nothing can be weighed.
  • The page follows the phone's position and uses the nearest scale that has a position. There is no scale picker; the scale chip at the top shows the scale and how far away it is.
  • The weigh buttons only work within Mobile Range (default 50 m, 10–1000) of that scale. Further away the page shows how far the phone is and waits. Once a weighment has started it stays on its scale, even if another is nearer.
  • Every weigh-in and weigh-out — including closing on a stored empty weight — sends the phone's position, and the server re-checks the distance to the scale before saving. A request from out of range is refused (403), whatever the page thinks.

It needs HTTPS. Browsers only share their location with https:// sites (and localhost). On a plain http:// address the phone app shows Secure connection needed and cannot weigh. Put the site behind HTTPS (see the deployment guides) or turn Require Location on Mobile off.

After upgrading the setting is on, so phone weighing stops until each scale has a position and the site is served over HTTPS. Turning it off gives the phone app exactly its old behaviour (scale picker, no location).

Phone GPS is typically good to 5–15 m in the open and worse next to buildings, which is why the default range is 50 m rather than the size of the deck. Scales closer together than the range will both be in reach — the phone uses whichever is nearer.

Custom Fields & Ticket Printing

Custom fields marked Show on printed ticket print in one of two ways:

  • Auto-append (default): a Name: value row is added near the bottom of the ticket for every field with a value.
  • Designer placement: every ticket-eligible custom field appears in the Ticket Designer's Field List as a parameter named cf_<FieldName> (non-alphanumerics become underscores). Drag it into the layout and save — the value then prints at that exact spot and the auto-appended row for that field is suppressed. Fields you don't place keep auto-appending, so existing layouts never change behavior.

Dropdown-type custom fields also get their own tab on Tables for managing the choice list (add, rename, delete, drag to reorder) without opening Setup.

Automatic Printing & Print Rules

Setup → Printing decides which tickets print by themselves when a truck is weighed — at the kiosk and on the office weigh forms. A reprint someone asks for (the Reprint buttons, the grids' Print dialog) always prints.

A ticket is checked in this order, and the first "no" wins:

  1. Print Inbound Tickets at the Kiosk / Print Outbound Tickets at the Kiosk — turn either kiosk ticket off. Kiosk only; both start on.
  2. Inbound Ticket for Card Weigh-Ins — whether a weigh-in started from a card prints its inbound ticket. Don't print for RFID scans (the default): a card presented at a kiosk's reader prints none, while a card number keyed in at the kiosk or used on the weigh forms prints as usual. Don't print for any card: no card weigh-in prints one. Print: every one does. The completed ticket always prints. Upgrading a site with card readers stops the inbound ticket for scanned cards until someone changes it.
  3. Print rules — each rule says where (kiosk, office or either), which ticket (inbound, outbound or either), the conditions — customer, carrier, truck ID, commodity, location, destination and one custom field, a blank condition matching anything — and whether a match prints or doesn't print. Rules are checked in their order and the first match decides.
  4. When No Print Rule Matches — print (the default) or don't. With it on Don't print, rules become a list of the only tickets that print.

With Retained Tare on, the kiosk's inbound leg still prints nothing, as before. When a kiosk ticket is kept from printing, the completion screen shows no Reprint button. The server log records every ticket kept from printing and why (Kiosk ticket 100123 not printed: print rule "Walk-ins").

Bold Ticket Text (on by default) prints every line of the ticket bold and the net weight at 15 pt — thermal printers print regular Courier thin and grey. It is applied when the ticket is rendered, so layouts already saved in the Ticket Designer get it too; turn it off to print a layout exactly as designed. The default layouts are bold in the designer as well.

Manual Weights

A weight keyed in by hand rather than read from a scale is marked manual, and a weight the scale read records which scale:

  • Grids — Trucks in Yard and Completed show an M beside a manual weight, and In Scale / Out Scale columns with the scale's name or Manual.
  • Printed tickets — a manual gross, tare or inbound weight prints as 40,000 lb (Manual).
  • Editing — changing a weight on a completed ticket (the Edit page, or the grid's API) marks it manual and clears its scale, since the scale no longer weighed that number. The Edit page shows where each weight came from and flags it as you type.
  • Basic Ticket — a weight typed over rather than captured is saved as manual.

A stored tare that closes a load (Retained Tare) is neither: it carries no scale and no manual mark.

Audit Trail

Weighments → Audit Trail (Manager or Admin) lists every change saved anywhere in the app: tickets, custom-field values, Tables, cards, users, scales, kiosks, print rules and Setup. Each row is one field — the old value, the new value, when, the user (the signed-in name when Require Login is on) and where from: Office, Kiosk · North Inbound (the kiosk's name), Phone, Signature Pad, API, Device Service, or System for background work. A record created or deleted is one row carrying its values.

Each ticket's History button (Edit page, and the Weigh In page for a truck in the yard) opens that ticket's changes alone. Passwords are never copied into the trail — only that one changed. Kiosk heartbeats and the next-ticket counter moving on are left out; a counter change made on the Setup page is kept.

The trail is written by the database layer on every save, so nothing needs to remember to log. It is kept indefinitely; a busy site writes a few hundred rows a day.

Shared Grid Layouts

The Trucks in Yard and Completed grids have one layout for everybody. A Manager or Admin (anyone, with Require Login off) reorders, resizes, shows and hides columns with the column chooser, and sets the default sort — each change saves for all users straight away. Reset Layout puts the built-in layout back for everyone.

Other users see that layout without the column tools. Anyone can still filter and sort for themselves; filters, search and the page they are on are never saved or shared. Layouts are stored in the database, so they survive browsers being cleared and follow users between workstations.

Cards

A card carries a load's details — customer, carrier, truck, commodity, bin, custom fields — so nobody enters them at the scale. The driver presents it at a kiosk card reader, or keys its number in on the kiosk keypad, and weighs in and out without answering prompts. Turn it on with Use Cards (Setup → System → Cards), which reveals the Cards and Readers pages and adds a Card Setup link to the navbar. Card Setup lays out for one-handed use on a phone and as an ordinary page on a desktop.

How a load runs

  1. The truck pulls up to the loader. The operator opens Card Setup on a phone, types the card number, and the card's last-issued details come back.
  2. The operator changes whatever this load needs — customer, commodity, truck, bin, custom fields — and taps Save & Issue.
  3. The driver presents the card at the kiosk reader, or keys its number in on the kiosk keypad. The kiosk fills in everything the card carries and only asks for fields that are required and not set on the card. Nothing on the card means the full prompt sequence, exactly as before.
  4. The driver weighs out with the same card — no ticket number to key in. If the truck has an active retained tare, the first presentation offers to close the load in one weighment — the driver picks that or resets the tare, same as a keyed weigh-in.
  5. When the load closes the card is deactivated until the operator re-issues it, unless the recycle gate is set — then it keeps its details and works again straight away. The kiosk tells the driver which it is: "KEEP YOUR CARD FOR YOUR NEXT LOAD" or "RETURN CARD TO THE LOADER OPERATOR".

Recycling is set site-wide by Recycle Cards in Setup, and any single card can override it on the Card Setup page — so regular haulers can hold a permanent card while one-off visitors get single-use ones.

Enrolling cards. Register each card once on Cards (Manager or Admin). Card numbers are 1 to 99999: ticket numbers start at 100000, so the kiosk keypad can tell a keyed card from a keyed ticket. With a reader connected, presenting a card fills its number in automatically; otherwise type it. A card that isn't registered is refused at the kiosk and on the setup page.

Finding and changing cards. Card Setup's Search finds a card by its number, description or anything it carries, for when the number isn't to hand. Managers and Admins also get Change Number there (refused while the card is on an open ticket) and Bulk Update (/Card/Bulk), which sets fields on many cards at once: filter the grid by any field, tick the cards, tick the fields to set. Cards partway through a load are skipped, and any value a card can't take is listed rather than silently dropped. On Card Setup, required fields are outlined until they are filled, and a stored choice the lists no longer offer shows blank.

The SP-6820 sends a 26-bit Wiegand credential, which the reader service decodes to the card number in decimal (0–65535) — normally the number printed on the card. Confirm that on the first card. Leave the reader's facility-code option off: it produces numbers like 123-45678, which cannot be enrolled or keyed in at the kiosk. See Card format.

Mapping a reader to a kiosk. Readers belong to an RFID Reader Service and are identified as serviceId:readerId. The Launch Kiosk dialog offers connected readers, or set it directly:

https://your-server/Kiosk?reader-id=default:kiosk-1-reader&scale-id=2&pin=12345

A kiosk with no reader-id ignores card presentations and behaves exactly as it always has.

Where a card works. With Use Cards on, Setup → System → Cards has a Yes/No for each place a card can fill in a load. All three start on.

  • Allow Cards at the Kiosk — present the card at the kiosk's reader or key its number in on the keypad, as above. Off, kiosks behave as if cards were off: no reader, no "present your card", and a keyed card number is not accepted.
  • Allow Cards on the Weigh Forms — the office Weigh In page gets a Card box. Use Card fills in the form from the card (anything no longer a choice on the form is flagged), and saving ties the card to the ticket so weighing it out frees the card. A card whose truck is already in the yard opens that ticket's Weigh Out page instead.
  • Allow Cards on the Phone — the phone app gets a USE PIN button under Weigh In, the same size. The driver keys in their PIN (the card number); the card answers the prompts, the phone asks only for required fields the card leaves blank, then shows the usual confirmation. A card whose truck is already in the yard hands that load to the phone to weigh out. The phone's location rule still applies.

Hide Weigh In Button at Card Kiosks (Setup → System → Cards, off by default) removes the green weigh-in button from kiosks mapped to a card reader, so every load starts from a card. The scan / key-in button stays, and Enter on a keyboard-driven kiosk opens it instead. Kiosks without a reader are unaffected. A card-reader kiosk's ready screen reads PRESENT YOUR CARD OR SELECT AN OPTION.

A card scanned at a kiosk's reader prints no inbound ticket by default; keyed-in card numbers still print. Setup → Printing → Inbound Ticket for Card Weigh-Ins changes that — see Automatic Printing & Print Rules.

Wherever a card is used, the server re-checks it (enrolled, enabled, issued, not already on another load) and refuses a card from a place where it is switched off. On the phone and the weigh forms the stored-tare questions follow that place's own settings; Allow Tare Reset from a Card is for kiosk card presentations. A load weighed out anywhere — kiosk, phone or office — frees its card.

Descriptions and trucks. A card's Description can be changed on Card Setup as well as on Cards. When the site requires a Truck ID (Setup → Kiosk Prompts), a Use the Truck ID as the description box sets it from the truck, now and whenever the truck changes; typing a description of your own unticks it. A card recalled with a description that isn't its truck ID starts unticked, so an existing label is never replaced unasked.

To copy card descriptions onto the trucks they name (Tables → Trucks → Description), run the script from the repository on any machine that can reach the server. Preview first with the dry run:

# Windows (PowerShell 5.1 or 7)
.\scripts\copy-card-descriptions-to-trucks.ps1 -Server https://your-server -DryRun
.\scripts\copy-card-descriptions-to-trucks.ps1 -Server https://your-server -Username admin
# Linux / Raspberry Pi
bash scripts/copy-card-descriptions-to-trucks.sh --server https://your-server --dry-run
bash scripts/copy-card-descriptions-to-trucks.sh --server https://your-server --user admin

Every enabled card with a description gives it to the truck it names. A card with no carrier uses its customer, which is added to Carriers if it isn't one already; a card with no truck ID uses its card number as the truck ID; and a truck that isn't in Tables yet is created. The card is then filled in with that carrier and truck so it names the truck from then on — except a card on an open ticket, which is passed over until its load weighs out. If Retained Tare is on, those trucks collect stored tares like any other from their next weigh-out. The report lists every carrier and truck added, every truck whose description changed, every card filled in, every truck left alone because two cards name it with different descriptions, and every card passed over (no description, or no carrier or customer). With Require Login on, sign in as a Manager or Admin (-Username / --user); the password is asked for. The scripts call POST /api/cardadmin/copy-descriptions-to-trucks (add ?dryRun=true to preview).

Access. Card Setup is available to any signed-in user (it's the loader operator's job). Card enrollment needs Manager or Admin; the Readers page is Admin-only, like the rest of the device configuration.

What a card can carry — every field on the weigh forms: the standard fields that are visible in Setup, plus active custom fields (including free-text ones the kiosk can't prompt for). Values are validated against the same master-data lists the weigh forms use, so a card can only hold a choice a driver could have picked.

Safety rails. A card mid-trip can't be re-issued, deactivated, or deleted until its load weighs out. Voiding or deleting an open ticket frees its card, and closing a card's ticket from the office releases it just like the kiosk does. Every ticket records the card it was weighed with.

Language (English / Spanish)

The driver-facing screens can run in English or Spanish. The whole feature is off by default — deploying this build changes nothing until someone turns it on.

Setup → System → Enable Spanish is the master switch. While it is off there is no language button on any screen, ?lang= is ignored, a leftover language cookie is ignored, and every screen is English — exactly how the app behaved before Spanish existed. Turning it off again needs no deploy and no data change, so a site can be rolled back from the Setup page mid-shift.

With it on, Setup → System → Default Language sets what those screens show before anyone chooses; everything else is an override on top of it.

What is translated

Screen Translated
Kiosk (/Kiosk) Yes — every prompt, overlay, button and error
Phone page (/Mobile) Yes
Signature pad (/SignaturePad) Yes
Ticket views + browser-print ticket Yes — the labels
Dashboard, weigh forms, Reports, Tables, Setup, Users No — English
Printed tickets from the print agents No — see below

How a screen picks its language, first match wins:

  1. Enable Spanish off → English, full stop. Nothing below is consulted.
  2. ?lang=es in the URL — pins one device regardless of the site default.
  3. The bw.lang cookie — what the on-screen EN / ES button leaves behind.
  4. The site default from Setup.

So one kiosk can run in Spanish while the office and every other kiosk stay in English. Toggling on a kiosk keeps pin, service-id, printer-id, scale-id and reader-id in the URL, so a language switch never unmaps a kiosk from its scale or printer.

Kiosks with Hide On-Screen Buttons turned on show no toggle — that kiosk is keyboard-driven, so set its language with ?lang= at install (install.sh prompts for it) or leave it on the site default.

What stays in English, deliberately

  • Your own data. Customer, carrier, commodity, bin, location and custom-field names come out of your tables and display exactly as the office typed them. A Spanish kiosk still lists Corn if that is what is in Tables — translating master data is a data decision, not a code one.
  • Physical tickets. The print agents render the DevExpress .repx layouts, which are customer-editable files owned by each site (Reports/*.repx, kept out of publish so a redeploy never overwrites local edits). Change their wording in Setup → Ticket Designers if you want Spanish tickets. The browser-print ticket (/Ticket/Print) is translated, so a site using browser printing gets Spanish tickets and a site using print agents does not — worth knowing before you switch a yard over.

Adding or changing wording

All Spanish lives in one file, web/Foundation.Web/Services/LangCatalog.cs, as plain ["English source"] = "Spanish" pairs. There is no translation service and no network call at runtime — a kiosk with no internet renders Spanish fine.

Editing a driver-facing screen is a two-step change:

<div class="big-message">@L["Back Up Slowly"]</div>      <!-- 1. wrap it -->
["Back Up Slowly"] = "Retroceda Despacio",               // 2. add the line

A string with no entry falls back to English rather than rendering blank, so a missing translation degrades a screen instead of breaking it.

The one that bites: the catalog is keyed by the English source text, so rewording an existing string silently orphans its translation. Change @L["Place Truck on Scale"] to @L["Pull Truck onto Scale"] and that line quietly reverts to English on every Spanish screen — no compile error, no warning.

Run the checker before opening a PR that touches those screens:

python3 scripts/check-translations.py

It reports strings referenced in code but missing from the catalog, catalog entries nothing references any more (the reword signal), duplicate keys, and — as warnings — English-looking text on a driver screen that nothing wraps. Plain Python, no dependencies, runs in about a second. Office and admin pages are outside its scope, since they are English by design.

Strings that reach the catalog through a helper rather than a literal T()dtRow('Gross:', …) on the kiosk completion screen, for one — are covered too, via the DYNAMIC_CALLS table at the top of the script. The script re-checks every line of that table against the source on each run, so a helper that stops translating is reported instead of quietly taking its strings out of scope. Add a line when you write a helper of that shape; until you do, its strings show up as orphaned.

Getting the Spanish written. When it reports missing strings:

python3 scripts/check-translations.py --prompt

That prints a ready-to-paste prompt — the strings needing Spanish, the entire existing catalog as a glossary so new entries match the vocabulary already on live kiosks, and the constraints that matter here (keep {0} placeholders, preserve ALL CAPS on the big kiosk calls to action, keep it short enough for a fixed-width button). Paste it into Claude Code or claude.ai, paste the returned ["English"] = "Spanish", lines into LangCatalog.cs.

No API key and no per-use cost — it is a prompt, not a service call. Nothing in the app ever calls a translation API; the kiosk renders Spanish with no network at all, which is the point.

Read what comes back before pasting it. These strings tell a driver standing next to a moving truck what to do.

User Login System

Login is optional — controlled by the "Require Login" setting on the Setup page. When disabled, all features are accessible without authentication.

Default admin credentials:

  • Username: admin
  • Password: michelli

Support backdoor account (for recovery if admin is locked out):

  • Username: support
  • Password: Scale_Us3r
  • This account has Admin role, does not appear in the user list, and cannot be edited or deleted.

Roles:

Role Access
User Weigh trucks in and out, view dashboard, reports, inbound/completed trucks
Manager Everything User can do + edit master data tables (customers, carriers, etc.)
Admin Everything Manager can do + Setup page + user management
Mobile The phone app (/Mobile) and nothing else — for drivers weighing themselves

Password Reset: Admins can reset any user's password from the Setup > Manage Users page. The password is reset to michelli and the user must change it on next login. There is no email-based recovery (the system may not have internet access).

Phone App with Login Enabled: the phone app needs a signed-in user, so give each driver an account. Any role can use it; a Mobile user can use nothing else — they land on /Mobile after signing in, every other page sends them back there, and any other API answers 403. The phone app shows a sign-out button and no dashboard button for them. With login off there are no accounts, and the phone app is open as before.

Kiosk Access with Login Enabled: Kiosks don't use the login screen. Instead, pass the Kiosk PIN code as a URL parameter:

https://your-server/Kiosk?pin=12345

The default PIN is 12345. Change it on the Setup page. The PIN is stored as a browser cookie so subsequent requests don't need it.

Three other optional query parameters select which print service and scale handle this kiosk:

https://your-server/Kiosk?service-id=office-1&printer-id=BIXOLON_BK3&scale-id=2&pin=12345
  • service-id — name of the Print/Camera Service instance (matches what's shown in the Setup page). Browser or blank means browser-print.
  • printer-id — physical printer the service drives (e.g. Zebra_LP2844). Browser for browser-print.
  • scale-id — the site scale this kiosk reads and records (the id from the Scales page). Omitted = the default (first active) scale. The Launch Kiosk dialog fills this in automatically on multi-scale sites.

If you're deploying a Pi-driven kiosk display, RaspberryPiKiosk/install.sh prompts for all three values and assembles the full URL — no need to hand-edit it into the boot config.

Device Services with Login Enabled: the Scale, RFID Reader, Print, Camera, Gate and QB Sync services need no credentials and no configuration change when "Require Login" is turned on. They are daemons on the yard network with no cookie jar and no login screen to use, so the endpoints they depend on are exempt from the gate by design:

Path Used by
/scaleHub every service — the SignalR channel that carries weights, card reads, print and capture commands
GET /api/ticket/{id}/pdf Print Service, Kiosk Print Agent
POST /api/ticket/{id}/image Camera Service (ticket photos)
POST /api/masterdata/sync/{customers,commodities} QB Sync
POST /api/transactions/mark-sent-to-qb QB Sync

Nothing else opens up: the rest of /api/masterdata/ and /api/transactions/ stays gated, and each exemption is matched exactly and only for the verb the service uses.

This matters because the failure was silent rather than loud. A gated POST meets a 302 to /Account/Login, HttpClient follows it as a GET, and the login page comes back 200 OK — so the service logged success for a request the server never carried out. Camera Service reported photos it had not stored; QB Sync marked tickets as invoiced when they had not been, and re-invoiced them on the next run.

To confirm a site is healthy after switching Require Login on, weigh one ticket end to end and check that the photo appears on it. If a service does misbehave, its own log now shows a real status code rather than a redirect to a login page it was never meant to see.

Updating Device Definitions

Scale brand / model / protocol metadata (baud rate, parity, weight regex, etc.) does not live in this repo. It lives in a separate public repo:

GTMichelli-Dev/device-definitions → file scales/scale-models.json

How the running system picks it up

The Scale Reader Service fetches the file via HTTPS from https://raw.githubusercontent.com/GTMichelli-Dev/device-definitions/main/scales/scale-models.json whenever:

  1. The service starts up.
  2. The web app's Scale Management page calls RequestScaleBrands over SignalR — this happens automatically on page load and on every click of the Refresh Definitions button at the top of that page.
  3. Anyone hits GET http://<scale-host>:5220/api/status/brands.

The result is also written to scale-models.json next to the service's .exe as a fallback cache.

Adding or editing a definition

  1. Edit scales/scale-models.json in the device-definitions repo and push to main.
  2. Verify the raw URL serves your change (there's sometimes a 1–2 min CDN delay):
    curl https://raw.githubusercontent.com/GTMichelli-Dev/device-definitions/main/scales/scale-models.json
  3. Open the Scale Management page in the web app. The header pill labelled Definitions turns:
    • Green — "Definitions: live (N)" when the service successfully refreshed from GitHub.
    • Yellow — "Definitions: cached (N)" when the service couldn't reach GitHub and is serving its on-disk fallback. Hover for the underlying error.
  4. If the pill is yellow, click Refresh Definitions after fixing connectivity, or restart the service:
    Restart-Service ScaleReaderService

Where the URL is configured

appsettings.json (in the Scale Reader Service install folder) seeds the URL on first run, but BrandsCache.RefreshAsync() reads the live value from the service's own Settings table. To check the URL the service is actually using:

curl http://<scale-host>:5220/api/settings

If it points at a fork or stale URL, update it via the same endpoint (PUT /api/settings), e.g.:

curl -X PUT http://<scale-host>:5220/api/settings \
  -H "Content-Type: application/json" \
  -d '{"brandsUrl":"https://raw.githubusercontent.com/GTMichelli-Dev/device-definitions/main/scales/scale-models.json"}'

The PUT triggers an internal service restart so the new value takes effect. On some installs that restart is a hard process exit — if systemctl status scale-reader-service shows it stopped afterwards, run sudo systemctl start scale-reader-service to bring it back.

Rebuilding the Database

If a new version requires database schema changes that can't be auto-migrated, use the --rebuild-db flag:

deploy\deploy.bat admin@149.28.xxx.xxx --domain basic.scaledata.net --email admin@example.com --rebuild-db

WARNING: This deletes the existing database and creates a fresh one. All transactions, master data, and users will be lost. Back up first if needed.

↑ Back to top


Troubleshooting

"Error" page with no details

The app runs in Development mode by default (shows full error details). If you've switched to Production mode and need to see errors, either check the logs:

ssh admin@149.28.xxx.xxx
sudo journalctl -u foundation -f

Or temporarily switch back to Development mode:

sudo nano /etc/systemd/system/foundation.service

Change ASPNETCORE_ENVIRONMENT=Production to Development, then:

sudo systemctl daemon-reload
sudo systemctl restart foundation

Remember to set it back to Production when done troubleshooting.

Let's Encrypt "Timeout during connect"

This means ports 80/443 are blocked. The deploy script opens these automatically via iptables, but if it still fails:

ssh admin@149.28.xxx.xxx
sudo iptables -I INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -I INPUT -p tcp --dport 443 -j ACCEPT

Also verify DNS is pointing to the server:

nslookup yourDNSName.scaledata.net

DNS not resolving

The deploy script checks DNS before deploying. If it fails, make sure:

  1. You created an A record in your DNS provider pointing to the server IP
  2. You waited for propagation (typically 1-5 minutes, up to 1 hour)
  3. Run nslookup yourDNSName.scaledata.net and confirm it returns the correct IP

↑ Back to top


Architecture

┌─────────────────────────────────────────────┐
│              Debian Server                   │
│                                              │
│   Nginx (port 80/443)                        │
│     ├── HTTPS termination (Let's Encrypt)    │
│     ├── Reverse proxy → localhost:5110       │
│     └── WebSocket passthrough (SignalR)      │
│                                              │
│   Foundation.Web (port 5110)                 │
│     ├── ASP.NET Core 10 / Kestrel            │
│     ├── SQLite database                      │
│     ├── DevExpress Report Engine             │
│     └── SignalR Hub (real-time updates)      │
│                                              │
└──────────────────┬──────────────────────────┘
                   │ SignalR (WebSocket)
                   │
        ┌──────────┴──────────┬─────────────────────┐
        │                     │                     │
┌───────┴───────┐   ┌────────┴────────┐   ┌────────┴─────────┐
│  Raspberry Pi  │   │  Raspberry Pi   │   │  Raspberry Pi    │
│  (Inbound)     │   │  (Outbound)     │   │  RFID Reader Svc │
│  PrinterId: 1  │   │  PrinterId: 2   │   │  RS-232 → prox   │
│  CUPS → Printer│   │  CUPS → Printer │   │  card reader     │
└────────────────┘   └─────────────────┘   └──────────────────┘

Device services all speak to the same SignalR hub and are installed independently — a site runs only the ones it needs:

Service Repo Purpose
Scale Reader scale-reader-service Weight from indicators (TCP / RS-232)
Web Print web-print-service Ticket printing — CUPS on Linux/Pi, Windows print on a PC
Camera Capture camera-capture-service Ticket images
QB Sync qb-sync-service QuickBooks export
RFID Reader RfidReaderService/ Prox card reads (RS-232) — not yet split into its own repo

Development

Prerequisites

Run Locally

git clone https://github.com/GTMichelli-Dev/foundation.git
cd foundation/web/Foundation.Web
dotnet run

Open http://localhost:5110 in your browser.

Enable Demo Mode in Setup to use the built-in scale simulator.


License

Copyright 2026 Michelli Weighing & Measurement. All rights reserved.

About

ASP.NET Core web application for truck scale weighing operations — real-time scale monitoring, inbound/outbound transactions, ticketing, and reporting

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages