diff --git a/README.md b/README.md
index 96b56da1..b3ddafaf 100644
--- a/README.md
+++ b/README.md
@@ -111,11 +111,11 @@ Build host tools only:
docker run --rm -v $(pwd):/workspace rtlplayground-dev make -C tools
```
-Run the web-interface simulator locally:
+Run the web-interface simulator through docker:
```
-docker run --rm -p 8080:8080 -v $(pwd):/workspace rtlplayground-dev \
- tools/output/httpd_sim /workspace/html
+docker run --rm -p 8081:8081 -v $(pwd):/workspace rtlplayground-dev \
+ tools/output/httpd_sim.py /workspace/html
```
Edit `config.txt` on your host, then re-run `make` — the
@@ -377,7 +377,31 @@ session xxxx = Web session timeout in seconds (default 3600)
Enjoy playing!
-## (11) Other documents
+## (13) HTTP Simulator for front-end development
+
+The simulator is available in tools as `./httpd_sim.py`
+By default it will serve the content in the current working directory so the easiest way to run this is to:
+```
+cd ./html
+../tools/httpd_sim.py
+```
+And the simulator will be available on http://127.0.0.1:8081
+
+There are commandline options available:
+```
+usage: httpd_sim.py [-h] [--machine MACHINE] [--port PORT] [--bindaddr BINDADDR] [--verbose]
+
+options:
+ -h, --help show this help message and exit
+ --machine MACHINE path to machine.c or .h file to parse (optional)
+ --port PORT port to bind to (default 8081)
+ --bindaddr BINDADDR address to bind (default 127.0.0.1)
+ --verbose enable verbose output
+```
+
+The machine parsing is somewhat experimental.
+
+## (12) Other documents
The following documents give further documentation on specific features of the RTL837x SoCs:
- [Adding a new device](doc/adding-a-device.md)
diff --git a/tools/httpd_sim.py b/tools/httpd_sim.py
new file mode 100755
index 00000000..dc6816d1
--- /dev/null
+++ b/tools/httpd_sim.py
@@ -0,0 +1,728 @@
+#!/usr/bin/env python3
+"""Serve files from the current directory and provide the example device API."""
+
+import os
+import secrets
+import time
+import re
+import argparse
+from email.parser import BytesParser
+from email.policy import default
+from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
+from pathlib import Path
+from urllib.parse import parse_qs, unquote, urlsplit
+
+HOST = "0.0.0.0"
+PORT = 8081
+PASSWORD = "1234"
+VERBOSE = False
+PORTS = 6
+N_COUNTERS = 52
+VLAN_MAX_ID = 4094
+L2_MAX_TRANSFER = 5
+
+ROOT = Path.cwd().resolve()
+
+machine_config = {
+ "phys_to_log_port": [ 4, 5, 6, 7, 3, 8 ],
+ "is_sfp": [0, 0, 0, 0, 0, 0, 1]
+}
+l2_entries = [
+ { "mac": "00:01:2f:00:00:01", "vlan":"001", "type":'l', "port":7, "idx":0x10 },
+ { "mac": "00:06:78:00:00:02", "vlan":"001", "type":'l', "port":7, "idx":0x20 },
+ { "mac": "00:01:2e:00:00:03", "vlan":"001", "type":'s', "port":7, "idx":0x30 },
+ { "mac": "60:7d:09:00:00:04", "vlan":"001", "type":'l', "port":7, "idx":0x40 },
+ { "mac": "1c:2a:a3:00:00:05", "vlan":"001", "type":'l', "port":7, "idx":0x50 },
+ { "mac": "1c:2a:a3:00:00:06", "vlan":"001", "type":'s', "port":7, "idx":0x60 },
+ { "mac": "02:e0:4c:00:00:07", "vlan":"001", "type":'l', "port":7, "idx":0x70 },
+ { "mac": "00:e0:4c:00:00:08", "vlan":"001", "type":'l', "port":4, "idx":0x80 },
+ { "mac": "1c:2a:a3:00:00:09", "vlan":"001", "type":'l', "port":9, "idx":0x90 }, # CPU-Port
+ { "mac": "3c:8c:f8:00:00:0a", "vlan":"001", "type":'l', "port":7, "idx":0xa0 },
+ { "mac": "00:01:2e:00:00:0b", "vlan":"001", "type":'l', "port":7, "idx":0xb0 },
+ { "mac": "00:01:2f:00:01:01", "vlan":"001", "type":'l', "port":7, "idx":0xc0 }
+]
+
+state = {
+ "lldp_enabled": False,
+ "vlans": {1: {"active": True, "name": "", "untagged":[1,2,3,4,5,6,7,8], "tagged":[], "pvid":[1,2,3,4,5,6,7,8]}},
+ "cmd_history": [],
+ "mgmt_vlan": 1,
+ "lags": [
+ {"cfg":"0000","aggValid":0,"agg":"000000000000","members":"0000"},
+ {"cfg":"0000","aggValid":0,"agg":"000000000000","members":"0000"},
+ {"cfg":"0000","aggValid":0,"agg":"000000000000","members":"0000"},
+ {"cfg":"0000","aggValid":0,"agg":"000000000000","members":"0000"}
+ ],
+}
+
+
+tx_g = [0] * PORTS
+rx_g = [0] * PORTS
+tx_b = [0] * PORTS
+rx_b = [0] * PORTS
+last_called = int(time.time())
+
+sessions = set()
+uploaded_config = None
+
+def vprint(string):
+ if VERBOSE:
+ print(string)
+
+class Handler(SimpleHTTPRequestHandler):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, directory=str(ROOT), **kwargs)
+
+ def api_handlers(self,query):
+ return {
+ "/bandwidth.json": self._api_bandwidth,
+ "/cmd_log": self._api_cmd_log,
+ "/counters.json": self._api_counters,
+ "/debug": self._api_debug,
+ "/eee.json": self._api_eee,
+ "/information.json": self._api_information,
+ "/isolation.json": self._api_isolation,
+ "/l2.json": lambda: self._api_l2(query),
+ "/lacp.json": self._api_lacp,
+ "/lag.json": self._api_lag,
+ "/lldp.json": self._api_lldp,
+ "/mirror.json": self._api_mirror,
+ "/mtu.json": self._api_mtu,
+ "/status.json": self._api_status,
+ "/storm.json": self._api_storm,
+ "/stp.json": self._api_stp,
+ "/vlan.json": lambda: self._api_vlan(query),
+ "/vlanlist": self._api_vlanlist,
+ }
+
+ def _send(self, status, body=b"", content_type="text/plain",
+ headers=()):
+ self.send_response(status)
+ self.send_header("Content-Type", content_type)
+ self.send_header("Content-Length", str(len(body)))
+ for name, value in headers:
+ self.send_header(name, value)
+ self.end_headers()
+ if body:
+ self.wfile.write(body)
+
+ def _json(self, value, headers=()):
+ import json
+
+ body = json.dumps(value, separators=(",", ":")).encode("utf-8")
+ return self._send(
+ 200, body, "application/json", headers=headers
+ )
+
+ def _redirect(self, location, headers=()):
+ return self._send(
+ 302, headers=(("Location", location), *headers)
+ )
+
+ def _authenticated(self):
+ for item in self.headers.get("Cookie", "").split(";"):
+ name, _, value = item.strip().partition("=")
+ if name == "session" and value in sessions:
+ return True
+ return False
+
+ def _protected(self):
+ if not self._authenticated():
+ self._send(401, b"Unauthorized\n")
+ return True
+ return False
+
+ @staticmethod
+ def _int_param(query, name):
+ try:
+ return int(query.get(name, ["0"])[0], 0)
+ except ValueError:
+ return 0
+
+ def _api_response(self, path, query):
+ handler = self.api_handlers(query).get(path)
+ return handler() if handler else {}
+
+ def do_GET(self):
+ global uploaded_config
+
+ parsed = urlsplit(self.path)
+ path = unquote(parsed.path)
+ query = parse_qs(parsed.query)
+
+ if path in self.api_handlers(None).keys():
+ if self._protected():
+ return
+ result = self._api_response(path, query)
+ if isinstance(result, tuple):
+ body, content_type = result
+ return self._send(200, body, content_type)
+ return self._json(
+ result,
+ headers=(("Cache-Control", "no-cache"),)
+ if path in ("/status.json", "/counters.json")
+ else (),
+ )
+
+ if path == "/config" and uploaded_config is not None:
+ if self._protected():
+ return
+ return self._send(200, uploaded_config, "text/plain")
+
+ if not self._authenticated() and path not in ("/login.html", "/favicon.ico"):
+ return self._redirect("/login.html")
+
+ # Resolve files under ROOT only; reject path traversal.
+ relative = path.lstrip("/") or "index.html"
+ target = (ROOT / relative).resolve()
+ if target != ROOT and ROOT not in target.parents:
+ return self._send(404, b"Not found\n")
+
+ if not target.is_file():
+ return self._send(404, b"Not found\n")
+
+ try:
+ data = target.read_bytes()
+ except OSError:
+ return self._send(404, b"Not found\n")
+
+ import mimetypes
+
+ mime = mimetypes.guess_type(str(target))[0] or "application/octet-stream"
+ return self._send(
+ 200,
+ data,
+ f"{mime}",
+ headers=(("Cache-Control", "max-age=60, must-revalidate"),),
+ )
+
+ def do_POST(self):
+ global uploaded_config, state
+
+ path = urlsplit(self.path).path
+
+ if path == "/login":
+ form = self._read_urlencoded_form()
+ if form.get("pwd", [""])[0] == PASSWORD:
+ token = secrets.token_urlsafe(32)
+ sessions.add(token)
+ return self._redirect(
+ "/index.html",
+ headers=((
+ "Set-Cookie",
+ f"session={token}; Path=/; SameSite=Strict; HttpOnly",
+ ),),
+ )
+ return self._redirect("/login.html")
+
+ if self._protected():
+ return
+
+ if path == "/cmd":
+ command = self._read_raw()
+ state["cmd_history"].append(command)
+ vprint(f"Received command: '{command}'")
+ response = self.parse_cmd(command.strip());
+
+ if response is not None:
+ return self._send(200, response, "text/plain")
+
+ return self._send(
+ 200,
+ b"
Upload OK"
+ b"Command executed successully
",
+ "text/html",
+ )
+
+ if path in ("/upload", "/config"):
+ if self.headers.get_content_type() != "multipart/form-data":
+ return self._send(400, b"Expected multipart/form-data\n")
+
+ parts = self._read_multipart()
+ if parts is None:
+ return self._send(400, b"Bad multipart request\n")
+
+ file_data = next(
+ (
+ data for content_type, data in parts
+ if content_type == "application/octet-stream"
+ ),
+ None,
+ )
+ if file_data is None:
+ return self._send(
+ 400, b"No application/octet-stream part\n"
+ )
+
+ if path == "/config":
+ uploaded_config = file_data
+
+ return self._send(
+ 200,
+ b" Upload OK"
+ b"File uploaded successfully
",
+ "text/html",
+ )
+
+ return self._send(404, b"Not found\n")
+
+ def _read_raw(self):
+ length = int(self.headers.get("Content-Length", "0"))
+ raw = self.rfile.read(length).decode("utf-8", errors="replace")
+ return raw
+
+ def _read_urlencoded_form(self):
+ length = int(self.headers.get("Content-Length", "0"))
+ raw = self.rfile.read(length).decode("utf-8", errors="replace")
+ return parse_qs(raw)
+
+ def _read_multipart(self):
+ length = int(self.headers.get("Content-Length", "0"))
+ raw = self.rfile.read(length)
+ content_type = self.headers.get("Content-Type", "")
+ message = BytesParser(policy=default).parsebytes(
+ b"Content-Type: "
+ + content_type.encode("utf-8")
+ + b"\r\nMIME-Version: 1.0\r\n\r\n"
+ + raw
+ )
+
+ if not message.is_multipart():
+ return None
+
+ parts = []
+ for part in message.iter_parts():
+ parts.append((
+ part.get_content_type(),
+ part.get_payload(decode=True) or b"",
+ ))
+ return parts
+
+ def parse_cmd(self, cmdstring):
+ response = bytearray()
+ for cmd in cmdstring.split("\n"):
+ pieces = cmd.split(" ")
+ handler = self.get_parsers().get(pieces[0])
+ result = handler(pieces)
+ if(result is not None):
+ response.extend(result)
+ response.extend(b"\n")
+ return response
+
+ def get_parsers(self):
+ return {
+ "vlan": self._parse_vlan,
+ "lldp": self._parse_lldp,
+ "pvid": self._parse_pvid,
+ "dns": self._parse_dns,
+ "ntp": self._parse_ntp,
+ }
+
+ def remove_vlan(self, id):
+ global state
+ if id in state["vlans"]:
+ del state["vlans"][id]
+
+ def newVlan(self, name):
+ return {"active":True, "name":name, "untagged":[], "tagged":[], "pvid":[]}
+
+ def _parse_vlan(self, pieces):
+ global state
+ vid = int(pieces[1]);
+ startIndex=2
+ name=""
+ if len(pieces) > 2:
+ if pieces[2] == "d":
+ return self.remove_vlan(vid)
+ elif pieces[2] == "mgmt":
+ state["mgmt_vlan"] = vid
+ return
+ elif not re.match('^[0-9]+$', pieces[2]):
+ name = pieces[2]
+ startIndex=3
+
+ if not vid in state["vlans"]:
+ state["vlans"][vid] = self.newVlan(name)
+ else:
+ state["vlans"][vid]["name"] = name
+
+ state["vlans"][vid]["tagged"] = []
+ state["vlans"][vid]["untagged"] = []
+
+ for i in pieces[startIndex:]:
+ if i[-1] == "t":
+ state["vlans"][vid]["tagged"].append(machine_config["phys_to_log_port"][int(i[:-1])-1])
+ else:
+ state["vlans"][vid]["untagged"].append(machine_config["phys_to_log_port"][int(i)-1])
+
+ def _parse_pvid(self, pieces):
+ global state
+ port = int(pieces[1])
+ vid = int(pieces[2])
+ if(vid in state["vlans"]):
+ state["vlans"][vid]["pvid"].append(machine_config["phys_to_log_port"][port-1])
+ else :
+ state["vlans"][vid] = self.newVlan("")
+
+ def _parse_lldp(self, pieces):
+ global state
+ state.lldp_enabled = (pieces[0] == "on")
+
+ def _parse_ntp(self, pieces):
+ global state
+ return b"NTP off, server pool.ntp.org, address 0.0.0.0, interval 60 min, timezone +00:00, dst off\nTime not synchronised"
+
+ def _parse_dns(self,pieces):
+ global state
+ return b"DNS servers 0.0.0.0 0.0.0.0, from DHCP 0.0.0.0"
+
+ def _api_storm(self):
+ global state
+ return [
+ {"portNum":i,"en":"0000","pps":"0000","rate":"000000000000000000000000"}
+ for i in range(1, PORTS + 1)
+ ]
+
+ def _api_stp(self):
+ return {
+ "on":0,
+ "rstp":1,
+ "prio":8,
+ "hello":2,
+ "maxage":20,
+ "fwd":15,
+ "txhold":6,
+ "rootPrio":"8000",
+ "rootMac":"060f00124737",
+ "myMac":"060f00124737",
+ "cost":"00000000",
+ "weRoot":1,
+ "rootPort":0,
+ "tc":"0000",
+ "ports":[
+ {"p":i,"lag":0,"mbr":0,"st":3,"role":2,"f":5,"pc":"00000000","prio":128,"p2":0,"db":"8000060f00124737","dp":"8009","dc":"00000000"}
+ for i in range(1, PORTS + 1)
+ ]
+ }
+
+ def _api_status(self):
+ global last_called, machine_config
+ now = max(int(time.time()), last_called + 1)
+ elapsed = now - last_called
+ ports = []
+
+ for i in range(1, PORTS + 1):
+ n = i - 1
+ if i % 2:
+ rate = 2_400_000_000 if i == 1 else 950_000_000
+ tx_g[n] += rate * elapsed
+ rx_g[n] += rate * elapsed
+ tx_b[n] += rate * elapsed // 10_000_000
+ rx_b[n] += rate * elapsed // 10_000_000
+ item = {
+ "portNum": i,
+ "logPort": machine_config["phys_to_log_port"][n],
+ "name": "",
+ "isSFP": machine_config["is_sfp"][i],
+ "enabled": int(i % 4 != 0),
+ "link": (5 if i == 1 else 2) if i % 2 else 0,
+ "txG": f"0x{tx_g[n]:016x}",
+ "txB": f"0x{tx_b[n]:016x}",
+ "rxG": f"0x{rx_g[n]:016x}",
+ "rxB": f"0x{rx_b[n]:016x}",
+ }
+
+ if machine_config["is_sfp"][i]:
+ item.update({
+ "sfp_vendor": "OEM",
+ "sfp_model": "10G-SFP+",
+ "sfp_serial": "12345678",
+ "sfp_options": "0x68",
+ "sfp_temp": f"0x{0x28fb + secrets.randbelow(100):04x}",
+ "sfp_vcc": f"0x{0x7eda + secrets.randbelow(100):04x}",
+ "sfp_txbias": f"0x{0x0d24 + secrets.randbelow(100):04x}",
+ "sfp_txpower": f"0x{0x14bd + secrets.randbelow(100):04x}",
+ "sfp_rxpower": "0x0000",
+ "sfp_laser": "0x0000",
+ })
+ else:
+ item["adv"] = (
+ "100000" if i == 1
+ else "000011" if i == 2
+ else "000100"
+ )
+
+ ports.append(item)
+
+ last_called = now
+ return ports
+
+ def _api_eee(self):
+ global state, machine_config
+ return [
+ {
+ "portNum": i,
+ "isSFP": machine_config["is_sfp"][i],
+ "eee": "00000010",
+ "eee_lp": "00000100",
+ "active": int(i % 2 == 1),
+ }
+ for i in range(1, PORTS + 1)
+ ]
+
+ def _api_bandwidth(self):
+ ports = []
+ for i in range(1, PORTS + 1):
+ ingress_limited = i % 2 == 1
+ egress_limited = i % 4 != 0
+ ports.append({
+ "portNum": i,
+ "iLimited": int(ingress_limited),
+ "iFC": int(i % 4 != 0) if ingress_limited else 0,
+ "iBW": f"{0x100 if ingress_limited else 0xfffff:08x}",
+ "eLimited": int(egress_limited),
+ "eBW": f"{0x1000 if egress_limited else 0xfffff:08x}",
+ })
+ return ports
+
+ def _api_mirror(self):
+ return {
+ "mPort": 1,
+ "enabled": 1,
+ "mirror_tx": f"{0b000110:016b}",
+ "mirror_rx": f"{0b000010:016b}",
+ }
+
+ def _api_lacp(self):
+ global state
+ return {
+ "on": 0,
+ "lags": state["lags"],
+ "ports": [
+ {
+ "p":i,
+ "lag":255,
+ "a":"00",
+ "pt":"00",
+ "rs":0,
+ "rx":"0000",
+ "psys":"000000000000"
+ } for i in range(1, PORTS + 1)
+ ]
+ }
+
+ def _api_lag(self):
+ return [
+ {
+ "lagNum": i,
+ "members": f"{(0x30 << (i * 2)) & 0x1f8:016b}",
+ "hash": "0x7e",
+ }
+ for i in range(4)
+ ]
+
+ def _api_mtu(self):
+ return [
+ {
+ "portNum": i,
+ "mtu": f"0x{16383 if i % 2 else 1522:04x}",
+ }
+ for i in range(1, PORTS + 1)
+ ]
+
+ def _api_lldp(self):
+ return {
+ "on": 1,
+ "portStatus": [
+ {
+ "portNum": i,
+ "logPort": i - 1,
+ "name": "",
+ "permitted": 1,
+ }
+ for i in range(1, PORTS + 1)
+ ],
+ }
+
+ def _api_counters(self):
+ return [f"0x{0x1234 + i:016x}" for i in range(N_COUNTERS)]
+
+ def _api_vlanlist(self):
+ global state
+ vlan_list = []
+ for i in state["vlans"].keys():
+ if i in state["vlans"] and state["vlans"][i]["active"]:
+ if(i == 0):
+ continue
+ vlan_list.append({
+ "id": i,
+ "name": state["vlans"][i]["name"],
+ })
+ return {"mgmt": state["mgmt_vlan"], "vlan": vlan_list}
+
+ def _api_vlan(self,query):
+ global state
+ vlan_id = self._int_param(query, "vid")
+
+ vlan = state["vlans"][vlan_id]
+ if vlan and vlan["active"]:
+ return {
+ "name": vlan["name"],
+ "members": self.vlanMembers(vlan),
+ "pvid": self.vlanPvid(vlan),
+ }
+ return {}
+
+ def vlanMembers(self, vlan):
+ member_mask = 0
+ for port in vlan["tagged"] + vlan["untagged"]:
+ member_mask |= 1 << port
+
+ untagged_mask = 0
+ for port in vlan["untagged"]:
+ untagged_mask |= 1 << port
+
+ members = member_mask | (untagged_mask << 10)
+ return f"0x{members:08x}"
+
+ def vlanPvid(self, vlan):
+ pvid_mask = 0
+ for port in vlan["pvid"]:
+ pvid_mask |= 1 << port
+ return f"0x{pvid_mask:04x}"
+
+ def _api_l2(self,query):
+ idx = self._int_param(query, "idx")
+ entries = sorted(l2_entries, key=lambda entry: entry["idx"])
+ start = next(
+ (n for n, entry in enumerate(entries) if entry["idx"] >= idx),
+ len(entries),
+ )
+ result = []
+ for entry in entries[start:start + L2_MAX_TRANSFER]:
+ result.append({
+ "mac": entry["mac"],
+ "vlan": entry["vlan"],
+ "type": "l" if entry.get("type") == "l" else "s",
+ "port": entry["port"],
+ "idx": f"{entry['idx']:04x}",
+ })
+ return result
+
+ def _api_cmd_log(self):
+ global state
+ text = "\n".join(state["cmd_history"])
+ if state["cmd_history"]:
+ text += "\n"
+ return text.encode("utf-8"), "text/plain"
+
+ def _api_isolation(self):
+ global state
+ return [
+ {
+ "portNum": i,
+ "allow": "03ff"
+ }
+ for i in range(1, PORTS + 1)
+ ]
+
+ def _api_information(self):
+ return {
+ "ip_address":"192.168.10.247",
+ "ip_gateway":"192.168.2.22",
+ "ip_netmask":"255.255.255.0",
+ "mac_address":"1c:2a:a3:23:00:02",
+ "sw_ver":"0.1-sim",
+ "hw_ver":"SWGT024-V2.0"
+ }
+
+ def _api_debug(self):
+ global state
+ return {
+ "state": state
+ }
+
+# Parse the provided .c or .h file to attempt to make the simulator act more like that configuration
+def loadMachineConfig(filename):
+ global machine_config, PORTS, phys_to_log_port, tx_g, rx_g, tx_b, rx_b
+ c_file=filename[:-1] +'c'
+ c_content = ""
+ with open(c_file, 'r') as file:
+ c_content = file.read()
+
+ h_file=filename[:-1] + 'h'
+ h_content=""
+ with open(h_file, 'r') as file:
+ h_content = file.read()
+
+ combined_file_content = h_content+"\n"+c_content
+
+ regexForCArrayLiteral=r"\{\s*\d\s*(?:,\s*\d\s*)*\}"
+
+ patterns = [
+ r"\.(isRTL8373)\s*=\s*(\d+)\s*",
+ r"\s(MACHINE_CHIPSET)\s+(\S+)\s*",
+ r"\.(log_to_phys_port)\s*=\s*("+regexForCArrayLiteral+r")\s*",
+ r"\.(phys_to_log_port)\s*=\s*("+regexForCArrayLiteral+r")\s*",
+ r"\.(is_sfp)\s*=\s*("+regexForCArrayLiteral+r")\s*",
+ ]
+
+ machine_config = { "file": c_file, "header": h_file }
+
+ vprint("Parsing File content:\n--------\n")
+ vprint(combined_file_content)
+ vprint("-------")
+
+ for pattern in patterns:
+ matches = re.search(pattern, combined_file_content)
+ if not matches:
+ continue
+ bits = matches.groups()
+ value = str(bits[1])
+ if value[0] == "{":
+ value = list(map(int, value[1:-1].split(",")))
+ machine_config[bits[0]] = value
+
+ if machine_config["MACHINE_CHIPSET"] == "RTL8372":
+ machine_config["min_port"] = 3
+ else:
+ machine_config["min_port"] = 0
+ if machine_config["MACHINE_CHIPSET"] == "RTL8370":
+ machine_config["max_port"] = 7
+ else:
+ machine_config["max_port"] = 8
+
+ if "max_port" in machine_config and "min_port" in machine_config:
+ PORTS = int(machine_config["max_port"]) - int(machine_config["min_port"])
+
+ tx_g = [0] * PORTS
+ rx_g = [0] * PORTS
+ tx_b = [0] * PORTS
+ rx_b = [0] * PORTS
+
+ vprint("------------")
+ vprint(machine_config)
+ vprint("------------")
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument("--machine", default=None, help="path to machine.c or .h file to parse (optional)")
+ parser.add_argument("--port", default=8081, type=int, help="port to bind to (default 8081)")
+ parser.add_argument("--bindaddr", default="127.0.0.1", help="address to bind (default 127.0.0.1)")
+ parser.add_argument("--verbose", action="store_true", help="enable verbose output")
+
+ args = parser.parse_args()
+
+ if(args.verbose):
+ VERBOSE = True
+
+ if(args.machine is not None):
+ vprint("Loading machine configuration:"+args.machine)
+ loadMachineConfig(args.machine)
+
+ HOST = args.bindaddr
+ PORT = args.port
+
+ print(f"Serving {ROOT} on http://{HOST}:{PORT}")
+ ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()