Source code for pymod.codec.bits
"""Extract bits from a register block.
Algorithm (per ADR 04):
1. Each register is two bytes, MSB-first per the Modbus spec.
2. Apply `byte_order` per-register, then `word_order` across registers,
producing one big-endian byte string of `count * 16` bits — equivalently
one large unsigned integer.
3. Index into that integer per `bit_numbering`. The default is
`lsb_first`: bit 0 is the LSB, equivalent to `(value >> bit_index) & 1`.
`msb_first` flips so bit 0 is the most-significant bit of the assembled
integer (vendors that label bits from the top).
"""
from __future__ import annotations
from collections.abc import Sequence
from typing import Literal
from ._common import ByteOrder, WordOrder, registers_to_bytes
BitNumbering = Literal["lsb_first", "msb_first"]
def _resolve_pos(bit_index: int, total_bits: int, bit_numbering: BitNumbering) -> int:
if not 0 <= bit_index < total_bits:
raise ValueError(
f"bit_index {bit_index} out of range for {total_bits}-bit window"
)
if bit_numbering == "lsb_first":
return bit_index
return total_bits - 1 - bit_index
def _registers_to_int(
registers: Sequence[int],
word_order: WordOrder,
byte_order: ByteOrder,
) -> int:
raw = registers_to_bytes(registers, word_order, byte_order)
return int.from_bytes(raw, "big", signed=False)
[docs]
def extract_bit(
registers: Sequence[int],
bit_index: int,
*,
word_order: WordOrder = "big",
byte_order: ByteOrder = "big",
bit_numbering: BitNumbering = "lsb_first",
) -> bool:
"""Return a single bit from a register block."""
if len(registers) == 0:
raise ValueError("register block is empty")
total_bits = len(registers) * 16
pos = _resolve_pos(bit_index, total_bits, bit_numbering)
value = _registers_to_int(registers, word_order, byte_order)
return bool((value >> pos) & 1)
[docs]
def extract_bits(
registers: Sequence[int],
bit_indices: Sequence[int],
*,
word_order: WordOrder = "big",
byte_order: ByteOrder = "big",
bit_numbering: BitNumbering = "lsb_first",
) -> list[bool]:
"""Return a list of bits at the given indices from a register block.
Returned list is parallel to `bit_indices`.
"""
if len(registers) == 0:
raise ValueError("register block is empty")
total_bits = len(registers) * 16
value = _registers_to_int(registers, word_order, byte_order)
return [
bool((value >> _resolve_pos(idx, total_bits, bit_numbering)) & 1)
for idx in bit_indices
]