Metadata-Version: 2.4
Name: baltech-sdk
Version: 5.2026.6.1
Summary: SDK to communicate with Baltech RFID readers.
Author-email: Baltech AG <info@baltech.de>
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: typing-extensions
Requires-Dist: brp_lib>=4.0.0
Provides-Extra: format
Requires-Dist: black; extra == "format"

# Baltech SDK

## Installation
```bash
pip install baltech-sdk
```

## Usage

```python
from baltech_sdk import Brp, UsbHid

# Sample code for GetInfo
with Brp(UsbHid()) as brp:
    print(brp.Sys_GetInfo())

# This is a shortcut for:
io = UsbHid()
brp = Brp(io, open=False)
brp.open()
print(brp.Sys_GetInfo())
brp.close()
```
A complete reference of the supported commands can be found [here](https://docs.baltech.de/refman/cmds/index.html).

### Supported IO protocols
```python
from baltech_sdk import Brp, UsbHid, RS232

# USB HID autodetect connected reader
brp = Brp(UsbHid())

# USB HID by serialnumber
brp = Brp(UsbHid(serialnumber=99999999))

# Serial with (optional) custom settings
brp = Brp(RS232("COM1", baudrate=115200, parity=b"N"))
```

### Reader Update
```python
from baltech_sdk import Brp, UsbHid, ReaderUpdater

with Brp(UsbHid(), open=True) as brp:
    updater = ReaderUpdater.from_path("firmware.bf3")

    # simple upload
    updater.run(brp)

    # upload with progress
    for progress in updater.iter(brp):
        print(f"{progress.progress:.0%} ({progress.transferred_bytes} bytes)")
```

### Access Reader Configuration
```python
from baltech_sdk import Config

cfg_src = {(0x0201, 0x02): b'\x01'}   # instead of a confDict also a brp object 
                                     # can be passed to modify reader's 
                                     # configuration directly
cfg = Config(cfg_src)

# set value
cfg.Device_Boot_StartAutoreadAtPowerup("EnableOnce")

# get value
print(cfg.Device_Boot_StartAutoreadAtPowerup.get())

# delete value
cfg.Device_Boot_StartAutoreadAtPowerup.delete()
```

### Templates and BaltechScripts
```python
from baltech_sdk import Config, Template, BaltechScript, TemplateFilter

confdict = {}
cfg = Config(confdict)

cfg.Scripts_Events_OnAccepted(
    BaltechScript()
        .ToggleInverted("RedLed", RepeatCount=3, Delay=20)
        .Toggle("GreenLed", RepeatCount=1, Delay=20)
        .DefaultAction()
)
cfg.Autoread_Rule_Template(
    0, 
    Template()
        .Static(b"SNR:")
        .Serialnr(TemplateFilter(BinToAscii=True, Unpack=True, BinToBcd=True))
)
print(confdict)
```

### Linux or macOS
To use ``baltech-sdk`` under Linux or macOS you need to build your own binary of the [BaltechSDK](https://docs.baltech.de/developers/sdk.html) and manually set the path to your binary.
```python
from pathlib import Path
from baltech_sdk import set_brp_lib_path

set_brp_lib_path(Path("path/to/brp_lib"))
```

### Custom protocols

Implement your own protocol in Python by subclassing `CustomProtocol`. Override
only the methods you need; **not overriding a method makes it a transparent
passthrough** to the layer below. Add the role marker (`IoProtocol` /
`CryptoProtocol`) matching where the protocol is meant to be mounted.

A **byte-stream I/O transport** (here over a TCP socket) overrides `send_frame`
and `recv_frame`. `recv_frame` drives the framing loop: `missing_bytes_cb(chunk)`
appends `chunk` and returns how many bytes are still missing (0 = frame complete):
```python
import socket
from baltech_sdk import Brp, CustomProtocol, IoProtocol
from brp_lib.err import BrpTimeoutError

class TcpIo(CustomProtocol, IoProtocol):
    INTERFACE_NAME = "IP"                      # log filename "{interface}-{instance}.html"

    def __init__(self, host, port):
        super().__init__()
        self.host, self.port = host, port

    @property
    def instance_id(self):                     # the "{instance}" half (e.g. "IP-192.168.0.42.html")
        return self.host

    def open(self):
        super().open()
        self.sock = socket.create_connection((self.host, self.port), timeout=5)

    def close(self):
        self.sock.close()
        super().close()

    def send_frame(self, data):
        self.sock.sendall(data)

    def recv_frame(self, timeout, missing_bytes_cb):
        self.sock.settimeout(None if timeout is None else timeout / 1000)
        chunk = b""
        while True:
            n = missing_bytes_cb(chunk)       # bytes still missing (0 = complete)
            if n == 0:
                break
            try:
                chunk = self.sock.recv(n)
            except (socket.timeout, TimeoutError):
                raise BrpTimeoutError()
            if not chunk:
                raise BrpTimeoutError()

with Brp(TcpIo("192.168.0.42", 9999)) as brp:
    print(brp.Sys_GetInfo())
```
(A message/datagram transport that reads whole frames overrides
`recv_any_frame(timeout)` instead of `recv_frame`.)

A **middle layer** transforms whole frames and forwards them to the layer below
via `self.base` (here a toy obfuscation in the crypto slot). It can also run a
**handshake when the stack is opened** by extending `open`: `super().open()`
brings up the layer below, then exchange frames over it; raise a `BrpError`
to abort (the stack stays closed):
```python
from baltech_sdk import Brp, UsbHid, CustomProtocol, CryptoProtocol, Layer
from brp_lib.err import OpenIOError

class XorLayer(CustomProtocol, CryptoProtocol):
    def open(self):
        super().open()                            # opens the layer below first
        self.base.send_frame(b"AUTH-REQ")          # negotiated in clear (no XOR)
        if self.base.recv_any_frame(2000) != b"AUTH-OK":
            raise OpenIOError("handshake rejected")

    def send_frame(self, data):
        self.base.send_frame(bytes(b ^ 0x5A for b in data))

    def recv_any_frame(self, timeout=None):
        return bytes(b ^ 0x5A for b in self.base.recv_any_frame(timeout))

brp = Brp(UsbHid())
brp.set_layer(Layer.CRYPTO, XorLayer())
brp.open()                                    # runs XorLayer.open()
```
For `open`/`close`, extend with `super()` as usual — like every implementor of
an open callback (in C too), an override is responsible for bringing up the
layer below itself, and `super().open()` does exactly that (forget it and the
handshake `send_frame` fails on the still-closed base). For *data* methods,
forward through `self.base`, never `super()` — their base implementations are
"not implemented" markers, the layer below is reached via `base`.
`self.base.send_frame` / `self.base.recv_any_frame` exchange *raw frames* with
the layer directly below. For high-level command negotiation (e.g.
`brp.Sys_GetInfo()`), run it after `brp.open()` — those calls need the BRP
framing layer above this one.


### Further parameters on connections fors sensible data 

Additional Options: 
 * [AES based Encryption](https://docs.baltech.de/developers/add-aes-auth-and-encryption.html) can be activated
 * [Enabled monitoring](https://docs.baltech.de/developers/analyze-communication.html#enable-monitoring) can be suppressed or extended to plaintext

```python
from baltech_sdk import Brp, UsbHid, SecureChannel

KEY = b'abcdefghijklmnuk'
brp = Brp(UsbHid(), 
          crypto=SecureChannel(security_level=1, key=KEY),  # encrypt communication
          monitor="plaintext"                               # log unencrypted data (if activated by user)
)
```
