Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions change_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from itertools import chain
from pathlib import Path
from typing import List, Optional
import re

import pydicom

Expand Down Expand Up @@ -69,6 +70,7 @@ def change_tags(
print(
"# warning: output matches '.dcm' and only one input. assuming you're saving to a file"
)

new_file = out_dir
else:
new_file = os.path.join(out_dir, os.path.basename(ex_dcm_file))
Expand Down Expand Up @@ -211,6 +213,7 @@ def main_make_mods():
# + gen_ids("mod2")
# + gen_acqdates()
# + gen_anon()

# )

# change_tags(
Expand Down
64 changes: 44 additions & 20 deletions mrqart.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@
import logging
import os
import re
from typing import Optional

import aionotify
from tornado.httpserver import HTTPServer
from tornado.web import Application, RequestHandler
from websockets.asyncio.server import broadcast, serve

from template_checker import TemplateChecker
from template_checker import TemplateChecker, CheckResult

Station = str
Sequence = str
Expand All @@ -34,6 +35,8 @@ def __init__(self, station: Station):
self.station = station
self.series_seqname = ""
self.count = 0
#: set using dcm_checker.check_header
self.hdr_check: Optional[CheckResult]= None

def update_isnew(self, series, seqname: Sequence) -> bool:
"""
Expand Down Expand Up @@ -97,14 +100,33 @@ class GetState(RequestHandler):
"""Return the current state as JSON"""

async def get(self):
self.write(json.dumps({k: repr(v) for k, v in STATE.items()}))
"""
GET /state returns JSON similiar to data sent over websocket
via broadcast(WS_CONNECTIONS, json.dumps(...))

data here missing 'msg' but otherwise matches. it looks like
{'station': 'AWP167046',
'content': {"conforms": true, "errors": {},
"template": {"iPAT": "p2", ...},
"input": {"iPAT":"p2", .... }}}
"""
#: 'station', 'content', and (not here) 'msg' (update|new)
#: are sent when inotify sees a new file.
#: mimic that for code reuse on javascript side
state_like_ws = {k: {'station': v.station,
'content': v.hdr_check}
for k, v in STATE.items()}
logging.debug("/state data sent: %s", state_like_ws)
self.write(json.dumps(state_like_ws, default=str))


class HttpIndex(RequestHandler):
"""Handle index page request"""

async def get(self):
"""Default is just the index page"""
# TODO: replace websocket port with one given on CLI?
# ... or provide new route that specifies websocket port
self.render("static/index.html")


Expand Down Expand Up @@ -163,14 +185,6 @@ async def monitor_dirs(watcher, dcm_checker):

event = await watcher.get_event()

# Refresh state every 60 seconds if no new event is found
if not event:
logging.info("Refreshing state...")
logging.debug("STATE before clearing: %s", STATE)
STATE.clear()
await asyncio.sleep(60) # 60 is the first attempt, we will see what works
continue

logging.debug("got event %s", event)
file = os.path.join(event.alias, event.name)

Expand All @@ -192,30 +206,40 @@ async def monitor_dirs(watcher, dcm_checker):

logging.debug("DICOM HEADER: %s", hdr)

current_ses = STATE.get(hdr["Station"])
station = hdr["Station"]
current_ses = STATE.get(station)
if not current_ses:
STATE[hdr["Station"]] = CurSeqStation(hdr["Station"])
current_ses = STATE.get(hdr["Station"])
STATE[station] = CurSeqStation(station)
current_ses = STATE.get(station)

# only send to browser if new
# TODO: what if browser started up rate
# browser will check /state (HTTP instead of WS)
# if it messes this new
if current_ses.update_isnew(hdr["SeriesNumber"], hdr["SequenceName"]):
logging.debug("first time seeing %s", current_ses)

# keep this in memory in case browser asks for it again (HTTP vs WS)
# see '/state' route and GetState
hdr_check = dcm_checker.check_header(hdr)
STATE[station].hdr_check = hdr_check


msg = {
"station": hdr["Station"],
"station": station,
"type": "new",
"content": dcm_checker.check_header(hdr),
"content": hdr_check,
}
# logging here but not update
logging.debug(msg)
broadcast(WS_CONNECTIONS, json.dumps(msg, default=list))
else:
msg = {
"station": hdr["Station"],
"type": "update",
"content": current_ses.count,
}
broadcast(WS_CONNECTIONS, json.dumps(msg, default=list))
logging.debug("already have %s", STATE[hdr["Station"]])
logging.debug("already have %s", current_ses)
# send data to browser via websocket
broadcast(WS_CONNECTIONS, json.dumps(msg, default=list))

# TODO: if epi maybe try plotting motion?
# async alignment
Expand All @@ -231,7 +255,7 @@ async def main(paths):
Run all services on different threads.
HTTP and inotify are forked. Websocket holds the main thread.
"""
dcm_checker = TemplateChecker()
dcm_checker = TemplateChecker() # TODO: this can be defined globally for the package?
watcher = aionotify.Watcher()
for path in paths:
logging.info("watching %s", path)
Expand Down
136 changes: 62 additions & 74 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@
/* Main websocket data parser. dispatches base on message type */
function receivedMessage(msg) {
data = JSON.parse(msg.data);
console.log("new message data:", data);
console.log("New message from websocket:", data);

if (data['type'] == 'new') {
add_new_series(data['content']);
}

if (data['type'] == 'update') {
console.log("Received update message:", data);

// Always fetch state when receiving an update
console.warn("Fetching state after update...");
fetchState();
console.log("ws msg is update");
let seq = document.getElementById("stations");
if(is_fresh_page()){
console.warn("websocket update w/o browser state! HTTP fetch to update browser.");
fetchState();
}
}
}

Expand All @@ -30,53 +31,23 @@
.then(response => response.json())
.then(data => {
console.log("Fetched state:", data);

if (Object.keys(data).length === 0) {
console.warn("Fetched empty state, retrying in 5 seconds...");
setTimeout(fetchState, 5000);
} else {
console.warn("Updating UI with fetched state...");
updateUIFromState(data);
}
console.warn("Updating UI with fetched state...");
updateUIFromState(data);
})
.catch(err => {
// will wait for next websockets push. hopefully no error then
console.error("Error fetching state:", err);
console.warn("Retrying state fetch in 5 seconds...");
setTimeout(fetchState, 5000);
});
}

