dapm-graph 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. #!/bin/sh
  2. # SPDX-License-Identifier: GPL-2.0
  3. #
  4. # Generate a graph of the current DAPM state for an audio card
  5. #
  6. # Copyright 2024 Bootlin
  7. # Author: Luca Ceresoli <luca.ceresol@bootlin.com>
  8. set -eu
  9. STYLE_COMPONENT_ON="color=dodgerblue;style=bold"
  10. STYLE_COMPONENT_OFF="color=gray40;style=filled;fillcolor=gray90"
  11. STYLE_NODE_ON="shape=box,style=bold,color=green4,fillcolor=white"
  12. STYLE_NODE_OFF="shape=box,style=filled,color=gray30,fillcolor=gray95"
  13. # Print usage and exit
  14. #
  15. # $1 = exit return value
  16. # $2 = error string (required if $1 != 0)
  17. usage()
  18. {
  19. if [ "${1}" -ne 0 ]; then
  20. echo "${2}" >&2
  21. fi
  22. echo "
  23. Generate a graph of the current DAPM state for an audio card.
  24. The DAPM state can be obtained via debugfs for a card on the local host or
  25. a remote target, or from a local copy of the debugfs tree for the card.
  26. Usage:
  27. $(basename $0) [options] -c CARD - Local sound card
  28. $(basename $0) [options] -c CARD -r REMOTE_TARGET - Card on remote system
  29. $(basename $0) [options] -d STATE_DIR - Local directory
  30. Options:
  31. -c CARD Sound card to get DAPM state of
  32. -r REMOTE_TARGET Get DAPM state from REMOTE_TARGET via SSH and SCP
  33. instead of using a local sound card
  34. -d STATE_DIR Get DAPM state from a local copy of a debugfs tree
  35. -o OUT_FILE Output file (default: dapm.dot)
  36. -D Show verbose debugging info
  37. -h Print this help and exit
  38. The output format is implied by the extension of OUT_FILE:
  39. * Use the .dot extension to generate a text graph representation in
  40. graphviz dot syntax.
  41. * Any other extension is assumed to be a format supported by graphviz for
  42. rendering, e.g. 'png', 'svg', and will produce both the .dot file and a
  43. picture from it. This requires the 'dot' program from the graphviz
  44. package.
  45. "
  46. exit ${1}
  47. }
  48. # Connect to a remote target via SSH, collect all DAPM files from debufs
  49. # into a tarball and get the tarball via SCP into $3/dapm.tar
  50. #
  51. # $1 = target as used by ssh and scp, e.g. "root@192.168.1.1"
  52. # $2 = sound card name
  53. # $3 = temp dir path (present on the host, created on the target)
  54. # $4 = local directory to extract the tarball into
  55. #
  56. # Requires an ssh+scp server, find and tar+gz on the target
  57. #
  58. # Note: the tarball is needed because plain 'scp -r' from debugfs would
  59. # copy only empty files
  60. grab_remote_files()
  61. {
  62. echo "Collecting DAPM state from ${1}"
  63. dbg_echo "Collected DAPM state in ${3}"
  64. ssh "${1}" "
  65. set -eu &&
  66. cd \"/sys/kernel/debug/asoc/${2}\" &&
  67. find * -type d -exec mkdir -p ${3}/dapm-tree/{} \; &&
  68. find * -type f -exec cp \"{}\" \"${3}/dapm-tree/{}\" \; &&
  69. cd ${3}/dapm-tree &&
  70. tar cf ${3}/dapm.tar ."
  71. scp -q "${1}:${3}/dapm.tar" "${3}"
  72. mkdir -p "${4}"
  73. tar xf "${tmp_dir}/dapm.tar" -C "${4}"
  74. }
  75. # Parse a widget file and generate graph description in graphviz dot format
  76. #
  77. # Skips any file named "bias_level".
  78. #
  79. # $1 = temporary work dir
  80. # $2 = component name
  81. # $3 = widget filename
  82. process_dapm_widget()
  83. {
  84. local tmp_dir="${1}"
  85. local c_name="${2}"
  86. local w_file="${3}"
  87. local dot_file="${tmp_dir}/main.dot"
  88. local links_file="${tmp_dir}/links.dot"
  89. local w_name="$(basename "${w_file}")"
  90. local w_tag="${c_name}_${w_name}"
  91. if [ "${w_name}" = "bias_level" ]; then
  92. return 0
  93. fi
  94. dbg_echo " + Widget: ${w_name}"
  95. cat "${w_file}" | (
  96. read line
  97. if echo "${line}" | grep -q ': On '
  98. then local node_style="${STYLE_NODE_ON}"
  99. else local node_style="${STYLE_NODE_OFF}"
  100. fi
  101. local w_type=""
  102. while read line; do
  103. # Collect widget type if present
  104. if echo "${line}" | grep -q '^widget-type '; then
  105. local w_type_raw="$(echo "$line" | cut -d ' ' -f 2)"
  106. dbg_echo " - Widget type: ${w_type_raw}"
  107. # Note: escaping '\n' is tricky to get working with both
  108. # bash and busybox ash, so use a '%' here and replace it
  109. # later
  110. local w_type="%n[${w_type_raw}]"
  111. fi
  112. # Collect any links. We could use "in" links or "out" links,
  113. # let's use "in" links
  114. if echo "${line}" | grep -q '^in '; then
  115. local w_route=$(echo "$line" | awk -F\" '{print $2}')
  116. local w_src=$(echo "$line" |
  117. awk -F\" '{print $6 "_" $4}' |
  118. sed 's/^(null)_/ROOT_/')
  119. dbg_echo " - Input route from: ${w_src}"
  120. dbg_echo " - Route: ${w_route}"
  121. local w_edge_attrs=""
  122. if [ "${w_route}" != "static" ]; then
  123. w_edge_attrs=" [label=\"${w_route}\"]"
  124. fi
  125. echo " \"${w_src}\" -> \"$w_tag\"${w_edge_attrs}" >> "${links_file}"
  126. fi
  127. done
  128. echo " \"${w_tag}\" [label=\"${w_name}${w_type}\",${node_style}]" |
  129. tr '%' '\\' >> "${dot_file}"
  130. )
  131. }
  132. # Parse the DAPM tree for a sound card component and generate graph
  133. # description in graphviz dot format
  134. #
  135. # $1 = temporary work dir
  136. # $2 = component directory
  137. # $3 = "ROOT" for the root card directory, empty otherwise
  138. process_dapm_component()
  139. {
  140. local tmp_dir="${1}"
  141. local c_dir="${2}"
  142. local c_name="${3}"
  143. local is_component=0
  144. local dot_file="${tmp_dir}/main.dot"
  145. local links_file="${tmp_dir}/links.dot"
  146. local c_attribs=""
  147. if [ -z "${c_name}" ]; then
  148. is_component=1
  149. # Extract directory name into component name:
  150. # "./cs42l51.0-004a/dapm" -> "cs42l51.0-004a"
  151. c_name="$(basename $(dirname "${c_dir}"))"
  152. fi
  153. dbg_echo " * Component: ${c_name}"
  154. if [ ${is_component} = 1 ]; then
  155. if [ -f "${c_dir}/bias_level" ]; then
  156. c_onoff=$(sed -n -e 1p "${c_dir}/bias_level" | awk '{print $1}')
  157. dbg_echo " - bias_level: ${c_onoff}"
  158. if [ "$c_onoff" = "On" ]; then
  159. c_attribs="${STYLE_COMPONENT_ON}"
  160. elif [ "$c_onoff" = "Off" ]; then
  161. c_attribs="${STYLE_COMPONENT_OFF}"
  162. fi
  163. fi
  164. echo "" >> "${dot_file}"
  165. echo " subgraph \"${c_name}\" {" >> "${dot_file}"
  166. echo " cluster = true" >> "${dot_file}"
  167. echo " label = \"${c_name}\"" >> "${dot_file}"
  168. echo " ${c_attribs}" >> "${dot_file}"
  169. fi
  170. # Create empty file to ensure it will exist in all cases
  171. >"${links_file}"
  172. # Iterate over widgets in the component dir
  173. for w_file in ${c_dir}/*; do
  174. process_dapm_widget "${tmp_dir}" "${c_name}" "${w_file}"
  175. done
  176. if [ ${is_component} = 1 ]; then
  177. echo " }" >> "${dot_file}"
  178. fi
  179. cat "${links_file}" >> "${dot_file}"
  180. }
  181. # Parse the DAPM tree for a sound card and generate graph description in
  182. # graphviz dot format
  183. #
  184. # $1 = temporary work dir
  185. # $2 = directory tree with DAPM state (either in debugfs or a mirror)
  186. process_dapm_tree()
  187. {
  188. local tmp_dir="${1}"
  189. local dapm_dir="${2}"
  190. local dot_file="${tmp_dir}/main.dot"
  191. echo "digraph G {" > "${dot_file}"
  192. echo " fontname=\"sans-serif\"" >> "${dot_file}"
  193. echo " node [fontname=\"sans-serif\"]" >> "${dot_file}"
  194. echo " edge [fontname=\"sans-serif\"]" >> "${dot_file}"
  195. # Process root directory (no component)
  196. process_dapm_component "${tmp_dir}" "${dapm_dir}/dapm" "ROOT"
  197. # Iterate over components
  198. for c_dir in "${dapm_dir}"/*/dapm
  199. do
  200. process_dapm_component "${tmp_dir}" "${c_dir}" ""
  201. done
  202. echo "}" >> "${dot_file}"
  203. }
  204. main()
  205. {
  206. # Parse command line
  207. local out_file="dapm.dot"
  208. local card_name=""
  209. local remote_target=""
  210. local dapm_tree=""
  211. local dbg_on=""
  212. while getopts "c:r:d:o:Dh" arg; do
  213. case $arg in
  214. c) card_name="${OPTARG}" ;;
  215. r) remote_target="${OPTARG}" ;;
  216. d) dapm_tree="${OPTARG}" ;;
  217. o) out_file="${OPTARG}" ;;
  218. D) dbg_on="1" ;;
  219. h) usage 0 ;;
  220. *) usage 1 ;;
  221. esac
  222. done
  223. shift $(($OPTIND - 1))
  224. if [ -n "${dapm_tree}" ]; then
  225. if [ -n "${card_name}${remote_target}" ]; then
  226. usage 1 "Cannot use -c and -r with -d"
  227. fi
  228. echo "Using local tree: ${dapm_tree}"
  229. elif [ -n "${remote_target}" ]; then
  230. if [ -z "${card_name}" ]; then
  231. usage 1 "-r requires -c"
  232. fi
  233. echo "Using card ${card_name} from remote target ${remote_target}"
  234. elif [ -n "${card_name}" ]; then
  235. echo "Using local card: ${card_name}"
  236. else
  237. usage 1 "Please choose mode using -c, -r or -d"
  238. fi
  239. # Define logging function
  240. if [ "${dbg_on}" ]; then
  241. dbg_echo() {
  242. echo "$*" >&2
  243. }
  244. else
  245. dbg_echo() {
  246. :
  247. }
  248. fi
  249. # Filename must have a dot in order the infer the format from the
  250. # extension
  251. if ! echo "${out_file}" | grep -qE '\.'; then
  252. echo "Missing extension in output filename ${out_file}" >&2
  253. usage
  254. exit 1
  255. fi
  256. local out_fmt="${out_file##*.}"
  257. local dot_file="${out_file%.*}.dot"
  258. dbg_echo "dot file: $dot_file"
  259. dbg_echo "Output file: $out_file"
  260. dbg_echo "Output format: $out_fmt"
  261. tmp_dir="$(mktemp -d /tmp/$(basename $0).XXXXXX)"
  262. trap "{ rm -fr ${tmp_dir}; }" INT TERM EXIT
  263. if [ -z "${dapm_tree}" ]
  264. then
  265. dapm_tree="/sys/kernel/debug/asoc/${card_name}"
  266. fi
  267. if [ -n "${remote_target}" ]; then
  268. dapm_tree="${tmp_dir}/dapm-tree"
  269. grab_remote_files "${remote_target}" "${card_name}" "${tmp_dir}" "${dapm_tree}"
  270. fi
  271. # In all cases now ${dapm_tree} contains the DAPM state
  272. process_dapm_tree "${tmp_dir}" "${dapm_tree}"
  273. cp "${tmp_dir}/main.dot" "${dot_file}"
  274. if [ "${out_file}" != "${dot_file}" ]; then
  275. dot -T"${out_fmt}" "${dot_file}" -o "${out_file}"
  276. fi
  277. echo "Generated file ${out_file}"
  278. }
  279. main "${@}"