This commit is contained in:
@@ -25,6 +25,7 @@ Reparaturen, Berichte und die Synchronisationswarteschlange sind über das Menü
|
||||
- automatische Hardware-ID und serverseitig vergebene Gerätenummer
|
||||
- Schutz vor doppelten Datensätzen
|
||||
- gespeichertes Werkstatt-WLAN mit geschütztem Passwort
|
||||
- umschaltbare deutsche oder US-Tastaturbelegung
|
||||
- geräteabhängige Tests für Akku, Touchpad, Webcam und mehrere Laufwerke
|
||||
- getrennte Ergebnisse für Hardwarefehler, nicht verfügbaren Test,
|
||||
Einrichtungsproblem, nicht geprüft und nicht vorhanden
|
||||
|
||||
@@ -101,6 +101,20 @@ fn ethernet_connected() -> bool {
|
||||
.iter()
|
||||
.any(|(_, kind, carrier, _)| kind == "LAN/other" && carrier == "1")
|
||||
}
|
||||
fn mobile_device() -> bool {
|
||||
let chassis = read_trim("/sys/class/dmi/id/chassis_type")
|
||||
.or_else(|| read_trim("/run/csr-intake/chassis_type"))
|
||||
.unwrap_or_default();
|
||||
matches!(
|
||||
chassis.as_str(),
|
||||
"8" | "9" | "10" | "11" | "14" | "30" | "31" | "32"
|
||||
) || fs::read_dir("/sys/class/power_supply").is_ok_and(|entries| {
|
||||
entries.flatten().any(|entry| {
|
||||
read_trim(&entry.path().join("type").to_string_lossy())
|
||||
.is_some_and(|kind| kind == "Battery")
|
||||
})
|
||||
})
|
||||
}
|
||||
fn memory_dmi() -> (String, String) {
|
||||
let text = match output("dmidecode", &["-t", "memory"]) {
|
||||
Some(s) => s,
|
||||
@@ -257,8 +271,8 @@ pub fn guided_tests(data: &Path) {
|
||||
println!("Invalid asset ID");
|
||||
return;
|
||||
}
|
||||
println!("\nNETWORK PRECHECK: Before Wi-Fi test, unplug Ethernet if one is connected.");
|
||||
if ethernet_connected() {
|
||||
println!("\nNETWORK PRECHECK");
|
||||
if mobile_device() && ethernet_connected() {
|
||||
println!("WARNING: Wired network link detected. Unplug LAN cable before Wi-Fi test.");
|
||||
let _ = ask("After unplugging LAN press Enter to recheck...");
|
||||
if ethernet_connected() {
|
||||
@@ -267,7 +281,7 @@ pub fn guided_tests(data: &Path) {
|
||||
} else {
|
||||
println!("No active wired link detected. Wireless testing may proceed.");
|
||||
}
|
||||
let wifi = if ethernet_connected() {
|
||||
let wifi = if mobile_device() && ethernet_connected() {
|
||||
"NOT_TESTED"
|
||||
} else {
|
||||
println!(
|
||||
|
||||
@@ -31,6 +31,38 @@ fn exec(tool: &str, args: &[&str]) -> Result<String, String> {
|
||||
Err(format!("{tool} ({})\n{log}", out.status))
|
||||
}
|
||||
}
|
||||
fn mobile_chassis(value: &str) -> bool {
|
||||
matches!(
|
||||
value.trim(),
|
||||
"8" | "9" | "10" | "11" | "14" | "30" | "31" | "32"
|
||||
)
|
||||
}
|
||||
fn mobile_device() -> bool {
|
||||
let chassis = fs::read_to_string("/sys/class/dmi/id/chassis_type")
|
||||
.or_else(|_| fs::read_to_string("/run/csr-intake/chassis_type"))
|
||||
.unwrap_or_default();
|
||||
if mobile_chassis(&chassis) {
|
||||
return true;
|
||||
}
|
||||
fs::read_dir("/sys/class/power_supply").is_ok_and(|entries| {
|
||||
entries.flatten().any(|entry| {
|
||||
fs::read_to_string(entry.path().join("type")).is_ok_and(|kind| kind.trim() == "Battery")
|
||||
})
|
||||
})
|
||||
}
|
||||
fn wired_link_active() -> bool {
|
||||
fs::read_dir("/sys/class/net").is_ok_and(|entries| {
|
||||
entries.flatten().any(|entry| {
|
||||
let path = entry.path();
|
||||
entry.file_name() != "lo"
|
||||
&& !path.join("wireless").exists()
|
||||
&& fs::read_to_string(path.join("carrier")).is_ok_and(|v| v.trim() == "1")
|
||||
})
|
||||
})
|
||||
}
|
||||
pub fn needs_lan_disconnect() -> bool {
|
||||
mobile_device() && wired_link_active()
|
||||
}
|
||||
fn temps() -> Vec<(String, f64)> {
|
||||
let mut values = Vec::new();
|
||||
let Ok(chips) = fs::read_dir("/sys/class/hwmon") else {
|
||||
@@ -316,7 +348,12 @@ fn run_legacy(tool: &str, settings: &crate::workflow::Settings) -> (bool, String
|
||||
&["-f", "IN-USE,SSID,SIGNAL", "device", "wifi", "list"],
|
||||
)
|
||||
.unwrap_or_else(|e| e);
|
||||
(true,format!("Status:\n{status}\nGeräte:\n{dev}\nNetze:\n{scan}\nLAN trennen und WLAN-Verbindung aktiv testen; Sichtbarkeit ist noch kein PASS."))
|
||||
let lan = if needs_lan_disconnect() {
|
||||
"\nWARNUNG: Mobiles Gerät mit aktiver LAN-Verbindung. LAN-Kabel vor dem WLAN-Test entfernen."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
(true,format!("Status:\n{status}\nGeräte:\n{dev}\nNetze:\n{scan}{lan}\nWLAN-Verbindung aktiv testen; Sichtbarkeit ist noch kein PASS."))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,4 +516,14 @@ mod tests {
|
||||
);
|
||||
assert_eq!(peak_temperature("PEAK_TEMPERATURE_C=NaN"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portable_chassis_types_are_recognized() {
|
||||
for value in ["8", "9", "10", "11", "14", "30", "31", "32"] {
|
||||
assert!(mobile_chassis(value));
|
||||
}
|
||||
for value in ["3", "7", "13", "15", "unknown"] {
|
||||
assert!(!mobile_chassis(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,6 +650,15 @@ impl App {
|
||||
));
|
||||
if let Some(d) = &self.station_device {
|
||||
ui.heading(format!("#{}", d["number"]));
|
||||
match d["_registration"].as_str() {
|
||||
Some("NEW") => {
|
||||
ui.colored_label(GREEN, tr(en, "Neu im Inventory registriert", "Newly registered in inventory"));
|
||||
}
|
||||
Some("EXISTING") => {
|
||||
ui.label(tr(en, "Bereits im Inventory vorhanden", "Existing inventory device"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let stage = match d["stage"].as_str().unwrap_or("") {
|
||||
"INTAKE" => "Eingang",
|
||||
"INSPECTION" => "Inspektion",
|
||||
@@ -912,6 +921,16 @@ impl App {
|
||||
}
|
||||
ui.add_enabled_ui(!conflict, |ui| {
|
||||
if key == "wifi" {
|
||||
if diagnostics::needs_lan_disconnect() {
|
||||
ui.colored_label(
|
||||
ORANGE,
|
||||
tr(
|
||||
en,
|
||||
"LAN-Kabel entfernen: Bei diesem mobilen Gerät muss der WLAN-Test ohne aktive Kabelverbindung erfolgen.",
|
||||
"Disconnect Ethernet: test Wi-Fi on this mobile device without an active wired connection.",
|
||||
),
|
||||
);
|
||||
}
|
||||
self.wifi.set_language(en);
|
||||
let was_busy = self.wifi.busy();
|
||||
self.wifi.ui(ui, self.receiver.is_some());
|
||||
@@ -1437,6 +1456,29 @@ impl App {
|
||||
ui.selectable_value(&mut self.settings_draft.language, "de".into(), "Deutsch");
|
||||
ui.selectable_value(&mut self.settings_draft.language, "en".into(), "English");
|
||||
});
|
||||
ui.label(tr(en, "Tastaturbelegung", "Keyboard layout"));
|
||||
egui::ComboBox::from_id_salt("keyboard-layout")
|
||||
.selected_text(match self.settings_draft.keyboard_layout.as_str() {
|
||||
"us" => "English (US)",
|
||||
_ => "Deutsch (DE)",
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(
|
||||
&mut self.settings_draft.keyboard_layout,
|
||||
"de".into(),
|
||||
"Deutsch (DE)",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.settings_draft.keyboard_layout,
|
||||
"us".into(),
|
||||
"English (US)",
|
||||
);
|
||||
});
|
||||
ui.small(tr(
|
||||
en,
|
||||
"Die Tastaturbelegung wird beim nächsten Start des USB-Systems angewendet.",
|
||||
"The keyboard layout is applied on the next USB-system boot.",
|
||||
));
|
||||
ui.label(tr(en, "WLAN-Testserver", "Wi-Fi test server"));
|
||||
ui.text_edit_singleline(&mut self.settings_draft.test_url);
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
@@ -86,6 +86,7 @@ pub fn init_index(data: &Path) -> Result<(), String> {
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
pub language: String,
|
||||
pub keyboard_layout: String,
|
||||
pub test_url: String,
|
||||
pub stress_seconds: u64,
|
||||
pub recording_seconds: u64,
|
||||
@@ -98,6 +99,7 @@ impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
language: "de".into(),
|
||||
keyboard_layout: "de".into(),
|
||||
test_url: "https://example.com".into(),
|
||||
stress_seconds: 120,
|
||||
recording_seconds: 5,
|
||||
@@ -113,6 +115,9 @@ impl Settings {
|
||||
if !["de", "en"].contains(&self.language.as_str()) {
|
||||
return Err("Language must be de or en".into());
|
||||
}
|
||||
if !["de", "us"].contains(&self.keyboard_layout.as_str()) {
|
||||
return Err("Keyboard layout must be de or us".into());
|
||||
}
|
||||
if !(self.test_url.starts_with("https://") || self.test_url.starts_with("http://"))
|
||||
|| self.test_url.contains(['\n', '\r'])
|
||||
{
|
||||
|
||||
@@ -4,6 +4,19 @@
|
||||
[[ -z "${WAYLAND_DISPLAY:-}" ]] || return 0
|
||||
export WLR_RENDERER_ALLOW_SOFTWARE=1
|
||||
export XDG_SESSION_TYPE=wayland
|
||||
keyboard_layout=$(python3 - /mnt/intake-data/config/workshop.json <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
try:
|
||||
value = json.loads(pathlib.Path(sys.argv[1]).read_text()).get('keyboard_layout', 'de')
|
||||
except (OSError, ValueError):
|
||||
value = 'de'
|
||||
print(value if value in ('de', 'us') else 'de')
|
||||
PY
|
||||
)
|
||||
export XKB_DEFAULT_LAYOUT="$keyboard_layout"
|
||||
if [[ ! -w /mnt/intake-data ]]; then
|
||||
printf '\nCS-R Intake: data partition not writable; no scan can be saved.\n' >&2
|
||||
printf 'Press Enter for diagnostic console...'
|
||||
|
||||
@@ -111,7 +111,12 @@ def api_request(base, key, method, suffix, payload=None, timeout=12, ca_file=Non
|
||||
if ca_file:
|
||||
context.load_verify_locations(cafile=str(ca_file))
|
||||
with urllib.request.urlopen(request, timeout=timeout, context=context) as res:
|
||||
return json.loads(res.read(1024 * 1024).decode('utf-8'))
|
||||
result = json.loads(res.read(1024 * 1024).decode('utf-8'))
|
||||
if method == 'POST' and suffix.strip('/') == 'devices/resolve':
|
||||
status = getattr(res, 'status', None)
|
||||
if status in (200, 201):
|
||||
result['_registration'] = 'NEW' if status == 201 else 'EXISTING'
|
||||
return result
|
||||
|
||||
def credentials(config):
|
||||
base = config.get('url', '')
|
||||
@@ -346,9 +351,12 @@ def run_station(root,action,report_path):
|
||||
else:
|
||||
report=json.loads(report_path.read_text()) if report_path else station_identity()
|
||||
device=station_resolve(config,report)
|
||||
registration=device.get('_registration')
|
||||
if action=='intake' and device['stage']=='INTAKE':
|
||||
device=station_api(config,'POST',f"devices/{device['number']}/intake/",{
|
||||
'hwid':hardware_id(report),'expected_revision':device['revision'],'specifications':map_fields(report)})
|
||||
if registration:
|
||||
device['_registration']=registration
|
||||
if action == 'intake' and report_path:
|
||||
raw=report_path.read_bytes(); sha=hashlib.sha256(raw).hexdigest()
|
||||
with contextlib.closing(sqlite3.connect(root/'database/intake.sqlite3')) as db, db:
|
||||
|
||||
@@ -157,6 +157,17 @@ class ConnectionTests(unittest.TestCase):
|
||||
self.assertTrue(context.check_hostname)
|
||||
self.assertIs(request.call_args.kwargs['context'], context)
|
||||
|
||||
def test_resolve_exposes_new_or_existing_registration(self):
|
||||
for status, expected in ((201, 'NEW'), (200, 'EXISTING')):
|
||||
with self.subTest(status=status), patch.object(bridge.urllib.request, 'urlopen') as request:
|
||||
response = request.return_value.__enter__.return_value
|
||||
response.status = status
|
||||
response.read.return_value = b'{"number": 42}'
|
||||
result = bridge.api_request(
|
||||
'https://inventory.test', 'test-key', 'POST', 'devices/resolve/', {}
|
||||
)
|
||||
self.assertEqual(result['_registration'], expected)
|
||||
|
||||
def test_network_errors_are_actionable_without_raw_secrets(self):
|
||||
cases = [
|
||||
(bridge.ssl.SSLCertVerificationError(1, 'secret'), 'certificate'),
|
||||
|
||||
Reference in New Issue
Block a user