// Update UI with the fetched state
function updateUIFromState(stateData) {
const stationList = document.getElementById("stations");

// Loop through all stations in the fetched state
for (const [station, session] of Object.entries(stateData)) {
let stationId = `station-${station}`;
let existingStation = document.getElementById(stationId);

// If the station entry doesn't exist, create it
if (!existingStation) {
existingStation = document.createElement("div");
existingStation.id = stationId;
stationList.appendChild(existingStation);
}

// Extract session details
let sessionParts = session.split(" ");
let seqNumber = sessionParts[1] || "Unknown";
let seqName = sessionParts.slice(2, -1).join(" ") || "Unknown";
let count = sessionParts[sessionParts.length - 1] || "0";

// Update the UI content (replace text instead of appending)
existingStation.innerHTML = `<strong>${station}:</strong> Seq ${seqNumber}, ${seqName} (Count: ${count})`;

// Ensure the compliance table remains visible
let complianceTable = document.querySelector(".compliance-table");
if (complianceTable) {
stationList.prepend(complianceTable);
}
}
for (const [station, msg] of Object.entries(stateData)) {
console.log(`adding ${station} data`, msg)
add_new_series(msg['content']);
}
}

// Fetch state on page load
Expand All @@ -95,34 +66,55 @@


// new dicom data into table
/* returns table html element
will be embeded in collapsable 'details' for a sequence
elements styled by main.css such that
* correct/expect ("conform" class) is small and grayed out
* errors/unexpected values ("non-conform" class) are big and red
used by 'add_new_series()'
*/
function mktable(data) {
const keys = [
"SequenceType", "Phase", "PED_major", "TR", "TE", "Matrix",
"PixelResol", "FoV", "BWP", "BWPPE", "FA", "TA",
"iPAT", "Shims"
];
let table = "<table>";
/*Object.entries(data['input']).map(
([k,v]) => `<tr><th>${k}</th><td class=` +
((k in data['errors'])?"no-coform":"conform") +
`>${v}</td><td>${data['template'][k]}</td></tr>`) +
*/
let rows = ["<th></th>", "<th>input</th>", "<th>template</th>"]
for(const k of keys){
conf_css = ((k in data['errors'])?"no-conform":"conform")
rows[0] += `<th class=${conf_css}>${k}</th>`;;
rows[1] += `<td class=${conf_css}>${data['input'][k]}</td>`;
rows[2] += `<td class=${conf_css}>${data['template'][k]}</td>`;
}
table += `<tr>${rows[0]}</tr>` +
`<tr>${rows[1]}</tr>` +
`<tr>${rows[2]}</tr>`;
table += "</table>";
return(table)
}

