commit 9b56774d43cd12978138860529999ffeed638779 Author: kicer Date: Wed Jul 1 10:34:38 2026 +0800 init repo diff --git a/README.md b/README.md new file mode 100644 index 0000000..6cb82a6 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# MicroPython Wiegand Driver + +A lightweight, event-driven Wiegand protocol driver for MicroPython. +Supports 26-bit Wiegand, 34-bit Wiegand, and 8-bit keypad input. +No polling, no debouncing – just pure interrupt-driven collection with a one-shot timeout. + +## Features + +- **Fully interrupt-driven** – Records bits on both DATA0/DATA1 falling edges. +- **One‑shot timer** – 80 ms timeout started only on the first bit; zero CPU overhead when idle. +- **No software debouncing** – Relies on frame length and parity checks to reject noise. +- **Multi‑format support** – Automatically detects 26‑bit, 34‑bit, and 8‑bit keypad frames. +- **Raw payload output** – Returns the data bits without parity bits (24 bits for WG26, 32 bits for WG34), giving the application full control over facility code / card number extraction. +- **Automatic parity validation** – Standard Wiegand even/odd parity check. +- **Keypad verification** – For 8‑bit keypad frames, validates that the upper nibble is the bitwise complement of the lower nibble, then returns the key number (0–15). +- **Safe for MicroPython** – Uses a pre‑allocated `bytearray` buffer; no memory allocation inside interrupts. + +## Installation + +Copy the file `wiegand.py` to your MicroPython device (e.g., into `/lib/` or the root directory). + +## Usage + +```python +from machine import Pin +from wiegand import Wiegand + +def on_wiegand(data_type, value): + if data_type == 'wg26': + facility = (value >> 16) & 0xFF + card = value & 0xFFFF + print("WG26 - Facility: {}, Card: {}".format(facility, card)) + elif data_type == 'wg34': + print("WG34 raw payload: 0x{:08X}".format(value)) + elif data_type == 'keypad': + print("Key pressed:", value) + +# DATA0 on GPIO16, DATA1 on GPIO17 +reader = Wiegand(pin0=16, pin1=17, callback=on_wiegand) +``` + +## API + +### `Wiegand(pin0, pin1, callback, buf_len=34)` + +- **`pin0`** – GPIO number for the DATA0 (zero‑bit) line. +- **`pin1`** – GPIO number for the DATA1 (one‑bit) line. +- **`callback`** – Function called when a valid frame is received. + Signature: `callback(data_type, value)` + - `data_type` (str): one of `'wg26'`, `'wg34'`, `'keypad'`. + - `value` (int): + - `'wg26'`: 24‑bit payload (bits 1–24 of the 26‑bit frame, parity bits removed). + - `'wg34'`: 32‑bit payload (bits 1–32 of the 34‑bit frame). + - `'keypad'`: integer 0–15, the key number. +- **`buf_len`** – (optional) maximum buffer size, default `34`. Must be at least as large as the longest expected frame. + +## Supported Formats + +| Format | Total bits | Payload bits returned | Parity check | +|---------|------------|------------------------|----------------------------------------| +| WG26 | 26 | 24 | Even (first half) / Odd (second half) | +| WG34 | 34 | 32 | Even (first half) / Odd (second half) | +| Keypad | 8 | lower nibble (0–15) | Upper nibble must be ~lower nibble | + +*For WG26 and WG34 the leading and trailing parity bits are stripped. The user is responsible for extracting facility code and card number from the raw payload.* + +## How It Works + +1. **Pin setup** – Both DATA0 and DATA1 pins are configured as inputs with internal pull‑ups and falling‑edge interrupts. +2. **Interrupt handler** – On each falling edge, the handler reads the state of **both** pins and encodes it as `0b01` (bit 0) or `0b10` (bit 1). Any other combination (e.g., both low) is ignored. +3. **One‑shot timer** – When the **first** valid bit is recorded, a hardware timer is started in `ONE_SHOT` mode with an 80 ms period. +4. **Timeout** – When the timer fires, the collected bit array is copied, parsed, and the buffer is reset. + - If the frame length is 26 or 34 and parity checks pass, the callback is called with the raw payload. + - If the frame length is 8 and the keypad nibble inversion check passes, the key number is reported. + - Otherwise the frame is silently discarded. +5. **Ready for next read** – The buffer index is set to zero immediately after the timeout, so a new card can be processed right away. + +## Notes + +- The 80 ms timeout works for virtually all standard Wiegand readers. If you have an exceptionally slow reader, adjust the `period` value in `self.timer.init(period=...)` inside the source. +- No debouncing is applied. If your hardware produces spurious edges, the parity or length checks will reject the frame, and no callback will fire. This keeps the interrupt handler extremely short. +- The driver uses a pre‑allocated `bytearray` – it never allocates memory in the IRQ, making it safe for all MicroPython ports. +- If your platform does not support `Timer.ONE_SHOT`, you can replace it with a periodic timer that calls `self.timer.deinit()` on the first callback. +- This library is **not thread‑safe** – it is designed for the single‑threaded MicroPython environment. + +## License + +MIT License. diff --git a/wiegand.py b/wiegand.py new file mode 100644 index 0000000..c9aeb61 --- /dev/null +++ b/wiegand.py @@ -0,0 +1,70 @@ +from machine import Pin, Timer + +class Wiegand: + def __init__(self, pin0, pin1, callback, buf_len=34): + self.pin0 = Pin(pin0, Pin.IN, Pin.PULL_UP) + self.pin1 = Pin(pin1, Pin.IN, Pin.PULL_UP) + self.callback = callback + self.buf = bytearray(buf_len) + self.idx = 0 + self.timer = Timer(-1) + + self.pin0.irq(trigger=Pin.IRQ_FALLING, handler=self._irq) + self.pin1.irq(trigger=Pin.IRQ_FALLING, handler=self._irq) + + def _irq(self, p): + # 同时读取两根线,组合成 0b01 (bit=0) 或 0b10 (bit=1) + state = (self.pin1.value() << 1) | self.pin0.value() + if state not in (0b01, 0b10): + return + + if self.idx < len(self.buf): + self.buf[self.idx] = state + self.idx += 1 + if self.idx == 1: + # 只启动一次,80ms 后触发解析 + self.timer.init(period=80, mode=Timer.ONE_SHOT, callback=self._timeout) + + def _timeout(self, t): + n = self.idx + self.idx = 0 + data = bytes(self.buf[:n]) # 拷贝,防止中断覆盖 + self._parse(n, data) + + def _parse(self, n, data): + if n == 26 and self._check_parity(data, 26): + # 剔除首尾校验位,保留中间 24 位有效载荷 + payload = self._extract_bits(data, 1, 24) + self.callback('wg26', payload) + elif n == 34 and self._check_parity(data, 34): + # 剔除首尾校验位,保留中间 32 位有效载荷 + payload = self._extract_bits(data, 1, 32) + self.callback('wg34', payload) + elif n == 8: + # 键盘:低4位键值,高4位为低4位取反 + val = self._extract_bits(data, 0, 8) + if ((val >> 4) ^ (val & 0x0F)) == 0x0F: + self.callback('keypad', val & 0x0F) + # 其余长度或校验失败直接丢弃 + + def _check_parity(self, data, bits): + """标准 Wiegand 奇偶校验:前一半偶数个1,后一半奇数个1""" + half = bits // 2 + cnt_even = 0 + cnt_odd = 0 + for i in range(bits): + if data[i] == 0b10: # 该比特为 1 + if i < half: + cnt_even += 1 + else: + cnt_odd += 1 + return (cnt_even % 2 == 0) and (cnt_odd % 2 == 1) + + def _extract_bits(self, data, start, count): + """从 data 中提取连续比特,MSB first,返回整数""" + val = 0 + for i in range(start, start + count): + val <<= 1 + if data[i] == 0b10: + val |= 1 + return val