From 91c17416f15f81587273d2253dd25bc587288ae5 Mon Sep 17 00:00:00 2001 From: Lorent Lempereur Date: Mon, 17 Aug 2026 19:21:25 +0200 Subject: [PATCH] fix(wiegand): correct decode26 decimal input halving payload value --- CHANGELOG.md | 4 ++++ src/modules/wiegand/logic.test.ts | 23 +++++++++++++++++++++++ src/modules/wiegand/logic.ts | 18 +++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ffc843..344029d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +* [Bug] Wiegand Decode W26 — decimal input format returned a halved value instead of the original decimal (#1) + ## [1.0.10] - 2026-04-04 * [Feature] UUID Generator — UUIDv7 support (generate time-ordered UUIDs, version selector, inline timestamp display) diff --git a/src/modules/wiegand/logic.test.ts b/src/modules/wiegand/logic.test.ts index 49224ac..00ccbb1 100644 --- a/src/modules/wiegand/logic.test.ts +++ b/src/modules/wiegand/logic.test.ts @@ -162,6 +162,29 @@ describe('processWiegand', () => { expect(result?.mode).toBe('error') }) + + it('preserves the input decimal value in the decoded result', async () => { + const result = await processWiegand('decode26', '3268230', 'decimal') + + expect(result?.mode).toBe('decode26') + if (result?.mode === 'decode26') { + expect(result.decoded?.wiegand26InDecimal).toBe(3268230) + } + }) + + it('round-trips with encode26 via wiegand26InDecimal', async () => { + const encoded = await processWiegand('encode', 'ABC 123') + if (encoded?.mode !== 'encode' || !encoded.encoded26) { + throw new Error('encode failed') + } + + const decoded = await processWiegand('decode26', String(encoded.encoded26.wiegand26InDecimal), 'decimal') + + expect(decoded).toEqual({ + mode: 'decode26', + decoded: encoded.encoded26, + }) + }) }) describe('decode26 mode — plate format', () => { diff --git a/src/modules/wiegand/logic.ts b/src/modules/wiegand/logic.ts index 6b90c57..4cf831c 100644 --- a/src/modules/wiegand/logic.ts +++ b/src/modules/wiegand/logic.ts @@ -28,6 +28,21 @@ export async function processWiegand( } } +// anpr-wiegand only exposes decode26(hex), which strips the 2 parity bits from a raw Wiegand26 +// value but never computes them. Reconstructing correct parity bits here (same algorithm as the +// library's internal, unexported addParityBits) lets decimal-format input produce an accurate hex. +function popcount(value: number): number { + const withPairSums = value - ((value >> 1) & 0x55555555) + const withNibbleSums = (withPairSums & 0x33333333) + ((withPairSums >> 2) & 0x33333333) + return (((withNibbleSums + (withNibbleSums >> 4)) & 0x0f0f0f0f) * 0x01010101) >> 24 +} + +function addParityBits(payloadShiftedLeftByOne: number): number { + const withLeadingParity = + popcount((payloadShiftedLeftByOne >> 13) & 0x1fff) % 2 !== 0 ? payloadShiftedLeftByOne | (1 << 25) : payloadShiftedLeftByOne + return popcount((withLeadingParity >> 1) & 0xfff) % 2 === 0 ? withLeadingParity | 1 : withLeadingParity +} + async function decode26WithFormat(input: string, format: Decode26InputFormat): Promise { switch (format) { case 'decimal': { @@ -35,7 +50,8 @@ async function decode26WithFormat(input: string, format: Decode26InputFormat): P if (!/^\d+$/.test(input) || !Number.isInteger(asNumber) || asNumber < 0 || asNumber > 16_777_215) { return { mode: 'error', error: 'Invalid decimal value (must be 0-16777215)' } } - const hex = asNumber.toString(16).toUpperCase() + const withParityBits = addParityBits(asNumber << 1) + const hex = (withParityBits >>> 0).toString(16).toUpperCase().padStart(7, '0') return { mode: 'decode26', decoded: (await decode26(hex)) ?? null } } case 'hex':