| #!/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" |
| 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 "${@}" |