pl4y.store

6 - Shell

full_qmd.sh

.sh 596 lignes - 21KB

full_qmd.sh
#!/bin/bash
# full_qmd.sh - Bundle du depot en Quarto : doc + tests + .h + .c + .py + .sh,
# chaque fichier dans un bloc code type, theme "sketchy".
#
# DEUX MODES
#   MODE=single (defaut) : un seul .qmd. Simple, mais Deno/Quarto casse
#                          ("Maximum call stack size exceeded") au-dela de
#                          ~1-2 Mo, surtout avec embed-resources.
#   MODE=site            : projet Quarto multi-pages (recommande au-dela de 1 Mo).
#                          Sections decoupees en pages de SPLIT_KB max,
#                          recherche plein-texte integree, index global.
#
# Usage :
# TOUT EST ECRIT DANS UN SEUL DOSSIER ($OUTDIR, par defaut ./bundle-qmd/) :
# le .qmd, sketchy.css, sk-filter.html, et le rendu quarto. Le depot reste
# propre, et une seconde execution ne rescanne pas sa propre sortie.
#
# Usage :
#   MODE=site ./full_qmd.sh              # -> ./bundle-qmd/ (multi-pages)
#   ./full_qmd.sh                        # -> ./bundle-qmd/<depot>-full.qmd
#   ./full_qmd.sh out.qmd                # chemin explicite (mode single)
#   OUTDIR=/tmp/x ./full_qmd.sh          # ailleurs
#   PL4Y_SECTION=calypso ./full_qmd.sh   # section pl4y.store visee
#   SCOPE=hw/arm/calypso ./full_qmd.sh   # only this subdir
#   EXCLUDE='build|pc-bios' ./full_qmd.sh
#   MAX_KB=256 ./full_qmd.sh             # troncature par fichier
#   SPLIT_KB=300 MODE=site ./full_qmd.sh # taille max d'une page
#   EMBED=true ./full_qmd.sh             # html self-contained (risque de crash)
#   RENDER_MERMAID=0 ./full_qmd.sh       # .mmd en code au lieu de diagramme
#   RENDER=0 MODE=site ./full_qmd.sh     # sources .qmd seules, sans rendu

set -uo pipefail

case "${1:-}" in -h|--help) sed -n '2,30p' "$0"; exit 0;; esac

MODE="${MODE:-single}"
# Dossier unique de sortie, pour les DEUX modes. Le mode single ecrivait avant
# a la racine du depot (calypso-full.qmd + sketchy.css + sk-filter.html, puis
# calypso-full.html et calypso-full_files/ au rendu) : quatre artefacts laches
# dans le depot, que la passe suivante rescannait comme des sources.
# Nom derive du depot (osmo_egprs -> ./osmo_egprs-qmd) : les bundles de deux
# depots differents peuvent ainsi etre deposes cote a cote sans collision.
OUTDIR="${OUTDIR:-./$(basename "$(cd "$(dirname "$0")" && pwd)")-qmd}"
# Section pl4y.store visee par ce bundle : ecrite dans .pl4y-section a la racine
# du dossier de sortie. build.mjs (dans pl4y) la lit pour savoir sous quelle URL
# publier, plutot que de deviner d'apres le nom du dossier.
PL4Y_SECTION="${PL4Y_SECTION:-}"
OUT="${1:-}"
SCOPE="${SCOPE:-.}"
TITLE="${TITLE:-Calypso QEMU}"
SUBTITLE="${SUBTITLE:-Full source bundle - DSP C54x / osmocom-bb / SHUNT_LEGIT}"
MAX_KB="${MAX_KB:-512}"
SPLIT_KB="${SPLIT_KB:-400}"
RENDER_MERMAID="${RENDER_MERMAID:-1}"
EMBED="${EMBED:-false}"
# En mode site on rend par defaut : le dossier produit est alors DIRECTEMENT
# deposable dans pl4y/bundles/ (il contient _site/, du HTML). Sans rendu, ce
# n'est qu'un projet Quarto, que rien en aval ne sait publier.
# RENDER=0 pour ne generer que les sources .qmd.
if [ "$MODE" = "site" ]; then RENDER="${RENDER:-1}"; else RENDER="${RENDER:-0}"; fi
EXCLUDE_RE="${EXCLUDE:-subprojects|build|pc-bios|tests/functional|tests/qtest|tests/unit|tests/migration|tests/qemu-iotests|node_modules|\.git|\.pytest_cache}"

HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"

# Le dossier existe avant toute chose : les deux modes ecrivent dedans.
mkdir -p "$OUTDIR" || { echo "ERROR: mkdir '$OUTDIR'" >&2; exit 1; }
OUTDIR="$(cd "$OUTDIR" && pwd)"

# Slug de section : explicite via PL4Y_SECTION, sinon derive du depot.
# qemu-calypso est publie sous /calypso/ — d'ou la seule correspondance
# particuliere.
if [ -z "$PL4Y_SECTION" ]; then
    case "$(basename "$HERE")" in
        qemu-calypso) PL4Y_SECTION="calypso" ;;
        *)            PL4Y_SECTION="$(basename "$HERE")" ;;
    esac
fi
printf '%s\n' "$PL4Y_SECTION" > "$OUTDIR/.pl4y-section"

# Sans argument, le .qmd va dans $OUTDIR. Un argument explicite reste
# respecte : `./full_qmd.sh /tmp/x.qmd` ecrit bien la (et son css/js a cote).
if [ -z "$OUT" ]; then
    OUT="$OUTDIR/$(basename "$HERE")-full.qmd"
elif [ -d "$OUT" ]; then
    OUT="${OUT%/}/$(basename "$HERE")-full.qmd"
fi

MAXB=$(( MAX_KB * 1024 ))
SPLITB=$(( SPLIT_KB * 1024 ))
TMPD="$(mktemp -d)"
trap 'rm -rf "$TMPD"' EXIT

GITREV="$(git rev-parse --short HEAD 2>/dev/null || echo 'n/a')"
GITBRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'n/a')"
NOW="$(date -Iseconds)"

# --- cible + auto-exclusion -------------------------------------------------
SELF_FIXED=""      # fichier a exclure du scan (mode single)
SELF_PREFIX=""     # prefixe de dossier a exclure (mode site)

# $OUTDIR est exclu du scan dans LES DEUX modes. Auparavant le mode single
# n'excluait que le .qmd lui-meme : sketchy.css, sk-filter.html et le
# repertoire *_files/ du rendu precedent etaient reavales comme des sources a
# chaque execution, et le bundle grossissait a chaque passe.
SELF_PREFIX="${OUTDIR#"$HERE"/}/"
[ "$SELF_PREFIX" = "$OUTDIR/" ] && SELF_PREFIX=""   # $OUTDIR hors du depot

if [ "$MODE" = "site" ]; then
    DEST=""
else
    mkdir -p "$(dirname "$OUT")" || { echo "ERROR: mkdir pour '$OUT'" >&2; exit 1; }
    : > "$OUT" || { echo "ERROR: cannot write '$OUT'" >&2; exit 1; }
    OUT_ABS="$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")"
    SELF_FIXED="${OUT_ABS#"$HERE"/}"
    DEST="$OUT"
fi

echo "=== full_qmd.sh ==="
echo "Mode     : $MODE"
echo "Scope    : $SCOPE"
echo "Output   : $([ "$MODE" = site ] && echo "$OUTDIR/" || echo "$OUT")"
echo "Exclude  : $EXCLUDE_RE"
echo "Max/file : ${MAX_KB} ko$([ "$MODE" = site ] && echo " | page: ${SPLIT_KB} ko")"
echo

# ------------------------------------------------------------------ helpers --

_lang_for() {
    case "${1##*.}" in
        c|h)        echo c ;;
        cpp|cc|cxx) echo cpp ;;
        py)         echo python ;;
        sh|bash)    echo bash ;;
        md|qmd)     echo markdown ;;
        mmd)        echo mermaid ;;
        *)          echo default ;;
    esac
}

_fence_for() {
    local maxrun n
    maxrun=$(grep -oE '^`{3,}' "$1" 2>/dev/null | awk '{ if (length($0) > m) m = length($0) } END { print m+0 }')
    n=3; [ "${maxrun:-0}" -ge 3 ] && n=$(( maxrun + 1 ))
    printf '%*s' "$n" '' | tr ' ' '`'
}

_anchor_for() {
    printf 'f-%s' "$(printf '%s' "$1" | tr 'A-Z' 'a-z' | sed 's/[^a-z0-9]\+/-/g; s/^-//; s/-$//')"
}

_human() {
    if command -v numfmt >/dev/null 2>&1; then numfmt --to=iec --suffix=B "$1"; else echo "$1 B"; fi
}

_self_filter() {
    { if [ -n "$SELF_FIXED" ]; then grep -vxF "$SELF_FIXED"; else cat; fi; } \
  | { if [ -n "$SELF_PREFIX" ]; then grep -vF "$SELF_PREFIX"; else cat; fi; }
}

_find_into() {
    local excl="$1"; shift
    local args=() p first=1
    for p in "$@"; do
        if [ "$first" -eq 1 ]; then args+=( -name "$p" ); first=0
        else args+=( -o -name "$p" ); fi
    done
    find "$SCOPE" -type f \( "${args[@]}" \) 2>/dev/null \
        | grep -vE "$EXCLUDE_RE" \
        | { if [ -n "$excl" ]; then grep -vE "$excl"; else cat; fi; } \
        | sed 's|^\./||' \
        | _self_filter \
        | sort > "$TMPD/list"
    wc -l < "$TMPD/list"
}

# ------------------------------------------------------- emission d'un fichier

_add() {
    local f="$1" lang fence size nlines anchor ext eff
    ext="${f##*.}"
    lang="$(_lang_for "$f")"
    size=$(wc -c < "$f" 2>/dev/null || echo 0)
    nlines=$(wc -l < "$f" 2>/dev/null || echo 0)
    anchor="$(_anchor_for "$f")"

    {
        printf '\n## `%s` {#%s}\n\n' "$f" "$anchor"
        printf '::: {.file-meta}\n[.%s]{.tag} %s lignes - %s\n:::\n\n' "$ext" "$nlines" "$(_human "$size")"
    } >> "$DEST"

    if [ "$size" -eq 0 ]; then
        printf '::: {.callout-note appearance="simple"}\nFichier vide.\n:::\n' >> "$DEST"
        eff=0
    elif ! grep -Iq . "$f" 2>/dev/null; then
        printf '::: {.callout-warning appearance="simple"}\nFichier binaire - contenu omis.\n:::\n' >> "$DEST"
        eff=0
    else
        fence="$(_fence_for "$f")"
        if [ "$lang" = "mermaid" ] && [ "$RENDER_MERMAID" = "1" ]; then
            printf '%s{mermaid}\n' "$fence" >> "$DEST"
        else
            printf '%s{.%s .numberLines filename="%s"}\n' "$fence" "$lang" "$f" >> "$DEST"
        fi
        if [ "$size" -gt "$MAXB" ]; then
            head -c "$MAXB" "$f" >> "$DEST"
            printf '\n\n... [TRONQUE a %s ko - %s au total] ...\n' "$MAX_KB" "$(_human "$size")" >> "$DEST"
            eff=$MAXB
        else
            cat "$f" >> "$DEST"
            eff=$size
        fi
        printf '\n%s\n' "$fence" >> "$DEST"
    fi

    # manifest : section | page | fichier | ancre | lignes | taille
    printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
        "$SECNUM" "$(basename "${DEST%.qmd}")" "$f" "$anchor" "$nlines" "$(_human "$size")" \
        >> "$TMPD/manifest"

    PAGE_BYTES=$(( PAGE_BYTES + eff + 400 ))
}

# ------------------------------------------------------------- mode site : page

_new_page() {
    local base
    base="$(printf 'sec%s-%02d' "$SECNUM" "$PART")"
    DEST="$OUTDIR/$base.qmd"
    printf '%s\t%s\t%s\t%s\n' "$SECNUM" "$SECTITLE" "$base.qmd" "partie $PART" >> "$TMPD/pages"
    cat > "$DEST" <<EOF
---
title: "$SECTITLE$([ "$PART" -gt 1 ] && printf ' - partie %s' "$PART")"
---

EOF
    PAGE_BYTES=0
}

# ----------------------------------------------------------------- une section

_section() {
    SECNUM="$1"; SECTITLE="$2"
    local extra_excl="$3" n f fsize
    shift 3
    n=$(_find_into "$extra_excl" "$@")
    echo "Section $SECNUM : $n files"

    if [ "$MODE" = "site" ]; then
        PART=1; _new_page
        while IFS= read -r f; do
            [ -n "$f" ] || continue
            fsize=$(wc -c < "$f" 2>/dev/null || echo 0)
            [ "$fsize" -gt "$MAXB" ] && fsize=$MAXB
            if [ "$PAGE_BYTES" -gt 0 ] && [ $(( PAGE_BYTES + fsize )) -gt "$SPLITB" ]; then
                PART=$(( PART + 1 )); _new_page
            fi
            _add "$f"
        done < "$TMPD/list"
    else
        {
            printf '\n\n# %s {#sec-%s}\n\n' "$SECTITLE" "$SECNUM"
            printf '::: {.section-count}\n**%s fichiers**\n:::\n' "$n"
        } >> "$DEST"
        PAGE_BYTES=0
        while IFS= read -r f; do
            [ -n "$f" ] && _add "$f"
        done < "$TMPD/list"
    fi
}

# --------------------------------------------------------------------- le CSS

