Scale Daemon is a high-performance Windows Service designed to bridge industrial weighing scales (RS232/Serial) with modern web applications. Unlike simple serial readers, this daemon acts as persistent middleware that handles automatic reconnection, noise filtering, and data distribution via low-latency WebSockets.
Optimized for retail and logistics environments, it allows any browser on the local network to receive real-time weight readings without installing additional drivers on the client.
The daemon uses an asynchronous Broadcaster model. A dedicated serial reader (Producer) feeds a central channel, which distributes data concurrently to all connected WebSocket clients (Consumers).
graph TD classDef go fill: #e1f5fe, stroke: #01579b, stroke-width: 2px, color: #000 classDef data fill: #fff3e0, stroke: #e65100, stroke-width: 2px, color: #000 classDef hw fill: #f3e5f5, stroke: #4a148c, stroke-width: 2px, color: #000 classDef sec fill: #e8f5e9, stroke: #1b5e20, stroke-width: 2px, color: #000 subgraph Host["Windows Service Host"] direction TB Service["svc.Service Wrapper"]:::go -->|Init/Start| Auth["Auth Manager"]:::sec Service -->|Start| HTTP["HTTP/WS Server"]:::go Service -->|Start| Reader["Serial Reader Loop"]:::go Reader -->|Channel| Broadcast["Broadcaster Engine"]:::go Auth -.->|Validate| HTTP end subgraph Hardware["Physical Layer"] Scale["Industrial Scale"]:::hw -->|RS232/9600 baud| Reader end subgraph Network["Distribution"] Broadcast -->|Fan - Out| Client1["Web POS 1"]:::data Broadcast -->|Fan - Out| Client2["Web POS 2"]:::data Broadcast -->|Fan - Out| ClientN["Dashboard / Apps"]:::data end HTTP -->|Serve| Dashboard["Embedded Dashboard"]:::data The service implements a hot-swap configuration system. When a config message is received via WebSocket, the daemon safely stops the current read goroutine, closes the serial port, and restarts the loop with new parameters (Port, Brand, or Test Mode) β without disconnecting other clients.
sequenceDiagram participant C as Web Client participant S as WebSocket Server participant R as Serial Reader participant H as Hardware (COM) Note over R, H: Active read loop C ->> S: {"tipo":"config", "puerto":"COM4", "auth_token":"..."} S ->> S: Validate token + rate limit S ->> R: Config change signal R ->> H: Close Port Note over R: Updating configuration R ->> H: Open Port (COM4) R -->> S: OK / Resumed S -->> C: Streaming resumes - π Hardware Abstraction β Multi-brand scale support via configurable serial commands (Rhino, etc.)
- π Automatic Resilience β Retry strategy with backoff for physical cable disconnections
- π§ͺ Built-in Simulation Mode β Realistic fluctuating weight generation for development without physical hardware
- π Embedded Diagnostic Dashboard β Web interface served via
go:embedfor real-time weight monitoring and configuration - π Layered Security β bcrypt login, session cookies (HttpOnly/SameSite), brute-force lockout, per-client rate limiting, and config token authorization
- π¨ Real-Time Error Broadcasting β Connection failures (port not found, cable disconnected) are pushed to clients via WebSocket, not just logged server-side
- β»οΈ Hot Configuration Reload β Change serial port, scale brand, or test mode via WebSocket without restarting the service
- π Auto-Rotating Logs β 5 MB threshold with last-1000-line preservation and verbose/quiet filtering
- π₯ Health Endpoint β JSON health check with scale connection status, uptime, and build info
The API uses a hybrid protocol: JSON objects for control/metadata, raw JSON strings for weight data streaming ( minimizing overhead).
| Endpoint | Description |
|---|---|
ws://{host}:{port}/ws | Real-time weight data + configuration |
http://{host}:{port}/ | Embedded diagnostic dashboard |
http://{host}:{port}/health | Service health check (JSON) |
http://{host}:{port}/ping | Latency check β pong |
"15.42"| Code | Description |
|---|---|
ERR_SCALE_CONN | Cannot open serial port |
ERR_EOF | Cable physically disconnected |
ERR_TIMEOUT | Scale not responding (5s) |
ERR_READ | Read error (noise/driver) |
π Full API documentation:
api/v1/SCALE_WEBSOCKET_V1.md| JSON Schema:api/v1/scale_websocket.schema.json
| Layer | Protects | Mechanism |
|---|---|---|
| Dashboard Login | Panel access (/) | bcrypt password + HttpOnly session cookie |
| Config Token | WebSocket config changes | Auth token validated per message |
| Rate Limiter | Config abuse | Max 15 changes/min per client |
| Brute Force | Login attacks | IP lockout after 5 failed attempts (5 min) |
PUBLIC (no auth required) βββ GET /login Login page βββ POST /auth/login Process login βββ GET /ping Latency check βββ GET /health Service diagnostics βββ WS /ws Weight streaming + config (token protected) βββ GET /css/* Static assets βββ GET /js/* Static assets PROTECTED (session required) βββ GET / Dashboard (injects config token) Note:
/wsis public so POS applications can receive weight data without dashboard authentication. Config changes within WebSocket are protected by theauth_token, only available to authenticated dashboard sessions.
- Go 1.24+ (download)
- Task (Taskfile runner) β
go install github.com/go-task/task/v3/cmd/task@latest - Windows (the service uses Windows SCM APIs)
# Clone the repository git clone https://github.com/adcondev/scale-daemon.git cd scale-daemon # Install Go dependencies go mod downloadCreate a .env file in the project root:
# β οΈ Do NOT commit to version control SCALE_DASHBOARD_HASH=<base64-encoded-bcrypt-hash> SCALE_AUTH_TOKEN=<your-secret-token> BUILD_ENV=local| Variable | If Empty | Description |
|---|---|---|
SCALE_DASHBOARD_HASH | Auth disabled (direct dashboard access) | bcrypt hash (base64) for dashboard login |
SCALE_AUTH_TOKEN | Config changes accepted without token | Token required in WebSocket config messages |
# Build the service binary (console mode) task build # Build and run immediately task run # Clean build artifacts task clean# Run all tests with race detection go test -v -race ./... # Run benchmarks go test -bench=. -benchmem ./... # Run linter golangci-lint run --config=.golangci.ymlscale-daemon/ βββ api/v1/ # API documentation & JSON Schema βββ cmd/BasculaServicio/ # Service entry point (main.go) βββ internal/ β βββ assets/web/ # Embedded web dashboard (HTML/CSS/JS) β βββ auth/ # Authentication, sessions, brute-force protection β βββ config/ # Runtime configuration with hot-swap β βββ daemon/ # Service lifecycle (Init/Start/Stop) β βββ logging/ # Log rotation, filtering, secure file access β βββ scale/ # Serial port reader, brand commands, simulation β βββ server/ # HTTP/WS server, broadcaster, rate limiting, models βββ .github/ β βββ workflows/ # CI, CodeQL, PR automation, PR status dashboard β βββ codeql-config.yml # CodeQL security analysis config βββ embed.go # go:embed directive for web assets βββ Taskfile.yml # Build automation with ldflags injection βββ .golangci.yml # Linter configuration (15+ linters) βββ go.mod # Go module definition Logs are stored in %PROGRAMDATA% with an auto-rotation system:
- Path:
C:\ProgramData\{ServiceName}\{ServiceName}.log - Limit: 5 MB (when exceeded, last 1000 lines are preserved)
- Filtering: Non-critical messages (weight readings, client connect/disconnect) are suppressed when verbose mode is off
- Fallback: If the log directory is not writable (console mode), logs go to stdout
Contributions are welcome! Please ensure:
- PR titles follow Conventional Commits (enforced by CI)
- All tests pass with race detection (
go test -race ./...) - Code passes
golangci-lintwith the project config - Use the PR template provided
This project is licensed under the MIT License.
Copyright (c) 2025 Red 2000