diff --git a/README.md b/README.md index 5e9962d..48644fb 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,26 @@ $global.FIRMWARE=my_awesome_binary.elf include @run_firmware.resc ``` +# Board visualization +There is possibility to visualize board using python visualization plugin. +> [!IMPORTANT] +> Only Raspberry Pico based boards are currently supported +> You can add buttons or leds and they will be automatically registered +> But totally different boards are not supported yet + +To use it create python virtual env, then inside virtualenv: +``` +pip3 install -r visualization/requirements.txt +renode --console your_simulation.resc + +inside renode console: +(rasbperry_pico) startVisualization 8080 +``` +and open localhost:8080 in your web browser. + +Current visualization is ugly, but it works! + + # Multi Node simulation. Many RP2040 simulators may interwork together. I am using that possibility in full MSPC simulation. To interwork between them GPIOConnector may be used, please check existing usage (`simulation` directory): [MSPC Board Simulation](https://github.com/matgla/mspc-south-bridge/) diff --git a/boards/initialize_custom_board.resc b/boards/initialize_custom_board.resc index d8458b0..73514d7 100644 --- a/boards/initialize_custom_board.resc +++ b/boards/initialize_custom_board.resc @@ -1,8 +1,11 @@ $machine_name?="raspberry_pico" +$visualization_path?=$ORIGIN/../visualization include $ORIGIN/../cores/initialize_peripherals.resc machine LoadPlatformDescription $platform_file sysbus LoadELF $ORIGIN/../bootroms/rp2040/b2.elf +include $ORIGIN/../visualization/visualization.py +setVisualizationPath $visualization_path diff --git a/emulation/peripherals/gpio/rp2040_gpio.cs b/emulation/peripherals/gpio/rp2040_gpio.cs index 358af11..8f042f7 100644 --- a/emulation/peripherals/gpio/rp2040_gpio.cs +++ b/emulation/peripherals/gpio/rp2040_gpio.cs @@ -802,7 +802,7 @@ public bool GetPullUp(int pin) public void SetPullDown(int pin, bool state) { pullDown[pin] = state; - if (!IsPinOutput(pin) && state == true) + if (state == true) { State[pin] = false; Connections[pin].Set(false); @@ -812,7 +812,7 @@ public void SetPullDown(int pin, bool state) public void SetPullUp(int pin, bool state) { pullUp[pin] = state; - if (!IsPinOutput(pin) && state == true) + if (state == true) { State[pin] = true; Connections[pin].Set(true); @@ -1247,14 +1247,22 @@ private void EnableInterruptsForCore(int core, int startingPin, ulong value) bool levelLow = (value & (1u << (i * 4))) != 0; if (levelLow) { - this.Log(LogLevel.Noisy, "Enabling level low interrupt for pin: {0}", pin); + this.Log(LogLevel.Noisy, "Enabling level low interrupt for pin: {0}, current: {1}", pin, State[pin]); + if (!State[pin]) + { + IRQ[core].Set(true); + } } irqProc[core, pin].LevelLow = levelLow; bool levelHigh = (value & (1u << (i * 4 + 1))) != 0; if (levelHigh) { - this.Log(LogLevel.Noisy, "Enabling level high interrupt for pin: {0}", pin); + this.Log(LogLevel.Noisy, "Enabling level high interrupt for pin: {0}, current: {1}", pin, State[pin]); + if (State[pin]) + { + IRQ[core].Set(true); + } } irqProc[core, pin].LevelHigh = levelHigh; diff --git a/run_firmware.resc b/run_firmware.resc index 7687cc9..cdcfce1 100644 --- a/run_firmware.resc +++ b/run_firmware.resc @@ -1,5 +1,6 @@ $machine_name?="rasbperry_pico" $platform_file?=$ORIGIN/boards/raspberry_pico.repl +$visualization_file?=$ORIGIN/visualization/raspberry_pico include $ORIGIN/boards/initialize_custom_board.resc sysbus LoadELF $global.FIRMWARE diff --git a/visualization/__init__.py b/visualization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/visualization/assets/button.svg b/visualization/assets/button.svg new file mode 100644 index 0000000..4664511 --- /dev/null +++ b/visualization/assets/button.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/visualization/assets/led.svg b/visualization/assets/led.svg new file mode 100644 index 0000000..d269068 --- /dev/null +++ b/visualization/assets/led.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/visualization/index.html b/visualization/index.html new file mode 100644 index 0000000..76beb35 --- /dev/null +++ b/visualization/index.html @@ -0,0 +1,23 @@ + + + + + + + + +
+
+ + + +
+
+
+
+
+ + + + diff --git a/visualization/led.js b/visualization/led.js new file mode 100644 index 0000000..e69de29 diff --git a/visualization/licenses.txt b/visualization/licenses.txt new file mode 100644 index 0000000..ba41cd6 --- /dev/null +++ b/visualization/licenses.txt @@ -0,0 +1 @@ +raspberry pico - https://commons.wikimedia.org/wiki/File:Raspberry_Pi_Pico_top.jpg diff --git a/visualization/messages.js b/visualization/messages.js new file mode 100644 index 0000000..aae405c --- /dev/null +++ b/visualization/messages.js @@ -0,0 +1,164 @@ +var ws; + +i = 0; +ledColors = ["red", "green", "blue", "orange", "pink"]; + +function getNextColor() { + colorIndex = i++ % ledColors.length; + console.log("Getting: ", colorIndex); + + return ledColors[colorIndex]; +} + +function changeLedState(led, state, color = null) { + svg = led.querySelector("svg"); + if (svg) { + const circle = svg.querySelector("#On"); + if (circle && !state) { + if (circle.style != null) { + circle.style.display = "none"; + } + } + else if(circle) { + if (circle.style != null) { + circle.style.display = "block"; + } + } + if (color) + { + console.log(color, " is ") + } + if (circle && color != null) + { + console.log("Setting color: " + color) + const bulb = circle.querySelector("circle"); + if (bulb) + { + bulb.style.fill = color; + } + } + } +} + +function registerLed(name, state) { + if (name == "led") { + console.log("adding board led handler"); + return; + } + console.log("adding user led: ", name) + var ledContainer = document.createElement("div"); + ledContainer.className = "ledElement" + + var led = document.createElement("div"); + fetch("./assets/led.svg") + .then(response => response.text()) + .then(svgContent => { + led.innerHTML = svgContent + led.id = name; + led.className = "led"; + console.log("Adding led"); + changeLedState(led, state, getNextColor()); + var ledText = document.createElement("div"); + ledText.innerHTML += name; + ledContainer.appendChild(ledText) + ledContainer.appendChild(led) + document.getElementById("leds").appendChild(ledContainer); + }) + .catch(error => console.error("Error loading SVG: ", error)); +} + +function sendMessage(obj) { + ws.send(JSON.stringify(obj)); +} + +function registerButton(name) { + var buttonsContainer = document.createElement("div"); + buttonsContainer.className = "buttonElement"; + var button = document.createElement("div"); + button.className = "button"; + button.id = name; + + fetch("./assets/button.svg") + .then(response => response.text()) + .then(svgContent => { + button.innerHTML = svgContent; + var buttonText = document.createElement("div"); + buttonText.innerHTML += name; + buttonsContainer.appendChild(buttonText); + buttonsContainer.appendChild(button); + document.getElementById("buttons").appendChild(buttonsContainer); + }) + .catch(error => console.error("Error loading SVG: ", error)); + + button.addEventListener('mousedown', function () { + console.log("Button " + name + " pressed"); + let message = { + "type": "action", + "target": "button", + "name": name, + "action": "press" + }; + sendMessage(message); + }); + + button.addEventListener('mouseup', function () { + console.log("Button " + name + "released"); + let message = { + "type": "action", + "target": "button", + "name": name, + "action": "release" + }; + sendMessage(message); + }) + + document.getElementById("buttons").appendChild(button); +} + +function ledStateChange(msg) { + changeLedState(document.getElementById(msg["name"]), msg["state"]); +} + +function registerAsset(msg) { + if (msg["peripheral_type"] == "led") { + registerLed(msg["name"], msg["state"]); + } + if (msg["peripheral_type"] == "button") { + registerButton(msg["name"]); + } +} + +function processMessage(msg) { + if (msg["msg"] == "register") { + registerAsset(msg); + return; + } + if (msg["msg"] == "state_change") { + ledStateChange(msg); + return; + } + console.log("Got unhandled message: ", msg); +} + +window.addEventListener('DOMContentLoaded', (event) => { + let interactive = document.getElementsByClassName("interactive"); + ws = new WebSocket("ws://" + location.host + "/ws"); + + ws.onopen = function () { + console.log("WebSocket is open"); + } + + ws.onmessage = function (e) { + const obj = JSON.parse(e.data); + processMessage(obj); + } + + ws.onclose = function () { + console.log("WebSocket is close") + } + + ws.onerror = function (e) { + console.log("WebSocket error:") + console.log(e) + } +}) diff --git a/visualization/raspberry_pico/assets/Raspberry_Pi_Pico_top.jpg b/visualization/raspberry_pico/assets/Raspberry_Pi_Pico_top.jpg new file mode 100644 index 0000000..78a8f15 Binary files /dev/null and b/visualization/raspberry_pico/assets/Raspberry_Pi_Pico_top.jpg differ diff --git a/visualization/raspberry_pico/assets/style.css b/visualization/raspberry_pico/assets/style.css new file mode 100644 index 0000000..7f92d72 --- /dev/null +++ b/visualization/raspberry_pico/assets/style.css @@ -0,0 +1,71 @@ +#container { + position: relative; + height: 40%; + width: 100%; + text-align: center; + justify-content: center; + align-items: center; + display: flex; +} + +#breadboard { + width: 100%; + height: 100%; +} + +#board { + position: flex; + flex-direction: column; + bottom: 10px; + right: 10px; + width: 200px; + height: 300px; + +} + +.led { + width: 50px; + height: 50px; +} + +.ledElement { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.buttonElement { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +#widgets { + display: flex; + flex-direction: column; +} + +#leds { + display: flex; + justify-content: center; + margin-right: 10px; + border: 1px solid black; +} + +#buttons { + display: flex; + justify-content: center; + margin-right: 10px; + border: 1px solid black; +} + + + +.button { + width: 50px; + height: 50px; + background: grey; +} + diff --git a/visualization/requirements.txt b/visualization/requirements.txt new file mode 100644 index 0000000..62a0cd6 --- /dev/null +++ b/visualization/requirements.txt @@ -0,0 +1,3 @@ +psutil==6.1.0 +websockets==14.1 +aiohttp==3.11.8 diff --git a/visualization/visualization.py b/visualization/visualization.py new file mode 100644 index 0000000..6de32fb --- /dev/null +++ b/visualization/visualization.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- + +# +# visualization_glue.py +# +# Copyright (c) 2024 Mateusz Stadnik +# +# Distributed under the terms of the MIT License. +# +# + +import clr +clr.AddReference("Renode-peripherals") +clr.AddReference("IronPython.StdLib") + +import os +script_dir = os.path.dirname(os.path.abspath(__file__)) + +import sys +sys.path.append(script_dir) +visualizationPath = None +os.chdir(script_dir) + +from Antmicro.Renode.Peripherals.Miscellaneous import LED +from Antmicro.Renode.Peripherals.Miscellaneous import Button +from Antmicro.Renode.Core import MachineStateChangedEventArgs + +from threading import Thread +from multiprocessing import Process, Pipe +import subprocess +import json + + +close = False +receiver = None +machine = None +buttons = {} + +def led_state_change(led, state): + sendMessage({ + "msg": "state_change", + "peripheral_type": "led", + "name": machine.GetLocalName(led), + "state": state + }) + +def process_message(msg): + if msg["type"] == "action": + if msg["target"] == "button": + if msg["action"] == "press": + buttons[msg["name"]].Press() + else: + buttons[msg["name"]].Release() + +def mc_setVisualizationPath(path): + print("Visualization will be served from: " + path) + visualizationPath = path + os.chdir(path) + +def mc_stopVisualization(): + global process + global close + global receiver + print("Closing visualization") + if process is not None: + sendMessage({ + "msg": "exit" + }) + process.wait() + + close = True + print("Closing receiver thread") + if receiver is not None: + receiver.join() + + print("Visualization was closed") + process = None + receiver = None + +def machine_state_changed(machine, state): + if state.CurrentState == MachineStateChangedEventArgs.State.Disposed: + mc_stopVisualization() + +def mc_startVisualization(port): + global process + global machine + global receiver + global close + + command = ["python3", script_dir + "/visualization_server.py", "--port", str(port)] + process = subprocess.Popen(command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + print("Spawned process with PID: " + str(process.pid)) + + emulation = Antmicro.Renode.Core.EmulationManager.Instance.CurrentEmulation + machine = emulation.Machines[0] + machine.StateChanged += machine_state_changed + leds = machine.GetPeripheralsOfType[LED]() + for led in leds: + led.StateChanged += led_state_change + sendMessage({ + "msg": "register", + "peripheral_type": "led", + "name": machine.GetLocalName(led), + "state": led.State + }) + machine_buttons = machine.GetPeripheralsOfType[Button]() + global buttons + for button in machine_buttons: + sendMessage({ + "msg": "register", + "peripheral_type": "button", + "name": machine.GetLocalName(button), + "state": led.State + }) + buttons[machine.GetLocalName(button)] = button + + receiver = Thread(target = getMessage) + close = False + receiver.start() + + +def sendMessage(message): + global process + if process.poll() is None: + process.stdin.write(json.dumps(message) + "\n") + process.stdin.flush() + +def getMessage(): + global process + while not close and process.poll() is None: + data = process.stdout.readline() + try: + process_message(json.loads(data.strip())) + except: + continue + print("Process IO has died") diff --git a/visualization/visualization2.py b/visualization/visualization2.py new file mode 100644 index 0000000..9898310 --- /dev/null +++ b/visualization/visualization2.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# +# visualization.py +# +# Copyright (c) 2024 Mateusz Stadnik +# +# Distributed under the terms of the MIT License. +# + +import clr +clr.AddReference("Renode-peripherals") +clr.AddReference("IronPython.StdLib") + +import os +script_dir = os.path.dirname(os.path.abspath(__file__)) + +import sys +sys.path.append(script_dir) +visualizationPath = None +os.chdir(script_dir) + +from threading import Thread + +from visualization_server import VisualizationServer + +from Antmicro.Renode.Peripherals.Miscellaneous import LED +from Antmicro.Renode.Peripherals.Miscellaneous import Button + + +# This is just glue code to Renode environment + +def led_state_change(led, state): + global visualization + visualization.on_led_change(machine.GetLocalName(led), state) + + +def mc_setVisualizationPath(path): + print("Visualization will be served from: " + path) + global visualizationPath + visualizationPath = path + os.chdir(path) + + +def mc_startVisualization(port): + global visualizationPath + if visualizationPath is None: + print("Set visualizationPath before starting server!") + return + + global visualization + global machine + emulation = Antmicro.Renode.Core.EmulationManager.Instance.CurrentEmulation + machine = emulation.Machines[0] + leds = machine.GetPeripheralsOfType[LED]() + buttons = machine.GetPeripheralsOfType[Button]() + + visualization = VisualizationServer(port) + visualization.serve() + + for led in leds: + led.StateChanged += led_state_change + visualization.register_led(machine.GetLocalName(led)) + + for button in buttons: + visualization.register_button(machine.GetLocalName(button), button) + +def mc_stopVisualization(): + global visualization + + if visualization is None: + return + + visualization.stop() + diff --git a/visualization/visualization_server.py b/visualization/visualization_server.py new file mode 100644 index 0000000..1e61068 --- /dev/null +++ b/visualization/visualization_server.py @@ -0,0 +1,140 @@ +import sys +import time +import psutil +import os +import json +import asyncio +import websockets +import aiohttp +from aiohttp import web +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("--port", type=int, help="Server Port", required=True) +args, _ = parser.parse_known_args() + +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(script_dir) +os.chdir(script_dir) + +parent_pid = psutil.Process(os.getpid()).ppid() + +async def handle_root(request): + return web.FileResponse("index.html") + +app = web.Application() +app.router.add_get("/", handle_root) +app.router.add_static("/", path=".", name="static", show_index=True) + +clients = [] + +def run_http_server(): + runner = web.AppRunner(app) + return runner + +leds = {} +buttons = [] + +async def send_to_clients(msg): + if clients: + for ws in clients: + await ws.send_str(json.dumps(msg)) + +async def register_device(msg): + if msg["peripheral_type"] == "led": + leds[msg["name"]] = msg["state"] + await send_to_clients(msg) + if msg["peripheral_type"] == "button": + buttons.append(msg["name"]) + await send_to_clients(msg) + +async def state_change(msg): + if msg["peripheral_type"] == "led": + leds[msg["name"]] = msg["state"] + await send_to_clients(msg) + +async def process_message(message, stop_event): + if message["msg"] == "exit": + stop_event.set() + for ws in clients: + await ws.close() + + if message["msg"] == "register": + await register_device(message) + + if message["msg"] == "state_change": + await state_change(message) + +async def process_message_from_ws(msg): + sys.stdout.write(msg + "\n") + sys.stdout.flush() + +async def websocket_handler(request): + global ws + ws = web.WebSocketResponse() + await ws.prepare(request) + + clients.append(ws) + for led, value in leds.items(): + await ws.send_str(json.dumps({ + "msg": "register", + "peripheral_type": "led", + "name": led, + "state": value + })) + + for button in buttons: + await ws.send_str(json.dumps({ + "msg": "register", + "peripheral_type": "button", + "name": button + })) + + try: + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + await process_message_from_ws(msg.data) + elif msg.type == aiohttp.WSMsgType.ERROR or msg.type == aiohttp.WSMsgType.CLOSED: + clents.remove(ws) + finally: + clients.remove(ws) + return ws + + +app.add_routes([web.get('/ws', websocket_handler)]) + +async def check_parent(stop_event): + while not stop_event.is_set() and psutil.pid_exists(parent_pid): + loop = asyncio.get_event_loop() + data = await loop.run_in_executor(None, sys.stdin.readline) + try: + msg = json.loads(data.strip()) + await process_message(msg, stop_event) + except: + if msg == "quit": + stop_event.set() + for ws in clients: + await ws.close() + + stop_event.set() + + +async def start_http_server(runner, stop_event): + await runner.setup() + site = web.TCPSite(runner, "localhost", args.port) + await site.start() + await stop_event.wait() + await runner.cleanup() + +async def main(): + stop_event = asyncio.Event() + runner = run_http_server() + server_task = asyncio.create_task(start_http_server(runner, stop_event)) + parent_exists = asyncio.create_task(check_parent(stop_event)) + try: + await asyncio.gather(server_task, parent_exists) + except asyncio.CancelledError: + pass + +asyncio.run(main()) +