Add DiSEqC motor control, QO-100 DATV reception, and carrier survey
Firmware v3.03.0: DiSEqC Manchester encoder (cmd 0x8D extended), parameterized spectrum sweep (0xBA), adaptive blind scan (0xBB), error code reporting (0xBC). All new function locals moved to XDATA to fit within FX2LP 256-byte internal RAM constraint. Motor control: DiSEqC 1.2 positioner with USALS GotoX, stored positions, interactive keyboard jog, 30-second safety auto-halt. QO-100 DATV: Es'hail-2 wideband transponder tools — LNB IF calculator, narrowband scan, tune, and TS-to-video pipe (ffplay/mpv). Carrier survey: six-stage pipeline (coarse sweep → peak detection → fine sweep → blind scan → TS sample → catalog). JSON catalog with differential analysis, QO-100 optimized mode, CSV/text export. TUI: F9 Motor screen (3-column layout with signal gauge), F10 Survey screen (Full Band + QO-100 tabs). Bridge, demo, and theme updated. Docs: motor.mdx, survey.mdx, qo100-datv.mdx guide, tui.mdx updated for 10 screens. Site builds 41 pages, all links valid.
This commit is contained in:
parent
0f4ba4766f
commit
cc3a0707a1
20 changed files with 5645 additions and 84 deletions
|
|
@ -341,6 +341,388 @@ def parse_pmt(section: dict) -> dict:
|
|||
}
|
||||
|
||||
|
||||
def parse_sdt(section: dict) -> dict:
|
||||
"""
|
||||
Parse a Service Description Table section.
|
||||
|
||||
Table IDs: 0x42 = SDT actual transport stream,
|
||||
0x46 = SDT other transport stream.
|
||||
Carried on PID 0x0011.
|
||||
|
||||
Returns dict with:
|
||||
transport_stream_id - TS ID from the table extension
|
||||
original_network_id - ONID from bytes [0:2] of section data
|
||||
services - list of service dicts, each containing:
|
||||
service_id - program number
|
||||
service_type - numeric type (1=digital TV, 2=digital radio, etc)
|
||||
service_name - decoded service name string
|
||||
provider_name - decoded provider name string
|
||||
eit_schedule - bool, EIT schedule flag
|
||||
eit_present - bool, EIT present/following flag
|
||||
running_status - numeric running status
|
||||
free_ca - bool, free/scrambled flag
|
||||
|
||||
Descriptor parsing: looks for tag 0x48 (service_descriptor) which
|
||||
encodes service_type (1 byte), provider_name_length + provider_name,
|
||||
service_name_length + service_name.
|
||||
"""
|
||||
if section is None:
|
||||
return None
|
||||
if section["table_id"] not in (0x42, 0x46):
|
||||
return None
|
||||
if not section.get("section_syntax"):
|
||||
return None
|
||||
|
||||
transport_stream_id = section["table_id_ext"]
|
||||
data = section.get("data", b'')
|
||||
|
||||
if len(data) < 2:
|
||||
return None
|
||||
|
||||
original_network_id = (data[0] << 8) | data[1]
|
||||
# Byte 2 is reserved_future_use
|
||||
offset = 3
|
||||
|
||||
services = []
|
||||
while offset + 5 <= len(data):
|
||||
service_id = (data[offset] << 8) | data[offset + 1]
|
||||
# byte 2: EIT flags and running status
|
||||
flags_byte = data[offset + 2]
|
||||
eit_schedule = bool(flags_byte & 0x02)
|
||||
eit_present = bool(flags_byte & 0x01)
|
||||
|
||||
status_byte = data[offset + 3]
|
||||
running_status = (status_byte >> 5) & 0x07
|
||||
free_ca = bool(status_byte & 0x10)
|
||||
descriptors_loop_length = ((status_byte & 0x0F) << 8) | data[offset + 4]
|
||||
|
||||
offset += 5
|
||||
|
||||
# Parse descriptors for this service
|
||||
service_type = 0
|
||||
service_name = ""
|
||||
provider_name = ""
|
||||
|
||||
desc_end = offset + descriptors_loop_length
|
||||
if desc_end > len(data):
|
||||
desc_end = len(data)
|
||||
|
||||
while offset + 2 <= desc_end:
|
||||
desc_tag = data[offset]
|
||||
desc_len = data[offset + 1]
|
||||
desc_data = data[offset + 2:offset + 2 + desc_len]
|
||||
offset += 2 + desc_len
|
||||
|
||||
if desc_tag == 0x48 and len(desc_data) >= 1:
|
||||
# service_descriptor
|
||||
service_type = desc_data[0]
|
||||
pos = 1
|
||||
|
||||
# Provider name
|
||||
if pos < len(desc_data):
|
||||
prov_len = desc_data[pos]
|
||||
pos += 1
|
||||
if pos + prov_len <= len(desc_data):
|
||||
provider_name = _decode_dvb_string(desc_data[pos:pos + prov_len])
|
||||
pos += prov_len
|
||||
|
||||
# Service name
|
||||
if pos < len(desc_data):
|
||||
svc_len = desc_data[pos]
|
||||
pos += 1
|
||||
if pos + svc_len <= len(desc_data):
|
||||
service_name = _decode_dvb_string(desc_data[pos:pos + svc_len])
|
||||
|
||||
# Advance past any unprocessed descriptor bytes
|
||||
offset = max(offset, desc_end)
|
||||
|
||||
services.append({
|
||||
"service_id": service_id,
|
||||
"service_type": service_type,
|
||||
"service_name": service_name,
|
||||
"provider_name": provider_name,
|
||||
"eit_schedule": eit_schedule,
|
||||
"eit_present": eit_present,
|
||||
"running_status": running_status,
|
||||
"free_ca": free_ca,
|
||||
})
|
||||
|
||||
return {
|
||||
"table_id": section["table_id"],
|
||||
"transport_stream_id": transport_stream_id,
|
||||
"original_network_id": original_network_id,
|
||||
"version": section["version"],
|
||||
"services": services,
|
||||
}
|
||||
|
||||
|
||||
def parse_nit(section: dict) -> dict:
|
||||
"""
|
||||
Parse a Network Information Table section.
|
||||
|
||||
Table IDs: 0x40 = NIT actual network,
|
||||
0x41 = NIT other network.
|
||||
Carried on PID 0x0010.
|
||||
|
||||
Returns dict with:
|
||||
network_id - network ID from the table extension
|
||||
network_name - decoded network name string (from descriptor 0x40)
|
||||
transports - list of transport dicts, each containing:
|
||||
ts_id - transport stream ID
|
||||
original_network_id - ONID
|
||||
frequency_ghz - satellite frequency in GHz (from 0x43)
|
||||
polarization - string: 'H', 'V', 'L', or 'R'
|
||||
symbol_rate - symbol rate in sps
|
||||
fec - FEC inner code rate string
|
||||
orbital_position - orbital position in degrees (+ east, - west)
|
||||
modulation - modulation string
|
||||
roll_off - roll-off factor string
|
||||
|
||||
Descriptor parsing: looks for tag 0x43 (satellite_delivery_system_descriptor)
|
||||
which is 11 bytes of BCD-encoded satellite parameters, and tag 0x40
|
||||
(network_name_descriptor) for the network name.
|
||||
"""
|
||||
if section is None:
|
||||
return None
|
||||
if section["table_id"] not in (0x40, 0x41):
|
||||
return None
|
||||
if not section.get("section_syntax"):
|
||||
return None
|
||||
|
||||
network_id = section["table_id_ext"]
|
||||
data = section.get("data", b'')
|
||||
|
||||
if len(data) < 2:
|
||||
return None
|
||||
|
||||
# Network descriptors loop
|
||||
network_desc_length = ((data[0] & 0x0F) << 8) | data[1]
|
||||
offset = 2
|
||||
|
||||
network_name = ""
|
||||
|
||||
nd_end = offset + network_desc_length
|
||||
if nd_end > len(data):
|
||||
nd_end = len(data)
|
||||
|
||||
while offset + 2 <= nd_end:
|
||||
desc_tag = data[offset]
|
||||
desc_len = data[offset + 1]
|
||||
desc_data = data[offset + 2:offset + 2 + desc_len]
|
||||
offset += 2 + desc_len
|
||||
|
||||
if desc_tag == 0x40:
|
||||
# network_name_descriptor
|
||||
network_name = _decode_dvb_string(desc_data)
|
||||
|
||||
offset = nd_end
|
||||
|
||||
# Transport stream loop
|
||||
if offset + 2 > len(data):
|
||||
return {
|
||||
"table_id": section["table_id"],
|
||||
"network_id": network_id,
|
||||
"network_name": network_name,
|
||||
"version": section["version"],
|
||||
"transports": [],
|
||||
}
|
||||
|
||||
ts_loop_length = ((data[offset] & 0x0F) << 8) | data[offset + 1]
|
||||
offset += 2
|
||||
|
||||
transports = []
|
||||
ts_end = offset + ts_loop_length
|
||||
if ts_end > len(data):
|
||||
ts_end = len(data)
|
||||
|
||||
while offset + 6 <= ts_end:
|
||||
ts_id = (data[offset] << 8) | data[offset + 1]
|
||||
original_network_id = (data[offset + 2] << 8) | data[offset + 3]
|
||||
td_length = ((data[offset + 4] & 0x0F) << 8) | data[offset + 5]
|
||||
offset += 6
|
||||
|
||||
# Parse transport descriptors
|
||||
frequency_ghz = 0.0
|
||||
polarization = ""
|
||||
symbol_rate = 0
|
||||
fec = ""
|
||||
orbital_position = 0.0
|
||||
modulation = ""
|
||||
roll_off = ""
|
||||
|
||||
td_end = offset + td_length
|
||||
if td_end > ts_end:
|
||||
td_end = ts_end
|
||||
|
||||
while offset + 2 <= td_end:
|
||||
desc_tag = data[offset]
|
||||
desc_len = data[offset + 1]
|
||||
desc_data = data[offset + 2:offset + 2 + desc_len]
|
||||
offset += 2 + desc_len
|
||||
|
||||
if desc_tag == 0x43 and len(desc_data) >= 11:
|
||||
# satellite_delivery_system_descriptor (11 bytes BCD)
|
||||
frequency_ghz = _bcd_freq(desc_data[0:4])
|
||||
orbital_position = _bcd_orbital(desc_data[4:6])
|
||||
# Byte 6: west/east flag (bit 7), polarization (bits 6-5),
|
||||
# roll-off (bits 4-3), modulation system (bit 2),
|
||||
# modulation type (bits 1-0)
|
||||
flag_byte = desc_data[6]
|
||||
if not (flag_byte & 0x80):
|
||||
orbital_position = -orbital_position # West
|
||||
pol_bits = (flag_byte >> 5) & 0x03
|
||||
polarization = ["H", "V", "L", "R"][pol_bits]
|
||||
ro_bits = (flag_byte >> 3) & 0x03
|
||||
roll_off = ["0.35", "0.25", "0.20", "reserved"][ro_bits]
|
||||
mod_sys = (flag_byte >> 2) & 0x01
|
||||
mod_type = flag_byte & 0x03
|
||||
if mod_sys == 0:
|
||||
modulation = ["auto", "QPSK", "8PSK", "16QAM"][mod_type]
|
||||
else:
|
||||
modulation = ["auto", "QPSK", "8PSK", "16APSK"][mod_type]
|
||||
symbol_rate = _bcd_sr(desc_data[7:11])
|
||||
fec_inner = desc_data[10] & 0x0F
|
||||
fec = _fec_inner_str(fec_inner)
|
||||
|
||||
offset = max(offset, td_end)
|
||||
|
||||
transports.append({
|
||||
"ts_id": ts_id,
|
||||
"original_network_id": original_network_id,
|
||||
"frequency_ghz": frequency_ghz,
|
||||
"polarization": polarization,
|
||||
"symbol_rate": symbol_rate,
|
||||
"fec": fec,
|
||||
"orbital_position": orbital_position,
|
||||
"modulation": modulation,
|
||||
"roll_off": roll_off,
|
||||
})
|
||||
|
||||
return {
|
||||
"table_id": section["table_id"],
|
||||
"network_id": network_id,
|
||||
"network_name": network_name,
|
||||
"version": section["version"],
|
||||
"transports": transports,
|
||||
}
|
||||
|
||||
|
||||
def _decode_dvb_string(data: bytes) -> str:
|
||||
"""
|
||||
Decode a DVB text string per EN 300 468 Annex A.
|
||||
|
||||
If the first byte is a character table selector (0x01-0x1F),
|
||||
select the appropriate encoding. Otherwise assume ISO 8859-1.
|
||||
"""
|
||||
if not data:
|
||||
return ""
|
||||
|
||||
first = data[0]
|
||||
if first < 0x20:
|
||||
# Character table selector byte
|
||||
if first == 0x01:
|
||||
return data[1:].decode('iso-8859-5', errors='replace')
|
||||
elif first == 0x02:
|
||||
return data[1:].decode('iso-8859-6', errors='replace')
|
||||
elif first == 0x03:
|
||||
return data[1:].decode('iso-8859-7', errors='replace')
|
||||
elif first == 0x04:
|
||||
return data[1:].decode('iso-8859-8', errors='replace')
|
||||
elif first == 0x05:
|
||||
return data[1:].decode('iso-8859-9', errors='replace')
|
||||
elif first == 0x06:
|
||||
return data[1:].decode('iso-8859-10', errors='replace')
|
||||
elif first == 0x07:
|
||||
return data[1:].decode('iso-8859-11', errors='replace')
|
||||
elif first == 0x09:
|
||||
return data[1:].decode('iso-8859-13', errors='replace')
|
||||
elif first == 0x0A:
|
||||
return data[1:].decode('iso-8859-14', errors='replace')
|
||||
elif first == 0x0B:
|
||||
return data[1:].decode('iso-8859-15', errors='replace')
|
||||
elif first == 0x10:
|
||||
# Two more selector bytes follow
|
||||
if len(data) >= 3:
|
||||
sub = (data[1] << 8) | data[2]
|
||||
try:
|
||||
return data[3:].decode(f'iso-8859-{sub}', errors='replace')
|
||||
except (LookupError, ValueError):
|
||||
return data[3:].decode('iso-8859-1', errors='replace')
|
||||
return data[1:].decode('iso-8859-1', errors='replace')
|
||||
elif first == 0x11:
|
||||
return data[1:].decode('utf-16-be', errors='replace')
|
||||
elif first == 0x13:
|
||||
return data[1:].decode('gb2312', errors='replace')
|
||||
elif first == 0x15:
|
||||
return data[1:].decode('utf-8', errors='replace')
|
||||
else:
|
||||
# Unknown selector, skip it
|
||||
return data[1:].decode('iso-8859-1', errors='replace')
|
||||
|
||||
return data.decode('iso-8859-1', errors='replace')
|
||||
|
||||
|
||||
def _bcd_freq(data: bytes) -> float:
|
||||
"""
|
||||
Decode 4-byte BCD frequency from satellite_delivery_system_descriptor.
|
||||
Per EN 300 468, the 8 BCD digits encode the frequency such that
|
||||
the integer value divided by 10^5 yields GHz.
|
||||
e.g., 0x11 0x72 0x75 0x00 -> digits 11727500 -> 11.72750 GHz.
|
||||
"""
|
||||
value = 0
|
||||
for b in data:
|
||||
value = value * 100 + ((b >> 4) & 0x0F) * 10 + (b & 0x0F)
|
||||
return value / 1_000_000.0
|
||||
|
||||
|
||||
def _bcd_orbital(data: bytes) -> float:
|
||||
"""
|
||||
Decode 2-byte BCD orbital position per EN 300 468.
|
||||
4 BCD digits: XX.XX degrees (2 integer + 2 fractional).
|
||||
e.g., 0x28 0x20 = 28.20 degrees (Astra 28.2E).
|
||||
"""
|
||||
value = 0
|
||||
for b in data:
|
||||
value = value * 100 + ((b >> 4) & 0x0F) * 10 + (b & 0x0F)
|
||||
return value / 100.0
|
||||
|
||||
|
||||
def _bcd_sr(data: bytes) -> int:
|
||||
"""
|
||||
Decode symbol rate from satellite_delivery_system_descriptor.
|
||||
4 bytes: upper 28 bits = 7 BCD digits of symbol rate (XXXX.XXX Msps),
|
||||
lower 4 bits = FEC inner code (handled separately by caller).
|
||||
e.g., 0x00 0x27 0x50 0x03 -> digits 0027500 -> 27.500 Msps = 27,500,000 sps.
|
||||
"""
|
||||
# Extract 7 BCD digits from the upper 28 bits (ignore last nibble = FEC)
|
||||
value = 0
|
||||
for b in data[:4]:
|
||||
value = value * 100 + ((b >> 4) & 0x0F) * 10 + (b & 0x0F)
|
||||
# value now has 8 BCD digits decoded; drop the last one (FEC nibble)
|
||||
value = value // 10
|
||||
# value = XXXX.XXX Msps as integer XXXXXXX, divide by 1000 for Msps
|
||||
# Multiply by 1000 to get sps: (value / 1000) * 1e6 = value * 1000
|
||||
return value * 1000
|
||||
|
||||
|
||||
def _fec_inner_str(code: int) -> str:
|
||||
"""Convert FEC inner code rate nibble to string."""
|
||||
fec_map = {
|
||||
0: "not defined",
|
||||
1: "1/2",
|
||||
2: "2/3",
|
||||
3: "3/4",
|
||||
4: "5/6",
|
||||
5: "7/8",
|
||||
6: "8/9",
|
||||
7: "3/5",
|
||||
8: "4/5",
|
||||
9: "9/10",
|
||||
15: "none",
|
||||
}
|
||||
return fec_map.get(code, f"reserved({code})")
|
||||
|
||||
|
||||
def open_input(path: str):
|
||||
"""Open TS input from a file path or stdin ('-')."""
|
||||
if path == '-':
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue