Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions src/modules/wiegand/logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
18 changes: 17 additions & 1 deletion src/modules/wiegand/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,30 @@ 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<WiegandDecoded26 | WiegandResult> {
switch (format) {
case 'decimal': {
const asNumber = Number(input)
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':
Expand Down
Loading