| 1 | #!/usr/bin/env python3
|
|---|
| 2 | """Write a BNC raw file with crafted, CRC-valid RTCM3 frames that use the
|
|---|
| 3 | upper end of the field ranges allowed by the format (decoder robustness test).
|
|---|
| 4 |
|
|---|
| 5 | Streams (one station ID each):
|
|---|
| 6 | CRAFT0IGS0 IGS-SSR VTEC (4076/201), decoded by SsrCorrIgs, followed by
|
|---|
| 7 | RTCM 1300/1301/1302 with 31-character names (BNC's RTCM scan
|
|---|
| 8 | lists them only for streams that also decode successfully);
|
|---|
| 9 | in the first epoch also two RTCM 1033 (antenna and receiver
|
|---|
| 10 | descriptors): a complete one and one whose receiver descriptor
|
|---|
| 11 | length (200) runs beyond the end of the message
|
|---|
| 12 | CRAFT0RTC0 RTCM-SSR VTEC (1264), decoded by SsrCorrRtcm
|
|---|
| 13 | CRAFT0NEW0 new RTCM-SSR metadata (60), not dispatched by BNC at present;
|
|---|
| 14 | decoded by ssr_harness.cpp with SsrCorrRtcmNew:
|
|---|
| 15 | - 7 entries (maximum), additional parameter of 40 and 0 bits
|
|---|
| 16 | - 31 entries (5-bit count, more than META_MAXNUMENTRIES=7)
|
|---|
| 17 | and satellite antenna messages (DF+010-DF+021):
|
|---|
| 18 | - BDS (83): C01, C17, C33, C49, C64 (mask bits in all four
|
|---|
| 19 | 16-bit quarters of DF394), individual blocks
|
|---|
| 20 | - GPS (80): G01, G02, G32 sharing one block and one frequency
|
|---|
| 21 | block; nadir angle corrections incl. the invalid value
|
|---|
| 22 |
|
|---|
| 23 | VTEC epochs (every 5 s):
|
|---|
| 24 | t0 2 layers: layer 1 degree 16/order 2, layer 2 degree 2/order 2
|
|---|
| 25 | t0+5 4 layers: layers 1-3 degree 1/order 1, layer 4 degree 16/order 16
|
|---|
| 26 | t0+10 1 layer degree 2/order 2 (decoding continues normally)
|
|---|
| 27 | Degree and order are transmitted as N-1 in 4 bits, so 16 is the maximum
|
|---|
| 28 | (DF474/DF475, IDF037/IDF038). Coefficient C(d,o) is (100*d + o)/200 TECU
|
|---|
| 29 | and S(d,o) its negative, so every value in the .ssr output can be checked
|
|---|
| 30 | by eye; the heights are 100 km * layer number.
|
|---|
| 31 |
|
|---|
| 32 | With --rawfile, a raw file with hostile chunk headers is written instead
|
|---|
| 33 | (test of the raw file reader, data/crafted_rawfile.raw):
|
|---|
| 34 | ../ESCAPE0 station ID with a path: RTCM-SSR VTEC whose .ssr file must be
|
|---|
| 35 | written inside the output directory
|
|---|
| 36 | CRAFT0RTC0 RTCM-SSR VTEC, 5 s later
|
|---|
| 37 | then a chunk header announcing 2000000000 bytes, which must be rejected
|
|---|
| 38 |
|
|---|
| 39 | Usage: mkcrafted.py <out.raw>
|
|---|
| 40 | mkcrafted.py --rawfile <out.raw>
|
|---|
| 41 | """
|
|---|
| 42 |
|
|---|
| 43 | import datetime
|
|---|
| 44 | import os
|
|---|
| 45 | import sys
|
|---|
| 46 |
|
|---|
| 47 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|---|
| 48 | from bncraw import crc24q, write_chunks # noqa: E402
|
|---|
| 49 |
|
|---|
| 50 | T0 = datetime.datetime(2026, 10, 23, 12, 0, 0) # GPS time, a Friday
|
|---|
| 51 | NAME31 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ01234"
|
|---|
| 52 |
|
|---|
| 53 |
|
|---|
| 54 | class BitWriter:
|
|---|
| 55 | def __init__(self):
|
|---|
| 56 | self.bits = []
|
|---|
| 57 |
|
|---|
| 58 | def u(self, value, n):
|
|---|
| 59 | assert 0 <= value < (1 << n), (value, n)
|
|---|
| 60 | self.bits += [(value >> (n - 1 - i)) & 1 for i in range(n)]
|
|---|
| 61 |
|
|---|
| 62 | def s(self, value, n):
|
|---|
| 63 | assert -(1 << (n - 1)) <= value < (1 << (n - 1)), (value, n)
|
|---|
| 64 | self.u(value & ((1 << n) - 1), n)
|
|---|
| 65 |
|
|---|
| 66 | def text(self, string, nlen):
|
|---|
| 67 | self.u(len(string), nlen)
|
|---|
| 68 | for c in string.encode("ascii"):
|
|---|
| 69 | self.u(c, 8)
|
|---|
| 70 |
|
|---|
| 71 | def payload(self):
|
|---|
| 72 | bits = self.bits + [0] * (-len(self.bits) % 8)
|
|---|
| 73 | return bytes(int("".join(map(str, bits[i:i + 8])), 2)
|
|---|
| 74 | for i in range(0, len(bits), 8))
|
|---|
| 75 |
|
|---|
| 76 |
|
|---|
| 77 | def frame(payload):
|
|---|
| 78 | assert len(payload) <= 1023, len(payload)
|
|---|
| 79 | head = bytes([0xD3, len(payload) >> 8, len(payload) & 0xFF]) + payload
|
|---|
| 80 | return head + crc24q(head).to_bytes(3, "big")
|
|---|
| 81 |
|
|---|
| 82 |
|
|---|
| 83 | def gps_sow(t):
|
|---|
| 84 | return (t.weekday() + 1) % 7 * 86400 + t.hour * 3600 + t.minute * 60 + t.second
|
|---|
| 85 |
|
|---|
| 86 |
|
|---|
| 87 | def vtec_body(w, t, layers):
|
|---|
| 88 | """VTEC content after the message number(s): IDF003-IDF041 / DF385-DF478."""
|
|---|
| 89 | w.u(gps_sow(t), 20) # epoch time 1 s
|
|---|
| 90 | w.u(2, 4) # update interval 5 s
|
|---|
| 91 | w.u(0, 1) # multiple message indicator
|
|---|
| 92 | w.u(1, 4) # SSR IOD
|
|---|
| 93 | w.u(4711, 16) # provider ID
|
|---|
| 94 | w.u(1, 4) # solution ID
|
|---|
| 95 | w.u(20, 9) # VTEC quality 1.0 TECU
|
|---|
| 96 | w.u(len(layers) - 1, 2)
|
|---|
| 97 | for n, (degree, order) in enumerate(layers, 1):
|
|---|
| 98 | w.u(10 * n, 8) # height, 10 km units
|
|---|
| 99 | w.u(degree - 1, 4)
|
|---|
| 100 | w.u(order - 1, 4)
|
|---|
| 101 | for o in range(order + 1):
|
|---|
| 102 | for d in range(o, degree + 1):
|
|---|
| 103 | w.s(100 * d + o, 16)
|
|---|
| 104 | for o in range(1, order + 1):
|
|---|
| 105 | for d in range(o, degree + 1):
|
|---|
| 106 | w.s(-(100 * d + o), 16)
|
|---|
| 107 |
|
|---|
| 108 |
|
|---|
| 109 | def vtec_igs(t, layers):
|
|---|
| 110 | w = BitWriter()
|
|---|
| 111 | w.u(4076, 12)
|
|---|
| 112 | w.u(1, 3) # IGS SSR version
|
|---|
| 113 | w.u(201, 8) # IGS message subtype: VTEC
|
|---|
| 114 | vtec_body(w, t, layers)
|
|---|
| 115 | return frame(w.payload())
|
|---|
| 116 |
|
|---|
| 117 |
|
|---|
| 118 | def vtec_rtcm(t, layers):
|
|---|
| 119 | w = BitWriter()
|
|---|
| 120 | w.u(1264, 12)
|
|---|
| 121 | vtec_body(w, t, layers)
|
|---|
| 122 | return frame(w.payload())
|
|---|
| 123 |
|
|---|
| 124 |
|
|---|
| 125 | def service_crs():
|
|---|
| 126 | w = BitWriter()
|
|---|
| 127 | w.u(1300, 12)
|
|---|
| 128 | w.text(NAME31, 5)
|
|---|
| 129 | w.u(2000, 16) # coordinate epoch 1900 + 20.00
|
|---|
| 130 | return frame(w.payload())
|
|---|
| 131 |
|
|---|
| 132 |
|
|---|
| 133 | def helmert():
|
|---|
| 134 | w = BitWriter()
|
|---|
| 135 | w.u(1301, 12)
|
|---|
| 136 | w.text(NAME31, 5) # source name
|
|---|
| 137 | w.text(NAME31[::-1], 5) # target name
|
|---|
| 138 | w.u(1, 8) # system identification number
|
|---|
| 139 | w.u(0, 10) # utilized transformation message indicator
|
|---|
| 140 | w.u(61000 - 44244, 16) # MJD of t0
|
|---|
| 141 | for n in (23, 23, 23, 32, 32, 32, 25, 17, 17, 17, 17, 17, 17, 14):
|
|---|
| 142 | w.s(0, n)
|
|---|
| 143 | return frame(w.payload())
|
|---|
| 144 |
|
|---|
| 145 |
|
|---|
| 146 | def rtcm_crs():
|
|---|
| 147 | w = BitWriter()
|
|---|
| 148 | w.u(1302, 12)
|
|---|
| 149 | w.text(NAME31, 5)
|
|---|
| 150 | w.u(1, 1) # anchor: plate-fixed
|
|---|
| 151 | w.u(7, 5) # plate number: Eurasia
|
|---|
| 152 | w.u(2, 3) # number of database links
|
|---|
| 153 | w.text(NAME31, 5)
|
|---|
| 154 | w.text(NAME31[::-1], 5)
|
|---|
| 155 | return frame(w.payload())
|
|---|
| 156 |
|
|---|
| 157 |
|
|---|
| 158 | def descriptors(strings, truncate=False):
|
|---|
| 159 | """RTCM 1033: antenna, setup ID, serial, receiver, firmware, serial."""
|
|---|
| 160 | w = BitWriter()
|
|---|
| 161 | w.u(1033, 12)
|
|---|
| 162 | w.u(0, 12) # reference station ID
|
|---|
| 163 | ant, antser, rec, firm, recser = strings
|
|---|
| 164 | w.text(ant, 8)
|
|---|
| 165 | w.u(0, 8) # antenna setup ID
|
|---|
| 166 | w.text(antser, 8)
|
|---|
| 167 | if truncate:
|
|---|
| 168 | w.u(200, 8) # receiver descriptor length beyond the message
|
|---|
| 169 | for c in rec.encode("ascii"):
|
|---|
| 170 | w.u(c, 8)
|
|---|
| 171 | else:
|
|---|
| 172 | w.text(rec, 8)
|
|---|
| 173 | w.text(firm, 8)
|
|---|
| 174 | w.text(recser, 8)
|
|---|
| 175 | return frame(w.payload())
|
|---|
| 176 |
|
|---|
| 177 |
|
|---|
| 178 | def metadata(entries):
|
|---|
| 179 | """entries: list of None (default model) or number of additional bits."""
|
|---|
| 180 | w = BitWriter()
|
|---|
| 181 | w.u(60, 12)
|
|---|
| 182 | w.u(1, 4) # SSR IOD
|
|---|
| 183 | w.u(4711, 16) # provider ID
|
|---|
| 184 | w.u(1, 4) # solution ID
|
|---|
| 185 | w.u(len(entries), 5)
|
|---|
| 186 | for n, bits in enumerate(entries):
|
|---|
| 187 | w.u(n % 32, 5) # model correction type
|
|---|
| 188 | w.u(1, 1) # applied
|
|---|
| 189 | w.u(bits is not None, 1)
|
|---|
| 190 | if bits is not None:
|
|---|
| 191 | w.u(1, 3) # non-default model identifier
|
|---|
| 192 | w.u(0, 1) # no data IOD
|
|---|
| 193 | if bits is not None:
|
|---|
| 194 | w.u(1, 1) # additional parameter follows
|
|---|
| 195 | w.u(bits, 8)
|
|---|
| 196 | for i in range(bits):
|
|---|
| 197 | w.u(i & 1, 1)
|
|---|
| 198 | return frame(w.payload())
|
|---|
| 199 |
|
|---|
| 200 |
|
|---|
| 201 | def antenna(msg, sats, shared, nadir):
|
|---|
| 202 | """Satellite antenna message; sats: PRNs (1..64) of the mask."""
|
|---|
| 203 | w = BitWriter()
|
|---|
| 204 | w.u(msg, 12)
|
|---|
| 205 | w.u(4711, 16) # provider ID
|
|---|
| 206 | w.u(5, 6) # satellite antenna IOD
|
|---|
| 207 | w.u(1, 1) # phase center information
|
|---|
| 208 | w.u(0, 1) # group delay information
|
|---|
| 209 | w.u(1, 1) # nadir angle dependent corrections follow
|
|---|
| 210 | w.u(3, 5) # maximum off-nadir angle: corrections 0..3 deg
|
|---|
| 211 | w.u(1, 4) # range extension: 3 + 1 = 4 bits
|
|---|
| 212 | w.u(shared, 1) # satellite set indicator
|
|---|
| 213 | mask = 0
|
|---|
| 214 | for prn in sats:
|
|---|
| 215 | mask |= 1 << (64 - prn)
|
|---|
| 216 | w.u(mask, 64)
|
|---|
| 217 | for n in range(1 if shared else len(sats)):
|
|---|
| 218 | w.u(shared, 1) # frequency set indicator
|
|---|
| 219 | w.u(0b101000, 6) # frequencies 0 and 2
|
|---|
| 220 | for f in range(1 if shared else 2):
|
|---|
| 221 | w.u(1, 1) # nadir correction follows
|
|---|
| 222 | w.s(100 * n + 10 * f + 1, 12) # mm
|
|---|
| 223 | for deg in range(4):
|
|---|
| 224 | w.s(nadir[(n + f + deg) % len(nadir)], 4) # mm, -8: invalid
|
|---|
| 225 | return frame(w.payload())
|
|---|
| 226 |
|
|---|
| 227 |
|
|---|
| 228 | def rawfile(path):
|
|---|
| 229 | chunks = []
|
|---|
| 230 | for i, sta in enumerate(["../ESCAPE0", "CRAFT0RTC0"]):
|
|---|
| 231 | t = T0 + datetime.timedelta(seconds=5 * i)
|
|---|
| 232 | chunks.append((t.isoformat(), sta, "RTCM_3", vtec_rtcm(t, [(2, 2)])))
|
|---|
| 233 | write_chunks(path, chunks)
|
|---|
| 234 | t = T0 + datetime.timedelta(seconds=10)
|
|---|
| 235 | with open(path, "ab") as o:
|
|---|
| 236 | o.write(("%s CRAFT0RTC0 RTCM_3 2000000000\n" % t.isoformat()).encode())
|
|---|
| 237 | o.write(vtec_rtcm(t, [(2, 2)]) + b"\n")
|
|---|
| 238 |
|
|---|
| 239 |
|
|---|
| 240 | def main():
|
|---|
| 241 | if len(sys.argv) == 3 and sys.argv[1] == "--rawfile":
|
|---|
| 242 | rawfile(sys.argv[2])
|
|---|
| 243 | return
|
|---|
| 244 | if len(sys.argv) != 2:
|
|---|
| 245 | sys.exit(__doc__)
|
|---|
| 246 | epochs = [
|
|---|
| 247 | [(16, 2), (2, 2)],
|
|---|
| 248 | [(1, 1), (1, 1), (1, 1), (16, 16)],
|
|---|
| 249 | [(2, 2)],
|
|---|
| 250 | ]
|
|---|
| 251 | chunks = []
|
|---|
| 252 | for i, layers in enumerate(epochs):
|
|---|
| 253 | t = T0 + datetime.timedelta(seconds=5 * i)
|
|---|
| 254 | stamp = t.isoformat()
|
|---|
| 255 | data = vtec_igs(t, layers) + service_crs() + helmert() + rtcm_crs()
|
|---|
| 256 | if i == 0:
|
|---|
| 257 | data += descriptors(["LEIAR25.R4 LEIT", "12345", "SEPT POLARX5",
|
|---|
| 258 | "5.5.0", "3001234"])
|
|---|
| 259 | data += descriptors(["TRUNCATED ANTENNA", "9", "RECEIVER", "", ""],
|
|---|
| 260 | truncate=True)
|
|---|
| 261 | chunks.append((stamp, "CRAFT0IGS0", "RTCM_3", data))
|
|---|
| 262 | chunks.append((stamp, "CRAFT0RTC0", "RTCM_3", vtec_rtcm(t, layers)))
|
|---|
| 263 | stamp = (T0 + datetime.timedelta(seconds=15)).isoformat()
|
|---|
| 264 | chunks.append((stamp, "CRAFT0NEW0", "RTCM_3",
|
|---|
| 265 | metadata([40, 0] + [None] * 5) + metadata([None] * 31)
|
|---|
| 266 | + antenna(83, [1, 17, 33, 49, 64], 0, [-7, 0, 7, 3])
|
|---|
| 267 | + antenna(80, [1, 2, 32], 1, [5, -8, -1, 2])))
|
|---|
| 268 | write_chunks(sys.argv[1], chunks)
|
|---|
| 269 |
|
|---|
| 270 |
|
|---|
| 271 | if __name__ == "__main__":
|
|---|
| 272 | main()
|
|---|