Scanner and point-read library for field controllers over the P2 protocol. Runs as a CLI, a Tk GUI, or an importable Python library.
Single Python module, zero external dependencies, read-only by design.
Discovery
- Cold-site onboarding via BACnet recon + tiered dictionary probe
- Passive multicast listener (no probes sent)
- Passive TCP 5034 listener for live COV / virtual-point / routing events
- Port-scan discovery on TCP 5033
- Automatic firmware-dialect detection (legacy vs modern panels)
Point operations
- Read by point name or slot number
- Full-panel walk — every point on a controller, including panel-internal variables
- FLN device enumeration
- PPCL program source dump
- 797 TEC application definitions bundled — state labels, units, and types
- Comm-status detection — distinguishes live readings from stale cached data on offline devices
- Structured error decoding (not silent
Nones)
I/O
- Table, CSV, and JSON output
- Sniff or decode packet captures for automatic BLN-name learning
- Per-host dialect cache avoids re-probing on reconnect
| File | Description |
|---|---|
p2_scanner.py |
CLI / library. Python 3.6+. |
p2_gui.py, p2_gui_widgets.py, p2_gui_workers.py |
Tk GUI front-end |
tecpoints.json |
Point definitions for 797 TEC applications |
site.json |
Site-configuration template |
PROTOCOL.md |
Wire-level protocol reference |
- Python 3.6 or later
- Network access to PXC controllers on TCP/5033
tsharkon PATH (optional — only needed for live-sniff discovery)- L2 access to the BAS subnet (only needed for cold discovery)
No pip packages required for the scanner itself.
python p2_scanner.py --cold-discover --range 192.168.1.0/24 --save site.jsonpython p2_scanner.py --discover --range 192.168.1.0/24 --network MYBLN --save site.jsonpython p2_scanner.py --config site.json -n NODE1 -d DEVICE1 -p "ROOM TEMP"python p2_gui.pyCommon commands shown below. Full flag list: python p2_scanner.py --help.
# Default cold discovery (BACnet recon + dictionary probe)
python p2_scanner.py --cold-discover --range 192.168.1.0/24 --save site.json
# Conservative (2-second delay between probes — safe during production hours)
python p2_scanner.py --cold-discover --range 192.168.1.0/24 --cold-delay 2
# Passive multicast listen (no probes sent)
python p2_scanner.py --listen 60 --save site.json
# Learn BLN name from a packet capture
python p2_scanner.py --pcap capture.pcapng --save site.json# All points on a device
python p2_scanner.py --config site.json -n NODE1 -d DEVICE1
# Specific point by name
python p2_scanner.py --config site.json -n NODE1 -d DEVICE1 -p "ROOM TEMP"
# Specific point by slot number (Desigo-style)
python p2_scanner.py --config site.json -n NODE1 -d DEVICE1 -p 4
# Full-panel walk — includes panel-internal / PPCL variables
python p2_scanner.py --config site.json -n NODE1 --walk-points
# Dump PPCL program source
python p2_scanner.py --config site.json -n NODE1 --dump-programs
# Panel firmware info
python p2_scanner.py --config site.json -n NODE1 --infopython p2_scanner.py --config site.json -n NODE1 -d DEVICE1 -f csv > out.csv
python p2_scanner.py --config site.json -n NODE1 -d DEVICE1 -f json > out.json10.0.0.50 · 10.0.0.0/24 · 10.0.0.80-200 · 10.0.0 (shorthand for .1-254) · comma-separated ranges.
Each point read produces a result dictionary:
| Field | Description |
|---|---|
point_name |
Point name |
point_slot |
Subpoint slot number (1–99) |
value |
Numeric value |
value_text |
Label for digital points ("NIGHT", "COOL", etc.) |
units |
Engineering units |
point_type |
analog_ro / analog_rw / digital_ro / digital_rw |
comm_status |
online or comm_fault |
point_info |
Full metadata (state labels, units, scaling) |
Table output shows Desigo-style (slot) NAME for each point. Digital points render as LABEL (raw). Comm-faulted points get a #COM suffix.
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Scan ran but returned nothing (device offline, no readable points) |
2 |
Input rejected before any network I/O |
{
"p2_network": "MYBLN",
"p2_site": "SITE",
"scanner_name": "P2SCAN|5034",
"known_nodes": {
"NODE1": "192.168.1.10",
"NODE2": "192.168.1.11"
}
}p2_network— BLN network name. Required. PXCs reject messages with the wrong name.scanner_name— Scanner identity. Some sites require a specific format; if handshakes fail, try<SITE>DCC-SVR|5034.known_nodes— Node name → IP map.
Auto-updated by --save. Extra keys are preserved.
import p2_scanner as p2
p2.P2_NETWORK = "MYBLN"
p2.SCANNER_NAME = "P2SCAN|5034"
conn = p2.P2Connection("192.168.1.50")
if conn.connect("NODE1"):
result = conn.read_point("DEVICE1", "ROOM TEMP", "NODE1")
print(result['value'], result.get('units'))
conn.close()Core API:
| Function | Purpose |
|---|---|
P2Connection(host) |
Open TCP session |
conn.connect(node_name) |
Handshake |
conn.read_point(device, point, node) |
Read a single point |
conn.enumerate_fln(node) |
List FLN devices |
conn.read_firmware(node) |
Panel model / firmware |
scan_device(host, device, ...) |
High-level device scan with rendering |
get_point_info(app, point_name) |
Metadata lookup |
render_point_value(value, info) |
Value → display string |
The library is synchronous. For a GUI, run reads on a worker thread (see p2_gui_workers.py for reference).
| Issue | Fix |
|---|---|
| "P2 network name required" | Provide --network or --config |
| "Handshake failed" | Verify BLN / scanner / node names |
| Handshake takes 2+ seconds | First connect to a modern-firmware panel; the dialect auto-probe is doing its thing. Cached on subsequent connects. |
| Listener hears nothing | Site has multicast disabled — use --sniff or --cold-discover |
| "Max peer sessions reached" | Try when other supervisor connections are idle |
All device points show #COM |
FLN bus disconnected |
| Digital point shows raw float instead of label | Read APPLICATION first — use -n NODE -d DEVICE so the scanner can auto-detect the app |
- Read-only. The scanner never writes points, changes setpoints, or modifies controller state. Write-capable opcodes are documented in
PROTOCOL.mdbut intentionally not exposed. - PXCs have a limited number of peer sessions (typically 8–16). Don't run parallel scanners against the same panel.
- Cold discovery sends dictionary probes. Observed as non-disruptive, but use
--cold-delay 2during production hours as a precaution.
- PROTOCOL.md — wire-level protocol reference. Every opcode, every format variant, every edge case. Read this if you're debugging unusual responses, implementing your own client, or just curious how a BAS protocol works on the wire.