|  | #!/bin/sh | 
|  | # SPDX-License-Identifier: GPL-2.0 | 
|  | # | 
|  | # Generate a graph of the current DAPM state for an audio card | 
|  | # | 
|  | # Copyright 2024 Bootlin | 
|  | # Author: Luca Ceresoli <luca.ceresol@bootlin.com> | 
|  |  | 
|  | set -eu | 
|  |  | 
|  | STYLE_COMPONENT_ON="color=dodgerblue;style=bold" | 
|  | STYLE_COMPONENT_OFF="color=gray40;style=filled;fillcolor=gray90" | 
|  | STYLE_NODE_ON="shape=box,style=bold,color=green4,fillcolor=white" | 
|  | STYLE_NODE_OFF="shape=box,style=filled,color=gray30,fillcolor=gray95" | 
|  |  | 
|  | # Print usage and exit | 
|  | # | 
|  | # $1 = exit return value | 
|  | # $2 = error string (required if $1 != 0) | 
|  | usage() | 
|  | { | 
|  | if [  "${1}" -ne 0 ]; then | 
|  | echo "${2}" >&2 | 
|  | fi | 
|  |  | 
|  | echo " | 
|  | Generate a graph of the current DAPM state for an audio card. | 
|  |  | 
|  | The DAPM state can be obtained via debugfs for a card on the local host or | 
|  | a remote target, or from a local copy of the debugfs tree for the card. | 
|  |  | 
|  | Usage: | 
|  | $(basename $0) [options] -c CARD                  - Local sound card | 
|  | $(basename $0) [options] -c CARD -r REMOTE_TARGET - Card on remote system | 
|  | $(basename $0) [options] -d STATE_DIR             - Local directory | 
|  |  | 
|  | Options: | 
|  | -c CARD             Sound card to get DAPM state of | 
|  | -r REMOTE_TARGET    Get DAPM state from REMOTE_TARGET via SSH and SCP | 
|  | instead of using a local sound card | 
|  | -d STATE_DIR        Get DAPM state from a local copy of a debugfs tree | 
|  | -o OUT_FILE         Output file (default: dapm.dot) | 
|  | -D                  Show verbose debugging info | 
|  | -h                  Print this help and exit | 
|  |  | 
|  | The output format is implied by the extension of OUT_FILE: | 
|  |  | 
|  | * Use the .dot extension to generate a text graph representation in | 
|  | graphviz dot syntax. | 
|  | * Any other extension is assumed to be a format supported by graphviz for | 
|  | rendering, e.g. 'png', 'svg', and will produce both the .dot file and a | 
|  | picture from it. This requires the 'dot' program from the graphviz | 
|  | package. | 
|  | " | 
|  |  | 
|  | exit ${1} | 
|  | } | 
|  |  | 
|  | # Connect to a remote target via SSH, collect all DAPM files from debufs | 
|  | # into a tarball and get the tarball via SCP into $3/dapm.tar | 
|  | # | 
|  | # $1 = target as used by ssh and scp, e.g. "root@192.168.1.1" | 
|  | # $2 = sound card name | 
|  | # $3 = temp dir path (present on the host, created on the target) | 
|  | # $4 = local directory to extract the tarball into | 
|  | # | 
|  | # Requires an ssh+scp server, find and tar+gz on the target | 
|  | # | 
|  | # Note: the tarball is needed because plain 'scp -r' from debugfs would | 
|  | # copy only empty files | 
|  | grab_remote_files() | 
|  | { | 
|  | echo "Collecting DAPM state from ${1}" | 
|  | dbg_echo "Collected DAPM state in ${3}" | 
|  |  | 
|  | ssh "${1}" " | 
|  | set -eu && | 
|  | cd \"/sys/kernel/debug/asoc/${2}\" && | 
|  | find * -type d -exec mkdir -p ${3}/dapm-tree/{} \; && | 
|  | find * -type f -exec cp \"{}\" \"${3}/dapm-tree/{}\" \; && | 
|  | cd ${3}/dapm-tree && | 
|  | tar cf ${3}/dapm.tar ." | 
|  | scp -q "${1}:${3}/dapm.tar" "${3}" | 
|  |  | 
|  | mkdir -p "${4}" | 
|  | tar xf "${tmp_dir}/dapm.tar" -C "${4}" | 
|  | } | 
|  |  | 
|  | # Parse a widget file and generate graph description in graphviz dot format | 
|  | # | 
|  | # Skips any file named "bias_level". | 
|  | # | 
|  | # $1 = temporary work dir | 
|  | # $2 = component name | 
|  | # $3 = widget filename | 
|  | process_dapm_widget() | 
|  | { | 
|  | local tmp_dir="${1}" | 
|  | local c_name="${2}" | 
|  | local w_file="${3}" | 
|  | local dot_file="${tmp_dir}/main.dot" | 
|  | local links_file="${tmp_dir}/links.dot" | 
|  |  | 
|  | local w_name="$(basename "${w_file}")" | 
|  | local w_tag="${c_name}_${w_name}" | 
|  |  | 
|  | if [ "${w_name}" = "bias_level" ]; then | 
|  | return 0 | 
|  | fi | 
|  |  | 
|  | dbg_echo "   + Widget: ${w_name}" | 
|  |  | 
|  | cat "${w_file}" | ( | 
|  | read line | 
|  |  | 
|  | if echo "${line}" | grep -q ': On ' | 
|  | then local node_style="${STYLE_NODE_ON}" | 
|  | else local node_style="${STYLE_NODE_OFF}" | 
|  | fi | 
|  |  | 
|  | local w_type="" | 
|  | while read line; do | 
|  | # Collect widget type if present | 
|  | if echo "${line}" | grep -q '^widget-type '; then | 
|  | local w_type_raw="$(echo "$line" | cut -d ' ' -f 2)" | 
|  | dbg_echo "     - Widget type: ${w_type_raw}" | 
|  |  | 
|  | # Note: escaping '\n' is tricky to get working with both | 
|  | # bash and busybox ash, so use a '%' here and replace it | 
|  | # later | 
|  | local w_type="%n[${w_type_raw}]" | 
|  | fi | 
|  |  | 
|  | # Collect any links. We could use "in" links or "out" links, | 
|  | # let's use "in" links | 
|  | if echo "${line}" | grep -q '^in '; then | 
|  | local w_route=$(echo "$line" | awk -F\" '{print $2}') | 
|  | local w_src=$(echo "$line" | | 
|  | awk -F\" '{print $6 "_" $4}' | | 
|  | sed  's/^(null)_/ROOT_/') | 
|  | dbg_echo "     - Input route from: ${w_src}" | 
|  | dbg_echo "     - Route: ${w_route}" | 
|  | local w_edge_attrs="" | 
|  | if [ "${w_route}" != "static" ]; then | 
|  | w_edge_attrs=" [label=\"${w_route}\"]" | 
|  | fi | 
|  | echo "  \"${w_src}\" -> \"$w_tag\"${w_edge_attrs}" >> "${links_file}" | 
|  | fi | 
|  | done | 
|  |  | 
|  | echo "    \"${w_tag}\" [label=\"${w_name}${w_type}\",${node_style}]" | | 
|  | tr '%' '\\' >> "${dot_file}" | 
|  | ) | 
|  | } | 
|  |  | 
|  | # Parse the DAPM tree for a sound card component and generate graph | 
|  | # description in graphviz dot format | 
|  | # | 
|  | # $1 = temporary work dir | 
|  | # $2 = component directory | 
|  | # $3 = "ROOT" for the root card directory, empty otherwise | 
|  | process_dapm_component() | 
|  | { | 
|  | local tmp_dir="${1}" | 
|  | local c_dir="${2}" | 
|  | local c_name="${3}" | 
|  | local is_component=0 | 
|  | local dot_file="${tmp_dir}/main.dot" | 
|  | local links_file="${tmp_dir}/links.dot" | 
|  | local c_attribs="" | 
|  |  | 
|  | if [ -z "${c_name}" ]; then | 
|  | is_component=1 | 
|  |  | 
|  | # Extract directory name into component name: | 
|  | #   "./cs42l51.0-004a/dapm" -> "cs42l51.0-004a" | 
|  | c_name="$(basename $(dirname "${c_dir}"))" | 
|  | fi | 
|  |  | 
|  | dbg_echo " * Component: ${c_name}" | 
|  |  | 
|  | if [ ${is_component} = 1 ]; then | 
|  | if [ -f "${c_dir}/bias_level" ]; then | 
|  | c_onoff=$(sed -n -e 1p "${c_dir}/bias_level" | awk '{print $1}') | 
|  | dbg_echo "   - bias_level: ${c_onoff}" | 
|  | if [ "$c_onoff" = "On" ]; then | 
|  | c_attribs="${STYLE_COMPONENT_ON}" | 
|  | elif [ "$c_onoff" = "Off" ]; then | 
|  | c_attribs="${STYLE_COMPONENT_OFF}" | 
|  | fi | 
|  | fi | 
|  |  | 
|  | echo ""                           >> "${dot_file}" | 
|  | echo "  subgraph \"${c_name}\" {" >> "${dot_file}" | 
|  | echo "    cluster = true"         >> "${dot_file}" | 
|  | echo "    label = \"${c_name}\""  >> "${dot_file}" | 
|  | echo "    ${c_attribs}"           >> "${dot_file}" | 
|  | fi | 
|  |  | 
|  | # Create empty file to ensure it will exist in all cases | 
|  | >"${links_file}" | 
|  |  | 
|  | # Iterate over widgets in the component dir | 
|  | for w_file in ${c_dir}/*; do | 
|  | process_dapm_widget "${tmp_dir}" "${c_name}" "${w_file}" | 
|  | done | 
|  |  | 
|  | if [ ${is_component} = 1 ]; then | 
|  | echo "  }" >> "${dot_file}" | 
|  | fi | 
|  |  | 
|  | cat "${links_file}" >> "${dot_file}" | 
|  | } | 
|  |  | 
|  | # Parse the DAPM tree for a sound card and generate graph description in | 
|  | # graphviz dot format | 
|  | # | 
|  | # $1 = temporary work dir | 
|  | # $2 = directory tree with DAPM state (either in debugfs or a mirror) | 
|  | process_dapm_tree() | 
|  | { | 
|  | local tmp_dir="${1}" | 
|  | local dapm_dir="${2}" | 
|  | local dot_file="${tmp_dir}/main.dot" | 
|  |  | 
|  | echo "digraph G {" > "${dot_file}" | 
|  | echo "  fontname=\"sans-serif\"" >> "${dot_file}" | 
|  | echo "  node [fontname=\"sans-serif\"]" >> "${dot_file}" | 
|  | echo "  edge [fontname=\"sans-serif\"]" >> "${dot_file}" | 
|  |  | 
|  | # Process root directory (no component) | 
|  | process_dapm_component "${tmp_dir}" "${dapm_dir}/dapm" "ROOT" | 
|  |  | 
|  | # Iterate over components | 
|  | for c_dir in "${dapm_dir}"/*/dapm | 
|  | do | 
|  | process_dapm_component "${tmp_dir}" "${c_dir}" "" | 
|  | done | 
|  |  | 
|  | echo "}" >> "${dot_file}" | 
|  | } | 
|  |  | 
|  | main() | 
|  | { | 
|  | # Parse command line | 
|  | local out_file="dapm.dot" | 
|  | local card_name="" | 
|  | local remote_target="" | 
|  | local dapm_tree="" | 
|  | local dbg_on="" | 
|  | while getopts "c:r:d:o:Dh" arg; do | 
|  | case $arg in | 
|  | c)  card_name="${OPTARG}"      ;; | 
|  | r)  remote_target="${OPTARG}"  ;; | 
|  | d)  dapm_tree="${OPTARG}"      ;; | 
|  | o)  out_file="${OPTARG}"       ;; | 
|  | D)  dbg_on="1"                 ;; | 
|  | h)  usage 0                    ;; | 
|  | *)  usage 1                    ;; | 
|  | esac | 
|  | done | 
|  | shift $(($OPTIND - 1)) | 
|  |  | 
|  | if [ -n "${dapm_tree}" ]; then | 
|  | if [ -n "${card_name}${remote_target}" ]; then | 
|  | usage 1 "Cannot use -c and -r with -d" | 
|  | fi | 
|  | echo "Using local tree: ${dapm_tree}" | 
|  | elif [ -n "${remote_target}" ]; then | 
|  | if [ -z "${card_name}" ]; then | 
|  | usage 1 "-r requires -c" | 
|  | fi | 
|  | echo "Using card ${card_name} from remote target ${remote_target}" | 
|  | elif [ -n "${card_name}" ]; then | 
|  | echo "Using local card: ${card_name}" | 
|  | else | 
|  | usage 1 "Please choose mode using -c, -r or -d" | 
|  | fi | 
|  |  | 
|  | # Define logging function | 
|  | if [ "${dbg_on}" ]; then | 
|  | dbg_echo() { | 
|  | echo "$*" >&2 | 
|  | } | 
|  | else | 
|  | dbg_echo() { | 
|  | : | 
|  | } | 
|  | fi | 
|  |  | 
|  | # Filename must have a dot in order the infer the format from the | 
|  | # extension | 
|  | if ! echo "${out_file}" | grep -qE '\.'; then | 
|  | echo "Missing extension in output filename ${out_file}" >&2 | 
|  | usage | 
|  | exit 1 | 
|  | fi | 
|  |  | 
|  | local out_fmt="${out_file##*.}" | 
|  | local dot_file="${out_file%.*}.dot" | 
|  |  | 
|  | dbg_echo "dot file:      $dot_file" | 
|  | dbg_echo "Output file:   $out_file" | 
|  | dbg_echo "Output format: $out_fmt" | 
|  |  | 
|  | tmp_dir="$(mktemp -d /tmp/$(basename $0).XXXXXX)" | 
|  | trap "{ rm -fr ${tmp_dir}; }" INT TERM EXIT | 
|  |  | 
|  | if [ -z "${dapm_tree}" ] | 
|  | then | 
|  | dapm_tree="/sys/kernel/debug/asoc/${card_name}" | 
|  | fi | 
|  | if [ -n "${remote_target}" ]; then | 
|  | dapm_tree="${tmp_dir}/dapm-tree" | 
|  | grab_remote_files "${remote_target}" "${card_name}" "${tmp_dir}" "${dapm_tree}" | 
|  | fi | 
|  | # In all cases now ${dapm_tree} contains the DAPM state | 
|  |  | 
|  | process_dapm_tree "${tmp_dir}" "${dapm_tree}" | 
|  | cp "${tmp_dir}/main.dot" "${dot_file}" | 
|  |  | 
|  | if [ "${out_file}" != "${dot_file}" ]; then | 
|  | dot -T"${out_fmt}" "${dot_file}" -o "${out_file}" | 
|  | fi | 
|  |  | 
|  | echo "Generated file ${out_file}" | 
|  | } | 
|  |  | 
|  | main "${@}" |