added webUI

This commit is contained in:
ParkerTenBroeck 2026-03-05 11:54:40 -05:00
parent a266096f32
commit 0201990df8
16 changed files with 2590 additions and 341 deletions

View file

@ -11,7 +11,7 @@ pub struct SimState{
} }
static STATE: SimState = SimState{ static STATE: SimState = SimState{
switch: AtomicU32::new(0), switch: AtomicU32::new(512),
button: AtomicU32::new(0), button: AtomicU32::new(0),
led: AtomicU32::new(0), led: AtomicU32::new(0),
hex: AtomicU32::new(0), hex: AtomicU32::new(0),
@ -52,9 +52,10 @@ pub extern "C" fn ffi_get_key() -> u32 {
#[no_mangle] #[no_mangle]
pub extern "C" fn ffi_set_outputs(led: u32, hex: u32) { pub extern "C" fn ffi_set_outputs(led: u32, hex: u32) {
if STATE.led.swap(led, Ordering::Relaxed) != led{ if STATE.led.swap(led, Ordering::Relaxed) != led{
println!("LED={:#x?}", STATE.led.load(Ordering::Relaxed)) eprintln!("LED={}", STATE.led.load(Ordering::Relaxed))
} }
if STATE.hex.swap(hex, Ordering::Relaxed) != hex{ if STATE.hex.swap(hex, Ordering::Relaxed) != hex{
println!("HEX={:#x?}", STATE.hex.load(Ordering::Relaxed)) eprintln!("HEX={}", STATE.hex.load(Ordering::Relaxed))
} }
std::thread::sleep(std::time::Duration::from_millis(1));
} }

1
relay/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

928
relay/Cargo.lock generated Normal file
View file

@ -0,0 +1,928 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "axum"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
"base64",
"bytes",
"form_urlencoded",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde_core",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sha1",
"sync_wrapper",
"tokio",
"tokio-tungstenite",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "data-encoding"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-macro"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-macro",
"futures-sink",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "http"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
"http",
"http-body",
"hyper",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom",
]
[[package]]
name = "relay"
version = "0.1.0"
dependencies = [
"axum",
"futures-util",
"serde",
"serde_json",
"tokio",
"tower-http",
]
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [
"itoa",
"serde",
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [
"bytes",
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-tungstenite"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"http-range-header",
"httpdate",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
[[package]]
name = "tungstenite"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
dependencies = [
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand",
"sha1",
"thiserror",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "zerocopy"
version = "0.8.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

12
relay/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "relay"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = { version = "0.8", features = ["ws"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "io-util", "sync"] }
tower-http = { version = "0.6", features = ["fs", "trace"] }
futures-util = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

92
relay/src/build.rs Normal file
View file

@ -0,0 +1,92 @@
use std::{collections::HashMap, ops::Deref, path::{Path, PathBuf}};
use tokio::{
process::{Child, Command},
};
async fn ensure_ok(child: Child) -> Result<(), Box<dyn std::error::Error + Send + Sync>>{
let result = child.wait_with_output().await?;
if !result.status.success(){
return Err(format!("{}\n{}", String::from_utf8_lossy(&result.stdout), String::from_utf8_lossy(&result.stderr)))?
}
Ok(())
}
pub struct TempDir(PathBuf);
impl Drop for TempDir{
fn drop(&mut self) {
_ = std::fs::remove_dir_all(&self.0)
}
}
impl Deref for TempDir{
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<PathBuf> for TempDir{
fn as_ref(&self) -> &PathBuf {
&self.0
}
}
impl AsRef<Path> for TempDir{
fn as_ref(&self) -> &Path {
&self.0
}
}
pub async fn build(files: HashMap<String, String>) -> Result<TempDir, Box<dyn std::error::Error + Send + Sync>>{
use std::hash::*;
let mut hasher = std::hash::DefaultHasher::default();
for (key, value) in &files{
key.hash(&mut hasher);
value.hash(&mut hasher);
}
let hash = hasher.finish();
let mut work_dir = std::env::temp_dir();
work_dir.push(format!("ghdl-relay-{hash:x?}"));
_ = std::fs::create_dir(&work_dir);
let work_dir = TempDir(work_dir);
for (name, contents) in &files{
let mut path = work_dir.clone();
path.push(name);
std::fs::write(path, contents)?;
}
let mut cmd = Command::new("ghdl");
cmd.kill_on_drop(true);
cmd.args(["-a", "-g", "--std=08"]);
for name in files.keys(){
let mut path = work_dir.clone();
path.push(name);
cmd.arg(path);
}
cmd.arg(std::fs::canonicalize("../rtl/tb.vhdl")?);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
cmd.current_dir(&work_dir);
ensure_ok(cmd.spawn()?).await?;
let mut cmd = Command::new("ghdl");
cmd.kill_on_drop(true);
cmd.args(["-e", "--std=08"]);
cmd.arg(format!("-Wl,{}", std::fs::canonicalize("../conn/target/release/libvhdl_ui.a")?.display()));
cmd.arg("tb");
cmd.current_dir(&work_dir);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
ensure_ok(cmd.spawn()?).await?;
Ok(work_dir)
}

153
relay/src/main.rs Normal file
View file

@ -0,0 +1,153 @@
use axum::{
Router,
extract::ws::{Message, WebSocket, WebSocketUpgrade},
routing::get,
};
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, net::SocketAddr};
use tokio::{
io::{AsyncBufReadExt, BufReader},
};
use tower_http::services::ServeDir;
pub mod build;
pub mod run;
#[tokio::main]
async fn main() {
let app = Router::new()
.route(
"/ws",
get(|ws: WebSocketUpgrade| async move { ws.on_upgrade(ws_handler) }),
)
.fallback_service(ServeDir::new("ui"));
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
println!("Open UI: http://{}/", addr);
axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app)
.await
.unwrap();
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
struct ClientInput{
/// bitfield of 32 switches
switch: u32,
/// bitfield of 32 buttons
buttons: u32
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ServerMsg<'a> {
Log { stream: &'a str, line: &'a str },
/// bitfield of 32 leds
Led(u32),
/// bitfield of 4 hex displays, 7 segment with decimal
Hex(u32)
}
async fn ws_handler(socket: WebSocket) {
let (mut sender, mut receiver) = socket.split();
let files = if let Some(Ok(Message::Text(msg))) = receiver.next().await
&& let Ok(files) = serde_json::from_str::<'_, HashMap<String, String>>(&msg)
{
files
} else {
return;
};
let artifact_dir = match build::build(files).await{
Ok(dir) => dir,
Err(err) => {
_ = sender.send(Message::Text(format!("Failed to build: {err}").into())).await;
return;
},
};
let (_process, mut sin, sout, serr) = match run::run(&artifact_dir).await{
Ok(process) => process,
Err(err) => {
_ = sender.send(Message::Text(format!("Failed to run: {err}").into())).await;
return;
},
};
let mut sout = BufReader::new(sout).lines();
let mut serr = BufReader::new(serr).lines();
let artifact_prefix = artifact_dir.to_str().unwrap_or("\0\0NOPE");
let result: Result<(), Box<dyn std::error::Error + Sync + Send>> = async {
loop{
tokio::select! {
receive = receiver.next() => {
match receive{
Some(Ok(Message::Close(_))) => break,
Some(Ok(Message::Text(msg))) => {
let input = serde_json::from_str::<'_, ClientInput>(&msg)?;
use tokio::io::AsyncWriteExt;
sin.write_all(format!("key={}\n", input.buttons).as_bytes()).await?;
sin.write_all(format!("sw={}\n", input.switch).as_bytes()).await?;
},
Some(Ok(_)) => {},
Some(Err(err)) => Err(err)?,
_ => break,
}
}
out = sout.next_line() => {
match out{
Ok(Some(line)) => {
let msg = ServerMsg::Log {
stream: "stdout",
line: line.strip_prefix(artifact_prefix).unwrap_or(&line),
};
sender.send(Message::Text(serde_json::to_string(&msg)?.into())).await?;
},
Ok(None) => break,
Err(err) => {
Err(format!("Failed to read proccess sout: {err}"))?;
}
}
}
err = serr.next_line() => {
match err{
Ok(Some(line)) => {
let msg = if let Some(repr) = line.strip_prefix("LED="){
ServerMsg::Led(repr.parse().unwrap_or(0))
}else if let Some(repr) = line.strip_prefix("HEX="){
ServerMsg::Hex(repr.parse().unwrap_or(0))
}else{
ServerMsg::Log {
stream: "stderr",
line: line.strip_prefix(artifact_prefix).unwrap_or(&line),
}
};
sender.send(Message::Text(serde_json::to_string(&msg)?.into())).await?;
},
Ok(None) => break,
Err(err) => {
Err(format!("Failed to read proccess serr: {err}"))?
}
}
}
}
}
Ok(())
}.await;
match result{
Ok(_) => {},
Err(err) => {
_ = sender.send(Message::Text(format!("{err}").into())).await;
},
}
}

31
relay/src/run.rs Normal file
View file

@ -0,0 +1,31 @@
use std::{path::Path};
use tokio::process::{Child, ChildStdin, ChildStdout, ChildStderr, Command};
pub struct Process{
child: Child,
}
impl Drop for Process{
fn drop(&mut self) {
_ = self.child.start_kill()
}
}
pub async fn run(artifact_dir: &Path) -> Result<(Process, ChildStdin, ChildStdout, ChildStderr), Box<dyn std::error::Error + Send + Sync>>{
let mut cmd = Command::new("ghdl");
cmd.args(["-r", "--std=08", "tb", "--stop-delta=2147483647", "--unbuffered"]);
cmd.current_dir(artifact_dir);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn()?;
let stdin = child.stdin.take().ok_or("no stdin")?;
let stdout = child.stdout.take().ok_or("no stdout")?;
let stderr = child.stderr.take().ok_or("no stderr")?;
Ok((Process { child }, stdin, stdout, stderr))
}

468
relay/ui/app.js Normal file
View file

@ -0,0 +1,468 @@
// ----- LocalStorage keys -----
const LS_KEY_VHDL = "circuit_ui:circuit.vhdl";
// ----- State -----
let ws = null;
let isConnected = false;
let switches = 0 >>> 0; // u32
let buttons = 0 >>> 0; // u32
let autoscroll = true;
// ----- DOM -----
const wsPill = document.getElementById("wsPill");
const wsUrlText = document.getElementById("wsUrlText");
const connectToggleBtn = document.getElementById("connectToggleBtn");
const sendInputsBtn = document.getElementById("sendInputsBtn");
const vhdlEditor = document.getElementById("vhdlEditor");
const lineGutter = document.getElementById("lineGutter");
const loadExampleBtn = document.getElementById("loadExampleBtn");
const ledRow = document.getElementById("ledRow");
const ledLabels = document.getElementById("ledLabels");
const hexRow = document.getElementById("hexRow");
const switchGrid = document.getElementById("switchGrid");
const buttonGrid = document.getElementById("buttonGrid");
const logView = document.getElementById("logView");
const clearLogBtn = document.getElementById("clearLogBtn");
const autoscrollBtn = document.getElementById("autoscrollBtn");
const allSwOffBtn = document.getElementById("allSwOffBtn");
const allSwOnBtn = document.getElementById("allSwOnBtn");
// ----- Helpers -----
function wsUrl() {
const proto = (location.protocol === "https:") ? "wss" : "ws";
return `${proto}://${location.host}/ws`;
}
function setStatus(connected) {
isConnected = connected;
wsPill.textContent = connected ? "CONNECTED" : "DISCONNECTED";
wsPill.style.borderColor = connected ? "rgba(34,197,94,.6)" : "rgba(239,68,68,.6)";
wsPill.style.background = connected ? "rgba(34,197,94,.14)" : "rgba(239,68,68,.10)";
sendInputsBtn.disabled = !connected;
// single button label
connectToggleBtn.textContent = connected ? "Disconnect" : "Connect";
connectToggleBtn.classList.toggle("secondary", connected);
}
function appendLog(stream, line) {
const prefix = stream === "stderr" ? "[stderr]" : "[stdout]";
logView.textContent += `${prefix} ${line}\n`;
if (autoscroll) {
logView.scrollTop = logView.scrollHeight;
}
}
function clearLogs() {
logView.textContent = "";
}
function resetOutputsVisuals() {
// reset LED/HEX visuals to 0 immediately
setLeds(0);
setHex(0);
}
function u32BitGet(x, i) {
return ((x >>> i) & 1) === 1;
}
function u32BitSet(x, i, on) {
if (on) return (x | (1 << i)) >>> 0;
return (x & ~(1 << i)) >>> 0;
}
function sendClientInput() {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
const msg = {
type: "client_input",
switch: switches >>> 0,
buttons: buttons >>> 0,
};
ws.send(JSON.stringify(msg));
}
function parseHexDigits(hexU32) {
const d0 = (hexU32 >>> 0) & 0xFF;
const d1 = (hexU32 >>> 8) & 0xFF;
const d2 = (hexU32 >>> 16) & 0xFF;
const d3 = (hexU32 >>> 24) & 0xFF;
return [d0, d1, d2, d3];
}
// ----- Line numbers -----
function updateLineNumbers() {
const text = vhdlEditor.value || "";
// count lines: number of '\n' + 1 (even empty text -> 1 line)
const lines = text.length ? (text.split("\n").length) : 1;
// Build as one string for performance
let out = "";
for (let i = 1; i <= lines; i++) out += i + "\n";
lineGutter.textContent = out;
syncGutterScroll();
}
function syncGutterScroll() {
// keep gutter aligned to editor scroll
lineGutter.scrollTop = vhdlEditor.scrollTop;
}
// ----- LocalStorage save (debounced) -----
let saveTimer = null;
function saveEditorToLocalStorageDebounced() {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
try {
localStorage.setItem(LS_KEY_VHDL, vhdlEditor.value ?? "");
} catch (e) {
// ignore storage failures (private mode, quota)
}
}, 250);
}
function loadEditorFromLocalStorage() {
try {
const saved = localStorage.getItem(LS_KEY_VHDL);
if (saved !== null) return saved;
} catch {}
return null;
}
// ----- UI Builders -----
function buildLeds() {
ledRow.innerHTML = "";
ledLabels.innerHTML = "";
for (let i = 0; i < 32; i++) {
const el = document.createElement("div");
el.className = "led";
el.title = `LED[${i}]`;
el.dataset.bit = String(i);
ledRow.appendChild(el);
const lab = document.createElement("span");
lab.textContent = String(i);
ledLabels.appendChild(lab);
}
}
function setLeds(bitsU32) {
for (const el of ledRow.children) {
const i = Number(el.dataset.bit);
el.classList.toggle("on", u32BitGet(bitsU32 >>> 0, i));
}
}
function makeSevenSeg(digitIndex) {
const wrap = document.createElement("div");
const disp = document.createElement("div");
disp.className = "sevenSeg";
disp.dataset.digit = String(digitIndex);
const segs = ["a","b","c","d","e","f","g","dp"];
for (const s of segs) {
const seg = document.createElement("div");
seg.className = `seg ${s}`;
seg.dataset.seg = s;
disp.appendChild(seg);
}
const label = document.createElement("div");
label.className = "digitLabel";
label.textContent = `HEX[${digitIndex}]`;
wrap.appendChild(disp);
wrap.appendChild(label);
return wrap;
}
function buildHex() {
hexRow.innerHTML = "";
for (let i = 0; i < 4; i++) {
hexRow.appendChild(makeSevenSeg(i));
}
}
function setHex(hexU32) {
const digits = parseHexDigits(hexU32 >>> 0);
for (let i = 0; i < 4; i++) {
const byte = digits[i] & 0xFF;
const disp = hexRow.querySelector(`.sevenSeg[data-digit="${i}"]`);
if (!disp) continue;
const map = { a:0, b:1, c:2, d:3, e:4, f:5, g:6, dp:7 };
for (const segEl of disp.querySelectorAll(".seg")) {
const name = segEl.dataset.seg;
const bit = map[name];
const on = ((byte >>> bit) & 1) === 1;
segEl.classList.toggle("on", on);
}
}
}
function buildSwitches() {
switchGrid.innerHTML = "";
for (let i = 0; i < 32; i++) {
const cell = document.createElement("div");
cell.className = "ioCell";
const label = document.createElement("label");
label.textContent = `SW[${i}]`;
const toggle = document.createElement("div");
toggle.className = "toggle";
toggle.dataset.bit = String(i);
toggle.title = `Toggle switch ${i}`;
toggle.addEventListener("click", () => {
const bit = Number(toggle.dataset.bit);
const now = !u32BitGet(switches, bit);
switches = u32BitSet(switches, bit, now);
toggle.classList.toggle("on", now);
sendClientInput();
});
cell.appendChild(toggle);
cell.appendChild(label);
switchGrid.appendChild(cell);
}
}
function syncSwitchesUI() {
for (const toggle of switchGrid.querySelectorAll(".toggle")) {
const i = Number(toggle.dataset.bit);
toggle.classList.toggle("on", u32BitGet(switches, i));
}
}
function buildButtons() {
buttonGrid.innerHTML = "";
for (let i = 0; i < 32; i++) {
const cell = document.createElement("div");
cell.className = "ioCell";
const label = document.createElement("label");
label.textContent = `KEY[${i}]`;
const btn = document.createElement("button");
btn.className = "momentary";
btn.type = "button";
btn.textContent = "press";
btn.dataset.bit = String(i);
btn.title = `Momentary button ${i}`;
const press = () => {
const bit = Number(btn.dataset.bit);
buttons = u32BitSet(buttons, bit, true);
btn.classList.add("down");
sendClientInput();
};
const release = () => {
const bit = Number(btn.dataset.bit);
buttons = u32BitSet(buttons, bit, false);
btn.classList.remove("down");
sendClientInput();
};
// Mouse
btn.addEventListener("mousedown", (e) => { e.preventDefault(); press(); });
btn.addEventListener("mouseup", (e) => { e.preventDefault(); release(); });
btn.addEventListener("mouseleave",(e) => { e.preventDefault(); release(); });
// Touch
btn.addEventListener("touchstart",(e) => { e.preventDefault(); press(); }, {passive:false});
btn.addEventListener("touchend", (e) => { e.preventDefault(); release(); }, {passive:false});
btn.addEventListener("touchcancel",(e)=> { e.preventDefault(); release(); }, {passive:false});
cell.appendChild(btn);
cell.appendChild(label);
buttonGrid.appendChild(cell);
}
}
// ----- WebSocket -----
function connect() {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
// Requirement (3): reset logs + outputs when connect is pressed
clearLogs();
resetOutputsVisuals();
const url = wsUrl();
wsUrlText.textContent = url;
ws = new WebSocket(url);
ws.addEventListener("open", () => {
setStatus(true);
appendLog("stdout", "WebSocket connected.");
// First message MUST be the file map
const files = {
"circuit.vhdl": vhdlEditor.value ?? ""
};
ws.send(JSON.stringify(files));
// Push initial input state
sendClientInput();
});
ws.addEventListener("message", (ev) => {
let parsed = null;
try {
parsed = JSON.parse(ev.data);
} catch {
appendLog("stderr", String(ev.data));
return;
}
if (parsed.log !== undefined) {
appendLog(parsed.log.stream ?? "stdout", parsed.log.line ?? "");
return;
}
if (parsed.led !== undefined) {
const v = (parsed.led ?? parsed.value ?? parsed[0] ?? parsed["0"] ?? 0) >>> 0;
setLeds(v);
return;
}
if (parsed.hex !== undefined) {
const v = (parsed.hex ?? parsed.value ?? parsed[0] ?? parsed["0"] ?? 0) >>> 0;
setHex(v);
return;
}
appendLog("stderr", `Unknown msg: ${ev.data}`);
});
ws.addEventListener("close", () => {
appendLog("stderr", "WebSocket closed.");
setStatus(false);
});
ws.addEventListener("error", () => {
appendLog("stderr", "WebSocket error.");
setStatus(false);
});
}
function disconnect() {
if (ws) ws.close();
}
function toggleConnect() {
if (isConnected) disconnect();
else connect();
}
// ----- Wire up controls -----
connectToggleBtn.addEventListener("click", toggleConnect);
sendInputsBtn.addEventListener("click", () => {
appendLog("stdout", `Manual send: sw=${switches >>> 0} key=${buttons >>> 0}`);
sendClientInput();
});
clearLogBtn.addEventListener("click", () => { clearLogs(); });
autoscrollBtn.addEventListener("click", () => {
autoscroll = !autoscroll;
autoscrollBtn.textContent = `Autoscroll: ${autoscroll ? "on" : "off"}`;
});
allSwOffBtn.addEventListener("click", () => {
switches = 0 >>> 0;
syncSwitchesUI();
sendClientInput();
});
allSwOnBtn.addEventListener("click", () => {
switches = 0xFFFF_FFFF >>> 0;
syncSwitchesUI();
sendClientInput();
});
loadExampleBtn.addEventListener("click", () => {
vhdlEditor.value =
`library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- Do not modify the following entity block
entity circuit is
port (
clk: in std_logic; -- 500 Hz, period 2 ms
key: in std_logic_vector(31 downto 0); -- active low
sw: in std_logic_vector(31 downto 0); -- active high
led: out std_logic_vector(31 downto 0) := (others => '0'); -- active high
hex: out std_logic_vector(31 downto 0) := (others => '0') -- active low
);
end circuit;
architecture description of circuit is
signal counter: unsigned(31 downto 0) := x"00000000";
begin
led <= std_logic_vector(counter);
process(clk)
begin
counter <= counter+1;
end process;
end description;`;
saveEditorToLocalStorageDebounced();
updateLineNumbers();
});
// Editor events: save + line numbers + gutter sync
vhdlEditor.addEventListener("input", () => {
saveEditorToLocalStorageDebounced();
updateLineNumbers();
});
vhdlEditor.addEventListener("scroll", () => {
syncGutterScroll();
});
// ----- Init -----
(function init() {
wsUrlText.textContent = wsUrl();
buildLeds();
buildHex();
buildSwitches();
buildButtons();
resetOutputsVisuals();
setStatus(false);
// Load from localStorage if present
const saved = loadEditorFromLocalStorage();
if (saved !== null) {
vhdlEditor.value = saved;
} else {
vhdlEditor.value =
`-- circuit.vhdl
-- Paste your circuit here. The UI will send it on Connect.`;
}
updateLineNumbers();
})();

119
relay/ui/index.html Normal file
View file

@ -0,0 +1,119 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>VHDL Circuit UI</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<header class="topbar">
<div class="brand">
<div class="dot"></div>
<div>
<div class="title">Circuit Web UI</div>
<div class="subtitle">LEDs • HEX • Switches • Buttons • circuit.vhdl</div>
</div>
</div>
<div class="controls">
<div class="status">
<span class="pill" id="wsPill">DISCONNECTED</span>
<span class="muted" id="wsUrlText"></span>
</div>
<!-- single toggle button now -->
<button id="connectToggleBtn">Connect</button>
<button id="sendInputsBtn" class="secondary" disabled>Send Inputs</button>
</div>
</header>
<main class="grid">
<!-- Left: Editor -->
<section class="card editor">
<div class="cardHeader">
<div class="cardTitle">circuit.vhdl</div>
<div class="cardActions">
<button id="loadExampleBtn" class="secondary">Load example</button>
</div>
</div>
<!-- NEW: editor with line-number gutter -->
<div class="editorWrap">
<pre id="lineGutter" class="lineGutter" aria-hidden="true"></pre>
<textarea id="vhdlEditor" spellcheck="false"></textarea>
</div>
<div class="hint">
Saved locally as you type. On <b>Connect</b>, the UI sends <code>{"circuit.vhdl": "..."}</code> as the first WebSocket message.
</div>
</section>
<!-- Right: IO -->
<section class="card io">
<div class="cardHeader">
<div class="cardTitle">Outputs</div>
</div>
<div class="outputs">
<div class="block">
<div class="blockTitle">LEDs (32)</div>
<div id="ledRow" class="ledRow"></div>
<div class="bitLabelRow" id="ledLabels"></div>
</div>
<div class="block">
<div class="blockTitle">HEX (4 digits, 7-seg + dp)</div>
<div id="hexRow" class="hexRow"></div>
<div class="hint small">
Assumes each digit is 8 bits: <code>bit0=a</code>, <code>1=b</code>, <code>2=c</code>, <code>3=d</code>, <code>4=e</code>, <code>5=f</code>, <code>6=g</code>, <code>7=dp</code>.
</div>
</div>
</div>
</section>
<!-- Bottom-left: Inputs -->
<section class="card inputs">
<div class="cardHeader">
<div class="cardTitle">Inputs</div>
<div class="cardActions">
<button id="allSwOffBtn" class="secondary">All switches off</button>
<button id="allSwOnBtn" class="secondary">All switches on</button>
</div>
</div>
<div class="twoCols">
<div class="block">
<div class="blockTitle">Switches (32, latched)</div>
<div id="switchGrid" class="ioGrid"></div>
</div>
<div class="block">
<div class="blockTitle">Buttons (32, momentary)</div>
<div id="buttonGrid" class="ioGrid"></div>
</div>
</div>
<div class="hint">
Switches toggle a bit. Buttons set the bit while pressed (mouse/touch) and clear on release.
</div>
</section>
<!-- Bottom-right: Logs -->
<section class="card logs">
<div class="cardHeader">
<div class="cardTitle">Logs</div>
<div class="cardActions">
<button id="clearLogBtn" class="secondary">Clear</button>
<button id="autoscrollBtn" class="secondary">Autoscroll: on</button>
</div>
</div>
<pre id="logView" class="logView"></pre>
</section>
</main>
<script src="app.js"></script>
</body>
</html>

396
relay/ui/styles.css Normal file
View file

@ -0,0 +1,396 @@
:root{
--bg: #0b0f17;
--card: #121a2a;
--card2:#0f1726;
--text: #e6eefc;
--muted:#9fb0d0;
--accent:#58a6ff;
--ok:#22c55e;
--bad:#ef4444;
--warn:#f59e0b;
--line:#1f2a44;
--shadow: 0 12px 30px rgba(0,0,0,.35);
--radius: 14px;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
}
*{ box-sizing:border-box; }
html,body{ height:100%; }
body{
margin:0;
background: radial-gradient(1000px 600px at 20% -10%, rgba(88,166,255,.20), transparent 60%),
radial-gradient(900px 500px at 90% 0%, rgba(34,197,94,.12), transparent 60%),
var(--bg);
color:var(--text);
font-family: var(--sans);
}
.topbar{
position: sticky;
top:0;
z-index: 10;
display:flex;
justify-content:space-between;
align-items:center;
padding:14px 16px;
border-bottom:1px solid rgba(255,255,255,.06);
background: rgba(11,15,23,.8);
backdrop-filter: blur(10px);
}
.brand{
display:flex;
gap:12px;
align-items:center;
}
.brand .dot{
width:12px;height:12px;border-radius:50%;
background: var(--accent);
box-shadow: 0 0 18px rgba(88,166,255,.7);
}
.title{ font-weight:700; letter-spacing:.2px; }
.subtitle{ font-size:12px; color:var(--muted); margin-top:2px; }
.controls{
display:flex;
gap:10px;
align-items:center;
}
.status{
display:flex;
gap:8px;
align-items:center;
margin-right:6px;
}
.pill{
font-size:12px;
padding:6px 10px;
border-radius:999px;
border:1px solid rgba(255,255,255,.12);
background: rgba(255,255,255,.04);
}
.muted{ color:var(--muted); font-size:12px; }
button{
border:1px solid rgba(255,255,255,.14);
background: rgba(88,166,255,.14);
color:var(--text);
padding:8px 12px;
border-radius:10px;
cursor:pointer;
transition: transform .05s ease, background .2s ease;
}
button:hover{ background: rgba(88,166,255,.20); }
button:active{ transform: translateY(1px); }
button.secondary{
background: rgba(255,255,255,.06);
}
button.secondary:hover{ background: rgba(255,255,255,.10); }
button:disabled{
opacity:.55;
cursor:not-allowed;
}
.grid{
display:grid;
grid-template-columns: 1.2fr 1fr;
grid-template-rows: 420px 1fr;
gap:14px;
padding:14px;
}
.card{
background: linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.03));
border:1px solid rgba(255,255,255,.08);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow:hidden;
min-height: 0;
}
.cardHeader{
display:flex;
justify-content:space-between;
align-items:center;
padding:12px 12px;
border-bottom:1px solid rgba(255,255,255,.06);
background: rgba(0,0,0,.15);
}
.cardTitle{ font-weight:700; }
.cardActions{ display:flex; gap:8px; }
.editor{
display:flex;
flex-direction:column;
}
textarea{
width:100%;
height:100%;
padding:12px;
background: rgba(0,0,0,.22);
color:var(--text);
border:0;
outline:none;
resize:none;
font-family: var(--mono);
font-size: 13px;
line-height: 1.35;
}
.hint{
padding:10px 12px;
border-top:1px solid rgba(255,255,255,.06);
color:var(--muted);
font-size:12px;
background: rgba(0,0,0,.12);
}
.hint.small{ font-size:11px; }
.io .outputs{
padding:12px;
display:flex;
flex-direction:column;
gap:14px;
}
.blockTitle{
font-size:12px;
color:var(--muted);
margin-bottom:8px;
}
.ledRow{
display:grid;
grid-template-columns: repeat(16, 1fr);
gap:6px;
}
.led{
width: 100%;
aspect-ratio: 1/1;
border-radius: 999px;
border:1px solid rgba(255,255,255,.14);
background: rgba(255,255,255,.08);
box-shadow: inset 0 0 0 2px rgba(0,0,0,.18);
}
.led.on{
background: rgba(34,197,94,.70);
border-color: rgba(34,197,94,.9);
box-shadow: 0 0 14px rgba(34,197,94,.45);
}
.bitLabelRow{
display:grid;
grid-template-columns: repeat(16, 1fr);
gap:6px;
margin-top:6px;
color: var(--muted);
font-size: 10px;
font-family: var(--mono);
opacity: .85;
}
.bitLabelRow span{
text-align:center;
}
.hexRow{
display:flex;
gap:14px;
flex-wrap:wrap;
align-items:center;
}
/* 7-seg display */
.sevenSeg{
width: 90px;
height: 140px;
position: relative;
background: rgba(0,0,0,.18);
border:1px solid rgba(255,255,255,.10);
border-radius: 14px;
padding:10px;
}
.seg{
position:absolute;
background: rgba(255,255,255,.10);
border-radius: 8px;
filter: drop-shadow(0 0 0 rgba(0,0,0,0));
}
.seg.on{
background: rgba(88,166,255,.75);
filter: drop-shadow(0 0 8px rgba(88,166,255,.35));
}
/* segment positions */
.seg.a{ top:10px; left:18px; width:54px; height:12px; }
.seg.d{ bottom:10px; left:18px; width:54px; height:12px; }
.seg.g{ top:64px; left:18px; width:54px; height:12px; }
.seg.f{ top:18px; left:10px; width:12px; height:54px; }
.seg.b{ top:18px; right:10px; width:12px; height:54px; }
.seg.e{ bottom:18px; left:10px; width:12px; height:54px; }
.seg.c{ bottom:18px; right:10px; width:12px; height:54px; }
.seg.dp{
width:12px; height:12px;
bottom:10px; right:10px;
border-radius: 999px;
}
.digitLabel{
margin-top:8px;
font-family: var(--mono);
font-size: 11px;
color: var(--muted);
text-align:center;
}
.inputs{
grid-column: 1 / 2;
display:flex;
flex-direction:column;
min-height: 0;
}
.twoCols{
padding:12px;
display:grid;
grid-template-columns: 1fr 1fr;
gap:12px;
min-height: 0;
}
.ioGrid{
display:grid;
grid-template-columns: repeat(8, 1fr);
gap:8px;
}
.ioCell{
display:flex;
flex-direction:column;
gap:6px;
align-items:center;
padding:8px 6px;
background: rgba(0,0,0,.16);
border:1px solid rgba(255,255,255,.08);
border-radius:12px;
}
.ioCell label{
font-family: var(--mono);
font-size: 11px;
color: var(--muted);
}
.toggle{
width: 38px;
height: 22px;
border-radius: 999px;
position: relative;
background: rgba(255,255,255,.10);
border:1px solid rgba(255,255,255,.14);
cursor:pointer;
}
.toggle::after{
content:"";
width: 16px;
height: 16px;
position:absolute;
top:2px;
left:2px;
border-radius: 999px;
background: rgba(255,255,255,.45);
transition: left .12s ease, background .12s ease;
}
.toggle.on{
background: rgba(34,197,94,.24);
border-color: rgba(34,197,94,.55);
}
.toggle.on::after{
left: 20px;
background: rgba(34,197,94,.75);
}
.momentary{
width: 40px;
height: 28px;
border-radius: 10px;
border:1px solid rgba(255,255,255,.14);
background: rgba(255,255,255,.08);
cursor:pointer;
}
.momentary.down{
background: rgba(245,158,11,.25);
border-color: rgba(245,158,11,.55);
box-shadow: 0 0 12px rgba(245,158,11,.18);
}
.logs{
grid-column: 2 / 3;
display:flex;
flex-direction:column;
min-height: 0;
}
.logView{
margin:0;
padding:12px;
height:100%;
overflow:auto;
background: rgba(0,0,0,.22);
font-family: var(--mono);
font-size: 12px;
line-height: 1.35;
white-space: pre-wrap;
word-break: break-word;
}
/* Responsive */
@media (max-width: 1100px){
.grid{
grid-template-columns: 1fr;
grid-template-rows: auto;
}
.inputs, .logs{
grid-column: auto;
}
.twoCols{
grid-template-columns: 1fr;
}
}
/* Editor w/ line numbers */
.editorWrap{
display:flex;
width:100%;
height:100%;
min-height: 0;
background: rgba(0,0,0,.22);
}
.lineGutter{
margin:0;
padding:12px 10px 12px 12px;
width: 54px; /* gutter width */
overflow:hidden;
user-select:none;
text-align:right;
color: rgba(159,176,208,.85);
font-family: var(--mono);
font-size: 13px;
line-height: 1.35;
border-right: 1px solid rgba(255,255,255,.06);
background: rgba(0,0,0,.10);
}
#lineGutter .ln { display:block; }
textarea#vhdlEditor{
overscroll-behavior-y: none;
flex:1;
height:100%;
padding:12px;
background: transparent; /* uses editorWrap background */
color:var(--text);
border:0;
outline:none;
resize:none;
font-family: var(--mono);
font-size: 13px;
line-height: 1.35;
overflow:auto;
}