_write_css() {
    cat > "$1" <<'SKETCHYCSS'
@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@600;700&family=Architects+Daughter&family=JetBrains+Mono:wght@400;700&display=swap');

:root{
  --sk-ink:#20242e;
  --sk-paper:#fffdf6;
  --sk-red:#e4572e;
  --sk-teal:#17968a;
  --sk-yellow:#ffd469;
  --sk-rough:255px 12px 225px 15px / 15px 225px 15px 255px;
  --sk-rough2:15px 225px 15px 255px / 255px 15px 225px 15px;
}

body{
  background-color:var(--sk-paper);
  background-image:
    linear-gradient(rgba(32,36,46,.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(32,36,46,.05) 1px, transparent 1px);
  background-size:28px 28px;
  font-family:'Architects Daughter','Neucha',ui-rounded,system-ui,sans-serif;
  color:var(--sk-ink);
}

h1.title{
  font-family:'Caveat',cursive; font-size:3.6rem; line-height:1.05;
  display:inline-block; padding:.1em .5em; transform:rotate(-1.1deg);
  background:var(--sk-yellow); border:3px solid var(--sk-ink);
  border-radius:var(--sk-rough); box-shadow:6px 6px 0 var(--sk-ink);
}
.subtitle{ font-size:1.2rem; opacity:.75; transform:rotate(.4deg); display:inline-block; }

h1:not(.title){
  font-family:'Caveat',cursive; font-size:2.9rem; margin-top:3.2rem;
  border-bottom:4px solid var(--sk-ink); border-radius:var(--sk-rough2);
  padding-bottom:.15em; transform:rotate(-.35deg);
}
h2{
  font-family:'JetBrains Mono',monospace; font-size:1.05rem; font-weight:700;
  margin-top:2.6rem; padding:.35em .7em; display:inline-block;
  background:#fff; border:2.5px dashed var(--sk-ink);
  border-radius:var(--sk-rough); box-shadow:3px 3px 0 rgba(23,150,138,.35);
}
h2 code{ background:none; color:var(--sk-ink); padding:0; font-size:1em; }

div.sourceCode, pre:not(.sourceCode){
  border:2.5px solid var(--sk-ink);
  border-radius:var(--sk-rough);
  box-shadow:5px 5px 0 rgba(32,36,46,.9), 10px 10px 0 rgba(228,87,46,.18);
  background:#fffef9 !important;
  transform:rotate(-.12deg);
}
code, pre, code span{ font-family:'JetBrains Mono',ui-monospace,monospace !important; }
div.sourceCode{ max-height:70vh; overflow:auto; }
code.sourceCode > span > a:first-child::before{ color:rgba(32,36,46,.28); }

.code-with-filename .code-with-filename-file{
  font-family:'Caveat',cursive !important; font-size:1.2rem; font-weight:700;
  background:var(--sk-yellow) !important; color:var(--sk-ink) !important;
  border:2.5px solid var(--sk-ink); border-bottom:none;
  border-radius:12px 110px 6px 6px / 40px 8px 6px 6px;
  transform:rotate(-.5deg); display:inline-block; padding:.15em .9em;
}
.code-with-filename .code-with-filename-file pre{ border:none; box-shadow:none; background:none; }

.file-meta{ font-size:.9rem; opacity:.8; margin:.1rem 0 .8rem .2rem; }
.file-meta .tag{
  background:var(--sk-teal); color:#fff; padding:.05em .55em; margin-right:.5em;
  border-radius:var(--sk-rough2); font-family:'JetBrains Mono',monospace; font-size:.8rem;
}
.section-count{
  display:inline-block; padding:.1em .8em; background:#fff;
  border:2px solid var(--sk-red); color:var(--sk-red);
  border-radius:var(--sk-rough2); transform:rotate(.7deg); margin-bottom:1rem;
}
.bundle-index table{ font-family:'JetBrains Mono',monospace; font-size:.82rem; width:100%; }
.bundle-index{
  border:2.5px solid var(--sk-ink); border-radius:var(--sk-rough);
  padding:.6rem 1rem; background:#fff; box-shadow:5px 5px 0 rgba(23,150,138,.3);
}
.bundle-index a{ text-decoration:none; }
.bundle-index tr:hover{ background:rgba(255,212,105,.35); }

#TOC, nav[role=doc-toc]{ font-family:'JetBrains Mono',monospace; font-size:.78rem; }
#TOC a.active, nav[role=doc-toc] a.active{ background:var(--sk-yellow); border-radius:var(--sk-rough2); }
#quarto-sidebar{ font-family:'JetBrains Mono',monospace; font-size:.85rem; }

.sk-filter{ position:sticky; top:0; z-index:50; padding:.4rem 0 .8rem; background:var(--sk-paper); }
.sk-filter input{
  width:100%; font-family:'Caveat',cursive; font-size:1.4rem; padding:.25em .8em;
  background:#fff; border:2.5px solid var(--sk-ink); border-radius:var(--sk-rough);
  box-shadow:4px 4px 0 rgba(228,87,46,.35); outline:none;
}
.sk-filter input:focus{ box-shadow:4px 4px 0 var(--sk-teal); }

a{ color:var(--sk-red); text-decoration-style:wavy; text-decoration-thickness:1px; }
blockquote{ border-left:4px solid var(--sk-teal); }
SKETCHYCSS
}

_write_filter_js() {
    cat > "$1" <<'FILTERJS'
<script>
document.addEventListener('DOMContentLoaded', function () {
  var box = document.getElementById('sk-filter');
  if (!box) return;
  var rows = Array.from(document.querySelectorAll('.bundle-index tbody tr'));
  var toc  = Array.from(document.querySelectorAll('#TOC a, nav[role=doc-toc] a'));
  box.addEventListener('input', function () {
    var q = box.value.trim().toLowerCase();
    rows.forEach(function (r) {
      r.style.display = (!q || r.textContent.toLowerCase().includes(q)) ? '' : 'none';
    });
    toc.forEach(function (a) {
      var li = a.closest('li') || a;
      li.style.display = (!q || a.textContent.toLowerCase().includes(q)) ? '' : 'none';
    });
  });
});
</script>
FILTERJS
}

: > "$TMPD/manifest"
: > "$TMPD/pages"

# =========================================================== MODE SINGLE ======

if [ "$MODE" != "site" ]; then

cat >> "$OUT" <<EOF
---
title: "$TITLE"
subtitle: "$SUBTITLE"
date: "$NOW"
date-format: "YYYY-MM-DD HH:mm"
lang: fr
format:
  html:
    theme: [sketchy]
    css: sketchy.css
    toc: true
    toc-depth: 2
    toc-location: left
    toc-title: "Fichiers"
    toc-expand: 1
    number-sections: false
    anchor-sections: true
    smooth-scroll: true
    code-copy: true
    code-overflow: scroll
    code-block-bg: true
    highlight-style: arrow
    embed-resources: $EMBED
    include-after-body: sk-filter.html
    grid:
      sidebar-width: 340px
      body-width: 1150px
      margin-width: 140px
    mermaid:
      theme: neutral
---

::: {.sk-filter}
<input id="sk-filter" type="text" placeholder="filtrer les fichiers (ex: dsp, .c, shunt)..." autocomplete="off">
:::

::: {.callout-note appearance="simple" icon=false}
**Bundle plat du depot** - doc, tests, headers, sources, python, shell.
Scope \`$SCOPE\` - git \`$GITBRANCH@$GITREV\` - genere $NOW
- exclusions \`$EXCLUDE_RE\` - troncature ${MAX_KB} ko/fichier.
:::
EOF

_write_css "$(dirname "$OUT")/sketchy.css"
_write_filter_js "$(dirname "$OUT")/sk-filter.html"

fi

# ============================================================= MODE SITE ======

if [ "$MODE" = "site" ]; then
    _write_css "$OUTDIR/sketchy.css"
    _write_filter_js "$OUTDIR/sk-filter.html"
fi

# ------------------------------------------------------------------ sections

_section 1 "1 - Documentation" ''                                   '*.md' '*.mmd' '*.qmd'
_section 2 "2 - Tests"         ''                                   'test_*.py' 'conftest.py'
_section 3 "3 - Headers"       ''                                   '*.h'
_section 4 "4 - Sources"       ''                                   '*.c' '*.cpp'
_section 5 "5 - Python"        '/tests/|test_.*\.py$|conftest\.py$' '*.py'
_section 6 "6 - Shell"         ''                                   '*.sh' '*.bash'

# ------------------------------------------------------- index + config site --

if [ "$MODE" = "site" ]; then

    # index.qmd : table globale, liens inter-pages
    {
        cat <<EOF
---
title: "$TITLE"
subtitle: "$SUBTITLE"
date: "$NOW"
date-format: "YYYY-MM-DD HH:mm"
---

::: {.callout-note appearance="simple" icon=false}
**Bundle du depot** - doc, tests, headers, sources, python, shell.
Scope \`$SCOPE\` - git \`$GITBRANCH@$GITREV\` - genere $NOW
- exclusions \`$EXCLUDE_RE\` - troncature ${MAX_KB} ko/fichier - pages de ${SPLIT_KB} ko max.
:::

::: {.sk-filter}
<input id="sk-filter" type="text" placeholder="filtrer les fichiers (ex: dsp, .c, shunt)..." autocomplete="off">
:::
EOF
        cursec=""
        while IFS=$'\t' read -r sec page f anchor nlines size; do
            if [ "$sec" != "$cursec" ]; then
                [ -n "$cursec" ] && printf '\n:::\n'
                title="$(awk -F'\t' -v s="$sec" '$1==s {print $2; exit}' "$TMPD/pages")"
                printf '\n# %s\n\n::: {.bundle-index}\n\n' "$title"
                printf '| Fichier | Lignes | Taille |\n|:---|---:|---:|\n'
                cursec="$sec"
            fi
            printf '| [`%s`](%s.html#%s) | %s | %s |\n' "$f" "$page" "$anchor" "$nlines" "$size"
        done < "$TMPD/manifest"
        printf '\n:::\n'
    } > "$OUTDIR/index.qmd"

    # _quarto.yml
    {
        cat <<EOF
project:
  type: website
  output-dir: _site

website:
  title: "$TITLE"
  search:
    location: sidebar
    type: textbox
  sidebar:
    style: floating
    collapse-level: 1
    contents:
      - text: "Index"
        href: index.qmd
EOF
        cursec=""
        while IFS=$'\t' read -r sec title page part; do
            if [ "$sec" != "$cursec" ]; then
                printf '      - section: "%s"\n        contents:\n' "$title"
                cursec="$sec"
            fi
            printf '          - href: %s\n            text: "%s"\n' "$page" "$part"
        done < "$TMPD/pages"

        cat <<EOF

format:
  html:
    theme: [sketchy]
    css: sketchy.css
    toc: true
    toc-depth: 2
    toc-location: right
    toc-title: "Fichiers"
    number-sections: false
    anchor-sections: true
    smooth-scroll: true
    code-copy: true
    code-overflow: scroll
    code-block-bg: true
    highlight-style: arrow
    embed-resources: false
    include-after-body: sk-filter.html
    mermaid:
      theme: neutral
EOF
    } > "$OUTDIR/_quarto.yml"
fi

# -------------------------------------------------------------------- footer

if [ "$MODE" != "site" ]; then
cat >> "$OUT" <<EOF

# Fin du bundle

::: {.section-count}
genere $NOW - \`$(basename "$0")\`
:::
EOF
fi

echo
echo "=== DONE ==="
if [ "$MODE" = "site" ]; then
    echo "Output : $OUTDIR/  ($(ls "$OUTDIR"/*.qmd | wc -l) pages, $(du -sh "$OUTDIR" | cut -f1))"
    echo "Rendu  : (cd \"$OUTDIR\" && quarto render)"
    echo "Preview: (cd \"$OUTDIR\" && quarto preview)"
else
    echo "Output : $OUT ($(du -h "$OUT" | cut -f1), $(wc -l < "$OUT") lignes)"
    echo "         + sketchy.css + sk-filter.html dans $(dirname "$OUT")/"
    echo "Rendu  : quarto render \"$OUT\"   (sortie dans $(dirname "$OUT")/)"
    if [ "$(wc -c < "$OUT")" -gt 1500000 ]; then
        echo
        echo "!! $(du -h "$OUT" | cut -f1) dans un seul document : Deno risque de casser"
        echo "   (RangeError: Maximum call stack size exceeded). Utilise MODE=site."
    fi
fi

if [ "$RENDER" = "1" ]; then
    if command -v quarto >/dev/null 2>&1; then
        if [ "$MODE" = "site" ]; then (cd "$OUTDIR" && quarto render); else quarto render "$OUT"; fi
    else
        echo "quarto introuvable dans le PATH - rendu ignore." >&2
    fi
fi

make-overlay.sh

.sh 138 lignes - 7,0KB

make-overlay.sh
#!/bin/bash
# make-overlay.sh — back-port qemu-src (working tree, SOURCE OF TRUTH) into the
#   qemu-calypso overlay. Reverse of make-fork.sh (which does overlay -> fork).
#
#     genuine QEMU  +  qemu-calypso (overlay)   =  qemu-src (assembled working tree)
#     make-fork.sh    : overlay + genuine  ->  fork/working-tree
#     make-overlay.sh : working-tree       ->  overlay            (this script)
#
# Copies every file the overlay TRACKS (git ls-files) from qemu-src into the overlay,
# preserving layout. Files that live only in the overlay (backups, *.minimal) and all
# of vanilla QEMU are left untouched. Nothing is ever deleted.
#
# New Calypso files created in qemu-src (e.g. calypso.nu.env) are NOT auto-added:
# `git add` them in the overlay once, then this script keeps them in sync.
#
# Usage: ./make-overlay.sh [--dry-run] [OVERLAY_DIR] [SRC_DIR]
set -euo pipefail
DRY=0; [ "${1:-}" = "--dry-run" ] && { DRY=1; shift; }
OVERLAY="${1:-/opt/GSM/qemu-calypso}"
SRC="${2:-$(cd "$(dirname "$0")" && pwd)}"
[ -d "$OVERLAY/.git" ] || { echo "!! overlay git repo not found: $OVERLAY" >&2; exit 1; }
[ -f "$SRC/VERSION" ]  || { echo "!! qemu-src (VERSION) not found: $SRC" >&2; exit 1; }
echo "src (truth) : $SRC"
echo "overlay     : $OVERLAY"
[ "$DRY" = 1 ] && echo "mode        : DRY-RUN (no writes)"
echo "----"
sync=0; same=0; only=0
while IFS= read -r f; do
    if [ -f "$SRC/$f" ]; then
        if cmp -s "$SRC/$f" "$OVERLAY/$f" 2>/dev/null; then
            same=$((same+1))
        else
            echo "  sync  $f"
            if [ "$DRY" = 0 ]; then
                mkdir -p "$OVERLAY/$(dirname "$f")"
                cp -a "$SRC/$f" "$OVERLAY/$f"
            fi
            sync=$((sync+1))
        fi
    else
        only=$((only+1))
    fi
done < <(cd "$OVERLAY" && git ls-files)

# --- Passe 2 : auto-ajout du NOUVEL ARBRE Calypso ---------------------------
# Les repertoires Calypso de l'overlay sont 100% overlay (absents de QEMU vanilla).
# On ramene tout NOUVEAU fichier cree dans qemu-src sous ces racines (nouvelles
# sources calypso_*, docs unifies, MASTER/STATUS/TODO) et on le `git add` dans
# l'overlay pour qu'il soit suivi ensuite. Exclut artefacts de build et backups.
# Racines surchargées via CALYPSO_OVERLAY_ROOTS.
# [2026-07-29] Ajout des quatre racines nées du refactor. Elles sont 100 %
# Calypso (absentes de QEMU vanilla), donc éligibles à l'auto-ajout :
#   run_modules/    le lanceur modulaire (contrat check/status/start/wait/stop)
#   tmux_modules/   une disposition tmux par profil
#   environnement/  la configuration découpée par domaine
#   firmware/       l'image layer1 versionnée avec le dépôt
# Sans elles, 102 fichiers — tout le lanceur — restaient hors de l'overlay, donc
# hors image et hors ISO, sans aucun message. On n'ajoute QUE du Calypso : les
# répertoires vanilla (accel/, block/, target/…) doivent rester dehors, c'est le
# principe du montage overlay + genuine.
ADD_ROOTS="${CALYPSO_OVERLAY_ROOTS:-hw/arm/calypso tools/calypso-ipc-device run_modules tmux_modules environnement firmware}"
added=0
for root in $ADD_ROOTS; do
    [ -d "$SRC/$root" ] || continue
    while IFS= read -r f; do
        case "$f" in
            *.o|*.d|*.orig|*~|*.pyc|*.a|*.so|*.tmp|*.tmp.*|*.swp|*.rej) continue;;
            *.bak|*.bak.*|*.bak-*|*.bak_*|*preNoCell*|*preBRINT0*|*preFNPROBE*) continue;;
            */build/*|*/.git/*|*/meson-*|*/.ninja*) continue;;
        esac
        # deja tracke dans l'overlay -> gere par la passe 1, on saute
        if (cd "$OVERLAY" && git ls-files --error-unmatch "$f" >/dev/null 2>&1); then
            continue
        fi
        echo "  add   $f"
        if [ "$DRY" = 0 ]; then
            mkdir -p "$OVERLAY/$(dirname "$f")"
            cp -a "$SRC/$f" "$OVERLAY/$f"
            (cd "$OVERLAY" && git add "$f")
        fi
        added=$((added+1))
    done < <(cd "$SRC" && find "$root" -type f | sort)
done

# --- Passe 2b : fichiers Calypso a la RACINE de qemu-src ---------------------
# Les env de modes (calypso.env socle + calypso_<mode>.env) vivent a la racine,
# hors ADD_ROOTS -> la passe 2 ne les voit pas. On les auto-ajoute ici (100%%
# Calypso, absents de QEMU vanilla). Globs surchargeables CALYPSO_OVERLAY_ROOT_GLOBS.
# [2026-07-27] RAPPORT_DFBDET.md ajoute : rapport d enquete indexe dans le README,
# il doit suivre l overlay comme run_results.md (sinon lien mort cote overlay).
# [2026-07-28] glob RAPPORT_*.md + PLAN_*.md : couvre RAPPORT_DFBDET/OPCODES/BRINT0/
# SYNTHESE et PLAN_APPLICATION sans re-editer ce script a chaque nouveau rapport.
# [2026-07-29] start-oqc.sh : point d'entrée du dépôt frère (osmo-nitb), appelé
# par la documentation ; il vivait à la racine sans être suivi par l'overlay.
# [2026-08-04] `tools/*.py` AJOUTE — les 11 outils d'analyse Calypso qui vivent a
# la racine de tools/ (corr_iq.py, tic54x-dis.py, mailbox-annote.py, fcch_*.py,
# gmsk_grgsm.py, iq_proxy.py, doppler.py, dsp_txt2bin.py, irda_capture.py,
# mailbox-gdb.py) n'etaient couverts par RIEN : ADD_ROOTS ne liste que
# `tools/calypso-ipc-device`, et la passe 1 ne voit que ce qui est deja tracke.
# Un outil NOUVEAU restait donc hors overlay, hors image et hors ISO **sans aucun
# message** — exactement le mode d'echec que la passe 2 avait ete ecrite pour
# supprimer (les 102 fichiers du lanceur, cf. le commentaire du 29/07).
#
# ⚠️ POURQUOI UN GLOB ET PAS UNE RACINE `tools`. `tools/` n'est PAS 100 % Calypso :
# il contient `ebpf/` et `i386/`, qui sont du QEMU vanilla. L'ajouter a ADD_ROOTS
# ferait entrer du vanilla dans l'overlay et casserait le principe overlay+genuine.
# Le glob ne prend que les `.py` de la RACINE de tools/, tous Calypso (verifie le
# 04/08 : 11 fichiers, aucun vanilla). Si un jour QEMU livre un `tools/*.py`
# vanilla, ce glob devra etre restreint a une liste explicite.
ROOT_GLOBS="${CALYPSO_OVERLAY_ROOT_GLOBS:-calypso*.env QUICK_START.md run_results.md RAPPORT_*.md PLAN_*.md start-oqc.sh TODO.md tools/*.py}"
for pat in $ROOT_GLOBS; do
    for f in $SRC/$pat; do
        [ -f "$f" ] || continue
        rel="${f#$SRC/}"
        if (cd "$OVERLAY" && git ls-files --error-unmatch "$rel" >/dev/null 2>&1); then
            continue   # deja tracke -> passe 1
        fi
        echo "  add   $rel"
        if [ "$DRY" = 0 ]; then
            # [2026-08-04] mkdir -p ajoute : depuis que ROOT_GLOBS accepte un
            # sous-repertoire (tools/*.py), la destination peut ne pas exister
            # dans l'overlay. Sans ca, `cp` echouait et `set -e` tuait le script.
            mkdir -p "$OVERLAY/$(dirname "$rel")"
            cp -a "$f" "$OVERLAY/$rel"
            (cd "$OVERLAY" && git add "$rel")
        fi
        added=$((added+1))
    done
done

echo "----"
if [ "$DRY" = 1 ]; then
    echo "$sync file(s) WOULD sync, $added new file(s) WOULD add, $same unchanged, $only overlay-only (untouched)"
    echo "re-run without --dry-run to apply."
else
    echo "$sync file(s) synced, $added new file(s) added (git add), $same unchanged, $only overlay-only (untouched)"
fi
echo "review: cd $OVERLAY && git status -s && git diff --stat"

run_modules/00-prereqs.sh

.sh 29 lignes - 862B

run_modules/00-prereqs.sh
MOD_REGISTER prereqs "Vérification des prérequis"
MOD_REQUIRED[prereqs]=1
MOD_PURE[prereqs]=1

mod_prereqs_check() {
    local missing="" v p
    for v in QEMU_BIN FIRMWARE_ELF FIRMWARE_BIN OSMOCON; do
        p="${!v:-}"
        [ -n "$p" ] && [ -e "$p" ] || missing="$missing $v"
    done
    if [ -n "$missing" ]; then
        mod_hint "réglez les chemins dans environnement/bench.env, puis ./run.sh --check-paths"
        mod_fail "dépendances introuvables :$missing"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_prereqs_start() {
    mod_say "arbre         : ${QEMU_TREE}"
    mod_say "binaire QEMU  : ${QEMU_BIN}"
    mod_say "firmware      : ${FIRMWARE_ELF}"
    mod_say "osmocon       : ${OSMOCON}"
    mod_say "exécution     : ${RUN_DIR}"
    mod_say "journaux      : ${LOG_DIR}"
    mod_ok
}

mod_prereqs_status() { return $MOD_RC_FAIL; }

run_modules/02-outils.sh

.sh 62 lignes - 2,2KB

run_modules/02-outils.sh
MOD_REGISTER outils "Outillage de l'hôte"
MOD_REQUIRED[outils]=1
MOD_PURE[outils]=1
MOD_TIMEOUT[outils]=10

: "${OUTILS_REQUIS:=bash sleep pgrep pkill kill stat grep sed awk ip mkfifo rm}"

if ! declare -p OUTILS_OPTIONNELS >/dev/null 2>&1; then
    OUTILS_OPTIONNELS=(
        "ss|sonde des ports d'écoute (repli : /dev/tcp, sans nom de tenant)"
        "tmux|fenêtres de la pile et épilogue « se connecter »"
        "setsid|détachement des processus lancés en arrière-plan"
        "flock|verrou d'unicité du démon audio"
        "socat|passerelles série et sockets de test"
        "nc|alimentation du HLR (repli telnet : fermeture différente, échec silencieux)"
        "python3|outils d'analyse, gr-gsm, transceiver de test"
    )
fi

mod_outils_check() {
    local c manquants=""
    for c in $OUTILS_REQUIS; do
        command -v "$c" >/dev/null 2>&1 || manquants="$manquants $c"
    done
    if [ -n "$manquants" ]; then
        mod_hint "installez-les :  apt-get install -y coreutils procps iproute2 grep sed gawk"
        mod_fail "commandes indispensables absentes du PATH :$manquants"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_outils_status() { return $MOD_RC_FAIL; }

mod_outils_start() {
    local c n d
    for c in $OUTILS_REQUIS; do
        mod_say "requis     $c -> $(command -v "$c" 2>/dev/null)"
    done
    for c in "${OUTILS_OPTIONNELS[@]}"; do
        n="${c%%|*}"; d="${c#*|}"
        if command -v "$n" >/dev/null 2>&1; then
            mod_say "optionnel  $n -> $(command -v "$n")"
        else
            mod_say "ABSENT     $n — dégrade : $d"
        fi
    done
    mod_ok
}

_outils_enfant_resout() {
    bash -c 'command -v ip >/dev/null 2>&1 && command -v pgrep >/dev/null 2>&1 && command -v stat >/dev/null 2>&1'
}

mod_outils_wait() {
    if ! wait_until "${MOD_TIMEOUT[outils]}" "outillage visible d'un enfant" _outils_enfant_resout; then
        mod_hint "PATH courant : $PATH — exportez-le, ou donnez les chemins absolus dans environnement/paths.env"
        mod_fail "un shell enfant ne résout pas l'outillage (PATH non hérité)"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

run_modules/03-rundir.sh

.sh 66 lignes - 2,2KB

run_modules/03-rundir.sh
MOD_REGISTER rundir "Répertoire d'exécution"
MOD_REQUIRED[rundir]=1
MOD_DEPS[rundir]="profil"
MOD_TIMEOUT[rundir]=15

mod_rundir_check() {
    local d="${RUN_DIR:-}" parent
    [ -n "$d" ] || { mod_hint "RUN_DIR est posé par environnement/paths.env"
                     mod_fail "RUN_DIR non défini"
                     return $MOD_RC_FAIL; }
    if [ -e "$d" ] && [ ! -d "$d" ]; then
        mod_hint "rm -f $d   (ou choisissez un autre RUN_DIR)"
        mod_fail "$d existe et n'est pas un répertoire"
        return $MOD_RC_FAIL
    fi
    if [ ! -d "$d" ]; then
        parent="$(dirname "$d")"
        while [ ! -d "$parent" ] && [ "$parent" != "/" ]; do parent="$(dirname "$parent")"; done
        if [ ! -w "$parent" ]; then
            mod_hint "choisissez un emplacement inscriptible :  RUN_DIR=\$HOME/calypso ./start-oqc.sh"
            mod_fail "impossible de créer $d : $parent n'est pas inscriptible"
            return $MOD_RC_FAIL
        fi
    fi
    mod_ok
}

mod_rundir_status() { [ -d "${RUN_DIR:-}" ] && [ -w "${RUN_DIR:-}" ]; }

mod_rundir_start() {
    mkdir -p "${RUN_DIR}" 2>/dev/null || { mod_fail "création impossible : ${RUN_DIR}"; return $MOD_RC_FAIL; }
    chmod 0755 "${RUN_DIR}" 2>/dev/null
    mod_say "répertoire d'exécution : ${RUN_DIR}"
    local f n=0
    for f in "${RUN_DIR}"/*.pid "${RUN_DIR}"/*.sock; do
        [ -e "$f" ] && { mod_say "reste du run précédent : $f"; n=$((n+1)); }
    done
    [ "$n" -gt 0 ] && mod_say "$n reste(s) — 04-restes puis 10-teardown s'en chargent"
    mod_ok
}

_rundir_ecriture_ok() {
    local t="${RUN_DIR}/.temoin.$$"
    : > "$t" 2>/dev/null || return 1
    rm -f "$t" 2>/dev/null
    return 0
}

mod_rundir_wait() {
    if ! wait_until "${MOD_TIMEOUT[rundir]}" "écriture dans ${RUN_DIR}" _rundir_ecriture_ok; then
        mod_hint "montage en lecture seule ou volume plein :  df -h ${RUN_DIR}"
        mod_fail "${RUN_DIR} existe mais l'écriture y échoue"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_rundir_stop() {
    local f pid
    for f in "${RUN_DIR:-/nonexistent}"/*.pid; do
        [ -f "$f" ] || continue
        pid="$(cat "$f" 2>/dev/null || echo 0)"
        kill -0 "$pid" 2>/dev/null || rm -f "$f"
    done
    return 0
}

run_modules/04-restes.sh

.sh 82 lignes - 2,5KB

run_modules/04-restes.sh
MOD_REGISTER restes "Extinction des processus enregistrés"
MOD_REQUIRED[restes]=0
MOD_DEPS[restes]="rundir"
MOD_TIMEOUT[restes]=20
MOD_ENABLED_IF[restes]='[ "${NO_STARTUP_STOP:-0}" != 1 ]'

_res_pids() {
    local f pid
    for f in "${RUN_DIR:-/nonexistent}"/*.pid; do
        [ -f "$f" ] || continue
        pid="$(cat "$f" 2>/dev/null || echo 0)"
        [ "$pid" -gt 0 ] 2>/dev/null || continue
        [ "$pid" = "$$" ] && continue
        kill -0 "$pid" 2>/dev/null && printf '%s\n' "$pid"
    done
}
_res_vide() { [ -z "$(_res_pids)" ]; }

_res_cmdline() { tr '\0' ' ' < "/proc/$1/cmdline" 2>/dev/null | cut -c1-140; }

_res_signal() {
    local sig="$1" p
    for p in $(_res_pids); do
        kill "-$sig" -- "-$p" 2>/dev/null || kill "-$sig" "$p" 2>/dev/null
    done
    return 0
}

_res_menage() {
    local f pid
    for f in "${RUN_DIR:-/nonexistent}"/*.pid; do
        [ -f "$f" ] || continue
        pid="$(cat "$f" 2>/dev/null || echo 0)"
        kill -0 "$pid" 2>/dev/null || { rm -f "$f"; mod_say "fichier PID retiré : $f"; }
    done
    return 0
}

mod_restes_check() {
    command -v kill >/dev/null 2>&1 || { mod_fail "commande kill introuvable"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_restes_status() { _res_vide; }

mod_restes_start() {
    local liste p
    liste="$(_res_pids | tr '\n' ' ')"
    if [ -z "${liste// /}" ]; then
        _res_menage
        mod_already "aucun processus enregistré n'a survécu au run précédent"
        return $MOD_RC_ALREADY
    fi
    for p in $liste; do mod_say "encore vivant : $p  $(_res_cmdline "$p")"; done
    _res_signal TERM
    mod_ok
}

mod_restes_wait() {
    local moitie=$(( ${MOD_TIMEOUT[restes]} / 2 ))
    [ "$moitie" -lt 2 ] && moitie=2

    if ! wait_until "$moitie" "extinction douce" _res_vide; then
        mod_say "toujours vivants après TERM : $(_res_pids | tr '\n' ' ') — passage en KILL"
        _res_signal KILL
        if ! wait_until "$moitie" "extinction forcée" _res_vide; then
            mod_hint "identifiez le tenace :  ps -o pid,ppid,stat,cmd -p \$(cat ${RUN_DIR:-/tmp/calypso}/*.pid | tr '\\n' ',')"
            mod_fail "processus enregistrés impossibles à arrêter : $(_res_pids | tr '\n' ' ')"
            return $MOD_RC_FAIL
        fi
    fi
    _res_menage
    mod_ok
}

mod_restes_stop() {
    _res_vide && { _res_menage; return 0; }
    _res_signal TERM
    wait_until 5 "extinction douce" _res_vide || { _res_signal KILL; wait_until 5 "extinction forcée" _res_vide; }
    _res_menage
    return 0
}

run_modules/05-config.sh

.sh 64 lignes - 2,4KB

run_modules/05-config.sh
MOD_REGISTER config "Résolution des chemins et des ports"
MOD_REQUIRED[config]=1
MOD_DEPS[config]="prereqs"
MOD_PURE[config]=1
MOD_TIMEOUT[config]=5

mod_config_check() {
    if [ ! -d "${QEMU_TREE:-}/run_modules" ]; then
        mod_hint "posez QEMU_TREE sur la racine du dépôt, ou lancez ./run.sh depuis cette racine"
        mod_fail "QEMU_TREE ne désigne pas ce dépôt : ${QEMU_TREE:-<non défini>}"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_config_status() { return $MOD_RC_FAIL; }

mod_config_start() {
    : "${BTS_CFG:=${OSMOCOM_CFG:-/etc/osmocom}/osmo-bts-trx.cfg}"
    : "${MOBILE_CFG_SRC:=${QEMU_CFGS:-$QEMU_TREE/cfgs}/mobile_group1.cfg}"
    : "${MOBILE_CFG:=${OSMOCOM_HOME:-$HOME/.osmocom}/bb/mobile_group1.cfg}"
    : "${QEMU_MON_SOCK:=${RUN_DIR:-/tmp/calypso}/qemu-monitor.sock}"
    : "${L1CTL_SOCK_PATH:=/tmp/osmocom_l2}"
    : "${TMUX_SESSION:=calypso}"
    : "${PORT_TRXD_CLOCK:=5700}"
    : "${PORT_TRXD_CTRL:=5701}"
    : "${PORT_TRXD_DATA:=5702}"
    : "${PORT_GSMTAP:=4729}"
    : "${PORT_GSMTAP_SI:=4730}"
    : "${PORT_GSMTAP_SCH:=4731}"
    mod_say "BTS_CFG          = $BTS_CFG"
    mod_say "MOBILE_CFG_SRC   = $MOBILE_CFG_SRC"
    mod_say "MOBILE_CFG       = $MOBILE_CFG"
    mod_say "QEMU_MON_SOCK    = $QEMU_MON_SOCK"
    mod_say "L1CTL_SOCK_PATH  = $L1CTL_SOCK_PATH (non exportée)"
    mod_say "ports            : TRXD $PORT_TRXD_CLOCK-$PORT_TRXD_DATA (pont) · GSMTAP $PORT_GSMTAP · QEMU $PORT_GSMTAP_SI/$PORT_GSMTAP_SCH"
    local f
    for f in "$BTS_CFG" "$MOBILE_CFG_SRC"; do
        [ -r "$f" ] && mod_say "lisible  : $f" || mod_say "ABSENT   : $f (le module qui l'utilise le signalera)"
    done
    mod_ok
}

_config_vars() {
    printf '%s\n' BTS_CFG MOBILE_CFG MOBILE_CFG_SRC QEMU_MON_SOCK L1CTL_SOCK_PATH TMUX_SESSION \
                  PORT_TRXD_CLOCK PORT_GSMTAP PORT_GSMTAP_SI PORT_GSMTAP_SCH
}
_config_missing() {
    local v out=""
    while read -r v; do [ -n "${!v:-}" ] || out="$out $v"; done < <(_config_vars)
    printf '%s' "${out# }"
}
_config_all_set() { [ -z "$(_config_missing)" ]; }

mod_config_wait() {
    if ! wait_until "${MOD_TIMEOUT[config]}" "variables de configuration" _config_all_set; then
        mod_hint "vérifiez environnement/bench.env (QEMU_TREE, OSMOCOM_CFG, OSMOCOM_HOME, RUN_DIR)"
        mod_fail "variables de configuration vides : $(_config_missing)"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_config_stop() { return 0; }

run_modules/08-gabarits.sh

.sh 112 lignes - 4,4KB

run_modules/08-gabarits.sh
MOD_REGISTER gabarits "Gabarits de configuration du cœur"
MOD_REQUIRED[gabarits]=0
MOD_DEPS[gabarits]="config"
MOD_TIMEOUT[gabarits]=20

MOD_ENABLED_IF[gabarits]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${NO_GABARITS:-0}" != 1 ]'

: "${EGPRS_DIR:=${NITB_ROOT}}"
: "${EGPRS_LIB:=$EGPRS_DIR/lib/gabarits.sh}"
: "${OPERATOR_ID:=1}"
: "${MCC:=001}"
: "${MNC:=01}"
: "${OPERATOR_NAME:=OsmoDirect}"
: "${ENCRYPTION:=a5 0}"

_gab_installes() {
    printf '%s\n' "${OSMOCOM_CFG:-/etc/osmocom}/osmo-bsc.cfg" \
                  "${OSMOCOM_CFG:-/etc/osmocom}/osmo-msc.cfg"
}

mod_gabarits_check() {
    if [ ! -d "$EGPRS_DIR/configs" ]; then
        mod_hint "posez EGPRS_DIR=<racine osmo_egprs> si l'arborescence a bougé"
        mod_skip "osmo_egprs introuvable ($EGPRS_DIR) : les gabarits ne sont pas de ce dépôt"
        return $MOD_RC_SKIP
    fi
    if [ ! -r "$EGPRS_LIB" ]; then
        mod_hint "la bibliothèque est l'extraction de start-direct.sh.legacy L174-258 ; elle doit accompagner osmo_egprs"
        mod_fail "bibliothèque de gabarits absente : $EGPRS_LIB"
        return $MOD_RC_FAIL
    fi
    if [ "$(id -u)" -ne 0 ]; then
        mod_fail "root requis pour écrire dans ${OSMOCOM_CFG:-/etc/osmocom} et /etc/asterisk"
        return $MOD_RC_FAIL
    fi
    if [ "${N_OPERATORS:-1}" -gt 1 ]; then
        mod_hint "le multi-opérateur pose une configuration par netns ; aucun module du cœur ne démarre encore dans un netns"
        mod_fail "N_OPERATORS=${N_OPERATORS} : ce module ne sait poser que le mono-opérateur (cf. POURQUOI 4)"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_gabarits_status() { return $MOD_RC_FAIL; }

mod_gabarits_start() {
    local retour="$PWD" rc=0
    cd "$EGPRS_DIR" || { mod_fail "impossible d'entrer dans $EGPRS_DIR"; return $MOD_RC_FAIL; }

    . "$EGPRS_LIB" || { cd "$retour"; mod_fail "bibliothèque illisible : $EGPRS_LIB"; return $MOD_RC_FAIL; }

    local tmpdir; tmpdir="$(mktemp -d)" || { cd "$retour"; mod_fail "mktemp -d a échoué"; return $MOD_RC_FAIL; }

    local cip gw
    cip="$(op_private_ip "$OPERATOR_ID")"
    gw="$(op_private_gw "$OPERATOR_ID")"

    mod_say "opérateur $OPERATOR_ID · MCC/MNC ${MCC}/${MNC} · chiffrement « ${ENCRYPTION} »"
    mod_say "gabarits  : $EGPRS_DIR/configs -> ${OSMOCOM_CFG:-/etc/osmocom}, /etc/asterisk, $HOME/.osmocom/bb"

    local _inter_shutdown="shutdown" _multi="/etc/osmocom/osmo-multi.conf"
    if [ -r "$_multi" ] && grep -q '^MULTI_HUB_IP=' "$_multi" 2>/dev/null; then
        _inter_shutdown=""
        mod_say "inter-STP déclaré (osmo-multi.conf) : ASP as-inter laissé ACTIF"
    fi
    if apply_config_templates "$tmpdir" "$cip" "$gw" "$OPERATOR_ID" \
            "1.1.1" "1.1.2" "1.1.3" "$MCC" "$MNC" "$OPERATOR_NAME" \
            "${INTER_STP_IP:-127.0.0.1}" "$_inter_shutdown" "1"; then
        install_configs_native "$tmpdir" "" || rc=1
    else
        rc=1
    fi

    rm -rf "$tmpdir"
    cd "$retour"
    [ $rc -eq 0 ] || { mod_fail "la pose des gabarits a échoué (détail dans le journal du module)"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_gabarits_wait() {
    local f restants=""
    while IFS= read -r f; do
        if [ ! -f "$f" ]; then
            mod_hint "vérifiez que $EGPRS_DIR/configs contient bien le gabarit correspondant"
            mod_fail "configuration non installée : $f"
            return $MOD_RC_FAIL
        fi
        if grep -qE '__[A-Z0-9_]+__' "$f" 2>/dev/null; then
            restants="$restants $(basename "$f"):$(grep -oE '__[A-Z0-9_]+__' "$f" | sort -u | tr '\n' ',' )"
        fi
    done < <(_gab_installes)

    if [ -n "$restants" ]; then
        mod_hint "ces jetons n'ont pas de valeur dans apply_config_templates (lib/gabarits.sh)"
        mod_fail "jetons non substitués :$restants"
        return $MOD_RC_FAIL
    fi

    local cfg
    for cfg in "${OSMOCOM_CFG:-/etc/osmocom}/osmo-bsc.cfg" \
               "${OSMOCOM_CFG:-/etc/osmocom}/osmo-msc.cfg"; do
        if grep -q '__ENCRYPTION__' "$cfg" 2>/dev/null || ! grep -qF "$ENCRYPTION" "$cfg" 2>/dev/null; then
            mod_hint "attendu « $ENCRYPTION » dans $cfg ; vérifiez que le gabarit porte bien __ENCRYPTION__"
            mod_fail "le chiffrement demandé n'est pas dans la configuration installée ($cfg)"
            return $MOD_RC_FAIL
        fi
        mod_say "chiffrement « $ENCRYPTION » confirmé dans $cfg"
    done
    mod_ok
}

mod_gabarits_stop() { return 0; }

run_modules/09-logs.sh

.sh 81 lignes - 3,0KB

run_modules/09-logs.sh
MOD_REGISTER logs "Journaux et espace de travail"
MOD_REQUIRED[logs]=1
MOD_DEPS[logs]="config"
MOD_TIMEOUT[logs]=10

: "${LOG_ARCHIVES_KEEP:=10}"
: "${OSMO_LOG_DIR:=/var/log/osmocom}"
: "${OSMO_LOG_USER:=osmocom}"

mod_logs_check() {
    local d="${LOG_DIR:-}" p
    [ -n "$d" ] || { mod_fail "LOG_DIR non défini"; return $MOD_RC_FAIL; }
    p="$d"
    while [ ! -d "$p" ] && [ "$p" != "/" ] && [ "$p" != "." ]; do p="$(dirname "$p")"; done
    if [ ! -w "$p" ]; then
        mod_hint "posez LOG_DIR sur un chemin inscriptible (environnement/bench.env, LOG_DIR)"
        mod_fail "aucun parent inscriptible pour LOG_DIR=$d (bloqué à $p)"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_logs_status() { return $MOD_RC_FAIL; }

mod_logs_start() {
    local _n _cible _lien _faits=0
    if [ "${CALYPSO_LIENS_ROOT:-1}" = 1 ] && [ -w /root ]; then
        for _n in qemu osmocon mobile bts sidecar-mobile; do
            _cible="$LOG_DIR/$_n.log"; _lien="/root/$_n.log"
            if [ -e "$_lien" ] && [ ! -L "$_lien" ]; then
                mod_say "⚠ /root/$_n.log est un vrai fichier — laissé en place"
                continue
            fi
            ln -sfn "$_cible" "$_lien" 2>/dev/null && _faits=$(( _faits + 1 ))
        done
        [ "$_faits" -gt 0 ] && mod_say "$_faits liens de compatibilité dans /root (→ $LOG_DIR)"
    fi
    [ -d "$OSMO_LOG_DIR" ] || mkdir -p "$OSMO_LOG_DIR" 2>/dev/null || true
    if id "$OSMO_LOG_USER" >/dev/null 2>&1; then
        chown "$OSMO_LOG_USER":"$OSMO_LOG_USER" "$OSMO_LOG_DIR" 2>/dev/null || true
    fi
    if [ -d "$OSMO_LOG_DIR" ]; then
        mod_say "journaux des services : $OSMO_LOG_DIR (propriétaire $OSMO_LOG_USER)"
    else
        mod_say "⚠ $OSMO_LOG_DIR absent — les services n'écriront que sur journald"
    fi
    mkdir -p "$LOG_DIR" "$LOG_DIR/mod" "${RUN_DIR:-/tmp/calypso}" "${CAPTURE_DIR:-$RUN_DIR/captures}" 2>/dev/null
    local osmocon_log="$LOG_DIR/osmocon.log"
    if [ -s "$osmocon_log" ]; then
        local arch="${osmocon_log%.log}.$(date +%Y%m%d_%H%M%S).log"
        if mv "$osmocon_log" "$arch" 2>/dev/null; then
            mod_say "osmocon.log du run précédent archivé -> $arch"
        fi
    fi
    ls -1t "${osmocon_log%.log}".[0-9]*.log 2>/dev/null \
        | tail -n +$((LOG_ARCHIVES_KEEP + 1)) | xargs -r rm -f 2>/dev/null || true
    for _n in qemu osmocon mobile bts; do
        : > "$LOG_DIR/$_n.log" 2>/dev/null
    done
    mod_say "LOG_DIR    = $LOG_DIR"
    mod_say "RUN_DIR    = ${RUN_DIR:-/tmp/calypso}"
    mod_ok
}

_logs_writable() {
    local probe="$LOG_DIR/.probe.$$"
    : > "$probe" 2>/dev/null || return 1
    rm -f "$probe" 2>/dev/null
    return 0
}

mod_logs_wait() {
    if ! wait_until "${MOD_TIMEOUT[logs]}" "espace de journaux" _logs_writable; then
        mod_hint "df -h $LOG_DIR — volume plein ou monté en lecture seule ?"
        mod_fail "écriture impossible dans $LOG_DIR (test réel, pas un test de droits)"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_logs_stop() { return 0; }

run_modules/09-teardown.sh

.sh 144 lignes - 4,5KB

run_modules/09-teardown.sh
MOD_REGISTER teardown "Nettoyage du run précédent"
MOD_REQUIRED[teardown]=1
MOD_DEPS[teardown]="logs"
MOD_TIMEOUT[teardown]=25

_td_patterns() {
    cat <<'EOF'
f:qemu-system-arm
x:osmo-bts-trx
x:osmocon
x:mobile
x:trxcon
f:pont/pont.py
EOF
}

_td_port_busy() {
    ss -ln 2>/dev/null | grep -qE "[:.]${1}[[:space:]]"
}

_td_ports() {
    printf '%s\n' "${PORT_TRXD_CLOCK:-5700}" "${PORT_TRXD_CTRL:-5701}" "${PORT_TRXD_DATA:-5702}" \
                  "${PORT_GSMTAP_SI:-4730}" "${PORT_GSMTAP_SCH:-4731}"
}

_td_sockets() {
    printf '%s\n' "${L1CTL_SOCK_PATH:-/tmp/osmocom_l2}" \
                  "${SC_L2_SOCK:-/tmp/ms2_l2}" \
                  "${QEMU_MON_SOCK:-${RUN_DIR:-/tmp/calypso}/qemu-monitor.sock}"
}

_td_live() {
    local _p _st
    for _p in $(pgrep "$1" "$2" 2>/dev/null); do
        _st="$(ps -o stat= -p "$_p" 2>/dev/null | tr -d ' ')"
        case "$_st" in
            ''|Z*) continue ;;
            *)     return 0 ;;
        esac
    done
    return 1
}

_td_leftovers() {
    local line kind pat out="" p
    while IFS= read -r line; do
        kind="${line%%:*}"; pat="${line#*:}"
        case "$kind" in
            x) _td_live -x "$pat" && out="$out $pat" ;;
            f) _td_live -f "$pat" && out="$out $pat" ;;
        esac
    done < <(_td_patterns)
    while IFS= read -r p; do
        _td_port_busy "$p" && out="$out port:$p"
    done < <(_td_ports)
    while IFS= read -r p; do
        [ -e "$p" ] && out="$out socket:$p"
    done < <(_td_sockets)
    printf '%s' "${out# }"
}
_td_clean() { [ -z "$(_td_leftovers)" ]; }

mod_teardown_check() {
    local t
    for t in pgrep pkill; do
        command -v "$t" >/dev/null 2>&1 || {
            mod_hint "installez procps (paquet procps / procps-ng)"
            mod_fail "$t introuvable : impossible de garantir une machine propre"
            return $MOD_RC_FAIL
        }
    done
    command -v ss >/dev/null 2>&1 || mod_say "ss absent : la libération des ports UDP ne sera pas vérifiée"
    mod_ok
}

mod_teardown_status() { _td_clean; }

_td_kill_all() {
    local line kind pat
    while IFS= read -r line; do
        kind="${line%%:*}"; pat="${line#*:}"
        case "$kind" in
            x) _td_live -x "$pat" && { mod_say "kill -x $pat"; pkill -9 -x "$pat" 2>/dev/null; } ;;
            f) _td_live -f "$pat" && { mod_say "kill -f $pat"; pkill -9 -f "$pat" 2>/dev/null; } ;;
        esac
    done < <(_td_patterns)
    return 0
}

_td_kill_ports() {
    command -v ss >/dev/null 2>&1 || { mod_say "ss absent : passe par port impossible"; return 0; }
    local p pid cmd busy=0 named=0
    while IFS= read -r p; do
        _td_port_busy "$p" || continue
        busy=1
        for pid in $(ss -lnp "sport = :$p" 2>/dev/null \
                     | grep -oE 'pid=[0-9]+' | cut -d= -f2 | sort -u); do
            named=1
            [ "$pid" -gt 1 ] 2>/dev/null || continue
            [ "$pid" = "$$" ] && continue
            cmd=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)
            mod_say "port $p tenu par PID $pid (${cmd:-cmdline illisible}) — kill -9"
            kill -9 "$pid" 2>/dev/null
        done
    done < <(_td_ports)
    if [ "$busy" = 1 ] && [ "$named" = 0 ]; then
        mod_say "ports occupes mais ss n'a rendu aucun pid= (ss sans -p, ou proprietaire hors de notre namespace) — inspectez : ss -lnp"
    fi
    return 0
}

mod_teardown_start() {
    if [ -n "${TMUX:-}" ]; then
        mod_say "lancé depuis tmux : kill-server évité, on ne touche qu'à la session ${TMUX_SESSION:-calypso}"
        tmux kill-session -t "${TMUX_SESSION:-calypso}" 2>/dev/null
    else
        tmux kill-server 2>/dev/null
    fi
    _td_kill_all
    local p
    while IFS= read -r p; do rm -f "$p" 2>/dev/null; done < <(_td_sockets)
    rm -f /dev/shm/calypso_kc /dev/shm/calypso_kc_l1 2>/dev/null
    rm -f "${RUN_DIR:-/tmp/calypso}/qemu.pid" 2>/dev/null
    mod_ok
}

mod_teardown_wait() {
    local half=$(( ${MOD_TIMEOUT[teardown]} / 2 ))
    if wait_until "$half" "machine propre" _td_clean; then mod_ok; return $MOD_RC_OK; fi
    mod_say "restes après $half s : $(_td_leftovers) — deuxième passe"
    _td_kill_all
    _td_kill_ports
    if wait_until "$half" "machine propre" _td_clean; then mod_ok; return $MOD_RC_OK; fi
    mod_hint "identifiez le tenace : pgrep -a -f <motif> ; ss -lnp | grep <port>"
    mod_fail "restes du run précédent : $(_td_leftovers)"
    return $MOD_RC_FAIL
}

mod_teardown_stop() {
    _td_kill_all
    local p
    while IFS= read -r p; do rm -f "$p" 2>/dev/null; done < <(_td_sockets)
    return 0
}

run_modules/10-reseau.sh

.sh 108 lignes - 4,1KB

run_modules/10-reseau.sh
MOD_REGISTER reseau "Interfaces et plan d'adressage"
MOD_REQUIRED[reseau]=1
MOD_DEPS[reseau]="rundir"
MOD_TIMEOUT[reseau]=15

: "${RESEAU_PORTS_LIBRES:=${PORT_GSMTAP_SI:-4730} ${PORT_GSMTAP_SCH:-4731}}"
: "${NEED_TUN:=0}"

_net_tenant() {
    local port="$1" out=""
    if command -v ss >/dev/null 2>&1; then
        out="$(ss -lntp "sport = :$port" 2>/dev/null | awk 'NR>1{print $0}')"
    fi
    [ -n "$out" ] && printf '%s' "$out" || printf 'tenant inconnu (ss absent ou sans droits)'
}
_net_lo_up() { ip link show lo 2>/dev/null | grep -q 'state UNKNOWN\|state UP\|<.*UP'; }
_net_route_loopback() { ip route get 127.0.0.2 >/dev/null 2>&1; }
_net_port_occupe() {
    if command -v ss >/dev/null 2>&1; then
        ss -ln 2>/dev/null | grep -qE "[:.]${1}[[:space:]]" && return 0
        return 1
    fi
    have_port "$1"
}
_net_ports_libres() {
    local p
    for p in $RESEAU_PORTS_LIBRES; do
        _net_port_occupe "$p" && return 1
    done
    return 0
}
_net_tun_ok() { [ -c /dev/net/tun ]; }

mod_reseau_check() {
    command -v ip >/dev/null 2>&1 || {
        mod_hint "apt-get install -y iproute2"
        mod_fail "commande « ip » absente : le réseau ne peut pas être constaté"
        return $MOD_RC_FAIL; }
    local p occupes=""
    for p in $RESEAU_PORTS_LIBRES; do
        _net_port_occupe "$p" && occupes="$occupes $p"
    done
    if [ -n "$occupes" ]; then
        for p in $occupes; do mod_say "port $p occupé : $(_net_tenant "$p")"; done
        mod_hint "arrêtez le run précédent :  ./run.sh --stop   (le module teardown balaie au démarrage)"
        mod_fail "ports déjà occupés :$occupes — le plan ne pourra pas les ouvrir"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_reseau_status() {
    _net_lo_up || return $MOD_RC_FAIL
    _net_route_loopback || return $MOD_RC_FAIL
    if [ "${NEED_TUN}" = 1 ]; then _net_tun_ok || return $MOD_RC_FAIL; fi
    return $MOD_RC_OK
}

mod_reseau_start() {
    _net_lo_up || ip link set lo up 2>/dev/null
    if ! _net_tun_ok; then
        if [ "$(id -u)" != 0 ]; then
            mod_say "/dev/net/tun absent et nous ne sommes pas root : création impossible"
        else
            modprobe tun 2>/dev/null
            if ! _net_tun_ok; then
                mkdir -p /dev/net 2>/dev/null
                mknod /dev/net/tun c 10 200 2>/dev/null && chmod 0666 /dev/net/tun 2>/dev/null
            fi
        fi
        if _net_tun_ok; then
            mod_say "/dev/net/tun : créé"
        elif [ "${NEED_TUN}" = 1 ]; then
            mod_hint "relancez le conteneur avec --device /dev/net/tun (ou --privileged), ou posez NEED_TUN=0"
            mod_fail "/dev/net/tun indisponible alors que NEED_TUN=1"
            return $MOD_RC_FAIL
        else
            mod_say "/dev/net/tun indisponible — sans effet (NEED_TUN=0)"
        fi
    fi
    mod_say "lo        : $(ip -o -4 addr show dev lo 2>/dev/null | awk '{print $4}' | tr '\n' ' ')"
    mod_say "ports     : $RESEAU_PORTS_LIBRES (attendus libres)"
    mod_ok
}

mod_reseau_wait() {
    wait_until "${MOD_TIMEOUT[reseau]}" "interface lo active" _net_lo_up || {
        mod_hint "ip link set lo up"
        mod_fail "la boucle locale n'est pas active"
        return $MOD_RC_FAIL; }
    wait_until "${MOD_TIMEOUT[reseau]}" "route vers 127.0.0.2" _net_route_loopback || {
        mod_hint "la pile écrit en dur sur 127.0.0.2 (HLR, contrôle BTS) : vérifiez  ip route get 127.0.0.2"
        mod_fail "127.0.0.2 n'est pas routable"
        return $MOD_RC_FAIL; }
    wait_until "${MOD_TIMEOUT[reseau]}" "libération des ports" _net_ports_libres || {
        mod_hint "un processus tient encore un port :  ss -lnp | grep -E '$(printf '%s' "$RESEAU_PORTS_LIBRES" | tr ' ' '|')'"
        mod_fail "ports toujours occupés parmi : $RESEAU_PORTS_LIBRES"
        return $MOD_RC_FAIL; }
    if [ "${NEED_TUN}" = 1 ]; then
        wait_until "${MOD_TIMEOUT[reseau]}" "périphérique TUN" _net_tun_ok || {
            mod_hint "--device /dev/net/tun au lancement du conteneur"
            mod_fail "/dev/net/tun absent (NEED_TUN=1)"
            return $MOD_RC_FAIL; }
    fi
    mod_ok
}

mod_reseau_stop() { return 0; }

run_modules/10-stp.sh

.sh 59 lignes - 2,3KB

run_modules/10-stp.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER stp "Cœur — OsmoSTP (SS7/M3UA)"
MOD_REQUIRED[stp]=1
MOD_JOURNAL[stp]="osmo-stp"
MOD_TIMEOUT[stp]=25
MOD_ENABLED_IF[stp]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${STP_UNIT:=osmo-stp}"
: "${STP_VTY_PORT:=4239}"

_stp_cfg()  { core_cfg osmo-stp; }
_stp_m3ua() { core_cfg_field "$(_stp_cfg)" '^[[:space:]]*listen[[:space:]]+m3ua[[:space:]]' 3 2905; }

mod_stp_check() {
    core_bin osmo-stp >/dev/null || {
        mod_hint "paquet osmo-stp absent — vérifiez l'image, ou réglez OSMOCOM_CFG/PATH"
        mod_fail "binaire osmo-stp introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_stp_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-stp.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_stp_cfg)"; return $MOD_RC_FAIL; }
    if [ ! -e /proc/net/sctp/snmp ] && ! lsmod 2>/dev/null | grep -q '^sctp'; then
        mod_hint "chargez le module côté hôte : modprobe sctp (un conteneur ne peut pas le faire)"
        mod_fail "SCTP indisponible dans ce noyau — l'écoute M3UA est impossible"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_stp_status() { core_unit_active "$STP_UNIT" || core_vty_listen "$STP_VTY_PORT"; }

mod_stp_start() {
    core_svc_start "$STP_UNIT" "$(core_bin osmo-stp)" -c "$(_stp_cfg)" \
        || { mod_fail "systemctl start $STP_UNIT a échoué"
             mod_hint "journalctl -u $STP_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_stp_wait() {
    local to="${MOD_TIMEOUT[stp]}" m3ua; m3ua="$(_stp_m3ua)"

    if ! wait_until "$to" "VTY OsmoSTP ($STP_VTY_PORT)" core_vty_listen "$STP_VTY_PORT"; then
        mod_hint "journalctl -u $STP_UNIT -n 30 ; port $STP_VTY_PORT déjà pris ?"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "écoute M3UA SCTP ($m3ua)" core_sctp_listen "$m3ua"; then
        mod_hint "vérifiez « listen m3ua $m3ua » dans $(_stp_cfg) et le support SCTP (ss -ln --sctp)"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$STP_UNIT"; then
        mod_hint "journalctl -u $STP_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoSTP a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_stp_stop() { core_svc_stop "$STP_UNIT" "osmo-stp -c"; }

run_modules/11-hlr.sh

.sh 62 lignes - 2,5KB

run_modules/11-hlr.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER hlr "Cœur — OsmoHLR (registre GSUP)"
MOD_REQUIRED[hlr]=1
MOD_JOURNAL[hlr]="osmo-hlr"
MOD_TIMEOUT[hlr]=30
MOD_ENABLED_IF[hlr]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${HLR_UNIT:=osmo-hlr}"
: "${HLR_VTY_PORT:=4258}"
: "${HLR_GSUP_PORT:=4222}"
: "${HLR_DB:=/var/lib/osmocom/hlr.db}"

_hlr_cfg()     { core_cfg osmo-hlr; }
_hlr_gsup_ip() { core_cfg_field "$(_hlr_cfg)" '^[[:space:]]*bind[[:space:]]+ip[[:space:]]' 3 127.0.0.2; }

mod_hlr_check() {
    core_bin osmo-hlr >/dev/null || {
        mod_fail "binaire osmo-hlr introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_hlr_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-hlr.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_hlr_cfg)"; return $MOD_RC_FAIL; }
    local dbdir; dbdir="$(dirname "$HLR_DB")"
    [ -d "$dbdir" ] || {
        mod_hint "mkdir -p $dbdir && chown osmocom:osmocom $dbdir"
        mod_fail "répertoire de base absent : $dbdir"; return $MOD_RC_FAIL; }
    core_ip_local "$(_hlr_gsup_ip)" || {
        mod_hint "ip addr add $(_hlr_gsup_ip)/24 dev lo, ou changez « gsup / bind ip » dans $(_hlr_cfg)"
        mod_fail "adresse GSUP $(_hlr_gsup_ip) portée par aucune interface"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_hlr_status() { core_unit_active "$HLR_UNIT" || core_vty_listen "$HLR_VTY_PORT"; }

mod_hlr_start() {
    core_svc_start "$HLR_UNIT" "$(core_bin osmo-hlr)" -c "$(_hlr_cfg)" -l "$HLR_DB" \
        || { mod_fail "systemctl start $HLR_UNIT a échoué"
             mod_hint "journalctl -u $HLR_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_hlr_wait() {
    local to="${MOD_TIMEOUT[hlr]}" gip; gip="$(_hlr_gsup_ip)"

    if ! wait_until "$to" "VTY OsmoHLR ($HLR_VTY_PORT)" core_vty_listen "$HLR_VTY_PORT"; then
        mod_hint "journalctl -u $HLR_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "GSUP ($gip:$HLR_GSUP_PORT)" core_tcp_listen "$HLR_GSUP_PORT" "$gip"; then
        mod_hint "base verrouillée ou illisible ? ls -l $HLR_DB ; journalctl -u $HLR_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$HLR_UNIT"; then
        mod_hint "journalctl -u $HLR_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoHLR a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_hlr_stop() { core_svc_stop "$HLR_UNIT" "osmo-hlr -c"; }

run_modules/12-mgw.sh

.sh 57 lignes - 2,2KB

run_modules/12-mgw.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER mgw "Cœur — OsmoMGW (média MGCP/RTP)"
MOD_REQUIRED[mgw]=1
MOD_JOURNAL[mgw]="osmo-mgw"
MOD_TIMEOUT[mgw]=25
MOD_ENABLED_IF[mgw]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${MGW_UNIT:=osmo-mgw}"
: "${MGW_VTY_PORT:=4243}"

_mgw_cfg()  { core_cfg osmo-mgw; }
_mgw_ip()   { core_cfg_field "$(_mgw_cfg)" '^[[:space:]]*bind[[:space:]]+ip[[:space:]]'   3 127.0.0.1; }
_mgw_port() { core_cfg_field "$(_mgw_cfg)" '^[[:space:]]*bind[[:space:]]+port[[:space:]]' 3 2427; }

mod_mgw_check() {
    core_bin osmo-mgw >/dev/null || { mod_fail "binaire osmo-mgw introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_mgw_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-mgw.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_mgw_cfg)"; return $MOD_RC_FAIL; }
    core_ip_local "$(_mgw_ip)" || {
        mod_hint "corrigez « mgcp / bind ip » dans $(_mgw_cfg), ou ajoutez l'adresse à une interface"
        mod_fail "adresse MGCP $(_mgw_ip) portée par aucune interface"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_mgw_status() { core_unit_active "$MGW_UNIT" || core_vty_listen "$MGW_VTY_PORT"; }

mod_mgw_start() {
    core_svc_start "$MGW_UNIT" "$(core_bin osmo-mgw)" -s -c "$(_mgw_cfg)" \
        || { mod_fail "systemctl start $MGW_UNIT a échoué"
             mod_hint "journalctl -u $MGW_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_mgw_wait() {
    local to="${MOD_TIMEOUT[mgw]}" ip port
    ip="$(_mgw_ip)"; port="$(_mgw_port)"

    if ! wait_until "$to" "VTY OsmoMGW ($MGW_VTY_PORT)" core_vty_listen "$MGW_VTY_PORT"; then
        mod_hint "journalctl -u $MGW_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "MGCP UDP ($ip:$port)" core_udp_listen "$port" "$ip"; then
        mod_hint "port $port déjà pris ? ss -lun | grep $port"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$MGW_UNIT"; then
        mod_hint "journalctl -u $MGW_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoMGW a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_mgw_stop() { core_svc_stop "$MGW_UNIT" "osmo-mgw -s -c"; }

run_modules/13-msc.sh

.sh 65 lignes - 2,5KB

run_modules/13-msc.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER msc "Cœur — OsmoMSC (commutation CS)"
MOD_REQUIRED[msc]=1
MOD_DEPS[msc]="stp hlr mgw"
MOD_JOURNAL[msc]="osmo-msc"
MOD_TIMEOUT[msc]=40
MOD_ENABLED_IF[msc]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${MSC_UNIT:=osmo-msc}"
: "${MSC_VTY_PORT:=4254}"

_msc_cfg()      { core_cfg osmo-msc; }
_msc_asp_port() { core_cfg_field "$(_msc_cfg)" '^[[:space:]]*asp[[:space:]]+.*[[:space:]]m3ua[[:space:]]*$' 4 ""; }
_msc_mncc()     { core_cfg_field "$(_msc_cfg)" '^[[:space:]]*mncc[[:space:]]+external[[:space:]]' 3 ""; }

mod_msc_check() {
    core_bin osmo-msc >/dev/null || { mod_fail "binaire osmo-msc introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_msc_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-msc.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_msc_cfg)"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_msc_status() { core_unit_active "$MSC_UNIT" || core_vty_listen "$MSC_VTY_PORT"; }

mod_msc_start() {
    core_svc_start "$MSC_UNIT" "$(core_bin osmo-msc)" -c "$(_msc_cfg)" \
        || { mod_fail "systemctl start $MSC_UNIT a échoué"
             mod_hint "journalctl -u $MSC_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_msc_wait() {
    local to="${MOD_TIMEOUT[msc]}" asp mncc
    asp="$(_msc_asp_port)"; mncc="$(_msc_mncc)"

    if ! wait_until "$to" "VTY OsmoMSC ($MSC_VTY_PORT)" core_vty_listen "$MSC_VTY_PORT"; then
        mod_hint "journalctl -u $MSC_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if [ -n "$asp" ]; then
        if ! wait_until "$to" "lien M3UA vers le STP (SCTP local :$asp)" core_sctp_estab "$asp"; then
            mod_hint "STP joignable ? ss -an --sctp | grep $asp ; vérifiez « asp ... m3ua » dans $(_msc_cfg)"
            return $MOD_RC_FAIL
        fi
    else
        mod_say "aucun ASP m3ua dans $(_msc_cfg) — barrière SS7 non applicable"
    fi
    if [ -n "$mncc" ]; then
        if ! wait_until "$to" "socket MNCC ($mncc)" core_unix_listen "$mncc"; then
            mod_hint "« mncc external $mncc » configuré mais la socket n'apparaît pas : ss -xl | grep msc_mncc"
            return $MOD_RC_FAIL
        fi
    fi
    if core_restarted_since "$MSC_UNIT"; then
        mod_hint "journalctl -u $MSC_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoMSC a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_msc_stop() { core_svc_stop "$MSC_UNIT" "osmo-msc -c"; }

run_modules/13-sidecar-cfg.sh

.sh 97 lignes - 3,5KB

run_modules/13-sidecar-cfg.sh
. "$(dirname "${BASH_SOURCE[0]}")/_lib/radio.sh"

MOD_REGISTER sidecar-cfg "Configurations du BTS#1 (side-car)"
MOD_REQUIRED[sidecar-cfg]=1
MOD_DEPS[sidecar-cfg]="config"
MOD_PURE[sidecar-cfg]=1
MOD_TIMEOUT[sidecar-cfg]=10

: "${BSC_CFG:=${OSMOCOM_CFG:-/etc/osmocom}/osmo-bsc.cfg}"

_sc_bsc_declare() { grep -qE "^[[:space:]]*ipa unit-id ${SC_UNIT_ID} 0" "$BSC_CFG" 2>/dev/null; }

mod_sidecar_cfg_check() {
    [ -r "$BSC_CFG" ] || {
        mod_hint "posez BSC_CFG, ou déployez /etc/osmocom/osmo-bsc.cfg"
        mod_fail "configuration BSC illisible : $BSC_CFG"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_cfg_status() {
    _sc_bsc_declare && [ -r "$SC_BTS_CFG" ] && [ -r "$SC_MOBILE_CFG" ]
}

_sc_bts_block() {
    cat <<BLOC
 bts 1
  type osmo-bts
  band DCS1800
  cell_identity ${SC_UNIT_ID}
  location_area_code 0x0001
  base_station_id_code 8
  ms max power 15
  cell reselection hysteresis 4
  rxlev access min 0
  radio-link-timeout 32
  channel allocator mode chan-req descending
  channel allocator mode assignment ascending
  channel allocator mode handover ascending
  rach tx integer 9
  rach max transmission 7
  ipa unit-id ${SC_UNIT_ID} 0
  oml ipa stream-id 255 line 0
  neighbor-list mode automatic
  codec-support fr
  gprs mode none
  trx 0
   rf_locked 0
   arfcn ${SC_ARFCN}
   nominal power 23
   max_power_red 0
   rsl e1 tei 0
$(for t in 0 1 2 3 4 5 6 7; do
    case "$t" in
      0) pchan='CCCH+SDCCH4' ;;
      1) pchan="${SC_TS1_PCHAN:-SDCCH8}" ;;
      *) pchan='TCH/F' ;;
    esac
    printf '   timeslot %s\n    phys_chan_config %s\n    hopping enabled 0\n' "$t" "$pchan"
  done)
BLOC
}

mod_sidecar_cfg_start() {
    if _sc_bsc_declare; then
        mod_say "bloc « bts 1 » (unit-id $SC_UNIT_ID) déjà déclaré dans $BSC_CFG"
    else
        grep -qE '^msc 0' "$BSC_CFG" || {
            mod_hint "le bloc « bts 1 » s'insère avant « msc 0 » ; cette ancre est absente"
            mod_fail "structure inattendue dans $BSC_CFG"; return $MOD_RC_FAIL; }
        cp -f "$BSC_CFG" "${BSC_CFG}.avant-sidecar" 2>/dev/null
        local tmp; tmp="$(mktemp)"
        awk -v bloc="$(_sc_bts_block)" '
            /^msc 0/ && !fait { print bloc; fait = 1 }
            { print }
        ' "$BSC_CFG" > "$tmp" || { rm -f "$tmp"; mod_fail "réécriture de $BSC_CFG impossible"; return $MOD_RC_FAIL; }
        cat "$tmp" > "$BSC_CFG" && rm -f "$tmp"
        _sc_bsc_declare || { mod_fail "insertion du bloc « bts 1 » sans effet"; return $MOD_RC_FAIL; }
        mod_say "bloc « bts 1 » inséré (sauvegarde : ${BSC_CFG}.avant-sidecar)"
    fi

    [ -r "$SC_BTS_CFG" ] || {
        mod_hint "attendu : $SC_BTS_CFG (VTY $SC_BTS_VTY_PORT, base-port remote $SC_TRX_PORT)"
        mod_fail "configuration du BTS#1 absente"; return $MOD_RC_FAIL; }
    local port; port="$(radio_cfg_val "$SC_BTS_CFG" "osmotrx base-port remote")"
    [ "${port:-$SC_TRX_PORT}" = "$SC_TRX_PORT" ] && mod_say "BTS#1 : $SC_BTS_CFG (TRXD $SC_TRX_PORT)" \
        || mod_say "⚠ $SC_BTS_CFG annonce base-port remote $port, le side-car attend $SC_TRX_PORT"

    if [ ! -r "$SC_MOBILE_CFG" ]; then
        mod_hint "attendu : $SC_MOBILE_CFG (layer2-socket $SC_L2_SOCK, VTY $SC_MOBILE_VTY_PORT)"
        mod_fail "configuration du MS#2 absente"; return $MOD_RC_FAIL
    fi
    local imsi; imsi="$(awk '$1=="imsi"{print $2; exit}' "$SC_MOBILE_CFG" 2>/dev/null)"
    mod_say "MS#2 : $SC_MOBILE_CFG (imsi ${imsi:-?}, socket $SC_L2_SOCK)"
    mod_ok
}

mod_sidecar_cfg_stop() { return 0; }

run_modules/14-bsc.sh

.sh 71 lignes - 2,8KB

run_modules/14-bsc.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER bsc "Cœur — OsmoBSC (contrôleur BTS)"
MOD_REQUIRED[bsc]=1
MOD_DEPS[bsc]="stp mgw msc"
MOD_JOURNAL[bsc]="osmo-bsc"
MOD_TIMEOUT[bsc]=40
MOD_ENABLED_IF[bsc]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${BSC_UNIT:=osmo-bsc}"
: "${BSC_VTY_PORT:=4242}"
: "${BSC_OML_PORT:=3002}"
: "${BSC_RSL_PORT:=3003}"

_bsc_cfg()      { core_cfg osmo-bsc; }
_bsc_asp_port() { core_cfg_field "$(_bsc_cfg)" '^[[:space:]]*asp[[:space:]]+.*[[:space:]]m3ua[[:space:]]*$' 4 ""; }

mod_bsc_check() {
    core_bin osmo-bsc >/dev/null || { mod_fail "binaire osmo-bsc introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_bsc_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-bsc.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_bsc_cfg)"; return $MOD_RC_FAIL; }
    if grep -qa '__[A-Z_]\+__' "$(_bsc_cfg)" 2>/dev/null; then
        mod_hint "la configuration contient encore des jetons de gabarit — régénérez-la"
        mod_fail "$(_bsc_cfg) : gabarit non substitué"; return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_bsc_status() { core_unit_active "$BSC_UNIT" || core_vty_listen "$BSC_VTY_PORT"; }

mod_bsc_start() {
    core_svc_start "$BSC_UNIT" "$(core_bin osmo-bsc)" -c "$(_bsc_cfg)" -s \
        || { mod_fail "systemctl start $BSC_UNIT a échoué"
             mod_hint "journalctl -u $BSC_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_bsc_wait() {
    local to="${MOD_TIMEOUT[bsc]}" asp; asp="$(_bsc_asp_port)"

    if ! wait_until "$to" "VTY OsmoBSC ($BSC_VTY_PORT)" core_vty_listen "$BSC_VTY_PORT"; then
        mod_hint "journalctl -u $BSC_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "écoute A-bis OML ($BSC_OML_PORT)" core_tcp_listen "$BSC_OML_PORT"; then
        mod_hint "port $BSC_OML_PORT déjà pris par un autre BSC ? ss -ltn | grep $BSC_OML_PORT"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "écoute A-bis RSL ($BSC_RSL_PORT)" core_tcp_listen "$BSC_RSL_PORT"; then
        mod_hint "port $BSC_RSL_PORT déjà pris ? ss -ltn | grep $BSC_RSL_PORT"
        return $MOD_RC_FAIL
    fi
    if [ -n "$asp" ]; then
        if ! wait_until "$to" "lien M3UA vers le STP (SCTP local :$asp)" core_sctp_estab "$asp"; then
            mod_hint "STP joignable ? ss -an --sctp | grep $asp ; vérifiez « asp ... m3ua » dans $(_bsc_cfg)"
            return $MOD_RC_FAIL
        fi
    else
        mod_say "aucun ASP m3ua dans $(_bsc_cfg) — barrière SS7 non applicable"
    fi
    if core_restarted_since "$BSC_UNIT"; then
        mod_hint "journalctl -u $BSC_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoBSC a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_bsc_stop() { core_svc_stop "$BSC_UNIT" "osmo-bsc -c"; }

run_modules/15-apn0.sh

.sh 48 lignes - 1,8KB

run_modules/15-apn0.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER apn0 "Cœur — interface TUN de l'APN"
MOD_REQUIRED[apn0]=0
MOD_TIMEOUT[apn0]=15
MOD_ENABLED_IF[apn0]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_GPRS:-1}" = 1 ]'

: "${APN_DEV:=apn0}"
: "${APN_ADDR:=176.16.32.0/24}"

_apn_up()   { ip -o link show "$APN_DEV" 2>/dev/null | grep -qE '<[^>]*(^|<|,)UP(,|>)'; }
_apn_addr() { ip -o -4 addr show dev "$APN_DEV" 2>/dev/null | grep -q "${APN_ADDR%%/*}"; }

mod_apn0_check() {
    command -v ip >/dev/null 2>&1 || { mod_fail "commande ip absente (paquet iproute2)"; return $MOD_RC_FAIL; }
    [ -c /dev/net/tun ] || {
        mod_hint "conteneur sans TUN : ajoutez --device /dev/net/tun et --cap-add NET_ADMIN"
        mod_fail "/dev/net/tun absent — impossible de créer une interface TUN"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_apn0_status() { _apn_up && _apn_addr; }

mod_apn0_start() {
    ip link show "$APN_DEV" >/dev/null 2>&1 && ip link del dev "$APN_DEV" 2>/dev/null
    ip tuntap add dev "$APN_DEV" mode tun 2>&1 || {
        mod_hint "droits insuffisants : lancez le conteneur avec --cap-add NET_ADMIN"
        mod_fail "création de $APN_DEV refusée"; return $MOD_RC_FAIL; }
    ip addr add "$APN_ADDR" dev "$APN_DEV" 2>&1
    ip link set dev "$APN_DEV" up 2>&1
    mod_ok
}

mod_apn0_wait() {
    if ! wait_until "${MOD_TIMEOUT[apn0]}" "interface $APN_DEV UP" _apn_up; then
        mod_hint "ip link show $APN_DEV"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "${MOD_TIMEOUT[apn0]}" "adresse $APN_ADDR sur $APN_DEV" _apn_addr; then
        mod_hint "adresse déjà portée par une autre interface ? ip -4 addr | grep ${APN_ADDR%%/*}"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_apn0_stop() { ip link del dev "$APN_DEV" 2>/dev/null; return 0; }

run_modules/16-ggsn.sh

.sh 65 lignes - 2,6KB

run_modules/16-ggsn.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER ggsn "Cœur — OsmoGGSN (data GTP)"
MOD_REQUIRED[ggsn]=0
MOD_DEPS[ggsn]="apn0"
MOD_JOURNAL[ggsn]="osmo-ggsn"
MOD_TIMEOUT[ggsn]=25
MOD_ENABLED_IF[ggsn]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_GPRS:-1}" = 1 ]'

: "${GGSN_UNIT:=osmo-ggsn}"
: "${GGSN_VTY_PORT:=4260}"
: "${GGSN_GTPC_PORT:=2123}"
: "${GGSN_GTPU_PORT:=2152}"

_ggsn_cfg() { core_cfg osmo-ggsn; }
_ggsn_ip()  { core_cfg_field "$(_ggsn_cfg)" '^[[:space:]]*gtp[[:space:]]+bind-ip[[:space:]]' 3 ""; }

mod_ggsn_check() {
    core_bin osmo-ggsn >/dev/null || { mod_fail "binaire osmo-ggsn introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_ggsn_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-ggsn.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_ggsn_cfg)"; return $MOD_RC_FAIL; }
    local ip; ip="$(_ggsn_ip)"
    [ -n "$ip" ] || { mod_fail "« gtp bind-ip » absent de $(_ggsn_cfg)"; return $MOD_RC_FAIL; }
    core_ip_local "$ip" || {
        mod_hint "corrigez « gtp bind-ip » dans $(_ggsn_cfg) : aucune interface ne porte $ip"
        mod_fail "adresse GTP $ip introuvable localement — le GGSN refusera de démarrer"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_ggsn_status() { core_unit_active "$GGSN_UNIT" || core_vty_listen "$GGSN_VTY_PORT"; }

mod_ggsn_start() {
    core_svc_start "$GGSN_UNIT" "$(core_bin osmo-ggsn)" -c "$(_ggsn_cfg)" \
        || { mod_fail "systemctl start $GGSN_UNIT a échoué"
             mod_hint "journalctl -u $GGSN_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_ggsn_wait() {
    local to="${MOD_TIMEOUT[ggsn]}" ip; ip="$(_ggsn_ip)"

    if ! wait_until "$to" "VTY OsmoGGSN ($GGSN_VTY_PORT)" core_vty_listen "$GGSN_VTY_PORT"; then
        mod_hint "journalctl -u $GGSN_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "GTP-C ($ip:$GGSN_GTPC_PORT)" core_udp_listen "$GGSN_GTPC_PORT" "$ip"; then
        mod_hint "l'interface TUN de l'APN est-elle montée ? ip link show ${APN_DEV:-apn0}"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "GTP-U ($ip:$GGSN_GTPU_PORT)" core_udp_listen "$GGSN_GTPU_PORT" "$ip"; then
        mod_hint "port $GGSN_GTPU_PORT déjà pris ? ss -lun | grep $GGSN_GTPU_PORT"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$GGSN_UNIT"; then
        mod_hint "journalctl -u $GGSN_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoGGSN a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_ggsn_stop() { core_svc_stop "$GGSN_UNIT" "osmo-ggsn -c"; }

run_modules/17-sgsn.sh

.sh 60 lignes - 2,3KB

run_modules/17-sgsn.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER sgsn "Cœur — OsmoSGSN (rattachement paquet)"
MOD_REQUIRED[sgsn]=0
MOD_DEPS[sgsn]="hlr"
MOD_JOURNAL[sgsn]="osmo-sgsn"
MOD_TIMEOUT[sgsn]=25
MOD_ENABLED_IF[sgsn]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_GPRS:-1}" = 1 ]'

: "${SGSN_UNIT:=osmo-sgsn}"
: "${SGSN_VTY_PORT:=4245}"

_sgsn_cfg()  { core_cfg osmo-sgsn; }
_sgsn_ns_ip()   { core_cfg_field "$(_sgsn_cfg)" '^[[:space:]]*listen[[:space:]]+[0-9]' 2 ""; }
_sgsn_ns_port() { core_cfg_field "$(_sgsn_cfg)" '^[[:space:]]*listen[[:space:]]+[0-9]' 3 23000; }

mod_sgsn_check() {
    core_bin osmo-sgsn >/dev/null || { mod_fail "binaire osmo-sgsn introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_sgsn_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-sgsn.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_sgsn_cfg)"; return $MOD_RC_FAIL; }
    local ip; ip="$(_sgsn_ns_ip)"
    if [ -n "$ip" ] && ! core_ip_local "$ip"; then
        mod_hint "corrigez « ns / bind udp local / listen » dans $(_sgsn_cfg)"
        mod_fail "adresse NS $ip portée par aucune interface"; return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_sgsn_status() { core_unit_active "$SGSN_UNIT" || core_vty_listen "$SGSN_VTY_PORT"; }

mod_sgsn_start() {
    core_svc_start "$SGSN_UNIT" "$(core_bin osmo-sgsn)" -c "$(_sgsn_cfg)" \
        || { mod_fail "systemctl start $SGSN_UNIT a échoué"
             mod_hint "journalctl -u $SGSN_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sgsn_wait() {
    local to="${MOD_TIMEOUT[sgsn]}" ip port
    ip="$(_sgsn_ns_ip)"; port="$(_sgsn_ns_port)"

    if ! wait_until "$to" "VTY OsmoSGSN ($SGSN_VTY_PORT)" core_vty_listen "$SGSN_VTY_PORT"; then
        mod_hint "journalctl -u $SGSN_UNIT -n 30"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "NS UDP ($ip:$port)" core_udp_listen "$port" "$ip"; then
        mod_hint "adresse $ip absente ou port $port déjà pris : ss -lun | grep $port"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$SGSN_UNIT"; then
        mod_hint "journalctl -u $SGSN_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "OsmoSGSN a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_sgsn_stop() { core_svc_stop "$SGSN_UNIT" "osmo-sgsn -c"; }

run_modules/18-pcu.sh

.sh 53 lignes - 2,0KB

run_modules/18-pcu.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER pcu "Cœur — OsmoPCU (ordonnanceur paquet)"
MOD_REQUIRED[pcu]=0
MOD_DEPS[pcu]="sgsn"
MOD_JOURNAL[pcu]="osmo-pcu"
MOD_TIMEOUT[pcu]=20
MOD_ENABLED_IF[pcu]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_GPRS:-1}" = 1 ]'

: "${PCU_UNIT:=osmo-pcu}"
: "${PCU_VTY_PORT:=4240}"
: "${PCU_SOCKET:=/tmp/pcu_bts}"

_pcu_cfg() { core_cfg osmo-pcu; }

mod_pcu_check() {
    core_bin osmo-pcu >/dev/null || { mod_fail "binaire osmo-pcu introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_pcu_cfg)" ] || {
        mod_hint "déployez la configuration : cp configs/osmo-pcu.cfg $OSMOCOM_CFG/"
        mod_fail "configuration illisible : $(_pcu_cfg)"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_pcu_status() { core_unit_active "$PCU_UNIT" || core_vty_listen "$PCU_VTY_PORT"; }

mod_pcu_start() {
    core_svc_start "$PCU_UNIT" "$(core_bin osmo-pcu)" -c "$(_pcu_cfg)" \
        || { mod_fail "systemctl start $PCU_UNIT a échoué"
             mod_hint "journalctl -u $PCU_UNIT -n 30"; return $MOD_RC_FAIL; }
    [ -e "$PCU_SOCKET" ] && chmod 777 "$PCU_SOCKET" 2>/dev/null
    mod_ok
}

mod_pcu_wait() {
    if ! core_alive "$PCU_UNIT"; then
        mod_hint "journalctl -u $PCU_UNIT -n 30"
        mod_fail "osmo-pcu n'est pas resté en vie après le lancement"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "${MOD_TIMEOUT[pcu]}" "VTY OsmoPCU ($PCU_VTY_PORT)" core_vty_listen "$PCU_VTY_PORT"; then
        mod_say "VTY $PCU_VTY_PORT non observé — port non déclaré dans $(_pcu_cfg), repli sur la sonde de vie"
    fi
    if core_restarted_since "$PCU_UNIT"; then
        mod_hint "journalctl -u $PCU_UNIT -n 50 : conf invalide ou socket PCU inaccessible"
        mod_fail "OsmoPCU redémarre en boucle"
        return $MOD_RC_FAIL
    fi
    core_unix_listen "$PCU_SOCKET" && mod_say "socket PCU $PCU_SOCKET présente (BTS déjà démarrée)"
    mod_ok
}

mod_pcu_stop() { core_svc_stop "$PCU_UNIT" "osmo-pcu -c"; }

run_modules/19-asterisk.sh

.sh 66 lignes - 2,2KB

run_modules/19-asterisk.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER asterisk "Cœur — Asterisk (PBX SIP)"
MOD_REQUIRED[asterisk]=0
MOD_JOURNAL[asterisk]="asterisk"
MOD_TIMEOUT[asterisk]=45
MOD_ENABLED_IF[asterisk]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_VOICE:-1}" = 1 ]'

: "${ASTERISK_UNIT:=asterisk}"
: "${ASTERISK_CFG:=/etc/asterisk/asterisk.conf}"

_ast_cli() { asterisk -rx "$1" 2>/dev/null; }
_ast_ready() { _ast_cli "core show uptime" | grep -qi 'uptime'; }

mod_asterisk_check() {
    command -v asterisk >/dev/null 2>&1 || {
        mod_hint "installez asterisk, ou désactivez la voix : CORE_VOICE=0"
        mod_fail "binaire asterisk introuvable"; return $MOD_RC_FAIL; }
    [ -r "$ASTERISK_CFG" ] || {
        mod_fail "configuration illisible : $ASTERISK_CFG"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_asterisk_status() { _ast_ready; }

mod_asterisk_start() {
    if _ast_ready; then mod_say "déjà actif — on ne relance pas"; mod_ok; return 0; fi

    local bin pf log
    bin="$(command -v asterisk)"
    pf="$(core_pidfile "$ASTERISK_UNIT")"
    log="${LOG_DIR}/${ASTERISK_UNIT}.log"
    mkdir -p "$RUN_DIR" "$LOG_DIR" 2>/dev/null || true

    mod_say "lancement direct : $bin -f -U asterisk"
    setsid "$bin" -f -U asterisk >>"$log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "$pf"

    sleep 1
    if ! kill -0 "$(cat "$pf" 2>/dev/null)" 2>/dev/null; then
        mod_hint "tail -30 $log"
        mod_fail "asterisk est mort dans la seconde qui a suivi le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_asterisk_wait() {
    if ! wait_until "${MOD_TIMEOUT[asterisk]}" "console Asterisk" _ast_ready; then
        mod_hint "asterisk -rx 'core show uptime' ; journalctl -u $ASTERISK_UNIT -n 40"
        return $MOD_RC_FAIL
    fi
    local pf; pf="$(core_pidfile "$ASTERISK_UNIT")"
    if [ -f "$pf" ] && ! kill -0 "$(cat "$pf" 2>/dev/null)" 2>/dev/null; then
        mod_hint "tail -50 ${LOG_DIR}/${ASTERISK_UNIT}.log"
        mod_fail "le PID lancé n'existe plus : Asterisk est mort ou a été remplacé"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_asterisk_stop() {
    _ast_cli "core stop now" >/dev/null 2>&1
    core_svc_stop "$ASTERISK_UNIT" ""
}

run_modules/20-mobile-cfg.sh

.sh 89 lignes - 3,9KB

run_modules/20-mobile-cfg.sh
MOD_REGISTER mobile-cfg "Configuration du mobile"
MOD_REQUIRED[mobile-cfg]=0
MOD_DEPS[mobile-cfg]="config"
MOD_TIMEOUT[mobile-cfg]=10

_mcfg_imsi() { grep -oE '^[[:space:]]*imsi [0-9]{15}' "${MOBILE_CFG:-}" 2>/dev/null | head -1 | grep -oE '[0-9]{15}'; }
_mcfg_ki()   { sed -n 's/^[[:space:]]*ki comp128 \([0-9a-fA-F ]*\)$/\1/p' "${MOBILE_CFG:-}" 2>/dev/null | head -1 | tr -d ' '; }
_mcfg_stick(){ sed -n 's/^[[:space:]]*stick \([0-9]\+\).*/\1/p' "${MOBILE_CFG:-}" 2>/dev/null | head -1; }

mod_mobile_cfg_check() {
    if [ "${CALYPSO_SYNC_MOBILE_CFG:-1}" != "1" ]; then
        [ -r "${MOBILE_CFG:-}" ] && { mod_say "synchronisation désactivée, fichier existant conservé"; mod_ok; return $MOD_RC_OK; }
        mod_hint "CALYPSO_SYNC_MOBILE_CFG=1 pour réinstaller la version du dépôt"
        mod_fail "synchronisation désactivée et ${MOBILE_CFG:-<non défini>} absent"
        return $MOD_RC_FAIL
    fi
    if [ ! -r "${MOBILE_CFG_SRC:-}" ]; then
        mod_hint "attendu : \$QEMU_TREE/cfgs/mobile_group1.cfg — vérifiez QEMU_CFGS dans paths.env"
        mod_skip "configuration versionnée absente : ${MOBILE_CFG_SRC:-<non défini>}"
        return $MOD_RC_SKIP
    fi
    mod_ok
}

mod_mobile_cfg_status() {
    [ -r "${MOBILE_CFG:-}" ] || return 1
    [ "${CALYPSO_SYNC_MOBILE_CFG:-1}" = "1" ] || return 0
    cmp -s "${MOBILE_CFG_SRC:-}" "$MOBILE_CFG" 2>/dev/null || return 1
    [ -z "${CALYPSO_STICK_ARFCN:-}" ] || [ "${CALYPSO_STICK_ARFCN}" = "0" ]
}

mod_mobile_cfg_start() {
    if [ "${CALYPSO_SYNC_MOBILE_CFG:-1}" = "1" ]; then
        mkdir -p "$(dirname "$MOBILE_CFG")" 2>/dev/null
        if ! cp "$MOBILE_CFG_SRC" "$MOBILE_CFG" 2>/dev/null; then
            mod_hint "vérifiez les droits sur $(dirname "$MOBILE_CFG")"
            mod_fail "copie impossible : $MOBILE_CFG_SRC -> $MOBILE_CFG"
            return $MOD_RC_FAIL
        fi
        mod_say "installée : $MOBILE_CFG_SRC -> $MOBILE_CFG"
    fi

    local a="${CALYPSO_STICK_ARFCN:-}"
    if [ -n "$a" ] && [ "$a" != "0" ]; then
        if grep -qE '^[[:space:]]*stick [0-9]+' "$MOBILE_CFG" 2>/dev/null; then
            sed -i "s/^\([[:space:]]*\)stick [0-9]\+/\1stick $a/" "$MOBILE_CFG"
        elif grep -qE '^[[:space:]]*no stick' "$MOBILE_CFG" 2>/dev/null; then
            sed -i "s/^\([[:space:]]*\)no stick/\1stick $a/" "$MOBILE_CFG"
        else
            mod_say "aucune directive stick/no stick dans $MOBILE_CFG : rien à surcharger"
        fi
        mod_say "stick demandé = $a, lu = $(_mcfg_stick)"
    fi

    MOBILE_IMSI="$(_mcfg_imsi)"
    MOBILE_KI="$(_mcfg_ki)"
    mod_say "IMSI = ${MOBILE_IMSI:-<non trouvé>}"
    mod_say "Ki   = ${MOBILE_KI:+<présent, ${#MOBILE_KI} caractères>}${MOBILE_KI:-<non trouvé>}"
    mod_ok
}

_mcfg_ready() {
    [ -s "${MOBILE_CFG:-}" ] || return 1
    [ -n "$(_mcfg_imsi)" ] || return 1
    [ -n "$(_mcfg_ki)" ]   || return 1
    local a="${CALYPSO_STICK_ARFCN:-}"
    if [ -n "$a" ] && [ "$a" != "0" ] && grep -qE '^[[:space:]]*stick ' "$MOBILE_CFG" 2>/dev/null; then
        [ "$(_mcfg_stick)" = "$a" ] || return 1
    fi
    return 0
}

mod_mobile_cfg_wait() {
    if ! wait_until "${MOD_TIMEOUT[mobile-cfg]}" "configuration du mobile" _mcfg_ready; then
        if [ ! -s "${MOBILE_CFG:-}" ]; then
            mod_fail "$MOBILE_CFG absent ou vide après la copie"
        elif [ -n "${CALYPSO_STICK_ARFCN:-}" ] && [ "$(_mcfg_stick)" != "${CALYPSO_STICK_ARFCN}" ]; then
            mod_hint "vérifiez la ligne 'stick' de $MOBILE_CFG_SRC"
            mod_fail "ARFCN non appliqué : demandé ${CALYPSO_STICK_ARFCN}, lu $(_mcfg_stick)"
        else
            mod_hint "attendu dans $MOBILE_CFG : une ligne 'imsi <15 chiffres>' et une ligne 'ki comp128 <16 octets>'"
            mod_fail "IMSI ou Ki introuvable dans $MOBILE_CFG — le HLR ne pourra pas être alimenté"
        fi
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_mobile_cfg_stop() { return 0; }

run_modules/20-sip-connector.sh

.sh 64 lignes - 2,7KB

run_modules/20-sip-connector.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER sip-connector "Cœur — passerelle MNCC↔SIP"
MOD_REQUIRED[sip-connector]=0
MOD_DEPS[sip-connector]="msc asterisk"
MOD_JOURNAL[sip-connector]="osmo-sip-connector"
MOD_TIMEOUT[sip-connector]=25
MOD_ENABLED_IF[sip-connector]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_VOICE:-1}" = 1 ]'

: "${SIPC_UNIT:=osmo-sip-connector}"

_sipc_cfg()  { core_cfg osmo-sip-connector; }
_sipc_ip()   { core_cfg_field "$(_sipc_cfg)" '^[[:space:]]*local[[:space:]]+[0-9]' 2 127.0.0.1; }
_sipc_port() { core_cfg_field "$(_sipc_cfg)" '^[[:space:]]*local[[:space:]]+[0-9]' 3 5060; }
_sipc_mncc() { core_cfg_field "$(_sipc_cfg)" '^[[:space:]]*socket-path[[:space:]]' 2 /tmp/msc_mncc; }

_sipc_mncc_connected() {
    local p; p="$(_sipc_mncc)"
    ss -xH 2>/dev/null | grep -F -- "$p" | grep -q 'ESTAB\|CONNECTED'
}

mod_sip_connector_check() {
    core_bin osmo-sip-connector >/dev/null || {
        mod_hint "installez osmo-sip-connector, ou désactivez la voix : CORE_VOICE=0"
        mod_fail "binaire osmo-sip-connector introuvable"; return $MOD_RC_FAIL; }
    [ -r "$(_sipc_cfg)" ] || {
        mod_fail "configuration illisible : $(_sipc_cfg)"; return $MOD_RC_FAIL; }
    core_ip_local "$(_sipc_ip)" || {
        mod_hint "corrigez « sip / local » dans $(_sipc_cfg)"
        mod_fail "adresse SIP $(_sipc_ip) portée par aucune interface"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sip_connector_status() { core_unit_active "$SIPC_UNIT" || have_proc "osmo-sip-connector -c"; }

mod_sip_connector_start() {
    core_svc_start "$SIPC_UNIT" "$(core_bin osmo-sip-connector)" -c "$(_sipc_cfg)" \
        || { mod_fail "systemctl start $SIPC_UNIT a échoué"
             mod_hint "journalctl -u $SIPC_UNIT -n 30"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sip_connector_wait() {
    local to="${MOD_TIMEOUT[sip-connector]}" ip port
    ip="$(_sipc_ip)"; port="$(_sipc_port)"

    if ! wait_until "$to" "bind SIP ($ip:$port)" core_udp_listen "$port" "$ip"; then
        mod_hint "port $port déjà pris (Asterisk ?) : ss -lun | grep $port"
        return $MOD_RC_FAIL
    fi
    if ! wait_until "$to" "connexion MNCC ($(_sipc_mncc))" _sipc_mncc_connected; then
        mod_hint "le MSC expose-t-il « mncc external $(_sipc_mncc) » ? ss -x | grep $(_sipc_mncc)"
        return $MOD_RC_FAIL
    fi
    if core_restarted_since "$SIPC_UNIT"; then
        mod_hint "journalctl -u $SIPC_UNIT -n 50 : le service redémarre en boucle"
        mod_fail "osmo-sip-connector a redémarré depuis le lancement"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_sip_connector_stop() { core_svc_stop "$SIPC_UNIT" "osmo-sip-connector -c"; }

run_modules/21-abonnes-hlr.sh

.sh 94 lignes - 4,1KB

run_modules/21-abonnes-hlr.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER abonnes-hlr "Cœur — abonnés provisionnés dans le HLR"
MOD_REQUIRED[abonnes-hlr]=0
MOD_DEPS[abonnes-hlr]="hlr"
MOD_TIMEOUT[abonnes-hlr]=30
MOD_ENABLED_IF[abonnes-hlr]='[ "${NO_OSMO_START:-0}" != 1 ]'

: "${HLR_VTY_PORT:=4258}"
: "${HLR_DB:=/var/lib/osmocom/hlr.db}"
: "${OPERATOR_ID:=1}"
: "${N_MS:=1}"

_abo_mcc() { printf '%s\n' "${MCC:-$(core_cfg_field "$(core_cfg osmo-msc)" '^[[:space:]]*network country code[[:space:]]' 4 001)}"; }
_abo_mnc() { printf '%s\n' "${MNC:-$(core_cfg_field "$(core_cfg osmo-msc)" '^[[:space:]]*mobile network code[[:space:]]' 4 01)}"; }

_abo_imsi() { printf '%s%s%04d%06d\n' "$(_abo_mcc)" "$(_abo_mnc)" "$OPERATOR_ID" "$1"; }
_abo_ki()   { printf '00112233445566778899aabbccdd%02x%02x\n' "$1" "$OPERATOR_ID"; }

_abo_node() {
    local n="${OSMO_WAN_NODE:-${WAN_NODE_ID:-}}"
    [ -n "$n" ] || n="$(sed -n 's/^PLAN_NODE=//p' "${OSMOCOM_CFG:-/etc/osmocom}/radio-plan.env" 2>/dev/null | tail -1)"
    case "$n" in [1-9]) ;; *) n=1 ;; esac
    printf '%s' "$n"
}
_abo_msisdn() { echo $(( $(_abo_node) * 100000 + OPERATOR_ID * 100 + $1 )); }

_abo_present() {
    local imsi="$1" n=""
    if [ -r "$HLR_DB" ] && command -v sqlite3 >/dev/null 2>&1; then
        n="$(sqlite3 -readonly "$HLR_DB" "select count(*) from subscriber where imsi='$imsi';" 2>/dev/null)"
        case "$n" in ''|*[!0-9]*) n="" ;; esac
        if [ -n "$n" ]; then [ "$n" -ge 1 ]; return $?; fi
    fi
    core_vty_ask "$HLR_VTY_PORT" "enable" "subscriber imsi $imsi show" 2>/dev/null | grep -q "IMSI: $imsi"
}

mod_abonnes_hlr_check() {
    case "$N_MS" in ''|*[!0-9]*) mod_fail "N_MS invalide : « $N_MS »"; return $MOD_RC_FAIL ;; esac
    [ "$N_MS" -ge 1 ] || { mod_skip "N_MS=$N_MS : aucun abonné à créer"; return $MOD_RC_SKIP; }
    core_vty_listen "$HLR_VTY_PORT" || {
        mod_hint "le HLR doit être prêt : ./run.sh --only hlr"
        mod_fail "VTY HLR ($HLR_VTY_PORT) injoignable"; return $MOD_RC_FAIL; }
    command -v socat >/dev/null 2>&1 || command -v telnet >/dev/null 2>&1 || {
        mod_hint "installez socat (ou telnet) : le provisionnement passe par le VTY"
        mod_fail "ni socat ni telnet — impossible d'écrire dans le HLR"; return $MOD_RC_FAIL; }
    mod_ok
}

_abo_msisdn_now() {
    local imsi="$1" v=""
    if [ -r "$HLR_DB" ] && command -v sqlite3 >/dev/null 2>&1; then
        v="$(sqlite3 -readonly "$HLR_DB" "select msisdn from subscriber where imsi='$imsi';" 2>/dev/null)"
        [ -n "$v" ] && { printf '%s' "$v"; return 0; }
    fi
    core_vty_ask "$HLR_VTY_PORT" "enable" "subscriber imsi $imsi show" 2>/dev/null \
        | sed -n 's/.*MSISDN: *\([0-9][0-9]*\).*/\1/p' | head -1
}

mod_abonnes_hlr_status() {
    local imsi; imsi="$(_abo_imsi "$N_MS")"
    _abo_present "$imsi" || return 1
    [ "$(_abo_msisdn_now "$imsi")" = "$(_abo_msisdn "$N_MS")" ]
}

mod_abonnes_hlr_start() {
    local m cmds=() imsi msisdn
    cmds+=("enable")
    for m in $(seq 1 "$N_MS"); do
        imsi="$(_abo_imsi "$m")"
        msisdn="$(_abo_msisdn "$m")"
        cmds+=("subscriber imsi $imsi create")
        cmds+=("subscriber imsi $imsi update msisdn $msisdn")
        cmds+=("subscriber imsi $imsi update aud2g comp128v1 ki $(_abo_ki "$m")")
    done
    cmds+=("exit")
    mod_say "provisionnement de $N_MS abonné(s), nœud $(_abo_node), opérateur $OPERATOR_ID, PLMN $(_abo_mcc)-$(_abo_mnc) — MSISDN $(_abo_msisdn 1)..$(_abo_msisdn "$N_MS")"
    core_vty_ask "$HLR_VTY_PORT" "${cmds[@]}" || {
        mod_hint "vérifiez le VTY : socat STDIO TCP:127.0.0.1:$HLR_VTY_PORT,crlf"
        mod_fail "le dialogue VTY avec le HLR n'a rien retourné"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_abonnes_hlr_wait() {
    local imsi; imsi="$(_abo_imsi "$N_MS")"
    if ! wait_until "${MOD_TIMEOUT[abonnes-hlr]}" "abonné $imsi présent dans le HLR" _abo_present "$imsi"; then
        mod_hint "essayez à la main : subscriber imsi $imsi show (VTY $HLR_VTY_PORT) ; base = $HLR_DB"
        return $MOD_RC_FAIL
    fi
    mod_ok
}

mod_abonnes_hlr_stop() { return 0; }

run_modules/22-smsc.sh

.sh 68 lignes - 2,8KB

run_modules/22-smsc.sh
: "${MODDIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
. "$MODDIR/_lib/core.sh"

MOD_REGISTER smsc "Cœur — passerelle SMS (proto-SMSC)"
MOD_REQUIRED[smsc]=0
MOD_DEPS[smsc]="hlr"
MOD_TIMEOUT[smsc]=60
MOD_ENABLED_IF[smsc]='[ "${NO_OSMO_START:-0}" != 1 ] && [ "${CORE_SMS:-1}" = 1 ]'

: "${OPERATOR_ID:=1}"
: "${SMSC_SCRIPT:=/etc/osmocom/smsc-start.sh}"
: "${SMSC_RELAY_SCRIPT:=/etc/osmocom/sms-interop-relay.py}"
: "${SMSC_RELAY_PORT:=7890}"
: "${SMSC_SENDMT_SOCKET:=/tmp/sendmt_socket}"
: "${SMSC_HLR_IP:=127.0.0.2}"

mod_smsc_check() {
    command -v proto-smsc-daemon >/dev/null 2>&1 || {
        mod_hint "sans proto-smsc-daemon les SMS passent uniquement par le VTY du MSC ; CORE_SMS=0 pour ne plus l'essayer"
        mod_skip "proto-smsc-daemon absent"; return $MOD_RC_SKIP; }
    [ -x "$SMSC_SCRIPT" ] || {
        mod_hint "déployez les scripts : cp scripts/smsc-start.sh $OSMOCOM_CFG/"
        mod_fail "script de démarrage absent : $SMSC_SCRIPT"; return $MOD_RC_FAIL; }
    core_tcp_listen 4222 "$SMSC_HLR_IP" || {
        mod_hint "le HLR doit écouter en GSUP : ./run.sh --only hlr"
        mod_fail "GSUP HLR ($SMSC_HLR_IP:4222) injoignable"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_smsc_status() { have_proc 'proto-smsc-daemon'; }

mod_smsc_start() {
    mkdir -p "$RUN_DIR" "$LOG_DIR" /var/log/osmocom 2>/dev/null || true
    pkill -f 'proto-smsc-daemon' 2>/dev/null
    local log="$LOG_DIR/smsc-op${OPERATOR_ID}.log"
    mod_say "journal : $log ; SMS MO reçus : /var/log/osmocom/mo-sms-op${OPERATOR_ID}.log"
    setsid env OPERATOR_ID="$OPERATOR_ID" HLR_IP="$SMSC_HLR_IP" RELAY_PORT="$SMSC_RELAY_PORT" \
        stdbuf -oL -eL bash "$SMSC_SCRIPT" >"$log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "$RUN_DIR/smsc-op${OPERATOR_ID}.pid"
    mod_ok
}

mod_smsc_wait() {
    local to="${MOD_TIMEOUT[smsc]}"

    if ! wait_until "$to" "socket d'envoi MT ($SMSC_SENDMT_SOCKET)" core_unix_listen "$SMSC_SENDMT_SOCKET"; then
        mod_hint "voir $LOG_DIR/smsc-op${OPERATOR_ID}.log : le daemon n'a pas pu se connecter au HLR ($SMSC_HLR_IP:4222)"
        return $MOD_RC_FAIL
    fi
    if [ -f "$SMSC_RELAY_SCRIPT" ]; then
        if ! wait_until "$to" "relay SMS interop (TCP $SMSC_RELAY_PORT)" core_tcp_listen "$SMSC_RELAY_PORT"; then
            mod_hint "port $SMSC_RELAY_PORT déjà pris, ou sms-routing.conf illisible : voir $LOG_DIR/smsc-op${OPERATOR_ID}.log"
            return $MOD_RC_FAIL
        fi
    else
        mod_say "$SMSC_RELAY_SCRIPT absent — pas de relay inter-opérateurs, barrière non applicable"
    fi
    mod_ok
}

mod_smsc_stop() {
    local pf="$RUN_DIR/smsc-op${OPERATOR_ID}.pid"
    [ -f "$pf" ] && { kill "$(cat "$pf" 2>/dev/null)" 2>/dev/null; rm -f "$pf"; }
    pkill -f 'proto-smsc-daemon' 2>/dev/null
    pkill -f 'sms-interop-relay.py' 2>/dev/null
    rm -f "$SMSC_SENDMT_SOCKET" 2>/dev/null
    return 0
}

run_modules/25-audio.sh

.sh 75 lignes - 2,7KB

run_modules/25-audio.sh
MOD_REGISTER audio "Pont audio (PulseAudio + GAPK)"
MOD_REQUIRED[audio]=0
MOD_TIMEOUT[audio]=25

MOD_ENABLED_IF[audio]='[ "${AUDIO:-1}" = 1 ]'

: "${EGPRS_DIR:=${NITB_ROOT}}"
: "${AUDIO_LIB:=$EGPRS_DIR/lib/audio.sh}"
: "${AUDIO_SINK:=gsm_audio}"
: "${AUDIO_TMUX_SESSION:=gapk}"

_audio_sink_present() {
    pactl list short sinks 2>/dev/null | grep -q "$AUDIO_SINK"
}
_audio_gapk_vivant() {
    pgrep -f "gapk-start.sh" >/dev/null 2>&1 \
        || tmux has-session -t "${AUDIO_TMUX:-gapk}" 2>/dev/null
}

mod_audio_check() {
    if [ ! -r "$AUDIO_LIB" ]; then
        mod_hint "la bibliothèque est l'extraction de start-direct.sh.legacy L566-722 ; elle accompagne osmo_egprs"
        mod_skip "bibliothèque audio absente ($AUDIO_LIB) : pont audio non monté"
        return $MOD_RC_SKIP
    fi
    local manquant=""
    command -v tmux      >/dev/null 2>&1 || manquant="$manquant tmux"
    command -v osmo-gapk >/dev/null 2>&1 || manquant="$manquant osmo-gapk"
    if [ -n "$manquant" ]; then
        mod_hint "installez-les pour avoir le son :$manquant — ou AUDIO=0 pour ne plus l'essayer"
        mod_skip "outillage audio absent :$manquant"
        return $MOD_RC_SKIP
    fi
    mod_ok
}

mod_audio_status() { _audio_gapk_vivant && _audio_sink_present; }

mod_audio_start() {
    local retour="$PWD" rc=0
    cd "$EGPRS_DIR" || { mod_fail "impossible d'entrer dans $EGPRS_DIR"; return $MOD_RC_FAIL; }
    HERE="$EGPRS_DIR"
    . "$AUDIO_LIB" || { cd "$retour"; mod_fail "bibliothèque illisible : $AUDIO_LIB"; return $MOD_RC_FAIL; }

    mod_say "sink visé : $AUDIO_SINK · session tmux : $AUDIO_TMUX_SESSION"
    ensure_gapk || rc=$?
    cd "$retour"
    [ "$rc" -eq 0 ] || { mod_fail "ensure_gapk a rendu $rc"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_audio_wait() {
    wait_until "${MOD_TIMEOUT[audio]}" "démon PulseAudio" pactl info || {
        mod_hint "regardez $LOG_DIR/pulse-system.log : le démon système n'a pas démarré"
        mod_fail "PulseAudio ne répond pas"
        return $MOD_RC_FAIL
    }
    wait_until "${MOD_TIMEOUT[audio]}" "sink $AUDIO_SINK" _audio_sink_present || {
        mod_hint "pactl load-module module-null-sink sink_name=$AUDIO_SINK"
        mod_fail "le sink $AUDIO_SINK n'est pas enregistré"
        return $MOD_RC_FAIL
    }
    wait_until "${MOD_TIMEOUT[audio]}" "superviseur gapk" _audio_gapk_vivant || {
        mod_hint "regardez $LOG_DIR/gapk-auto.log : le superviseur s'est arrêté aussitôt lancé"
        mod_fail "le superviseur gapk ne tourne pas"
        return $MOD_RC_FAIL
    }
    mod_ok
}

mod_audio_stop() {
    tmux kill-session -t "$AUDIO_TMUX_SESSION" 2>/dev/null
    pkill -x osmo-gapk 2>/dev/null
    return 0
}

run_modules/35-tmux.sh

.sh 53 lignes - 2,0KB

run_modules/35-tmux.sh
MOD_REGISTER tmux "Session d'observation tmux"
MOD_REQUIRED[tmux]=0
MOD_DEPS[tmux]="logs"
MOD_TIMEOUT[tmux]=10
MOD_ENABLED_IF[tmux]='command -v tmux >/dev/null 2>&1'

: "${TMUX_SESSION:=calypso}"
: "${TMUX_MODULES_DIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")/../tmux_modules" 2>/dev/null && pwd)}"

mod_tmux_check() {
    command -v tmux >/dev/null 2>&1 || {
        mod_hint "apt-get install tmux — ou lancez sans : les services tournent quand même"
        mod_fail "tmux introuvable"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_tmux_status() { tmux has-session -t "$TMUX_SESSION" 2>/dev/null; }

_tmux_charger() {
    local d="${TMUX_MODULES_DIR:-}"
    [ -n "$d" ] && [ -r "$d/_commun.sh" ] && [ -r "$d/bench.sh" ] || {
        mod_hint "attendu : <dépôt>/tmux_modules/_commun.sh et bench.sh"
        mod_fail "disposition tmux introuvable (TMUX_MODULES_DIR=${d:-<vide>})"
        return $MOD_RC_FAIL; }
    . "$d/_commun.sh"
    . "$d/bench.sh"
    mod_say "disposition : bench.sh"
    return 0
}

mod_tmux_start() {
    _tmux_charger || return $MOD_RC_FAIL
    tmux new-session -d -s "$TMUX_SESSION" -n "${TMUX_FENETRE_PREMIERE:-radio}" \
        "$(tmux_layout_premiere)" 2>/dev/null || {
        mod_hint "un serveur tmux périmé peut refuser la session : tmux kill-server puis relancez"
        mod_fail "création de la session « $TMUX_SESSION » refusée"; return $MOD_RC_FAIL; }
    _tmux_env_repropre
    tmux_layout
    _tmux_style
    tmux select-window -t "$TMUX_SESSION:${TMUX_FENETRE_PREMIERE:-radio}" 2>/dev/null
    mod_say "${TMUX_RESUME:-session créée}"
    mod_ok
}

mod_tmux_wait() {
    wait_until "${MOD_TIMEOUT[tmux]}" "session tmux « $TMUX_SESSION »" \
        tmux has-session -t "$TMUX_SESSION" || return $MOD_RC_FAIL
    [ "$(tmux list-windows -t "$TMUX_SESSION" 2>/dev/null | wc -l)" -ge 1 ] || {
        mod_fail "session créée mais aucune fenêtre"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_tmux_stop() { tmux kill-session -t "$TMUX_SESSION" 2>/dev/null; return 0; }

run_modules/40-qemu.sh

.sh 98 lignes - 3,7KB

run_modules/40-qemu.sh
MOD_REGISTER qemu "Émulateur Calypso (QEMU)"
MOD_REQUIRED[qemu]=1
MOD_DEPS[qemu]="prereqs"
MOD_TIMEOUT[qemu]=30

: "${QEMU_LOG_MAX:=$((64 * 1024 * 1024))}"
: "${QEMU_LOG_HEAD:=$((8 * 1024 * 1024))}"

mod_qemu_check() {
    [ -x "${QEMU_BIN:-}" ] || { mod_fail "binaire QEMU absent : ${QEMU_BIN:-<non défini>}"
                                mod_hint "compilez-le : ./configure --target-list=arm-softmmu && ninja -C build qemu-system-arm"
                                return $MOD_RC_FAIL; }
    [ -r "${FIRMWARE_ELF:-}" ] || { mod_fail "firmware ARM introuvable : ${FIRMWARE_ELF:-<non défini>}"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_qemu_status() { have_proc "qemu-system-arm.*calypso"; }

_qemu_save_head() {
    local src="$1" dst="$2" taille="$3" i=0
    while [ "$i" -lt 120 ]; do
        if [ -s "$src" ] && [ "$(stat -c %s "$src" 2>/dev/null || echo 0)" -ge "$taille" ]; then
            head -c "$taille" "$src" > "$dst" 2>/dev/null
            return 0
        fi
        sleep 1; i=$(( i + 1 ))
    done
    [ -s "$src" ] && head -c "$taille" "$src" > "$dst" 2>/dev/null
    return 0
}

_qemu_log_guard() {
    local f="$1" max="$2" qpid="$3" n=0
    while kill -0 "$qpid" 2>/dev/null; do
        if [ -f "$f" ] && [ "$(stat -c %s "$f" 2>/dev/null || echo 0)" -gt "$max" ]; then
            n=$((n+1))
            printf '\n--- journal remis à zéro (%d fois) : plafond de %s octets atteint.\n--- La tête du journal est conservée dans qemu-tete.log ---\n' \
                   "$n" "$max" > "$f"
        fi
        sleep 5
    done
}

mod_qemu_start() {
    local qlog="${LOG_DIR}/qemu.log" qpid
    mod_say "machine  : calypso"
    mod_say "journal  : $qlog (plafond ${QEMU_LOG_MAX} o)"
    setsid "$QEMU_BIN" -M calypso -cpu arm946 \
        -serial pty -serial pty \
        -monitor "unix:${RUN_DIR}/qemu-monitor.sock,server,nowait" \
        -kernel "$FIRMWARE_ELF" >>"$qlog" 2>&1 </dev/null &
    qpid=$!
    printf '%s\n' "$qpid" > "${RUN_DIR}/qemu.pid"
    ( trap '' HUP; _qemu_save_head "$qlog" "${LOG_DIR}/qemu-tete.log" "${QEMU_LOG_HEAD}" ) >/dev/null 2>&1 </dev/null &
    ( trap '' HUP; _qemu_log_guard "$qlog" "$QEMU_LOG_MAX" "$qpid" ) >/dev/null 2>&1 </dev/null &
    mod_say "tête     : ${LOG_DIR}/qemu-tete.log"
    mod_ok
}

_qemu_running() {
    local sock="$1"
    printf 'info status\n' \
        | timeout 3 socat -t 1 - "UNIX-CONNECT:$sock" 2>/dev/null \
        | grep -q 'VM status: running'
}

mod_qemu_wait() {
    local sock="${RUN_DIR}/qemu-monitor.sock" qlog="${LOG_DIR}/qemu.log"
    wait_until "${MOD_TIMEOUT[qemu]}" "socket du moniteur QEMU" have_unix "$sock" || return $MOD_RC_FAIL
    local qpid; qpid="$(cat "${RUN_DIR}/qemu.pid" 2>/dev/null || echo 0)"
    if ! kill -0 "$qpid" 2>/dev/null; then
        modb_tail "$qlog" 20
        mod_hint "cause typique : machine type ou firmware invalide"
        mod_fail "QEMU a démarré puis s'est arrêté"
        return $MOD_RC_FAIL
    fi
    if command -v socat >/dev/null 2>&1; then
        wait_until "${MOD_TIMEOUT[qemu]}" "machine virtuelle en marche" _qemu_running "$sock" || {
            modb_tail "$qlog" 20
            mod_hint "socat - UNIX-CONNECT:$sock puis « info status » pour voir l'état de la machine"
            return $MOD_RC_FAIL
        }
    else
        wait_until "${MOD_TIMEOUT[qemu]}" "PTY série alloués" grep -aq 'label serial1' "$qlog" || {
            modb_tail "$qlog" 20
            return $MOD_RC_FAIL
        }
    fi
    mod_ok
}

mod_qemu_stop() {
    local qpid; qpid="$(cat "${RUN_DIR}/qemu.pid" 2>/dev/null || echo 0)"
    [ "$qpid" != 0 ] && kill "$qpid" 2>/dev/null
    pkill -f "qemu-system-arm.*calypso" 2>/dev/null
    rm -f "${RUN_DIR}/qemu.pid" "${RUN_DIR}/qemu-monitor.sock"
    return 0
}

run_modules/41-pty.sh

.sh 75 lignes - 2,6KB

run_modules/41-pty.sh
MOD_REGISTER pty "PTY série de l'émulateur"
MOD_REQUIRED[pty]=1
MOD_DEPS[pty]="qemu"
MOD_TIMEOUT[pty]=60

: "${QEMU_MON_SOCK:=${RUN_DIR:-/tmp/calypso}/qemu-monitor.sock}"

_pty_from_monitor() {
    local sock="$1" label="$2"
    [ -S "$sock" ] || return 1
    command -v socat >/dev/null 2>&1 || return 1
    printf 'info chardev\n' \
        | timeout 3 socat -t 1 - "UNIX-CONNECT:$sock" 2>/dev/null \
        | grep -E "^${label}:" \
        | grep -oE '/dev/pts/[0-9]+' \
        | head -1
}

_pty_from_log() {
    local log="$1" label="$2"
    [ -r "$log" ] || return 1
    grep -a "redirected to /dev/pts/.* (label ${label})" "$log" 2>/dev/null \
        | grep -oE '/dev/pts/[0-9]+' \
        | head -1
}

_pty_resolve() {
    local log="${LOG_DIR}/qemu.log" p
    if [ -z "${OSMOCON_PTY:-}" ] || [ ! -c "${OSMOCON_PTY:-/nonexistent}" ]; then
        p="$(_pty_from_monitor "$QEMU_MON_SOCK" serial0)"
        [ -n "$p" ] || p="$(_pty_from_log "$log" serial0)"
        [ -n "$p" ] && OSMOCON_PTY="$p"
    fi
    [ -n "${OSMOCON_PTY:-}" ] && [ -c "$OSMOCON_PTY" ]
}

mod_pty_check() {
    if ! command -v socat >/dev/null 2>&1; then
        mod_say "socat absent : lecture du moniteur impossible, secours par le journal seulement"
    fi
    [ -S "$QEMU_MON_SOCK" ] || [ -r "${LOG_DIR}/qemu.log" ] || {
        mod_hint "le module qemu doit avoir démarré : ./run.sh --only qemu"
        mod_fail "ni moniteur ($QEMU_MON_SOCK) ni journal QEMU : rien à interroger"
        return $MOD_RC_FAIL
    }
    mod_ok
}

mod_pty_status() { return 1; }
mod_pty_start() { _pty_resolve; mod_ok; }

mod_pty_wait() {
    local deadline=$(( SECONDS + ${MOD_TIMEOUT[pty]} ))
    local qpid; qpid="$(cat "${RUN_DIR:-/tmp/calypso}/qemu.pid" 2>/dev/null || echo 0)"
    while (( SECONDS < deadline )); do
        if _pty_resolve; then
            export OSMOCON_PTY
            mod_say "serial0=$OSMOCON_PTY"
            mod_ok
            return $MOD_RC_OK
        fi
        if [ "$qpid" != 0 ] && ! kill -0 "$qpid" 2>/dev/null; then
            modb_tail "${LOG_DIR}/qemu.log" 20
            mod_hint "cause typique : machine type ou firmware invalide"
            mod_fail "QEMU (pid $qpid) s'est arrêté avant d'allouer son PTY"
            return $MOD_RC_FAIL
        fi
        sleep 0.5
    done
    mod_hint "vérifiez que QEMU est lancé avec « -serial pty » ; sinon : socat - UNIX-CONNECT:$QEMU_MON_SOCK puis « info chardev »"
    mod_fail "aucun PTY serial0 après ${MOD_TIMEOUT[pty]}s (moniteur $QEMU_MON_SOCK et journal muets)"
    return $MOD_RC_FAIL
}

mod_pty_stop() { return 0; }

run_modules/50-osmocon.sh

.sh 77 lignes - 3,0KB

run_modules/50-osmocon.sh
MOD_REGISTER osmocon "Passerelle osmocon"
MOD_REQUIRED[osmocon]=1
MOD_DEPS[osmocon]="qemu pty"
MOD_TIMEOUT[osmocon]=30

: "${OSMOCON_MODEL:=romload}"
: "${OSMOCON_INTER_BYTE_DELAY:=100}"
: "${OSMOCON_DEBUG:=tr}"
: "${L1CTL_SOCK_PATH:=/tmp/osmocom_l2}"

_osmocon_log() { printf '%s\n' "${LOG_DIR}/osmocon.log"; }
_osmocon_vivant() { pgrep -f "osmocon .*-p " >/dev/null 2>&1; }

mod_osmocon_check() {
    [ -x "${OSMOCON:-}" ] || {
        mod_hint "réglez OSMOCOM_BB dans environnement/bench.env, ou compilez osmocon"
        mod_fail "osmocon introuvable : ${OSMOCON:-<non défini>}"; return $MOD_RC_FAIL; }
    [ -r "${FIRMWARE_BIN:-}" ] || {
        mod_hint "réglez FIRMWARE_BIN dans environnement/bench.env"
        mod_fail "image firmware illisible : ${FIRMWARE_BIN:-<non défini>}"; return $MOD_RC_FAIL; }
    [ -n "${OSMOCON_PTY:-}" ] && [ -c "$OSMOCON_PTY" ] || {
        mod_hint "le module « pty » doit avoir résolu le port série de QEMU ; regardez son journal"
        mod_fail "PTY série indisponible : ${OSMOCON_PTY:-<non résolu>}"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_osmocon_status() { _osmocon_vivant && have_unix "$L1CTL_SOCK_PATH"; }

mod_osmocon_start() {
    local log; log="$(_osmocon_log)"
    rm -f "$L1CTL_SOCK_PATH" 2>/dev/null
    mod_say "PTY      : $OSMOCON_PTY"
    mod_say "firmware : $FIRMWARE_BIN"
    mod_say "modèle   : $OSMOCON_MODEL"
    mod_say "L1CTL    : $L1CTL_SOCK_PATH"
    setsid stdbuf -oL -eL "$OSMOCON" \
        -m "$OSMOCON_MODEL" \
        -i "$OSMOCON_INTER_BYTE_DELAY" \
        -p "$OSMOCON_PTY" \
        -s "$L1CTL_SOCK_PATH" \
        "$FIRMWARE_BIN" \
        -d "$OSMOCON_DEBUG" >>"$log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "${RUN_DIR:-/tmp/calypso}/osmocon.pid"
    mod_ok
}

mod_osmocon_wait() {
    local log; log="$(_osmocon_log)"
    wait_until 5 "processus osmocon" _osmocon_vivant || {
        modb_tail "$log" 15
        mod_hint "PTY $OSMOCON_PTY refusé ? regardez $log"
        mod_fail "osmocon s'est arrêté aussitôt lancé"
        return $MOD_RC_FAIL
    }
    if ! wait_until "${MOD_TIMEOUT[osmocon]}" "socket L1CTL $L1CTL_SOCK_PATH" \
         have_unix "$L1CTL_SOCK_PATH"; then
        modb_tail "$log" 15
        if _osmocon_vivant; then
            mod_hint "osmocon vit mais n'a pas fini son romload : modèle inadapté (OSMOCON_MODEL=$OSMOCON_MODEL — « romload » attendu sous QEMU) ou PTY qui ne mène pas au baseband"
            mod_fail "aucune socket L1CTL : le firmware n'a pas été chargé"
        else
            mod_hint "regardez $log"
            mod_fail "osmocon est mort pendant le chargement"
        fi
        return $MOD_RC_FAIL
    fi
    mod_say "firmware chargé, socket L1CTL ouverte"
    mod_ok
}

mod_osmocon_stop() {
    local pid; pid="$(cat "${RUN_DIR:-/tmp/calypso}/osmocon.pid" 2>/dev/null || echo 0)"
    [ "$pid" != 0 ] && kill "$pid" 2>/dev/null
    pkill -f "osmocon .*-p " 2>/dev/null
    rm -f "${RUN_DIR:-/tmp/calypso}/osmocon.pid" "$L1CTL_SOCK_PATH"
    return 0
}

run_modules/58-sidecar-faketrx.sh

.sh 55 lignes - 2,2KB

run_modules/58-sidecar-faketrx.sh
. "$(dirname "${BASH_SOURCE[0]}")/_lib/radio.sh"

MOD_REGISTER sidecar-faketrx "Transceiver simulé du BTS#1"
MOD_REQUIRED[sidecar-faketrx]=1
MOD_DEPS[sidecar-faketrx]="sidecar-cfg"
MOD_TIMEOUT[sidecar-faketrx]=20

: "${FAKETRX_PY:=${OSMOCOM_BB:-${GSM_ROOT}/osmocom-bb}/src/target/trx_toolkit/fake_trx.py}"
: "${FAKETRX_PYTHON:=python3}"

mod_sidecar_faketrx_check() {
    command -v "$FAKETRX_PYTHON" >/dev/null 2>&1 || {
        mod_fail "interpréteur introuvable : $FAKETRX_PYTHON"; return $MOD_RC_FAIL; }
    [ -r "$FAKETRX_PY" ] || {
        mod_hint "posez FAKETRX_PY (trx_toolkit/fake_trx.py d'osmocom-bb)"
        mod_fail "fake_trx.py illisible : $FAKETRX_PY"; return $MOD_RC_FAIL; }
    local p
    for p in "$SC_TRX_PORT" "$SC_BB_PORT"; do
        if radio_udp_bound "$p" && ! radio_alive sidecar-faketrx; then
            mod_hint "ss -uanp | grep :$p — arrêtez le tenant, ou décalez SC_TRX_PORT/SC_BB_PORT"
            mod_fail "port UDP $p déjà pris par un autre processus"; return $MOD_RC_FAIL
        fi
    done
    mod_ok
}

mod_sidecar_faketrx_status() { radio_alive sidecar-faketrx; }

mod_sidecar_faketrx_start() {
    radio_dirs
    local log; log="$(radio_log sidecar-faketrx)"
    mod_say "fake_trx -P $SC_TRX_PORT (BTS#1) -p $SC_BB_PORT (bande de base)"
    mod_say "journal  : $log"
    setsid stdbuf -oL -eL "$FAKETRX_PYTHON" "$FAKETRX_PY" \
        -b "$TRX_BIND_IP" -R "$TRX_BTS_IP" -r "$TRX_BB_IP" \
        -P "$SC_TRX_PORT" -p "$SC_BB_PORT" >>"$log" 2>&1 </dev/null &
    radio_save_pid sidecar-faketrx $!
    mod_ok
}

mod_sidecar_faketrx_wait() {
    local log; log="$(radio_log sidecar-faketrx)"
    wait_until "${MOD_TIMEOUT[sidecar-faketrx]}" "port UDP $SC_TRX_PORT (côté BTS#1)" \
        radio_udp_bound "$SC_TRX_PORT" || {
        modb_tail "$log" 20
        mod_hint "fake_trx a-t-il démarré ? $log"
        mod_fail "port $SC_TRX_PORT jamais lié"; return $MOD_RC_FAIL; }
    wait_until 10 "port UDP $SC_BB_PORT (côté bande de base)" \
        radio_udp_bound "$SC_BB_PORT" || {
        mod_fail "côté BTS lié mais pas le côté bande de base ($SC_BB_PORT)"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_faketrx_stop() { radio_kill sidecar-faketrx; return 0; }

run_modules/59-sidecar-bts.sh

.sh 65 lignes - 2,3KB

run_modules/59-sidecar-bts.sh
. "$(dirname "${BASH_SOURCE[0]}")/_lib/radio.sh"

MOD_REGISTER sidecar-bts "Station de base BTS#1 (side-car)"
MOD_REQUIRED[sidecar-bts]=1
MOD_DEPS[sidecar-bts]="sidecar-faketrx bsc"
MOD_TIMEOUT[sidecar-bts]=30

: "${SC_BTS_BIN:=osmo-bts-trx}"
: "${SC_BTS_STAB_SECS:=8}"
: "${SC_BTS_RETRIES:=3}"

_sc_bts_mort() {
    grep -qE "No clock since TRX|BTS_SHUTDOWN.*Shutting down" "$(radio_log sidecar-bts)" 2>/dev/null
}

mod_sidecar_bts_check() {
    command -v "$SC_BTS_BIN" >/dev/null 2>&1 || {
        mod_fail "binaire introuvable : $SC_BTS_BIN"; return $MOD_RC_FAIL; }
    [ -r "$SC_BTS_CFG" ] || {
        mod_hint "le module « sidecar-cfg » doit avoir posé ce fichier"
        mod_fail "configuration illisible : $SC_BTS_CFG"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_bts_status() { radio_alive sidecar-bts; }

mod_sidecar_bts_start() {
    radio_dirs
    local log; log="$(radio_log sidecar-bts)"
    local essai=0 reste
    while :; do
        essai=$(( essai + 1 ))
        : > "$log"
        setsid stdbuf -oL -eL "$SC_BTS_BIN" -c "$SC_BTS_CFG" >>"$log" 2>&1 </dev/null &
        radio_save_pid sidecar-bts $!
        reste="$SC_BTS_STAB_SECS"
        while [ "$reste" -gt 0 ]; do
            radio_alive sidecar-bts || break
            _sc_bts_mort && break
            sleep 1; reste=$(( reste - 1 ))
        done
        if [ "$reste" -eq 0 ] && radio_alive sidecar-bts && ! _sc_bts_mort; then
            mod_say "BTS#1 stable après ${SC_BTS_STAB_SECS}s (essai $essai)"
            mod_ok; return $MOD_RC_OK
        fi
        radio_kill sidecar-bts
        if [ "$essai" -ge "$SC_BTS_RETRIES" ]; then
            modb_tail "$log" 20
            mod_hint "fake_trx publie-t-il son horloge ? voir $(radio_log sidecar-faketrx)"
            mod_fail "BTS#1 instable après $essai tentatives (course « No clock »)"
            return $MOD_RC_FAIL
        fi
        mod_say "BTS#1 tombé (« No clock ») — nouvelle tentative $essai/$SC_BTS_RETRIES"
    done
}

mod_sidecar_bts_wait() {
    wait_until "${MOD_TIMEOUT[sidecar-bts]}" "VTY du BTS#1 ($SC_BTS_VTY_PORT)" \
        have_port "$SC_BTS_VTY_PORT" || {
        modb_tail "$(radio_log sidecar-bts)" 20
        mod_fail "VTY $SC_BTS_VTY_PORT jamais en écoute"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_bts_stop() { radio_kill sidecar-bts; return 0; }

run_modules/60-bts.sh

.sh 90 lignes - 3,4KB

run_modules/60-bts.sh
MOD_REGISTER bts "Station de base BTS#0 (osmo-bts-trx, pont)"
MOD_REQUIRED[bts]=0
MOD_DEPS[bts]="bsc"
MOD_TIMEOUT[bts]=30

: "${OSMO_BTS_TRX:=}"
: "${BTS_CFG:=${OSMOCOM_CFG:-/etc/osmocom}/osmo-bts-trx.cfg}"
: "${BTS_LOG:=${LOG_DIR}/bts.log}"
: "${BTS_VTY_PORT:=4241}"

mod_bts_check() {
    if [ -z "$OSMO_BTS_TRX" ] || [ ! -x "$OSMO_BTS_TRX" ]; then
        OSMO_BTS_TRX="$(command -v osmo-bts-trx 2>/dev/null)"
    fi
    [ -n "$OSMO_BTS_TRX" ] && [ -x "$OSMO_BTS_TRX" ] || {
        mod_hint "installez osmo-bts (cible trx) ou posez OSMO_BTS_TRX=<chemin>"
        mod_fail "osmo-bts-trx introuvable"
        return $MOD_RC_FAIL
    }
    [ -r "$BTS_CFG" ] || {
        mod_hint "attendu : <OSMOCOM_CFG>/osmo-bts-trx.cfg — ou posez BTS_CFG=<chemin>"
        mod_fail "configuration de la BTS illisible : $BTS_CFG"
        return $MOD_RC_FAIL
    }
    have_port "${BSC_VTY_PORT:-4242}" || mod_say "AVERTISSEMENT : le VTY du BSC (${BSC_VTY_PORT:-4242}) ne répond pas — l'OML risque de boucler"
    mod_say "binaire=$OSMO_BTS_TRX cfg=$BTS_CFG"
    mod_ok
}

_bts_est_le_notre() {
    tr '\0' ' ' < "/proc/$1/cmdline" 2>/dev/null | grep -qF "$BTS_CFG"
}

mod_bts_status() {
    local pid; pid="$(cat "${RUN_DIR}/bts.pid" 2>/dev/null || echo 0)"
    if [ "$pid" != 0 ] && kill -0 "$pid" 2>/dev/null && _bts_est_le_notre "$pid"; then
        return 0
    fi
    local p
    for p in $(pgrep -x osmo-bts-trx 2>/dev/null); do
        _bts_est_le_notre "$p" && return 0
    done
    return 1
}

mod_bts_start() {
    mkdir -p "${RUN_DIR:-/tmp/calypso}" "$(dirname "$BTS_LOG")" 2>/dev/null || true
    : > "$BTS_LOG" 2>/dev/null || true
    setsid stdbuf -oL -eL "$OSMO_BTS_TRX" -c "$BTS_CFG" >>"$BTS_LOG" 2>&1 </dev/null &
    printf '%s\n' "$!" > "${RUN_DIR:-/tmp/calypso}/bts.pid"
    mod_ok
}

mod_bts_wait() {
    local pid; pid="$(cat "${RUN_DIR:-/tmp/calypso}/bts.pid" 2>/dev/null || echo 0)"
    if ! wait_until "${MOD_TIMEOUT[bts]}" "VTY de la BTS ($BTS_VTY_PORT)" have_port "$BTS_VTY_PORT"; then
        modb_tail "$BTS_LOG" 25
        if [ "$pid" != 0 ] && ! kill -0 "$pid" 2>/dev/null; then
            mod_hint "causes fréquentes : phy/instance absente de $BTS_CFG, ou unit-id déjà pris par une autre BTS"
            mod_fail "osmo-bts-trx s'est arrêté pendant son initialisation"
        else
            mod_hint "détail : $BTS_LOG"
            mod_fail "osmo-bts-trx tourne mais n'expose pas son VTY après ${MOD_TIMEOUT[bts]}s"
        fi
        return $MOD_RC_FAIL
    fi
    if [ "$pid" != 0 ] && ! kill -0 "$pid" 2>/dev/null; then
        modb_tail "$BTS_LOG" 25
        mod_hint "une BTS résiduelle (unité systemd ?) tient le VTY : systemctl stop osmo-bts-trx"
        mod_fail "le VTY $BTS_VTY_PORT répond, mais ce n'est PAS notre processus (il est mort)"
        return $MOD_RC_FAIL
    fi
    if grep -qi 'rsl' "$BTS_LOG" 2>/dev/null; then
        mod_say "RSL mentionné dans $BTS_LOG (lien vers le BSC en cours d'établissement)"
    else
        mod_say "aucune mention de RSL dans $BTS_LOG pour l'instant — surveillez l'OML côté BSC"
    fi
    mod_ok
}

mod_bts_stop() {
    local pidf="${RUN_DIR:-/tmp/calypso}/bts.pid" pid _p
    pid="$(cat "$pidf" 2>/dev/null || echo 0)"
    [ "$pid" != 0 ] && kill "$pid" 2>/dev/null
    for _p in $(pgrep -x osmo-bts-trx 2>/dev/null); do
        _bts_est_le_notre "$_p" && kill "$_p" 2>/dev/null
    done
    rm -f "$pidf"
    return 0
}

run_modules/67-sidecar-trxcon.sh

.sh 44 lignes - 1,7KB

run_modules/67-sidecar-trxcon.sh
. "$(dirname "${BASH_SOURCE[0]}")/_lib/radio.sh"

MOD_REGISTER sidecar-trxcon "Couche 1 du MS#2 (trxcon)"
MOD_REQUIRED[sidecar-trxcon]=1
MOD_DEPS[sidecar-trxcon]="sidecar-faketrx"
MOD_TIMEOUT[sidecar-trxcon]=20

: "${SC_TRXCON_BIN:=trxcon}"
: "${SC_TRXCON_FN_ADVANCE:=100}"

mod_sidecar_trxcon_check() {
    command -v "$SC_TRXCON_BIN" >/dev/null 2>&1 || {
        mod_fail "binaire introuvable : $SC_TRXCON_BIN"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_trxcon_status() { radio_alive sidecar-trxcon && have_unix "$SC_L2_SOCK"; }

mod_sidecar_trxcon_start() {
    radio_dirs
    local log; log="$(radio_log sidecar-trxcon)"
    rm -f "$SC_L2_SOCK" 2>/dev/null
    mod_say "trxcon -p $SC_BB_PORT -s $SC_L2_SOCK (avance de trame $SC_TRXCON_FN_ADVANCE)"
    mod_say "journal : $log"
    setsid stdbuf -oL -eL "$SC_TRXCON_BIN" -i "$TRX_BIND_IP" -b "$TRX_BB_IP" \
        -p "$SC_BB_PORT" -s "$SC_L2_SOCK" -C 1 -F "$SC_TRXCON_FN_ADVANCE" >>"$log" 2>&1 </dev/null &
    radio_save_pid sidecar-trxcon $!
    mod_ok
}

mod_sidecar_trxcon_wait() {
    local log; log="$(radio_log sidecar-trxcon)"
    wait_until 5 "processus trxcon" radio_alive sidecar-trxcon || {
        modb_tail "$log" 20
        mod_fail "trxcon s'est arrêté aussitôt lancé"; return $MOD_RC_FAIL; }
    wait_until "${MOD_TIMEOUT[sidecar-trxcon]}" "socket L1CTL $SC_L2_SOCK" \
        have_unix "$SC_L2_SOCK" || {
        modb_tail "$log" 20
        mod_hint "fake_trx répond-il sur $SC_BB_PORT ? voir $(radio_log sidecar-faketrx)"
        mod_fail "socket L1CTL jamais créée"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_trxcon_stop() { radio_kill sidecar-trxcon; rm -f "$SC_L2_SOCK"; return 0; }

run_modules/68-sidecar-mobile.sh

.sh 53 lignes - 2,1KB

run_modules/68-sidecar-mobile.sh
. "$(dirname "${BASH_SOURCE[0]}")/_lib/radio.sh"

MOD_REGISTER sidecar-mobile "Second abonné (MS#2)"
MOD_REQUIRED[sidecar-mobile]=0
MOD_DEPS[sidecar-mobile]="sidecar-trxcon"
MOD_TIMEOUT[sidecar-mobile]=30

: "${SC_MOBILE_BIN:=mobile}"
: "${SC_MOBILE_DELAY:=3}"
: "${CALYPSO_PULSE_LATENCY_MSEC:=80}"

mod_sidecar_mobile_check() {
    command -v "$SC_MOBILE_BIN" >/dev/null 2>&1 || {
        mod_fail "binaire introuvable : $SC_MOBILE_BIN"; return $MOD_RC_FAIL; }
    [ -r "$SC_MOBILE_CFG" ] || {
        mod_hint "le module « sidecar-cfg » doit avoir posé ce fichier"
        mod_fail "configuration illisible : $SC_MOBILE_CFG"; return $MOD_RC_FAIL; }
    mod_ok
}

mod_sidecar_mobile_status() { radio_alive sidecar-mobile; }

mod_sidecar_mobile_start() {
    radio_dirs
    local log; log="$(radio_log sidecar-mobile)"
    wait_until 30 "socket L1CTL $SC_L2_SOCK" have_unix "$SC_L2_SOCK" || {
        mod_fail "socket L1CTL absente : $SC_L2_SOCK"; return $MOD_RC_FAIL; }
    if [ "${SC_MOBILE_DELAY}" != 0 ]; then
        mod_say "attente de ${SC_MOBILE_DELAY}s avant lancement"
        sleep "$SC_MOBILE_DELAY"
    fi
    mod_say "mobile -c $SC_MOBILE_CFG (VTY $SC_MOBILE_VTY_PORT)"
    mod_say "journal : $log"
    mod_say "latence : PULSE_LATENCY_MSEC=${CALYPSO_PULSE_LATENCY_MSEC:-<defaut greffon>}"
    mod_say "debug    : $CALYPSO_MOBILE_DEBUG"
    PULSE_LATENCY_MSEC="$CALYPSO_PULSE_LATENCY_MSEC" \
        setsid stdbuf -oL -eL "$SC_MOBILE_BIN" -c "$SC_MOBILE_CFG" \
        -d "$CALYPSO_MOBILE_DEBUG" >>"$log" 2>&1 </dev/null &
    radio_save_pid sidecar-mobile $!
    mod_ok
}

mod_sidecar_mobile_wait() {
    wait_until "${MOD_TIMEOUT[sidecar-mobile]}" "VTY du MS#2 ($SC_MOBILE_VTY_PORT)" \
        have_port "$SC_MOBILE_VTY_PORT" || {
        modb_tail "$(radio_log sidecar-mobile)" 20
        mod_hint "port déjà pris ? ss -tlnp | grep :$SC_MOBILE_VTY_PORT"
        mod_fail "VTY $SC_MOBILE_VTY_PORT jamais en écoute"; return $MOD_RC_FAIL; }
    mod_say "prêt — telnet 127.0.0.1 $SC_MOBILE_VTY_PORT puis « show ms »"
    mod_ok
}

mod_sidecar_mobile_stop() { radio_kill sidecar-mobile; return 0; }

run_modules/70-l2.sh

.sh 97 lignes - 4,0KB

run_modules/70-l2.sh
MOD_REGISTER l2 "Client de couche 2 (mobile, MS#1)"
MOD_REQUIRED[l2]=0
MOD_DEPS[l2]="osmocon"
MOD_TIMEOUT[l2]=30

: "${L1CTL_SOCK_PATH:=/tmp/osmocom_l2}"
: "${CALYPSO_MOBILE_DEBUG:=DCS:DNB:DPLMN:DRR:DMM:DSIM:DCC:DMNCC:DSS:DLSMS:DPAG:DSUM:DSAP:DGPS:DMOB:DPRIM:DLUA:DGAPK:DLLAPD}"
: "${CALYPSO_PULSE_LATENCY_MSEC:=80}"
: "${CALYPSO_L1_READY_MARKER:=your code is running now}"
: "${CALYPSO_L1_READY_TIMEOUT:=20}"

_l2_log() { printf '%s' "${LOG_DIR}/mobile.log"; }
_l2_cfg() {
    if [ -n "${MOBILE_CFG:-}" ]; then printf '%s' "$MOBILE_CFG"
    elif [ -r "${OSMOCOM_HOME:-$HOME/.osmocom}/bb/mobile_group1.cfg" ]; then
        printf '%s' "${OSMOCOM_HOME:-$HOME/.osmocom}/bb/mobile_group1.cfg"
    else printf '%s' "${QEMU_CFGS:-${QEMU_TREE}/cfgs}/mobile_group1.cfg"; fi
}
_l2_pat() { printf '%s' "mobile -c $(_l2_cfg)"; }

mod_l2_check() {
    command -v mobile >/dev/null 2>&1 || {
        mod_hint "compilez osmocom-bb (layer23) ou ajoutez son répertoire au PATH"
        mod_fail "client L2 introuvable dans le PATH : mobile"
        return $MOD_RC_FAIL; }
    local cfg; cfg="$(_l2_cfg)"
    [ -r "$cfg" ] || {
        mod_hint "le module mobile-cfg doit avoir copié cfgs/mobile_group1.cfg vers \$OSMOCOM_HOME/bb/"
        mod_fail "configuration du mobile illisible : $cfg"
        return $MOD_RC_FAIL; }
    if [ "${DRY:-0}" = 1 ]; then
        mod_say "simulation : attente de la socket L1CTL et du marqueur L1 non jouées"
        mod_ok; return $MOD_RC_OK
    fi
    wait_until "${MOD_TIMEOUT[l2]}" "socket L1CTL $L1CTL_SOCK_PATH" \
               have_unix "$L1CTL_SOCK_PATH" || {
        mod_hint "c'est osmocon qui crée cette socket (option -s) : regardez le journal du module osmocon"
        mod_fail "socket L1CTL absente : $L1CTL_SOCK_PATH"
        return $MOD_RC_FAIL; }
    if ! wait_until "$CALYPSO_L1_READY_TIMEOUT" "L1 prête" \
                    log_has "${LOG_DIR}/osmocon.log" "$CALYPSO_L1_READY_MARKER"; then
        mod_say "AVERTISSEMENT : marqueur « $CALYPSO_L1_READY_MARKER » absent d'osmocon.log — on lance quand même"
    fi
    mod_ok
}

mod_l2_status() {
    local pid; pid="$(cat "${RUN_DIR}/l2.pid" 2>/dev/null || echo 0)"
    if [ "$pid" != 0 ] && kill -0 "$pid" 2>/dev/null; then
        tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -qF "$(_l2_cfg)" && return 0
    fi
    have_proc "$(_l2_pat)"
}

mod_l2_start() {
    local log cfg
    log="$(_l2_log)"; cfg="$(_l2_cfg)"
    mkdir -p "$LOG_DIR" "$RUN_DIR" 2>/dev/null
    : > "$log"
    mod_say "journal  : $log"
    mod_say "cfg      : $cfg"
    mod_say "debug    : $CALYPSO_MOBILE_DEBUG"
    mod_say "latence  : PULSE_LATENCY_MSEC=$CALYPSO_PULSE_LATENCY_MSEC"
    PULSE_LATENCY_MSEC="$CALYPSO_PULSE_LATENCY_MSEC" \
        setsid mobile -c "$cfg" -d "$CALYPSO_MOBILE_DEBUG" >>"$log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "${RUN_DIR}/l2.pid"
    mod_ok
}

mod_l2_wait() {
    local pid log; pid="$(cat "${RUN_DIR}/l2.pid" 2>/dev/null || echo 0)"
    log="$(_l2_log)"
    wait_until "${MOD_TIMEOUT[l2]}" "première trace du mobile" test -s "$log" || {
        mod_hint "lisez $log ; « same layer2-socket » = un autre client tient déjà $L1CTL_SOCK_PATH"
        mod_fail "mobile n'a produit aucune trace"
        return $MOD_RC_FAIL; }
    kill -0 "$pid" 2>/dev/null || {
        mod_hint "lisez la fin de $log : la cause est presque toujours dans les 10 dernières lignes"
        mod_fail "mobile a démarré puis s'est arrêté"
        return $MOD_RC_FAIL; }
    if command -v ss >/dev/null 2>&1; then
        if ss -x 2>/dev/null | grep -q "$L1CTL_SOCK_PATH"; then
            mod_say "socket L1CTL $L1CTL_SOCK_PATH : connexion observée"
        else
            mod_say "socket L1CTL $L1CTL_SOCK_PATH : pas de connexion visible via ss (indice, pas verdict)"
        fi
    fi
    mod_ok
}

mod_l2_stop() {
    local pid; pid="$(cat "${RUN_DIR}/l2.pid" 2>/dev/null || echo 0)"
    [ "$pid" != 0 ] && kill "$pid" 2>/dev/null
    pkill -f "$(_l2_pat)" 2>/dev/null
    rm -f "${RUN_DIR}/l2.pid"
    return 0
}

run_modules/75-gsmtap.sh

.sh 63 lignes - 2,3KB

run_modules/75-gsmtap.sh
MOD_REGISTER gsmtap "Capture GSMTAP (pcap)"
MOD_REQUIRED[gsmtap]=0
MOD_DEPS[gsmtap]="l2"
MOD_TIMEOUT[gsmtap]=15

: "${PORT_GSMTAP:=4729}"
: "${CALYPSO_GSMTAP_IFACE:=any}"
: "${CAPTURE_DIR:=${RUN_DIR:-/tmp/calypso}/captures}"

_gsmtap_pat()  { printf '%s' "tcpdump .*udp port ${PORT_GSMTAP}"; }
_gsmtap_file() { cat "${RUN_DIR}/gsmtap.pcap.path" 2>/dev/null; }

mod_gsmtap_check() {
    command -v tcpdump >/dev/null 2>&1 || {
        mod_hint "installez-le vous-même : apt-get install tcpdump — ou lancez avec --skip gsmtap"
        mod_fail "tcpdump absent : aucune capture GSMTAP possible"
        return $MOD_RC_FAIL; }
    local d="$CAPTURE_DIR"
    [ -d "$d" ] || d="$(dirname "$CAPTURE_DIR")"
    [ -w "$d" ] || {
        mod_hint "posez CAPTURE_DIR vers un répertoire inscriptible"
        mod_fail "répertoire de capture non inscriptible : $d"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_gsmtap_status() { have_proc "$(_gsmtap_pat)"; }

mod_gsmtap_start() {
    local pcap log
    pcap="$CAPTURE_DIR/mobile-gsmtap-$(date +%Y%m%d_%H%M%S).pcap"
    log="${LOG_DIR}/gsmtap.log"
    mkdir -p "$LOG_DIR" "$RUN_DIR" "$CAPTURE_DIR" 2>/dev/null
    printf '%s\n' "$pcap" > "${RUN_DIR}/gsmtap.pcap.path"
    mod_say "capture  : $pcap"
    mod_say "filtre   : udp port $PORT_GSMTAP sur $CALYPSO_GSMTAP_IFACE"
    setsid tcpdump -i "$CALYPSO_GSMTAP_IFACE" -U \
            -w "$pcap" "udp port $PORT_GSMTAP" >>"$log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "${RUN_DIR}/gsmtap.pid"
    mod_ok
}

mod_gsmtap_wait() {
    local pid pcap; pid="$(cat "${RUN_DIR}/gsmtap.pid" 2>/dev/null || echo 0)"
    pcap="$(_gsmtap_file)"
    wait_until "${MOD_TIMEOUT[gsmtap]}" "création du pcap" test -s "$pcap" || {
        mod_hint "lisez ${LOG_DIR}/gsmtap.log ; « permission denied » = capture sans droits (CAP_NET_RAW)"
        mod_fail "tcpdump n'a pas créé $pcap"
        return $MOD_RC_FAIL; }
    kill -0 "$pid" 2>/dev/null || {
        mod_fail "tcpdump a démarré puis s'est arrêté"
        mod_hint "lisez ${LOG_DIR}/gsmtap.log"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_gsmtap_stop() {
    local pid; pid="$(cat "${RUN_DIR}/gsmtap.pid" 2>/dev/null || echo 0)"
    [ "$pid" != 0 ] && kill "$pid" 2>/dev/null
    pkill -f "$(_gsmtap_pat)" 2>/dev/null
    rm -f "${RUN_DIR}/gsmtap.pid"
    return 0
}

run_modules/80-panes.sh

.sh 67 lignes - 2,6KB

run_modules/80-panes.sh
MOD_REGISTER panes "Vue de supervision (tmux)"
MOD_REQUIRED[panes]=0
MOD_DEPS[panes]="tmux"
MOD_TIMEOUT[panes]=15

: "${TMUX_SESSION:=calypso}"
: "${CALYPSO_GSM_SNIFF:=${QEMU_TREE}/opt-gsm-scripts/gsm_sniff.py}"

_panes_count() { tmux list-panes -t "$TMUX_SESSION:all" 2>/dev/null | wc -l; }
_panes_enough() { [ "$(_panes_count)" -ge 2 ]; }

mod_panes_check() {
    command -v tmux >/dev/null 2>&1 || { mod_skip "tmux absent — pas de vue de supervision"; return $MOD_RC_SKIP; }
    if [ "${DRY:-0}" = 1 ]; then
        mod_say "simulation : existence de la session tmux non vérifiée"
        mod_ok; return $MOD_RC_OK
    fi
    tmux has-session -t "$TMUX_SESSION" 2>/dev/null || {
        mod_hint "le module tmux crée la session ; ./run.sh --only tmux"
        mod_fail "session tmux « $TMUX_SESSION » inexistante"
        return $MOD_RC_FAIL; }
    mod_ok
}

mod_panes_status() { tmux list-windows -t "$TMUX_SESSION" 2>/dev/null | grep -q '^[0-9]*: all'; }

mod_panes_start() {
    tmux new-window -t "$TMUX_SESSION" -n all \
        "clear; echo '=== osmocon ==='; tail -F ${LOG_DIR}/osmocon.log" || {
        mod_fail "tmux a refusé de créer la fenêtre « all »"
        return $MOD_RC_FAIL; }
    local specs=() spec name what cmd added=0
    specs+=("qemu|${LOG_DIR}/qemu.log")
    [ -r "$CALYPSO_GSM_SNIFF" ] && specs+=("burst|__BURST__")
    specs+=("mobile|${LOG_DIR}/mobile.log")
    specs+=("pont|/dev/shm/pont.log")
    for spec in "${specs[@]}"; do
        name="${spec%%|*}"; what="${spec##*|}"
        case "$what" in
            __BURST__)
                cmd="clear; echo '=== BURST decode (4731 + 5700-5702) ==='; python3 -u $CALYPSO_GSM_SNIFF burst" ;;
            *)  cmd="clear; echo '=== $name ==='; tail -F $what" ;;
        esac
        if tmux split-window -t "$TMUX_SESSION:all" "$cmd" 2>/dev/null; then
            added=$((added + 1))
        else
            mod_say "pane « $name » refusé par tmux (place insuffisante)"
        fi
        tmux select-layout -t "$TMUX_SESSION:all" tiled >/dev/null 2>&1
    done
    mod_say "fenêtre « all » : $((added + 1)) panes demandés"
    mod_ok
}

mod_panes_wait() {
    wait_until "${MOD_TIMEOUT[panes]}" "panes de la fenêtre « all »" _panes_enough || {
        mod_hint "tmux list-panes -t $TMUX_SESSION:all pour voir ce qui a survécu"
        mod_fail "la fenêtre « all » n'a pas gardé ses panes"
        return $MOD_RC_FAIL; }
    mod_say "panes vivants : $(_panes_count)"
    mod_ok
}

mod_panes_stop() {
    tmux kill-window -t "$TMUX_SESSION:all" 2>/dev/null
    return 0
}

run_modules/95-summary.sh

.sh 25 lignes - 1,3KB

run_modules/95-summary.sh
MOD_REGISTER summary "Identité du build et résumé"
MOD_REQUIRED[summary]=0
MOD_PURE[summary]=1
MOD_TIMEOUT[summary]=10

mod_summary_check() { mod_ok; }
mod_summary_status() { return $MOD_RC_FAIL; }

mod_summary_start() {
    mod_say "===== IDENTITÉ DU BUILD ====="
    if command -v git >/dev/null 2>&1 && [ -d "${QEMU_TREE}/.git" ]; then
        mod_say "  révision git    : $(git -C "$QEMU_TREE" rev-parse --short HEAD 2>/dev/null || echo inconnue)"
        mod_say "  arbre modifié   : $(git -C "$QEMU_TREE" diff --stat 2>/dev/null | tail -1)"
    fi
    mod_say "  binaire QEMU    : ${QEMU_BIN:-<non défini>}"
    mod_say "  date du binaire : $(stat -c %y "${QEMU_BIN:-/nonexistent}" 2>/dev/null | cut -d. -f1)"
    mod_say "  firmware        : ${FIRMWARE_ELF:-<non défini>}"
    mod_say "===== RÉSUMÉ D'ENVIRONNEMENT ====="
    mod_say "  couche 1        : gr-gsm (GSMTAP ${PORT_GSMTAP_SI:-4730}, SCH ${PORT_GSMTAP_SCH:-4731}) · pont TRXD ${PORT_TRXD_CLOCK:-5700}-${PORT_TRXD_DATA:-5702}"
    mod_say "  cellule du pont : ARFCN ${PONT_ARFCN:-?} BSIC ${PONT_BSIC:-?}"
    mod_say "  chiffrement     : ${ENCRYPTION:-?}"
    mod_say "  MS#1 / MS#2     : ${MOBILE_CFG:-?} / ${SC_MOBILE_CFG:-?}"
    mod_say "  LOG_DIR / RUN_DIR : ${LOG_DIR:-} / ${RUN_DIR:-}"
    mod_ok
}

run_modules/99-attach.sh

.sh 50 lignes - 1,9KB

run_modules/99-attach.sh
MOD_REGISTER attach "Reprise en main de la session tmux"
MOD_REQUIRED[attach]=0
MOD_DEPS[attach]="tmux"
MOD_TIMEOUT[attach]=10
MOD_ENABLED_IF[attach]='[ "${CALYPSO_NO_ATTACH:-0}" != 1 ] && [ -t 1 ]'

: "${TMUX_SESSION:=calypso}"
: "${CALYPSO_DEFAULT_WINDOW:=all}"

_attach_current_window() {
    tmux display-message -p -t "$TMUX_SESSION" '#W' 2>/dev/null
}

mod_attach_check() {
    command -v tmux >/dev/null 2>&1 || { mod_skip "tmux absent — rien à sélectionner"; return $MOD_RC_SKIP; }
    tmux has-session -t "$TMUX_SESSION" 2>/dev/null || {
        mod_hint "le module tmux (35) crée la session ; sans elle la pile tourne quand même, en arrière-plan"
        mod_skip "session tmux « $TMUX_SESSION » inexistante"
        return $MOD_RC_SKIP; }
    mod_ok
}

mod_attach_status() { [ "$(_attach_current_window)" = "$CALYPSO_DEFAULT_WINDOW" ]; }

mod_attach_start() {
    mkdir -p "$RUN_DIR" 2>/dev/null
    tmux select-window -t "$TMUX_SESSION:$CALYPSO_DEFAULT_WINDOW" 2>/dev/null \
        || tmux select-window -t "$TMUX_SESSION:qemu" 2>/dev/null \
        || mod_say "aucune des fenêtres « $CALYPSO_DEFAULT_WINDOW » / « qemu » n'existe"

    printf 'tmux attach -t %s\n' "$TMUX_SESSION" > "${RUN_DIR}/attach.cmd"
    mod_say "reprise en main : tmux attach -t $TMUX_SESSION"
    mod_say "fin de session  : tmux kill-session -t $TMUX_SESSION"
    mod_ok
}

mod_attach_wait() {
    wait_until "${MOD_TIMEOUT[attach]}" "session tmux « $TMUX_SESSION »" \
               tmux has-session -t "$TMUX_SESSION" || {
        mod_fail "la session tmux a disparu"
        return $MOD_RC_FAIL; }
    local w; w="$(_attach_current_window)"
    [ -n "$w" ] || { mod_fail "aucune fenêtre sélectionnable dans « $TMUX_SESSION »"
                     mod_hint "tmux list-windows -t $TMUX_SESSION"
                     return $MOD_RC_FAIL; }
    mod_say "fenêtre courante : $w"
    mod_ok
}

mod_attach_stop() { return 0; }

run_modules/_lib/core.sh

.sh 131 lignes - 4,5KB

run_modules/_lib/core.sh
[ -n "${CORE_LIB_LOADED:-}" ] && return 0
CORE_LIB_LOADED=1

: "${OSMOCOM_CFG:=/etc/osmocom}"
: "${RUN_DIR:=/tmp/calypso}"
: "${LOG_DIR:=$RUN_DIR/logs}"
: "${CORE_VTY_HOST:=127.0.0.1}"

core_bin() {
    local n="$1" p
    for p in "/usr/bin/$n" "/usr/local/bin/$n"; do
        [ -x "$p" ] && { printf '%s\n' "$p"; return 0; }
    done
    p="$(command -v "$n" 2>/dev/null)" || return 1
    [ -n "$p" ] && { printf '%s\n' "$p"; return 0; }
    return 1
}

core_cfg() { printf '%s/%s.cfg\n' "$OSMOCOM_CFG" "$1"; }

core_cfg_field() {
    local f="$1" re="$2" n="$3" def="${4:-}" v=""
    if [ -r "$f" ]; then
        v="$(grep -aE "$re" "$f" 2>/dev/null | head -n1 | awk -v k="$n" '{print $k}')"
    fi
    printf '%s\n' "${v:-$def}"
}

core_has_systemd() { [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; }
core_unit_exists() { core_has_systemd && systemctl cat "$1" >/dev/null 2>&1; }
core_unit_active() { core_has_systemd && systemctl is-active --quiet "$1"; }
core_nrestarts()   { core_has_systemd && systemctl show -p NRestarts --value "$1" 2>/dev/null || echo 0; }
core_pidfile()     { printf '%s/%s.pid\n' "$RUN_DIR" "$1"; }

core_svc_start() {
    local unit="$1" bin="$2"; shift 2
    mkdir -p "$RUN_DIR" "$LOG_DIR" 2>/dev/null || true
    core_nrestarts "$unit" > "$RUN_DIR/$unit.nrestarts" 2>/dev/null || true
    if core_unit_exists "$unit"; then
        mod_say "systemctl start $unit"
        systemctl start "$unit" || return 1
        return 0
    fi
    mod_say "systemd indisponible — lancement direct : $bin $*"
    setsid "$bin" "$@" >>"$LOG_DIR/$unit.log" 2>&1 </dev/null &
    printf '%s\n' "$!" > "$(core_pidfile "$unit")"
    return 0
}

core_svc_stop() {
    local unit="$1" pat="${2:-}" pf
    core_unit_exists "$unit" && systemctl stop "$unit" 2>/dev/null
    pf="$(core_pidfile "$unit")"
    if [ -f "$pf" ]; then
        kill "$(cat "$pf" 2>/dev/null)" 2>/dev/null
        rm -f "$pf"
    fi
    [ -n "$pat" ] && pkill -f "$pat" 2>/dev/null
    rm -f "$RUN_DIR/$unit.nrestarts" 2>/dev/null
    return 0
}

core_alive() {
    local unit="$1" pf
    if core_unit_exists "$unit"; then core_unit_active "$unit"; return $?; fi
    pf="$(core_pidfile "$unit")"
    [ -f "$pf" ] && kill -0 "$(cat "$pf" 2>/dev/null)" 2>/dev/null
}

core_restarted_since() {
    local unit="$1"
    local f="$RUN_DIR/$unit.nrestarts"
    local before now
    [ -f "$f" ] || return 1
    before="$(cat "$f" 2>/dev/null || echo 0)"
    now="$(core_nrestarts "$unit")"
    [ "${now:-0}" -gt "${before:-0}" ] 2>/dev/null
}

core_healthy() { core_alive "$1" && ! core_restarted_since "$1"; }

_core_listen() {
    local opts="$1" port="$2" ip="${3:-}"
    ss -H $opts 2>/dev/null | awk -v want="$port" -v wip="$ip" '
        { a = $4
          if (a == "") next
          if (match(a, /:[0-9]+$/) == 0) next
          p = substr(a, RSTART + 1); h = substr(a, 1, RSTART - 1)
          if (p != want) next
          if (wip == "" || h == wip || h == "0.0.0.0" || h == "*" || h == "[::]" || h == "::") { f = 1; exit }
        }
        END { exit !f }'
}
core_tcp_listen()  { _core_listen "-ltn"      "$@"; }
core_udp_listen()  { _core_listen "-lun"      "$@"; }
core_sctp_listen() { _core_listen "-ln --sctp" "$@"; }
core_vty_listen()  { core_tcp_listen "$1" "$CORE_VTY_HOST"; }

core_sctp_estab() {
    ss -nH --sctp 2>/dev/null | awk -v want="$1" '
        $1 == "ESTAB" { a = $4
            if (match(a, /:[0-9]+$/)) { p = substr(a, RSTART + 1); if (p == want) { f = 1; exit } } }
        END { exit !f }'
}

core_unix_listen() {
    local p="$1"
    ss -xlH 2>/dev/null | awk -v w="$p" '{ for (i = 1; i <= NF; i++) if ($i == w) { f = 1; exit } } END { exit !f }' \
        || [ -S "$p" ]
}

core_ip_local() {
    local w="$1"
    case "$w" in ""|0.0.0.0|127.*) return 0 ;; esac
    ip -o -4 addr show 2>/dev/null | awk -v w="$w" '{ split($4, a, "/"); if (a[1] == w) { f = 1; exit } } END { exit !f }'
}

core_vty_ask() {
    local port="$1"; shift
    local body="" c t="${CORE_VTY_TIMEOUT:-10}"
    for c in "$@"; do body+="$c"$'\n'; done
    if command -v socat >/dev/null 2>&1; then
        { sleep "${CORE_VTY_OPEN:-1}"; printf '%s' "$body"; sleep "${CORE_VTY_READ:-2}"; } \
            | timeout "$t" socat -T"$t" STDIO "TCP:${CORE_VTY_HOST}:${port},crlf" 2>/dev/null
    elif command -v telnet >/dev/null 2>&1; then
        { sleep "${CORE_VTY_OPEN:-1}"; printf '%s' "$body"; sleep "${CORE_VTY_READ:-2}"; } \
            | timeout "$t" telnet "$CORE_VTY_HOST" "$port" 2>/dev/null
    else
        return 1
    fi
}

run_modules/_lib/i18n.sh

.sh 630 lignes - 28KB

run_modules/_lib/i18n.sh
: "${CALYPSO_LANG:=en}"

declare -gA _T _TM

_T[fr:plan]="Plan — %d modules"
_T[en:plan]="Plan — %d modules"
_T[es:plan]="Plan — %d módulos"
_T[pt:plan]="Plano — %d módulos"
_T[it:plan]="Piano — %d moduli"
_T[hi:plan]="योजना — %d मॉड्यूल"
_T[zh:plan]="方案 — %d 个模块"
_T[ar:plan]="الخطة — %d وحدة"
_T[fr:ok]="OK"
_T[en:ok]="OK"
_T[es:ok]="OK"
_T[pt:ok]="OK"
_T[it:ok]="OK"
_T[hi:ok]="ठीक"
_T[zh:ok]="完成"
_T[ar:ok]="تم"
_T[fr:fail]="ÉCHC"
_T[en:fail]="FAIL"
_T[es:fail]="FALL"
_T[pt:fail]="FALH"
_T[it:fail]="ERRO"
_T[hi:fail]="विफल"
_T[zh:fail]="失败"
_T[ar:fail]="فشل"
_T[fr:skip]="SAUT"
_T[en:skip]="SKIP"
_T[es:skip]="OMIT"
_T[pt:skip]="OMIT"
_T[it:skip]="SALT"
_T[hi:skip]="छोड़ा"
_T[zh:skip]="跳过"
_T[ar:skip]="تخطٍ"
_T[fr:dry]="SIMU"
_T[en:dry]="DRY "
_T[es:dry]="SIMU"
_T[pt:dry]="SIMU"
_T[it:dry]="SIMU"
_T[hi:dry]="सिमु"
_T[zh:dry]="模拟"
_T[ar:dry]="محاكاة"
_T[fr:simule]="simulé"
_T[en:simule]="simulated"
_T[es:simule]="simulado"
_T[pt:simule]="simulado"
_T[it:simule]="simulato"
_T[hi:simule]="सिमुलेटेड"
_T[zh:simule]="已模拟"
_T[ar:simule]="محاكى"
_T[fr:deja]="déjà démarré"
_T[en:deja]="already running"
_T[es:deja]="ya iniciado"
_T[pt:deja]="já iniciado"
_T[it:deja]="già avviato"
_T[hi:deja]="पहले से चालू"
_T[zh:deja]="已在运行"
_T[ar:deja]="يعمل بالفعل"
_T[fr:desactive]="désactivé par %s"
_T[en:desactive]="disabled by %s"
_T[es:desactive]="desactivado por %s"
_T[pt:desactive]="desativado por %s"
_T[it:desactive]="disattivato da %s"
_T[hi:desactive]="%s द्वारा निष्क्रिय"
_T[zh:desactive]="由 %s 停用"
_T[ar:desactive]="معطّل بواسطة %s"
_T[fr:interrompu]="séquence interrompue (module obligatoire)"
_T[en:interrompu]="sequence aborted (required module)"
_T[es:interrompu]="secuencia interrumpida (módulo obligatorio)"
_T[pt:interrompu]="sequência interrompida (módulo obrigatório)"
_T[it:interrompu]="sequenza interrotta (modulo obbligatorio)"
_T[hi:interrompu]="अनुक्रम रुका (अनिवार्य मॉड्यूल)"
_T[zh:interrompu]="流程中止(必需模块)"
_T[ar:interrompu]="توقف التسلسل (وحدة إلزامية)"
_T[fr:journal]="journal : %s"
_T[en:journal]="log : %s"
_T[es:journal]="registro : %s"
_T[pt:journal]="registo : %s"
_T[it:journal]="registro : %s"
_T[hi:journal]="लॉग : %s"
_T[zh:journal]="日志 : %s"
_T[ar:journal]="السجل : %s"
_T[fr:pas_pret]="démarré mais jamais prêt : %s"
_T[en:pas_pret]="started but never ready: %s"
_T[es:pas_pret]="iniciado pero nunca listo: %s"
_T[pt:pas_pret]="iniciado mas nunca pronto: %s"
_T[it:pas_pret]="avviato ma mai pronto: %s"
_T[hi:pas_pret]="चालू हुआ पर तैयार नहीं: %s"
_T[zh:pas_pret]="已启动但始终未就绪:%s"
_T[ar:pas_pret]="بدأ لكنه لم يجهز: %s"
_T[fr:sans_raison]="raison non fournie par le module"
_T[en:sans_raison]="no reason given by the module"
_T[es:sans_raison]="el módulo no indicó la causa"
_T[pt:sans_raison]="o módulo não indicou a causa"
_T[it:sans_raison]="il modulo non ha indicato la causa"
_T[hi:sans_raison]="मॉड्यूल ने कारण नहीं दिया"
_T[zh:sans_raison]="模块未给出原因"
_T[ar:sans_raison]="الوحدة لم تذكر السبب"
_T[fr:prereq_sim]="prérequis non vérifiable en simulation : %s"
_T[en:prereq_sim]="prerequisite not checkable in simulation: %s"
_T[es:prereq_sim]="requisito no verificable en simulación: %s"
_T[pt:prereq_sim]="requisito não verificável em simulação: %s"
_T[it:prereq_sim]="prerequisito non verificabile in simulazione: %s"
_T[hi:prereq_sim]="सिमुलेशन में पूर्वापेक्षा जाँच योग्य नहीं: %s"
_T[zh:prereq_sim]="模拟模式下无法校验前置条件:%s"
_T[ar:prereq_sim]="لا يمكن التحقق من المتطلب في المحاكاة: %s"
_T[fr:bilan]="%d ok · %d ignorés · %d échecs"
_T[en:bilan]="%d ok · %d skipped · %d failed"
_T[es:bilan]="%d ok · %d omitidos · %d fallos"
_T[pt:bilan]="%d ok · %d omitidos · %d falhas"
_T[it:bilan]="%d ok · %d saltati · %d errori"
_T[hi:bilan]="%d ठीक · %d छोड़े · %d विफल"
_T[zh:bilan]="%d 完成 · %d 跳过 · %d 失败"
_T[ar:bilan]="%d تم · %d تخطٍ · %d فشل"
_T[fr:opt_inconnue]="option inconnue : %s"
_T[en:opt_inconnue]="unknown option: %s"
_T[es:opt_inconnue]="opción desconocida: %s"
_T[pt:opt_inconnue]="opção desconhecida: %s"
_T[it:opt_inconnue]="opzione sconosciuta: %s"
_T[hi:opt_inconnue]="अज्ञात विकल्प: %s"
_T[zh:opt_inconnue]="未知选项:%s"
_T[ar:opt_inconnue]="خيار غير معروف: %s"

_TM[fr:abonnes-hlr]="Cœur — abonnés provisionnés dans le HLR"
_TM[en:abonnes-hlr]="Core — subscribers provisioned in the HLR"
_TM[es:abonnes-hlr]="Núcleo — abonados aprovisionados en el HLR"
_TM[pt:abonnes-hlr]="Núcleo — assinantes provisionados no HLR"
_TM[it:abonnes-hlr]="Core — abbonati registrati nell'HLR"
_TM[hi:abonnes-hlr]="कोर — HLR में पंजीकृत ग्राहक"
_TM[zh:abonnes-hlr]="核心 — HLR 中已开通的用户"
_TM[ar:abonnes-hlr]="النواة — المشتركون المسجلون في HLR"
_TM[fr:apn0]="Cœur — interface TUN de l'APN"
_TM[en:apn0]="Core — APN TUN interface"
_TM[es:apn0]="Núcleo — interfaz TUN del APN"
_TM[pt:apn0]="Núcleo — interface TUN do APN"
_TM[it:apn0]="Core — interfaccia TUN dell'APN"
_TM[hi:apn0]="कोर — APN का TUN इंटरफ़ेस"
_TM[zh:apn0]="核心 — APN 的 TUN 接口"
_TM[ar:apn0]="النواة — واجهة TUN لنقطة APN"
_TM[fr:asterisk]="Cœur — Asterisk (PBX SIP)"
_TM[en:asterisk]="Core — Asterisk (SIP PBX)"
_TM[es:asterisk]="Núcleo — Asterisk (PBX SIP)"
_TM[pt:asterisk]="Núcleo — Asterisk (PBX SIP)"
_TM[it:asterisk]="Core — Asterisk (PBX SIP)"
_TM[hi:asterisk]="कोर — Asterisk (SIP PBX)"
_TM[zh:asterisk]="核心 — Asterisk(SIP 交换机)"
_TM[ar:asterisk]="النواة — Asterisk (مقسم SIP)"
_TM[fr:attach]="Reprise en main de la session tmux"
_TM[en:attach]="Attaching to the tmux session"
_TM[es:attach]="Reconexión a la sesión tmux"
_TM[pt:attach]="Reconexão à sessão tmux"
_TM[it:attach]="Ripresa della sessione tmux"
_TM[hi:attach]="tmux सत्र से पुनः जुड़ाव"
_TM[zh:attach]="接入 tmux 会话"
_TM[ar:attach]="الاتصال بجلسة tmux"
_TM[fr:audio]="Pont audio (PulseAudio + GAPK)"
_TM[en:audio]="Audio bridge (PulseAudio + GAPK)"
_TM[es:audio]="Puente de audio (PulseAudio + GAPK)"
_TM[pt:audio]="Ponte de áudio (PulseAudio + GAPK)"
_TM[it:audio]="Ponte audio (PulseAudio + GAPK)"
_TM[hi:audio]="ऑडियो सेतु (PulseAudio + GAPK)"
_TM[zh:audio]="音频桥(PulseAudio + GAPK)"
_TM[ar:audio]="جسر الصوت (PulseAudio + GAPK)"
_TM[fr:bsc]="Cœur — OsmoBSC (contrôleur BTS)"
_TM[en:bsc]="Core — OsmoBSC (BTS controller)"
_TM[es:bsc]="Núcleo — OsmoBSC (controlador BTS)"
_TM[pt:bsc]="Núcleo — OsmoBSC (controlador BTS)"
_TM[it:bsc]="Core — OsmoBSC (controllore BTS)"
_TM[hi:bsc]="कोर — OsmoBSC (BTS नियंत्रक)"
_TM[zh:bsc]="核心 — OsmoBSC(基站控制器)"
_TM[ar:bsc]="النواة — OsmoBSC (متحكم BTS)"
_TM[fr:bts]="Station de base osmo-bts-trx"
_TM[en:bts]="Base station osmo-bts-trx"
_TM[es:bts]="Estación base osmo-bts-trx"
_TM[pt:bts]="Estação base osmo-bts-trx"
_TM[it:bts]="Stazione base osmo-bts-trx"
_TM[hi:bts]="बेस स्टेशन osmo-bts-trx"
_TM[zh:bts]="基站 osmo-bts-trx"
_TM[ar:bts]="المحطة القاعدية osmo-bts-trx"
_TM[fr:config]="Résolution des chemins et des ports"
_TM[en:config]="Resolving paths and ports"
_TM[es:config]="Resolución de rutas y puertos"
_TM[pt:config]="Resolução de caminhos e portas"
_TM[it:config]="Risoluzione di percorsi e porte"
_TM[hi:config]="पथ और पोर्ट का निर्धारण"
_TM[zh:config]="解析路径与端口"
_TM[ar:config]="تحديد المسارات والمنافذ"
_TM[fr:gabarits]="Gabarits de configuration du cœur"
_TM[en:gabarits]="Core configuration templates"
_TM[es:gabarits]="Plantillas de configuración del núcleo"
_TM[pt:gabarits]="Modelos de configuração do núcleo"
_TM[it:gabarits]="Modelli di configurazione del core"
_TM[hi:gabarits]="कोर विन्यास टेम्पलेट"
_TM[zh:gabarits]="核心配置模板"
_TM[ar:gabarits]="قوالب إعداد النواة"
_TM[fr:ggsn]="Cœur — OsmoGGSN (data GTP)"
_TM[en:ggsn]="Core — OsmoGGSN (GTP data)"
_TM[es:ggsn]="Núcleo — OsmoGGSN (datos GTP)"
_TM[pt:ggsn]="Núcleo — OsmoGGSN (dados GTP)"
_TM[it:ggsn]="Core — OsmoGGSN (dati GTP)"
_TM[hi:ggsn]="कोर — OsmoGGSN (GTP डेटा)"
_TM[zh:ggsn]="核心 — OsmoGGSN(GTP 数据)"
_TM[ar:ggsn]="النواة — OsmoGGSN (بيانات GTP)"
_TM[fr:gsmtap]="Capture GSMTAP (pcap)"
_TM[en:gsmtap]="GSMTAP capture (pcap)"
_TM[es:gsmtap]="Captura GSMTAP (pcap)"
_TM[pt:gsmtap]="Captura GSMTAP (pcap)"
_TM[it:gsmtap]="Cattura GSMTAP (pcap)"
_TM[hi:gsmtap]="GSMTAP कैप्चर (pcap)"
_TM[zh:gsmtap]="GSMTAP 抓包(pcap)"
_TM[ar:gsmtap]="التقاط GSMTAP (pcap)"
_TM[fr:hlr]="Cœur — OsmoHLR (registre GSUP)"
_TM[en:hlr]="Core — OsmoHLR (GSUP registry)"
_TM[es:hlr]="Núcleo — OsmoHLR (registro GSUP)"
_TM[pt:hlr]="Núcleo — OsmoHLR (registo GSUP)"
_TM[it:hlr]="Core — OsmoHLR (registro GSUP)"
_TM[hi:hlr]="कोर — OsmoHLR (GSUP रजिस्टर)"
_TM[zh:hlr]="核心 — OsmoHLR(GSUP 注册库)"
_TM[ar:hlr]="النواة — OsmoHLR (سجل GSUP)"
_TM[fr:l2]="Client de couche 2"
_TM[en:l2]="Layer 2 client"
_TM[es:l2]="Cliente de capa 2"
_TM[pt:l2]="Cliente de camada 2"
_TM[it:l2]="Client di livello 2"
_TM[hi:l2]="लेयर 2 क्लाइंट"
_TM[zh:l2]="第二层客户端"
_TM[ar:l2]="عميل الطبقة الثانية"
_TM[fr:logs]="Journaux et espace de travail"
_TM[en:logs]="Logs and working area"
_TM[es:logs]="Registros y espacio de trabajo"
_TM[pt:logs]="Registos e área de trabalho"
_TM[it:logs]="Registri e area di lavoro"
_TM[hi:logs]="लॉग और कार्यक्षेत्र"
_TM[zh:logs]="日志与工作区"
_TM[ar:logs]="السجلات ومساحة العمل"
_TM[fr:mgw]="Cœur — OsmoMGW (média MGCP/RTP)"
_TM[en:mgw]="Core — OsmoMGW (MGCP/RTP media)"
_TM[es:mgw]="Núcleo — OsmoMGW (medios MGCP/RTP)"
_TM[pt:mgw]="Núcleo — OsmoMGW (média MGCP/RTP)"
_TM[it:mgw]="Core — OsmoMGW (media MGCP/RTP)"
_TM[hi:mgw]="कोर — OsmoMGW (MGCP/RTP मीडिया)"
_TM[zh:mgw]="核心 — OsmoMGW(MGCP/RTP 媒体)"
_TM[ar:mgw]="النواة — OsmoMGW (وسائط MGCP/RTP)"
_TM[fr:mobile-cfg]="Configuration du mobile"
_TM[en:mobile-cfg]="Mobile configuration"
_TM[es:mobile-cfg]="Configuración del móvil"
_TM[pt:mobile-cfg]="Configuração do móvel"
_TM[it:mobile-cfg]="Configurazione del mobile"
_TM[hi:mobile-cfg]="मोबाइल विन्यास"
_TM[zh:mobile-cfg]="手机端配置"
_TM[ar:mobile-cfg]="إعداد الهاتف"
_TM[fr:msc]="Cœur — OsmoMSC (commutation CS)"
_TM[en:msc]="Core — OsmoMSC (CS switching)"
_TM[es:msc]="Núcleo — OsmoMSC (conmutación CS)"
_TM[pt:msc]="Núcleo — OsmoMSC (comutação CS)"
_TM[it:msc]="Core — OsmoMSC (commutazione CS)"
_TM[hi:msc]="कोर — OsmoMSC (CS स्विचिंग)"
_TM[zh:msc]="核心 — OsmoMSC(电路域交换)"
_TM[ar:msc]="النواة — OsmoMSC (تبديل الدارات)"
_TM[fr:osmocon]="Passerelle osmocon"
_TM[en:osmocon]="osmocon gateway"
_TM[es:osmocon]="Pasarela osmocon"
_TM[pt:osmocon]="Gateway osmocon"
_TM[it:osmocon]="Gateway osmocon"
_TM[hi:osmocon]="osmocon गेटवे"
_TM[zh:osmocon]="osmocon 网关"
_TM[ar:osmocon]="بوابة osmocon"
_TM[fr:outils]="Outillage de l'hôte"
_TM[en:outils]="Host tooling"
_TM[es:outils]="Herramientas del anfitrión"
_TM[pt:outils]="Ferramentas do anfitrião"
_TM[it:outils]="Strumenti dell'host"
_TM[hi:outils]="होस्ट उपकरण"
_TM[zh:outils]="主机工具"
_TM[ar:outils]="أدوات المضيف"
_TM[fr:panes]="Vue de supervision (tmux)"
_TM[en:panes]="Supervision view (tmux)"
_TM[es:panes]="Vista de supervisión (tmux)"
_TM[pt:panes]="Vista de supervisão (tmux)"
_TM[it:panes]="Vista di supervisione (tmux)"
_TM[hi:panes]="निगरानी दृश्य (tmux)"
_TM[zh:panes]="监控视图(tmux)"
_TM[ar:panes]="عرض المراقبة (tmux)"
_TM[fr:pcu]="Cœur — OsmoPCU (ordonnanceur paquet)"
_TM[en:pcu]="Core — OsmoPCU (packet scheduler)"
_TM[es:pcu]="Núcleo — OsmoPCU (planificador de paquetes)"
_TM[pt:pcu]="Núcleo — OsmoPCU (escalonador de pacotes)"
_TM[it:pcu]="Core — OsmoPCU (scheduler pacchetti)"
_TM[hi:pcu]="कोर — OsmoPCU (पैकेट अनुसूचक)"
_TM[zh:pcu]="核心 — OsmoPCU(分组调度器)"
_TM[ar:pcu]="النواة — OsmoPCU (مجدول الحزم)"
_TM[fr:prereqs]="Vérification des prérequis"
_TM[en:prereqs]="Prerequisite checks"
_TM[es:prereqs]="Comprobación de requisitos"
_TM[pt:prereqs]="Verificação de requisitos"
_TM[it:prereqs]="Verifica dei prerequisiti"
_TM[hi:prereqs]="पूर्वापेक्षा जाँच"
_TM[zh:prereqs]="前置条件检查"
_TM[ar:prereqs]="التحقق من المتطلبات"
_TM[fr:pty]="PTY série de l'émulateur"
_TM[en:pty]="Emulator serial PTY"
_TM[es:pty]="PTY serie del emulador"
_TM[pt:pty]="PTY série do emulador"
_TM[it:pty]="PTY seriale dell'emulatore"
_TM[hi:pty]="एमुलेटर का सीरियल PTY"
_TM[zh:pty]="仿真器串口 PTY"
_TM[ar:pty]="منفذ PTY التسلسلي للمحاكي"
_TM[fr:qemu]="Émulateur Calypso (QEMU)"
_TM[en:qemu]="Calypso emulator (QEMU)"
_TM[es:qemu]="Emulador Calypso (QEMU)"
_TM[pt:qemu]="Emulador Calypso (QEMU)"
_TM[it:qemu]="Emulatore Calypso (QEMU)"
_TM[hi:qemu]="Calypso एमुलेटर (QEMU)"
_TM[zh:qemu]="Calypso 仿真器(QEMU)"
_TM[ar:qemu]="محاكي Calypso (QEMU)"
_TM[fr:reseau]="Interfaces et plan d'adressage"
_TM[en:reseau]="Interfaces and addressing plan"
_TM[es:reseau]="Interfaces y plan de direccionamiento"
_TM[pt:reseau]="Interfaces e plano de endereçamento"
_TM[it:reseau]="Interfacce e piano di indirizzamento"
_TM[hi:reseau]="इंटरफ़ेस और पता-योजना"
_TM[zh:reseau]="接口与编址方案"
_TM[ar:reseau]="الواجهات وخطة العنونة"
_TM[fr:restes]="Extinction des processus enregistrés"
_TM[en:restes]="Shutting down recorded processes"
_TM[es:restes]="Cierre de los procesos registrados"
_TM[pt:restes]="Encerramento dos processos registados"
_TM[it:restes]="Arresto dei processi registrati"
_TM[hi:restes]="दर्ज प्रक्रियाओं की समाप्ति"
_TM[zh:restes]="关闭已登记的进程"
_TM[ar:restes]="إنهاء العمليات المسجلة"
_TM[fr:rundir]="Répertoire d'exécution"
_TM[en:rundir]="Run directory"
_TM[es:rundir]="Directorio de ejecución"
_TM[pt:rundir]="Diretório de execução"
_TM[it:rundir]="Directory di esecuzione"
_TM[hi:rundir]="रन निर्देशिका"
_TM[zh:rundir]="运行目录"
_TM[ar:rundir]="دليل التشغيل"
_TM[fr:sgsn]="Cœur — OsmoSGSN (rattachement paquet)"
_TM[en:sgsn]="Core — OsmoSGSN (packet attach)"
_TM[es:sgsn]="Núcleo — OsmoSGSN (conexión de paquetes)"
_TM[pt:sgsn]="Núcleo — OsmoSGSN (ligação de pacotes)"
_TM[it:sgsn]="Core — OsmoSGSN (attach pacchetti)"
_TM[hi:sgsn]="कोर — OsmoSGSN (पैकेट अटैच)"
_TM[zh:sgsn]="核心 — OsmoSGSN(分组附着)"
_TM[ar:sgsn]="النواة — OsmoSGSN (ارتباط الحزم)"
_TM[fr:sidecar-bts]="Station de base BTS#1 (side-car)"
_TM[en:sidecar-bts]="Base station BTS#1 (side-car)"
_TM[es:sidecar-bts]="Estación base BTS#1 (side-car)"
_TM[pt:sidecar-bts]="Estação base BTS#1 (side-car)"
_TM[it:sidecar-bts]="Stazione base BTS#1 (side-car)"
_TM[hi:sidecar-bts]="बेस स्टेशन BTS#1 (साइड-कार)"
_TM[zh:sidecar-bts]="基站 BTS#1(边车)"
_TM[ar:sidecar-bts]="المحطة القاعدية BTS#1 (الرديفة)"
_TM[fr:sidecar-cfg]="Configurations du BTS#1 (side-car)"
_TM[en:sidecar-cfg]="BTS#1 configurations (side-car)"
_TM[es:sidecar-cfg]="Configuraciones del BTS#1 (side-car)"
_TM[pt:sidecar-cfg]="Configurações do BTS#1 (side-car)"
_TM[it:sidecar-cfg]="Configurazioni del BTS#1 (side-car)"
_TM[hi:sidecar-cfg]="BTS#1 के विन्यास (साइड-कार)"
_TM[zh:sidecar-cfg]="BTS#1 配置(边车)"
_TM[ar:sidecar-cfg]="إعدادات BTS#1 (الرديفة)"
_TM[fr:sidecar-faketrx]="Transceiver simulé du BTS#1"
_TM[en:sidecar-faketrx]="BTS#1 simulated transceiver"
_TM[es:sidecar-faketrx]="Transceptor simulado del BTS#1"
_TM[pt:sidecar-faketrx]="Transcetor simulado do BTS#1"
_TM[it:sidecar-faketrx]="Ricetrasmettitore simulato del BTS#1"
_TM[hi:sidecar-faketrx]="BTS#1 का सिमुलेटेड ट्रांससीवर"
_TM[zh:sidecar-faketrx]="BTS#1 的模拟收发器"
_TM[ar:sidecar-faketrx]="جهاز الإرسال المحاكى لـ BTS#1"
_TM[fr:sidecar-mobile]="Second abonné (MS#2)"
_TM[en:sidecar-mobile]="Second subscriber (MS#2)"
_TM[es:sidecar-mobile]="Segundo abonado (MS#2)"
_TM[pt:sidecar-mobile]="Segundo assinante (MS#2)"
_TM[it:sidecar-mobile]="Secondo abbonato (MS#2)"
_TM[hi:sidecar-mobile]="दूसरा ग्राहक (MS#2)"
_TM[zh:sidecar-mobile]="第二个用户(MS#2)"
_TM[ar:sidecar-mobile]="المشترك الثاني (MS#2)"
_TM[fr:sidecar-trxcon]="Couche 1 du MS#2 (trxcon)"
_TM[en:sidecar-trxcon]="MS#2 layer 1 (trxcon)"
_TM[es:sidecar-trxcon]="Capa 1 del MS#2 (trxcon)"
_TM[pt:sidecar-trxcon]="Camada 1 do MS#2 (trxcon)"
_TM[it:sidecar-trxcon]="Livello 1 del MS#2 (trxcon)"
_TM[hi:sidecar-trxcon]="MS#2 की लेयर 1 (trxcon)"
_TM[zh:sidecar-trxcon]="MS#2 的第一层(trxcon)"
_TM[ar:sidecar-trxcon]="الطبقة الأولى لـ MS#2 (trxcon)"
_TM[fr:sip-connector]="Cœur — passerelle MNCC↔SIP"
_TM[en:sip-connector]="Core — MNCC↔SIP gateway"
_TM[es:sip-connector]="Núcleo — pasarela MNCC↔SIP"
_TM[pt:sip-connector]="Núcleo — gateway MNCC↔SIP"
_TM[it:sip-connector]="Core — gateway MNCC↔SIP"
_TM[hi:sip-connector]="कोर — MNCC↔SIP गेटवे"
_TM[zh:sip-connector]="核心 — MNCC↔SIP 网关"
_TM[ar:sip-connector]="النواة — بوابة MNCC↔SIP"
_TM[fr:smsc]="Cœur — passerelle SMS (proto-SMSC)"
_TM[en:smsc]="Core — SMS gateway (proto-SMSC)"
_TM[es:smsc]="Núcleo — pasarela SMS (proto-SMSC)"
_TM[pt:smsc]="Núcleo — gateway SMS (proto-SMSC)"
_TM[it:smsc]="Core — gateway SMS (proto-SMSC)"
_TM[hi:smsc]="कोर — SMS गेटवे (proto-SMSC)"
_TM[zh:smsc]="核心 — 短信网关(proto-SMSC)"
_TM[ar:smsc]="النواة — بوابة الرسائل (proto-SMSC)"
_TM[fr:stp]="Cœur — OsmoSTP (SS7/M3UA)"
_TM[en:stp]="Core — OsmoSTP (SS7/M3UA)"
_TM[es:stp]="Núcleo — OsmoSTP (SS7/M3UA)"
_TM[pt:stp]="Núcleo — OsmoSTP (SS7/M3UA)"
_TM[it:stp]="Core — OsmoSTP (SS7/M3UA)"
_TM[hi:stp]="कोर — OsmoSTP (SS7/M3UA)"
_TM[zh:stp]="核心 — OsmoSTP(SS7/M3UA)"
_TM[ar:stp]="النواة — OsmoSTP (SS7/M3UA)"
_TM[fr:summary]="Identité du build et résumé"
_TM[en:summary]="Build identity and summary"
_TM[es:summary]="Identidad de la compilación y resumen"
_TM[pt:summary]="Identidade da compilação e resumo"
_TM[it:summary]="Identità della build e riepilogo"
_TM[hi:summary]="बिल्ड पहचान और सारांश"
_TM[zh:summary]="构建标识与摘要"
_TM[ar:summary]="هوية البناء والملخص"
_TM[fr:teardown]="Nettoyage du run précédent"
_TM[en:teardown]="Cleaning up the previous run"
_TM[es:teardown]="Limpieza de la ejecución anterior"
_TM[pt:teardown]="Limpeza da execução anterior"
_TM[it:teardown]="Pulizia dell'esecuzione precedente"
_TM[hi:teardown]="पिछले रन की सफ़ाई"
_TM[zh:teardown]="清理上一次运行"
_TM[ar:teardown]="تنظيف التشغيل السابق"
_TM[fr:tmux]="Session d'observation tmux"
_TM[en:tmux]="tmux observation session"
_TM[es:tmux]="Sesión de observación tmux"
_TM[pt:tmux]="Sessão de observação tmux"
_TM[it:tmux]="Sessione di osservazione tmux"
_TM[hi:tmux]="tmux अवलोकन सत्र"
_TM[zh:tmux]="tmux 观察会话"
_TM[ar:tmux]="جلسة مراقبة tmux"

_T[fr:arret]="Arrêt de %s"
_T[en:arret]="Stopping %s"
_T[es:arret]="Deteniendo %s"
_T[pt:arret]="A parar %s"
_T[it:arret]="Arresto di %s"
_T[hi:arret]="%s रोका जा रहा है"
_T[zh:arret]="正在停止 %s"
_T[ar:arret]="إيقاف %s"
_T[fr:dep_ko]="dépendance non satisfaite : %s"
_T[en:dep_ko]="unmet dependency: %s"
_T[es:dep_ko]="dependencia no satisfecha: %s"
_T[pt:dep_ko]="dependência não satisfeita: %s"
_T[it:dep_ko]="dipendenza non soddisfatta: %s"
_T[hi:dep_ko]="निर्भरता अपूर्ण: %s"
_T[zh:dep_ko]="依赖未满足:%s"
_T[ar:dep_ko]="تبعية غير مستوفاة: %s"

_T[fr:obligatoire]="obligatoire"
_T[en:obligatoire]="required"
_T[es:obligatoire]="obligatorio"
_T[pt:obligatoire]="obrigatório"
_T[it:obligatoire]="obbligatorio"
_T[hi:obligatoire]="अनिवार्य"
_T[zh:obligatoire]="必需"
_T[ar:obligatoire]="إلزامي"
_T[fr:optionnel]="optionnel"
_T[en:optionnel]="optional"
_T[es:optionnel]="opcional"
_T[pt:optionnel]="opcional"
_T[it:optionnel]="opzionale"
_T[hi:optionnel]="वैकल्पिक"
_T[zh:optionnel]="可选"
_T[ar:optionnel]="اختياري"
_T[fr:apres]="après: %s"
_T[en:apres]="after: %s"
_T[es:apres]="tras: %s"
_T[pt:apres]="após: %s"
_T[it:apres]="dopo: %s"
_T[hi:apres]="के बाद: %s"
_T[zh:apres]="依赖:%s"
_T[ar:apres]="بعد: %s"

_T[de:plan]="Plan — %d Module"
_T[de:ok]="OK"
_T[de:fail]="FEHL"
_T[de:skip]="ÜBER"
_T[de:dry]="PROB"
_T[de:simule]="simuliert"
_T[de:deja]="läuft bereits"
_T[de:desactive]="deaktiviert durch %s"
_T[de:interrompu]="Ablauf abgebrochen (Pflichtmodul)"
_T[de:journal]="Protokoll: %s"
_T[de:pas_pret]="gestartet, aber nie bereit: %s"
_T[de:sans_raison]="kein Grund vom Modul angegeben"
_T[de:prereq_sim]="Voraussetzung im Probelauf nicht prüfbar: %s"
_T[de:bilan]="%d ok · %d übersprungen · %d Fehler"
_T[de:opt_inconnue]="unbekannte Option: %s"
_T[de:arret]="%s wird beendet"
_T[de:dep_ko]="Abhängigkeit nicht erfüllt: %s"
_T[de:obligatoire]="Pflicht"
_T[de:optionnel]="optional"
_T[de:apres]="nach: %s"
_TM[de:abonnes-hlr]="Kern — im HLR eingerichtete Teilnehmer"
_TM[de:apn0]="Kern — TUN-Schnittstelle des APN"
_TM[de:asterisk]="Kern — Asterisk (SIP-Telefonanlage)"
_TM[de:attach]="Rückkehr in die tmux-Sitzung"
_TM[de:audio]="Audiobrücke (PulseAudio + GAPK)"
_TM[de:bsc]="Kern — OsmoBSC (BTS-Steuerung)"
_TM[de:bts]="Basisstation osmo-bts-trx"
_TM[de:config]="Auflösung von Pfaden und Ports"
_TM[de:gabarits]="Konfigurationsvorlagen des Kerns"
_TM[de:ggsn]="Kern — OsmoGGSN (GTP-Daten)"
_TM[de:gsmtap]="GSMTAP-Mitschnitt (pcap)"
_TM[de:hlr]="Kern — OsmoHLR (GSUP-Register)"
_TM[de:l2]="Schicht-2-Client"
_TM[de:logs]="Protokolle und Arbeitsbereich"
_TM[de:mgw]="Kern — OsmoMGW (MGCP/RTP-Medien)"
_TM[de:mobile-cfg]="Konfiguration des Mobilteils"
_TM[de:msc]="Kern — OsmoMSC (leitungsvermittelt)"
_TM[de:osmocon]="osmocon-Übergang"
_TM[de:outils]="Werkzeuge des Wirtssystems"
_TM[de:panes]="Überwachungsansicht (tmux)"
_TM[de:pcu]="Kern — OsmoPCU (Paketzuteilung)"
_TM[de:prereqs]="Prüfung der Voraussetzungen"
_TM[de:pty]="Serielles PTY des Emulators"
_TM[de:qemu]="Calypso-Emulator (QEMU)"
_TM[de:reseau]="Schnittstellen und Adressplan"
_TM[de:restes]="Beenden der erfassten Prozesse"
_TM[de:rundir]="Ausführungsverzeichnis"
_TM[de:sgsn]="Kern — OsmoSGSN (Paketanbindung)"
_TM[de:sidecar-bts]="Basisstation BTS#1 (Beiwagen)"
_TM[de:sidecar-cfg]="Konfigurationen der BTS#1 (Beiwagen)"
_TM[de:sidecar-faketrx]="Simulierter Transceiver der BTS#1"
_TM[de:sidecar-mobile]="Zweiter Teilnehmer (MS#2)"
_TM[de:sidecar-trxcon]="Schicht 1 des MS#2 (trxcon)"
_TM[de:sip-connector]="Kern — MNCC↔SIP-Übergang"
_TM[de:smsc]="Kern — SMS-Übergang (proto-SMSC)"
_TM[de:stp]="Kern — OsmoSTP (SS7/M3UA)"
_TM[de:summary]="Build-Kennung und Zusammenfassung"
_TM[de:teardown]="Aufräumen des vorherigen Laufs"
_TM[de:tmux]="tmux-Beobachtungssitzung"

_T[fr:up]="ACTIF"
_T[de:up]="AKTIV"
_T[en:up]="UP"
_T[es:up]="ACTIVO"
_T[pt:up]="ATIVO"
_T[it:up]="ATTIVO"
_T[hi:up]="चालू"
_T[zh:up]="运行"
_T[ar:up]="يعمل"
_T[fr:down]="ARRÊT"
_T[de:down]="AUS"
_T[en:down]="DOWN"
_T[es:down]="PARADO"
_T[pt:down]="PARADO"
_T[it:down]="FERMO"
_T[hi:down]="बंद"
_T[zh:down]="停止"
_T[ar:down]="متوقف"
_T[fr:connecter]="se connecter"
_T[de:connecter]="verbinden"
_T[en:connecter]="attach"
_T[es:connecter]="conectar"
_T[pt:connecter]="ligar"
_T[it:connecter]="collegarsi"
_T[hi:connecter]="जुड़ें"
_T[zh:connecter]="接入"
_T[ar:connecter]="اتصال"
_T[fr:pas_session]="(aucune session tmux « %s » — la pile tourne en arrière-plan)"
_T[de:pas_session]="(keine tmux-Sitzung „%s“ — der Stapel läuft im Hintergrund)"
_T[en:pas_session]="(no tmux session \"%s\" — the stack runs in the background)"
_T[es:pas_session]="(sin sesión tmux «%s» — la pila corre en segundo plano)"
_T[pt:pas_session]="(sem sessão tmux «%s» — a pilha corre em segundo plano)"
_T[it:pas_session]="(nessuna sessione tmux «%s» — lo stack gira in background)"
_T[hi:pas_session]="(कोई tmux सत्र «%s» नहीं — स्टैक पृष्ठभूमि में चल रहा है)"
_T[zh:pas_session]="(没有 tmux 会话「%s」— 整套服务在后台运行)"
_T[ar:pas_session]="(لا توجد جلسة tmux «%s» — الحزمة تعمل في الخلفية)"
_T[fr:journaux]="journaux"
_T[de:journaux]="Protokolle"
_T[en:journaux]="logs"
_T[es:journaux]="registros"
_T[pt:journaux]="registos"
_T[it:journaux]="registri"
_T[hi:journaux]="लॉग"
_T[zh:journaux]="日志"
_T[ar:journaux]="السجلات"
_T[fr:etat]="état"
_T[de:etat]="Status"
_T[en:etat]="status"
_T[es:etat]="estado"
_T[pt:etat]="estado"
_T[it:etat]="stato"
_T[hi:etat]="स्थिति"
_T[zh:etat]="状态"
_T[ar:etat]="الحالة"
_T[fr:arreter]="arrêter"
_T[de:arreter]="beenden"
_T[en:arreter]="stop"
_T[es:arreter]="detener"
_T[pt:arreter]="parar"
_T[it:arreter]="fermare"
_T[hi:arreter]="रोकें"
_T[zh:arreter]="停止"
_T[ar:arreter]="إيقاف"
_T[fr:aucun_mod]="aucun module dans %s — rien à faire"
_T[de:aucun_mod]="kein Modul in %s — nichts zu tun"
_T[en:aucun_mod]="no module in %s — nothing to do"
_T[es:aucun_mod]="ningún módulo en %s — nada que hacer"
_T[pt:aucun_mod]="nenhum módulo em %s — nada a fazer"
_T[it:aucun_mod]="nessun modulo in %s — niente da fare"
_T[hi:aucun_mod]="%s में कोई मॉड्यूल नहीं — कुछ नहीं करना"
_T[zh:aucun_mod]="%s 中没有模块 — 无事可做"
_T[ar:aucun_mod]="لا توجد وحدات في %s — لا شيء لفعله"

t() {
    local cle="$1"; shift
    local fmt="${_T[${CALYPSO_LANG}:$cle]:-${_T[en:$cle]:-${_T[fr:$cle]:-$cle}}}"
    printf "$fmt" "$@"
}

t_mod() {
    printf '%s' "${_TM[${CALYPSO_LANG}:$1]:-${_TM[en:$1]:-${_TM[fr:$1]:-${MOD_DESC[$1]:-$1}}}}"
}

case " fr de en es pt it hi zh ar " in
    *" $CALYPSO_LANG "*) ;;
    *) printf 'CALYPSO_LANG=%s inconnue — repli sur « en » (fr de en es pt it hi zh ar)\n' \
              "$CALYPSO_LANG" >&2
       CALYPSO_LANG=en ;;
esac

run_modules/_lib/mod.sh

.sh 80 lignes - 2,4KB

run_modules/_lib/mod.sh
. "$(dirname "${BASH_SOURCE[0]}")/i18n.sh"

readonly MOD_RC_OK=0
readonly MOD_RC_FAIL=1
readonly MOD_RC_UNKNOWN=2
readonly MOD_RC_ALREADY=3
readonly MOD_RC_SKIP=4

declare -a MOD_ORDER=()
declare -A MOD_DESC=()
declare -A MOD_DEPS=()
declare -A MOD_REQUIRED=()
declare -A MOD_LOG=()
declare -A MOD_JOURNAL=()
declare -A MOD_PURE=()
declare -A MOD_ENABLED_IF=()
declare -A MOD_TIMEOUT=()

MOD_REGISTER() {
    local slug="$1" desc="$2"
    MOD_ORDER+=("$slug")
    MOD_DESC[$slug]="$desc"
    MOD_DEPS[$slug]=""
    MOD_REQUIRED[$slug]=1
    MOD_LOG[$slug]=""
    MOD_JOURNAL[$slug]=""
    MOD_PURE[$slug]=0
    MOD_ENABLED_IF[$slug]='true'
    MOD_TIMEOUT[$slug]=30
}

mod_prefix() { printf 'mod_%s' "${1//-/_}"; }

_MOD_REASON=""
_MOD_HINT=""

mod_ok()      { _MOD_REASON=""   ; return $MOD_RC_OK; }
mod_fail()    { _MOD_REASON="$*" ; return $MOD_RC_FAIL; }
mod_already() { _MOD_REASON="$*" ; return $MOD_RC_ALREADY; }
mod_skip()    { _MOD_REASON="$*" ; return $MOD_RC_SKIP; }
mod_hint()    { _MOD_HINT="$*"; }
mod_say()     { printf '%s\n' "$*"; }

wait_until() {
    local timeout="$1" what="$2"; shift 2
    local deadline=$(( SECONDS + timeout ))
    while (( SECONDS < deadline )); do
        if "$@" >/dev/null 2>&1; then return $MOD_RC_OK; fi
        sleep 0.2
    done
    mod_fail "$what : toujours pas prêt après ${timeout}s"
}

have_proc() {
    local motif="$1" p ligne ns
    ns="$(readlink /proc/self/ns/pid 2>/dev/null)"
    for p in $(pgrep -f "$motif" 2>/dev/null); do
        [ "$p" = "$$" ] || [ "$p" = "$PPID" ] && continue
        [ -z "$ns" ] || [ "$(readlink "/proc/$p/ns/pid" 2>/dev/null)" = "$ns" ] || continue
        ligne="$( { tr '\0' ' ' < "/proc/$p/cmdline"; } 2>/dev/null )"
        [ -n "$ligne" ] || continue
        case "$ligne" in
            *"tail "*|*"tail -"*|*"less "*|*"multitail "*|*"watch "*|\
            *"sleep infinity"*|*"journalctl "*) continue ;;
        esac
        return 0
    done
    return 1
}
have_port()  { (exec 3<>"/dev/tcp/127.0.0.1/$1") >/dev/null 2>&1; }
have_unix()  { [ -S "$1" ]; }
log_has()    { [ -f "$1" ] && grep -q "$2" "$1" 2>/dev/null; }

modb_tail() {
    local f="$1" n="${2:-20}"
    if [ ! -r "$f" ]; then mod_say "journal absent ou illisible : $f"; return 0; fi
    mod_say "--- $n dernières lignes de $f ---"
    tail -n "$n" "$f" 2>/dev/null
    mod_say "--- fin de $f ---"
}

run_modules/_lib/radio.sh

.sh 59 lignes - 2,0KB

run_modules/_lib/radio.sh
[ -n "${_RADIO_LIB_LOADED:-}" ] && return 0
_RADIO_LIB_LOADED=1

: "${SC_TRX_PORT:=5720}"
: "${SC_BB_PORT:=6720}"
: "${SC_BTS_VTY_PORT:=4250}"
: "${SC_MOBILE_VTY_PORT:=4248}"
: "${SC_UNIT_ID:=6002}"
: "${SC_ARFCN:=516}"
: "${SC_BTS_CFG:=${OSMOCOM_CFG:-/etc/osmocom}/osmo-bts-trx-bts1.cfg}"
: "${SC_MOBILE_CFG:=/root/.osmocom/bb/mobile_faketrx_bts1.cfg}"
: "${SC_L2_SOCK:=/tmp/ms2_l2}"
: "${TRX_BIND_IP:=127.0.0.1}"
: "${TRX_BTS_IP:=127.0.0.1}"
: "${TRX_BB_IP:=127.0.0.1}"
: "${RUN_DIR:=/tmp/calypso}"
: "${LOG_DIR:=$RUN_DIR/logs}"

radio_dirs() { mkdir -p "$RUN_DIR" "$LOG_DIR" 2>/dev/null || true; }
radio_log() { printf '%s/%s.log\n' "$LOG_DIR" "$1"; }
radio_pidfile() { printf '%s/%s.pid\n' "$RUN_DIR" "$1"; }
radio_save_pid() { radio_dirs; printf '%s\n' "$2" > "$(radio_pidfile "$1")"; }
radio_pid()   { cat "$(radio_pidfile "$1")" 2>/dev/null || echo 0; }
radio_alive() { local p; p="$(radio_pid "$1")"; [ "$p" != 0 ] && kill -0 "$p" 2>/dev/null; }

radio_kill() {
    local slug="$1" pattern="${2:-}" p i
    p="$(radio_pid "$slug")"
    [ "$p" != 0 ] && kill "$p" 2>/dev/null
    [ -n "$pattern" ] && pkill -f "$pattern" 2>/dev/null
    for i in 1 2 3 4 5 6 7 8 9 10; do
        if [ "$p" = 0 ] || ! kill -0 "$p" 2>/dev/null; then break; fi
        sleep 0.2
    done
    if [ "$p" != 0 ] && kill -0 "$p" 2>/dev/null; then
        kill -9 "$p" 2>/dev/null
        [ -n "$pattern" ] && pkill -9 -f "$pattern" 2>/dev/null
    fi
    rm -f "$(radio_pidfile "$slug")"
    return 0
}

radio_udp_bound() {
    ss -uan 2>/dev/null | grep -qE "[:.]$1[[:space:]]"
}

radio_cfg_val() {
    local f="$1"; shift
    [ -r "$f" ] || return 1
    awk -v k="$*" '
        {
            line = $0
            sub(/^[[:space:]]+/, "", line); sub(/[[:space:]]+$/, "", line)
            n = split(k, kk, /[[:space:]]+/)
            split(line, ff, /[[:space:]]+/)
            for (i = 1; i <= n; i++) if (ff[i] != kk[i]) next
            if (ff[n+1] != "") { print ff[n+1]; exit }
        }' "$f"
}

run.sh

.sh 305 lignes - 13KB

run.sh
#!/bin/bash
set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODDIR="$HERE/run_modules"

DRY=0 ONLY="" SKIP="" ACTION=start FORCE=0
NO_ATTACH="${CALYPSO_NO_ATTACH:-0}"

usage() {
    cat <<'USAGE'
Usage : ./run.sh [options]
  --list              affiche le plan et sort, sans rien lancer
  --dry-run           déroule le plan sans effet de bord
  --only  <slugs>     ne joue que ces modules (séparés par des virgules)
  --skip  <slugs>     saute ces modules
  --stop              arrête la pile (plan en ordre inverse)
  --status            interroge l'état de chaque module
  --force             relance même les modules déjà démarrés
  --no-attach         ne pas s'attacher à tmux à la fin
  --check-paths       vérifie que les dépendances déclarées existent
  --restart           = --reset puis démarrage — la relance sûre au quotidien
  --reset             repart d'un état propre : arrête la pile, tue le serveur
                      tmux, archive les journaux, purge sockets et fichiers partagés
  --lang <code>       langue de l'affichage (fr de en es pt it hi zh ar)
  --profile <nom>     accepté pour compatibilité, sans effet (un seul banc)
  --verbose           accepté pour compatibilité, sans effet
  -h, --help          cette aide
USAGE
}

_reset() {
    local horodat rep n=0
    printf '\nRemise à zéro — rien ne sera relancé.\n\n' >&2
    printf '  arrêt de la pile…\n' >&2
    "$0" --stop >/dev/null 2>&1 || true
    local logdir="$LOG_DIR"
    if [ -d "$logdir" ] && [ -n "$(ls -A "$logdir" 2>/dev/null)" ]; then
        horodat="$(date +%Y%m%d-%H%M%S)"
        rep="$logdir/../archives/$horodat"
        mkdir -p "$rep" 2>/dev/null && \
        if cp -a "$logdir/." "$rep/" 2>/dev/null; then
            printf '  journaux archivés     %s\n' "$rep" >&2
        fi
        local arcdir="$logdir/../archives"
        local keep="${CALYPSO_ARCHIVES_KEEP:-3}"
        local maxmb="${CALYPSO_ARCHIVE_MAX_MB:-32}"
        local f taille avant apres
        for f in "$rep"/*; do
            [ -f "$f" ] || continue
            taille=$(stat -c %s "$f" 2>/dev/null || echo 0)
            if [ "$taille" -gt $(( maxmb * 1024 * 1024 )) ]; then
                head -c $(( maxmb * 1024 * 1024 )) "$f" > "$f.tete" 2>/dev/null &&
                mv -f "$f.tete" "$f" 2>/dev/null &&
                printf '  archive tronquée      %s (%s Mo -> %s Mo, tête conservée)\n' \
                       "$(basename "$f")" "$(( taille / 1048576 ))" "$maxmb" >&2
            fi
        done
        if [ -d "$arcdir" ]; then
            avant=$(du -sm "$arcdir" 2>/dev/null | cut -f1)
            ls -1t "$arcdir" 2>/dev/null | tail -n +$(( keep + 1 )) | while read -r vieux; do
                [ -n "$vieux" ] && rm -rf -- "$arcdir/$vieux"
            done
            apres=$(du -sm "$arcdir" 2>/dev/null | cut -f1)
            [ "${avant:-0}" -gt "${apres:-0}" ] 2>/dev/null &&
                printf '  archives purgées      %s gardées (%s Mo -> %s Mo)\n' \
                       "$keep" "$avant" "$apres" >&2
        fi
        rm -f "$logdir"/*.log 2>/dev/null
        printf '  journaux remis à zéro %s\n' "$logdir" >&2
        df -h "$logdir" 2>/dev/null | tail -1 | \
            awk '{printf "  espace disponible     %s (%s utilisé)\n", $4, $5}' >&2
    fi
    if command -v tmux >/dev/null 2>&1 && tmux ls >/dev/null 2>&1; then
        tmux kill-server 2>/dev/null || true
        printf '  serveur tmux tué      (environnement global purgé)\n' >&2
    fi
    local motifs="qemu-system-arm osmocon osmo-bts-trx fake_trx trxcon"
    local m p pids
    for m in $motifs; do
        pids="$(pgrep -f -- "$m" 2>/dev/null || true)"
        for p in $pids; do
            [ "$p" = "$$" ] && continue
            [ "$p" = "${PPID:-0}" ] && continue
            kill -TERM "$p" 2>/dev/null && n=$((n + 1))
        done
    done
    [ "$n" -gt 0 ] && printf '  processus arrêtés     %d\n' "$n" >&2
    local art
    for art in /tmp/osmocom_l2 /tmp/ms2_l2 /dev/shm/calypso_*; do
        [ -e "$art" ] && rm -f "$art" 2>/dev/null && printf '  supprimé              %s\n' "$art" >&2
    done
    printf '\nÉtat propre. Relancez :\n' >&2
    printf '   ./run.sh\n\n' >&2
}

while [ $# -gt 0 ]; do
    case "$1" in
        --list)        ACTION=list ;;
        --dry-run)     DRY=1 ;;
        --only)        ONLY="${2:-}"; shift ;;
        --skip)        SKIP="${2:-}"; shift ;;
        --profile)     shift ;;
        --stop)        ACTION=stop ;;
        --status)      ACTION=status ;;
        --force)       FORCE=1 ;;
        --no-attach)   NO_ATTACH=1 ;;
        --verbose)     ;;
        --check-paths) ACTION=checkpaths ;;
        --reset)       ACTION=reset ;;
        --restart)     ACTION=restart ;;
        --lang)        CALYPSO_LANG="${2:-}"; export CALYPSO_LANG; shift ;;
        -h|--help)     usage; exit 0 ;;
        *) printf 'option inconnue : %s\n\n' "$1" >&2; usage >&2; exit 2 ;;
    esac
    shift
done

if [ -f "$HERE/environnement/bench.env" ]; then
    set -a; . "$HERE/environnement/bench.env"; set +a
else
    printf 'configuration introuvable : %s\n' "$HERE/environnement/bench.env" >&2
    exit 2
fi

if [ "$ACTION" = reset ]; then _reset; exit 0; fi
if [ "$ACTION" = restart ]; then _reset; ACTION=start; fi

LOGDIR="$LOG_DIR"
mkdir -p "$LOGDIR/mod" 2>/dev/null || true

if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
    TTY=1; C_OK=$'\033[32m'; C_KO=$'\033[31m'; C_SK=$'\033[33m'; C_DIM=$'\033[2m'; C_Z=$'\033[0m'
else
    TTY=0; C_OK=""; C_KO=""; C_SK=""; C_DIM=""; C_Z=""
fi
say_begin() { if [ $TTY -eq 1 ]; then printf '[ %s.. %s] %s' "$C_DIM" "$C_Z" "$1"; else printf '[ .. ] %s\n' "$1"; fi; }
say_end()   {
    if [ $TTY -eq 1 ]; then printf '\r\033[K'; fi
    printf '[%s%-4s%s] %s' "$2" "$1" "$C_Z" "$3"
    [ -n "${4:-}" ] && printf ' %s(%s)%s' "$C_DIM" "$4" "$C_Z"
    printf '\n'
}

. "$MODDIR/_lib/mod.sh"
shopt -s nullglob
for f in "$MODDIR"/[0-9][0-9]-*.sh; do . "$f"; done
shopt -u nullglob

if [ ${#MOD_ORDER[@]} -eq 0 ]; then
    printf '%s\n' "$(t aucun_mod "$MODDIR")" >&2
    exit 2
fi

in_csv() { case ",$1," in *",$2,"*) return 0;; esac; return 1; }
selected=()
for m in "${MOD_ORDER[@]}"; do
    [ -n "$ONLY" ] && { in_csv "$ONLY" "$m" || continue; }
    [ -n "$SKIP" ] && { in_csv "$SKIP" "$m" && continue; }
    selected+=("$m")
done

if [ "$ACTION" = list ]; then
    printf '%s\n\n' "$(t plan "${#selected[@]}")"
    for m in "${selected[@]}"; do
        printf '  %-18s %-42s %s%s\n' "$m" "$(t_mod "$m")" \
            "$([ "${MOD_REQUIRED[$m]}" = 1 ] && t obligatoire || t optionnel)" \
            "$([ -n "${MOD_DEPS[$m]}" ] && printf '  %s' "$(t apres "${MOD_DEPS[$m]}")")"
    done
    exit 0
fi

if [ "$ACTION" = checkpaths ]; then
    rc=0
    for v in QEMU_BIN FIRMWARE_ELF FIRMWARE_BIN OSMOCON OSMOCOM_CFG; do
        p="${!v:-}"
        if [ -z "$p" ];      then say_end "$(t fail)" "$C_KO" "$v" "non défini"; rc=1
        elif [ -e "$p" ];    then say_end " $(t ok) " "$C_OK" "$v" "$p"
        else                      say_end "$(t fail)" "$C_KO" "$v" "introuvable : $p"; rc=1; fi
    done
    exit $rc
fi

declare -A STATE=()
nb_ok=0 nb_fail=0 nb_skip=0 nb_warn=0
[ "$ACTION" = stop ] && { tmp=(); for ((i=${#selected[@]}-1;i>=0;i--)); do tmp+=("${selected[$i]}"); done; selected=("${tmp[@]}"); }

for m in "${selected[@]}"; do
    p="$(mod_prefix "$m")"
    log="${MOD_LOG[$m]:-$LOGDIR/mod/$m.log}"
    _MOD_REASON=""; _MOD_HINT=""

    if [ "$ACTION" = stop ]; then
        say_begin "$(t arret "$(t_mod "$m")")"
        declare -F "${p}_stop" >/dev/null && "${p}_stop" >>"$log" 2>&1
        say_end " $(t ok) " "$C_OK" "$(t arret "$(t_mod "$m")")"
        continue
    fi

    if [ "$ACTION" = status ]; then
        if declare -F "${p}_status" >/dev/null && "${p}_status" >>"$log" 2>&1; then
            say_end "$(t up)" "$C_OK" "$(t_mod "$m")"
        else
            say_end "$(t down)" "$C_SK" "$(t_mod "$m")"
        fi
        continue
    fi

    if ! eval "${MOD_ENABLED_IF[$m]}" 2>/dev/null; then
        say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "$(t desactive "${MOD_ENABLED_IF[$m]}")"
        STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue
    fi

    dep_ko=""
    for d in ${MOD_DEPS[$m]}; do
        case "${STATE[$d]:-absent}" in ok|skip) ;; *) dep_ko="$d"; break;; esac
    done
    if [ -n "$dep_ko" ]; then
        say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "$(t dep_ko "$dep_ko")"
        STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue
    fi

    say_begin "$(t_mod "$m")"

    if [ $FORCE -eq 0 ] && declare -F "${p}_status" >/dev/null && "${p}_status" >>"$log" 2>&1; then
        say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "$(t deja)"
        STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue
    fi

    if declare -F "${p}_check" >/dev/null; then
        "${p}_check" >>"$log" 2>&1; rc=$?
        if [ $rc -eq $MOD_RC_FAIL ] && [ $DRY -eq 1 ]; then
            say_end "$(t dry)" "$C_DIM" "$(t_mod "$m")" \
                    "$(t prereq_sim "${_MOD_REASON:-}")"
            STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue
        fi
        if [ $rc -eq $MOD_RC_FAIL ]; then
            say_end "$(t fail)" "$C_KO" "$(t_mod "$m")" "${_MOD_REASON:-$(t sans_raison)}"
            [ -n "$_MOD_HINT" ] && printf '       %s→ %s%s\n' "$C_DIM" "$_MOD_HINT" "$C_Z"
            printf '       %s%s%s\n' "$C_DIM" "$(t journal "$log")" "$C_Z"
            STATE[$m]=fail; nb_fail=$((nb_fail+1))
            [ "${MOD_REQUIRED[$m]}" = 1 ] && { printf '\n%s\n' "$(t interrompu)"; exit 1; }
            nb_warn=$((nb_warn+1)); continue
        elif [ $rc -eq $MOD_RC_SKIP ]; then
            say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "${_MOD_REASON:-}"
            STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue
        fi
    fi

    if [ $DRY -eq 1 ] && [ "${MOD_PURE[$m]}" != 1 ]; then
        say_end " -- " "$C_DIM" "$(t_mod "$m")" "$(t simule)"
        STATE[$m]=ok; continue
    fi

    "${p}_start" >>"$log" 2>&1; rc=$?
    case $rc in
        $MOD_RC_ALREADY) say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "${_MOD_REASON:-$(t deja)}"
                         STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue ;;
        $MOD_RC_SKIP)    say_end "$(t skip)" "$C_SK" "$(t_mod "$m")" "${_MOD_REASON:-}"
                         STATE[$m]=skip; nb_skip=$((nb_skip+1)); continue ;;
        $MOD_RC_FAIL)    say_end "$(t fail)" "$C_KO" "$(t_mod "$m")" "${_MOD_REASON:-$(t sans_raison)}"
                         [ -n "$_MOD_HINT" ] && printf '       %s→ %s%s\n' "$C_DIM" "$_MOD_HINT" "$C_Z"
                         printf '       %s%s%s\n' "$C_DIM" "$(t journal "$log")" "$C_Z"
                         STATE[$m]=fail; nb_fail=$((nb_fail+1))
                         [ "${MOD_REQUIRED[$m]}" = 1 ] && { printf '\n%s\n' "$(t interrompu)"; exit 1; }
                         nb_warn=$((nb_warn+1)); continue ;;
    esac

    if declare -F "${p}_wait" >/dev/null; then
        if ! "${p}_wait" >>"$log" 2>&1; then
            say_end "$(t fail)" "$C_KO" "$(t_mod "$m")" "$(t pas_pret "${_MOD_REASON:-délai dépassé}")"
            printf '       %s%s%s\n' "$C_DIM" "$(t journal "$log")" "$C_Z"
            STATE[$m]=fail; nb_fail=$((nb_fail+1))
            [ "${MOD_REQUIRED[$m]}" = 1 ] && { printf '\n%s\n' "$(t interrompu)"; exit 1; }
            nb_warn=$((nb_warn+1)); continue
        fi
    fi

    say_end " $(t ok) " "$C_OK" "$(t_mod "$m")"
    STATE[$m]=ok; nb_ok=$((nb_ok+1))
done

printf '\n%s\n' "$(t bilan "$nb_ok" "$nb_skip" "$nb_fail")"

if [ "$ACTION" = start ] && [ $DRY -eq 0 ] && [ $nb_fail -eq 0 ]; then
    _sess="${TMUX_SESSION:-calypso}"
    printf '\n'
    if tmux has-session -t "$_sess" 2>/dev/null; then
        printf '  %s%s%s   tmux attach -t %s\n' "$C_OK" "$(t connecter)" "$C_Z" "$_sess"
    else
        printf '  %s%s%s   %s\n' "$C_DIM" "$(t connecter)" "$C_Z" "$(t pas_session "$_sess")"
    fi
    printf '  %s%s%s       %s\n' "$C_DIM" "$(t journaux)" "$C_Z" "$LOGDIR"
    printf '  %s%s%s           ./run.sh --status\n' "$C_DIM" "$(t etat)" "$C_Z"
    printf '  %s%s%s        ./run.sh --stop\n' "$C_DIM" "$(t arreter)" "$C_Z"
    printf '\n'
    if [ $TTY -eq 1 ] && [ "$NO_ATTACH" != 1 ] && [ -z "${TMUX:-}" ] \
       && tmux has-session -t "$_sess" 2>/dev/null; then
        printf '  %sconnexion a la session…  (Ctrl-b d pour detacher)%s\n\n' "$C_DIM" "$C_Z"
        exec tmux attach -t "$_sess"
    fi
fi

[ $nb_fail -gt 0 ] && exit 1
exit 0

scripts/populate-si.sh

.sh 96 lignes - 3,6KB

scripts/populate-si.sh
#!/bin/bash
# populate-si.sh — XXX TRANSITIONAL STUB (étape 2b, disposable).
#
# Écrit les 5 SI hex blobs RSL-extracted (osmo-bsc → osmo-bts trace 2026-04-30)
# dans /dev/shm/calypso_si.bin per doc/MMAP_SI_FORMAT.md v1.
#
# Permet de valider l'interface mmap (étape 2a) sans dépendance sur le RSL
# parser (étape 3). À supprimer dès que scripts/rsl_si_tap.py est opérationnel
# en steady-state.
#
# Usage:
#   ./populate-si.sh                              # défaut /dev/shm/calypso_si.bin
#   CALYPSO_SI_MMAP_PATH=/tmp/foo ./populate-si.sh
#
# Vérification:
#   xxd /dev/shm/calypso_si.bin | head           # check magic "CSI1" + slots

set -e

OUTPUT="${CALYPSO_SI_MMAP_PATH:-/dev/shm/calypso_si.bin}"

python3 - "$OUTPUT" << 'PYEOF'
import sys, struct, os

OUTPUT = sys.argv[1]

# 23-byte BCCH SI blobs, byte-exact from RSL re-attach trace
# (osmo-bsc emits BCCH INFORMATION when osmo-bts attaches via Abis OML).
# LAI 001/01/0001, CI=6001, BSIC=7, ARFCN=514.
SI1  = bytes([0x55, 0x06, 0x19, 0x8f, 0x01,
              0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
              0xe5, 0x04, 0x00, 0x2b])
SI2  = bytes([0x59, 0x06, 0x1a,
              0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
              0xff, 0xe5, 0x04, 0x00])
SI3  = bytes([0x49, 0x06, 0x1b, 0x17, 0x71, 0x00, 0xf1, 0x10, 0x00, 0x01, 0xc9, 0x03,
              0x05, 0x27, 0x47, 0x40, 0xe5, 0x04, 0x00, 0x2c, 0x0b, 0x2b, 0x2b])
SI4  = bytes([0x31, 0x06, 0x1c, 0x00, 0xf1, 0x10, 0x00, 0x01, 0x47, 0x40, 0xe5, 0x04,
              0x00, 0x01, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b])
SI13 = bytes([0x01, 0x06, 0x00, 0x90, 0x00, 0x18, 0x5a, 0x6f, 0xc9, 0xf2, 0xb5, 0x30,
              0x42, 0x08, 0xeb, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b])

assert len(SI1)  == 23, f"SI1  len={len(SI1)}"
assert len(SI2)  == 23, f"SI2  len={len(SI2)}"
assert len(SI3)  == 23, f"SI3  len={len(SI3)}"
assert len(SI4)  == 23, f"SI4  len={len(SI4)}"
assert len(SI13) == 23, f"SI13 len={len(SI13)}"

# Layout per MMAP_SI_FORMAT.md v1 :
#   header  (16 bytes) : magic + version + slot_count + last_update_fn + reserved
#   slots   (5 × 32B)  : si_type + flags + blob_len + pad + 23B blob + 5B pad
SLOT_VALID = 0x01

def make_slot(si_type, blob):
    s = bytearray(32)
    s[0] = si_type
    s[1] = SLOT_VALID
    s[2] = len(blob)
    s[3] = 0  # padding
    s[4:4+23] = blob
    # 27..31 padding zero (already)
    return bytes(s)

buf = bytearray(176)
# Header
buf[0:4]  = b"CSI1"
buf[4]    = 0x01      # version
buf[5]    = 0x05      # slot_count
buf[6:8]  = struct.pack("<H", 0)  # last_update_fn = 0 (stub static, no live FN)
# bytes 8..15 reserved zero (already)

# Slots in fixed order : SI1, SI2, SI3, SI4, SI13
buf[16:48]    = make_slot(0x01, SI1)
buf[48:80]    = make_slot(0x02, SI2)
buf[80:112]   = make_slot(0x03, SI3)
buf[112:144]  = make_slot(0x04, SI4)
buf[144:176]  = make_slot(0x0d, SI13)

with open(OUTPUT, "wb") as f:
    f.write(buf)

# Verify
with open(OUTPUT, "rb") as f:
    chk = f.read()
assert chk == bytes(buf), "readback mismatch"
print(f"OK : wrote {len(buf)} bytes to {OUTPUT}")
print(f"     magic={chk[0:4]!r} version={chk[4]} slot_count={chk[5]}")
for slot_idx, name in enumerate(["SI1", "SI2", "SI3", "SI4", "SI13"]):
    off = 16 + slot_idx * 32
    si_type = chk[off]
    flags   = chk[off+1]
    blob_len= chk[off+2]
    blob    = chk[off+4:off+4+23]
    print(f"     slot[{slot_idx}] {name}: type=0x{si_type:02x} flags=0x{flags:02x} "
          f"len={blob_len} blob[0:4]={blob[:4].hex()} blob[-4:]={blob[-4:].hex()}")
PYEOF

scripts/test_record_replay.sh

.sh 131 lignes - 5,5KB

scripts/test_record_replay.sh
#!/bin/bash
# test_record_replay.sh — diagnostic determinisme : record live + N replays
#
# Workflow :
#   1. RECORD : run normal avec BSP_DUMP_RX_FILE=/tmp/bsp_rx.dump
#   2. REPLAY 1/2/3 : runs avec CALYPSO_BSP_REPLAY_FILE=/tmp/bsp_rx.dump
#   3. Affiche tableau comparatif des signatures d_fb_det / SOFT-RESET-TRIG /
#      BURST-IN / etc. pour decider : determinisme restaure (= course feed) ou
#      encore nondeterministe (= autre source).
#
# Usage : ./test_record_replay.sh [duration_sec]    (defaut 30)
#
# Hypotheses :
#   - run.sh est dans le PATH ou /root/run.sh
#   - CALYPSO_NO_ATTACH=1 supporte par run.sh pour spawn en background

set -u
DUR="${1:-30}"
DUMP=/tmp/bsp_rx.dump
LOGDIR=/tmp/record_replay_logs
mkdir -p "$LOGDIR"
RUN_SH="${RUN_SH:-${QEMU_TREE}/run.sh}"
QEMU_LOG="${QEMU_LOG:-/root/qemu.log}"

# ---- helper : kill toute la stack et nettoyer ----
killall_run() {
    tmux kill-session -t calypso 2>/dev/null || true
    killall -9 qemu-system-arm osmo-bts-trx mobile osmocon osmo-trx-ipc 2>/dev/null || true
    pkill -9 -f bridge.py 2>/dev/null || true
    pkill -9 -f calypso-ipc-device 2>/dev/null || true
    pkill -9 -f irda_capture.py 2>/dev/null || true
    sleep 1
}

# ---- helper : lance un run en background avec env, attend DUR secondes ----
do_run() {
    local label="$1"; shift
    echo "===== $label : start (timeout=${DUR}s) ====="
    killall_run

    # CALYPSO_NO_ATTACH=1 : run.sh exit apres tmux session lance en bg
    CALYPSO_NO_ATTACH=1 "$@" bash "$RUN_SH" > "$LOGDIR/$label.stdout" 2>&1 &
    local launcher_pid=$!

    # Attente DUR secondes, qemu.log se remplit
    sleep "$DUR"

    # Snapshot qemu.log et kill stack
    cp -f "$QEMU_LOG" "$LOGDIR/$label.qemu.log" 2>/dev/null || true
    killall_run
    wait "$launcher_pid" 2>/dev/null || true
    echo "===== $label : done, log=$LOGDIR/$label.qemu.log ($(wc -l < $LOGDIR/$label.qemu.log 2>/dev/null || echo 0) lines) ====="
    echo
}

# ---- helper : extrait une signature compact d'un qemu.log ----
sig() {
    local log="$1"
    if [ ! -s "$log" ]; then
        echo "  (vide ou inexistant)"
        return
    fi
    local boot_redirect soft_reset_trig fb_wr_total fb_wr_dom_pc fb_wr_dom_val
    local latch_consume_dom burst_mod burst_tonal burst_silent
    local task5 task6 sync_demod fbsb_state imr_zero last_insn

    boot_redirect=$(grep -c 'SILICON-BOOT-REDIRECT' "$log")
    soft_reset_trig=$(grep -c 'SOFT-RESET-TRIG' "$log")
    fb_wr_total=$(grep -c 'FB-DET-WR' "$log")
    fb_wr_dom_pc=$(grep 'FB-DET-WR' "$log" | grep -oE 'PC=0x[0-9a-f]+' | sort | uniq -c | sort -rn | head -1 | awk '{print $2"×"$1}')
    fb_wr_dom_val=$(grep 'FB-DET-WR' "$log" | grep -oE '<- 0x[0-9a-f]+' | sort | uniq -c | sort -rn | head -1 | awk '{print $3"×"$1}')
    latch_consume_dom=$(grep 'LATCH-CONSUME' "$log" | grep -oE '= 0x[0-9a-f]+' | sort | uniq -c | sort -rn | head -1 | awk '{print $3"×"$1}')
    burst_mod=$(grep 'BURST-IN' "$log" | grep -c MODULATED)
    burst_tonal=$(grep 'BURST-IN' "$log" | grep -c TONAL_FB)
    burst_silent=$(grep 'BURST-IN' "$log" | grep -c SILENT)
    task5=$(grep 'on_dsp_task_change' "$log" | grep -c 'task=5')
    task6=$(grep 'on_dsp_task_change' "$log" | grep -c 'task=6')
    sync_demod=$(grep '\[fbsb\]' "$log" | grep -oE 'last\(snr=[0-9]+ toa=[0-9]+ ang=[0-9]+ pm=[0-9]+\)' | sort | uniq | head -3 | tr '\n' ' | ')
    fbsb_state=$(grep '\[fbsb\]' "$log" | grep -oE 'state=[A-Z_0-9]+' | sort | uniq -c | tr '\n' ' ')
    imr_zero=$(grep -c 'IMR-W \*ZERO\*' "$log")
    last_insn=$(grep -oE 'insn=[0-9]+' "$log" | grep -oE '[0-9]+' | sort -n | tail -1)

    printf "  SILICON-BOOT-REDIRECT : %s\n" "$boot_redirect"
    printf "  SOFT-RESET-TRIG       : %s\n" "$soft_reset_trig"
    printf "  FB-DET-WR total       : %s\n" "$fb_wr_total"
    printf "  FB-DET-WR dom PC      : %s\n" "$fb_wr_dom_pc"
    printf "  FB-DET-WR dom val     : %s\n" "$fb_wr_dom_val"
    printf "  LATCH-CONSUME dom     : %s\n" "$latch_consume_dom"
    printf "  BURST-IN cat (M/T/S)  : %s / %s / %s\n" "$burst_mod" "$burst_tonal" "$burst_silent"
    printf "  ARM task=5 / task=6   : %s / %s\n" "$task5" "$task6"
    printf "  fbsb state distrib    : %s\n" "$fbsb_state"
    printf "  a_sync_demod uniq     : %s\n" "$sync_demod"
    printf "  IMR-W ZERO            : %s\n" "$imr_zero"
    printf "  Last insn_count       : %s\n" "$last_insn"
}

# ---- 1. RECORD ----
rm -f "$DUMP"
do_run RECORD env BSP_DUMP_RX_FILE="$DUMP"

if [ ! -s "$DUMP" ]; then
    echo "!!! RECORD failed: dump '$DUMP' vide ou absent (taille $(stat -c%s "$DUMP" 2>/dev/null || echo 0))"
    echo "Verifier qu'au moins 1 burst a ete livre au DSP (gate BDLENA)."
    exit 1
fi
echo ">>> RECORD dump size: $(stat -c%s "$DUMP") bytes"
echo

# ---- 2. REPLAY 1/2/3 ----
for i in 1 2 3; do
    do_run REPLAY$i env CALYPSO_BSP_REPLAY_FILE="$DUMP"
done

# ---- 3. Comparaison ----
echo "##############################################"
echo "##       SIGNATURE COMPARISON TABLE         ##"
echo "##############################################"
for label in RECORD REPLAY1 REPLAY2 REPLAY3; do
    echo "----- $label -----"
    sig "$LOGDIR/$label.qemu.log"
    echo
done

echo "##############################################"
echo "## Interpretation :                          ##"
echo "## - REPLAY 1/2/3 identiques => determinisme ##"
echo "##   retabli avec feed fixe => course feed   ##"
echo "##   est la root cause                       ##"
echo "## - REPLAY 1/2/3 differents => nondeter-    ##"
echo "##   minisme ailleurs (DSP/host thread/etc)  ##"
echo "##############################################"

tests/run_tests.sh

.sh 32 lignes - 1,5KB

tests/run_tests.sh
#!/bin/bash
# Runner tests Calypso — pile VIVANTE (lit /root/qemu.log, /root/osmocon.log…).
# COMPLET : tout tests/*.py (calypso + génériques top-level), on IGNORE seulement
# functional/ + guest-debug/ (suite d'intégration QEMU : binaires/downloads absents).
# --continue-on-collection-errors = ne bloque jamais.
# Sortie EN DIRECT (pytest -v + tee, line-buffered) — chaque test s'affiche au fil.
# Génère grafcet.html/md + rapport_final.pdf via hook conftest::pytest_sessionfinish.
#
# Usage : run_tests.sh [loop]
set -u
PY="${CALYPSO_PY:-/root/.env/bin/python3}"
TESTS=${QEMU_TREE}/tests
OUT="${CALYPSO_TEST_OUT:-/root/test_reports}"
PERIOD="${CALYPSO_TEST_PERIOD:-90}"
mkdir -p "$OUT"

run_once() {
  echo "==================== pytest COMPLET (pile vivante) $(date +%H:%M:%S) ===================="
  ( cd "$TESTS" && CALYPSO_TEST_OUT="$OUT" \
      stdbuf -oL -eL "$PY" -m pytest -v -ra --color=yes \
        -p no:cacheprovider --continue-on-collection-errors \
        --ignore=functional --ignore=guest-debug --ignore=migration \
        --ignore=qemu-iotests 2>&1 | stdbuf -oL tee "$OUT/pytest_last.log" )
  echo "-> $OUT/{grafcet.html, grafcet.md, rapport_final.pdf}  (log: $OUT/pytest_last.log)"
}

if [ "${1:-}" = "loop" ]; then
  echo "[run_tests] LOOP mode, période=${PERIOD}s (Ctrl-C pour stop) — sortie en direct"
  while true; do run_once; echo "[run_tests] prochain run dans ${PERIOD}s…"; sleep "$PERIOD"; done
else
  run_once
fi

tmux_modules/bench.sh

.sh 37 lignes - 1,9KB

tmux_modules/bench.sh
TMUX_FENETRE_PREMIERE=radio
TMUX_RESUME="6 fenêtres : radio · coeur · ms1 · ms2 (side-car) · voix · shell"

tmux_layout_premiere() {
    printf "tail -n 200 -F '%s/qemu.log' 2>/dev/null | stdbuf -oL tr -d '\\\\007' || sleep infinity" \
           "${LOG_DIR}"
}

tmux_layout() {
    local L="${LOG_DIR}"
    tmux select-pane -t "$TMUX_SESSION:radio" -T \
        "QEMU | baseband emule (ARM7, couche 1 gr-gsm) - traces du modele" 2>/dev/null
    _paint radio "$C_RADIO"
    _split radio "osmocon | lien serie firmware - romload puis transport L1CTL" \
        "$(_tail "'$L/osmocon.log'")" "$C_RADIO"
    _split radio "pont | transceiver du BTS#0 - bursts TRXD 5700-5702 <-> QEMU 4730/4731" \
        "$(_tail "'/dev/shm/pont.log'")" "$C_RADIO"
    _fenetre_coeur
    _w     ms1 "MS#1 | pile L2/L3 du telephone emule - camp, LU, appels" \
        "$(_tail "'$L/mobile.log'")" "$C_MS1"
    _split ms1 "BTS#0 | sa station de base - ARFCN ${PONT_ARFCN:-514}, unit-id 6001" \
        "$(_tail "'$L/bts.log'")" "$C_MS1"
    _split ms1 "GSMTAP | capture pcap - a ouvrir dans Wireshark" \
        "$(_tail "'$L/gsmtap.log'")" "$C_MS1"
    _w     ms2 "MS#2 | second abonne - campe sur le BTS#1, appelle le MS#1" \
        "$(_tail "'$L/sidecar-mobile.log'")" "$C_MS2"
    _split ms2 "trxcon | sa couche 1 - TRXD <-> L1CTL (remplace le firmware)" \
        "$(_tail "'$L/sidecar-trxcon.log'")" "$C_MS2"
    _split ms2 "BTS#1 | seconde station - ARFCN ${SC_ARFCN:-516}, unit-id ${SC_UNIT_ID:-6002}" \
        "$(_tail "'$L/sidecar-bts.log'")" "$C_MS2"
    _split ms2 "fake_trx | commutateur de bursts UDP ${SC_TRX_PORT:-5720} <-> ${SC_BB_PORT:-6720}" \
        "$(_tail "'$L/sidecar-faketrx.log'")" "$C_MS2"
    _fenetre_voix
    _fenetre_shell \
        "MS#1  : telnet 127.0.0.1 4247   puis  show ms" \
        "MS#2  : telnet 127.0.0.1 ${SC_MOBILE_VTY_PORT:-4248}   puis  call 1 10001"
}

tmux_modules/_commun.sh

.sh 97 lignes - 4,0KB

tmux_modules/_commun.sh
C_RADIO=colour81
C_COEUR=colour114
C_MS1=colour179
C_MS2=colour215
C_VOIX=colour176
SUIVRE="tail -n 100 -F"
: "${OSMO_LOG_DIR:=/var/log/osmocom}"

_paint() {
    [ -n "${2:-}" ] || return 0
    tmux select-pane -t "$TMUX_SESSION:$1" -P "fg=$2" 2>/dev/null || true
}

_w() {
    tmux new-window -t "$TMUX_SESSION" -n "$1" "$3" 2>/dev/null \
        && tmux select-pane -t "$TMUX_SESSION:$1" -T "$2" 2>/dev/null \
        && _paint "$1" "${4:-}"
}

_split() {
    tmux split-window -t "$TMUX_SESSION:$1" "$3" 2>/dev/null \
        && tmux select-pane -t "$TMUX_SESSION:$1" -T "$2" 2>/dev/null \
        && _paint "$1" "${4:-}" \
        && tmux select-layout -t "$TMUX_SESSION:$1" tiled 2>/dev/null
}

_tail() {
    printf "%s %s 2>/dev/null | stdbuf -oL tr -d '\\\\007' || sleep infinity" \
           "$SUIVRE" "$*"
}

_fenetre_coeur() {
    local O="${OSMO_LOG_DIR:-/var/log/osmocom}"
    _w     coeur "MSC | commutation circuit - appels, SMS, authentification" \
        "$(_tail "'$O/osmo-msc.log'")" "$C_COEUR"
    _split coeur "BSC | controleur de stations - allocation des canaux" \
        "$(_tail "'$O/osmo-bsc.log'")" "$C_COEUR"
    _split coeur "HLR | registre des abonnes - IMSI, cles, souscriptions" \
        "$(_tail "'$O/osmo-hlr.log'")" "$C_COEUR"
    _split coeur "MGW + STP | media MGCP/RTP et routage SS7" \
        "$(_tail "'$O/osmo-mgw.log' '$O/osmo-stp.log'")" "$C_COEUR"
}

_fenetre_voix() {
    local L="${LOG_DIR}" O="${OSMO_LOG_DIR:-/var/log/osmocom}"
    _w     voix "gapk | transcodage GSM-FR <-> PCM (actif pendant un appel)" \
        "$(_tail "'$L/gapk-auto.log'")" "$C_VOIX"
    _split voix "SIP | passerelle MNCC <-> SIP vers Asterisk" \
        "$(_tail "'$O/osmo-sip-connector.log'")" "$C_VOIX"
}

_fenetre_shell() {
    local aide=""
    local l; for l in "$@"; do aide="${aide}  ${l}\\n"; done
    _w shell "shell | depot qosmo-grgsm" \
        "cd '${QEMU_TREE:-.}' && printf '\n  %s\n  %s\n${aide}\n' \
             'etat  : ./run.sh --status' 'arret : ./run.sh --stop' \
         && exec ${SHELL:-/bin/bash}"
}

_tmux_env_repropre() {
    command -v tmux >/dev/null 2>&1 || return 0
    tmux show-environment -g 2>/dev/null \
        | sed -n 's/^\(CALYPSO_[A-Za-z0-9_]*\)=.*/\1/p' \
        | while IFS= read -r _v; do
              [ -n "$_v" ] && tmux set-environment -g -u "$_v" 2>/dev/null
          done
    unset _v
}

_tmux_style() {
    local s="$TMUX_SESSION"
    tmux set-environment -t "$s" LANG   "${LANG:-C.UTF-8}"   2>/dev/null
    tmux set-environment -t "$s" LC_ALL "${LC_ALL:-C.UTF-8}" 2>/dev/null
    tmux set -t "$s" -g mouse on            2>/dev/null
    tmux set -t "$s" -g base-index 1        2>/dev/null
    tmux set -t "$s" -g pane-base-index 1   2>/dev/null
    tmux set -t "$s" -g history-limit 20000 2>/dev/null
    tmux set -t "$s" -g renumber-windows on 2>/dev/null
    tmux set -t "$s" -g bell-action none  2>/dev/null
    tmux set -t "$s" -g visual-bell off   2>/dev/null
    tmux set -t "$s" -g monitor-bell off  2>/dev/null
    tmux set -t "$s" -g status-style        "bg=colour234,fg=colour250" 2>/dev/null
    tmux set -t "$s" -g status-left-length  40 2>/dev/null
    tmux set -t "$s" -g status-left \
        "#[bg=colour31,fg=colour15,bold] calypso #[default] " 2>/dev/null
    tmux set -t "$s" -g status-right-length 76 2>/dev/null
    tmux set -t "$s" -g status-right \
        "#[fg=colour215]gr-gsm#[fg=colour244] · #[fg=colour114]pont ARFCN ${PONT_ARFCN:-?} BSIC ${PONT_BSIC:-?}#[fg=colour244] · #[fg=colour81]${ENCRYPTION:-?}#[default]  #[fg=colour250]%H:%M " 2>/dev/null
    tmux set -t "$s" -g window-status-format         " #I #W " 2>/dev/null
    tmux set -t "$s" -g window-status-current-format \
        "#[bg=colour31,fg=colour15,bold] #I #W #[default]" 2>/dev/null
    tmux set -t "$s" -g pane-border-style        "fg=colour238" 2>/dev/null
    tmux set -t "$s" -g pane-active-border-style "fg=colour31"  2>/dev/null
    tmux set -t "$s" -g pane-border-status top 2>/dev/null
    tmux set -t "$s" -g pane-border-format " #{pane_title} " 2>/dev/null
}