mirror of
https://github.com/ParkerTenBroeck/hdl_sim.git
synced 2026-06-06 21:24:06 -04:00
better workspace support
This commit is contained in:
parent
53eb596861
commit
c6136920cb
6 changed files with 78 additions and 70 deletions
|
|
@ -6,6 +6,7 @@ use tokio::process::{Child, Command};
|
||||||
use crate::HResult;
|
use crate::HResult;
|
||||||
|
|
||||||
const EMBEDDED_VHDL_UI_LIB: &[u8] = include_bytes!(env!("EMBEDDED_VHDL_CONN_LIB_PATH"));
|
const EMBEDDED_VHDL_UI_LIB: &[u8] = include_bytes!(env!("EMBEDDED_VHDL_CONN_LIB_PATH"));
|
||||||
|
const EMBEDDED_TB_VHDL: &str = include_str!("../../rtl/tb.vhdl");
|
||||||
|
|
||||||
async fn ensure_ok(child: Child) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
async fn ensure_ok(child: Child) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let result = child.wait_with_output().await?;
|
let result = child.wait_with_output().await?;
|
||||||
|
|
@ -73,10 +74,12 @@ pub async fn copy_and_build(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn build(path: &Path, src: &Path) -> HResult<()>{
|
pub async fn build(build: &Path, src: &Path) -> HResult<()>{
|
||||||
std::fs::create_dir_all(path)?;
|
std::fs::create_dir_all(build)?;
|
||||||
let embedded_lib_path = path.join("libvhdl_conn.a");
|
let embedded_lib_path = build.join("libvhdl_conn.a");
|
||||||
|
let embedded_tb_path = build.join("tb.vhdl");
|
||||||
std::fs::write(&embedded_lib_path, EMBEDDED_VHDL_UI_LIB)?;
|
std::fs::write(&embedded_lib_path, EMBEDDED_VHDL_UI_LIB)?;
|
||||||
|
std::fs::write(&embedded_tb_path, EMBEDDED_TB_VHDL)?;
|
||||||
|
|
||||||
let mut cmd = Command::new("ghdl");
|
let mut cmd = Command::new("ghdl");
|
||||||
cmd.kill_on_drop(true);
|
cmd.kill_on_drop(true);
|
||||||
|
|
@ -84,32 +87,28 @@ pub async fn build(path: &Path, src: &Path) -> HResult<()>{
|
||||||
|
|
||||||
for file in src.read_dir().unwrap().flatten(){
|
for file in src.read_dir().unwrap().flatten(){
|
||||||
if Path::new(&file.file_name()).extension() == Some(OsStr::new("vhdl")) {
|
if Path::new(&file.file_name()).extension() == Some(OsStr::new("vhdl")) {
|
||||||
cmd.arg(file.path());
|
cmd.arg(file.path().canonicalize()?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cmd.arg(&embedded_tb_path.canonicalize()?);
|
||||||
cmd.arg(std::fs::canonicalize("../rtl/tb.vhdl")?);
|
|
||||||
|
|
||||||
cmd.stdin(std::process::Stdio::piped())
|
cmd.stdin(std::process::Stdio::piped())
|
||||||
.stdout(std::process::Stdio::piped())
|
.stdout(std::process::Stdio::piped())
|
||||||
.stderr(std::process::Stdio::piped());
|
.stderr(std::process::Stdio::piped());
|
||||||
|
|
||||||
cmd.current_dir(path);
|
cmd.current_dir(build);
|
||||||
ensure_ok(cmd.spawn()?).await?;
|
ensure_ok(cmd.spawn()?).await?;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let mut cmd = Command::new("ghdl");
|
let mut cmd = Command::new("ghdl");
|
||||||
cmd.kill_on_drop(true);
|
cmd.kill_on_drop(true);
|
||||||
cmd.args(["-m", "--std=08"]);
|
cmd.args(["-m", "--std=08"]);
|
||||||
cmd.arg(format!(
|
cmd.arg(format!(
|
||||||
"-Wl,{}",
|
"-Wl,{}",
|
||||||
embedded_lib_path.display()
|
embedded_lib_path.canonicalize()?.display()
|
||||||
));
|
));
|
||||||
cmd.arg("tb");
|
cmd.arg("tb");
|
||||||
cmd.current_dir(path);
|
cmd.current_dir(build);
|
||||||
cmd.stdin(std::process::Stdio::piped())
|
cmd.stdin(std::process::Stdio::piped())
|
||||||
.stdout(std::process::Stdio::piped())
|
.stdout(std::process::Stdio::piped())
|
||||||
.stderr(std::process::Stdio::piped());
|
.stderr(std::process::Stdio::piped());
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,13 @@ use std::{
|
||||||
|
|
||||||
pub mod build;
|
pub mod build;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
pub mod local;
|
pub mod uploaded;
|
||||||
pub mod remote;
|
pub mod workspace;
|
||||||
|
|
||||||
const UI_INDEX_HTML: &str = include_str!("../ui/index.html");
|
const UI_INDEX_HTML: &str = include_str!("../ui/index.html");
|
||||||
const UI_STYLES_CSS: &str = include_str!("../ui/styles.css");
|
const UI_STYLES_CSS: &str = include_str!("../ui/styles.css");
|
||||||
const UI_APP_JS: &str = include_str!("../ui/app.js");
|
const UI_APP_JS: &str = include_str!("../ui/app.js");
|
||||||
|
|
||||||
async fn serve_index() -> impl IntoResponse {
|
|
||||||
Html(UI_INDEX_HTML)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn serve_styles() -> impl IntoResponse {
|
async fn serve_styles() -> impl IntoResponse {
|
||||||
(
|
(
|
||||||
[(header::CONTENT_TYPE, "text/css; charset=utf-8")],
|
[(header::CONTENT_TYPE, "text/css; charset=utf-8")],
|
||||||
|
|
@ -38,6 +34,10 @@ async fn serve_app_js() -> impl IntoResponse {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn serve_index() -> impl IntoResponse {
|
||||||
|
Html(UI_INDEX_HTML)
|
||||||
|
}
|
||||||
|
|
||||||
async fn not_found() -> impl IntoResponse {
|
async fn not_found() -> impl IntoResponse {
|
||||||
(StatusCode::NOT_FOUND, "not found")
|
(StatusCode::NOT_FOUND, "not found")
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +47,7 @@ struct Config {
|
||||||
ip: IpAddr,
|
ip: IpAddr,
|
||||||
port: u16,
|
port: u16,
|
||||||
update_ms: u64,
|
update_ms: u64,
|
||||||
|
workspace_ws: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
|
@ -55,6 +56,7 @@ impl Default for Config {
|
||||||
ip: IpAddr::from([127, 0, 0, 1]),
|
ip: IpAddr::from([127, 0, 0, 1]),
|
||||||
port: 8080,
|
port: 8080,
|
||||||
update_ms: 30,
|
update_ms: 30,
|
||||||
|
workspace_ws: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,12 +86,17 @@ fn parse_config_from_args() -> Result<Config, String> {
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.map_err(|err| format!("invalid --update-ms `{value}`: {err}"))?;
|
.map_err(|err| format!("invalid --update-ms `{value}`: {err}"))?;
|
||||||
}
|
}
|
||||||
|
"--workspace" => {
|
||||||
|
cfg.workspace_ws = true;
|
||||||
|
}
|
||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
return Err("usage: relay [--ip <ip>] [--port <port>] [--update-ms <ms>]".into());
|
return Err(
|
||||||
|
"usage: relay [--ip <ip>] [--port <port>] [--update-ms <ms>] [--workspace]".into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"unknown argument `{arg}`\nusage: relay [--ip <ip>] [--port <port>] [--update-ms <ms>]"
|
"unknown argument `{arg}`\nusage: relay [--ip <ip>] [--port <port>] [--update-ms <ms>] [--workspace]"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,39 +116,44 @@ async fn main() {
|
||||||
};
|
};
|
||||||
|
|
||||||
let update_interval = Duration::from_millis(cfg.update_ms);
|
let update_interval = Duration::from_millis(cfg.update_ms);
|
||||||
let app = Router::new()
|
let mut app = Router::new()
|
||||||
.route("/", get(serve_index))
|
.route(
|
||||||
.route("/index.html", get(serve_index))
|
"/",
|
||||||
|
get(move || {
|
||||||
|
async move { serve_index().await }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/index.html",
|
||||||
|
get(move || {
|
||||||
|
async move { serve_index().await }
|
||||||
|
}),
|
||||||
|
)
|
||||||
.route("/styles.css", get(serve_styles))
|
.route("/styles.css", get(serve_styles))
|
||||||
.route("/app.js", get(serve_app_js))
|
.route("/app.js", get(serve_app_js))
|
||||||
.route(
|
.route(
|
||||||
"/ws/remote",
|
"/ws/uploaded",
|
||||||
get(move |ws: WebSocketUpgrade| {
|
get(move |ws: WebSocketUpgrade| {
|
||||||
let update_interval = update_interval;
|
let update_interval = update_interval;
|
||||||
async move {
|
async move {
|
||||||
ws.on_upgrade(move |socket| remote::ws_handler(socket, update_interval))
|
ws.on_upgrade(move |socket| uploaded::ws_handler(socket, update_interval))
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
.route(
|
|
||||||
"/ws/local",
|
if cfg.workspace_ws {
|
||||||
|
app = app.route(
|
||||||
|
"/ws/workspace",
|
||||||
get(move |ws: WebSocketUpgrade| {
|
get(move |ws: WebSocketUpgrade| {
|
||||||
let update_interval = update_interval;
|
let update_interval = update_interval;
|
||||||
async move {
|
async move {
|
||||||
ws.on_upgrade(move |socket| local::ws_handler(socket, update_interval))
|
ws.on_upgrade(move |socket| workspace::ws_handler(socket, update_interval))
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
.route(
|
}
|
||||||
"/ws",
|
|
||||||
get(move |ws: WebSocketUpgrade| {
|
app = app.fallback(get(not_found));
|
||||||
let update_interval = update_interval;
|
|
||||||
async move {
|
|
||||||
ws.on_upgrade(move |socket| remote::ws_handler(socket, update_interval))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.fallback(get(not_found));
|
|
||||||
|
|
||||||
let addr = SocketAddr::new(cfg.ip, cfg.port);
|
let addr = SocketAddr::new(cfg.ip, cfg.port);
|
||||||
println!("Open UI: http://{}/", addr);
|
println!("Open UI: http://{}/", addr);
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ struct Handler {
|
||||||
|
|
||||||
impl Handler {
|
impl Handler {
|
||||||
|
|
||||||
fn local(socket: WebSocket, build: PathBuf, src: PathBuf, refresh_time: Duration) -> Self {
|
fn workspace(socket: WebSocket, build: PathBuf, src: PathBuf, refresh_time: Duration) -> Self {
|
||||||
let (sender, receiver) = socket.split();
|
let (sender, receiver) = socket.split();
|
||||||
Self {
|
Self {
|
||||||
sender,
|
sender,
|
||||||
|
|
@ -208,5 +208,5 @@ impl Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ws_handler(socket: WebSocket, refresh_time: Duration) {
|
pub async fn ws_handler(socket: WebSocket, refresh_time: Duration) {
|
||||||
Handler::local(socket, "../target".into(), "../src".into(), refresh_time).run().await;
|
Handler::workspace(socket, "./target".into(), "./src".into(), refresh_time).run().await;
|
||||||
}
|
}
|
||||||
|
|
@ -648,7 +648,7 @@ class CircuitUiApp {
|
||||||
|
|
||||||
this.editor = new EditorController({
|
this.editor = new EditorController({
|
||||||
...this.dom,
|
...this.dom,
|
||||||
enabled: config.initialMode === "local",
|
enabled: config.initialMode === "uploaded",
|
||||||
externalFiles: config.externalFiles,
|
externalFiles: config.externalFiles,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -670,7 +670,7 @@ class CircuitUiApp {
|
||||||
this.outputs.resetVisuals();
|
this.outputs.resetVisuals();
|
||||||
},
|
},
|
||||||
onOpen: () => {
|
onOpen: () => {
|
||||||
if (this.mode === "local") {
|
if (this.mode === "uploaded") {
|
||||||
this.connection.send(this.editor.getFilesPayload());
|
this.connection.send(this.editor.getFilesPayload());
|
||||||
}
|
}
|
||||||
this.connection.send({ input: this.inputs.getInputPayload() });
|
this.connection.send({ input: this.inputs.getInputPayload() });
|
||||||
|
|
@ -705,7 +705,7 @@ class CircuitUiApp {
|
||||||
this.setRunButtonEnabled(false);
|
this.setRunButtonEnabled(false);
|
||||||
this.setRunning(false);
|
this.setRunning(false);
|
||||||
this.updateStatusIndicator();
|
this.updateStatusIndicator();
|
||||||
if (this.mode === "remote") {
|
if (this.mode === "workspace") {
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -741,13 +741,14 @@ class CircuitUiApp {
|
||||||
|
|
||||||
wireModeControls() {
|
wireModeControls() {
|
||||||
this.dom.modeToggle.addEventListener("change", () => {
|
this.dom.modeToggle.addEventListener("change", () => {
|
||||||
const nextMode = this.dom.modeToggle.checked ? "remote" : "local";
|
const nextMode = this.dom.modeToggle.checked ? "uploaded" : "workspace";
|
||||||
this.applyMode(nextMode);
|
this.applyMode(nextMode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
applyMode(nextMode, fromInit = false) {
|
applyMode(nextMode, fromInit = false) {
|
||||||
const mode = nextMode === "remote" ? "remote" : "local";
|
const mode =
|
||||||
|
nextMode === "workspace" && this.config.workspaceEnabled ? "workspace" : "uploaded";
|
||||||
const changed = this.mode !== mode;
|
const changed = this.mode !== mode;
|
||||||
|
|
||||||
if (!fromInit && changed && this.connection.isConnected()) {
|
if (!fromInit && changed && this.connection.isConnected()) {
|
||||||
|
|
@ -759,13 +760,14 @@ class CircuitUiApp {
|
||||||
localStorage.setItem(LS_KEY_MODE, mode);
|
localStorage.setItem(LS_KEY_MODE, mode);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const isRemote = mode === "remote";
|
const isUploaded = mode === "uploaded";
|
||||||
this.dom.modeToggle.checked = isRemote;
|
this.dom.modeToggle.checked = isUploaded;
|
||||||
this.editor.setEnabled(!isRemote);
|
this.dom.modeToggle.disabled = !this.config.workspaceEnabled;
|
||||||
this.dom.connectToggleBtn.classList.toggle("is-hidden", isRemote);
|
this.editor.setEnabled(isUploaded);
|
||||||
this.dom.runToggleBtn.classList.toggle("is-hidden", !isRemote);
|
this.dom.connectToggleBtn.classList.toggle("is-hidden", !isUploaded);
|
||||||
|
this.dom.runToggleBtn.classList.toggle("is-hidden", isUploaded);
|
||||||
|
|
||||||
if (isRemote) {
|
if (!isUploaded) {
|
||||||
this.scheduleReconnect(0);
|
this.scheduleReconnect(0);
|
||||||
} else {
|
} else {
|
||||||
this.cancelReconnect();
|
this.cancelReconnect();
|
||||||
|
|
@ -831,27 +833,29 @@ function resolveConfig() {
|
||||||
const config = window.VHDL_UI_CONFIG ?? {};
|
const config = window.VHDL_UI_CONFIG ?? {};
|
||||||
const query = new URLSearchParams(location.search);
|
const query = new URLSearchParams(location.search);
|
||||||
const queryMode = (query.get("mode") ?? "").toLowerCase();
|
const queryMode = (query.get("mode") ?? "").toLowerCase();
|
||||||
|
const workspaceEnabled = config.workspaceEnabled !== false;
|
||||||
|
|
||||||
let storedMode = "";
|
let storedMode = "";
|
||||||
try {
|
try {
|
||||||
storedMode = (localStorage.getItem(LS_KEY_MODE) ?? "").toLowerCase();
|
storedMode = (localStorage.getItem(LS_KEY_MODE) ?? "").toLowerCase();
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
let initialMode = "local";
|
let initialMode = workspaceEnabled ? "workspace" : "uploaded";
|
||||||
if (queryMode === "local" || queryMode === "remote") {
|
if (queryMode === "workspace" || queryMode === "uploaded") {
|
||||||
initialMode = queryMode;
|
initialMode = queryMode;
|
||||||
} else if (storedMode === "local" || storedMode === "remote") {
|
} else if (storedMode === "workspace" || storedMode === "uploaded") {
|
||||||
initialMode = storedMode;
|
initialMode = storedMode;
|
||||||
} else if (config.mode === "local" || config.mode === "remote") {
|
} else if (config.mode === "workspace" || config.mode === "uploaded") {
|
||||||
initialMode = config.mode;
|
initialMode = config.mode;
|
||||||
} else if (query.has("externalEditor")) {
|
} else if (query.has("externalEditor")) {
|
||||||
initialMode = parseBoolean(query.get("externalEditor")) ? "remote" : "local";
|
initialMode = parseBoolean(query.get("externalEditor")) ? "uploaded" : "workspace";
|
||||||
} else if (parseBoolean(config.externalEditor)) {
|
} else if (parseBoolean(config.externalEditor)) {
|
||||||
initialMode = "remote";
|
initialMode = "uploaded";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialMode,
|
initialMode: initialMode === "workspace" && !workspaceEnabled ? "uploaded" : initialMode,
|
||||||
|
workspaceEnabled,
|
||||||
externalFiles: config.externalFiles ?? null,
|
externalFiles: config.externalFiles ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,10 @@
|
||||||
<div class="panelTopStatus">
|
<div class="panelTopStatus">
|
||||||
<span class="pill state-disabled" id="statusPill">DISABLED</span>
|
<span class="pill state-disabled" id="statusPill">DISABLED</span>
|
||||||
<label class="modeToggle" for="modeToggle">
|
<label class="modeToggle" for="modeToggle">
|
||||||
<span class="modeLabel">Local</span>
|
<span class="modeLabel">Workspace</span>
|
||||||
<input id="modeToggle" type="checkbox" aria-label="Toggle remote mode" />
|
<input id="modeToggle" type="checkbox" aria-label="Toggle uploaded mode" />
|
||||||
<span class="modeSlider" aria-hidden="true"></span>
|
<span class="modeSlider" aria-hidden="true"></span>
|
||||||
<span class="modeLabel">Remote</span>
|
<span class="modeLabel">Uploaded</span>
|
||||||
</label>
|
</label>
|
||||||
<button id="connectToggleBtn">Connect</button>
|
<button id="connectToggleBtn">Connect</button>
|
||||||
<button id="runToggleBtn" class="secondary" disabled>Start</button>
|
<button id="runToggleBtn" class="secondary" disabled>Start</button>
|
||||||
|
|
@ -796,13 +796,6 @@
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
|
||||||
// Optional runtime config hook:
|
|
||||||
// window.VHDL_UI_CONFIG = {
|
|
||||||
// mode: "local" | "remote",
|
|
||||||
// externalFiles: { "circuit.vhdl": "..." }
|
|
||||||
// };
|
|
||||||
</script>
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue