2026-04-06 16:50:17 +07:00
# TinyLink User Manual
Welcome to TinyLink: the friendly lab middleware that never sleeps, rarely panics, and keeps your results moving even when downstream systems are having a bad day.
## What TinyLink Can Do
TinyLink sits between laboratory instruments and CLQMS, then handles the heavy lifting:
- Receives messages from instruments over three connector types:
- `http-json` (HTTP endpoint)
- `hl7-tcp` (TCP socket)
- `astm-serial` (physical serial port/COM)
2026-04-07 11:30:11 +07:00
- Wraps incoming payloads as raw content, then normalizes them into one canonical JSON format via the translator pipeline.
2026-04-06 16:50:17 +07:00
- Stores raw and normalized data in SQLite for durability.
- Deduplicates repeated payloads using a hash key.
- Sends results to CLQMS with automatic retry and backoff.
- Moves non-deliverable payloads to dead letter for review.
- Exposes operational endpoints for health and metrics.
Think of it as a reliable translator + traffic controller for instrument data.
## Default Ports and Endpoints
2026-04-07 11:30:11 +07:00
By default (from `config/app.yaml` ):
2026-04-06 16:50:17 +07:00
- Instrument config + health + metrics API: `4001` (from `host.port` )
- Instrument connectors are configured per instrument entity (`inst1` , `inst2` , ...)
Useful endpoints:
- `GET http://localhost:4001/health`
- `GET http://localhost:4001/health/ready`
- `GET http://localhost:4001/metrics`
2026-04-09 16:34:53 +07:00
- `GET http://localhost:4001/dashboard`
2026-04-06 16:50:17 +07:00
- `POST http://localhost:3001/messages` (for JSON instrument payloads)
2026-04-09 16:34:53 +07:00
Architecture reference:
- `docs/separated_architecture.md`
2026-04-06 16:50:17 +07:00
## Add a New Instrument (New "Inst")
TinyLink now uses a single file-based configuration. There is no `POST /instruments` write flow.
To add an instrument, keep one config file in:
```text
2026-04-07 11:30:11 +07:00
core/
2026-04-06 16:50:17 +07:00
config/
app.yaml
```
### Step 1: Edit `app.yaml`
Example:
```yaml
host:
url: http://localhost:4000/api/results
apikey: ""
port: 4001
inst1:
enabled: true
connector:
type: serial
port: COM1
baudRate: 9600
config:
location: lab-a
translator:
2026-04-07 07:30:07 +07:00
engine: overrides
2026-04-06 16:50:17 +07:00
forceInstrumentId: true
meta:
profile: astm-default
```
2026-04-07 11:30:11 +07:00
### Optional: Use per-instrument `.map` translator files
2026-04-09 16:34:53 +07:00
If you want a shorter per-instrument translator config, set `translator` to the map name.
Example: `translator: inst1` loads `config/inst1.map` .
2026-04-07 11:30:11 +07:00
`config/app.yaml` example:
```yaml
inst1:
enabled: true
connector:
type: serial
port: COM1
2026-04-09 16:34:53 +07:00
translator: inst1
2026-04-07 11:30:11 +07:00
```
2026-04-09 16:34:53 +07:00
`config/inst1.map` example:
2026-04-07 11:30:11 +07:00
```text
2026-04-09 16:34:53 +07:00
# HEADER
< VT > H|\^& |||WST^P1|||||{instrument_id}^System1||P|1|{sample_id}< CR >
SAMPLE_ID = O[3]
CHEMNUM = R[3]
RESULT = R[4]
# ORDER
O|1|{SAMPLE_ID}||{order_tests}||||||{specimen_type}||||{tube_type}||||||||||O|< CR >
# RESULT
< <
@for R
R|1|^^^{CHEMNUM}|{RESULT}< CR >
@end
>>
# TERMINATOR
L|1|N< FS > < CR >
2026-04-07 11:30:11 +07:00
```
Notes:
2026-04-09 16:34:53 +07:00
- Preferred style is section-based: `# MESSAGE_NAME` plus one body line.
- Use `<<` and `>>` for multiline section bodies.
- `KEY = value` remains supported for backwards compatibility.
- `ALIAS = R[3]` (or `O[11]` , `P[2]` , `OBX[5.2]` ) defines readable field aliases.
- You can reference selectors directly as placeholders: `{R[3]}` .
- Control tokens in message bodies are decoded: `<VT>` , `<FS>` , `<STX>` , `<ETX>` , `<CR>` , `<LF>` .
- `@for R ... @end` repeats a multiline block for each incoming record of type `R` .
- If `records` are not pre-parsed, TinyLink auto-extracts records from `raw_payload` /`meta.raw_payload` for selector usage.
2026-04-07 11:30:11 +07:00
- Placeholders use `{name}` and missing values default to empty strings.
2026-04-09 16:34:53 +07:00
- `translator.messages` controls output order. If omitted, messages render in map file order.
2026-04-07 11:30:11 +07:00
2026-04-06 16:50:17 +07:00
What it does:
- `host` contains upstream endpoint and API settings (`url` , `apikey` , `port` ).
- Every top-level entity other than `host` is an instrument (`inst1` , `inst2` , ...).
- `connector` is embedded per instrument, so each instrument has its own connector type/settings.
2026-04-07 11:30:11 +07:00
- `translator` stays embedded per instrument, so each instrument can define mapping rules and metadata.
2026-04-06 16:50:17 +07:00
### Step 2: Restart TinyLink
TinyLink validates instrument files before startup. Restart after adding or editing files.
Use this preflight command before `npm start` :
```powershell
npm run instrument:check
```
### Step 3: Verify instrument is loaded
You can still use read-only endpoints:
```powershell
curl http://localhost:4001/instruments
curl http://localhost:4001/instruments/inst1
```
If your instrument does not appear, run `npm run instrument:check` and fix reported errors.
### Matching rules (important)
TinyLink picks one instrument by matching connector + `instruments[].match` rules.
- 0 matches: payload is dropped (`no matching instrument config` ).
- More than 1 match: payload is dropped (`ambiguous instrument match` ).
- Exactly 1 match: translator runs and message continues through queue + delivery.
TinyLink is strict because your audit trail deserves peace and quiet.
## Canonical Payload Shape
2026-04-07 11:30:11 +07:00
After wrapping and normalization, TinyLink expects this shape.
2026-04-06 16:50:17 +07:00
Required fields:
- `instrument_id`
- `sample_id`
- `result_time` (ISO timestamp)
- `results[]` with at least one item containing:
- `test_code`
- `value`
Optional fields:
- `unit` , `flag` , `patient_id` , `operator_id` , `meta`
Example:
```json
{
"instrument_id": "SYSMEX_XN1000",
"sample_id": "SMP-20260326-001",
"result_time": "2026-03-26T10:20:00Z",
"results": [
{
"test_code": "WBC",
"value": "8.2",
"unit": "10^3/uL",
"flag": "N"
}
],
"meta": {
"source_protocol": "HL7",
"connector": "hl7-tcp"
}
}
```
## Delivery and Retry Behavior
TinyLink does not give up easily:
- Retries transient failures (timeouts, DNS/connection issues, HTTP 5xx).
- Uses backoff schedule: `30s -> 2m -> 10m -> 30m -> 2h -> 6h` .
- Maximum attempts: `10` .
- HTTP `400` and `422` are treated as non-retriable and moved to dead letter immediately.
## Health, Readiness, and Metrics
Use these to check system status:
- `GET /health` : connector status + queue counts (`pending` , `retrying` , `deadLetters` ).
- `GET /health/ready` : confirms SQLite readiness.
- `GET /metrics` : Prometheus-style metrics, including attempts, latency, and success timestamp.
## Daily Operations (Quick Checklist)
- Start app: `npm start`
- Validate instrument files: `npm run instrument:check`
- Run migrations manually: `npm run migrate`
- Backup DB: `npm run maintenance -- backup`
- Vacuum DB: `npm run maintenance -- vacuum`
- Prune old delivery logs: `npm run maintenance -- prune --days=30`
## Troubleshooting New Instrument Onboarding
### Instrument returns `404`
- Confirm exact `instrument_id` spelling and casing.
2026-04-07 11:30:11 +07:00
- Verify the instrument exists in `config/app.yaml` as its own top-level key (`inst1` , `inst2` , ...).
2026-04-09 16:34:53 +07:00
- Verify that instrument has `connector.type` , connector settings, and `translator` .
2026-04-06 16:50:17 +07:00
### Data is not flowing
- Confirm instrument has `enabled: true` .
- Confirm connector matches incoming protocol.
- Confirm match rules fit connector metadata (`localPort` , `remoteAddress` , `remotePort` , or `comPort` ).
- Check `/health` and `/health/ready` .
### Backlog or dead letters are growing
- Check `/metrics` for queue and delivery trends.
- Validate CLQMS URL/token/timeouts and downstream availability.
## Final Tip
If you are adding many instruments, standardize your naming (for example: `LAB1_XN1000` , `LAB1_COBAS_E411` ) and keep connector mappings documented. Future-you will send present-you a thank-you card.