""" Builds v2x2map-bridge__all.deb for Debian/Ubuntu. A .deb is an ar(1) archive containing three members: debian-binary plain text "2.0\\n" control.tar.gz DEBIAN/ metadata data.tar.gz the files to install No external tools required — pure Python (tarfile + gzip). Run from V2X2MAP/bridge: python build_deb.py Output: dist/v2x2map-bridge_0.2.1_all.deb """ import gzip import hashlib import io import os import stat import tarfile import time VERSION = "0.2.1" PACKAGE = "v2x2map-bridge" ARCH = "all" # ── ar archive ─────────────────────────────────────────────────────────────── def _ar_header(name: str, size: int) -> bytes: ts = str(int(time.time())) return ( name.encode().ljust(16)[:16] + ts.encode().ljust(12)[:12] + b"0 " # uid + b"0 " # gid + b"100644 " # mode + str(size).encode().ljust(10)[:10] + b"`\n" ) def ar_pack(members: list[tuple[str, bytes]]) -> bytes: buf = bytearray(b"!\n") for name, data in members: buf += _ar_header(name, len(data)) buf += data if len(data) % 2: buf += b"\n" # ar entries must start on even offsets return bytes(buf) # ── tar.gz builder ──────────────────────────────────────────────────────────── def make_targz(entries: list) -> bytes: """ entries is a list of: ("./path/to/dir/", None, 0o755) → directory ("./path/to/file", bytes_content, 0o644) → regular file """ raw = io.BytesIO() with gzip.GzipFile(fileobj=raw, mode="wb", mtime=0) as gz: tf = tarfile.open(fileobj=gz, mode="w:") for path, content, mode in entries: info = tarfile.TarInfo(name=path) info.mtime = 0 info.uid = 0 info.gid = 0 info.uname = "root" info.gname = "root" if content is None: info.type = tarfile.DIRTYPE info.mode = mode tf.addfile(info) else: info.mode = mode info.size = len(content) tf.addfile(info, io.BytesIO(content)) tf.close() return raw.getvalue() # ── file content helpers ─────────────────────────────────────────────────────── def _read(rel: str) -> bytes: with open(rel, "rb") as f: return f.read() def _txt(s: str) -> bytes: return s.encode("utf-8") # ── package metadata ────────────────────────────────────────────────────────── WRAPPER = _txt("""\ #!/bin/bash exec "${V2X2MAP_PYTHON:-python3}" /usr/lib/v2x2map/its_g5_bridge.py "$@" """) CONTROL = _txt(f"""\ Package: {PACKAGE} Version: {VERSION} Architecture: {ARCH} Maintainer: pit711 Installed-Size: 250 Depends: python3 (>= 3.10), python3-serial, python3-paho-mqtt Description: V2X2MAP ITS-G5 USB-to-MQTT bridge and live dashboard Captures IEEE 802.11p / ITS-G5 frames from an ESP32-C5 via USB serial and republishes them to one or more MQTT brokers. . Includes a live web dashboard (http://127.0.0.1:8080) with: - Map view with type-specific vehicle/RSU markers - Grouped frame log with filter chips - Statistics tab (type breakdown, sparkline, broker status) - MAC forensics tab (first/last seen, frame count, CSV export) - Runtime PCAP recording . Config : ~/.config/v2x2map/v2x2map.cfg (JSON, created on first run) Records : ~/v2x2map/recordings/its5_*.pcap . Quick start: v2x2map-bridge --node-id V2X2MAP:d2cf13ed6293 . Subsequent runs (reads config automatically): v2x2map-bridge """) POSTINST = _txt("""\ #!/bin/bash set -e echo "" echo " V2X2MAP ITS-G5 bridge installed." echo " For VAM decoding install requirements.txt in a venv and set V2X2MAP_PYTHON to its python." echo " Usage: v2x2map-bridge --node-id V2X2MAP:<12-hex-mac>" echo " Dashboard opens at http://127.0.0.1:8080" echo "" echo " Serial port access: add your user to the 'dialout' group:" echo " sudo usermod -aG dialout $USER (re-login required)" echo "" """) PRERM = _txt("""\ #!/bin/bash set -e """) COPYRIGHT = _txt(f"""\ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: {PACKAGE} Source: https://github.com/pit711/v2x2map Files: * Copyright: 2026 pit711 License: MIT """) # ── md5sums ─────────────────────────────────────────────────────────────────── def md5sums(file_entries: list[tuple[str, bytes]]) -> bytes: lines = [] for path, content, _ in file_entries: if content is not None: h = hashlib.md5(content).hexdigest() lines.append(f"{h} {path.lstrip('./')}") return _txt("\n".join(lines) + "\n") # ── build ───────────────────────────────────────────────────────────────────── def build(): os.makedirs("dist", exist_ok=True) bridge_py = _read("its_g5_bridge.py") server_py = _read("dashboard_server.py") dash_html = _read("dashboard.html") # Files that will be installed (used for md5sums too) data_files = [ ("./usr/lib/v2x2map/its_g5_bridge.py", bridge_py, 0o644), ("./usr/lib/v2x2map/dashboard_server.py", server_py, 0o644), ("./usr/lib/v2x2map/dashboard.html", dash_html, 0o644), ("./usr/bin/v2x2map-bridge", WRAPPER, 0o755), ("./usr/share/doc/v2x2map-bridge/copyright", COPYRIGHT, 0o644), ] for name in ("vam.py", "requirements.txt", "asn1/VAM-PDU-Descriptions.asn", "asn1/TS102894-2v241-CDD.asn", "asn1/COPYRIGHT.md"): data_files.append(("./usr/lib/v2x2map/" + name, _read(name), 0o644)) data_entries = [ ("./", None, 0o755), ("./usr/", None, 0o755), ("./usr/lib/", None, 0o755), ("./usr/lib/v2x2map/", None, 0o755), ("./usr/bin/", None, 0o755), ("./usr/share/", None, 0o755), ("./usr/share/doc/", None, 0o755), ("./usr/share/doc/v2x2map-bridge/", None, 0o755), ] + data_files ctrl_entries = [ ("./", None, 0o755), ("./control", CONTROL, 0o644), ("./md5sums", md5sums(data_files), 0o644), ("./postinst", POSTINST, 0o755), ("./prerm", PRERM, 0o755), ] control_tgz = make_targz(ctrl_entries) data_tgz = make_targz(data_entries) deb = ar_pack([ ("debian-binary", b"2.0\n"), ("control.tar.gz", control_tgz), ("data.tar.gz", data_tgz), ]) name = f"{PACKAGE}_{VERSION}_{ARCH}.deb" out = os.path.join("dist", name) with open(out, "wb") as f: f.write(deb) kb = len(deb) // 1024 print(f"Built: {out} ({kb} KB)") print(f"Install with:") print(f" sudo apt install ./{name}") print(f" -- or --") print(f" sudo dpkg -i {name}") if __name__ == "__main__": build()