diff --git a/.env b/.env index 1e214d01..e4247eb7 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -FIBERS_VER=43a04d1 +FIBERS_VER=sleep-as-yield TRIE_VER=28b3572 BUS_VER=89af71a UI_VER=a8c5965 diff --git a/sprint-docs/modem-research/command_outputs/collect_modem_snapshot.sh b/sprint-docs/modem-research/command_outputs/collect_modem_snapshot.sh new file mode 100644 index 00000000..d66e08fb --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/collect_modem_snapshot.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# collect_modem_snapshot.sh +# Usage: ./collect_modem_snapshot.sh /tmp/idle_run +# Env overrides (optional): MODEM, SIM, QMI_DEV + +set -u + +RUN_DIR="${1:-/tmp/modem_run}" +MODEM="${MODEM:-0}" +SIM="${SIM:-0}" +QMI_DEV="${QMI_DEV:-/dev/cdc-wdm0}" + +MMCLI_DIR="${RUN_DIR}/mmcli" +QMI_DIR="${RUN_DIR}/qmicli" + +mkdir -p "${MMCLI_DIR}" "${QMI_DIR}" + +echo "Snapshot in RUN_DIR='${RUN_DIR}', MODEM='${MODEM}', SIM='${SIM}', QMI_DEV='${QMI_DEV}'" >&2 + +############################################################################### +# mmcli read-only snapshots +############################################################################### + +# mmcli -J -m +mmcli -J -m "${MODEM}" \ + > "${MMCLI_DIR}/modem.json" 2>&1 || true + +# mmcli -J -i +mmcli -J -i "${SIM}" \ + > "${MMCLI_DIR}/sim.json" 2>&1 || true + +# mmcli -J -m --signal-get +mmcli -J -m "${MODEM}" --signal-get \ + > "${MMCLI_DIR}/signal.json" 2>&1 || true + +# mmcli -J -m --location-status +mmcli -J -m "${MODEM}" --location-status \ + > "${MMCLI_DIR}/location-status.json" 2>&1 || true + +############################################################################### +# qmicli read-only snapshots +############################################################################### + +# qmicli --uim-get-card-status +qmicli -p -d "${QMI_DEV}" --uim-get-card-status \ + > "${QMI_DIR}/uim-get-card-status.txt" 2>&1 || true + +# qmicli --uim-read-transparent=...6F3E (GID1) +qmicli -p -d "${QMI_DEV}" --uim-read-transparent=0x3F00,0x7FFF,0x6F3E \ + > "${QMI_DIR}/uim-read-transparent-gid1.txt" 2>&1 || true + +# qmicli --nas-get-rf-band-info +qmicli -p -d "${QMI_DEV}" --nas-get-rf-band-info \ + > "${QMI_DIR}/nas-get-rf-band-info.txt" 2>&1 || true + +# qmicli --nas-get-home-network +qmicli -p -d "${QMI_DEV}" --nas-get-home-network \ + > "${QMI_DIR}/nas-get-home-network.txt" 2>&1 || true + +# qmicli --nas-get-serving-system +qmicli -p -d "${QMI_DEV}" --nas-get-serving-system \ + > "${QMI_DIR}/nas-get-serving-system.txt" 2>&1 || true + +# qmicli --nas-get-signal-info +qmicli -p -d "${QMI_DEV}" --nas-get-signal-info \ + > "${QMI_DIR}/nas-get-signal-info.txt" 2>&1 || true + +echo "Snapshot done under '${RUN_DIR}'." >&2 diff --git a/sprint-docs/modem-research/command_outputs/collect_modem_transitions.sh b/sprint-docs/modem-research/command_outputs/collect_modem_transitions.sh new file mode 100644 index 00000000..2ba8b3ab --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/collect_modem_transitions.sh @@ -0,0 +1,155 @@ +#!/bin/sh +# collect_modem_transitions_matrix.sh +# +# Usage: +# MODEM=0 QMI_DEV=/dev/cdc-wdm0 \ +# sh collect_modem_transitions_matrix.sh \ +# /tmp/transitions enabled on +# +# Args: +# $1 = RUN_DIR (root output dir) +# $2 = INITIAL_MODEM_STATE: registered|disabled|connected +# $3 = INITIAL_SIM_STATE: on|off +# +# You should manually verify the modem really starts in this state. + +set -u + +RUN_DIR="${1:-/tmp/modem_transitions}" +INITIAL_MODEM_STATE="${2:-registered}" # registered|disabled|connected +INITIAL_SIM_STATE="${3:-on}" # on|off + +MODEM="${MODEM:-0}" +QMI_DEV="${QMI_DEV:-/dev/cdc-wdm0}" + +echo "RUN_DIR='${RUN_DIR}', INITIAL_MODEM_STATE='${INITIAL_MODEM_STATE}', INITIAL_SIM_STATE='${INITIAL_SIM_STATE}', MODEM='${MODEM}', QMI_DEV='${QMI_DEV}'" >&2 + +# Outputs go under: +# ${RUN_DIR}/${INITIAL_MODEM_STATE}_modem_${INITIAL_SIM_STATE}_sim/mmcli/.txt +# ${RUN_DIR}/${INITIAL_MODEM_STATE}_modem_${INITIAL_SIM_STATE}_sim/qmicli/.txt +INITIAL_STATE_DIR="${RUN_DIR}/${INITIAL_MODEM_STATE}_modem_${INITIAL_SIM_STATE}_sim" +MMCLI_DIR="${INITIAL_STATE_DIR}/mmcli" +QMI_DIR="${INITIAL_STATE_DIR}/qmicli" + +mkdir -p "${MMCLI_DIR}" "${QMI_DIR}" + +ensure_modem_state() { + case "$1" in + registered) + mmcli -m "${MODEM}" -e >/dev/null 2>&1 || true + sleep 1 + ;; + connected) + if [ -z "${CONNECTION_STRING:-}" ]; then + echo "CONNECTION_STRING is not set; cannot ensure 'connected' state." >&2 + return 1 + fi + mmcli -m "${MODEM}" --simple-connect="${CONNECTION_STRING}" + sleep 1 + ;; + disabled) + mmcli -m "${MODEM}" -d >/dev/null 2>&1 || true + ;; + *) + echo "Unknown modem state '$1' (expected registered|disabled|connected)" >&2 + ;; + esac +} + +ensure_sim_state() { + case "$1" in + on) + qmicli -p -d "${QMI_DEV}" --uim-sim-power-on=1 >/dev/null 2>&1 || true + ;; + off) + qmicli -p -d "${QMI_DEV}" --uim-sim-power-off=1 >/dev/null 2>&1 || true + ;; + *) + echo "Unknown SIM state '$1' (expected on|off)" >&2 + ;; + esac +} + +# Helper to run a single experiment: +# name: logical name (e.g. mm-enable, sim-power-off) +# kind: "modem" or "sim" (which aspect this experiment changes) +# forward: shell code for the transition command (no redirection) +run_experiment() { + name="$1" + kind="$2" + forward_cmd="$3" + + echo "=== Experiment '${name}' (initial state ${INITIAL_MODEM_STATE}/${INITIAL_SIM_STATE}) ===" >&2 + + # 1. Force the relevant initial state (modem *or* SIM, not both) + case "${kind}" in + modem) + ensure_modem_state "${INITIAL_MODEM_STATE}" + ;; + sim) + ensure_sim_state "${INITIAL_SIM_STATE}" + ;; + *) + echo "Unknown experiment kind '${kind}' (expected modem|sim)" >&2 + ;; + esac + + # 2. Run transition and capture output + # Choose output directory based on kind and write to .txt + case "${kind}" in + modem) + out_file="${MMCLI_DIR}/${name}.txt" + ;; + sim) + out_file="${QMI_DIR}/${name}.txt" + ;; + *) + out_file="${INITIAL_STATE_DIR}/${name}.txt" + ;; + esac + + # shellcheck disable=SC2086 + sh -c "${forward_cmd} > '${out_file}' 2>&1" || true + + # 3. Restore initial state for the same aspect (modem or SIM) + case "${kind}" in + modem) + ensure_modem_state "${INITIAL_MODEM_STATE}" + ;; + sim) + ensure_sim_state "${INITIAL_SIM_STATE}" + ;; + esac +} + +############################################################################### +# Define experiments +# +# You can comment out any you don’t care about. +############################################################################### + +# Modem enable (forward: -e, reverse: ensure initial state again) +run_experiment \ + "mm-enable" \ + "modem" \ + "mmcli -m '${MODEM}' -e" + +# Modem disable +run_experiment \ + "mm-disable" \ + "modem" \ + "mmcli -m '${MODEM}' -d" + +# SIM power off +run_experiment \ + "sim-power-off" \ + "sim" \ + "qmicli -p -d '${QMI_DEV}' --uim-sim-power-off=1" + +# SIM power on +run_experiment \ + "sim-power-on" \ + "sim" \ + "qmicli -p -d '${QMI_DEV}' --uim-sim-power-on=1" + +echo "All experiments done; each should have returned to initial state (best effort)." >&2 diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/location-status.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/location-status.json new file mode 100644 index 00000000..43f70bc0 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/location-status.json @@ -0,0 +1,24 @@ +{ + "modem": { + "location": { + "capabilities": [ + "3gpp-lac-ci", + "gps-raw", + "gps-nmea", + "gps-unmanaged", + "agps-msa", + "agps-msb" + ], + "enabled": [ + "3gpp-lac-ci" + ], + "gps": { + "assistance": [], + "assistance-servers": [], + "refresh-rate": "30", + "supl-server": "--" + }, + "signals": "no" + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/modem.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/modem.json new file mode 100644 index 00000000..be49085b --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/modem.json @@ -0,0 +1,189 @@ +{ + "modem": { + "3gpp": { + "5gnr": { + "registration-settings": { + "drx-cycle": "--", + "mico-mode": "--" + } + }, + "enabled-locks": [ + "fixed-dialing" + ], + "eps": { + "initial-bearer": { + "dbus-path": "/org/freedesktop/ModemManager1/Bearer/7", + "settings": { + "apn": "", + "ip-type": "ipv4", + "password": "--", + "user": "--" + } + }, + "ue-mode-operation": "csps-2" + }, + "imei": "867929068986514", + "operator-code": "23410", + "operator-name": "O2 - UK", + "packet-service-state": "attached", + "pco": "--", + "registration-state": "home" + }, + "cdma": { + "activation-state": "--", + "cdma1x-registration-state": "--", + "esn": "--", + "evdo-registration-state": "--", + "meid": "--", + "nid": "--", + "sid": "--" + }, + "dbus-path": "/org/freedesktop/ModemManager1/Modem/6", + "generic": { + "access-technologies": [ + "lte" + ], + "bearers": [ + "/org/freedesktop/ModemManager1/Bearer/6", + "/org/freedesktop/ModemManager1/Bearer/5", + "/org/freedesktop/ModemManager1/Bearer/4", + "/org/freedesktop/ModemManager1/Bearer/3" + ], + "carrier-configuration": "ROW_Generic_3GPP", + "carrier-configuration-revision": "0501081F", + "current-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "current-capabilities": [ + "gsm-umts, lte" + ], + "current-modes": "allowed: 2g, 3g, 4g; preferred: 4g", + "device": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "device-identifier": "b5bf9f66a67a0da6308a67fb858cdfef75d82565", + "drivers": [ + "option1", + "qmi_wwan" + ], + "equipment-identifier": "867929068986514", + "hardware-revision": "10000", + "manufacturer": "QUALCOMM INCORPORATED", + "model": "QUECTEL Mobile Broadband Module", + "own-numbers": [], + "physdev": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "plugin": "quectel", + "ports": [ + "cdc-wdm0 (qmi)", + "ttyUSB0 (ignored)", + "ttyUSB1 (gps)", + "ttyUSB2 (at)", + "ttyUSB3 (at)", + "wwan0 (net)" + ], + "power-state": "on", + "primary-port": "cdc-wdm0", + "primary-sim-slot": "1", + "revision": "EG25GGBR07A08M2G", + "signal-quality": { + "recent": "yes", + "value": "65" + }, + "sim": "/org/freedesktop/ModemManager1/SIM/2", + "sim-slots": [ + "/org/freedesktop/ModemManager1/SIM/2", + "/" + ], + "state": "connected", + "state-failed-reason": "--", + "supported-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "supported-capabilities": [ + "gsm-umts, lte" + ], + "supported-ip-families": [ + "ipv4", + "ipv6", + "ipv4v6" + ], + "supported-modes": [ + "allowed: 2g; preferred: none", + "allowed: 3g; preferred: none", + "allowed: 4g; preferred: none", + "allowed: 2g, 3g; preferred: 3g", + "allowed: 2g, 3g; preferred: 2g", + "allowed: 2g, 4g; preferred: 4g", + "allowed: 2g, 4g; preferred: 2g", + "allowed: 3g, 4g; preferred: 4g", + "allowed: 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 4g", + "allowed: 2g, 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 2g" + ], + "unlock-required": "sim-pin2", + "unlock-retries": [ + "sim-pin (3)", + "sim-puk (10)", + "sim-pin2 (3)", + "sim-puk2 (10)" + ] + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/signal.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/signal.json new file mode 100644 index 00000000..5d4fcadd --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/signal.json @@ -0,0 +1,48 @@ +{ + "modem": { + "signal": { + "5g": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "snr": "--" + }, + "cdma1x": { + "ecio": "--", + "error-rate": "--", + "rssi": "--" + }, + "evdo": { + "ecio": "--", + "error-rate": "--", + "io": "--", + "rssi": "--", + "sinr": "--" + }, + "gsm": { + "error-rate": "--", + "rssi": "--" + }, + "lte": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "rssi": "--", + "snr": "--" + }, + "refresh": { + "rate": "0" + }, + "threshold": { + "error-rate": "no", + "rssi": "0" + }, + "umts": { + "ecio": "--", + "error-rate": "--", + "rscp": "--", + "rssi": "--" + } + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/sim.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/sim.json new file mode 100644 index 00000000..050a27d5 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/mmcli/sim.json @@ -0,0 +1,22 @@ +{ + "sim": { + "dbus-path": "/org/freedesktop/ModemManager1/SIM/2", + "properties": { + "active": "yes", + "eid": "--", + "emergency-numbers": [ + "999", + "00112" + ], + "esim-status": "--", + "gid1": "85FFFFFFFFFF47454E4945494E20202020202020", + "gid2": "FFFFFFFFFFFF2020202020202020202020202020", + "iccid": "8944110069073915392", + "imsi": "234103403359194", + "operator-code": "23410", + "operator-name": "--", + "removability": "--", + "sim-type": "--" + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-home-network.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-home-network.txt new file mode 100644 index 00000000..84351fca --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-home-network.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got home network: + Home network: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Network name source: se13 diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-rf-band-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-rf-band-info.txt new file mode 100644 index 00000000..6a1adfe6 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-rf-band-info.txt @@ -0,0 +1,12 @@ +[/dev/cdc-wdm0] Successfully got RF band info +Band Information: + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Band Information (Extended): + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Bandwidth: + Radio Interface: 'lte' + Bandwidth: '20' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-serving-system.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-serving-system.txt new file mode 100644 index 00000000..b047d9cb --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-serving-system.txt @@ -0,0 +1,29 @@ +[/dev/cdc-wdm0] Successfully got serving system: + Registration state: 'registered' + CS: 'attached' + PS: 'attached' + Selected network: '3gpp' + Radio interfaces: '1' + [0]: 'lte' + Roaming status: 'off' + Data service capabilities: '1' + [0]: 'lte' + Current PLMN: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Roaming indicators: '1' + [0]: 'off' (lte) + 3GPP location area code: '65534' + 3GPP cell ID: '153667976' + Detailed status: + Status: 'available' + Capability: 'cs-ps' + HDR Status: 'none' + HDR Hybrid: 'no' + Forbidden: 'no' + LTE tracking area code: '14768' + Full operator code info: + MCC: '234' + MNC: '10' + MNC with PCS digit: 'no' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-signal-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-signal-info.txt new file mode 100644 index 00000000..ddf57088 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/nas-get-signal-info.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got signal info +LTE: + RSSI: '-73 dBm' + RSRQ: '-11 dB' + RSRP: '-103 dBm' + SNR: '10.4 dB' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-get-card-status.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-get-card-status.txt new file mode 100644 index 00000000..07aedb5e --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-get-card-status.txt @@ -0,0 +1,37 @@ +[/dev/cdc-wdm0] Successfully got card status +Provisioning applications: + Primary GW: slot '1', application '1' + Primary 1X: session doesn't exist + Secondary GW: session doesn't exist + Secondary 1X: session doesn't exist +Slot [1]: + Card state: 'present' + UPIN state: 'not-initialized' + UPIN retries: '0' + UPUK retries: '0' + Application [1]: + Application type: 'usim (2)' + Application state: 'ready' + Application ID: + A0:00:00:00:87:10:02:FF:44:FF:FF:89:01:01:01:00 + Personalization state: 'ready' + UPIN replaces PIN1: 'no' + PIN1 state: 'disabled' + PIN1 retries: '3' + PUK1 retries: '10' + PIN2 state: 'enabled-not-verified' + PIN2 retries: '3' + PUK2 retries: '10' + Application [2]: + Application type: 'unknown (0)' + Application state: 'detected' + Application ID: + A0:00:00:00:63:50:4B:FF:34:FF:07:89:54:46:52:38 + Personalization state: 'unknown' + UPIN replaces PIN1: 'no' + PIN1 state: 'not-initialized' + PIN1 retries: '0' + PUK1 retries: '0' + PIN2 state: 'not-initialized' + PIN2 retries: '0' + PUK2 retries: '0' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-read-transparent-gid1.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-read-transparent-gid1.txt new file mode 100644 index 00000000..b0a6f712 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/connected/qmicli/uim-read-transparent-gid1.txt @@ -0,0 +1,7 @@ +[/dev/cdc-wdm0] Successfully read information from the UIM: +Card result: + SW1: '0x90' + SW2: '0x00' +Read result: + 85:FF:FF:FF:FF:FF:47:45:4E:49:45:49:4E:20:20:20:20:20:20:20 + diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/location-status.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/location-status.json new file mode 100644 index 00000000..8db81530 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/location-status.json @@ -0,0 +1 @@ +error: modem not enabled yet diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/modem.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/modem.json new file mode 100644 index 00000000..c2eec1f9 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/modem.json @@ -0,0 +1,182 @@ +{ + "modem": { + "3gpp": { + "5gnr": { + "registration-settings": { + "drx-cycle": "--", + "mico-mode": "--" + } + }, + "enabled-locks": [ + "fixed-dialing" + ], + "eps": { + "initial-bearer": { + "dbus-path": "--", + "settings": { + "apn": "internet", + "ip-type": "ipv4", + "password": "--", + "user": "--" + } + }, + "ue-mode-operation": "csps-2" + }, + "imei": "867929068986514", + "operator-code": "--", + "operator-name": "--", + "packet-service-state": "--", + "pco": "--", + "registration-state": "--" + }, + "cdma": { + "activation-state": "--", + "cdma1x-registration-state": "--", + "esn": "--", + "evdo-registration-state": "--", + "meid": "--", + "nid": "--", + "sid": "--" + }, + "dbus-path": "/org/freedesktop/ModemManager1/Modem/6", + "generic": { + "access-technologies": [], + "bearers": [], + "carrier-configuration": "ROW_Generic_3GPP", + "carrier-configuration-revision": "0501081F", + "current-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "current-capabilities": [ + "gsm-umts, lte" + ], + "current-modes": "allowed: 2g, 3g, 4g; preferred: 4g", + "device": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "device-identifier": "b5bf9f66a67a0da6308a67fb858cdfef75d82565", + "drivers": [ + "option1", + "qmi_wwan" + ], + "equipment-identifier": "867929068986514", + "hardware-revision": "10000", + "manufacturer": "QUALCOMM INCORPORATED", + "model": "QUECTEL Mobile Broadband Module", + "own-numbers": [], + "physdev": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "plugin": "quectel", + "ports": [ + "cdc-wdm0 (qmi)", + "ttyUSB0 (ignored)", + "ttyUSB1 (gps)", + "ttyUSB2 (at)", + "ttyUSB3 (at)", + "wwan0 (net)" + ], + "power-state": "on", + "primary-port": "cdc-wdm0", + "primary-sim-slot": "1", + "revision": "EG25GGBR07A08M2G", + "signal-quality": { + "recent": "yes", + "value": "0" + }, + "sim": "/org/freedesktop/ModemManager1/SIM/2", + "sim-slots": [ + "/org/freedesktop/ModemManager1/SIM/2", + "/" + ], + "state": "disabled", + "state-failed-reason": "--", + "supported-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "supported-capabilities": [ + "gsm-umts, lte" + ], + "supported-ip-families": [ + "ipv4", + "ipv6", + "ipv4v6" + ], + "supported-modes": [ + "allowed: 2g; preferred: none", + "allowed: 3g; preferred: none", + "allowed: 4g; preferred: none", + "allowed: 2g, 3g; preferred: 3g", + "allowed: 2g, 3g; preferred: 2g", + "allowed: 2g, 4g; preferred: 4g", + "allowed: 2g, 4g; preferred: 2g", + "allowed: 3g, 4g; preferred: 4g", + "allowed: 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 4g", + "allowed: 2g, 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 2g" + ], + "unlock-required": "sim-pin2", + "unlock-retries": [ + "sim-pin (3)", + "sim-puk (10)", + "sim-pin2 (3)", + "sim-puk2 (10)" + ] + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/signal.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/signal.json new file mode 100644 index 00000000..5d4fcadd --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/signal.json @@ -0,0 +1,48 @@ +{ + "modem": { + "signal": { + "5g": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "snr": "--" + }, + "cdma1x": { + "ecio": "--", + "error-rate": "--", + "rssi": "--" + }, + "evdo": { + "ecio": "--", + "error-rate": "--", + "io": "--", + "rssi": "--", + "sinr": "--" + }, + "gsm": { + "error-rate": "--", + "rssi": "--" + }, + "lte": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "rssi": "--", + "snr": "--" + }, + "refresh": { + "rate": "0" + }, + "threshold": { + "error-rate": "no", + "rssi": "0" + }, + "umts": { + "ecio": "--", + "error-rate": "--", + "rscp": "--", + "rssi": "--" + } + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/sim.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/sim.json new file mode 100644 index 00000000..050a27d5 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/mmcli/sim.json @@ -0,0 +1,22 @@ +{ + "sim": { + "dbus-path": "/org/freedesktop/ModemManager1/SIM/2", + "properties": { + "active": "yes", + "eid": "--", + "emergency-numbers": [ + "999", + "00112" + ], + "esim-status": "--", + "gid1": "85FFFFFFFFFF47454E4945494E20202020202020", + "gid2": "FFFFFFFFFFFF2020202020202020202020202020", + "iccid": "8944110069073915392", + "imsi": "234103403359194", + "operator-code": "23410", + "operator-name": "--", + "removability": "--", + "sim-type": "--" + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-home-network.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-home-network.txt new file mode 100644 index 00000000..84351fca --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-home-network.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got home network: + Home network: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Network name source: se13 diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-rf-band-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-rf-band-info.txt new file mode 100644 index 00000000..6a1adfe6 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-rf-band-info.txt @@ -0,0 +1,12 @@ +[/dev/cdc-wdm0] Successfully got RF band info +Band Information: + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Band Information (Extended): + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Bandwidth: + Radio Interface: 'lte' + Bandwidth: '20' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-serving-system.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-serving-system.txt new file mode 100644 index 00000000..b047d9cb --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-serving-system.txt @@ -0,0 +1,29 @@ +[/dev/cdc-wdm0] Successfully got serving system: + Registration state: 'registered' + CS: 'attached' + PS: 'attached' + Selected network: '3gpp' + Radio interfaces: '1' + [0]: 'lte' + Roaming status: 'off' + Data service capabilities: '1' + [0]: 'lte' + Current PLMN: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Roaming indicators: '1' + [0]: 'off' (lte) + 3GPP location area code: '65534' + 3GPP cell ID: '153667976' + Detailed status: + Status: 'available' + Capability: 'cs-ps' + HDR Status: 'none' + HDR Hybrid: 'no' + Forbidden: 'no' + LTE tracking area code: '14768' + Full operator code info: + MCC: '234' + MNC: '10' + MNC with PCS digit: 'no' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-signal-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-signal-info.txt new file mode 100644 index 00000000..2d0fbcc0 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/nas-get-signal-info.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got signal info +LTE: + RSSI: '-71 dBm' + RSRQ: '-11 dB' + RSRP: '-104 dBm' + SNR: '8.0 dB' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-get-card-status.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-get-card-status.txt new file mode 100644 index 00000000..07aedb5e --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-get-card-status.txt @@ -0,0 +1,37 @@ +[/dev/cdc-wdm0] Successfully got card status +Provisioning applications: + Primary GW: slot '1', application '1' + Primary 1X: session doesn't exist + Secondary GW: session doesn't exist + Secondary 1X: session doesn't exist +Slot [1]: + Card state: 'present' + UPIN state: 'not-initialized' + UPIN retries: '0' + UPUK retries: '0' + Application [1]: + Application type: 'usim (2)' + Application state: 'ready' + Application ID: + A0:00:00:00:87:10:02:FF:44:FF:FF:89:01:01:01:00 + Personalization state: 'ready' + UPIN replaces PIN1: 'no' + PIN1 state: 'disabled' + PIN1 retries: '3' + PUK1 retries: '10' + PIN2 state: 'enabled-not-verified' + PIN2 retries: '3' + PUK2 retries: '10' + Application [2]: + Application type: 'unknown (0)' + Application state: 'detected' + Application ID: + A0:00:00:00:63:50:4B:FF:34:FF:07:89:54:46:52:38 + Personalization state: 'unknown' + UPIN replaces PIN1: 'no' + PIN1 state: 'not-initialized' + PIN1 retries: '0' + PUK1 retries: '0' + PIN2 state: 'not-initialized' + PIN2 retries: '0' + PUK2 retries: '0' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-read-transparent-gid1.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-read-transparent-gid1.txt new file mode 100644 index 00000000..b0a6f712 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/disabled/qmicli/uim-read-transparent-gid1.txt @@ -0,0 +1,7 @@ +[/dev/cdc-wdm0] Successfully read information from the UIM: +Card result: + SW1: '0x90' + SW2: '0x00' +Read result: + 85:FF:FF:FF:FF:FF:47:45:4E:49:45:49:4E:20:20:20:20:20:20:20 + diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/location-status.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/location-status.json new file mode 100644 index 00000000..8db81530 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/location-status.json @@ -0,0 +1 @@ +error: modem not enabled yet diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/modem.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/modem.json new file mode 100644 index 00000000..401bef7c --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/modem.json @@ -0,0 +1,175 @@ +{ + "modem": { + "3gpp": { + "5gnr": { + "registration-settings": { + "drx-cycle": "--", + "mico-mode": "--" + } + }, + "enabled-locks": [], + "eps": { + "initial-bearer": { + "dbus-path": "--", + "settings": { + "apn": "--", + "ip-type": "--", + "password": "--", + "user": "--" + } + }, + "ue-mode-operation": "--" + }, + "imei": "867929068986514", + "operator-code": "--", + "operator-name": "--", + "packet-service-state": "--", + "pco": "--", + "registration-state": "--" + }, + "cdma": { + "activation-state": "--", + "cdma1x-registration-state": "--", + "esn": "--", + "evdo-registration-state": "--", + "meid": "--", + "nid": "--", + "sid": "--" + }, + "dbus-path": "/org/freedesktop/ModemManager1/Modem/5", + "generic": { + "access-technologies": [], + "bearers": [], + "carrier-configuration": "ROW_Generic_3GPP", + "carrier-configuration-revision": "0501081F", + "current-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "current-capabilities": [ + "gsm-umts, lte" + ], + "current-modes": "allowed: 2g, 3g, 4g; preferred: 4g", + "device": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "device-identifier": "b5bf9f66a67a0da6308a67fb858cdfef75d82565", + "drivers": [ + "option1", + "qmi_wwan" + ], + "equipment-identifier": "867929068986514", + "hardware-revision": "10000", + "manufacturer": "QUALCOMM INCORPORATED", + "model": "QUECTEL Mobile Broadband Module", + "own-numbers": [], + "physdev": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "plugin": "quectel", + "ports": [ + "cdc-wdm0 (qmi)", + "ttyUSB0 (ignored)", + "ttyUSB1 (gps)", + "ttyUSB2 (at)", + "ttyUSB3 (at)", + "wwan0 (net)" + ], + "power-state": "on", + "primary-port": "cdc-wdm0", + "primary-sim-slot": "1", + "revision": "EG25GGBR07A08M2G", + "signal-quality": { + "recent": "yes", + "value": "0" + }, + "sim": "--", + "sim-slots": [ + "/", + "/" + ], + "state": "failed", + "state-failed-reason": "sim-missing", + "supported-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "supported-capabilities": [ + "gsm-umts, lte" + ], + "supported-ip-families": [ + "ipv4", + "ipv6", + "ipv4v6" + ], + "supported-modes": [ + "allowed: 2g; preferred: none", + "allowed: 3g; preferred: none", + "allowed: 4g; preferred: none", + "allowed: 2g, 3g; preferred: 3g", + "allowed: 2g, 3g; preferred: 2g", + "allowed: 2g, 4g; preferred: 4g", + "allowed: 2g, 4g; preferred: 2g", + "allowed: 3g, 4g; preferred: 4g", + "allowed: 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 4g", + "allowed: 2g, 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 2g" + ], + "unlock-required": "--", + "unlock-retries": [] + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/signal.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/signal.json new file mode 100644 index 00000000..190b5bee --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/signal.json @@ -0,0 +1 @@ +error: modem has no extended signal capabilities diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/sim.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/sim.json new file mode 100644 index 00000000..2fbcda33 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/mmcli/sim.json @@ -0,0 +1 @@ +error: couldn't find SIM diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-home-network.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-home-network.txt new file mode 100644 index 00000000..77b9a738 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-home-network.txt @@ -0,0 +1 @@ +error: couldn't get home network: QMI protocol error (37): 'UimUninitialized' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-rf-band-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-rf-band-info.txt new file mode 100644 index 00000000..9439faa2 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-rf-band-info.txt @@ -0,0 +1,9 @@ +[/dev/cdc-wdm0] Successfully got RF band info +Band Information: + Radio Interface: 'umts' + Active Band Class: 'wcdma-900' + Active Channel: '3025' +Band Information (Extended): + Radio Interface: 'umts' + Active Band Class: 'wcdma-900' + Active Channel: '3025' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-serving-system.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-serving-system.txt new file mode 100644 index 00000000..11968fcf --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-serving-system.txt @@ -0,0 +1,26 @@ +[/dev/cdc-wdm0] Successfully got serving system: + Registration state: 'not-registered-searching' + CS: 'detached' + PS: 'detached' + Selected network: '3gpp' + Radio interfaces: '1' + [0]: 'umts' + Roaming status: 'on' + Data service capabilities: '0' + Current PLMN: + MCC: '234' + MNC: '10' + Description: '' + Roaming indicators: '1' + [0]: 'on' (umts) + Detailed status: + Status: 'limited' + Capability: 'cs-ps' + HDR Status: 'none' + HDR Hybrid: 'no' + Forbidden: 'no' + UMTS primary scrambling code: '460' + Full operator code info: + MCC: '234' + MNC: '10' + MNC with PCS digit: 'no' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-signal-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-signal-info.txt new file mode 100644 index 00000000..96b2ad32 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/nas-get-signal-info.txt @@ -0,0 +1,4 @@ +[/dev/cdc-wdm0] Successfully got signal info +WCDMA: + RSSI: '-80 dBm' + ECIO: '-8.5 dBm' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-get-card-status.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-get-card-status.txt new file mode 100644 index 00000000..ec5c84bc --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-get-card-status.txt @@ -0,0 +1,11 @@ +[/dev/cdc-wdm0] Successfully got card status +Provisioning applications: + Primary GW: session doesn't exist + Primary 1X: session doesn't exist + Secondary GW: session doesn't exist + Secondary 1X: session doesn't exist +Slot [1]: + Card state: 'error: no-atr-received (3)' + UPIN state: 'not-initialized' + UPIN retries: '0' + UPUK retries: '0' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-read-transparent-gid1.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-read-transparent-gid1.txt new file mode 100644 index 00000000..32dbd275 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/failed-no-sim/qmicli/uim-read-transparent-gid1.txt @@ -0,0 +1 @@ +error: couldn't read transparent file from the UIM: QMI protocol error (3): 'Internal' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/location-status.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/location-status.json new file mode 100644 index 00000000..43f70bc0 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/location-status.json @@ -0,0 +1,24 @@ +{ + "modem": { + "location": { + "capabilities": [ + "3gpp-lac-ci", + "gps-raw", + "gps-nmea", + "gps-unmanaged", + "agps-msa", + "agps-msb" + ], + "enabled": [ + "3gpp-lac-ci" + ], + "gps": { + "assistance": [], + "assistance-servers": [], + "refresh-rate": "30", + "supl-server": "--" + }, + "signals": "no" + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/modem.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/modem.json new file mode 100644 index 00000000..bbc20422 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/modem.json @@ -0,0 +1,184 @@ +{ + "modem": { + "3gpp": { + "5gnr": { + "registration-settings": { + "drx-cycle": "--", + "mico-mode": "--" + } + }, + "enabled-locks": [ + "fixed-dialing" + ], + "eps": { + "initial-bearer": { + "dbus-path": "/org/freedesktop/ModemManager1/Bearer/2", + "settings": { + "apn": "internet", + "ip-type": "ipv4", + "password": "--", + "user": "--" + } + }, + "ue-mode-operation": "csps-2" + }, + "imei": "867929068986514", + "operator-code": "23410", + "operator-name": "O2 - UK", + "packet-service-state": "attached", + "pco": "--", + "registration-state": "home" + }, + "cdma": { + "activation-state": "--", + "cdma1x-registration-state": "--", + "esn": "--", + "evdo-registration-state": "--", + "meid": "--", + "nid": "--", + "sid": "--" + }, + "dbus-path": "/org/freedesktop/ModemManager1/Modem/6", + "generic": { + "access-technologies": [ + "lte" + ], + "bearers": [], + "carrier-configuration": "ROW_Generic_3GPP", + "carrier-configuration-revision": "0501081F", + "current-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "current-capabilities": [ + "gsm-umts, lte" + ], + "current-modes": "allowed: 2g, 3g, 4g; preferred: 4g", + "device": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "device-identifier": "b5bf9f66a67a0da6308a67fb858cdfef75d82565", + "drivers": [ + "option1", + "qmi_wwan" + ], + "equipment-identifier": "867929068986514", + "hardware-revision": "10000", + "manufacturer": "QUALCOMM INCORPORATED", + "model": "QUECTEL Mobile Broadband Module", + "own-numbers": [], + "physdev": "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb2/2-2/2-2.2", + "plugin": "quectel", + "ports": [ + "cdc-wdm0 (qmi)", + "ttyUSB0 (ignored)", + "ttyUSB1 (gps)", + "ttyUSB2 (at)", + "ttyUSB3 (at)", + "wwan0 (net)" + ], + "power-state": "on", + "primary-port": "cdc-wdm0", + "primary-sim-slot": "1", + "revision": "EG25GGBR07A08M2G", + "signal-quality": { + "recent": "yes", + "value": "68" + }, + "sim": "/org/freedesktop/ModemManager1/SIM/2", + "sim-slots": [ + "/org/freedesktop/ModemManager1/SIM/2", + "/" + ], + "state": "registered", + "state-failed-reason": "--", + "supported-bands": [ + "egsm", + "dcs", + "pcs", + "g850", + "utran-1", + "utran-4", + "utran-6", + "utran-5", + "utran-8", + "utran-2", + "eutran-1", + "eutran-2", + "eutran-3", + "eutran-4", + "eutran-5", + "eutran-7", + "eutran-8", + "eutran-12", + "eutran-13", + "eutran-18", + "eutran-19", + "eutran-20", + "eutran-25", + "eutran-26", + "eutran-28", + "eutran-38", + "eutran-39", + "eutran-40", + "eutran-41", + "utran-19" + ], + "supported-capabilities": [ + "gsm-umts, lte" + ], + "supported-ip-families": [ + "ipv4", + "ipv6", + "ipv4v6" + ], + "supported-modes": [ + "allowed: 2g; preferred: none", + "allowed: 3g; preferred: none", + "allowed: 4g; preferred: none", + "allowed: 2g, 3g; preferred: 3g", + "allowed: 2g, 3g; preferred: 2g", + "allowed: 2g, 4g; preferred: 4g", + "allowed: 2g, 4g; preferred: 2g", + "allowed: 3g, 4g; preferred: 4g", + "allowed: 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 4g", + "allowed: 2g, 3g, 4g; preferred: 3g", + "allowed: 2g, 3g, 4g; preferred: 2g" + ], + "unlock-required": "sim-pin2", + "unlock-retries": [ + "sim-pin (3)", + "sim-puk (10)", + "sim-pin2 (3)", + "sim-puk2 (10)" + ] + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/signal.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/signal.json new file mode 100644 index 00000000..5d4fcadd --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/signal.json @@ -0,0 +1,48 @@ +{ + "modem": { + "signal": { + "5g": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "snr": "--" + }, + "cdma1x": { + "ecio": "--", + "error-rate": "--", + "rssi": "--" + }, + "evdo": { + "ecio": "--", + "error-rate": "--", + "io": "--", + "rssi": "--", + "sinr": "--" + }, + "gsm": { + "error-rate": "--", + "rssi": "--" + }, + "lte": { + "error-rate": "--", + "rsrp": "--", + "rsrq": "--", + "rssi": "--", + "snr": "--" + }, + "refresh": { + "rate": "0" + }, + "threshold": { + "error-rate": "no", + "rssi": "0" + }, + "umts": { + "ecio": "--", + "error-rate": "--", + "rscp": "--", + "rssi": "--" + } + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/sim.json b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/sim.json new file mode 100644 index 00000000..050a27d5 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/mmcli/sim.json @@ -0,0 +1,22 @@ +{ + "sim": { + "dbus-path": "/org/freedesktop/ModemManager1/SIM/2", + "properties": { + "active": "yes", + "eid": "--", + "emergency-numbers": [ + "999", + "00112" + ], + "esim-status": "--", + "gid1": "85FFFFFFFFFF47454E4945494E20202020202020", + "gid2": "FFFFFFFFFFFF2020202020202020202020202020", + "iccid": "8944110069073915392", + "imsi": "234103403359194", + "operator-code": "23410", + "operator-name": "--", + "removability": "--", + "sim-type": "--" + } + } +} diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-home-network.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-home-network.txt new file mode 100644 index 00000000..84351fca --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-home-network.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got home network: + Home network: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Network name source: se13 diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-rf-band-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-rf-band-info.txt new file mode 100644 index 00000000..6a1adfe6 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-rf-band-info.txt @@ -0,0 +1,12 @@ +[/dev/cdc-wdm0] Successfully got RF band info +Band Information: + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Band Information (Extended): + Radio Interface: 'lte' + Active Band Class: 'eutran-40' + Active Channel: '39250' +Bandwidth: + Radio Interface: 'lte' + Bandwidth: '20' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-serving-system.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-serving-system.txt new file mode 100644 index 00000000..b047d9cb --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-serving-system.txt @@ -0,0 +1,29 @@ +[/dev/cdc-wdm0] Successfully got serving system: + Registration state: 'registered' + CS: 'attached' + PS: 'attached' + Selected network: '3gpp' + Radio interfaces: '1' + [0]: 'lte' + Roaming status: 'off' + Data service capabilities: '1' + [0]: 'lte' + Current PLMN: + MCC: '234' + MNC: '10' + Description: 'O2 - UK' + Roaming indicators: '1' + [0]: 'off' (lte) + 3GPP location area code: '65534' + 3GPP cell ID: '153667976' + Detailed status: + Status: 'available' + Capability: 'cs-ps' + HDR Status: 'none' + HDR Hybrid: 'no' + Forbidden: 'no' + LTE tracking area code: '14768' + Full operator code info: + MCC: '234' + MNC: '10' + MNC with PCS digit: 'no' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-signal-info.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-signal-info.txt new file mode 100644 index 00000000..2cef5e91 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/nas-get-signal-info.txt @@ -0,0 +1,6 @@ +[/dev/cdc-wdm0] Successfully got signal info +LTE: + RSSI: '-71 dBm' + RSRQ: '-14 dB' + RSRP: '-102 dBm' + SNR: '8.2 dB' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-get-card-status.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-get-card-status.txt new file mode 100644 index 00000000..07aedb5e --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-get-card-status.txt @@ -0,0 +1,37 @@ +[/dev/cdc-wdm0] Successfully got card status +Provisioning applications: + Primary GW: slot '1', application '1' + Primary 1X: session doesn't exist + Secondary GW: session doesn't exist + Secondary 1X: session doesn't exist +Slot [1]: + Card state: 'present' + UPIN state: 'not-initialized' + UPIN retries: '0' + UPUK retries: '0' + Application [1]: + Application type: 'usim (2)' + Application state: 'ready' + Application ID: + A0:00:00:00:87:10:02:FF:44:FF:FF:89:01:01:01:00 + Personalization state: 'ready' + UPIN replaces PIN1: 'no' + PIN1 state: 'disabled' + PIN1 retries: '3' + PUK1 retries: '10' + PIN2 state: 'enabled-not-verified' + PIN2 retries: '3' + PUK2 retries: '10' + Application [2]: + Application type: 'unknown (0)' + Application state: 'detected' + Application ID: + A0:00:00:00:63:50:4B:FF:34:FF:07:89:54:46:52:38 + Personalization state: 'unknown' + UPIN replaces PIN1: 'no' + PIN1 state: 'not-initialized' + PIN1 retries: '0' + PUK1 retries: '0' + PIN2 state: 'not-initialized' + PIN2 retries: '0' + PUK2 retries: '0' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-read-transparent-gid1.txt b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-read-transparent-gid1.txt new file mode 100644 index 00000000..b0a6f712 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/state/registered/qmicli/uim-read-transparent-gid1.txt @@ -0,0 +1,7 @@ +[/dev/cdc-wdm0] Successfully read information from the UIM: +Card result: + SW1: '0x90' + SW2: '0x00' +Read result: + 85:FF:FF:FF:FF:FF:47:45:4E:49:45:49:4E:20:20:20:20:20:20:20 + diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..302ea84f --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +successfully disabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..355006be --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +successfully enabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..b29806cd --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/connected_modem_on_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +error: could not power on SIM: QMI protocol error (26): 'NoEffect' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..302ea84f --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +successfully disabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..355006be --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +successfully enabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..0b8e19c7 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_off_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power on diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..049e0b85 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +error: couldn't find modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..049e0b85 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +error: couldn't find modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..27c2c4ec --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/disabled_modem_on_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +error: could not power on SIM: QMI protocol error (3): 'Internal' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..8b922995 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +error: couldn't disable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..668b1288 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +error: couldn't enable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..0b8e19c7 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_off_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power on diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..8b922995 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +error: couldn't disable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..668b1288 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +error: couldn't enable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..27c2c4ec --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +error: could not power on SIM: QMI protocol error (3): 'Internal' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-disable.txt new file mode 100644 index 00000000..8b922995 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-disable.txt @@ -0,0 +1 @@ +error: couldn't disable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-enable.txt new file mode 100644 index 00000000..668b1288 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/mm-enable.txt @@ -0,0 +1 @@ +error: couldn't enable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-on.txt new file mode 100644 index 00000000..27c2c4ec --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/failed_modem_on_sim_inserted/sim-power-on.txt @@ -0,0 +1 @@ +error: could not power on SIM: QMI protocol error (3): 'Internal' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..049e0b85 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +error: couldn't find modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..049e0b85 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +error: couldn't find modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..0b8e19c7 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_off_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power on diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-disable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-disable.txt new file mode 100644 index 00000000..302ea84f --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-disable.txt @@ -0,0 +1 @@ +successfully disabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-enable.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-enable.txt new file mode 100644 index 00000000..355006be --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/mmcli/mm-enable.txt @@ -0,0 +1 @@ +successfully enabled the modem diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-off.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-off.txt new file mode 100644 index 00000000..8dc98eda --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-off.txt @@ -0,0 +1 @@ +[/dev/cdc-wdm0] Successfully performed SIM power off diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-on.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-on.txt new file mode 100644 index 00000000..b29806cd --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/registered_modem_on_sim/qmicli/sim-power-on.txt @@ -0,0 +1 @@ +error: could not power on SIM: QMI protocol error (26): 'NoEffect' diff --git a/sprint-docs/modem-research/command_outputs/eg25-new/transitions/sim_montior.txt b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/sim_montior.txt new file mode 100644 index 00000000..36b5b041 --- /dev/null +++ b/sprint-docs/modem-research/command_outputs/eg25-new/transitions/sim_montior.txt @@ -0,0 +1,20 @@ +[/dev/cdc-wdm0] Received slot status indication: + Physical slot 1: + Card status: present + Slot status: active + Logical slot: 1 + ICCID: 8944110069073915392 + Protocol: uicc + Num apps: 0 + Is eUICC: no + Physical slot 2: + Card status: absent + Slot status: inactive +[/dev/cdc-wdm0] Received slot status indication: + Physical slot 1: + Card status: absent + Slot status: active + Logical slot: 1 + Physical slot 2: + Card status: absent + Slot status: inactive diff --git a/sprint-docs/modem-research/notes/modem_behaviour.md b/sprint-docs/modem-research/notes/modem_behaviour.md new file mode 100644 index 00000000..4593b54d --- /dev/null +++ b/sprint-docs/modem-research/notes/modem_behaviour.md @@ -0,0 +1,229 @@ +# EG25-new modem behaviour (for dummy modem modelling) + +This document summarises observed behaviour of an EG25 (new FW) modem from the mmcli/qmicli captures under +`scratchpad/modem-research/command_outputs/eg25-new`. It is intended to be the behavioural spec for the +test dummy modem. + +## High-level modem states + +There are two layers of "state": + +- **mmcli modem state**: `modem.generic.state` + - `failed` (e.g. `state-failed-reason = sim-missing`) + - `disabled` + - `registered` + - `connected` + +- **mmcli monitor state** (`mmcli -m X -w`, not fully captured here but implied by ModemManager docs and + behaviour): + - `enabling → enabled → searching → registered` + - `registered → connecting → connected` + - `connected → disconnecting → registered` + - SIM removal can be represented as a synthetic `no_sim` state at the HAL level. + +The HAL modem driver mainly looks at: + +- `modem.generic.state` (for failed vs non-failed) +- `modem["3gpp"]["registration-state"]` (for registered vs not) +- SIM presence via `modem.generic.sim` (`"--"` vs a SIM D-Bus path) + +## Data availability by state (mmcli) + +All observations below are from the JSON snapshots under `eg25-new/state/*/mmcli`. + +### `failed` (no SIM) + +Example: `state/failed-no-sim/mmcli`. + +- `mmcli -J -m` (modem.json) + - Succeeds. + - `modem.generic.state = "failed"`. + - `modem.generic["state-failed-reason"] = "sim-missing"`. + - `modem.generic.sim = "--"` (no SIM path). + - `modem["3gpp"]["registration-state"] = "--"`. + +- `mmcli --signal-get` (signal.json) + - Fails, output is a plain error string, not JSON: + - `error: modem has no extended signal capabilities` + - The HAL driver never calls `get_signal()` in this state because it gates on `generic.sim ~= "--"`. + +- `mmcli -J -i` (SIM info) + - Not applicable; there is no SIM D-Bus path in `modem.generic.sim`. + +### `disabled` (SIM present, modem disabled) + +Example: `state/disabled/mmcli`. + +- `mmcli -J -m` + - Succeeds. + - `modem.generic.state = "disabled"`. + - `modem.generic.sim` is a SIM path (e.g. `/org/freedesktop/ModemManager1/SIM/2`). + - `modem["3gpp"]["registration-state"] = "--"`. + +- `mmcli --signal-get` (signal.json) + - Succeeds and returns JSON with full `modem.signal` structure. + - All access technologies (`5g`, `cdma1x`, `evdo`, `gsm`, `lte`, `umts`) are present, but all metrics + are `"--"` (no meaningful signal values while disabled). + +- `mmcli -J -i` (SIM info) + - Succeeds when called with the SIM path from `modem.generic.sim`. + +### `registered` + +Example: `state/registered/mmcli`. + +- `mmcli -J -m` + - Succeeds. + - `modem.generic.state = "registered"`. + - `modem.generic.sim` is a SIM path. + - `modem["3gpp"]["registration-state"] = "home"`. + - `modem["3gpp"].operator-code` and `.operator-name` are populated (e.g. `23410`, `"O2 - UK"`). + - `modem["3gpp"]["packet-service-state"] = "attached"`. + - `modem.generic["access-technologies"]` contains `"lte"`. + +- `mmcli --signal-get` (signal.json) + - Succeeds with the same JSON structure as in `disabled`. + - For this firmware, extended signal metrics are still all `"--"`; HAL falls back to QMI NAS for + real RSRP/RSRQ/SNR via `nas-get-signal-info`. + +- `mmcli -J -i` (SIM info) + - Succeeds and returns SIM details. + +### `connected` + +Example: `state/connected/mmcli`. + +- `mmcli -J -m` + - Succeeds. + - `modem.generic.state = "connected"`. + - `modem.generic.bearers` contains one or more bearer paths. + - `modem["3gpp"]["registration-state"] = "home"` and `packet-service-state = "attached"`. + - `modem.generic["access-technologies"]` still includes `"lte"`. + +- `mmcli --signal-get` (signal.json) + - Same structure as `disabled`/`registered` and still all `"--"` for extended metrics. + +## Command behaviour by state (mmcli) + +From the transition captures under `eg25-new/transitions`: + +- `mmcli -m X -e` / `-d` while **modem is in `failed` state**: + - Both enable and disable fail with `WrongState` errors, e.g.: + - `error: couldn't enable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state'` + - `error: couldn't disable the modem: 'GDBus.Error:org.freedesktop.ModemManager1.Error.Core.WrongState: modem in failed state'` + - Dummy modem should refuse to change state when `enable`/`disable` are called in `FAILED`. + +- `mmcli -m X -e` / `-d` in **normal states**: + - From `connected` → `mmcli -d` succeeds and drives state towards `disabled`. + - From `disabled` → `mmcli -e` will eventually drive the monitor state sequence + `enabling → enabled → searching → registered` (not all steps are captured here but observed in HAL). + +## Data availability by state (qmicli) + +Using captures under `eg25-new/state/*/qmicli`. + +### UIM card status (SIM presence) + +- **Disabled (SIM present)** – `state/disabled/qmicli/uim-get-card-status.txt`: + - Slot [1]: `Card state: 'present'`. + - Full application list is reported (`Application [1]` usim ready, etc.). + - This is the "healthy SIM inserted" baseline. + +- **Failed, no SIM** – `state/failed-no-sim/qmicli/uim-get-card-status.txt`: + - Slot [1]: `Card state: 'error: no-atr-received (3)'`. + - No applications are listed. + - This aligns with `modem.generic.state = "failed"` and `state-failed-reason = "sim-missing"`. + +### UIM read transparent (GID1) + +- In non-failed, SIM-present states, `uim-read-transparent` returns a `Card result` and `Read result` + section with a hex string for GID1. +- In `failed`/no-SIM, this command is expected to fail or return no meaningful `Read result`. + - The HAL only calls `uim_get_gids()` when `modem.generic.state ~= 'failed'`. + +### NAS info (home network + signal) + +- `nas-get-home-network`: + - Only meaningful when `modem["3gpp"]["registration-state"] ~= "--"` (i.e. registered/connected). + - Returns MCC/MNC and description; the HAL uses this for MCC/MNC and operator info. + +- `nas-get-signal-info`: + - Returns LTE `RSSI`, `RSRQ`, `RSRP`, `SNR` when attached/registered/connected. + - This is the primary source of usable signal metrics for EG25-new, given that + `mmcli --signal-get` lacks extended values. + +## SIM power / presence behaviour + +From experiments scripted in `collect_modem_transitions.sh` and the user observations: + +- **SIM power-cycle with SIM inserted** (`qmicli --uim-sim-power-off=1` then `--uim-sim-power-on=1`): + - Causes the modem to effectively **disappear and reappear** from ModemManager's point of view. + - In practice this looks like a remove/add cycle on the USB device and a new D-Bus modem path. + - A modem that was in `failed` will come back as a **fresh modem in `disabled` state** once the SIM + is powered back on. + - This is the primary recovery path out of `failed(sim-missing)`. + +- **SIM removal** (physical SIM pulled out): + - Results in loss of SIM and subsequent `sim-missing` behaviour. + - In practice, the modem is observed to disappear and reappear from ModemManager, returning in a + `failed` state with `state-failed-reason = "sim-missing"` and `generic.sim = "--"`. + +For dummy modelling this implies: + +- When SIM power is toggled **off then on while a SIM is inserted**: + - Emit a modem `(-)` removal event on the mmcli monitor bus, then a `(+)` add event with a **new** + modem address. + - Reset the modem state machine to `DISABLED` with no bearer/registration state. + - Clear any previous failure reason. + +- When a SIM is **removed**: + - Emit a modem `(-)` removal event, then a `(+)` add event with a (potentially) new modem address. + - Recreate the modem in `FAILED` state with `state-failed-reason = "sim-missing"` and + `generic.sim = "--"`. + +## Driver-facing behavioural rules (what the dummy must respect) + +Summarising what the HAL modem driver expects from the underlying tools: + +- `get_modem_info()` (mmcli -J -m): + - Always succeeds in all states and reports at least: + - `modem.generic.state` + - `modem["3gpp"]["registration-state"]` + - `modem.generic.sim` (SIM path or `"--"`) + - Driver/mode/model fields (plugin, model, revision, drivers, ports, primary-port, equipment-identifier). + +- `get_sim_info()` (mmcli -J -i): + - Only called when `modem.generic.sim ~= "--"`. + - Should fail (or not be callable) when there is no SIM path. + +- `get_signal()` (mmcli --signal-get): + - Only called when `modem.generic.sim ~= "--"`. + - May legitimately fail or return no valid signals; driver will then report an error and move on. + - In `failed`/no-SIM state, the dummy should mimic `mmcli` by returning a non-JSON error string so + any accidental call fails cleanly. + +- `get_nas_info()` / `nas_get_rf_band_info()` / `uim_get_gids()` (QMI): + - `get_nas_info()` is only called when `modem["3gpp"]["registration-state"] ~= "--"`. + - `uim_get_gids()` is only called when `modem.generic.state ~= 'failed'`. + - The dummy should enforce the same gating and either return errors or empty results when invoked + outside these conditions. + +- `enable()` / `disable()` (mmcli -e/-d): + - Must fail with a `WrongState`-style error when current state is `FAILED`. + - Otherwise should drive the state-machine transitions described above via the monitor stream. + +These rules, plus the state/command availability matrices above, are what the dummy modem should +implement to behave like a realistic EG25-new instance. + +## QMI slot monitor + +The HAL uses `qmicli --uim-monitor-slot-status` (wrapped by `monitor_slot_status()` and parsed by +`utils.parse_slot_monitor`) to detect SIM insertion/removal events at runtime. + +- The dummy modem will need to emit slot-monitor lines that `parse_slot_monitor` can interpret as + transitions between "present" and "not present". +- Capturing some real `--uim-monitor-slot-status` output for: + - SIM present, + - SIM removed, + - SIM power-cycled, + would be useful to ensure the dummy's slot-monitor stream matches reality. diff --git a/src/services/hal.lua b/src/services/hal.lua index d00fb938..a4d50f27 100644 --- a/src/services/hal.lua +++ b/src/services/hal.lua @@ -1,10 +1,7 @@ -local modem_manager = require "services.hal.managers.modemcard" -local ubus_manager = require "services.hal.managers.ubus" -local uci_manager = require "services.hal.managers.uci" -local wlan_managaer = require "services.hal.managers.wlan" local fiber = require "fibers.fiber" local queue = require "fibers.queue" local op = require "fibers.op" +local context = require "fibers.context" local service = require "service" local new_msg = require("bus").new_msg local log = require "services.log" @@ -37,6 +34,11 @@ function hal_service:_register_device(device, capabilities) "Device Event: capability '%s' for device '%s' with id '%s' does not have an id", cap_name, device.type, device.id )) + elseif not cap.control then + log.error(string.format( + "Device Event: capability '%s' for device '%s' with id '%s' does not have a control field", + cap_name, device.type, device.id + )) else if not self.capabilities[cap_name] then self.capabilities[cap_name] = {} @@ -222,7 +224,7 @@ function hal_service:_handle_capability_control(request) end) end -function hal_service:_handle_capbility_info(data) +function hal_service:_handle_capability_info(data) if not data then return end if not data.type then @@ -272,7 +274,7 @@ end function hal_service:_apply_config(msg) log.trace(string.format( - '%s - %s: Recieved new HAL config', + "%s - %s: Received new HAL config", self.ctx:value("service_name"), self.ctx:value("fiber_name") )) @@ -309,10 +311,19 @@ function hal_service:_apply_config(msg) if manager then manager:apply_config(manager_config) else - local manager_pkg = require('services.hal.managers.' .. manager_name) - self.managers[manager_name] = manager_pkg.new() - self.managers[manager_name]:spawn(self.ctx, self.conn, self.device_event_q, self.capability_info_q) - self.managers[manager_name]:apply_config(manager_config) + local ok, manager_pkg = pcall(require, 'services.hal.managers.' .. manager_name) + if ok and type(manager_pkg) == "table" then + self.managers[manager_name] = manager_pkg.new() + self.managers[manager_name]:spawn(context.with_cancel(self.ctx), self.conn, self.device_event_q, self.capability_info_q) + self.managers[manager_name]:apply_config(manager_config) + else + log.error(string.format( + '%s - %s: Failed to load manager "%s"', + self.ctx:value("service_name"), + self.ctx:value("fiber_name"), + manager_name + )) + end end end end @@ -347,7 +358,7 @@ function hal_service:_control_main(ctx) config_sub:next_msg_op():wrap(function(msg) self:_apply_config(msg) end), cap_ctrl_sub:next_msg_op():wrap(function(msg) self:_handle_capability_control(msg) end), self.device_event_q:get_op():wrap(function(msg) self:_handle_device_connection_event(msg) end), - self.capability_info_q:get_op():wrap(function(msg) self:_handle_capbility_info(msg) end), + self.capability_info_q:get_op():wrap(function(msg) self:_handle_capability_info(msg) end), ctx:done_op() ):perform() end diff --git a/src/services/hal/backends/at.lua b/src/services/hal/backends/at.lua new file mode 100644 index 00000000..df54eb8b --- /dev/null +++ b/src/services/hal/backends/at.lua @@ -0,0 +1,71 @@ +package.path = '/usr/lib/lua/?.lua;/usr/lib/lua/?/init.lua;' .. package.path + +local file = require 'fibers.stream.file' +local op = require 'fibers.op' + +local function trim(input) + -- Pattern matches non-printable characters and spaces at the start and end of the string + -- %c matches control characters, %s matches all whitespace characters + -- %z matches the character with representation 0x00 (NUL byte) + return (input:gsub("^[%c%s%z]+", ""):gsub("[%c%s%z]+$", "")) +end + +local function send_with_context(ctx, port, command) + local reader, err = file.open(port, "r") + if not reader then return nil, "error opening AT read port: "..err end + + local writer = assert(file.open(port, "w")) + if not writer then return nil, "error opening AT write port: "..err end + + -- file write + op.choice( + writer:write_chars_op(command..'\r'), + ctx:done_op() + ):perform() + + writer:close() + + if ctx:err() then reader:close() return nil, ctx:err() end + + local res = {} + + while true do + local line = op.choice( + reader:read_line_op(), + ctx:done_op() + ):perform() + + if ctx:err() then reader:close() return nil, ctx:err() end + if not line then reader:close() return nil, 'unknown error' end + + line = trim(line) + + -- check for non-descriptive success/fail + if line:find("^OK$") then + reader:close() + return res, nil + elseif line:find("^ERROR$") then + reader:close() + return res, 'error' + else + -- check for descriptive fail + local error_code + error_code = line:match("^%+CME ERROR: (%d+)$") + if error_code then + reader:close() + return res, error_code + end + error_code = line:match("^%+CMS ERROR: (%d+)$") + if error_code then + reader:close() + return res, error_code + end + end + + if #line > 0 then table.insert(res, line) end + end +end + +return { + send_with_context = send_with_context +} \ No newline at end of file diff --git a/src/services/hal/backends/iw.lua b/src/services/hal/backends/iw.lua new file mode 100644 index 00000000..f155a4b8 --- /dev/null +++ b/src/services/hal/backends/iw.lua @@ -0,0 +1,40 @@ +local exec = require "fibers.exec" +local utils = require "services.hal.drivers.wireless.utils" + +local function get_iw_dev_info(ctx, interface) + local out, err = exec.command_context(ctx, "iw", "dev", interface, "info"):output() + if err then + return nil, err + end + + return utils.format_iw_dev_info(out) +end + +local function get_iw_event_stream(ctx) + return exec.command_context(ctx, "iw", "event") +end + +local function get_client_info(ctx, interface, mac) + local out, err = exec.command_context(ctx, "iw", "dev", interface, "station", "get", mac):output() + if err then + return nil, err + end + + return utils.format_iw_client_info(out) +end + +local function get_dev_noise(ctx, interface) + local out, err = exec.command_context(ctx, "iw", interface, "survey", "dump"):output() + if err then + return nil, err + end + + return utils.parse_dev_noise(out) +end + +return { + get_iw_dev_info = get_iw_dev_info, + get_iw_event_stream = get_iw_event_stream, + get_client_info = get_client_info, + get_dev_noise = get_dev_noise +} diff --git a/src/services/hal/backends/mmcli.lua b/src/services/hal/backends/mmcli.lua new file mode 100644 index 00000000..efe67ba9 --- /dev/null +++ b/src/services/hal/backends/mmcli.lua @@ -0,0 +1,150 @@ +local exec = require "fibers.exec" + +local backend = {} + +function backend.monitor_modems() + return exec.command('mmcli', '-M') +end + +function backend.inhibit(device) + return exec.command('mmcli', '-m', device, '--inhibit') +end + +function backend.connect(ctx, device, connection_string) + connection_string = string.format("--simple-connect=%s", connection_string) + return exec.command_context(ctx, 'mmcli', '-m', device, connection_string) +end + +function backend.disconnect(ctx, device) + return exec.command_context(ctx, 'mmcli', '-m', device, '--simple-disconnect') +end + +function backend.reset(ctx, device) + return exec.command_context(ctx, 'mmcli', '-m', device, '-r') +end + +function backend.enable(ctx, device) + return exec.command_context(ctx, 'mmcli', '-m', device, '-e') +end + +function backend.disable(ctx, device) + return exec.command_context(ctx, 'mmcli', '-m', device, '-d') +end + +function backend.monitor_state(device) + return exec.command('mmcli', '-m', device, '-w') +end + +function backend.information(ctx, device) + return exec.command_context(ctx, 'mmcli', '-J', '-m', device) +end + +function backend.sim_information(ctx, device) + return exec.command_context(ctx, 'mmcli', '-J', '-i', device) +end +function backend.location_status(ctx, device) + return exec.command_context(ctx, 'mmcli', '-J', '-m', device, '--location-status') +end + +function backend.signal_setup(ctx, device, rate) + return exec.command_context(ctx, 'mmcli', '-m', device, '--signal-setup=' .. tostring(rate)) +end + +function backend.signal_get(ctx, device) + return exec.command_context(ctx, 'mmcli', '-J', '-m', device, '--signal-get') +end + +function backend.three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) + local settings_string = string.format("--3gpp-set-initial-eps-bearer-settings=%s", settings) + return exec.command_context(ctx, 'mmcli', '-m', device, settings_string) +end + +local function monitor_modems() + return backend.monitor_modems() +end + +local function inhibit(ctx, device) + return backend.inhibit(device) +end + +local function connect(ctx, device, connection_string) + return backend.connect(ctx, device, connection_string) +end + +local function disconnect(ctx, device) + return backend.disconnect(ctx, device) +end + +local function reset(ctx, device) + return backend.reset(ctx, device) +end + +local function enable(ctx, device) + return backend.enable(ctx, device) +end + +local function disable(ctx, device) + return backend.disable(ctx, device) +end + +local function monitor_state(device) + return backend.monitor_state(device) +end + +local function information(ctx, device) + return backend.information(ctx, device) +end + +local function sim_information(ctx, device) + return backend.sim_information(ctx, device) +end + +local function location_status(ctx, device) + return backend.location_status(ctx, device) +end + +local function signal_setup(ctx, device, rate) + return backend.signal_setup(ctx, device, rate) +end + +local function signal_get(ctx, device) + return backend.signal_get(ctx, device) +end + +local function three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) + return backend.three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) +end + +local function use_backend(new_backend) + if not new_backend then + return "No backend provided" + end + for name, _ in pairs(backend) do + if not new_backend[name] then + return "New backend does not implement function: " .. name + end + end + backend = new_backend +end + +local mmcli_package = { + monitor_modems = monitor_modems, + inhibit = inhibit, + connect = connect, + disconnect = disconnect, + reset = reset, + enable = enable, + disable = disable, + monitor_state = monitor_state, + information = information, + sim_information = sim_information, + location_status = location_status, + signal_setup = signal_setup, + signal_get = signal_get, + three_gpp_set_initial_eps_bearer_settings = three_gpp_set_initial_eps_bearer_settings, + use_backend = use_backend -- function to swap out backend implementations +} + +package.loaded['services.hal.drivers.modem.mmcli'] = mmcli_package -- singleton + +return mmcli_package diff --git a/src/services/hal/backends/qmicli.lua b/src/services/hal/backends/qmicli.lua new file mode 100644 index 00000000..490485ca --- /dev/null +++ b/src/services/hal/backends/qmicli.lua @@ -0,0 +1,107 @@ +local exec = require "fibers.exec" + +-- Default backend implementation using qmicli commands +local backend = {} + +function backend.uim_get_card_status(ctx, port) + return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-get-card-status") +end + +function backend.uim_sim_power_off(ctx, port) + return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-sim-power-off=1") +end + +function backend.uim_sim_power_on(ctx, port) + return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-sim-power-on=1") +end + +function backend.uim_monitor_slot_status(port) + return exec.command('qmicli', '-p', '-d', port, '--uim-monitor-slot-status') +end + +function backend.uim_read_transparent(ctx, port, address_string) + local addresses = string.format('--uim-read-transparent=%s', address_string) + return exec.command_context(ctx, 'qmicli', '-p', '-d', port, addresses) +end + +function backend.nas_get_rf_band_info(ctx, port) + return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-rf-band-info') +end + +function backend.nas_get_home_network(ctx, port) + return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-home-network') +end + +function backend.nas_get_serving_system(ctx, port) + return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-serving-system') +end + +function backend.nas_get_signal_info(ctx, port) + return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-signal-info') +end + +local function uim_get_card_status(ctx, port) + return backend.uim_get_card_status(ctx, port) +end + +local function uim_sim_power_off(ctx, port) + return backend.uim_sim_power_off(ctx, port) +end + +local function uim_sim_power_on(ctx, port) + return backend.uim_sim_power_on(ctx, port) +end + +local function uim_monitor_slot_status(port) + return backend.uim_monitor_slot_status(port) +end + +local function uim_read_transparent(ctx, port, address_string) + return backend.uim_read_transparent(ctx, port, address_string) +end + +local function nas_get_rf_band_info(ctx, port) + return backend.nas_get_rf_band_info(ctx, port) +end + +local function nas_get_home_network(ctx, port) + return backend.nas_get_home_network(ctx, port) +end + +local function nas_get_serving_system(ctx, port) + return backend.nas_get_serving_system(ctx, port) +end + +local function nas_get_signal_info(ctx, port) + return backend.nas_get_signal_info(ctx, port) +end + +local function use_backend(new_backend) + if not new_backend then + return "No backend provided" + end + for name, _ in pairs(backend) do + if not new_backend[name] then + return "New backend does not implement function: " .. name + end + end + backend = new_backend +end + +local qmicli_package = { + uim_get_card_status = uim_get_card_status, + uim_sim_power_off = uim_sim_power_off, + uim_sim_power_on = uim_sim_power_on, + uim_monitor_slot_status = uim_monitor_slot_status, + uim_read_transparent = uim_read_transparent, + + nas_get_rf_band_info = nas_get_rf_band_info, + nas_get_home_network = nas_get_home_network, + nas_get_serving_system = nas_get_serving_system, + nas_get_signal_info = nas_get_signal_info, + + use_backend = use_backend -- function to swap out backend implementations +} + +package.loaded['services.hal.drivers.modem.qmicli'] = qmicli_package -- singleton +return qmicli_package diff --git a/src/services/hal/backends/ubus.lua b/src/services/hal/backends/ubus.lua new file mode 100644 index 00000000..d97fb125 --- /dev/null +++ b/src/services/hal/backends/ubus.lua @@ -0,0 +1,42 @@ +local exec = require "fibers.exec" +local cjson = require "cjson.safe" + +local Ubus = {} + +-- Context-free variants +function Ubus.list(...) + return exec.command('ubus', 'list', ...) +end + +function Ubus.call(path, method, ...) + return exec.command('ubus', 'call', path, method, ...) +end + +function Ubus.listen(...) + return exec.command('ubus', 'listen', ...) +end + +function Ubus.send(type, message) + local encoded_message = cjson.encode(message) + return exec.command('ubus', 'send', type, encoded_message) +end + +-- Context-aware variants +function Ubus.list_with_context(ctx, ...) + return exec.command_context(ctx, 'ubus', 'list', ...) +end + +function Ubus.call_with_context(ctx, path, method, ...) + return exec.command_context(ctx, 'ubus', 'call', path, method, ...) +end + +function Ubus.listen_with_context(ctx, ...) + return exec.command_context(ctx, 'ubus', 'listen', ...) +end + +function Ubus.send_with_context(ctx, type, message) + local encoded_message = cjson.encode(message) + return exec.command_context(ctx, 'ubus', 'send', type, encoded_message) +end + +return Ubus diff --git a/src/services/hal/backends/uci.lua b/src/services/hal/backends/uci.lua new file mode 100644 index 00000000..1fe13d8a --- /dev/null +++ b/src/services/hal/backends/uci.lua @@ -0,0 +1,202 @@ +local queue = require "fibers.queue" +local channel = require "fibers.channel" +local fibers = require "fibers" +local uci_mod = require "uci" + +local Q_SIZE = 10 + +---@class UCI +---@field commit_q Queue +---@field cursor unknown +local UCI = { + commit_q = queue.new(Q_SIZE), + cursor = uci_mod.cursor() +} + +---@class Session +---@field _changes table[] +local Session = {} +Session.__index = Session + +--- Create a new UCI session +--- @return Session +function Session.new() + local self = setmetatable({}, Session) + self._changes = {} + self._commited = false + return self +end + +--- Get a value from the UCI configuration +--- @param config string +--- @param section string +--- @param option string +--- @return any value +--- @return string? error +function Session:get(config, section, option) + return UCI.get(config, section, option) +end + +--- Set a value in the UCI configuration +--- @param config string +--- @param section string +--- @param option string +--- @param value any +--- @return string? error +function Session:set(config, section, option, value) + if self._commited then + return "Cannot modify a committed session" + end + if config == nil or section == nil or option == nil then + return "Invalid arguments for UCI set operation" + end + local change + if value == nil then + change = {command = "set_section", config = config, section_name = section, section_type = option} + else + change = {command = "set_value", config = config, section = section, option = option, value = value} + end + table.insert(self._changes, change) +end + +function Session:delete(config, section, option) + if self._commited then + return "Cannot modify a committed session" + end + if config == nil or section == nil then + return "Invalid arguments for UCI delete operation" + end + local change + if option == nil then + change = {command = "delete_section", config = config, section = section} + else + change = {command = "delete_option", config = config, section = section, option = option} + end + table.insert(self._changes, change) +end + +function Session:add(config, section_type) + if self._commited then + return "Cannot modify a committed session" + end + if config == nil or section_type == nil then + return nil, "Invalid arguments for UCI add operation" + end + local change = {command = "add_section", config = config, section_type = section_type} + table.insert(self._changes, change) +end + +function Session:foreach(config, section_type, map_func) + if self._commited then + return "Cannot modify a committed session" + end + if config == nil or section_type == nil or map_func == nil then + return "Invalid arguments for UCI foreach operation" + end + if type(map_func) ~= "function" then + return "map_func must be a function" + end + table.insert(self._changes, {command = "foreach", config = config, section_type = section_type, map_func = map_func}) +end + +function Session:commit() + if self._commited then + return "Session has already been committed" + end + local reply_ch = channel.new() + UCI.commit_q:put({ changes = self._changes, reply_ch = reply_ch }) + self._changes = {} + local reply = reply_ch:get() + if not reply then + return nil, "No reply received for UCI commit" + end + self._commited = true + return reply.success, reply.err +end + +-- Create a new UCI session +--- @return Session +function UCI.new_session() + return Session.new() +end + +--- Get a value from the UCI configuration +--- @param config string +--- @param section string +--- @param option string +--- @return any value +--- @return string? error +function UCI.get(config, section, option) + return UCI.cursor:get(config, section, option) +end + +--- A switch-case utility function +--- @param key any +--- @return fun(cases: table): ... +local function switch(key) + return function(cases) + local func = cases[key] + if func then + return func() + else + if cases["default"] then + return cases["default"]() + else + error("No case matched and no default case provided") + end + end + end +end + +function UCI.reactor() + local this_is_where_a_scope_check_would_go = true + while this_is_where_a_scope_check_would_go do + local commit = UCI.commit_q:get() + + local ret, err + for _, change in ipairs(commit.changes) do + ret, err = switch(change.command) { + set_value = function() + local success = UCI.cursor:set(change.config, change.section, change.option, change.value) + return success, success == false and "Failed to set value" or nil + end, + set_section = function() + local success = UCI.cursor:set(change.config, change.section_name, change.section_type) + return success, success == false and "Failed to set section" or nil + end, + delete_option = function() + local success = UCI.cursor:delete(change.config, change.section, change.option) + return success, success == false and "Failed to delete option" or nil + end, + delete_section = function() + local success = UCI.cursor:delete(change.config, change.section) + return success, success == false and "Failed to delete section" or nil + end, + add_section = function() + local name = UCI.cursor:add(change.config, change.section_type) + return name, name == nil and "Failed to add section" or nil + end, + foreach = function() + local success = UCI.cursor:foreach(change.config, change.section_type, change.map_func) + return success, success == false and "Failed to foreach" or nil + end, + default = function() + return nil, "Unknown UCI command: " .. tostring(change.command) + end + } + + if err then + commit.reply_ch:put({ success = ret, err = err }) + break + end + end + commit.reply_ch:put({ success = ret, err = nil }) + end +end + + + +fibers.spawn(UCI.reactor) + +return UCI + diff --git a/src/services/hal/drivers/modem/mmcli.lua b/src/services/hal/drivers/modem/mmcli.lua index f127961f..efe67ba9 100644 --- a/src/services/hal/drivers/modem/mmcli.lua +++ b/src/services/hal/drivers/modem/mmcli.lua @@ -1,63 +1,133 @@ local exec = require "fibers.exec" -local function monitor_modems() +local backend = {} + +function backend.monitor_modems() return exec.command('mmcli', '-M') end -local function inhibit(device) +function backend.inhibit(device) return exec.command('mmcli', '-m', device, '--inhibit') end -local function connect(ctx, device, connection_string) +function backend.connect(ctx, device, connection_string) connection_string = string.format("--simple-connect=%s", connection_string) return exec.command_context(ctx, 'mmcli', '-m', device, connection_string) end -local function disconnect(ctx, device) +function backend.disconnect(ctx, device) return exec.command_context(ctx, 'mmcli', '-m', device, '--simple-disconnect') end -local function reset(ctx, device) +function backend.reset(ctx, device) return exec.command_context(ctx, 'mmcli', '-m', device, '-r') end -local function enable(ctx, device) +function backend.enable(ctx, device) return exec.command_context(ctx, 'mmcli', '-m', device, '-e') end -local function disable(ctx, device) +function backend.disable(ctx, device) return exec.command_context(ctx, 'mmcli', '-m', device, '-d') end -local function monitor_state(device) +function backend.monitor_state(device) return exec.command('mmcli', '-m', device, '-w') end -local function information(ctx, device) +function backend.information(ctx, device) return exec.command_context(ctx, 'mmcli', '-J', '-m', device) end -local function sim_information(ctx, device) +function backend.sim_information(ctx, device) return exec.command_context(ctx, 'mmcli', '-J', '-i', device) end -local function location_status(ctx, device) +function backend.location_status(ctx, device) return exec.command_context(ctx, 'mmcli', '-J', '-m', device, '--location-status') end -local function signal_setup(ctx, device, rate) +function backend.signal_setup(ctx, device, rate) return exec.command_context(ctx, 'mmcli', '-m', device, '--signal-setup=' .. tostring(rate)) end -local function signal_get(ctx, device) +function backend.signal_get(ctx, device) return exec.command_context(ctx, 'mmcli', '-J', '-m', device, '--signal-get') end -local function three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) +function backend.three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) local settings_string = string.format("--3gpp-set-initial-eps-bearer-settings=%s", settings) return exec.command_context(ctx, 'mmcli', '-m', device, settings_string) end -return { +local function monitor_modems() + return backend.monitor_modems() +end + +local function inhibit(ctx, device) + return backend.inhibit(device) +end + +local function connect(ctx, device, connection_string) + return backend.connect(ctx, device, connection_string) +end + +local function disconnect(ctx, device) + return backend.disconnect(ctx, device) +end + +local function reset(ctx, device) + return backend.reset(ctx, device) +end + +local function enable(ctx, device) + return backend.enable(ctx, device) +end + +local function disable(ctx, device) + return backend.disable(ctx, device) +end + +local function monitor_state(device) + return backend.monitor_state(device) +end + +local function information(ctx, device) + return backend.information(ctx, device) +end + +local function sim_information(ctx, device) + return backend.sim_information(ctx, device) +end + +local function location_status(ctx, device) + return backend.location_status(ctx, device) +end + +local function signal_setup(ctx, device, rate) + return backend.signal_setup(ctx, device, rate) +end + +local function signal_get(ctx, device) + return backend.signal_get(ctx, device) +end + +local function three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) + return backend.three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) +end + +local function use_backend(new_backend) + if not new_backend then + return "No backend provided" + end + for name, _ in pairs(backend) do + if not new_backend[name] then + return "New backend does not implement function: " .. name + end + end + backend = new_backend +end + +local mmcli_package = { monitor_modems = monitor_modems, inhibit = inhibit, connect = connect, @@ -72,4 +142,9 @@ return { signal_setup = signal_setup, signal_get = signal_get, three_gpp_set_initial_eps_bearer_settings = three_gpp_set_initial_eps_bearer_settings, + use_backend = use_backend -- function to swap out backend implementations } + +package.loaded['services.hal.drivers.modem.mmcli'] = mmcli_package -- singleton + +return mmcli_package diff --git a/src/services/hal/drivers/modem/qmicli.lua b/src/services/hal/drivers/modem/qmicli.lua index a27405df..490485ca 100644 --- a/src/services/hal/drivers/modem/qmicli.lua +++ b/src/services/hal/drivers/modem/qmicli.lua @@ -1,40 +1,94 @@ local exec = require "fibers.exec" -local function uim_get_card_status(ctx, port) +-- Default backend implementation using qmicli commands +local backend = {} + +function backend.uim_get_card_status(ctx, port) return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-get-card-status") end -local function uim_sim_power_off(ctx, port) +function backend.uim_sim_power_off(ctx, port) return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-sim-power-off=1") end -local function uim_sim_power_on(ctx, port) +function backend.uim_sim_power_on(ctx, port) return exec.command_context(ctx, "qmicli", "-p", "-d", port, "--uim-sim-power-on=1") end -local function uim_monitor_slot_status(port) +function backend.uim_monitor_slot_status(port) return exec.command('qmicli', '-p', '-d', port, '--uim-monitor-slot-status') end -local function uim_read_transparent(ctx, port, address_string) +function backend.uim_read_transparent(ctx, port, address_string) local addresses = string.format('--uim-read-transparent=%s', address_string) return exec.command_context(ctx, 'qmicli', '-p', '-d', port, addresses) end -local function nas_get_rf_band_info(ctx, port) +function backend.nas_get_rf_band_info(ctx, port) return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-rf-band-info') end -local function nas_get_home_network(ctx, port) + +function backend.nas_get_home_network(ctx, port) return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-home-network') end -local function nas_get_serving_system(ctx, port) +function backend.nas_get_serving_system(ctx, port) return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-serving-system') end -local function nas_get_signal_info(ctx, port) + +function backend.nas_get_signal_info(ctx, port) return exec.command_context(ctx, 'qmicli', '-p', '-d', port, '--nas-get-signal-info') end -return { + +local function uim_get_card_status(ctx, port) + return backend.uim_get_card_status(ctx, port) +end + +local function uim_sim_power_off(ctx, port) + return backend.uim_sim_power_off(ctx, port) +end + +local function uim_sim_power_on(ctx, port) + return backend.uim_sim_power_on(ctx, port) +end + +local function uim_monitor_slot_status(port) + return backend.uim_monitor_slot_status(port) +end + +local function uim_read_transparent(ctx, port, address_string) + return backend.uim_read_transparent(ctx, port, address_string) +end + +local function nas_get_rf_band_info(ctx, port) + return backend.nas_get_rf_band_info(ctx, port) +end + +local function nas_get_home_network(ctx, port) + return backend.nas_get_home_network(ctx, port) +end + +local function nas_get_serving_system(ctx, port) + return backend.nas_get_serving_system(ctx, port) +end + +local function nas_get_signal_info(ctx, port) + return backend.nas_get_signal_info(ctx, port) +end + +local function use_backend(new_backend) + if not new_backend then + return "No backend provided" + end + for name, _ in pairs(backend) do + if not new_backend[name] then + return "New backend does not implement function: " .. name + end + end + backend = new_backend +end + +local qmicli_package = { uim_get_card_status = uim_get_card_status, uim_sim_power_off = uim_sim_power_off, uim_sim_power_on = uim_sim_power_on, @@ -44,5 +98,10 @@ return { nas_get_rf_band_info = nas_get_rf_band_info, nas_get_home_network = nas_get_home_network, nas_get_serving_system = nas_get_serving_system, - nas_get_signal_info = nas_get_signal_info + nas_get_signal_info = nas_get_signal_info, + + use_backend = use_backend -- function to swap out backend implementations } + +package.loaded['services.hal.drivers.modem.qmicli'] = qmicli_package -- singleton +return qmicli_package diff --git a/src/services/hal/types/core.lua b/src/services/hal/types/core.lua new file mode 100644 index 00000000..40df68a1 --- /dev/null +++ b/src/services/hal/types/core.lua @@ -0,0 +1,249 @@ +---@alias DeviceId string|integer +---@alias DeviceType string +---@alias CapabilityType string +---@alias CapabilityId string|integer +---@alias PublishMethod string +---@alias TopicEntry string|integer +---@alias SubTopic TopicEntry[] +---@alias Metadata table +---@alias Info table + +---@class Capability +---@field command_q Queue +---@field control_list string[] +---@field id CapabilityId + + +---@class DeviceConnectedEvent +---@field connected boolean +---@field type DeviceType +---@field id DeviceId +---@field data Metadata +---@field capabilities Capability[] +local DeviceConnectedEvent = {} +DeviceConnectedEvent.__index = DeviceConnectedEvent + +--- Build a new DeviceConnectedEvent +---@param dev_type DeviceType +---@param id_field string +---@param data Metadata +---@param capabilities Capability[] +---@return DeviceConnectedEvent? +---@return string? error +function DeviceConnectedEvent.new(dev_type, id_field, data, capabilities) + if dev_type == nil then + return nil, "dev_type must be provided" + end + if id_field == nil then + return nil, "id_field must be provided" + end + if data == nil then + return nil, "data must be provided" + end + if data[id_field] == nil then + return nil, "data must contain the id_field" + end + if type(capabilities) ~= "table" then + return nil, "capabilities must be a table" + end + local self = setmetatable({ + connected = true, + type = dev_type, + id = id_field, + data = data, + capabilities = capabilities + }, DeviceConnectedEvent) + return self, nil +end + +---@class DeviceDisconnectedEvent +---@field connected boolean +---@field type DeviceType +---@field id DeviceId +---@field data Metadata +local DeviceDisconnectedEvent = {} +DeviceDisconnectedEvent.__index = DeviceDisconnectedEvent + +--- Build a new DeviceDisconnectedEvent +---@param dev_type DeviceType +---@param id_field string +---@param data Metadata +---@return DeviceDisconnectedEvent? +---@return string? error +function DeviceDisconnectedEvent.new(dev_type, id_field, data) + if dev_type == nil then + return nil, "dev_type must be provided" + end + if id_field == nil then + return nil, "id_field must be provided" + end + if data == nil then + return nil, "data must be provided" + end + if data[id_field] == nil then + return nil, "data must contain the id_field" + end + local self = setmetatable({ + connected = false, + type = dev_type, + id = id_field, + data = data + }, DeviceDisconnectedEvent) + return self, nil +end + +---@alias DeviceConnectionEvent DeviceConnectedEvent|DeviceDisconnectedEvent + +---@class DeviceEvent +---@field connected boolean +---@field type DeviceType +---@field index DeviceId +---@field identity any +---@field metadata Metadata +local DeviceEvent = {} +DeviceEvent.__index = DeviceEvent + +--- Build a new DeviceEvent +---@param connected boolean +---@param dev_type DeviceType +---@param index DeviceId +---@param identity any +---@param metadata Metadata +---@return DeviceEvent? +---@return string? error +function DeviceEvent.new(connected, dev_type, index, identity, metadata) + if type(connected) ~= "boolean" then + return nil, "connected must be a boolean" + end + if dev_type == nil then + return nil, "dev_type must be provided" + end + if index == nil then + return nil, "index must be provided" + end + if identity == nil then + return nil, "identity must be provided" + end + if metadata == nil then + return nil, "metadata must be provided" + end + local self = setmetatable({ + connected = connected, + type = dev_type, + index = index, + identity = identity, + metadata = metadata + }, DeviceEvent) + return self, nil +end + +---@class CapabilityDevice +---@field type DeviceType +---@field id DeviceId + +---@class CapabilityEvent +---@field connected boolean +---@field type CapabilityType +---@field index CapabilityId +---@field device CapabilityDevice +local CapabilityEvent = {} +CapabilityEvent.__index = CapabilityEvent + +--- Build a new CapabilityEvent +---@param connected boolean +---@param cap_type CapabilityType +---@param index CapabilityId +---@param dev_type DeviceType +---@param device_id DeviceId +---@return CapabilityEvent? +---@return string? error +function CapabilityEvent.new(connected, cap_type, index, dev_type, device_id) + if type(connected) ~= "boolean" then + return nil, "connected must be a boolean" + end + if cap_type == nil then + return nil, "cap_type must be provided" + end + if index == nil then + return nil, "index must be provided" + end + if dev_type == nil then + return nil, "dev_type must be provided" + end + if device_id == nil then + return nil, "device_id must be provided" + end + local self = setmetatable({ + connected = connected, + type = cap_type, + index = index, + device = { type = dev_type, id = device_id } + }, CapabilityEvent) + return self, nil +end + +---@class InfoEvent +---@field type CapabilityType +---@field index CapabilityId +---@field sub_topic SubTopic +---@field publish_method PublishMethod +---@field info Info +local InfoEvent = {} +InfoEvent.__index = InfoEvent + +--- Build a new InfoEvent +---@param cap_type CapabilityType +---@param index CapabilityId +---@param sub_topic SubTopic +---@param publish_method PublishMethod +---@param info Info +---@return InfoEvent? +---@return string? error +function InfoEvent.new(cap_type, index, sub_topic, publish_method, info) + if cap_type == nil then + return nil, "cap_type must be provided" + end + if index == nil then + return nil, "index must be provided" + end + if type(sub_topic) ~= "table" then + return nil, "sub_topic must be a table" + end + if publish_method == nil then + return nil, "publish_method must be provided" + end + local self = setmetatable({ + type = cap_type, + index = index, + sub_topic = sub_topic, + publish_method = publish_method, + info = info + }, InfoEvent) + return self, nil +end + +---@class Reply +---@field result any +---@field error any +local Reply = {} +Reply.__index = Reply + +--- Build a new Reply +---@param result any +---@param error any +---@return Reply +function Reply.new(result, error) + return setmetatable({ + result = result, + error = error + }, Reply) +end + +return { + DeviceConnectedEvent = DeviceConnectedEvent, + DeviceDisconnectedEvent = DeviceDisconnectedEvent, + DeviceEvent = DeviceEvent, + CapabilityEvent = CapabilityEvent, + InfoEvent = InfoEvent, + Reply = Reply +} diff --git a/tests/hal/harness.lua b/tests/hal/harness.lua new file mode 100644 index 00000000..003e1830 --- /dev/null +++ b/tests/hal/harness.lua @@ -0,0 +1,121 @@ +local fiber = require 'fibers.fiber' +local context = require 'fibers.context' +local sleep = require 'fibers.sleep' + +local harness = {} + +-- Default maximum number of cooperative "ticks" to wait +-- before treating a wait as a timeout. +local DEFAULT_MAX_TICKS = 20 + +-- Environment helpers ------------------------------------------------------- + +function harness.get_env_variables() + local bg_ctx = context.background() + + local ctx = context.with_cancel( + context.with_value(bg_ctx, 'service_name', 'hal') + ) + + local bus = require 'bus' + + -- Force reload to reset state between tests + package.loaded['services.hal'] = nil + package.loaded['services.hal.managers.dummy'] = nil + local hal = require 'services.hal' + return hal, ctx, bus.new(), bus.new_msg +end + +function harness.config_path() + return { 'config', 'hal' } +end + +function harness.new_hal_env() + local hal, ctx, bus, new_msg = harness.get_env_variables() + local conn = bus:connect() + return hal, ctx, bus, conn, new_msg +end + +function harness.publish_config(conn, new_msg, payload) + conn:publish(new_msg(harness.config_path(), payload, { retained = true })) +end + +-- Tick-based waiting helpers ----------------------------------------------- + +-- Internal helper to build an alt function for perform_alt that: +-- - increments a tick counter +-- - yields the current fiber +-- - enforces a max tick budget +-- - bails out if the context is cancelled +local function make_alt_wait(ctx, max_ticks) + local ticks = 0 + max_ticks = max_ticks or DEFAULT_MAX_TICKS + + return function() + if ctx and ctx:err() then + return nil, 'context cancelled' + end + + ticks = ticks + 1 + if ticks > max_ticks then + return nil, 'timeout' + end + + sleep.sleep(0) -- yields + -- Special error sentinel used by wait helpers to + -- distinguish an alt-path from a real error. + return nil, '__ALT__' + end +end + +-- Wait for a message on a subscriber using a non-blocking choice. +-- Returns (msg, err). If the alt path exhausts the tick budget, +-- returns (nil, 'timeout'). If the context is cancelled, returns +-- (nil, 'context cancelled'). +function harness.wait_for_msg(sub, ctx, max_ticks) + local alt = make_alt_wait(ctx, max_ticks) + while true do + local msg, err = sub:next_msg_op():perform_alt(alt) + if err ~= '__ALT__' then + return msg, err + end + end +end + +function harness.wait_for_channel(ch, ctx, max_ticks) + local alt = make_alt_wait(ctx, max_ticks) + while true do + local val, err = ch:get_op():perform_alt(alt) + if err ~= '__ALT__' then + return val, err + end + end +end + +-- Wait until a predicate becomes true, yielding cooperatively +-- between checks. Returns true on success, or false, reason on +-- timeout or context cancellation. +function harness.wait_until(ctx, predicate, max_ticks) + local ticks = 0 + max_ticks = max_ticks or DEFAULT_MAX_TICKS + + while true do + if predicate() then + return true + end + + if ctx and ctx:err() then + return false, 'context cancelled' + end + + ticks = ticks + 1 + if ticks > max_ticks then + return false, 'timeout' + end + + fiber.yield() + end +end + +return harness + diff --git a/tests/hal/harness/backends/mmcli.lua b/tests/hal/harness/backends/mmcli.lua new file mode 100644 index 00000000..bac5093b --- /dev/null +++ b/tests/hal/harness/backends/mmcli.lua @@ -0,0 +1,97 @@ +local commands = {} + +local function set_command(name, cmd) + commands[name] = cmd +end + +local function monitor_modems() + if not commands.monitor_modems then error("monitor_modems command not set up") end + return commands.monitor_modems +end + +local function inhibit(device) + if not commands.inhibit then error("inhibit command not set up") end + return commands.inhibit +end + +local function connect(ctx, device, connection_string) + if not commands.connect then error("connect command not set up") end + return commands.connect +end + +local function disconnect(ctx, device) + if not commands.disconnect then error("disconnect command not set up") end + return commands.disconnect +end + +local function reset(ctx, device) + if not commands.reset then error("reset command not set up") end + return commands.reset +end + +local function enable(ctx, device) + if not commands.enable then error("enable command not set up") end + return commands.enable +end + +local function disable(ctx, device) + if not commands.disable then error("disable command not set up") end + return commands.disable +end + +local function monitor_state(device) + if not commands.monitor_state then error("monitor_state command not set up") end + return commands.monitor_state +end + +local function information(ctx, device) + if not commands.information then error("information command not set up") end + return commands.information +end + +local function sim_information(ctx, device) + if not commands.sim_information then error("sim_information command not set up") end + return commands.sim_information +end + +local function location_status(ctx, device) + if not commands.location_status then error("location_status command not set up") end + return commands.location_status +end + +local function signal_setup(ctx, device, rate) + if not commands.signal_setup then error("signal_setup command not set up") end + return commands.signal_setup +end + +local function signal_get(ctx, device) + if not commands.signal_get then error("signal_get command not set up") end + return commands.signal_get +end + +local function three_gpp_set_initial_eps_bearer_settings(ctx, device, settings) + if not commands.three_gpp_set_initial_eps_bearer_settings then + error("three_gpp_set_initial_eps_bearer_settings command not set up") + end + return commands.three_gpp_set_initial_eps_bearer_settings +end + +return { + monitor_modems = monitor_modems, + inhibit = inhibit, + connect = connect, + disconnect = disconnect, + reset = reset, + enable = enable, + disable = disable, + monitor_state = monitor_state, + information = information, + sim_information = sim_information, + location_status = location_status, + signal_setup = signal_setup, + signal_get = signal_get, + three_gpp_set_initial_eps_bearer_settings = three_gpp_set_initial_eps_bearer_settings, + + -- Test harness only + set_command = set_command, +} diff --git a/tests/hal/harness/backends/qmicli.lua b/tests/hal/harness/backends/qmicli.lua new file mode 100644 index 00000000..edf04a69 --- /dev/null +++ b/tests/hal/harness/backends/qmicli.lua @@ -0,0 +1,66 @@ +local commands = {} + +local function set_command(name, cmd) + commands[name] = cmd +end + +local function uim_get_card_status(ctx, port) + if not commands.uim_get_card_status then error("uim_get_card_status command not set up") end + return commands.uim_get_card_status +end + +local function uim_sim_power_off(ctx, port) + if not commands.uim_sim_power_off then error("uim_sim_power_off command not set up") end + return commands.uim_sim_power_off +end + +local function uim_sim_power_on(ctx, port) + if not commands.uim_sim_power_on then error("uim_sim_power_on command not set up") end + return commands.uim_sim_power_on +end + +local function uim_monitor_slot_status(port) + if not commands.uim_monitor_slot_status then error("uim_monitor_slot_status command not set up") end + return commands.uim_monitor_slot_status +end + +local function uim_read_transparent(ctx, port, address_string) + if not commands.uim_read_transparent then error("uim_read_transparent command not set up") end + return commands.uim_read_transparent +end + +local function nas_get_rf_band_info(ctx, port) + if not commands.nas_get_rf_band_info then error("nas_get_rf_band_info command not set up") end + return commands.nas_get_rf_band_info +end + +local function nas_get_home_network(ctx, port) + if not commands.nas_get_home_network then error("nas_get_home_network command not set up") end + return commands.nas_get_home_network +end + +local function nas_get_serving_system(ctx, port) + if not commands.nas_get_serving_system then error("nas_get_serving_system command not set up") end + return commands.nas_get_serving_system +end + +local function nas_get_signal_info(ctx, port) + if not commands.nas_get_signal_info then error("nas_get_signal_info command not set up") end + return commands.nas_get_signal_info +end + +return { + uim_get_card_status = uim_get_card_status, + uim_sim_power_off = uim_sim_power_off, + uim_sim_power_on = uim_sim_power_on, + uim_monitor_slot_status = uim_monitor_slot_status, + uim_read_transparent = uim_read_transparent, + + nas_get_rf_band_info = nas_get_rf_band_info, + nas_get_home_network = nas_get_home_network, + nas_get_serving_system = nas_get_serving_system, + nas_get_signal_info = nas_get_signal_info, + + -- Test harness only + set_command = set_command, +} diff --git a/tests/hal/harness/devices/modem.lua b/tests/hal/harness/devices/modem.lua new file mode 100644 index 00000000..906f9116 --- /dev/null +++ b/tests/hal/harness/devices/modem.lua @@ -0,0 +1,509 @@ +local templates = require 'tests.hal.templates' +local fiber = require 'fibers.fiber' +local modem_registry = require 'tests.hal.harness.devices.modem_registry' + +-- Mock out external modem commands +local real_mmcli = require 'services.hal.drivers.modem.mmcli' +local mmcli = require 'tests.hal.harness.backends.mmcli' +local mock_err = real_mmcli.use_backend(mmcli) +if mock_err then + error("Failed to set mmcli backend: " .. mock_err) +end + +local real_qmicli = require 'services.hal.drivers.modem.qmicli' +local qmicli = require 'tests.hal.harness.backends.qmicli' +local mock_err = real_qmicli.use_backend(qmicli) +if mock_err then + error("Failed to set qmicli backend: " .. mock_err) +end + +local MODEM_STATE = { + -- Upwards movement + FAILED = 0, + DISABLED = 1, + ENABLING = 2, + ENABLED = 3, + SEARCHING = 4, + REGISTERED = 5, + CONNECTING = 6, + CONNECTED = 7, + -- Downwards movement + DISCONNECTING = 8, + DISABLING = 9, +} + +local Sim = {} +Sim.__index = Sim + +function Sim.new() + local self = {} + self.active = true + return setmetatable(self, Sim) +end + +function Sim:set_imsi(imsi) + self.imsi = imsi +end + +function Sim:set_operator(operator_id, operator_name) + self.operator_id = operator_id + self.operator_name = operator_name +end + +function Sim:get_infomation() + local overrides = { + sim = { + ["active"] = self.active, + ["imsi"] = self.imsi, + ["operator-id"] = self.operator_id, + ["operator-name"] = self.operator_name, + }, + } + return templates.make_sim_information(overrides) +end + +local Modem = {} +Modem.__index = Modem + +-- helper functions + +local function make_full_address(index) + return string.format("/org/freedesktop/ModemManager1/Modem/%s", tostring(index)) +end + +local function make_monitor_event(is_added, address) + local sign = is_added and '(+)' or '(-)' + return string.format("%s %s [DUMMY MANAFACUTER] Dummy Modem Module", sign, + address) +end + +local function setup_mmcli_commands(commands) + commands.monitor_modems:stdout_pipe() -- create stdout pipe to share with modem manager +end + +-- Internal helpers --------------------------------------------------------- + +local function make_sim_dbus_path(index) + return string.format("/org/freedesktop/ModemManager1/SIM/%s", tostring(index)) +end + +local function clone_table(t) + local res = {} + for k, v in pairs(t or {}) do + if type(v) == 'table' then + res[k] = clone_table(v) + else + res[k] = v + end + end + return res +end + +-- Rebuild the mmcli -J -m information JSON based on current modem state. +function Modem:_refresh_mmcli_information() + local state = self.state + if type(state) ~= "string" then + state = tostring(state or "disabled") + end + + local sim_path = self.sim_path + if sim_path ~= nil and type(sim_path) ~= "string" then + sim_path = tostring(sim_path) + end + + local registration_state = self.registration_state + if type(registration_state) ~= "string" then + registration_state = tostring(registration_state or "--") + end + + local generic_overrides = { + state = state, + sim = sim_path or "--", + } + + local threegpp_overrides = { + ["registration-state"] = registration_state, + } + + local overrides = { + modem = { + generic = generic_overrides, + ["3gpp"] = threegpp_overrides, + } + } + + local encoded = templates.make_modem_information(overrides) + self.mmcli_data.information = encoded + + -- Update the existing static information command, if the backend has + -- already created one for this address. + local info_cmds = require('tests.hal.harness.backends.mmcli').information_cmds + local info_cmd = info_cmds[self.mmcli_data.address] + if info_cmd and info_cmd.write_out then + info_cmd:write_out(encoded) + end +end + +-- local function modem_state_machine(modem) +-- -- Placeholder: the real state machine will be event-driven based on +-- -- mmcli/qmicli shim commands. For now, we just initialise the +-- -- high-level state once; tests explicitly calling configuration +-- -- methods are responsible for refreshing information when state +-- -- changes. +-- modem.state = 'disabled' +-- modem.registration_state = "--" +-- -- Call as a plain function because the modem table has not yet +-- -- been given its metatable when this is invoked from Modem.new. +-- Modem._refresh_mmcli_information(modem) +-- end + +-- Modem hardware simulation methods + +function Modem:appear() + if not self.mmcli_data.address then + return "No address set for modem" + end + + local wr_err = self.mmcli_cmds.monitor_modems:write_out(make_monitor_event(true, self.mmcli_data.address)) + if wr_err then return wr_err end + + -- create info command output before modem is added + local information_cmd = mmcli.information(self.ctx, self.mmcli_data.address) + wr_err = information_cmd:write_out(self.mmcli_data.information) + return wr_err or nil +end + +function Modem:disappear() + if not self.mmcli_data.address then + return "No address set for modem" + end + + local wr_err = self.mmcli_cmds.monitor_modems:write_out(make_monitor_event(false, self.mmcli_data.address)) + if wr_err then return wr_err end +end + +function Modem:insert_sim(sim) + self.sim = sim + -- For now we always use SIM index 0 for the single + -- simulated SIM slot. + self.sim_path = make_sim_dbus_path(0) + -- When a SIM is inserted on real hardware, the modem does + -- not immediately transition into a registered state. + -- Instead, the SIM presence changes, the driver detects + -- this via QMI slot monitoring, and then power-cycles / + -- resets the modem which eventually comes back in a + -- disabled state. Reflect that by only updating SIM + -- presence here and leaving state transitions to the + -- driver-driven reset / power-cycle paths. + self:_refresh_mmcli_information() +end + +function Modem:remove_sim() + self.sim = nil + self.sim_path = nil + -- Clear SIM-related information but keep the current modem + -- state; any subsequent power-cycle/reset logic will drive + -- the state machine as in real hardware. + self.registration_state = "--" + self:_refresh_mmcli_information() + if self.qmi_slot_monitor_cmd then + self:_emit_sim_slot_status('absent') + end +end + +function Modem:block_signal() + -- TODO: make it so the modem cannot get past searching state +end + +function Modem:unblock_signal() + -- TODO: make it so the modem can get past searching state into registered state +end + +function Modem:block_connection() + -- TODO: make it so the modem cannot connect to network +end + +function Modem:unblock_connection() + -- TODO: make it so the modem can connect to network +end + +-- Modem configuration methods + +function Modem:set_address_index(index) + self.mmcli_data.address = make_full_address(index) + modem_registry.set_address(self, self.mmcli_data.address) +end + +function Modem:set_mmcli_information(overrides) + -- Allow tests to set a custom base template; store both the raw + -- information and decoded fields we care about. + local information = templates.make_modem_information(overrides) + self.mmcli_data.information = information +end + +-- Optional: allow tests to explicitly bind a QMI port to this modem so +-- that qmicli backend commands can be routed back here. +function Modem:set_qmi_port(port) + self.qmi_port = port + modem_registry.set_qmi_port(self, port) +end + +-- Command handlers invoked from the mmcli/qmicli backends ------------- + +function Modem:on_mmcli_monitor_state_start(cmd) + -- Remember the state monitor command so that subsequent state + -- changes can emit updates. + self.state_monitor_cmd = cmd + local initial_state = self.state or 'disabled' + local line = string.format("Initial state: '%s'\n", initial_state) + cmd:stdout_pipe() -- ensure stdout exists + local err = cmd:write_out(line) + return err +end + +local function emit_state_change(modem, prev, curr) + if not modem.state_monitor_cmd then return end + if prev == curr then return end + local line = string.format("State changed: '%s' -> '%s'\n", prev, curr) + modem.state_monitor_cmd:write_out(line) +end + +function Modem:_set_state(new_state) + local prev = self.state or new_state + if prev == new_state then + return + end + self.state = new_state + fiber.spawn(function() + emit_state_change(self, prev, new_state) + self:_refresh_mmcli_information() + end) +end + +local function inhibited_error(cmd) + local err = "modem inhibited" + cmd:stderr_pipe() + cmd:write_err(err .. "\n") + return err +end + +local function failed_state_error(cmd) + local err = "WrongState: modem in failed state" + cmd:stderr_pipe() + cmd:write_err(err .. "\n") + return err +end + +function Modem:on_mmcli_enable(cmd) + if self.inhibited then + return inhibited_error(cmd) + end + if self.state == 'failed' then + return failed_state_error(cmd) + end + + if self.state == 'disabled' then + self.registration_state = 'home' + self:_set_state('registered') + end + + return nil +end + +function Modem:on_mmcli_disable(cmd) + if self.inhibited then + return inhibited_error(cmd) + end + if self.state == 'failed' then + return failed_state_error(cmd) + end + + self.registration_state = "--" + self:_set_state('disabled') + return nil +end + +function Modem:on_mmcli_connect(cmd, connection_string) + if self.inhibited then + return inhibited_error(cmd) + end + if self.state ~= 'registered' then + local err = string.format("cannot connect from state '%s'", tostring(self.state)) + cmd:stderr_pipe() + cmd:write_err(err .. "\n") + return err + end + + self:_set_state('connected') + cmd:stdout_pipe() + cmd:write_out("connected\n") + return nil +end + +function Modem:on_mmcli_disconnect(cmd) + if self.inhibited then + return inhibited_error(cmd) + end + if self.state ~= 'connected' then + local err = string.format("cannot disconnect from state '%s'", tostring(self.state)) + cmd:stderr_pipe() + cmd:write_err(err .. "\n") + return err + end + + self:_set_state('registered') + cmd:stdout_pipe() + cmd:write_out("disconnected\n") + return nil +end + +local function perform_reset(modem) + modem.registration_state = "--" + -- Model a drop-off and re-appearance of the modem on the + -- monitor_modems channel while keeping the same address. + fiber.spawn(function() + modem:disappear() + modem:_set_state('disabled') + modem:appear() + end) +end + +function Modem:on_mmcli_reset(cmd) + if self.inhibited then + return inhibited_error(cmd) + end + perform_reset(self) + return nil +end + +function Modem:on_mmcli_inhibit_start(cmd) + self.inhibited = true + return nil +end + +function Modem:on_mmcli_inhibit_end(cmd) + self.inhibited = false +end + +function Modem:on_mmcli_signal_setup(cmd, rate) + -- For now, just accept the requested rate; the driver will also + -- update its own refresh_rate_channel. + cmd:stdout_pipe() + cmd:write_out(string.format("signal setup %s\n", tostring(rate))) + return nil +end + +function Modem:on_qmi_uim_sim_power_off(cmd, port) + self.sim_powered = false + local powered_off_msg = string.format( + "[%s] Successfully performed SIM power off", tostring(port) + ) + fiber.spawn(function() + cmd:write_out(powered_off_msg) + cmd:write_out(nil) + end) + return nil +end + +function Modem:on_qmi_uim_sim_power_on(cmd, port) + self.sim_powered = true + + local powered_on_msg = string.format( + "[%s] Successfully performed SIM power on", tostring(port) + ) + fiber.spawn(function() + cmd:write_out(powered_on_msg) + cmd:write_out(nil) + end) + -- If a SIM is present when power is turned back on, model the + -- behaviour as a modem reset. + if self.sim then + -- Emit a QMI slot-status indication so any wait_for_sim + -- logic can observe the SIM becoming present. + fiber.spawn(function() + perform_reset(self) + end) + if self.qmi_slot_monitor_cmd then + fiber.spawn(function() + self:_emit_sim_slot_status('present') + end) + end + end + return nil +end + +-- Emit a minimal QMI slot status indication matching the format +-- expected by utils.parse_slot_monitor, using the simulated port +-- name from our QMI mapping. +function Modem:_emit_sim_slot_status(card_status) + if not self.qmi_slot_monitor_cmd or not self.qmi_port then return end + -- Keep this to a single logical line so that the + -- "Card status ... Slot status ..." pattern matches even + -- though Lua patterns do not make '.' span newlines. + local body = string.format( + "Card status: %s Slot status: active", + card_status + ) + -- self.qmi_slot_monitor_cmd:stdout_pipe() + self.qmi_slot_monitor_cmd:write_out(body) +end + +function Modem:on_qmi_uim_monitor_start(cmd, port) + self.qmi_slot_monitor_cmd = cmd + -- Real qmicli does not replay the last slot-status event on + -- monitor start; it only emits on subsequent changes. The + -- dummy will therefore emit status lines only when the SIM + -- state actually changes (insert/remove/power events). + return nil +end + +function Modem.new(ctx, initial_state) + local self = {} + self.ctx = ctx + self.mmcli_data = {} + self.mmcli_data.information = templates.make_modem_information() + self.mmcli_cmds = { + monitor_modems = mmcli.monitor_modems() + } + self.state = initial_state or 'disabled' + self.registration_state = "--" + self.sim = nil + self.sim_path = nil + self.sim_powered = true + self.inhibited = false + self.signal_blocked = false + self.connection_blocked = false + self.qmi_slot_monitor_cmd = nil + setup_mmcli_commands(self.mmcli_cmds) + self.qmicli_data = {} + -- For now we hard-code the primary QMI port to match the + -- default modem information template used by tests. + self.qmi_port = "/dev/cdc-wdm0" + modem_registry.set_qmi_port(self, self.qmi_port) + local modem = setmetatable(self, Modem) + modem:_refresh_mmcli_information() + return modem +end + +local NoModem = {} +NoModem.__index = NoModem + +function NoModem.new() + local self = {} + self.mmcli_cmds = { + monitor_modems = mmcli.monitor_modems() + } + setup_mmcli_commands(self.mmcli_cmds) + return setmetatable(self, NoModem) +end + +function NoModem:appear() + local wr_err = self.mmcli_cmds.monitor_modems:write_out("No modems were found") + if wr_err then return wr_err end +end + +return { + new = Modem.new, + no_modem = NoModem.new, + new_sim = Sim.new +} diff --git a/tests/hal/harness/devices/modem_registry.lua b/tests/hal/harness/devices/modem_registry.lua new file mode 100644 index 00000000..122dda1d --- /dev/null +++ b/tests/hal/harness/devices/modem_registry.lua @@ -0,0 +1,39 @@ +local M = {} + +-- Simple in-memory registry used by the test modem backends to +-- map mmcli modem addresses and QMI ports back to the dummy modem +-- instance that owns them. + +local by_address = {} +local by_qmi_port = {} + +local function clear_mapping(map, modem) + for k, v in pairs(map) do + if v == modem then + map[k] = nil + end + end +end + +function M.set_address(modem, address) + if not modem or not address then return end + -- Remove any previous mapping for this modem to avoid stale keys. + clear_mapping(by_address, modem) + by_address[address] = modem +end + +function M.set_qmi_port(modem, port) + if not modem or not port then return end + clear_mapping(by_qmi_port, modem) + by_qmi_port[port] = modem +end + +function M.get_by_address(address) + return by_address[address] +end + +function M.get_by_qmi_port(port) + return by_qmi_port[port] +end + +return M diff --git a/tests/hal/harness/services/hal/managers/dummy.lua b/tests/hal/harness/services/hal/managers/dummy.lua new file mode 100644 index 00000000..2fb06f8d --- /dev/null +++ b/tests/hal/harness/services/hal/managers/dummy.lua @@ -0,0 +1,41 @@ +local service = require "service" +local op = require "fibers.op" +local sleep = require "fibers.sleep" +local new_msg = require 'bus'.new_msg + +local DummyManagement = {} +DummyManagement.__index = DummyManagement + +local function new() + local dummy_management = {} + return setmetatable(dummy_management, DummyManagement) +end + +function DummyManagement:apply_config(config) + self.test_arg = config.test_arg +end + +function DummyManagement:_manager(ctx, conn, device_event_q, capability_info_q) + conn:publish( + new_msg( + { "dummy", "status" }, + "running" + ) + ) + while not ctx:err() do + op.choice( + sleep.sleep_op(1), + ctx:done_op() + ):perform() + end +end + +function DummyManagement:spawn(ctx, conn, device_event_q, capability_info_q) + self.test_arg = nil + self.ctx = ctx + service.spawn_fiber("Dummy Manager", conn, ctx, function (fctx) + self:_manager(fctx, conn, device_event_q, capability_info_q) + end) +end + +return { new = new } diff --git a/tests/hal/harness/services/hal/managers/dummy2.lua b/tests/hal/harness/services/hal/managers/dummy2.lua new file mode 100644 index 00000000..eddab9ac --- /dev/null +++ b/tests/hal/harness/services/hal/managers/dummy2.lua @@ -0,0 +1,41 @@ +local service = require "service" +local op = require "fibers.op" +local sleep = require "fibers.sleep" +local new_msg = require 'bus'.new_msg + +local DummyManagement = {} +DummyManagement.__index = DummyManagement + +local function new() + local dummy_management = {} + return setmetatable(dummy_management, DummyManagement) +end + +function DummyManagement:apply_config(config) + self.test_arg = config.test_arg +end + +function DummyManagement:_manager(ctx, conn, device_event_q, capability_info_q) + conn:publish( + new_msg( + { "dummy2", "status" }, + "running" + ) + ) + while not ctx:err() do + op.choice( + sleep.sleep_op(1), + ctx:done_op() + ):perform() + end +end + +function DummyManagement:spawn(ctx, conn, device_event_q, capability_info_q) + self.test_arg = nil + self.ctx = ctx + service.spawn_fiber("Dummy Manager", conn, ctx, function (fctx) + self:_manager(fctx, conn, device_event_q, capability_info_q) + end) +end + +return { new = new } diff --git a/tests/hal/templates.lua b/tests/hal/templates.lua new file mode 100644 index 00000000..20d1b03f --- /dev/null +++ b/tests/hal/templates.lua @@ -0,0 +1,197 @@ +local json = require('dkjson') + +local function merge_tables(main, overrides) + local result = {} + for k, v in pairs(main) do + result[k] = v + end + for k, v in pairs(overrides) do + if type(v) == 'table' and type(result[k]) == 'table' then + result[k] = merge_tables(result[k], v) + else + result[k] = v + end + end + return result +end + +local function make_modem_information(overrides) + -- Minimal mmcli -J -m output containing only the fields the + -- modem driver (and mode/model overrides) actually read. + local base_information = { + modem = { + ["3gpp"] = { + ["registration-state"] = "--", + }, + generic = { + -- Drivers determine QMI/MBIM mode + drivers = { + "qmi_wwan", + }, + -- Used to select manufacturer/model mapping + plugin = "quectel", + model = "QUECTEL Mobile Broadband Module", + revision = "EG25GGBR07A08M2G", + + -- Identity + ["equipment-identifier"] = "867929068986654", + device = "/sys/devices/platform/axi/1000120000.pcie/1f00300000.usb/xhci-hcd.1/usb3/3-1", + + -- Ports used for QMI, AT and net stats + ports = { + "cdc-wdm0 (qmi)", + "ttyUSB2 (at)", + "wwan0 (net)", + }, + ["primary-port"] = "cdc-wdm0", + + -- SIM / state used by the driver polling logic + sim = "--", + state = "disabled", + }, + }, + } + local merged = merge_tables(base_information, overrides or {}) + + return json.encode(merged), merged +end + +-- Minimal mmcli -J -i output structure. The driver only +-- requires the top-level `sim` table, so we keep this very small +-- while allowing overrides for tests that care about details. +local function make_sim_information(overrides) + local base_information = { + sim = { + ["active"] = true, + ["imsi"] = "001010123456789", + ["operator-id"] = "00101", + ["operator-name"] = "Test Operator", + }, + } + local merged = merge_tables(base_information, overrides or {}) + return json.encode(merged), merged +end + +-- Minimal mmcli --signal-get JSON. You can select one or more +-- access technologies that should carry real values via +-- `active_techs`; all other technologies have their metrics set +-- to "--" by default. +-- +-- active_techs: either a single string ("lte") or an array of +-- tech strings (e.g. {"lte", "5g"}). Allowed +-- values are "5g", "cdma1x", "evdo", "gsm", +-- "lte", "umts". +-- overrides: optional table keyed by tech name, each value a +-- table merged into that tech's metrics. +local function make_signal_information(active_techs, overrides) + local base_signal = { + modem = { + signal = { + ["5g"] = { + ["error-rate"] = "--", + rsrp = "--", + rsrq = "--", + snr = "--", + }, + cdma1x = { + ecio = "--", + ["error-rate"] = "--", + rssi = "--", + }, + evdo = { + ecio = "--", + ["error-rate"] = "--", + io = "--", + rssi = "--", + sinr = "--", + }, + gsm = { + ["error-rate"] = "--", + rssi = "--", + }, + lte = { + ["error-rate"] = "--", + rsrp = "--", + rsrq = "--", + rssi = "--", + snr = "--", + }, + umts = { + ecio = "--", + ["error-rate"] = "--", + rscp = "--", + rssi = "--", + }, + refresh = { + rate = "0", + }, + threshold = { + ["error-rate"] = "no", + rssi = "0", + }, + }, + }, + } + + -- Normalise active_techs to an array of tech names + local tech_list + if type(active_techs) == "string" or active_techs == nil then + tech_list = { active_techs or "lte" } + elseif type(active_techs) == "table" then + tech_list = active_techs + else + tech_list = { "lte" } + end + + overrides = overrides or {} + + for _, tech in ipairs(tech_list) do + local tech_table = base_signal.modem.signal[tech] + if tech_table then + local tech_overrides = overrides[tech] or overrides + if tech_overrides and next(tech_overrides) ~= nil then + base_signal.modem.signal[tech] = merge_tables(tech_table, tech_overrides) + end + end + end + + local encoded = json.encode(base_signal) + return encoded, base_signal +end + +local function make_modem_device_event(overrides) + local base_event = { + connected = false, + data = { + device = "modemcard", + port = "/sys/devices/platform/axi/1000120000.pcie/1f00300000.usb/xhci-hcd.1/usb3/3-1" + }, + id_field = "port", + type = "usb" + } + if overrides and overrides.connected == true then + base_event.capabilities = { + modem = { + control = { + driver_q = { + buffer = { count = 0, first = 1, items = {} }, + buffer_size = 10, + getq = { count = 0, first = 1, items = {} }, + putq = { count = 0, first = 1, items = {} } + } + }, + id = "867929068986654" + } + } + base_event.device_control = {} + end + return merge_tables(base_event, overrides or {}) +end + +return { + make_modem_information = make_modem_information, + make_sim_information = make_sim_information, + make_signal_information = make_signal_information, + make_modem_device_event = make_modem_device_event, + merge_tables = merge_tables, +} diff --git a/tests/hal/test_core.lua b/tests/hal/test_core.lua new file mode 100644 index 00000000..32b9b910 --- /dev/null +++ b/tests/hal/test_core.lua @@ -0,0 +1,1197 @@ +-- Detect if this file is being run as the entry point +local this_file = debug.getinfo(1, "S").source:match("@?([^/]+)$") +local is_entry_point = arg and arg[0] and arg[0]:match("[^/]+$") == this_file + +if is_entry_point then + -- Match the test harness package.path setup (see tests/test.lua, + -- test_wifi.lua, test_metrics.lua, test_system.lua) + package.path = "../../src/lua-fibers/?.lua;" -- fibers submodule src + .. "../../src/lua-trie/src/?.lua;" -- trie submodule src + .. "../../src/lua-bus/src/?.lua;" -- bus submodule src + .. "../../src/?.lua;" -- main src tree + .. "../../?.lua;" -- repo root (for tests.hal.harness) + .. "./test_utils/?.lua;" -- shared test utilities + .. package.path + .. ";/usr/lib/lua/?.lua;/usr/lib/lua/?/init.lua;" + .. "./harness/?.lua;" + + _G._TEST = true -- Enable test exports in source code + local log = require 'services.log' + local rxilog = require 'rxilog' + for _, mode in ipairs(rxilog.modes) do + log[mode.name] = function() end -- no-op logging during tests + end +end + +local luaunit = require 'luaunit' +local fiber = require 'fibers.fiber' +local unpack = unpack or table.unpack + +local harness = require 'tests.hal.harness' + +-- Test harness for HAL configuration, device events, and capability events. + +-- HAL config tests + +TestHalConfig = {} + +local new_hal_env = harness.new_hal_env +local config_path = harness.config_path +local publish_config = harness.publish_config + +local function assert_no_managers(hal, msg) + luaunit.assertNil(next(hal.managers), msg or "Expected HAL to have zero managers") +end + +function TestHalConfig:test_simple_config() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local test_arg = "test" + local config = { -- spawns a dummy manager with a test arg + managers = { + dummy = { + test_arg = test_arg + }, + }, + } + + -- Subscribe before starting HAL to avoid races with the + -- initial status publication from the dummy manager. + local dummy_manager_sub = conn:subscribe({ "dummy", "status" }) + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") + local msg, err = harness.wait_for_msg(dummy_manager_sub, ctx) + luaunit.assertNil(err, "Expected to receive dummy manager status message") + luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") + luaunit.assertEquals(msg.payload, "running") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to have instantiated dummy manager") + luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, "Expected dummy manager to have applied config") + ctx:cancel("test complete") +end + +function TestHalConfig:test_nil_config() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = nil + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + local ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) ~= nil + end) + luaunit.assertFalse(ok, "Expected HAL to not create managers for nil config") + assert_no_managers(hal) + ctx:cancel("test complete") +end + +function TestHalConfig:test_empty_managers_config() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = { + managers = { + -- empty managers + }, + } + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + local ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) ~= nil + end) + luaunit.assertFalse(ok, "Expected HAL to not create managers for empty managers config") + assert_no_managers(hal) + ctx:cancel("test complete") +end + +function TestHalConfig:test_invalid_type_config() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = "This should be a table, not a string" + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + local ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) ~= nil + end) + luaunit.assertFalse(ok, "Expected HAL to not create managers for invalid type config") + assert_no_managers(hal) + ctx:cancel("test complete") +end + +function TestHalConfig:test_no_managers_config() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = { + -- no managers key + some_other_config = true + } + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + local ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) ~= nil + end) + luaunit.assertFalse(ok, "Expected HAL to not create managers when managers key is missing") + assert_no_managers(hal) + ctx:cancel("test complete") +end + +function TestHalConfig:test_invalid_manager() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = { + managers = { + invalid_manager_name = { + some_arg = "some_value" + }, + }, + } + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + local ok, reason = harness.wait_until(ctx, function() + return hal.managers.invalid_manager_name ~= nil + end) + luaunit.assertFalse(ok, "Expected HAL to ignore invalid manager name") + luaunit.assertNil(hal.managers.invalid_manager_name, "Expected HAL to ignore invalid manager name") + ctx:cancel("test complete") +end + +function TestHalConfig:test_remove_manager() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local test_arg = "test" + local config = { + managers = { + dummy = { + test_arg = test_arg + }, + }, + } + + local dummy_manager_sub = conn:subscribe({ "dummy", "status" }) + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") + local msg, err = harness.wait_for_msg(dummy_manager_sub, ctx) + luaunit.assertNil(err, "Expected to receive dummy manager status message" .. (err and (": " .. tostring(err)) or "")) + luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") + luaunit.assertEquals(msg.payload, "running") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to have instantiated dummy manager") + + local done = false + local dummy_ctx = hal.managers.dummy.ctx + fiber.spawn(function() + dummy_ctx:done_op():perform() + done = true + end) + + -- Now remove the dummy manager from HAL + + local new_config = { + managers = { + -- empty managers + }, + } + + conn:publish( + new_msg( + config_path(), + new_config, + { retained = true } + ) + ) + + local ok, reason = harness.wait_until(ctx, function() + return (next(hal.managers) == nil or hal.managers.dummy == nil) and done + end) + luaunit.assertTrue(ok, "Expected HAL to remove dummy manager, but wait_until failed: " .. tostring(reason)) + luaunit.assertNil(hal.managers.dummy, "Expected HAL to have removed dummy manager") + luaunit.assertNil(next(hal.managers), "Expected HAL to have zero managers") + luaunit.assertTrue(done, "Expected dummy manager context to be done") -- manager should revieve a cancel signal + ctx:cancel("test complete") +end + +function TestHalConfig:test_reconfigure_manager() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local test_arg = "initial_value" + local config = { + managers = { + dummy = { + test_arg = test_arg + }, + }, + } + + -- Subscribe before starting HAL so we reliably see the + -- initial status message published by the dummy manager. + local dummy_manager_sub = conn:subscribe({ "dummy", "status" }) + luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + local msg, err = harness.wait_for_msg(dummy_manager_sub, ctx) + luaunit.assertNil(err, "Expected to receive dummy manager status message") + luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") + luaunit.assertEquals(msg.payload, "running") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to have instantiated dummy manager") + luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, "Expected dummy manager to have applied initial config") + + -- Now reconfigure the dummy manager + + local new_test_arg = "updated_value" + local new_config = { + managers = { + dummy = { + test_arg = new_test_arg + }, + }, + } + + conn:publish( + new_msg( + config_path(), + new_config, + { retained = true } + ) + ) + + local ok, reason = harness.wait_until(ctx, function() + return hal.managers.dummy ~= nil and hal.managers.dummy.test_arg == new_test_arg + end) + luaunit.assertTrue(ok, + "Expected HAL to reconfigure dummy manager, but wait_until failed: " .. tostring(reason)) + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager after reconfiguration") + luaunit.assertEquals(hal.managers.dummy.test_arg, new_test_arg, + "Expected dummy manager to have applied updated config") + ctx:cancel("test complete") +end + +function TestHalConfig:test_invalid_config_does_not_affect_managers() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local test_arg = "initial_value" + local config = { + managers = { + dummy = { + test_arg = test_arg + }, + }, + } + + local dummy_manager_sub = conn:subscribe({ "dummy", "status" }) + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") + local msg, err = harness.wait_for_msg(dummy_manager_sub, ctx) + luaunit.assertNil(err, "Expected to receive dummy manager status message") + luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") + luaunit.assertEquals(msg.payload, "running") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to have instantiated dummy manager") + luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, "Expected dummy manager to have applied initial config") + + -- Now publish an invalid config type + + local invalid_config = "This should be a table, not a string" + + publish_config(conn, new_msg, invalid_config) + + local ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) == nil + end) + luaunit.assertFalse(ok, "Expected HAL to keep dummy manager after invalid config") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager after invalid config") + luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, + "Expected dummy manager to retain initial config after invalid config") + + -- Now publish a missing managers config + local missing_managers_config = { + some_other_config = true + } + publish_config(conn, new_msg, missing_managers_config) + + ok, reason = harness.wait_until(ctx, function() + return next(hal.managers) == nil + end) + luaunit.assertFalse(ok, "Expected HAL to keep dummy manager after missing managers config") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager after missing managers config") + luaunit.assertEquals(hal.managers.dummy.test_arg, test_arg, + "Expected dummy manager to retain initial config after missing managers config") + + ctx:cancel("test complete") +end + +function TestHalConfig:test_partial_manager_removal() + local hal, ctx, bus, conn, new_msg = new_hal_env() + + local config = { + managers = { + dummy = { + test_arg = "value1" + }, + dummy2 = { + test_arg = "value2" + }, + }, + } + + local dummy_manager_sub = conn:subscribe({ "dummy", "status" }) + local dummy2_manager_sub = conn:subscribe({ "dummy2", "status" }) + + hal:start(ctx, bus:connect()) + + publish_config(conn, new_msg, config) + + -- Wait for dummy manager 1 + luaunit.assertNotNil(dummy_manager_sub, "Expected to subscribe to dummy manager status messages") + local msg, err = harness.wait_for_msg(dummy_manager_sub, ctx) + luaunit.assertNil(err, "Expected to receive dummy manager status message") + luaunit.assertNotNil(msg, "Expected to receive dummy manager status message") + luaunit.assertEquals(msg.payload, "running") + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to have instantiated dummy manager") + + -- Wait for dummy manager 2 + luaunit.assertNotNil(dummy2_manager_sub, "Expected to subscribe to dummy2 manager status messages") + local msg2, err2 = harness.wait_for_msg(dummy2_manager_sub, ctx) + luaunit.assertNil(err2, "Expected to receive dummy2 manager status message") + luaunit.assertNotNil(msg2, "Expected to receive dummy2 manager status message") + luaunit.assertEquals(msg2.payload, "running") + luaunit.assertNotNil(hal.managers.dummy2, "Expected HAL to have instantiated dummy2 manager") + + -- Now remove only the dummy2 manager by omitting it from + -- the new config while keeping dummy present. + local new_config = { + managers = { + dummy = { + test_arg = "value2" + }, + }, + } + conn:publish( + new_msg( + config_path(), + new_config, + { retained = true } + ) + ) + + local ok, reason = harness.wait_until(ctx, function() + return hal.managers.dummy ~= nil and hal.managers.dummy.test_arg == "value2" and + hal.managers.dummy2 == nil + end) + luaunit.assertTrue(ok, + "Expected HAL to apply partial manager removal, but wait_until failed: " .. tostring(reason)) + -- HAL removes managers that are not present in the new + -- config, so dummy2 should be removed and dummy kept. + luaunit.assertNotNil(hal.managers.dummy, "Expected HAL to still have dummy manager") + luaunit.assertEquals(hal.managers.dummy.test_arg, "value2", "Expected dummy manager to have applied updated config") + luaunit.assertNil(hal.managers.dummy2, "Expected HAL to have removed dummy2 manager") + ctx:cancel("test complete") +end + +-- Device event tests + +TestHalDeviceEvent = {} + +local function make_dummy_device_event(connected, id, capabilities) + return { + connected = connected, + type = 'dummy_device', + id_field = "field", + data = { + field = id + }, + capabilities = connected and capabilities or nil, -- only present on connected + device_control = connected and {} or nil, + } +end + +local function device_event_path(device_type, device_id) + return { 'hal', 'device', device_type, device_id } +end + +local function assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event, description) + device_event_q:put(event) + local msg, err = harness.wait_for_msg(hal_device_event_sub, ctx) + luaunit.assertNil(msg, + "Expected to not receive HAL device event message for " .. description) + luaunit.assertEquals(err, 'timeout', + "Expected timeout when no HAL device event should be received for " .. description .. + ", got: " .. tostring(err)) +end + +function TestHalDeviceEvent:test_device_add_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_event_q = hal.device_event_q + local device_name = 'dummy1' + local device_add_event = make_dummy_device_event(true, device_name, {}) + + local hal_device_event_sub = conn:subscribe(device_event_path('dummy_device', device_name)) + + device_event_q:put(device_add_event) + + -- Next wait for a device event and check the value in the payload + local msg, err = harness.wait_for_msg(hal_device_event_sub, ctx) + luaunit.assertNil(err, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg.payload, "Expected HAL device event message to have data payload") + luaunit.assertEquals(msg.payload.connected, true, "Expected device connected state to be true") + luaunit.assertEquals(msg.payload.type, 'dummy_device', "Expected device type to be 'dummy_device'") + luaunit.assertEquals(msg.payload.index, device_name, "Expected device id to match") + ctx:cancel("test complete") +end + +function TestHalDeviceEvent:test_device_remove_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_event_q = hal.device_event_q + local device_name = 'dummy2' + local device_add_event = make_dummy_device_event(true, device_name, {}) + local device_remove_event = make_dummy_device_event(false, device_name, nil) + + local hal_device_event_sub = conn:subscribe(device_event_path('dummy_device', device_name)) + + device_event_q:put(device_add_event) + device_event_q:put(device_remove_event) + + -- First wait for the add event to be received + local msg, err = harness.wait_for_msg(hal_device_event_sub, ctx) + luaunit.assertNil(err, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg.payload, "Expected HAL device event message to have data payload") + luaunit.assertEquals(msg.payload.connected, true, "Expected device connected state to be true") + + -- Now wait for the remove event and check the value in the payload + local msg, err = harness.wait_for_msg(hal_device_event_sub, ctx) + luaunit.assertNil(err, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg, "Expected to receive HAL device event message") + luaunit.assertNotNil(msg.payload, "Expected HAL device event message to have data payload") + luaunit.assertEquals(msg.payload.connected, false, "Expected device connected state to be false") + luaunit.assertEquals(msg.payload.type, 'dummy_device', "Expected device type to be 'dummy_device'") + luaunit.assertEquals(msg.payload.index, device_name, "Expected device id to match") + ctx:cancel("test complete") +end + +function TestHalDeviceEvent:test_device_remove_nonexistent() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_event_q = hal.device_event_q + local device_name = 'dummy3' + local device_remove_event = make_dummy_device_event(false, device_name, nil) + + local hal_device_event_sub = conn:subscribe(device_event_path('dummy_device', device_name)) + + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, device_remove_event, 'nonexistent device') + ctx:cancel("test complete") +end + +function TestHalDeviceEvent:test_device_add_event_invalid() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_event_q = hal.device_event_q + local device_name = 'dummy_invalid' + + local hal_device_event_sub = conn:subscribe(device_event_path('dummy_device', device_name)) + + -- Each invalid event starts from a helper-generated valid event + -- and then removes exactly one required field. + + -- No type field + local event_no_type = make_dummy_device_event(true, device_name, {}) + event_no_type.type = nil + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event_no_type, 'invalid event: no type field') + + -- No connected field + local event_no_connected = make_dummy_device_event(true, device_name, {}) + event_no_connected.connected = nil + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event_no_connected, + 'invalid event: no connected field') + + -- No id_field field + local event_no_id_field = make_dummy_device_event(true, device_name, {}) + event_no_id_field.id_field = nil + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event_no_id_field, + 'invalid event: no id_field field') + + -- No data field + local event_no_data = make_dummy_device_event(true, device_name, {}) + event_no_data.data = nil + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event_no_data, 'invalid event: no data field') + + -- No capabilities field (for a connected device) + local event_no_capabilities = make_dummy_device_event(true, device_name, {}) + event_no_capabilities.capabilities = nil + assert_no_device_event(device_event_q, hal_device_event_sub, ctx, event_no_capabilities, + 'invalid event: no capabilities field') + + ctx:cancel('test complete') +end + +-- Capability event tests + +TestHalDeviceCapabilityEvent = {} + +local function wrap_result(...) + return { result = { ... }, err = nil } +end + +local function wrap_error(err_msg) + return { result = nil, err = err_msg } +end + +local function make_dummy_capability_list(length) + local capabilities = {} + for i = 1, length do + local cap = { + id = tostring(i), + control = { + no_args = function() + return wrap_result(i, "no_args_endpoint") + end, + single_arg = function(_, args) + return wrap_result(i, "single_arg_endpoint", args, #args) + end, + multi_arg = function(_, args) + return wrap_result(i, "multi_arg_endpoint", args, #args) + end, + error_fn = function() + return wrap_error("Capability function error") + end + } + } + capabilities["capability" .. i] = cap + end + return capabilities +end + +local function assert_capability_event(event, expected_event) + luaunit.assertNotNil(event, "Expected capability event to not be nil") + luaunit.assertEquals(event.connected, expected_event.connected, "Expected capability connected state to match") + luaunit.assertEquals(event.type, expected_event.type, "Expected capability type to match") + luaunit.assertEquals(event.index, expected_event.index, "Expected capability index to match") + luaunit.assertNotNil(event.device, "Expected capability event to have device field") + luaunit.assertEquals(event.device.type, expected_event.device.type, "Expected capability device type to match") + luaunit.assertEquals(event.device.index, expected_event.device.index, "Expected capability device index to match") +end + +local function make_expected_capability_event(device_name, capability_index, connected) + return { + connected = connected, + type = "capability" .. capability_index, + index = tostring(capability_index), + device = { + type = "dummy_device", + index = device_name, + }, + } +end + +local function capability_event_path(capability_index) + -- Topic used by HAL for capability connection events generated + -- as a side effect of device connection events. + return { 'hal', 'capability', 'capability' .. capability_index, tostring(capability_index) } +end + +local function all_capability_event_path() + -- Topic used by HAL for all capability connection events + return { 'hal', 'capability', '+', '+' } +end + +local function expect_capability_event(sub, ctx, expected_event, label) + local suffix = label and (" " .. label) or "" + local msg, err = harness.wait_for_msg(sub, ctx) + luaunit.assertNil(err, "Expected to receive HAL capability event message" .. suffix) + luaunit.assertNotNil(msg, "Expected to receive HAL capability event message" .. suffix) + assert_capability_event(msg.payload, expected_event) +end + +local function assert_no_capability_event(ctx, sub, label) + local suffix = label and (" " .. label) or "" + local msg, err = harness.wait_for_msg(sub, ctx) + luaunit.assertNil(msg, "Expected to not receive HAL capability event message" .. suffix) + luaunit.assertEquals(err, 'timeout', "Expected timeout when no capability event should be received" .. suffix .. + ", got: " .. tostring(err)) +end + +local function expect_retained_drop_event(ctx, sub) + local msg, err = harness.wait_for_msg(sub, ctx) + luaunit.assertNil(err, "Expected to receive a message") + luaunit.assertNotNil(msg, "Expected to receive a message") + luaunit.assertNil(msg.payload, "Expected retained drop message to have nil payload") +end + + +function TestHalDeviceCapabilityEvent:test_device_capability_add_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_capable_device' + local capabilities = make_dummy_capability_list(1) + local device_event = make_dummy_device_event(true, device_name, capabilities) + local hal_capability_info_sub = conn:subscribe(capability_event_path(1)) + hal.device_event_q:put(device_event) + + local expected_event = make_expected_capability_event(device_name, 1, true) + + -- Next wait for a capability event and check the value in the payload + expect_capability_event(hal_capability_info_sub, ctx, expected_event, "for capability1") + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_device_no_capability() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_no_capability_device' + local capabilities = {} -- empty capabilities + local device_event = make_dummy_device_event(true, device_name, capabilities) + local hal_capability_info_sub = conn:subscribe(all_capability_event_path()) + + hal.device_event_q:put(device_event) + + -- No capability event should be published + assert_no_capability_event(ctx, hal_capability_info_sub, + "Expect no capability events for device with no capabilities") + + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_device_multi_capability_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_multi_capable_device' + local capabilities = make_dummy_capability_list(3) + local device_event = make_dummy_device_event(true, device_name, capabilities) + local subs = {} + for i = 1, 3 do + subs[i] = conn:subscribe(capability_event_path(i)) + end + + hal.device_event_q:put(device_event) + + -- Next wait for capability info events and check the values in the payloads + for i, sub in ipairs(subs) do + local expected_event = make_expected_capability_event(device_name, i, true) + expect_capability_event(sub, ctx, expected_event, "for capability" .. i) + end + + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_device_capability_remove_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_capable_device_remove' + local capabilities = make_dummy_capability_list(1) + local device_add_event = make_dummy_device_event(true, device_name, capabilities) + local device_remove_event = make_dummy_device_event(false, device_name, nil) + local hal_capability_info_sub = conn:subscribe(capability_event_path(1)) + + hal.device_event_q:put(device_add_event) + + local expected_add_event = make_expected_capability_event(device_name, 1, true) + + -- Next wait for a capability info add event and check the value in the payload + expect_capability_event(hal_capability_info_sub, ctx, expected_add_event, "for capability1 add") + + -- Now send the remove event + hal.device_event_q:put(device_remove_event) + + local expected_remove_event = make_expected_capability_event(device_name, 1, false) + + -- A nil payload is sent first to remove retained values under the topic + expect_retained_drop_event(ctx, hal_capability_info_sub) + + -- Next wait for a capability info remove event and check the value in the payload + expect_capability_event(hal_capability_info_sub, ctx, expected_remove_event, "for capability1 remove") + + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_device_capability_invalid_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_invalid_capability_device' + + -- Start from two valid capabilities, then invalidate one of them + local capabilities = make_dummy_capability_list(2) + capabilities["capability2"].id = nil -- missing id should make this capability invalid + + local device_add_event = make_dummy_device_event(true, device_name, capabilities) + + local valid_cap_sub = conn:subscribe(capability_event_path(1)) + local invalid_cap_sub = conn:subscribe(capability_event_path(2)) + + -- Publish device add event with one valid and one invalid capability + hal.device_event_q:put(device_add_event) + + -- The valid capability should still produce an event + local expected_valid_event = make_expected_capability_event(device_name, 1, true) + expect_capability_event(valid_cap_sub, ctx, expected_valid_event, "for valid capability1") + + -- The invalid capability (missing id) should not produce any event + assert_no_capability_event(ctx, invalid_cap_sub, " for invalid capability2 (missing id)") + + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_device_capability_duplicate_id_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name1 = 'dummy_dup_cap_device1' + local device_name2 = 'dummy_dup_cap_device2' + local capabilities1 = make_dummy_capability_list(1) + local capabilities2 = make_dummy_capability_list(1) + local device1_add_event = make_dummy_device_event(true, device_name1, capabilities1) + local device2_add_event = make_dummy_device_event(true, device_name2, capabilities2) + local cap_sub = conn:subscribe(capability_event_path(1)) + + -- Publish two add events with the same capability id; HAL should handle + -- the duplicate id by overwriting the existing entry and still publishing + -- capability events for both devices. + hal.device_event_q:put(device1_add_event) + hal.device_event_q:put(device2_add_event) + + local expected_event1 = make_expected_capability_event(device_name1, 1, true) + expect_capability_event(cap_sub, ctx, expected_event1, "for capability1 first add") + + local expected_event2 = make_expected_capability_event(device_name2, 1, true) + expect_capability_event(cap_sub, ctx, expected_event2, "for capability1 second add (duplicate id)") + + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_add_remove_add_capability_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_add_remove_add_device' + local capabilities = make_dummy_capability_list(1) + local device_add_event = make_dummy_device_event(true, device_name, capabilities) + local device_remove_event = make_dummy_device_event(false, device_name, nil) + + local cap_sub = conn:subscribe(capability_event_path(1)) + + -- 1. Add event + hal.device_event_q:put(device_add_event) + + local expected_add_event = make_expected_capability_event(device_name, 1, true) + expect_capability_event(cap_sub, ctx, expected_add_event, "for capability1 add") + + -- 2. Remove event + hal.device_event_q:put(device_remove_event) + + -- A nil payload is sent first to remove retained values under the topic + expect_retained_drop_event(ctx, cap_sub) + + local expected_remove_event = make_expected_capability_event(device_name, 1, false) + expect_capability_event(cap_sub, ctx, expected_remove_event, "for capability1 remove") + + -- 3. Add event again + hal.device_event_q:put(device_add_event) + + expect_capability_event(cap_sub, ctx, expected_add_event, "for capability1 add again") + + ctx:cancel("test complete") +end + +function TestHalDeviceCapabilityEvent:test_device_capability_nil_control_event() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_nil_control_device' + local capabilities = make_dummy_capability_list(1) + -- Invalidate the control field; this capability should be ignored. + capabilities["capability1"].control = nil + local device_add_event = make_dummy_device_event(true, device_name, capabilities) + local cap_sub = conn:subscribe(capability_event_path(1)) + + hal.device_event_q:put(device_add_event) + + -- No capability event should be published when control is nil. + assert_no_capability_event(ctx, cap_sub, " for capability1 with nil control") + + ctx:cancel("test complete") +end + +TestHalCapabilityControl = {} + +local function capability_control_path(capability_type, capability_index, endpoint) + return { 'hal', 'capability', capability_type, tostring(capability_index), 'control', endpoint } +end + +function TestHalCapabilityControl:test_capability_control_endpoints() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local capabilities = make_dummy_capability_list(1) + local device_event = make_dummy_device_event(true, "test_device", capabilities) + local cap_sub = conn:subscribe(capability_event_path(1)) + + hal.device_event_q:put(device_event) + + local expected_event = make_expected_capability_event("test_device", 1, true) + expect_capability_event(cap_sub, ctx, expected_event, "for capability1 add") -- wait for capability to appear + + -- 1. Test no_args endpoint + local cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "no_args") + )) + + local response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + local expected_result = wrap_result(1, "no_args_endpoint") + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + -- 2. Test single_arg endpoint + cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "single_arg"), + { "arg1_value" } + )) + response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + expected_result = wrap_result(1, "single_arg_endpoint", { "arg1_value" }, 1) + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + -- 3. Test multi_arg endpoint + cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "multi_arg"), + { "arg1", "arg2", "arg3" } + )) + response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + expected_result = wrap_result(1, "multi_arg_endpoint", { "arg1", "arg2", "arg3" }, 3) + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + -- 4. Test error_fn endpoint + cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "error_fn") + )) + response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + expected_result = wrap_error("Capability function error") + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + ctx:cancel("test complete") +end + +function TestHalCapabilityControl:test_invalid_capability_control_endpoints() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local capabilities = make_dummy_capability_list(1) + local device_event = make_dummy_device_event(true, "test_device_invalid_control", capabilities) + local cap_sub = conn:subscribe(capability_event_path(1)) + + hal.device_event_q:put(device_event) + + local expected_event = make_expected_capability_event("test_device_invalid_control", 1, true) + expect_capability_event(cap_sub, ctx, expected_event, "for capability1 add") -- wait for capability to appear + + -- 1. Non-existent function + local cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "invalid_endpoint") + )) + + local response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + local expected_result = wrap_error('endpoint does not exist') + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + -- 2. Non-existent capability index + cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 999, "no_args") + )) + + response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + expected_result = wrap_error('capability instance does not exist') + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + -- 3. Non-existent capability type + cap_control_sub = conn:request(new_msg( + capability_control_path("invalid_capability", 1, "no_args") + )) + response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + expected_result = wrap_error('capability does not exist') + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + ctx:cancel("test complete") +end + +function TestHalCapabilityControl:test_no_endpoint_on_removal() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local device_name = 'dummy_control_remove_device' + local capabilities = make_dummy_capability_list(1) + local device_add_event = make_dummy_device_event(true, device_name, capabilities) + local device_remove_event = make_dummy_device_event(false, device_name, nil) + + local cap_sub = conn:subscribe(capability_event_path(1)) + + -- 1. Add event + hal.device_event_q:put(device_add_event) + + local expected_add_event = make_expected_capability_event(device_name, 1, true) + expect_capability_event(cap_sub, ctx, expected_add_event, "for capability1 add") + + -- 2. Remove event + hal.device_event_q:put(device_remove_event) + + -- A nil payload is sent first to remove retained values under the topic + expect_retained_drop_event(ctx, cap_sub) + + local expected_remove_event = make_expected_capability_event(device_name, 1, false) + expect_capability_event(cap_sub, ctx, expected_remove_event, "for capability1 remove") + + -- Now try to call an endpoint on the removed capability + local cap_control_sub = conn:request(new_msg( + capability_control_path("capability1", 1, "no_args") + )) + + local response, err = harness.wait_for_msg(cap_control_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability control response") + local expected_result = wrap_error('capability instance does not exist') + luaunit.assertEquals(response.payload, expected_result, "Expected capability control response to match") + + ctx:cancel("test complete") +end + +function TestHalCapabilityControl:test_publish_control() + local hal, ctx, bus, conn, new_msg = new_hal_env() + local channel = require 'fibers.channel' + local ch = channel.new() + hal:start(ctx, bus:connect()) + local capabilities = make_dummy_capability_list(1) + -- New endpoint to detect run of endpoint + capabilities["capability1"].control["trigger_channel"] = function(_, args) + ch:put(args[1]) + return wrap_result("") -- we won't receive this + end + local device_event = make_dummy_device_event(true, "test_device_publish_control", capabilities) + local cap_sub = conn:subscribe(capability_event_path(1)) + + hal.device_event_q:put(device_event) + + local expected_event = make_expected_capability_event("test_device_publish_control", 1, true) + expect_capability_event(cap_sub, ctx, expected_event, "for capability1 add") -- wait for capability to appear + + -- Now publish a control message directly to the capability control topic + conn:publish(new_msg( + capability_control_path("capability1", 1, "trigger_channel"), + { 42 } + )) + -- Wait for the channel to be triggered using cooperative waiting + local received + fiber.spawn(function() + received = ch:get() + end) + local ok, reason = harness.wait_until(ctx, function() + return received ~= nil + end) + luaunit.assertTrue(ok, + "Expected capability control endpoint to trigger channel, but wait_until failed: " .. tostring(reason)) + luaunit.assertEquals(received, 42, "Expected capability control endpoint to trigger channel with argument") +end + +TestHalCapabilityInfo = {} + +local function capability_info_path(type, id, endpoints) + if endpoints == nil then endpoints = {} end + return { 'hal', 'capability', type, id, 'info', unpack(endpoints) } +end + +function TestHalCapabilityInfo:test_simple_info() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local info_q = hal.capability_info_q + local info_sub = conn:subscribe(capability_info_path("dummy", "1")) + + local info = "test" + info_q:put({ + type = "dummy", + id = "1", + sub_topic = {}, + endpoints = "single", + info = info, + }) + + local msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability info message") + luaunit.assertNotNil(msg, "Expected to receive capability info message") + luaunit.assertEquals(msg.payload, info, "Expected capability info payload to match") + ctx:cancel("test complete") +end + +function TestHalCapabilityInfo:test_tabled_info() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local info_q = hal.capability_info_q + local info_sub = conn:subscribe(capability_info_path("dummy", "2")) + local no_info_sub_1 = conn:subscribe(capability_info_path("dummy", "2", { "field1" })) + local no_info_sub_2 = conn:subscribe(capability_info_path("dummy", "2", { "field2" })) + + local info = { + field1 = "value1", + field2 = 42, + } + info_q:put({ + type = "dummy", + id = "2", + sub_topic = {}, + endpoints = "single", + info = info, + }) + + local msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability info message") + luaunit.assertNotNil(msg, "Expected to receive capability info message") + luaunit.assertEquals(msg.payload, info, "Expected capability info payload to match") + + local msg2, err2 = harness.wait_for_msg(no_info_sub_1, ctx) + luaunit.assertNil(msg2, "Expected to not receive capability info message with subtopic") + luaunit.assertEquals(err2, 'timeout', + "Expected timeout with subtopic for missing capability info, got: " .. tostring(err2)) + + local msg3, err3 = harness.wait_for_msg(no_info_sub_2, ctx) + luaunit.assertNil(msg3, "Expected to not receive capability info message with subtopic") + luaunit.assertEquals(err3, 'timeout', + "Expected timeout with subtopic for missing capability info, got: " .. tostring(err3)) + + ctx:cancel("test complete") +end + +function TestHalCapabilityInfo:test_info_with_subtopic() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local info_q = hal.capability_info_q + local info_sub = conn:subscribe(capability_info_path("dummy", "3", { "subtopic1", "subtopic2" })) + + local info = "subtopic_info" + info_q:put({ + type = "dummy", + id = "3", + sub_topic = { "subtopic1", "subtopic2" }, + endpoints = "single", + info = info, + }) + + local msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(err, "Expected to receive capability info message") + luaunit.assertNotNil(msg, "Expected to receive capability info message") + luaunit.assertEquals(msg.payload, info, "Expected capability info payload to match") + ctx:cancel("test complete") +end + +function TestHalCapabilityInfo:test_tabled_info_publish_multiple() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local info_q = hal.capability_info_q + local info_sub_1 = conn:subscribe(capability_info_path("dummy", "3", { "field1" })) + local info_sub_2 = conn:subscribe(capability_info_path("dummy", "3", { "field2" })) + local no_info_sub = conn:subscribe(capability_info_path("dummy", "3")) + + local info = { + field1 = "value1", + field2 = 42, + } + info_q:put({ + type = "dummy", + id = "3", + endpoints = "multiple", + info = info, + }) + + local msg, err = harness.wait_for_msg(info_sub_1, ctx) + luaunit.assertNil(err, "Expected to receive capability info message") + luaunit.assertNotNil(msg, "Expected to receive capability info message") + luaunit.assertEquals(msg.payload, info.field1, "Expected capability info payload to match") + + local msg2, err2 = harness.wait_for_msg(info_sub_2, ctx) + luaunit.assertNil(err2, "Expected to receive capability info message") + luaunit.assertNotNil(msg2, "Expected to receive capability info message") + luaunit.assertEquals(msg2.payload, info.field2, "Expected capability info payload to match") + + local msg3, err3 = harness.wait_for_msg(no_info_sub, ctx) + luaunit.assertNil(msg3, "Expected to not receive capability info message without subtopic") + luaunit.assertEquals(err3, 'timeout', + "Expected timeout without subtopic for missing capability info, got: " .. tostring(err3)) + + ctx:cancel("test complete") +end + +function TestHalCapabilityInfo:test_info_invalid() + local hal, ctx, bus, conn, new_msg = new_hal_env() + hal:start(ctx, bus:connect()) + local info_q = hal.capability_info_q + local info_sub = conn:subscribe(capability_info_path("dummy", "4")) + + -- Missing type + info_q:put({ + id = "4", + sub_topic = {}, + endpoints = "single", + info = "invalid_info", + }) + + local msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(msg, "Expected to not receive capability info message with missing type") + luaunit.assertEquals(err, 'timeout', + "Expected timeout with missing type for capability info, got: " .. tostring(err)) + + -- Missing id + info_q:put({ + type = "dummy", + sub_topic = {}, + endpoints = "single", + info = "invalid_info", + }) + + msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(msg, "Expected to not receive capability info message with missing id") + luaunit.assertEquals(err, 'timeout', + "Expected timeout with missing id for capability info, got: " .. tostring(err)) + + -- Missing endpoints + info_q:put({ + type = "dummy", + id = "4", + sub_topic = {}, + info = "invalid_info", + }) + + msg, err = harness.wait_for_msg(info_sub, ctx) + luaunit.assertNil(msg, "Expected to not receive capability info message with missing endpoints") + luaunit.assertEquals(err, 'timeout', + "Expected timeout with missing endpoints for capability info, got: " .. tostring(err)) + + ctx:cancel("test complete") +end + +local function main() + fiber.spawn(function() + luaunit.LuaUnit.run() + fiber.stop() + end) +end + +-- Only run tests if this file is executed directly (not via dofile) +if is_entry_point then + main() + fiber.main() +end diff --git a/tests/hal/test_manager_modemcard.lua b/tests/hal/test_manager_modemcard.lua new file mode 100644 index 00000000..0b28678b --- /dev/null +++ b/tests/hal/test_manager_modemcard.lua @@ -0,0 +1,192 @@ +-- Detect if this file is being run as the entry point +local this_file = debug.getinfo(1, "S").source:match("@?([^/]+)$") +local is_entry_point = arg and arg[0] and arg[0]:match("[^/]+$") == this_file + +if is_entry_point then + -- Match the test harness package.path setup (see tests/test.lua, + -- test_wifi.lua, test_metrics.lua, test_system.lua, test_core.lua) + package.path = "../../src/lua-fibers/?.lua;" -- fibers submodule src + .. "../../src/lua-trie/src/?.lua;" -- trie submodule src + .. "../../src/lua-bus/src/?.lua;" -- bus submodule src + .. "../../src/?.lua;" -- main src tree + .. "../../?.lua;" -- repo root (for tests.hal.harness) + .. "./test_utils/?.lua;" -- shared test utilities + .. package.path + .. ";/usr/lib/lua/?.lua;/usr/lib/lua/?/init.lua;" + .. "./harness/?.lua;" + + _G._TEST = true -- Enable test exports in source code + local log = require 'services.log' + local rxilog = require 'rxilog' + for _, mode in ipairs(rxilog.modes) do + log[mode.name] = function() end -- no-op logging during tests, comment out to see logs + end +end + +local luaunit = require 'luaunit' +local fiber = require 'fibers.fiber' +local context = require 'fibers.context' +local sleep = require 'fibers.sleep' +local channel = require 'fibers.channel' + +local harness = require 'tests.hal.harness' +local mock = require 'tests.utils.mock' +local commands = require 'tests.utils.ShimCommands' + +local function make_monitor_event(is_added, address) + local sign = is_added and '(+)' or '(-)' + return string.format("%s %s [DUMMY MANAFACUTER] Dummy Modem Module", sign, + address) +end + +local function release(module_path) + package.loaded[module_path] = nil +end + +TestHalModemcardManager = {} + +function TestHalModemcardManager:test_detector() + local ctx = context.with_cancel(context.background()) + + -- Setup mmcli backend command mocks + local mmcli = require 'tests.hal.harness.backends.mmcli' + local mmcli_mock = mock.new_module( + "services.hal.drivers.modem.mmcli", + mmcli + ) + mmcli_mock:apply() + + local monitor_modems_cmd = commands.new_command(ctx) + mmcli.set_command("monitor_modems", monitor_modems_cmd) + + local modem_manager_module = require 'services.hal.managers.modemcard' + local modem_manager = modem_manager_module.new() + local modem_detect_ch = modem_manager.modem_detect_channel + local modem_remove_ch = modem_manager.modem_remove_channel + fiber.spawn(function() + modem_manager:_detector(ctx) + end) + + local address = "/org/freedesktop/ModemManager1/Modem/0" + + -- Simulate modem addition + monitor_modems_cmd.stdout_ch:put(make_monitor_event(true, address)) + local detected_address, err = harness.wait_for_channel(modem_detect_ch, ctx) + luaunit.assertNil(err) + luaunit.assertEquals(detected_address, address) + + + -- Simulate modem removal + monitor_modems_cmd.stdout_ch:put(make_monitor_event(false, address)) + local removed_address, err = harness.wait_for_channel(modem_remove_ch, ctx) + luaunit.assertNil(err) + luaunit.assertEquals(removed_address, address) + + -- Simulate no modems + monitor_modems_cmd.stdout_ch:put("No modems found") + local no_event, err = harness.wait_for_channel(modem_detect_ch, ctx) + luaunit.assertNil(no_event) + luaunit.assertEquals(err, 'timeout') + + -- Verify command call counts + luaunit.assertEquals(monitor_modems_cmd.calls.start, 1) + + ctx:cancel('test complete') + -- Cleanup modules from cache + mmcli_mock:clear() + release "services.hal.managers.modemcard" + sleep.sleep(0) -- allow fiber to exit + + luaunit.assertEquals(monitor_modems_cmd.calls.wait, 1) + luaunit.assertEquals(monitor_modems_cmd.calls.kill, 1) + luaunit.assertEquals(monitor_modems_cmd.calls.close, 1) +end + +function TestHalModemcardManager:test_manager() + local ctx = context.with_cancel(context.background()) + + -- Setup modem driver mock (this will be a driver instance) + local modem_mock = mock.new_object { + init = { nil }, + apply_capabilities = { {}, nil }, + spawn = {} + } + + local modem_inst = modem_mock:create_instance() + + -- Setup modem driver module mock (to return the driver instance) + local modem_driver_module_mock = mock.new_module( + "services.hal.drivers.modem", + { + -- a mock can take a function for dynamic behavior or table of return values for static behavior + new = function(mctx, address) + modem_inst.address = address + modem_inst.device = "dummy" + modem_inst.ctx = mctx + return modem_inst + end + } + ) + modem_driver_module_mock:apply() + + -- Setup mmcli backend command mocks + local mmcli = require 'tests.hal.harness.backends.mmcli' + local mmcli_mock = mock.new_module( + "services.hal.drivers.modem.mmcli", + mmcli + ) + mmcli_mock:apply() + + local modem_manager_module = require 'services.hal.managers.modemcard' + local modem_manager = modem_manager_module.new() + local modem_detect_ch = modem_manager.modem_detect_channel + local modem_remove_ch = modem_manager.modem_remove_channel + local device_event_q = channel.new() + local capability_info_q = channel.new() + fiber.spawn(function() + modem_manager:_manager( + ctx, + nil, + device_event_q, + capability_info_q + ) + end) + local address = "/org/freedesktop/ModemManager1/Modem/0" + + -- Simulate modem detection + modem_detect_ch:put(address) + local device_event, err = harness.wait_for_channel(device_event_q, ctx) + luaunit.assertNil(err) + luaunit.assertEquals(device_event.connected, true) + luaunit.assertEquals(device_event.data.port, "dummy") + luaunit.assertEquals(modem_inst._calls.init, 1) + luaunit.assertEquals(modem_inst._calls.apply_capabilities, 1) + luaunit.assertEquals(modem_inst._calls.spawn, 1) + + -- Simulate modem removal + modem_remove_ch:put(address) + local device_event, err = harness.wait_for_channel(device_event_q, ctx) + luaunit.assertNil(err) + luaunit.assertEquals(device_event.connected, false) + luaunit.assertEquals(device_event.data.port, "dummy") + + ctx:cancel('test complete') + -- Cleanup modules from cache + modem_driver_module_mock:clear() + mmcli_mock:clear() + release "services.hal.managers.modemcard" + sleep.sleep(0) -- allow fiber to exit +end + +local function main() + fiber.spawn(function() + luaunit.LuaUnit.run() + fiber.stop() + end) +end + +-- Only run tests if this file is executed directly (not via dofile) +if is_entry_point then + main() + fiber.main() +end diff --git a/tests/hal/test_modem_driver.lua b/tests/hal/test_modem_driver.lua new file mode 100644 index 00000000..dc4fd7de --- /dev/null +++ b/tests/hal/test_modem_driver.lua @@ -0,0 +1,134 @@ +-- Standalone demo test: bring up a dummy modem with no SIM, +-- wait a bit, then insert a SIM and wait again so we can +-- observe modem logs on the console. + +local this_file = debug.getinfo(1, "S").source:match("@?([^/]+)$") +local is_entry_point = arg and arg[0] and arg[0]:match("[^/]+$") == this_file + +if is_entry_point then + package.path = "../../src/lua-fibers/?.lua;" -- fibers submodule src + .. "../../src/lua-trie/src/?.lua;" -- trie submodule src + .. "../../src/lua-bus/src/?.lua;" -- bus submodule src + .. "../../src/?.lua;" -- main src tree + .. "../../?.lua;" -- repo root (for tests.hal.harness) + .. "./test_utils/?.lua;" -- shared test utilities + .. package.path + .. ";/usr/lib/lua/?.lua;/usr/lib/lua/?/init.lua;" + .. "./harness/?.lua;" + + _G._TEST = true +end + +local luaunit = require 'luaunit' +local fiber = require 'fibers.fiber' +local context = require 'fibers.context' +local sleep = require 'fibers.sleep' + +local harness = require 'tests.hal.harness' +local dummy_modem = require 'tests.hal.harness.devices.modem' + +TestHalModemDriver = {} +TestHalModemSimInsert = {} + +-- function TestHalModemDriver:test_modem_enable() +-- local hal, ctx, bus, conn, new_msg = harness.new_hal_env() + +-- hal:start(ctx, bus:connect()) + +-- local config = { +-- managers = { +-- modemcard = {}, +-- }, +-- } +-- harness.publish_config(conn, new_msg, config) + +-- local modem = dummy_modem.new(context.with_cancel(ctx), 'failed') +-- modem:set_address_index("0") +-- modem:set_mmcli_information{ +-- modem = { +-- generic = { +-- device = "/fake/port0", +-- ["equipment-identifier"] = "123456789", +-- } +-- } +-- } +-- end + +function TestHalModemSimInsert:test_modem_sim_insert_logs() + -- Bring up a fresh HAL environment with the modemcard manager + -- running under HAL so we can call capability control endpoints + -- like sim_detect. + local hal, ctx, bus, conn, new_msg = harness.new_hal_env() + + -- Start HAL control loop. + hal:start(ctx, bus:connect()) + + -- Enable only the modemcard manager via HAL config. + local config = { + managers = { + modemcard = {}, + }, + } + harness.publish_config(conn, new_msg, config) + + -- Create a dummy modem with no SIM present. + local modem = dummy_modem.new(context.with_cancel(ctx)) + modem:set_address_index("0") + modem:set_mmcli_information{ + modem = { + generic = { + device = "/fake/port0", + ["equipment-identifier"] = "123456789", + } + } + } + + -- Make the modem appear on the bus so the manager and driver + -- start up and begin logging. + local wr_err = modem:appear() + luaunit.assertNil(wr_err, "Failed to write to monitor command stdout") + + -- Wait a bit with no SIM inserted so we can see the initial + -- modem logs and then trigger SIM detection via the HAL + -- capability control endpoint. + sleep.sleep(1) + + -- Call the modem sim_detect endpoint so the driver starts + -- its SIM detection / warm-swap logic. The modem capability + -- id is the IMEI we set above (123456789). + local bus_pkg = require 'bus' + local new_msg_fn = bus_pkg.new_msg or new_msg + local sim_detect_sub = conn:request(new_msg_fn( + { 'hal', 'capability', 'modem', '123456789', 'control', 'sim_detect' }, + {} + )) + -- We don't assert on the response; this is just to ensure the + -- request is consumed and does not block. + local _ = sim_detect_sub:next_msg_with_context_op(context.with_timeout(ctx, 5)):perform() + sleep.sleep(5) + + -- Now insert a SIM and wait again to observe the resulting + -- modem state changes and logs. + local sim = dummy_modem.new_sim() + sim:set_imsi("001010123456789") + sim:set_operator("00101", "Test Operator") + modem:insert_sim(sim) + + sleep.sleep(2) + + -- Cleanly shut down the environment. + ctx:cancel('test complete') + sleep.sleep(0) +end + +local function main() + fiber.spawn(function() + luaunit.LuaUnit.run() + fiber.stop() + end) +end + +if is_entry_point then + main() + fiber.main() +end diff --git a/tests/utils/ShimCommands.lua b/tests/utils/ShimCommands.lua new file mode 100644 index 00000000..ee418bcb --- /dev/null +++ b/tests/utils/ShimCommands.lua @@ -0,0 +1,137 @@ +local channel = require 'fibers.channel' +local context = require 'fibers.context' +local op = require 'fibers.op' +local unpack = table.unpack or unpack +local dispatcher = require 'tests.utils.dispatcher' + +local COMMAND_STATE = { + CREATED = 'created', + STARTED = 'started', + FLUSHED = 'flushed', + KILLED = 'killed' +} + +local Pipe = {} +Pipe.__index = Pipe + +function Pipe:read_line_op() + return self.ch:get_op() +end + +function Pipe:close() + self.parent_cmd.calls.close = self.parent_cmd.calls.close + 1 +end + +local function new_pipe(parent_cmd, ch) + local self = { + parent_cmd = parent_cmd, + ch = ch + } + return setmetatable(self, Pipe) +end + +local Command = {} +Command.__index = Command + +local function new_command(ctx, static_returns) + local self = { + ctx = context.with_cancel(ctx), + stdout_ch = channel.new(), + calls = { + setprdeathsig = 0, + setpgid = 0, + start = 0, + run = 0, + wait = 0, + kill = 0, + close = 0, + }, + static_returns = static_returns or {}, + } + return setmetatable(self, Command) +end + +function Command:start() + self.calls.start = self.calls.start + 1 + if self.static_returns.start then + return unpack(self.static_returns.start) + end +end + +function Command:setprdeathsig(sig) + self.calls.setprdeathsig = self.calls.setprdeathsig + 1 + if self.static_returns.setprdeathsig then + return unpack(self.static_returns.setprdeathsig) + end +end + +function Command:setpgid() + self.calls.setpgid = self.calls.setpgid + 1 + if self.static_returns.setpgid then + return unpack(self.static_returns.setpgid) + end +end + +function Command:stdout_pipe() + return new_pipe(self, self.stdout_ch) +end + +function Command:combined_output() + local out_pipe = self:stdout_pipe() + + local buf = "" + local continue = true + local function push_data(data) + if data then + buf = buf .. data + else + continue = false + end + end + + while continue and not self.ctx:err() do + local read_op = op.choice( + out_pipe:read_line_op() + ):wrap(push_data) + op.choice( + read_op, + self.ctx:done_op() + ):perform() + end + + return buf, nil +end + +function Command:run() + self.calls.run = self.calls.run + 1 + if self.static_returns.run then + return unpack(self.static_returns.run) + end +end + +function Command:wait() + self.calls.wait = self.calls.wait + 1 + if self.static_returns.wait then + return unpack(self.static_returns.wait) + end +end + +function Command:kill() + self.ctx:cancel('killed') + self.calls.kill = self.calls.kill + 1 + if self.static_returns.kill then + return unpack(self.static_returns.kill) + end +end + +-- function Command:close() +-- self.ctx:cancel('ended') +-- self.calls.close = self.calls.close + 1 +-- if self.static_returns.close then +-- return unpack(self.static_returns.close) +-- end +-- end + +return { + new_command = new_command +} diff --git a/tests/test_utils/SimuCommands.lua b/tests/utils/SimuCommands.lua similarity index 100% rename from tests/test_utils/SimuCommands.lua rename to tests/utils/SimuCommands.lua diff --git a/tests/test_utils/assertions.lua b/tests/utils/assertions.lua similarity index 100% rename from tests/test_utils/assertions.lua rename to tests/utils/assertions.lua diff --git a/tests/utils/mock.lua b/tests/utils/mock.lua new file mode 100644 index 00000000..b6ff82db --- /dev/null +++ b/tests/utils/mock.lua @@ -0,0 +1,67 @@ +local unpack = table.unpack or unpack + +local function build_mock_function(on_run) + return function(...) + if type(on_run) ~= "function" and type(on_run) ~= "table" then + error("Invalid on_run type for mock function") + end + if type(on_run) == "function" then + return on_run(...) + end + return unpack(on_run) + end +end + +local ModuleMock = {} +ModuleMock.__index = ModuleMock + +function ModuleMock:apply() + if not self.module_path then return end + package.loaded[self.module_path] = self +end + +function ModuleMock:clear() + if not self.module_path then return end + package.loaded[self.module_path] = nil +end + +local ObjectMock = {} +ObjectMock.__index = ObjectMock + +function ObjectMock:create_instance() + local instance = setmetatable({ _calls = {} }, ObjectMock) + for k, v in pairs(self.method_table) do + if k ~= "_calls" then + local fn = build_mock_function(v) + instance[k] = function(...) + instance._calls[k] = instance._calls[k] + 1 + return fn(...) + end + instance._calls[k] = 0 + end + end + return instance +end + +local function new_module(module_path, method_table) + local mock = setmetatable({ _calls = {}, module_path = module_path }, ModuleMock) + for k, v in pairs(method_table) do + local fn = build_mock_function(v) + mock[k] = function(...) + mock._calls[k] = mock._calls[k] + 1 + return fn(...) + end + mock._calls[k] = 0 + end + return mock +end + +local function new_object(method_table) + local mock = setmetatable({ method_table = method_table }, ObjectMock) + return mock +end + +return { + new_module = new_module, + new_object = new_object +} diff --git a/tests/test_utils/shim_shifter.lua b/tests/utils/shim_shifter.lua similarity index 100% rename from tests/test_utils/shim_shifter.lua rename to tests/utils/shim_shifter.lua diff --git a/tests/test_utils/utils.lua b/tests/utils/utils.lua similarity index 100% rename from tests/test_utils/utils.lua rename to tests/utils/utils.lua