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())
+