diff --git a/driveAtlas.sh b/driveAtlas.sh index 753039b..a424423 100644 --- a/driveAtlas.sh +++ b/driveAtlas.sh @@ -828,44 +828,179 @@ generate_nuc_layout() { #------------------------------------------------------------------------------ # generate_large1_layout # -# Generates ASCII art representation of a large1 chassis (Rosewill RSV-L4500U). -# Shows storage controllers, 2 M.2 NVMe slots, and 15 front bays in 3x5 grid. +# Rosewill RSV-L4500U (4U, 427x178x635 mm) -- FRONT ELEVATION with the 3x 120 mm +# fan wall and the perforated drop-down door CUT AWAY, exposing all 15 internal +# 3.5" bays: 3 removable modules side by side, 5 drives stacked FLAT in each, so +# from the front every bay reads as a WIDE, SHORT slot -> a 5-row x 3-column grid. +# Rack ears with D-handles at both ends; slim I/O strip on the right. +# +# Bay numbering is a driveAtlas LOCAL convention (the chassis documents none): +# 1-5 = LEFT module top->bottom, 6-10 = MIDDLE, 11-15 = RIGHT. # # Args: # $1 - Hostname to display in the layout header # -# Side effects: Calls build_drive_map() to populate DRIVE_MAP -#------------------------------------------------------------------------------ +# Side effects: calls build_drive_map() (populates DRIVE_MAP) then da_color_init. +# Every emitted line is exactly 96 display columns wide, in colour or mono. +# +# The art printfs embed our own ANSI colour vars (DA_*) and literal box-drawing; +# device names are always passed as %s args and pre-truncated, so SC2059 does +# not apply here. +# shellcheck disable=SC2059 generate_large1_layout() { - local hostname="$1" - build_drive_map + local host="${1:-unknown}" - # large1 has 3 stacks of 5 bays at front (15 total) + 2 M.2 slots - # Physical bay mapping TBD - current mapping is by controller order - printf "┌─────────────────────────────────────────────────────────────────────────┐\n" - printf "│ %-69s │\n" "$hostname - Rosewill RSV-L4500U (15x 3.5\" Bays)" - printf "│ │\n" - printf "│ Storage Controllers: │\n" - while IFS= read -r ctrl; do - [[ -n "$ctrl" ]] && printf "│ %-69.69s│\n" "$ctrl" - done < <(get_storage_controllers) - printf "│ │\n" - printf "│ M.2 NVMe: M1: %-10s M2: %-10s │\n" "${DRIVE_MAP[m2-1]:-EMPTY}" "${DRIVE_MAP[m2-2]:-EMPTY}" - printf "│ │\n" - printf "│ Front Bays (3 stacks x 5 rows): [slot 5 of each stack MUST stay empty] │\n" - printf "│ Stack A Stack B Stack C │\n" - printf "│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │\n" - printf "│ │1:%-8s│ │2:%-8s│ │3:%-8s│ │\n" "${DRIVE_MAP[1]:-EMPTY}" "${DRIVE_MAP[2]:-EMPTY}" "${DRIVE_MAP[3]:-EMPTY}" - printf "│ ├──────────┤ ├──────────┤ ├──────────┤ │\n" - printf "│ │4:%-8s│ │5:%-8s│ │6:%-8s│ │\n" "${DRIVE_MAP[4]:-EMPTY}" "${DRIVE_MAP[5]:-EMPTY}" "${DRIVE_MAP[6]:-EMPTY}" - printf "│ ├──────────┤ ├──────────┤ ├──────────┤ │\n" - printf "│ │7:%-8s│ │8:%-8s│ │9:%-8s│ │\n" "${DRIVE_MAP[7]:-EMPTY}" "${DRIVE_MAP[8]:-EMPTY}" "${DRIVE_MAP[9]:-EMPTY}" - printf "│ ├──────────┤ ├──────────┤ ├──────────┤ │\n" - printf "│ │10:%-7s│ │11:%-7s│ │12:%-7s│ │\n" "${DRIVE_MAP[10]:-EMPTY}" "${DRIVE_MAP[11]:-EMPTY}" "${DRIVE_MAP[12]:-EMPTY}" - printf "│ ├──────────┤ ├──────────┤ ├──────────┤ │\n" - printf "│ │13:%-7s│ │14:%-7s│ │15:%-7s│ │\n" "${DRIVE_MAP[13]:-EMPTY}" "${DRIVE_MAP[14]:-EMPTY}" "${DRIVE_MAP[15]:-EMPTY}" - printf "│ └──────────┘ └──────────┘ └──────────┘ │\n" - printf "└─────────────────────────────────────────────────────────────────────────┘\n" + if declare -F build_drive_map >/dev/null 2>&1; then + build_drive_map || true # log_* goes to stderr; stdout stays clean + fi + da_color_init + + # ---- local helpers --------------------------------------------------- + _l1_rep() { # _l1_rep GLYPH N + if declare -F da_repeat >/dev/null 2>&1; then + da_repeat "$1" "$2" + else + local g="$1" n="$2" o='' + while [ "$n" -gt 0 ]; do o+="$g"; n=$((n - 1)); done + printf '%s' "$o" + fi + } + # DRIVE_MAP may be undeclared, empty, or (defensively) not associative + local has_map=0 + case "$(declare -p DRIVE_MAP 2>/dev/null)" in *'declare -A'*) has_map=1 ;; esac + _l1_map() { # _l1_map KEY -> value or '' + [ "$has_map" = 1 ] || return 0 + printf '%s' "${DRIVE_MAP[$1]:-}" + } + _l1_txt() { # _l1_txt TEXT [COLOUR] -- plain 96-col line + printf ' %s%-92.92s%s \n' "${2:-}" "$1" "${DA_RST}" + } + _l1_band() { # _l1_band TEXT WIDTH -- text centred in ▒ + local t="$1" w="$2" l r + [ "${#t}" -gt "$w" ] && t="${t:0:w}" + l=$(( (w - ${#t}) / 2 )); r=$(( w - ${#t} - l )) + printf '%s%s%s' "$(_l1_rep '▒' "$l")" "$t" "$(_l1_rep '▒' "$r")" + } + + # ---- geometry -------------------------------------------------------- + # 96 cols = 2 pad + 5 ear + 1 + 80 body + 1 + 5 ear + 2 pad + # interior = 78 = 6 fan sliver + 1 tear + 56 grid + 1 tear + 6 fan + 8 I/O + # grid 56 = 3 modules of 18 (16-col bay cell + 2 borders) + 2 gaps + local IW=78 + local i n p v rule16 + rule16="$(_l1_rep '─' 16)" + + # ---- bay cells: ONE colour run each, no churn inside a glyph run ------ + local -a cells=() + for ((i = 1; i <= 15; i++)); do + v="$(_l1_map "$i")" + printf -v n '%2d' "$i" + if [ -n "$v" ]; then + printf -v p '%-12.12s' "$v" + cells[i]="${DA_OK}▌${n} ${p}${DA_ST}" + else + printf -v p '%-12.12s' 'empty' + cells[i]="${DA_DIM}░${n} ${p}${DA_ST}" + fi + done + + # ---- module box pieces (18 wide) ------------------------------------- + local mtop="┌${rule16}┐" mmid="├${rule16}┤" mbot="└${rule16}┘" + local gtop="${mtop} ${mtop} ${mtop}" + local gmid="${mmid} ${mmid} ${mmid}" + local gbot="${mbot} ${mbot} ${mbot}" + + # ---- ghosted 120 mm fan rings, sliced open by the cutaway ------------- + local -a fanL=( + ' ╭─' ' ╭─╯ ' ' ╭╯ ' ' │ ' ' │ ' ' │ ' + ' │ ' ' │ ' ' ╰╮ ' ' ╰─╮ ' ' ╰─' + ) + local -a fanR=( + '─╮ ' ' ╰─╮ ' ' ╰╮ ' ' │ ' ' │ ' ' │ ' + ' │ ' ' │ ' ' ╭╯ ' ' ╭─╯ ' '─╯ ' + ) + # ---- slim front I/O strip (right-hand edge of the front face) -------- + local -a io=( + ' ╭────╮ ' ' │I/O │ ' ' │▄pwr│ ' ' │▄rst│ ' ' │▀led│ ' ' ├────┤ ' + ' │▄▄▄▄│ ' ' │USB3│ ' ' │▄▄▄▄│ ' ' │USB3│ ' ' ╰────╯ ' + ) + + # ---- banner, door band, top-cover caption ---------------------------- + local ban=' FAN WALL + DUST DOOR CUT AWAY - 15 BAYS EXPOSED ' banner lp rp + lp=$(( (IW - ${#ban}) / 2 )); rp=$(( IW - ${#ban} - lp )) + banner="$(_l1_rep '╌' "$lp")${ban}$(_l1_rep '╌' "$rp")" + + # perforated door band: two captions in a field of punched holes, keyed + # barrel lock dead centre on the door's lower edge + local door dlw=$(( (IW - 3) / 2 )) + door="$(_l1_band ' perforated drop-down door ' "$dlw")" + door="${door}${DA_HL}▐█▌${DA_DIM}$(_l1_band ' washable dust filter ' $(( IW - dlw - 3 )))" + + local tlbl='[ TOP COVER - modules release from above ]' ttop + ttop="┌──${tlbl}$(_l1_rep '─' $(( IW - 2 - ${#tlbl} )))┐" + + # ---- header ---------------------------------------------------------- + local hp mp + printf -v hp '%-38.38s' "$host" + printf -v mp '%40.40s' 'Rosewill RSV-L4500U 4U 15x 3.5" bays' + printf '%s\n' " ${DA_CYN}driveAtlas${DA_ST} :: ${DA_HL}${hp}${DA_SR}${mp}${DA_RST} " + _l1_txt 'FRONT ELEVATION, fan wall cut away - 5 bays tall x 3 modules wide, drives lie FLAT' "${DA_DIM}" + printf '%96s\n' '' + + # ---- chassis: rack ears carry the big vertical D-handles ------------- + local eartop='┌───┐' earhole='│ ▄ │' earcapT='│╭─╮│' earbody='││ ││' + local earcapB='│╰─╯│' earbot='└───┘' + local labs l1 l2 l3 + printf -v l1 '%*s%-*s' 5 '' 13 'LEFT 1-5' + printf -v l2 '%*s%-*s' 3 '' 15 'MIDDLE 6-10' + printf -v l3 '%*s%-*s' 3 '' 15 'RIGHT 11-15' + labs="${l1} ${l2} ${l3}" + local d grid E + + printf '%s\n' " ${DA_HL}${eartop}${DA_SR} ${ttop} ${DA_HL}${eartop}${DA_RST} " + printf '%s\n' " ${DA_HL}${earhole}${DA_SR} │${DA_DIM}${banner}${DA_ST}│ ${DA_HL}${earhole}${DA_RST} " + printf '%s\n' " ${DA_HL}${earcapT}${DA_SR} │${DA_DIM} ╎${DA_HL}${labs}${DA_DIM}╎ ${DA_ST}│ ${DA_HL}${earcapT}${DA_RST} " + + for ((d = 0; d < 11; d++)); do + case "$d" in + 0) grid="$gtop" ;; + 10) grid="$gbot" ;; + 1|3|5|7|9) + i=$(( (d + 1) / 2 )) + grid="│${cells[$i]}│ │${cells[$(( i + 5 ))]}│ │${cells[$(( i + 10 ))]}│" ;; + *) grid="$gmid" ;; + esac + E="${DA_HL}${earbody}${DA_SR}" + printf '%s\n' " ${E} │${DA_DIM}${fanL[$d]}╎${DA_ST}${grid}${DA_DIM}╎${fanR[$d]}${io[$d]}${DA_ST}│ ${E}${DA_RST} " + done + + printf '%s\n' " ${DA_HL}${earcapB}${DA_SR} │${DA_DIM}$(_l1_rep '╌' "$IW")${DA_ST}│ ${DA_HL}${earcapB}${DA_RST} " + printf '%s\n' " ${DA_HL}${earhole}${DA_SR} │${DA_DIM}${door}${DA_ST}│ ${DA_HL}${earhole}${DA_RST} " + printf '%s\n' " ${DA_HL}${earbot}${DA_SR} └$(_l1_rep '─' "$IW")┘ ${DA_HL}${earbot}${DA_RST} " + printf '%96s\n' '' + + # ---- legend + the three things you must know ------------------------- + local m1 m2 extra m1p m2p exp + m1="$(_l1_map m2-1)"; [ -n "$m1" ] || m1='-' + m2="$(_l1_map m2-2)"; [ -n "$m2" ] || m2='-' + extra='' + for ((i = 1; i <= 6; i++)); do + v="$(_l1_map "int-$i")" + [ -n "$v" ] && extra="${extra}int-${i} ${v} " + done + [ -n "$extra" ] || extra='-' + printf -v m1p '%-12.12s' "$m1" + printf -v m2p '%-12.12s' "$m2" + printf -v exp '%-29.29s' "$extra" + + printf '%s\n' " ${DA_ST}LEGEND ${DA_OK}▌${DA_ST} drive present ${DA_DIM}░${DA_ST} bay empty$(_l1_rep ' ' 50)${DA_RST} " + printf '%s\n' " ${DA_ST}NOT IN GRID m2-1 ${DA_HL}${m1p}${DA_SR} m2-2 ${DA_HL}${m2p}${DA_SR} internal ${DA_HL}${exp}${DA_RST} " + _l1_txt 'BAYS numbering is a driveAtlas LOCAL convention (the chassis documents none): 1-5' "${DA_ST}" + _l1_txt ' LEFT module top->bottom, 6-10 MIDDLE, 11-15 RIGHT -- identify a drive by SERIAL.' "${DA_ST}" + _l1_txt 'NOTE no backplane, no per-bay LEDs, not hot-swap: every drive is cabled direct to the' "${DA_DIM}" + _l1_txt ' board/HBA, and is serviced by removing the TOP COVER, not through the front door.' "${DA_DIM}" + + unset -f _l1_rep _l1_map _l1_txt _l1_band } #------------------------------------------------------------------------------