BCNP 3.2.1
Batched Command Network Protocol
Loading...
Searching...
No Matches
bcnp_codegen.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3BCNP Schema Compiler
4
5Compiles BCNP message schema (JSON) to C++ header and Python bindings.
6Generates serialization/deserialization code and computes schema hash (CRC32).
7
8Usage:
9 python bcnp_codegen.py schema/messages.json --cpp src/bcnp/generated/
10 python bcnp_codegen.py schema/messages.json --python examples/
11"""
12
13import argparse
14import json
15import sys
16from pathlib import Path
17from typing import Any
18
19# Type info: (wire_size, cpp_type, is_signed, python_struct_fmt)
20TYPE_INFO = {
21 "int8": (1, "int8_t", True, "b"),
22 "uint8": (1, "uint8_t", False, "B"),
23 "int16": (2, "int16_t", True, "h"),
24 "uint16": (2, "uint16_t", False, "H"),
25 "int32": (4, "int32_t", True, "i"),
26 "uint32": (4, "uint32_t", False, "I"),
27 "float32": (4, "float", True, "f"), # Encoded as int32 with scale
28}
29
30
31def compute_crc32(data: bytes) -> int:
32 """IEEE CRC32 matching BCNP's existing implementation."""
33 crc = 0xFFFFFFFF
34 for byte in data:
35 crc ^= byte
36 for _ in range(8):
37 if crc & 1:
38 crc = (crc >> 1) ^ 0xEDB88320
39 else:
40 crc >>= 1
41 return crc ^ 0xFFFFFFFF
42
43
44def canonical_json(schema: dict) -> str:
45 """
46 Produce a canonical JSON string for schema hashing.
47 Only includes structurally significant fields (id, name, type, scale).
48 """
49 canonical = {
50 "version": schema["version"],
51 "messages": []
52 }
53 for msg in sorted(schema["messages"], key=lambda m: m["id"]):
54 msg_canonical = {
55 "id": msg["id"],
56 "name": msg["name"],
57 "fields": []
58 }
59 for field in msg["fields"]:
60 field_canonical = {
61 "name": field["name"],
62 "type": field["type"]
63 }
64 if "scale" in field:
65 field_canonical["scale"] = field["scale"]
66 msg_canonical["fields"].append(field_canonical)
67 canonical["messages"].append(msg_canonical)
68 return json.dumps(canonical, separators=(",", ":"), sort_keys=True)
69
70
71def compute_schema_hash(schema: dict) -> int:
72 """Compute CRC32 of canonical JSON schema."""
73 canonical = canonical_json(schema)
74 return compute_crc32(canonical.encode("utf-8"))
75
76
77def compute_message_size(msg: dict) -> int:
78 """Compute wire size of a message in bytes."""
79 size = 0
80 for field in msg["fields"]:
81 size += TYPE_INFO[field["type"]][0]
82 return size
83
84
85def generate_cpp_header(schema: dict, output_dir: Path) -> None:
86 """Generate C++ header with message types and serialization.
87
88 The file is written to output_dir/bcnp/message_types.h so that
89 users can add output_dir to their include path and use:
90 #include <bcnp/message_types.h>
91 """
92 bcnp_dir = output_dir / "bcnp"
93 bcnp_dir.mkdir(parents=True, exist_ok=True)
94 output_file = bcnp_dir / "message_types.h"
95
96 namespace = schema.get("namespace", "bcnp")
97 version_parts = schema["version"].split(".")
98 major = int(version_parts[0])
99 minor = int(version_parts[1])
100 schema_hash = compute_schema_hash(schema)
101
102 lines = []
103 lines.append("// AUTO-GENERATED by bcnp_codegen.py - DO NOT EDIT")
104 lines.append(f"// Schema version: {schema['version']}")
105 lines.append(f"// Schema hash: 0x{schema_hash:08X}")
106 lines.append("#pragma once")
107 lines.append("")
108 lines.append("#include <algorithm>")
109 lines.append("#include <array>")
110 lines.append("#include <cstddef>")
111 lines.append("#include <cstdint>")
112 lines.append("#include <cmath>")
113 lines.append("#include <cstring>")
114 lines.append("#include <functional>")
115 lines.append("#include <limits>")
116 lines.append("#include <optional>")
117 lines.append("#include <variant>")
118 lines.append("")
119 lines.append(f"namespace {namespace} {{")
120 lines.append("")
121 lines.append("// ============================================================================")
122 lines.append("// Protocol Constants")
123 lines.append("// ============================================================================")
124 lines.append("")
125 lines.append(f"constexpr uint8_t kProtocolMajorV3 = {major};")
126 lines.append(f"constexpr uint8_t kProtocolMinorV3 = {minor};")
127 lines.append(f"constexpr uint32_t kSchemaHash = 0x{schema_hash:08X}U;")
128 lines.append("")
129 lines.append("// Handshake packet structure: \"BCNP\" (4 bytes) + schema hash (4 bytes)")
130 lines.append("constexpr std::size_t kHandshakeSize = 8;")
131 lines.append("constexpr std::array<uint8_t, 4> kHandshakeMagic = {{'B', 'C', 'N', 'P'}};")
132 lines.append("")
133 lines.append("// V3 Header: Major(1) + Minor(1) + Flags(1) + MsgTypeId(2) + MsgCount(2) = 7 bytes")
134 lines.append("constexpr std::size_t kHeaderSizeV3 = 7;")
135 lines.append("constexpr std::size_t kHeaderMsgTypeIndex = 3;")
136 lines.append("constexpr std::size_t kHeaderMsgCountIndex = 5;")
137 lines.append("")
138
139 # Message type IDs enum
140 lines.append("// ============================================================================")
141 lines.append("// Message Type IDs")
142 lines.append("// ============================================================================")
143 lines.append("")
144 lines.append("enum class MessageTypeId : uint16_t {")
145 lines.append(" Unknown = 0,")
146 for msg in schema["messages"]:
147 lines.append(f" {msg['name']} = {msg['id']},")
148 lines.append("};")
149 lines.append("")
150
151 # Message size constants
152 lines.append("// Message sizes (wire format, bytes)")
153 for msg in schema["messages"]:
154 size = compute_message_size(msg)
155 lines.append(f"constexpr std::size_t k{msg['name']}Size = {size};")
156 lines.append("")
157
158 # Byte order utilities
159 lines.append("// ============================================================================")
160 lines.append("// Byte Order Utilities (Big-Endian Wire Format)")
161 lines.append("// ============================================================================")
162 lines.append("")
163 lines.append("namespace detail {")
164 lines.append("")
165 lines.append("inline uint16_t LoadU16(const uint8_t* p) {")
166 lines.append(" return (uint16_t(p[0]) << 8) | uint16_t(p[1]);")
167 lines.append("}")
168 lines.append("")
169 lines.append("inline uint32_t LoadU32(const uint8_t* p) {")
170 lines.append(" return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) |")
171 lines.append(" (uint32_t(p[2]) << 8) | uint32_t(p[3]);")
172 lines.append("}")
173 lines.append("")
174 lines.append("inline int16_t LoadS16(const uint8_t* p) {")
175 lines.append(" return static_cast<int16_t>(LoadU16(p));")
176 lines.append("}")
177 lines.append("")
178 lines.append("inline int32_t LoadS32(const uint8_t* p) {")
179 lines.append(" return static_cast<int32_t>(LoadU32(p));")
180 lines.append("}")
181 lines.append("")
182 lines.append("inline void StoreU16(uint16_t v, uint8_t* p) {")
183 lines.append(" p[0] = static_cast<uint8_t>((v >> 8) & 0xFF);")
184 lines.append(" p[1] = static_cast<uint8_t>(v & 0xFF);")
185 lines.append("}")
186 lines.append("")
187 lines.append("inline void StoreU32(uint32_t v, uint8_t* p) {")
188 lines.append(" p[0] = static_cast<uint8_t>((v >> 24) & 0xFF);")
189 lines.append(" p[1] = static_cast<uint8_t>((v >> 16) & 0xFF);")
190 lines.append(" p[2] = static_cast<uint8_t>((v >> 8) & 0xFF);")
191 lines.append(" p[3] = static_cast<uint8_t>(v & 0xFF);")
192 lines.append("}")
193 lines.append("")
194 lines.append("inline void StoreS16(int16_t v, uint8_t* p) {")
195 lines.append(" StoreU16(static_cast<uint16_t>(v), p);")
196 lines.append("}")
197 lines.append("")
198 lines.append("inline void StoreS32(int32_t v, uint8_t* p) {")
199 lines.append(" StoreU32(static_cast<uint32_t>(v), p);")
200 lines.append("}")
201 lines.append("")
202 lines.append("inline int32_t QuantizeFloat(float value, float scale) {")
203 lines.append(" const double scaled = static_cast<double>(value) * static_cast<double>(scale);")
204 lines.append(" const double clamped = std::clamp(scaled,")
205 lines.append(" static_cast<double>(std::numeric_limits<int32_t>::min()),")
206 lines.append(" static_cast<double>(std::numeric_limits<int32_t>::max()));")
207 lines.append(" return static_cast<int32_t>(std::llround(clamped));")
208 lines.append("}")
209 lines.append("")
210 lines.append("inline float DequantizeFloat(int32_t fixed, float scale) {")
211 lines.append(" return static_cast<float>(static_cast<double>(fixed) / static_cast<double>(scale));")
212 lines.append("}")
213 lines.append("")
214 lines.append("} // namespace detail")
215 lines.append("")
216
217 # Generate message structs
218 lines.append("// ============================================================================")
219 lines.append("// Message Structs")
220 lines.append("// ============================================================================")
221 lines.append("")
222
223 for msg in schema["messages"]:
224 desc = msg.get("description", "")
225 lines.append(f"/// {desc}")
226 lines.append(f"struct {msg['name']} {{")
227 lines.append(f" static constexpr MessageTypeId kTypeId = MessageTypeId::{msg['name']};")
228 lines.append(f" static constexpr std::size_t kWireSize = k{msg['name']}Size;")
229 lines.append("")
230 for field in msg["fields"]:
231 cpp_type = TYPE_INFO[field["type"]][1]
232 if field["type"] == "float32":
233 cpp_type = "float" # User sees float, wire is int32
234 field_desc = field.get("description", "")
235 unit = field.get("unit", "")
236 comment = f" // {field_desc}" if field_desc else ""
237 if unit:
238 comment = f" // {field_desc} ({unit})" if field_desc else f" // ({unit})"
239 lines.append(f" {cpp_type} {field['name']}{{0}};{comment}")
240 lines.append("")
241
242 # Encode method
243 lines.append(" bool Encode(uint8_t* out, std::size_t capacity) const {")
244 lines.append(f" if (capacity < kWireSize) return false;")
245 offset = 0
246 for field in msg["fields"]:
247 ftype = field["type"]
248 fname = field["name"]
249 size = TYPE_INFO[ftype][0]
250
251 if ftype == "float32":
252 scale = field.get("scale", 10000)
253 lines.append(f" if (!std::isfinite({fname})) return false;")
254 lines.append(f" detail::StoreS32(detail::QuantizeFloat({fname}, {scale}.0f), &out[{offset}]);")
255 elif ftype == "int32":
256 lines.append(f" detail::StoreS32({fname}, &out[{offset}]);")
257 elif ftype == "uint32":
258 lines.append(f" detail::StoreU32({fname}, &out[{offset}]);")
259 elif ftype == "int16":
260 lines.append(f" detail::StoreS16({fname}, &out[{offset}]);")
261 elif ftype == "uint16":
262 lines.append(f" detail::StoreU16({fname}, &out[{offset}]);")
263 elif ftype in ("int8", "uint8"):
264 lines.append(f" out[{offset}] = static_cast<uint8_t>({fname});")
265 offset += size
266 lines.append(" return true;")
267 lines.append(" }")
268 lines.append("")
269
270 # Decode method
271 lines.append(f" static std::optional<{msg['name']}> Decode(const uint8_t* data, std::size_t length) {{")
272 lines.append(f" if (length < kWireSize) return std::nullopt;")
273 lines.append(f" {msg['name']} msg;")
274 offset = 0
275 for field in msg["fields"]:
276 ftype = field["type"]
277 fname = field["name"]
278 size = TYPE_INFO[ftype][0]
279
280 if ftype == "float32":
281 scale = field.get("scale", 10000)
282 lines.append(f" msg.{fname} = detail::DequantizeFloat(detail::LoadS32(&data[{offset}]), {scale}.0f);")
283 lines.append(f" if (!std::isfinite(msg.{fname})) return std::nullopt;")
284 elif ftype == "int32":
285 lines.append(f" msg.{fname} = detail::LoadS32(&data[{offset}]);")
286 elif ftype == "uint32":
287 lines.append(f" msg.{fname} = detail::LoadU32(&data[{offset}]);")
288 elif ftype == "int16":
289 lines.append(f" msg.{fname} = detail::LoadS16(&data[{offset}]);")
290 elif ftype == "uint16":
291 lines.append(f" msg.{fname} = detail::LoadU16(&data[{offset}]);")
292 elif ftype == "uint8":
293 lines.append(f" msg.{fname} = data[{offset}];")
294 elif ftype == "int8":
295 lines.append(f" msg.{fname} = static_cast<int8_t>(data[{offset}]);")
296 offset += size
297 lines.append(" return msg;")
298 lines.append(" }")
299 lines.append("};")
300 lines.append("")
301
302 # Message variant type (only if messages exist)
303 if schema["messages"]:
304 lines.append("// ============================================================================")
305 lines.append("// Message Variant (for type-erased handling)")
306 lines.append("// ============================================================================")
307 lines.append("")
308 variant_types = ", ".join(msg["name"] for msg in schema["messages"])
309 lines.append(f"using Message = std::variant<{variant_types}>;")
310 lines.append("")
311
312 # Message registry
313 lines.append("// ============================================================================")
314 lines.append("// Message Registry")
315 lines.append("// ============================================================================")
316 lines.append("")
317 lines.append("struct MessageInfo {")
318 lines.append(" MessageTypeId typeId;")
319 lines.append(" std::size_t wireSize;")
320 lines.append(" const char* name;")
321 lines.append("};")
322 lines.append("")
323 num_messages = len(schema["messages"])
324 if num_messages > 0:
325 lines.append(f"inline constexpr std::array<MessageInfo, {num_messages}> kMessageRegistry = {{{{")
326 for msg in schema["messages"]:
327 size = compute_message_size(msg)
328 lines.append(f" {{MessageTypeId::{msg['name']}, {size}, \"{msg['name']}\"}},")
329 lines.append("}};")
330 else:
331 lines.append("// No messages defined - add message types to your schema and regenerate")
332 lines.append("inline constexpr std::array<MessageInfo, 0> kMessageRegistry = {{}};")
333 lines.append("")
334 lines.append("inline std::optional<MessageInfo> GetMessageInfo(MessageTypeId typeId) {")
335 lines.append(" for (const auto& info : kMessageRegistry) {")
336 lines.append(" if (info.typeId == typeId) return info;")
337 lines.append(" }")
338 lines.append(" return std::nullopt;")
339 lines.append("}")
340 lines.append("")
341 lines.append("inline std::optional<MessageInfo> GetMessageInfo(uint16_t typeId) {")
342 lines.append(" return GetMessageInfo(static_cast<MessageTypeId>(typeId));")
343 lines.append("}")
344 lines.append("")
345
346 # Handshake utilities
347 lines.append("// ============================================================================")
348 lines.append("// Handshake Utilities")
349 lines.append("// ============================================================================")
350 lines.append("")
351 lines.append("/// Encode handshake with default schema hash")
352 lines.append("inline bool EncodeHandshake(uint8_t* out, std::size_t capacity) {")
353 lines.append(" if (capacity < kHandshakeSize) return false;")
354 lines.append(" std::memcpy(out, kHandshakeMagic.data(), 4);")
355 lines.append(" detail::StoreU32(kSchemaHash, &out[4]);")
356 lines.append(" return true;")
357 lines.append("}")
358 lines.append("")
359 lines.append("/// Encode handshake with custom schema hash (for testing)")
360 lines.append("inline bool EncodeHandshakeWithHash(uint8_t* out, std::size_t capacity, uint32_t schemaHash) {")
361 lines.append(" if (capacity < kHandshakeSize) return false;")
362 lines.append(" std::memcpy(out, kHandshakeMagic.data(), 4);")
363 lines.append(" detail::StoreU32(schemaHash, &out[4]);")
364 lines.append(" return true;")
365 lines.append("}")
366 lines.append("")
367 lines.append("inline bool ValidateHandshake(const uint8_t* data, std::size_t length) {")
368 lines.append(" if (length < kHandshakeSize) return false;")
369 lines.append(" if (std::memcmp(data, kHandshakeMagic.data(), 4) != 0) return false;")
370 lines.append(" const uint32_t remoteHash = detail::LoadU32(&data[4]);")
371 lines.append(" return remoteHash == kSchemaHash;")
372 lines.append("}")
373 lines.append("")
374 lines.append("/// Validate handshake against custom expected hash (for testing)")
375 lines.append("inline bool ValidateHandshakeWithHash(const uint8_t* data, std::size_t length, uint32_t expectedHash) {")
376 lines.append(" if (length < kHandshakeSize) return false;")
377 lines.append(" if (std::memcmp(data, kHandshakeMagic.data(), 4) != 0) return false;")
378 lines.append(" const uint32_t remoteHash = detail::LoadU32(&data[4]);")
379 lines.append(" return remoteHash == expectedHash;")
380 lines.append("}")
381 lines.append("")
382 lines.append("inline uint32_t ExtractSchemaHash(const uint8_t* data, std::size_t length) {")
383 lines.append(" if (length < kHandshakeSize) return 0;")
384 lines.append(" return detail::LoadU32(&data[4]);")
385 lines.append("}")
386 lines.append("")
387
388 lines.append(f"}} // namespace {namespace}")
389 lines.append("")
390
391 output_file.write_text("\n".join(lines))
392 print(f"Generated: {output_file}")
393
394
395def generate_python_bindings(schema: dict, output_dir: Path) -> None:
396 """Generate Python module with message classes and serialization."""
397 output_dir.mkdir(parents=True, exist_ok=True)
398 output_file = output_dir / "bcnp_messages.py"
399
400 schema_hash = compute_schema_hash(schema)
401
402 lines = []
403 lines.append('"""')
404 lines.append("AUTO-GENERATED by bcnp_codegen.py - DO NOT EDIT")
405 lines.append(f"Schema version: {schema['version']}")
406 lines.append(f"Schema hash: 0x{schema_hash:08X}")
407 lines.append('"""')
408 lines.append("")
409 lines.append("import struct")
410 lines.append("from dataclasses import dataclass")
411 lines.append("from typing import Optional, Union")
412 lines.append("")
413 lines.append(f"PROTOCOL_MAJOR = {schema['version'].split('.')[0]}")
414 lines.append(f"PROTOCOL_MINOR = {schema['version'].split('.')[1]}")
415 lines.append(f"SCHEMA_HASH = 0x{schema_hash:08X}")
416 lines.append("HANDSHAKE_MAGIC = b'BCNP'")
417 lines.append("HEADER_SIZE_V3 = 7")
418 lines.append("")
419
420 # CRC32 function
421 lines.append("def crc32(data: bytes) -> int:")
422 lines.append(' """IEEE CRC32 matching BCNP implementation."""')
423 lines.append(" crc = 0xFFFFFFFF")
424 lines.append(" for byte in data:")
425 lines.append(" crc ^= byte")
426 lines.append(" for _ in range(8):")
427 lines.append(" if crc & 1:")
428 lines.append(" crc = (crc >> 1) ^ 0xEDB88320")
429 lines.append(" else:")
430 lines.append(" crc >>= 1")
431 lines.append(" return crc ^ 0xFFFFFFFF")
432 lines.append("")
433
434 # Message type enum
435 lines.append("class MessageTypeId:")
436 lines.append(" Unknown = 0")
437 for msg in schema["messages"]:
438 lines.append(f" {msg['name']} = {msg['id']}")
439 lines.append("")
440
441 # Generate message classes
442 for msg in schema["messages"]:
443 size = compute_message_size(msg)
444 lines.append("@dataclass")
445 lines.append(f"class {msg['name']}:")
446 lines.append(f' """')
447 lines.append(f" {msg.get('description', msg['name'])}")
448 lines.append(f" Wire size: {size} bytes")
449 lines.append(f' """')
450 lines.append(f" TYPE_ID = MessageTypeId.{msg['name']}")
451 lines.append(f" WIRE_SIZE = {size}")
452 lines.append("")
453 for field in msg["fields"]:
454 py_type = "float" if field["type"] == "float32" else "int"
455 lines.append(f" {field['name']}: {py_type} = 0")
456 lines.append("")
457
458 # Encode method
459 lines.append(" def encode(self) -> bytes:")
460 lines.append(' """Encode message to wire format (big-endian)."""')
461 encode_parts = []
462 for field in msg["fields"]:
463 ftype = field["type"]
464 fname = field["name"]
465 if ftype == "float32":
466 scale = field.get("scale", 10000)
467 encode_parts.append(f"int(round(self.{fname} * {scale}))")
468 else:
469 encode_parts.append(f"self.{fname}")
470
471 # Build struct format string
472 fmt_parts = []
473 for field in msg["fields"]:
474 ftype = field["type"]
475 if ftype == "float32":
476 fmt_parts.append("i") # Encoded as int32
477 elif ftype in ("int32", "uint32"):
478 fmt_parts.append("i" if ftype == "int32" else "I")
479 elif ftype in ("int16", "uint16"):
480 fmt_parts.append("h" if ftype == "int16" else "H")
481 else:
482 fmt_parts.append("b" if ftype == "int8" else "B")
483
484 fmt_str = ">" + "".join(fmt_parts)
485 encode_args = ", ".join(encode_parts)
486 lines.append(f' return struct.pack("{fmt_str}", {encode_args})')
487 lines.append("")
488
489 # Decode method
490 lines.append(" @classmethod")
491 lines.append(f" def decode(cls, data: bytes) -> Optional['{msg['name']}']:")
492 lines.append(' """Decode message from wire format."""')
493 lines.append(f" if len(data) < cls.WIRE_SIZE:")
494 lines.append(" return None")
495 lines.append(f' values = struct.unpack("{fmt_str}", data[:cls.WIRE_SIZE])')
496
497 decode_parts = []
498 for i, field in enumerate(msg["fields"]):
499 ftype = field["type"]
500 fname = field["name"]
501 if ftype == "float32":
502 scale = field.get("scale", 10000)
503 decode_parts.append(f"{fname}=values[{i}] / {scale}")
504 else:
505 decode_parts.append(f"{fname}=values[{i}]")
506
507 decode_args = ", ".join(decode_parts)
508 lines.append(f" return cls({decode_args})")
509 lines.append("")
510
511 # Message union type (only if messages exist)
512 if schema["messages"]:
513 lines.append(f"Message = Union[{', '.join(msg['name'] for msg in schema['messages'])}]")
514 lines.append("")
515
516 # Registry
517 lines.append("MESSAGE_REGISTRY = {")
518 for msg in schema["messages"]:
519 lines.append(f" MessageTypeId.{msg['name']}: {msg['name']},")
520 lines.append("}")
521 else:
522 lines.append("# No messages defined - add message types to your schema and regenerate")
523 lines.append("MESSAGE_REGISTRY = {}")
524 lines.append("")
525
526 # Handshake functions
527 lines.append("def encode_handshake() -> bytes:")
528 lines.append(' """Create handshake packet with schema hash."""')
529 lines.append(" return HANDSHAKE_MAGIC + struct.pack('>I', SCHEMA_HASH)")
530 lines.append("")
531 lines.append("def validate_handshake(data: bytes) -> bool:")
532 lines.append(' """Validate incoming handshake matches our schema."""')
533 lines.append(" if len(data) < 8:")
534 lines.append(" return False")
535 lines.append(" if data[:4] != HANDSHAKE_MAGIC:")
536 lines.append(" return False")
537 lines.append(" remote_hash = struct.unpack('>I', data[4:8])[0]")
538 lines.append(" return remote_hash == SCHEMA_HASH")
539 lines.append("")
540
541 # Packet encoder
542 lines.append("def encode_packet(msg_type_id: int, messages: list, flags: int = 0) -> bytes:")
543 lines.append(' """Encode a complete BCNP v3 packet with header and CRC."""')
544 lines.append(" # Header: major(1) + minor(1) + flags(1) + msgTypeId(2) + msgCount(2)")
545 lines.append(" header = struct.pack('>BBBHH', PROTOCOL_MAJOR, PROTOCOL_MINOR, flags, msg_type_id, len(messages))")
546 lines.append(" payload = b''.join(m.encode() for m in messages)")
547 lines.append(" packet_data = header + payload")
548 lines.append(" checksum = struct.pack('>I', crc32(packet_data))")
549 lines.append(" return packet_data + checksum")
550 lines.append("")
551
552 output_file.write_text("\n".join(lines))
553 print(f"Generated: {output_file}")
554
555
556def main():
557 parser = argparse.ArgumentParser(description="BCNP Schema Compiler")
558 parser.add_argument("schema", type=Path, help="Path to messages.json")
559 parser.add_argument("--cpp", type=Path, help="Output directory for C++ header")
560 parser.add_argument("--python", type=Path, help="Output directory for Python bindings")
561 args = parser.parse_args()
562
563 if not args.schema.exists():
564 print(f"Error: Schema file not found: {args.schema}", file=sys.stderr)
565 sys.exit(1)
566
567 with open(args.schema) as f:
568 schema = json.load(f)
569
570 # Validate basic structure
571 if "version" not in schema or "messages" not in schema:
572 print("Error: Schema must have 'version' and 'messages' fields", file=sys.stderr)
573 sys.exit(1)
574
575 schema_hash = compute_schema_hash(schema)
576 print(f"Schema version: {schema['version']}")
577 print(f"Schema hash: 0x{schema_hash:08X}")
578 print(f"Messages: {len(schema['messages'])}")
579
580 if args.cpp:
581 generate_cpp_header(schema, args.cpp)
582
583 if args.python:
584 generate_python_bindings(schema, args.python)
585
586 if not args.cpp and not args.python:
587 print("\nNo output specified. Use --cpp and/or --python to generate code.")
588 print(f"\nCanonical JSON for hashing:\n{canonical_json(schema)}")
589
590
591if __name__ == "__main__":
592 main()
None generate_cpp_header(dict schema, Path output_dir)
int compute_message_size(dict msg)
int compute_crc32(bytes data)
str canonical_json(dict schema)
int compute_schema_hash(dict schema)
None generate_python_bindings(dict schema, Path output_dir)