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
|
- automatische Hardware-ID und serverseitig vergebene Gerätenummer
|
||||||
- Schutz vor doppelten Datensätzen
|
- Schutz vor doppelten Datensätzen
|
||||||
- gespeichertes Werkstatt-WLAN mit geschütztem Passwort
|
- gespeichertes Werkstatt-WLAN mit geschütztem Passwort
|
||||||
|
- umschaltbare deutsche oder US-Tastaturbelegung
|
||||||
- geräteabhängige Tests für Akku, Touchpad, Webcam und mehrere Laufwerke
|
- geräteabhängige Tests für Akku, Touchpad, Webcam und mehrere Laufwerke
|
||||||
- getrennte Ergebnisse für Hardwarefehler, nicht verfügbaren Test,
|
- getrennte Ergebnisse für Hardwarefehler, nicht verfügbaren Test,
|
||||||
Einrichtungsproblem, nicht geprüft und nicht vorhanden
|
Einrichtungsproblem, nicht geprüft und nicht vorhanden
|
||||||
|
|||||||
@@ -101,6 +101,20 @@ fn ethernet_connected() -> bool {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|(_, kind, carrier, _)| kind == "LAN/other" && carrier == "1")
|
.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) {
|
fn memory_dmi() -> (String, String) {
|
||||||
let text = match output("dmidecode", &["-t", "memory"]) {
|
let text = match output("dmidecode", &["-t", "memory"]) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
@@ -257,8 +271,8 @@ pub fn guided_tests(data: &Path) {
|
|||||||
println!("Invalid asset ID");
|
println!("Invalid asset ID");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
println!("\nNETWORK PRECHECK: Before Wi-Fi test, unplug Ethernet if one is connected.");
|
println!("\nNETWORK PRECHECK");
|
||||||
if ethernet_connected() {
|
if mobile_device() && ethernet_connected() {
|
||||||
println!("WARNING: Wired network link detected. Unplug LAN cable before Wi-Fi test.");
|
println!("WARNING: Wired network link detected. Unplug LAN cable before Wi-Fi test.");
|
||||||
let _ = ask("After unplugging LAN press Enter to recheck...");
|
let _ = ask("After unplugging LAN press Enter to recheck...");
|
||||||
if ethernet_connected() {
|
if ethernet_connected() {
|
||||||
@@ -267,7 +281,7 @@ pub fn guided_tests(data: &Path) {
|
|||||||
} else {
|
} else {
|
||||||
println!("No active wired link detected. Wireless testing may proceed.");
|
println!("No active wired link detected. Wireless testing may proceed.");
|
||||||
}
|
}
|
||||||
let wifi = if ethernet_connected() {
|
let wifi = if mobile_device() && ethernet_connected() {
|
||||||
"NOT_TESTED"
|
"NOT_TESTED"
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
@@ -31,6 +31,38 @@ fn exec(tool: &str, args: &[&str]) -> Result<String, String> {
|
|||||||
Err(format!("{tool} ({})\n{log}", out.status))
|
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)> {
|
fn temps() -> Vec<(String, f64)> {
|
||||||
let mut values = Vec::new();
|
let mut values = Vec::new();
|
||||||
let Ok(chips) = fs::read_dir("/sys/class/hwmon") else {
|
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"],
|
&["-f", "IN-USE,SSID,SIGNAL", "device", "wifi", "list"],
|
||||||
)
|
)
|
||||||
.unwrap_or_else(|e| e);
|
.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);
|
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 {
|
if let Some(d) = &self.station_device {
|
||||||
ui.heading(format!("#{}", d["number"]));
|
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("") {
|
let stage = match d["stage"].as_str().unwrap_or("") {
|
||||||
"INTAKE" => "Eingang",
|
"INTAKE" => "Eingang",
|
||||||
"INSPECTION" => "Inspektion",
|
"INSPECTION" => "Inspektion",
|
||||||
@@ -912,6 +921,16 @@ impl App {
|
|||||||
}
|
}
|
||||||
ui.add_enabled_ui(!conflict, |ui| {
|
ui.add_enabled_ui(!conflict, |ui| {
|
||||||
if key == "wifi" {
|
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);
|
self.wifi.set_language(en);
|
||||||
let was_busy = self.wifi.busy();
|
let was_busy = self.wifi.busy();
|
||||||
self.wifi.ui(ui, self.receiver.is_some());
|
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, "de".into(), "Deutsch");
|
||||||
ui.selectable_value(&mut self.settings_draft.language, "en".into(), "English");
|
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.label(tr(en, "WLAN-Testserver", "Wi-Fi test server"));
|
||||||
ui.text_edit_singleline(&mut self.settings_draft.test_url);
|
ui.text_edit_singleline(&mut self.settings_draft.test_url);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ pub fn init_index(data: &Path) -> Result<(), String> {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub language: String,
|
pub language: String,
|
||||||
|
pub keyboard_layout: String,
|
||||||
pub test_url: String,
|
pub test_url: String,
|
||||||
pub stress_seconds: u64,
|
pub stress_seconds: u64,
|
||||||
pub recording_seconds: u64,
|
pub recording_seconds: u64,
|
||||||
@@ -98,6 +99,7 @@ impl Default for Settings {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
language: "de".into(),
|
language: "de".into(),
|
||||||
|
keyboard_layout: "de".into(),
|
||||||
test_url: "https://example.com".into(),
|
test_url: "https://example.com".into(),
|
||||||
stress_seconds: 120,
|
stress_seconds: 120,
|
||||||
recording_seconds: 5,
|
recording_seconds: 5,
|
||||||
@@ -113,6 +115,9 @@ impl Settings {
|
|||||||
if !["de", "en"].contains(&self.language.as_str()) {
|
if !["de", "en"].contains(&self.language.as_str()) {
|
||||||
return Err("Language must be de or en".into());
|
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://"))
|
if !(self.test_url.starts_with("https://") || self.test_url.starts_with("http://"))
|
||||||
|| self.test_url.contains(['\n', '\r'])
|
|| self.test_url.contains(['\n', '\r'])
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,19 @@
|
|||||||
[[ -z "${WAYLAND_DISPLAY:-}" ]] || return 0
|
[[ -z "${WAYLAND_DISPLAY:-}" ]] || return 0
|
||||||
export WLR_RENDERER_ALLOW_SOFTWARE=1
|
export WLR_RENDERER_ALLOW_SOFTWARE=1
|
||||||
export XDG_SESSION_TYPE=wayland
|
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
|
if [[ ! -w /mnt/intake-data ]]; then
|
||||||
printf '\nCS-R Intake: data partition not writable; no scan can be saved.\n' >&2
|
printf '\nCS-R Intake: data partition not writable; no scan can be saved.\n' >&2
|
||||||
printf 'Press Enter for diagnostic console...'
|
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:
|
if ca_file:
|
||||||
context.load_verify_locations(cafile=str(ca_file))
|
context.load_verify_locations(cafile=str(ca_file))
|
||||||
with urllib.request.urlopen(request, timeout=timeout, context=context) as res:
|
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):
|
def credentials(config):
|
||||||
base = config.get('url', '')
|
base = config.get('url', '')
|
||||||
@@ -346,9 +351,12 @@ def run_station(root,action,report_path):
|
|||||||
else:
|
else:
|
||||||
report=json.loads(report_path.read_text()) if report_path else station_identity()
|
report=json.loads(report_path.read_text()) if report_path else station_identity()
|
||||||
device=station_resolve(config,report)
|
device=station_resolve(config,report)
|
||||||
|
registration=device.get('_registration')
|
||||||
if action=='intake' and device['stage']=='INTAKE':
|
if action=='intake' and device['stage']=='INTAKE':
|
||||||
device=station_api(config,'POST',f"devices/{device['number']}/intake/",{
|
device=station_api(config,'POST',f"devices/{device['number']}/intake/",{
|
||||||
'hwid':hardware_id(report),'expected_revision':device['revision'],'specifications':map_fields(report)})
|
'hwid':hardware_id(report),'expected_revision':device['revision'],'specifications':map_fields(report)})
|
||||||
|
if registration:
|
||||||
|
device['_registration']=registration
|
||||||
if action == 'intake' and report_path:
|
if action == 'intake' and report_path:
|
||||||
raw=report_path.read_bytes(); sha=hashlib.sha256(raw).hexdigest()
|
raw=report_path.read_bytes(); sha=hashlib.sha256(raw).hexdigest()
|
||||||
with contextlib.closing(sqlite3.connect(root/'database/intake.sqlite3')) as db, db:
|
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.assertTrue(context.check_hostname)
|
||||||
self.assertIs(request.call_args.kwargs['context'], context)
|
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):
|
def test_network_errors_are_actionable_without_raw_secrets(self):
|
||||||
cases = [
|
cases = [
|
||||||
(bridge.ssl.SSLCertVerificationError(1, 'secret'), 'certificate'),
|
(bridge.ssl.SSLCertVerificationError(1, 'secret'), 'certificate'),
|
||||||
|
|||||||
Reference in New Issue
Block a user