TULIP3 implementation guide
This page provides a quick orientation to the modern WIKA TULIP3 codec stack so that you can diagnose issues or extend the implementation without reverse-engineering the entire codebase. It highlights where the core logic lives, how data flows through the decoder, and the checklist to follow when you add capabilities or fix bugs.
Downlink support is not implemented yet. The encoder APIs are stubbed out intentionally and will be added in a future iteration.
High-level architecture
TULIP3 is implemented inside packages/parsers/src/codecs/tulip3/
and plugs into device parsers via defineParser()
just like TULIP2. The implementation is spread across a few focused modules:
codec.ts
:
Entry point that exportsdefineTULIP3Codec()
. It wires message dispatch, validates channels, and exposes the codec interface (decode
,canTryDecode
,adjustMeasuringRange
, rounding helpers).profile.ts
:
Type definitions and helpers for authoring device profiles. Device profiles declare sensor/channel layout, measurement ranges, rounding defaults, and alarm flag maps. Profiles are treated as mutable by runtime adjustments, so new codec instances must receive fresh profile objects.messages/
:
Message-specific decoders. Each file handles a message type or subtype (data, process alarms, device alarms, configuration, identification, keep-alive, spontaneous). Shared utilities likevalidateMessageHeader
live inmessages/index.ts
.registers/
:
Register decoding infrastructure.parseRegisterBlocks()
slices raw register payloads andevaluateRegisterBlocks()
applies lookup tables to produce structured output objects. Device-specific lookups live underdevices/<DEVICE>/parser/tulip3/registers/
.lookups.ts
:
Shared enumerations for status codes, measurand, units, protocol data types, and other dictionary-style metadata used across messages.
Device parsers integrate the codec by returning it from createTULIP3...Codec()
helpers (see packages/parsers/src/devices/<DEVICE>/parser/tulip3/
). Tests in each device folder exercise the codec using spec-required examples and schemas.
Message flow in a nutshell
defineParser()
validates all registered codecs and invokesdefineTULIP3Codec()
.defineTULIP3Codec()
inspects the first byte of the payload to choose a message handler (0x10–0x17). Subtype selection is handled byreadMessageSubtype()
andvalidateMessageHeader()
.- Handlers decode payloads into domain objects, leveraging:
- device profile metadata for channel names and ranges,
- shared lookups for human-readable values,
- register utilities for configuration/identification messages.
- The codec returns typed output (e.g.,
TULIP3UplinkOutput
) which is surfaced to callers through the parser API.
Updating or extending TULIP3
When you need to modify the implementation:
- Start with the device profile. Adjust sensor channels, measurement ranges, or alarm flags in the profile factory (
profile.ts
types ensure structure). Remember to create a new object per codec instance. - Update lookup tables if new enumerations are introduced (e.g., measurand or units). Keep them
as const
so TypeScript inference stays precise. - Extend message decoders inside
messages/
. Add new handlers or tweak existing ones; reuse helpers (rounding, validation) to keep behavior consistent. - Adjust register parsing when new registers appear. Add entries under
registers/
and update device-specific lookup maps. Tests should cover the mapping to guarantee casts remain accurate. - Regenerate schemas if msg payload shape changes (see Schemas).
- Add tests:
- Extend the device’s
examples.json
with new fixtures. - Add or update Vitest cases (
driver-examples.test.ts
or targeted unit tests in__tests__/
). - Confirm coverage includes register lookup paths when using casts.
- Extend the device’s
- Run validation:
pnpm test
and, if schemas changed,pnpm schema
.
Known limitations
- Downlink encoding:
Not implemented. The codec’sencode
function intentionally throws to prevent accidental usage. Future work will add encoder support once the specification stabilizes. - Generics ergonomics:
Some message handlers use focused@ts-expect-error
annotations due to complex profile generics. When editing, prefer keeping inference hints (e.g.,as const
on lookups) rather than introducing broadany
casts.
Use this guide to orient yourself, then dive into the referenced modules to implement changes. The combination of targeted TypeScript types, Valibot schemas, and the tests should make it straightforward to verify behavior after each modification.