352
rtl/circuit copy._vhdl Normal file
View file

@ -0,0 +1,352 @@
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- Do not modify the following entity block
entity circuit is
port (
clk: in std_logic;
key: in std_logic_vector(31 downto 0); -- active low
sw: in std_logic_vector(31 downto 0); -- active high
led: out std_logic_vector(31 downto 0); -- active high
hex: out std_logic_vector(31 downto 0) -- active low
);
end circuit;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity alu is
Port (
func: in unsigned(3 downto 0);
a: in unsigned(7 downto 0);
b: in unsigned(7 downto 0);
carry_in: in std_logic;
o: out unsigned(7 downto 0);
carry_out: out std_logic;
zero: out std_logic;
gt: out std_logic;
lt: out std_logic;
eq: out std_logic
);
end alu;
architecture Behavioral of alu is
signal tmp: unsigned(8 downto 0) := "000000000";
begin
with func select
tmp <= ("0"&a) + ("0"&b) when x"0",
("0"&a) + ("0"&b) + (x"00"&carry_in) when x"1",
("0"&a) - ("0"&b) when x"2",
("0"&a) - ("0"&b) - (x"00"&carry_in) when x"3",
("0"&a) and ("0"&b) when x"4",
("0"&a) or ("0"&b) when x"5",
("0"&a) xor ("0"&b) when x"6",
"0"&x"00" when others;
zero <= '1' when tmp = 0 else '0';
eq <= '1' when a = b else '0';
lt <= '1' when a < b else '0';
gt <= '1' when a > b else '0';
carry_out <= tmp(8);
o <= tmp(7 downto 0);
end Behavioral ; -- Behavioral
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity ram_8x256 is
Port (
clk : in std_logic;
we : in std_logic; -- write enable
addr : in unsigned(7 downto 0); -- 8-bit address
din : in unsigned(7 downto 0); -- data input
dout : out unsigned(7 downto 0) -- data output
);
end ram_8x256;
architecture Behavioral of ram_8x256 is
type ram_type is array (0 to 255) of unsigned(7 downto 0);
signal ram : ram_type := (others => x"AB");
begin
process(clk)
begin
if rising_edge(clk) then
if we = '1' then
ram(to_integer(unsigned(addr))) <= din;
end if;
dout <= ram(to_integer(unsigned(addr)));
end if;
end process;
end Behavioral;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity inst_ram_8x256 is
Port (
clk : in std_logic;
addr : in unsigned(7 downto 0); -- 8-bit address
dout : out unsigned(7 downto 0) -- data output
);
end inst_ram_8x256;
architecture Behavioral of inst_ram_8x256 is
type ram_type is array (0 to 255) of unsigned(7 downto 0);
signal ram : ram_type := (
0 => x"A0", -- 0 => a
2 => x"B1", -- 1 => b
3 => x"10", -- a+b => out
4 => x"FE", -- out
5 => x"AE", -- out => a
6 => x"10", -- a+b => out
7 => x"FE", -- out
8 => x"BE", -- out => b
9 => x"D0", -- jump to 3
10 => x"03",
others => (others => '0')
);
begin
process(clk)
begin
if rising_edge(clk) or falling_edge(clk) then
dout <= ram(to_integer(unsigned(addr)));
end if;
end process;
end Behavioral;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
architecture description of circuit is
signal clock: std_logic;
signal reg_pc: unsigned(7 downto 0) := "00000000";
signal reg_a: unsigned(7 downto 0) := "00000000";
signal reg_b: unsigned(7 downto 0) := "00000000";
signal reg_out: unsigned(7 downto 0) := "00000000";
signal inst_reg: unsigned(7 downto 0);
signal inst_bus: unsigned(7 downto 0);
signal data_read: unsigned(7 downto 0);
signal data_write: unsigned(7 downto 0) := "00000000";
signal data_addr: unsigned(7 downto 0) := "00000000";
signal data_write_e: std_logic := '0';
signal flag_carry: std_logic := '0';
signal flag_lt: std_logic := '0';
signal flag_gt: std_logic := '0';
signal flag_eq: std_logic := '0';
signal flag_zero: std_logic := '0';
signal alu_a, alu_b, alu_o : unsigned(7 downto 0) := "00000000";
signal alu_func : unsigned(3 downto 0) := "0000";
signal alu_carry, alu_zero, alu_gt, alu_lt, alu_eq : std_logic := '0';
signal alu_tmp: unsigned(8 downto 0) := "000000000";
function dec7seg(val: unsigned(3 downto 0)) return std_logic_vector is
begin
case val is
when "0000"=> return "1000000"; --0
when "0001"=> return "1111001"; --1
when "0010"=> return "0100100"; --2
when "0011"=> return "0110000"; --3
when "0100"=> return "0011001"; --4
when "0101"=> return "0010010"; --5
when "0110"=> return "0000010"; --6
when "0111"=> return "1111000"; --7
when "1000"=> return "0000000"; --8
when "1001"=> return "0011000"; --9
when "1010"=> return "0001000"; --A
when "1011"=> return "0000011"; --B
when "1100"=> return "1000110"; --C
when "1101"=> return "0100001"; --D
when "1110"=> return "0000110"; --E
when "1111"=> return "0001110"; --F
when others=> return "1111111"; ---
end case;
end function;
begin
-- hex(7 downto 4) <= dec7seg(reg_out(7 downto 4));
-- hex(3 downto 0) <= dec7seg(reg_out(3 downto 0));
clock <= clk when sw(9) = '1' else sw(8);
ram_inst : entity work.inst_ram_8x256
port map(
clk => clk,
addr => reg_pc,
dout => inst_bus
);
ram_data : entity work.ram_8x256
port map(
clk => clk,
we => data_write_e,
addr => data_addr,
din => data_write,
dout => data_read
);
with alu_func select
alu_tmp <= ("0"&alu_a) + ("0"&alu_b) when x"0",
("0"&alu_a) + ("0"&alu_b) + (x"00"&flag_carry) when x"1",
("0"&alu_a) - ("0"&alu_b) when x"2",
("0"&alu_a) - ("0"&alu_b) - (x"00"&flag_carry) when x"3",
("0"&alu_a) and ("0"&alu_b) when x"4",
("0"&alu_a) or ("0"&alu_b) when x"5",
("0"&alu_a) xor ("0"&alu_b) when x"6",
"0"&x"00" when others;
alu_zero <= '1' when alu_tmp = 0 else '0';
alu_eq <= '1' when alu_a = alu_b else '0';
alu_lt <= '1' when alu_a < alu_b else '0';
alu_gt <= '1' when alu_a > alu_b else '0';
alu_carry <= alu_tmp(8);
alu_o <= alu_tmp(7 downto 0);
-- alu : entity work.alu
-- port map(
-- func => alu_func,
-- a => alu_a,
-- b => alu_b,
-- carry_in => flag_carry,
-- o => alu_o,
-- carry_out => alu_carry,
-- zero => alu_zero,
-- gt => alu_gt,
-- lt => alu_lt,
-- eq => alu_eq
-- );
process(clock)
variable out_extended : unsigned(8 downto 0);
begin
if rising_edge(clock) then
inst_reg <= inst_bus;
data_write_e <= '0';
-- report "begin reg_a = " & integer'image(to_integer(unsigned(reg_a)))
-- & " reg_b = " & integer'image(to_integer(unsigned(reg_b)))
-- & " reg_out = " & integer'image(to_integer(unsigned(reg_out)))
-- & " reg_pc = " & integer'image(to_integer(unsigned(reg_pc)))
-- & " inst_bus = " & integer'image(to_integer(unsigned(inst_bus)));
-- alu operations a,b
if inst_bus(7 downto 4) = x"1" then
alu_func <= inst_bus(3 downto 0);
alu_a <= reg_a;
alu_b <= reg_b;
reg_out <= alu_o;
end if;
-- alu operations a,imm
if inst_bus(7 downto 4) = x"2" then
alu_func <= inst_bus(3 downto 0);
alu_a <= reg_a;
alu_b <= x"00";
reg_pc <= reg_pc+1;
end if;
-- alu operations imm,b
if inst_bus(7 downto 4) = x"3" then
alu_func <= inst_bus(3 downto 0);
alu_a <= x"00";
alu_b <= reg_b;
reg_pc <= reg_pc+1;
end if;
case inst_bus is
-- nop
when x"00" => null;
-- 0 => a
when x"A0" => reg_a <= x"00";
-- 1 => a
when x"A1" => reg_a <= x"01";
-- mem[reg b] => a
when x"AC" =>
data_addr <= reg_b;
-- out => a
when x"AE" => reg_a <= reg_out;
-- immediate => a
when x"AF" => reg_pc <= reg_pc+1;
-- 0 => b
when x"B0" => reg_b <= x"00";
-- 1 => b
when x"B1" => reg_b <= x"01";
-- mem[reg a] => b
when x"BC" =>
data_addr <= reg_b;
-- out => b
when x"BE" => reg_b <= reg_out;
-- immediate => b
when x"BF" => reg_pc <= reg_pc+1;
-- conditional
-- jump imm addr abs
when x"D0" => reg_pc <= reg_pc+1;
-- jump imm addr rel
when x"D1" => reg_pc <= reg_pc+1;
-- jump addr reg a
when x"DA" => reg_pc <= reg_a-1;
-- jump addr reg b
when x"DB" => reg_pc <= reg_b-1;
-- out
when x"FE" => report " out = " & integer'image(to_integer(unsigned(reg_out)));
-- halt
when x"FF" => reg_pc <= reg_pc-1;
when others => null;
end case;
end if;
if falling_edge(clock) then
case inst_reg is
when x"AC" => reg_a <= data_read;
when x"AF" => reg_a <= inst_bus;
when x"BC" => reg_b <= data_read;
when x"BF" => reg_b <= inst_bus;
when others => null;
end case;
case inst_reg is
-- jump imm addr abs
when x"D0" => reg_pc <= inst_bus;
-- jump imm addr rel
when x"D1" => reg_pc <= reg_pc+inst_bus;
when others => reg_pc <= reg_pc+1;
end case;
-- report "end reg_a = " & integer'image(to_integer(unsigned(reg_a)))
-- & " reg_b = " & integer'image(to_integer(unsigned(reg_b)))
-- & " reg_out = " & integer'image(to_integer(unsigned(reg_out)))
-- & " reg_pc = " & integer'image(to_integer(unsigned(reg_pc)))
-- & " inst_bus = " & integer'image(to_integer(unsigned(inst_bus)));
end if;
end process;
end description;

