14 KiB
Insin Integration — Design
Status: design (approved via brainstorming) Date: 2026-07-23 Scope: Combined runtime telemetry emission and manual publish flow for shipping this service as an Insin package.
Purpose
Two integrations with the Insin package server + on-device agent:
- Runtime telemetry — emit a small set of events to Insin so operators can see live service and printer state in the Insin admin UI.
- Package delivery — package the service as an Insin
.pkgso it can be delivered, installed, and upgraded on Raspberry Pi devices by the on-deviceinsin monitoragent, replacing the current hand-copy-to-Pi workflow.
Context
- Runs on Raspberry Pi (linux-arm64). Currently deployed manually via
scp+ hand-installed systemd units (epson.service,print_server.service). - Insin is delivery + installer + telemetry ingest, not a process supervisor. Systemd stays on the device.
- The on-device agent (
insin monitor, running underinsin.service) exposes a loopback HTTP listener athttp://127.0.0.1:47823/events— this is the only usable event-ingest path for a service that isn't a device itself. - Environment secrets
INSIN_URLandINSIN_TOKENare provided on the machine that runs the publish script (developer machine, not the Pi).
Non-goals
- No CI automation. Publish is a hand-run script triggered by the developer.
- No metrics (numeric samples), only events. Metrics can be added later if a specific need appears.
- No status-poll background task. Online/offline signal is driven purely by the existing discovery cycle.
- No persistence of "seen printer IPs" — restart re-fires
printer.discoveredfor already-known printers. Noise is acceptable. - No fleet or rollback automation. Reassigning group versions in the Insin admin UI is the operator's job.
Runtime — Telemetry Emission
Event catalog
| Kind | When | Message shape |
|---|---|---|
service.started |
Once, at end of PrintServerBootstrapper.StartAsync() |
version=<v> restaurantId=<id> |
job.printed |
PrinterQueue.ProcessQueueAsync() on PrintResult.Ok |
printer=<ip> receiptType=<type> durationMs=<n> |
job.failed.<reason> |
PrinterQueue.ProcessQueueAsync() on PrintResult.Fail |
printer=<ip> receiptType=<type> retry=<n> error="<short msg>" |
printer.discovered |
First time an IP is seen after service start | printer=<ip> model=<name> |
printer.online |
Discovery cycle where an IP transitions from absent → present | printer=<ip> |
printer.offline |
Discovery cycle where an IP transitions from present → absent | printer=<ip> |
PrintErrorType → suffix mapping (in PrinterQueue):
PaperOut→paper_outCoverOpen→cover_openConnection→connection- anything else →
other
Message format: flat key=value pairs, space-separated. Values containing spaces are double-quoted (error="paper end detected"). No structured JSON in the message body.
Interface
New namespace: Inspectron.Epson.PrintServer.Telemetry.
public interface IInsinTelemetry
{
void Emit(string kind, string message);
}
Implementations
LoopbackInsinTelemetry— posts{"kind": <k>, "message": <m>, "at": <ISO-8601 UTC>}tohttp://127.0.0.1:47823/events. 2-second timeout, fire-and-forget on a background task. Failures are caught and logged atLogLevel.Warningat most once per 5-minute window with"insin loopback unreachable at 127.0.0.1:47823, dropping events". Never re-throws. Uses the singletonHttpClientalready bound inPrintServerBootstrapper.NullInsinTelemetry— no-op. Bound whenEpsonPrintServiceConfiguration.EmulationModeis true.
DI binding
In PrintServerBootstrapper.StartAsync():
if (_config.EmulationMode)
_kernel.Bind<IInsinTelemetry>().To<NullInsinTelemetry>().InSingletonScope();
else
_kernel.Bind<IInsinTelemetry>().To<LoopbackInsinTelemetry>().InSingletonScope();
Wiring — where events fire from
service.started— one-line call at the end ofPrintServerBootstrapper.StartAsync(), afterIsRunning = true, reading_configfor restaurant id and reflecting the running assembly'sInformationalVersion/Version.job.printed/job.failed.<reason>— insidePrinterQueue.ProcessQueueAsync(), at the point wherePrintResultis inspected. Pass through thePrintJob(for printer ip, receipt type, retry count) and thePrintErrorTypeon failure.durationMsis measured from just beforeIPrintService.PrintAsyncto just after.printer.discovered/printer.online/printer.offline— new decoratorInsinTelemetryDiscoveredPrintersReceiverthat wrapsJamesDiscoveredPrintersReceiver. Holds:HashSet<string> _seenIps— IPs seen at least once during this process's lifetime.Dictionary<string, bool> _present— last-known presence per IP, keyed by IP. On eachHandle(...)call:
- Delegate to the wrapped receiver first (behavior preserved).
- Compute the diff between the incoming set of IPs and
_present. - For each new IP not in
_seenIps:Emit("printer.discovered", ...), then add to_seenIps. - For each IP transitioning absent → present:
Emit("printer.online", ...). - For each IP transitioning present → absent:
Emit("printer.offline", ...). - Update
_present. Bound in DI asIDiscoveredPrintersReceiver, takingJamesDiscoveredPrintersReceiveras a constructor dependency.
Error handling
- All
Emitcall sites are wrapped intry/catch (Exception)so a bug in message formatting can't break the caller. Caught exceptions log atLogLevel.Warning. LoopbackInsinTelemetry.Emitnever throws. HTTP failures are handled internally with rate-limited warn logging.- Print flow is never blocked on telemetry — every send is fire-and-forget on
Task.Run.
Package Delivery
.pkg layout
epson-print-service@<version>.pkg (zip)
├── install.cfg
├── epson-print-service # self-contained single-file linux-arm64 binary
├── epson.service # systemd unit — main service
├── print_server.service # systemd unit — companion
├── postinst
└── prem
install.cfg
install_path=/opt/epson-print-service
postinst (CWD = install dir)
#!/usr/bin/env bash
set -euo pipefail
INSTALL_DIR="$(pwd)"
chmod +x "$INSTALL_DIR/epson-print-service"
mkdir -p /var/lib/epson-print-service
install -m 644 epson.service /etc/systemd/system/epson.service
install -m 644 print_server.service /etc/systemd/system/print_server.service
systemctl daemon-reload
systemctl enable --now epson.service print_server.service
Idempotent: safe to re-run on upgrade over an existing install.
prem (CWD = temp dir on install, install dir on remove)
#!/usr/bin/env bash
set -euo pipefail
systemctl disable --now epson.service print_server.service 2>/dev/null || true
rm -f /etc/systemd/system/epson.service /etc/systemd/system/print_server.service
systemctl daemon-reload 2>/dev/null || true
Idempotent: safe to run when the unit was never installed or has already been removed. Critical: this MUST stop the systemd unit before install-time file copy, otherwise files get overwritten under a running process.
epson.service
[Unit]
Description=Epson Print Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/epson-print-service
ExecStart=/opt/epson-print-service/epson-print-service
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
print_server.service is rewritten to the same shape (companion service kept from current deploy).
Config-path change
ConfigurationPaths.Linux currently returns /root/epsonprintservice/config.txt. This is incompatible with running from /opt/epson-print-service/ under systemd. Change: ConfigurationPaths.Linux returns /opt/epson-print-service/config.txt. Users generate config.txt and drop it into the install directory post-install.
Missing-config-at-first-install behavior
The service fails to start. Systemd's Restart=on-failure RestartSec=5s retries every 5s. When the operator drops config.txt into /opt/epson-print-service/, the next restart picks it up. No code change needed — this is already the current behavior.
Publish Script
Trigger
Developer runs ./scripts/publish-insin.sh from the repo root when they choose to ship. No CI. No Gitea workflow. No scheduled automation.
Version source of truth
<Version> in EpsonPrintService/EpsonPrintService.csproj. Human bumps it in the PR that ships the change (matches existing cadence: bump EpsonPrintService version to 1.0.14). The script reads it via dotnet msbuild -getProperty:Version.
Re-publishing the same version returns 409 Conflict from Insin — this is the "you forgot to bump" reminder. The script surfaces the error and exits non-zero.
Repo additions
scripts/publish-insin.sh— the script below (git-tracked, executable).deploy/insin/—install.cfg,postinst,prem,epson.service,print_server.service(git-tracked)..gitignore— add.tools/(insin CLI cache) andpublish/(build output).
scripts/publish-insin.sh
#!/usr/bin/env bash
set -euo pipefail
: "${INSIN_URL:?INSIN_URL not set}"
: "${INSIN_TOKEN:?INSIN_TOKEN not set}"
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
cd "$repo_root"
version=$(dotnet msbuild EpsonPrintService/EpsonPrintService.csproj \
-getProperty:Version -nologo | tr -d '[:space:]')
echo "Publishing epson-print-service@${version}"
payload="publish/payload"
rm -rf publish && mkdir -p "$payload"
dotnet publish EpsonPrintService/EpsonPrintService.csproj \
-c Release -r linux-arm64 --self-contained true \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true \
-o "$payload"
cp deploy/insin/install.cfg "$payload/"
cp deploy/insin/postinst "$payload/"
cp deploy/insin/prem "$payload/"
cp deploy/insin/epson.service "$payload/"
cp deploy/insin/print_server.service "$payload/"
chmod +x "$payload/postinst" "$payload/prem"
mkdir -p .tools
manifest=$(curl -fsSL "$INSIN_URL/api/v1/downloads/cli")
cli_version=$(printf '%s' "$manifest" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)[0]["version"])')
cli_filename=$(printf '%s' "$manifest" \
| python3 -c 'import json,sys; a=next(x for x in json.load(sys.stdin)[0]["artifacts"] if x["rid"]=="linux-arm64"); print(a["filename"])')
if [ ! -x ".tools/insin/insin" ] || [ "$(cat .tools/insin/.version 2>/dev/null)" != "$cli_version" ]; then
echo "Fetching insin CLI $cli_version"
curl -fsSL "$INSIN_URL/api/v1/downloads/cli/$cli_version/$cli_filename" -o /tmp/insin.tar.gz
rm -rf .tools/insin && mkdir -p .tools/insin
tar -xzf /tmp/insin.tar.gz -C .tools/insin
chmod +x .tools/insin/insin
echo "$cli_version" > .tools/insin/.version
fi
insin_bin="$repo_root/.tools/insin/insin"
( cd "$payload" && "$insin_bin" pack "epson-print-service@${version}" )
pkg="publish/packages/epson-print-service@${version}.pkg"
[ -f "$pkg" ] || { echo "expected $pkg missing"; exit 1; }
"$insin_bin" publish "$pkg"
echo "Published epson-print-service@${version}"
Prerequisite for the developer running publish: the machine must be able to execute the insin CLI (arm64 or win-x64 build available). On x86_64 Linux, that means qemu-user-static installed. Outside the scope of this spec.
Testing
Runtime unit tests (in EpsonTest)
LoopbackInsinTelemetryTests— spin up an in-testHttpListeneron127.0.0.1:47823, capture posted payloads, assertkind,message, ISO-8601at. Cases: happy path, request timeout, connection refused (no listener bound), 500 response. Verify the rate-limited warn log fires once per 5-minute window and not more.InsinTelemetryDiscoveredPrintersReceiverTests— mockIDiscoveredPrintersReceiverinner, feed sequences of discovery cycles. Assertions:- First-see:
printer.discovered+printer.onlinein that order. - Re-see: no events.
- Absent after present:
printer.offline. - Re-appearance after offline:
printer.onlineonly (notdiscoveredagain). - Wrapped receiver's
Handleis called every cycle regardless of telemetry.
- First-see:
PrinterQueueTelemetryTests— mockIInsinTelemetry, run one success and one failure of eachPrintErrorType, assertkindsuffix mapping and message content (printer, receipt type, retry count).
Manual acceptance test (single Pi with real hardware)
./scripts/publish-insin.sh(with bumped version).- Assign package to device group in Insin admin UI.
- Wait one monitor tick (~60s).
systemctl status epson.serviceshowsactive (running).service.startedandprinter.discoveredevents appear in the Insin admin UI.- Trigger a real print →
job.printedevent. - Pull paper roll, trigger a print →
job.failed.paper_out. - Bump version, re-publish, reassign to new version. Confirm rolling upgrade: old process stopped, new process running, no lingering unit files, no telemetry gaps beyond the restart window.
Not tested (deferred)
- Multi-Pi fleet.
- Rollback smoke (reassigning to an older version). Same code path as upgrade — worth doing once but not blocking.
- Automated end-to-end.
Rollout plan
- Land runtime telemetry (interface, implementations, wiring, tests).
- Land package assets (
deploy/insin/,.csproj<Version>review,ConfigurationPaths.Linuxchange). - Land publish script.
- First manual publish + smoke on a single Pi.
- Assign to the rest of the fleet if smoke passes.