6.7 KiB
DA-12C Subnet Mask / Subnet-Bits Handling — Service-Tool Implementation Spec
Audience: the DA-12C service-tool (Python) project. Scope: host-side only. No DA-12 firmware change is required or authorized. Status: verified against DA-12C firmware. This document is self-contained — you do not need access to the firmware to implement against it.
1. What the DA-12C stores
The DA-12C stores its subnet setting as a single byte, xpSubnetBits.
This byte is the count of host bits — the number of trailing zero bits in the subnet mask. It is NOT the CIDR prefix length. This is the legacy Lantronix XPort encoding the firmware inherited.
xpSubnetBits = 32 − CIDR_prefix = number of trailing zero bits in the mask
255.255.255.0 (/24) -> stored value 8 (8 host/zero bits)
255.255.0.0 (/16) -> stored value 16
255.0.0.0 (/8) -> stored value 24
⚠️ The naming trap that has caused field confusion. The field is labeled "Subnet bits," which most people read as the CIDR prefix (24 for a /24). It is the opposite end — the host-bit count. A user who wants 255.255.255.0 and enters
24actually gets 255.0.0.0 (a /8). To get 255.255.255.0 they must enter8. Surfacing the dotted mask alongside the stored number (this spec) is intended to remove that ambiguity.
When the DA-12C applies the setting, it converts the stored byte back to a dotted mask with:
nm = 0xFFFFFFFF & ~((1UL << xpSubnetBits) - 1); // host-bit count -> dotted mask
This round-trips correctly for any contiguous mask, including non-octet-aligned ones (/23, /28, etc.). Nothing about the stored representation changes — this spec only adds a friendlier display and an alternate input format.
2. What to build in the service tool
2.1 Display
Show both the stored host-bit count and the equivalent dotted subnet mask, so the user sees what is stored and what it means. Example:
Subnet bits: 8 (mask 255.255.255.0)
2.2 Edit
Accept either input format and store the host-bit count unchanged:
- A bare integer
0–32→ interpreted as the host-bit count (the legacy method, preserved exactly so existing users are unaffected). - A dotted-decimal mask (contains
.) → converted to the host-bit count and stored.
Disambiguate by the presence of a .: contains a dot → parse as a mask; otherwise → parse
as an integer host-bit count.
The stored value written back to the device is always the host-bit count. The legacy entry method is not deprecated.
3. Conversion rules
Mask → stored value:
stored = 32 − prefix_length (prefix_length = count of leading 1-bits)
Validate the mask is contiguous (all 1-bits followed by all 0-bits) before converting.
Reject masks like 255.0.255.0.
Stored value → mask (for display; mirrors the firmware):
mask = 0xFFFFFFFF & ~((1 << stored) - 1)
4. Edge cases — handle these host-side
- Default sentinels. Stored
0and255are both treated by the firmware as "default → 255.255.255.0." When displaying a stored0or255, show the mask as255.255.255.0and label it the default. Do not convert an entered mask of 255.255.255.0 to stored0— store8for it (the normal value). Leave an existing stored0/255untouched unless the user edits the field. - Clamp valid stored values to 1–31. The firmware computes
1UL << stored, which is undefined behavior whenstored >= 32, and the device only guards the0/255sentinels on its primary (NetBurner) path. Reject anything outside 1–31, other than the0/255sentinels you may read back from a device. - Reject
0.0.0.0(/0). Maps to stored 32 (undefined behavior on the device). Not valid. - Reject
255.255.255.255(/32). Maps to host bits 0, which collides with the "default" sentinel and cannot be represented. Not meaningful for this device.
5. Round-trip reference table
| Dotted mask | CIDR | Stored xpSubnetBits |
|---|---|---|
| 255.255.255.252 | /30 | 2 |
| 255.255.255.240 | /28 | 4 |
| 255.255.255.128 | /25 | 7 |
| 255.255.255.0 | /24 | 8 |
| 255.255.254.0 | /23 | 9 |
| 255.255.252.0 | /22 | 10 |
| 255.255.0.0 | /16 | 16 |
| 255.0.0.0 | /8 | 24 |
6. Reference helpers (Python)
def mask_to_subnet_bits(mask: str) -> int:
"""Dotted-decimal mask -> stored host-bit count. Raises ValueError if invalid."""
octets = [int(o) for o in mask.split(".")]
if len(octets) != 4 or any(o < 0 or o > 255 for o in octets):
raise ValueError(f"Invalid mask: {mask}")
value = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]
# Must be contiguous 1s followed by contiguous 0s
inverted = (~value) & 0xFFFFFFFF # host portion as 1s
if inverted & (inverted + 1) != 0:
raise ValueError(f"Non-contiguous mask: {mask}")
host_bits = bin(inverted).count("1") # = trailing zeros of the mask
if not (1 <= host_bits <= 31):
raise ValueError(f"Mask out of supported range (1-31 host bits): {mask}")
return host_bits
def subnet_bits_to_mask(stored: int) -> str:
"""Stored host-bit count -> dotted-decimal mask (mirrors firmware)."""
if stored in (0, 255): # firmware default sentinels
return "255.255.255.0"
if not (1 <= stored <= 31):
raise ValueError(f"Unsupported stored subnet bits: {stored}")
nm = 0xFFFFFFFF & ~((1 << stored) - 1)
return f"{(nm >> 24) & 0xFF}.{(nm >> 16) & 0xFF}.{(nm >> 8) & 0xFF}.{nm & 0xFF}"
def parse_subnet_input(text: str) -> int:
"""Accept either a dotted mask or a bare host-bit count; return stored value."""
text = text.strip()
if "." in text:
return mask_to_subnet_bits(text)
n = int(text)
if not (1 <= n <= 31):
raise ValueError(f"Host-bit count out of range (1-31): {n}")
return n
7. Acceptance checks
- Entering
8and entering255.255.255.0both store8and display8 (mask 255.255.255.0). - Reading a device with stored
8displays both forms; writing it back is unchanged. - Stored
0/255display as255.255.255.0(default) and are left untouched unless the user edits the field. - Non-contiguous masks,
0.0.0.0,255.255.255.255, and values outside 1–31 are rejected with a clear message.
8. Provenance (firmware references, informational)
Not needed to implement, recorded for traceability:
config.h:155—xpSubnetBitsis a singleunsigned char.op05.c:689— the misleading "Subnet bits (0=class A B C)" menu label.netburner.c:480–494— the active DA-12C conversion path (sentinels0/255→ default;nm = 0xFFFFFFFF & ~((1UL << bits) - 1)).rn131.c:580,wroom.c:267— the same conversion on the legacy Wi-Fi paths.