View file

@ -6,19 +6,18 @@ use ieee.numeric_std.all;
entity circuit is entity circuit is
port ( port (
clk: in std_logic; -- 500 Hz, period 2 ms clk: in std_logic; -- 500 Hz, period 2 ms
key: in std_logic_vector(3 downto 0); -- active low key: in std_logic_vector(31 downto 0); -- active low
sw: in std_logic_vector(9 downto 0); -- active high sw: in std_logic_vector(31 downto 0); -- active high
led: out std_logic_vector(9 downto 0) := (others => '0'); -- active high led: out std_logic_vector(31 downto 0) := (others => '0'); -- active high
hex0: out std_logic_vector(6 downto 0) := (others => '0'); -- active low hex: out std_logic_vector(31 downto 0) := (others => '0') -- active low
hex1: out std_logic_vector(6 downto 0) := (others => '0') -- active low
); );
end circuit; end circuit;
architecture description of circuit is architecture description of circuit is
signal counter: unsigned(9 downto 0) := "0000000000"; signal counter: unsigned(31 downto 0) := x"00000000";
begin begin
led <= std_logic_vector(counter(9 downto 0)); led <= std_logic_vector(counter);
process(clk) process(clk)
begin begin
counter <= counter+1; counter <= counter+1;

View file

@ -5,325 +5,23 @@ use ieee.numeric_std.all;
-- Do not modify the following entity block -- Do not modify the following entity block
entity circuit is entity circuit is
port ( port (
clk: in std_logic; clk: in std_logic; -- 500 Hz, period 2 ms
key: in std_logic_vector(31 downto 0); -- active low key: in std_logic_vector(31 downto 0); -- active low
sw: in std_logic_vector(31 downto 0); -- active high sw: in std_logic_vector(31 downto 0); -- active high
led: out std_logic_vector(31 downto 0); -- active high led: out std_logic_vector(31 downto 0) := (others => '0'); -- active high
hex: out std_logic_vector(31 downto 0) -- active low hex: out std_logic_vector(31 downto 0) := (others => '0') -- active low
); );
end circuit; end circuit;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity alu is
Port (
func: in unsigned(3 downto 0);
a: in unsigned(7 downto 0);
b: in unsigned(7 downto 0);
carry_in: in std_logic;
o: out unsigned(7 downto 0);
carry_out: out std_logic;
zero: out std_logic;
gt: out std_logic;
lt: out std_logic;
eq: out std_logic
);
end alu;
architecture Behavioral of alu is
signal tmp: unsigned(8 downto 0);
begin
with func select
tmp <= ("0"&a) + ("0"&b) when x"0",
("0"&a) + ("0"&b) + (x"00"&carry_in) when x"1",
("0"&a) - ("0"&b) when x"2",
("0"&a) - ("0"&b) - (x"00"&carry_in) when x"3",
("0"&a) and ("0"&b) when x"4",
("0"&a) or ("0"&b) when x"5",
("0"&a) xor ("0"&b) when x"6",
"0"&x"00" when others;
zero <= '1' when tmp = 0 else '0';
eq <= '1' when a = b else '0';
lt <= '1' when a < b else '0';
gt <= '1' when a > b else '0';
carry_out <= tmp(8);
o <= tmp(7 downto 0);
end Behavioral ; -- Behavioral
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity ram_8x256 is
Port (
clk : in std_logic;
we : in std_logic; -- write enable
addr : in unsigned(7 downto 0); -- 8-bit address
din : in unsigned(7 downto 0); -- data input
dout : out unsigned(7 downto 0) -- data output
);
end ram_8x256;
architecture Behavioral of ram_8x256 is
type ram_type is array (0 to 255) of unsigned(7 downto 0);
signal ram : ram_type := (others => x"AB");
begin
process(clk)
begin
if rising_edge(clk) then
if we = '1' then
ram(to_integer(unsigned(addr))) <= din;
end if;
dout <= ram(to_integer(unsigned(addr)));
end if;
end process;
end Behavioral;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity inst_ram_8x256 is
Port (
clk : in std_logic;
addr : in unsigned(7 downto 0); -- 8-bit address
dout : out unsigned(7 downto 0) -- data output
);
end inst_ram_8x256;
architecture Behavioral of inst_ram_8x256 is
type ram_type is array (0 to 255) of unsigned(7 downto 0);
signal ram : ram_type := (
0 => x"A0", -- 0 => a
2 => x"B1", -- 1 => b
3 => x"10", -- a+b => out
4 => x"AE", -- out => a
5 => x"10", -- a+b => out
6 => x"BE", -- out => b
7 => x"D0", -- jump to 3
8 => x"03",
others => (others => '0')
);
begin
process(clk)
begin
if rising_edge(clk) or falling_edge(clk) then
dout <= ram(to_integer(unsigned(addr)));
end if;
end process;
end Behavioral;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
architecture description of circuit is architecture description of circuit is
signal clock: std_logic; signal counter: unsigned(9 downto 0) := "0000000000";
signal reg_pc: unsigned(7 downto 0) := "00000000";
signal reg_a: unsigned(7 downto 0) := "00000000";
signal reg_b: unsigned(7 downto 0) := "00000000";
signal reg_out: unsigned(7 downto 0) := "00000000";
signal inst_reg: unsigned(7 downto 0);
signal inst_bus: unsigned(7 downto 0);
signal data_read: unsigned(7 downto 0);
signal data_write: unsigned(7 downto 0);
signal data_addr: unsigned(7 downto 0) := "00000000";
signal data_write_e: std_logic := '0';
signal flag_carry: std_logic := '0';
signal flag_lt: std_logic := '0';
signal flag_gt: std_logic := '0';
signal flag_eq: std_logic := '0';
signal flag_zero: std_logic := '0';
signal alu_a, alu_b, alu_o : unsigned(7 downto 0);
signal alu_func : unsigned(3 downto 0);
signal alu_carry, alu_zero, alu_gt, alu_lt, alu_eq : std_logic;
function dec7seg(val: unsigned(3 downto 0)) return std_logic_vector is
begin
case val is
when "0000"=> return "1000000"; --0
when "0001"=> return "1111001"; --1
when "0010"=> return "0100100"; --2
when "0011"=> return "0110000"; --3
when "0100"=> return "0011001"; --4
when "0101"=> return "0010010"; --5
when "0110"=> return "0000010"; --6
when "0111"=> return "1111000"; --7
when "1000"=> return "0000000"; --8
when "1001"=> return "0011000"; --9
when "1010"=> return "0001000"; --A
when "1011"=> return "0000011"; --B
when "1100"=> return "1000110"; --C
when "1101"=> return "0100001"; --D
when "1110"=> return "0000110"; --E
when "1111"=> return "0001110"; --F
when others=> return "1111111"; ---
end case;
end function;
begin begin
led(9 downto 0) <= std_logic_vector(counter);
-- hex(7 downto 4) <= dec7seg(reg_out(7 downto 4)); -- led(10) <= clk;
-- hex(3 downto 0) <= dec7seg(reg_out(3 downto 0)); process(sw(0))
clock <= clk when sw(9) = '1' else sw(8);
ram_inst : entity work.inst_ram_8x256
port map(
clk => clk,
addr => reg_pc,
dout => inst_bus
);
ram_data : entity work.ram_8x256
port map(
clk => clk,
we => data_write_e,
addr => data_addr,
din => data_write,
dout => data_read
);
alu : entity work.alu
port map(
func => alu_func,
a => alu_a,
b => alu_b,
carry_in => flag_carry,
o => alu_o,
carry_out => alu_carry,
zero => alu_zero,
gt => alu_gt,
lt => alu_lt,
eq => alu_eq
);
process(clock)
variable out_extended : unsigned(8 downto 0);
begin begin
counter <= counter+1;
if rising_edge(clock) then report "meow";
inst_reg <= inst_bus;
data_write_e <= '0';
report "begin reg_a = " & integer'image(to_integer(unsigned(reg_a)))
& " reg_b = " & integer'image(to_integer(unsigned(reg_b)))
& " reg_out = " & integer'image(to_integer(unsigned(reg_out)))
& " reg_pc = " & integer'image(to_integer(unsigned(reg_pc)))
& " inst_bus = " & integer'image(to_integer(unsigned(inst_bus)));
-- alu operations a,b
if inst_reg(7 downto 4) = x"1" then
alu_func <= inst_reg(3 downto 0);
alu_a <= reg_a;
alu_b <= reg_b;
reg_out <= alu_o;
end if;
-- alu operations a,imm
if inst_reg(7 downto 4) = x"2" then
alu_func <= inst_reg(3 downto 0);
alu_a <= reg_a;
reg_pc <= reg_pc+1;
end if;
-- alu operations imm,b
if inst_reg(7 downto 4) = x"3" then
alu_func <= inst_reg(3 downto 0);
alu_b <= reg_b;
reg_pc <= reg_pc+1;
end if;
case inst_bus is
-- nop
when x"00" => null;
-- 0 => a
when x"A0" => reg_a <= x"00";
-- 1 => a
when x"A1" => reg_a <= x"01";
-- mem[reg b] => a
when x"AC" =>
data_addr <= reg_b;
-- out => a
when x"AE" => reg_a <= reg_out;
-- immediate => a
when x"AF" => reg_pc <= reg_pc+1;
-- 0 => b
when x"B0" => reg_b <= x"00";
-- 1 => b
when x"B1" => reg_b <= x"01";
-- mem[reg a] => b
when x"BC" =>
data_addr <= reg_b;
-- out => b
when x"BE" => reg_b <= reg_out;
-- immediate => b
when x"BF" => reg_pc <= reg_pc+1;
-- conditional
-- jump imm addr abs
when x"D0" => reg_pc <= reg_pc+1;
-- jump imm addr rel
when x"D1" => reg_pc <= reg_pc+1;
-- jump addr reg a
when x"DA" => reg_pc <= reg_a-1;
-- jump addr reg b
when x"DB" => reg_pc <= reg_b-1;
-- halt
when x"FF" => reg_pc <= reg_pc-1;
when others => null;
end case;
end if;
if falling_edge(clock) then
case inst_reg is
when x"AC" => reg_a <= data_read;
when x"AF" => reg_a <= inst_bus;
when x"BC" => reg_b <= data_read;
when x"BF" => reg_b <= inst_bus;
when others => null;
end case;
case inst_reg is
-- jump imm addr abs
when x"D0" => reg_pc <= inst_bus;
-- jump imm addr rel
when x"D1" => reg_pc <= reg_pc+inst_bus;
when others => reg_pc <= reg_pc+1;
end case;
report "end reg_a = " & integer'image(to_integer(unsigned(reg_a)))
& " reg_b = " & integer'image(to_integer(unsigned(reg_b)))
& " reg_out = " & integer'image(to_integer(unsigned(reg_out)))
& " reg_pc = " & integer'image(to_integer(unsigned(reg_pc)))
& " inst_bus = " & integer'image(to_integer(unsigned(inst_bus)));
end if;
end process; end process;
end description; end description;