let table = document.querySelector(".compliance-table");

// If table doesn't exist, create it
if (!table) {
table = document.createElement("table");
table.classList.add("compliance-table");
document.getElementById("stations").prepend(table);
}

let rows = ["<th></th>", "<th>input</th>", "<th>template</th>"];
for (const k of keys) {
let conf_css = (k in data['errors']) ? "no-conform" : "conform";
rows[0] += `<th class=${conf_css}>${k}</th>`;
rows[1] += `<td class=${conf_css}>${data['input'][k]}</td>`;
rows[2] += `<td class=${conf_css}>${data['template'][k]}</td>`;
}

table.innerHTML = `<tr>${rows[0]}</tr><tr>${rows[1]}</tr><tr>${rows[2]}</tr>`;
/* have we seen any data? */
function is_fresh_page(){
let seq = document.getElementById("stations");
return(seq.innerHTML === "waiting for scanner" || seq.innerHTML === "")
}

/* what to do with type=="new" data: a dicom from a new sequence */
/* what to do with type=="new" data: a dicom from a new sequence
@param data object with keys 'input', 'template', 'errors', and 'conforms'
the 'content' part of websocket (or /state fetch) message
cf. msg['station'] and msg['type']
message built in python by 'monitor_dirs' (WS) or 'GetState' (HTTP)
'input' is dicom hdr.
'template' is the extected values
'errors' enumrate all paramters in input not matching template
'conforms' is true/false. when false, 'errors' should be {}
*/
function add_new_series(data) {
let el = document.createElement("li");
el.className = data['conforms'] ? 'conform' : 'no-conform';
Expand All @@ -143,18 +135,12 @@
let note = `<details ${details_status}>
<summary>${summary}</summary>`;

// Replace table instead of appending multiple tables
let existingTable = document.querySelector(".compliance-table");
if (existingTable) {
existingTable.remove();
}

note += "<br>" + mktable(data) + "</details>";
el.innerHTML = note;

// Clear "waiting for scanner" text
let seq = document.getElementById("stations");
if (seq.innerHTML === "waiting for scanner") {
if(is_fresh_page()) {
seq.innerHTML = "";
}

Expand All @@ -171,9 +157,9 @@
station = newStation;
}

// Replace content instead of appending multiple times
station.innerHTML = "";
station.appendChild(el);
// browser accumulates sequences it's seen
// newest is always on top
station.prepend(el);
}

// TODO: parse url to set
Expand All @@ -189,8 +175,9 @@
/* hidden text box to test sending what would come from websocket
*/
function show_debug(){
document.getElementsByClassName("debug")[0].style=""
//$("#select_station").children.map((el) => el.style.visibility="visible")
let cur = document.getElementsByClassName("debug")[0].style.display;
let toggled = cur==='block'?'none':'block';
document.getElementsByClassName("debug")[0].style.display=toggled;
}
function simdata(){
let data = document.getElementById("debug");
Expand All @@ -201,6 +188,7 @@
</script>

<body>
<div style="position:absolute; right:0em; top: 0px;"><a href="#" style=text-decoration:none onclick="show_debug()">🐛</a></div>
MR Station: <select id="select_station" onchange="select_station()"><option value="all">all</option></select>
<div id="stations">waiting for scanner</div>

Expand Down
Loading