View file

@ -71,18 +71,18 @@ begin
variable sw_i : integer; variable sw_i : integer;
variable key_i : integer; variable key_i : integer;
begin begin
ffi_init; -- starts Rust listener thread ffi_init;
wait for 0 ns; wait for 0 ns;
while true loop while true loop
wait until rising_edge(clk) or falling_edge(clk); wait until rising_edge(clk) or falling_edge(clk);
wait for 0 ns;
sw_i := ffi_get_sw; sw_i := ffi_get_sw;
key_i := ffi_get_key; key_i := ffi_get_key;
sw <= std_logic_vector(to_unsigned(sw_i, 32)); sw <= std_logic_vector(to_signed(sw_i, 32));
key <= std_logic_vector(to_unsigned(key_i, 32)); key <= std_logic_vector(to_signed(key_i, 32));
ffi_set_outputs( ffi_set_outputs(
to_integer(unsigned(clean_slv(led))), to_integer(unsigned(clean_slv(led))),

20
run.sh
View file

@ -3,7 +3,6 @@ set -euo pipefail
mkdir -p build mkdir -p build
# Build Rust shared library
pushd conn >/dev/null pushd conn >/dev/null
cargo build --release cargo build --release
popd >/dev/null popd >/dev/null
@ -14,21 +13,10 @@ LIBSRC="../conn/target/release/libvhdl_ui.a"
LIBDIR="../conn/target/release" LIBDIR="../conn/target/release"
ghdl -a --std=08 ../rtl/*.vhdl ghdl -a -g --std=08 ../rtl/*.vhdl
ghdl -e --std=08 \ ghdl -e --std=08 \
-Wl,"$LIBSRC" \ -Wl,$LIBSRC \
-Wl,-Wl,-rpath -Wl,-Wl,$LIBDIR \ tb
tb
ghdl -r --std=08 tb --stop-delta=2147483647
echo "=== Running sim ==="
echo "Connect and stream inputs using:"
echo " nc 127.0.0.1 5555"
echo "Then type lines like:"
echo " sw=1"
echo " key=15"
echo " key=7 (press KEY3 if bit3 becomes 0, etc; active-low)"
echo
ghdl -r --std=08 tb

11
run_relay.sh Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
pushd conn >/dev/null
cargo build --release
popd >/dev/null
pushd relay >/dev/null
cargo run --release
popd >/dev/null