Chapter 11 開発者サポート (Developer Support)

# この記事について
[Mastering the FreeRTOS Real Time Kernel-A Hands-On Tutorial Guildの日本語訳](https://qiita.com/azuki_bar/items/91e12949737f460dbc11)

# 重要と思われるポイント

# Chapter 11 開発者サポート (Developer Support)
## 11.1 イントロダクションとスコープ
このチャプターでは生産性の最大化を目的としてフィーチャを紹介する

- アプリケーションタの振る舞いを観察する方法
- 最適化の方法
- エラー発生時のエラー検出

## 11.2 configASSERT()
C言語では、assert()はプログラムによって作られるassertionをverifyするために使われる。このassertionはCで記述され、falseと評価されると、assertionはfailする。例えば、Listing163ではpxMyPointerがNULLでないことをテストする。

```c:Listing163
/* Test the assertion that pxMyPointer is not NULL */ 
assert( pxMyPointer != NULL ); 
```

アプリケーション開発者は、assert()マクロの実装で、assertionがfailしたときのアクションを明確にする必要がある。

FreeRTOSソースコードではassert()は呼ばない。assert()はFreeRTOSがコンパイルするすべての環境で使えるわけではないためだ。代わりにFreeRTOSソースコードは多くのconfigASSERTを含んでいる。これはアプリケーション開発者がFreeRTOSConfig.hで定義して、標準Cのような振る舞いをさせることができる。

失敗したassertionはfatal errorとして扱う必要がある。assertionがfailしてから、その後のコードを実行すべきではない。

configASSERTは多くのエラーを検出、認識することで開発生産性を挙げることができる。FreeRTOSアプリケーションをデバッグする間は、configASSERTをを定義することを強く勧める。

configASSERTの定義は、ラインタイムデバッグの大きな助けとなるが、アプリケーションコードサイズを増加させ、処理時間が悪化する。configASSERT()が定義されていないと、デフォルトの空関数が使われ、Cプリプロセッサによって、除去される

### configASSERTの定義例
デバッガ実行に有効なconfigASSERT()の定義をListing164に示す。assertionがfailした行で実行を止めて、デバッグ実行がポーズしたときにfailした行を表示する。

```c:Listing164
/* Disable interrupts so the tick interrupt stops executing, then sit in a loop so 
execution does not move past the line that failed the assertion.  If the hardware 
supports a debug break instruction, then the debug break instruction can be used in 
place of the for() loop. */ 
#define configASSERT( x )  if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for(;;); } 
```

165の例では、デバッガのコントロール下ではないときの例となる。failした行をprintまたはrecordする。assertionがfailしたファイル名、行はC標準の__FILE__, __LINE__で取得することができる。

```c:Listing165
/* This function must be defined in a C source file, not the FreeRTOSConfig.h header 
file. */ 
void vAssertCalled( const char *pcFile, uint32_t ulLine ) 
{ 
    /* Inside this function, pcFile holds the name of the source file that contains  
    the line that detected the error, and ulLine holds the line number in the source  
    file.  The pcFile and ulLine values can be printed out, or otherwise recorded,  
    before the following infinite loop is entered. */ 
    RecordErrorInformationHere( pcFile, ulLine ); 
 
    /* Disable interrupts so the tick interrupt stops executing, then sit in a loop   
    so execution does not move past the line that failed the assertion. */ 
    taskDISABLE_INTERRUPTS(); 
    for( ;; ); 
} 
/*-----------------------------------------------------------*/ 
 
/* These following two lines must be placed in FreeRTOSConfig.h. */ 
extern void vAssertCalled( const char *pcFile, uint32_t ulLine ); 
#define configASSERT( x )  if( ( x ) == 0 ) vAssertCalled( __FILE__, __LINE__ ) 
```

## 11.3 FreeRTOS+Trace
FreeRTOS+Traceは、Percepio社から提供される、ラインタイムの診断、最適化ツールである。

FreeRTOS+Traceは重要な動的振る舞いの情報を取得して、グラフィカルに表示することができる。また、複数のViewを同期して表示することもできる。

FreeRTOS+Traceは従来のデバッガーと共存して、高レベルの時間軸をとった視点で補完することができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540259/f2de42ad-c611-8aab-f997-c3c4ed4a2995.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540259/6b309c93-6c16-ecf3-972d-7ecf93ba0c00.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540259/28055b3e-e8b6-f03d-8f8a-5c09d3c173b4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540259/1c33c3e0-8949-b202-6059-55e0132f2d92.png)

## 11.4 デバッグ関連のフック（コールバック）関数
### Malloc失敗フック関数
malloc失敗ふーっくはChapter2で説明した。

malloc failed hookは、task, queue, semaphore, event groupの生成に失敗したときに、開発者がすぐ気づくことができる。

### Stack Overflowフック関数
詳細は12.3で説明する。
stack overflow hookは、タスクに割り当てられたサイズを超えてスタックを使うと、開発者がすぐ気づくことができる。

## 11.5 ランタイムのタスク情報を見る
### タスクランタイム統計
タスクランタイム統計情報は、各タスクの処理時間になる。Aタスクのラインタイムは、アプリケーションがブートしてからRunning状態であった時間の合計となる。

ランタイム統計は、開発フェーズでのプロファイリング、デバッグ補助として使われる。この情報はランタイム統計クロックがオーバーフローするまで有効となる。ランタイム統計の収集はタスクのコンテキストスイッチ時間を増大させる。

バイナリのランタイム情報を取得するには、uxTaskGetSystemStateを呼ぶ。ASCCIで取得するにはvTaskGetRunTimeStatsを呼ぶ。

### ランタイム統計クロック
ランタイム統計はティック周期の分割を測定する必要がある。そのため、RTOS tick countはランタイム統計クロックとしては使われない。代わりにクロックはアプリケーションコードから提供される。tick interruptより、10から100倍速い周波数が推奨されている。速いほど詳細な統計が取れるが、オーバーフローも早くなる。

その時間情報は、自由に動作する32bitのペリフェラルタイマー、カウンターで生成され、Readするのにオーバーヘッドが生じないことが理想的である。利用できるペリフェラル、クロックスピードがこれを満たせない時は、代わりに効率性は落ちるが以下のテクニックがある

*1.*
求めるランタイム統計のクロック周期割り込みを生成するためにペリフェラルを設定する。生成された割り込みの数を、ランタイム統計クロックとして用いる。
この割り込みが統計情報のみに使われる場合は、この方法はとても非効率になる。しかし、アプリケーションがすでに周期割り込みを持っているのであれば、シンプルで効率的に存在するISRにカウントする仕組みをいれるだけとなる。

*2.*
free-runningする16bitペリフェラルタイマーを32bitの下位16bitとして使う。タイマーがオーバーフローした数を32bitの上位16bitとして扱う。

RTOS tick countとARM SysTickタイマーを組み合わせて、統計情報クロックを生成することが可能である（適切である程度複雑な）。これを達成するためのデモがダウンロードできる。

### ランタイム統計を収集するための設定
Table54にランタイム統計を収集するために必要なマクロ詳細を示す。RTOSポートレイヤーに含まれるマクロなので、prefixで"port"が入っている。しかし、FreeRTOSConfig.hで定義したほうが実践的だと確認されている。

|Macro|説明|
|:--|:--|
|configGENERATE_RUN_TIME_STATS|FreeRTOSConfig.hで1に設定する。1の場合、スケジューラは適切なタイミングで、この表で説明される他のマクロを呼び出す|
|portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()|ランタイム統計クロックに使われるペリフェラルを初期化するために使われる|
|portGET_RUN_TIME_COUNTER_VALUE(), or portALT_GET_RUN_TIME_COUNTER_VALUE(Time) |このマクロのうち１つが、現在のランタイムクロック値を返す。これはアプリケーションが実行された総時間となる。最初のマクロは現在のクロック値評価に使われ、２つ目は'Time'パラメータを現在のクロックにセットするために使われる|

### uxTaskGetSystemState API Function
uxTaskGetSystemStateは、FreeRTOSスケジューラによって管理されている各タスクのステータス情報スナップショットを提供する。この情報はTaskstatus_tの配列で提供され、各タスク１つのindexを使っている。TaskStatus_tはListing167, Table56で示す。

```c:Listing166
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray, 
                                  const UBaseType_t uxArraySize, 
                                  uint32_t * const pulTotalRunTime ); 
```

|パラメータ名|説明|
|:--|:--|
|pxTaskStatusArray|TaskStatu_t配列へのポインタ。この配列は各タスクためのTaskStatus_t structureが少なくも１つ含まれている必要がある。タスクの数はuxTaskGetNumberOfTasksで確認できる。TaskStatus_t構造体はListing167で示す。メンバーはtable56で示す|
|uxArraySize|pxTaskStatusArrayが指すポインタの配列サイズ。このサイズは配列のバイトサイズではなく、配列のインデックス数を設定する。|
|pulTotalRunTime| configGENERATE_RUN_TIME_STATS が1の場合、*pulTotalRunTimeはuxTaskGetSystemStateによって、ブートしてからの総実行時間がセットされる。これはオプション設定で、不要であればNULLを設定する|
|戻り値|TaskStatus_t構造体の数。戻り値は、uxTaskGetNumberOfTasksでリターンされる値と等しいはずだが、uxArraySizeが小さすぎる場合は0が返る|

```c:Listing167
typedef struct xTASK_STATUS 
{ 
    TaskHandle_t xHandle; 
    const char *pcTaskName; 
    UBaseType_t xTaskNumber; 
    eTaskState eCurrentState; 
    UBaseType_t uxCurrentPriority;     
    UBaseType_t uxBasePriority; 
    uint32_t ulRunTimeCounter;     
    uint16_t usStackHighWaterMark; 
} TaskStatus_t; 
```

|パラメータ名|説明|
|:--|:--|
|xHandle|この構造体に紐づけるタスクのhandle|
|pcTaskName|タスク名の人間が読めるテキスト|
|xTaskNumber|各タスクはユニークなxTaskNumberを持つ。アプリケーションがタスクをcreate, deleteすると、以前にdeleteされた同じhandleをcreateすることができる。xTaskNumberはアプリケーションコード、kenel aware debuggerに、有効なタスクとdeleteされた同じhandleを持つタスクを区別させる。|
|eCurrentState|taskの状態を示すenum。eRunnning, eReady, eBlocked, eSuspended, eDeletedがある。eDeleted状態は、vTaskDeleteをcallされてからidleタスクがdeleteされたtaskの内部メモリ、スタックをfreeするまでの短い間でのみ検出が可能となる。その後はタスクはすでに存在しないので、ハンドルが無効になる。|
|uxCurrentPriority|xxTaskGetSystemStateが呼ばれた時点のタスク優先度。uxCurrentPriorityはアプリケーション開発者によって設定された優先度が基本だが、mutexの優先度継承によって一時的に優先度が変更される場合がある。|
|uxBasePriority|アプリケーション開発が設定した優先度。configUSE_MUTEXESが1にセットされているときのみ有効|
|ulRunTimeCounter|タスクが生成されてから動作したトータル時間。ランタイム統計クロックによって測定される。ulRunTimerCounterはconfigGENERATE_RUN_TIME_STATSが1の場合のみ有効となる|
|usStackHighWaterMark|タスクが生成されてから、最小となったスタック送料。バイトで表されて、0に近づくほどスタックオーバーフローに近い状態となる|

### vTaskList Helper Functikon
vTaskList()はuxTaskGetSystemStateと似たようなタスクステータス情報を提供するが、バイナリではなくASCCIで提供する。

vTaskList()はとてもプロセッサ処理が集中する関数で、スケジューラを一定期間停止させる。そのため、デバッグ目的のみで使用して、リアルタイム組み込みシステムでは使わないことが推奨される。

vTaskList()はconfigUSE_TRACE_FACILITY, configUSE_STATS_FORMATTING_FUNCTIONSが1で利用可能となる。

```c:Listing168
void vTaskList( signed char *pcWriteBuffer ); 
```

|パラメータ名|説明|
|:--|:--|
|pcWriteBuffer|文字バッファへのポインタ。すべてのテーブルが書ききれる大きさである必要がある。境界チェックは行われない|

Figure88にvTaskListの出力例を示す。出力は、

- 各行に１つのタスク情報が出力される
- 最初の列はタスク名
- ２列目はタスクの状態で R:Ready B:Blocked S:Suspended D:Deletedとなる。DeletedはvTaskDeleteしてからIdleタスクがタスクのメモリをfreeするまでの短い間だけレポートされる状態である。
- ３列目はタスクの優先度
- ４列目はタスクのstack high water mark。Table56のusStackHighWaterMake参照
- ５列目はタスクのユニーク番号。Table56のxTaskNumber参照

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540259/b2700d3e-10f2-9391-5872-ff7768185832.png)

### vTaskGetRunTimeStats Helper Function
vTaskGetRunTimeStats()は収集したランタイム統計をASCIIに変換する。

vTaskGetRunTimeStas()はとてもプロセッサ処理が集中する関数で、スケジューラを一定期間停止させる。そのため、デバッグ目的のみで使用して、リアルタイム組み込みシステムでは使わないことが推奨される。

vTaskGetRunTimeStatsは、configGENERATE_RUN_TIME_STATS, configUSE_STATS_FORMATTING_FUNCTIONを1にすると利用可能になる。

```c:Listing168
void vTaskGetRunTimeStats( signed char *pcWriteBuffer ); 
```

|パラメータ名|説明|
|:--|:--|
|pcWriteBuffer|文字バッファへのポインタ。すべてのテーブルが書ききれる大きさである必要がある。境界チェックは行われない|

vTaskGetRunTimesStatsの出力例をFigure89に示す。出力は

- 各行は１つのタスクを示す
- １行目はタスク名
- ２行目はRunning状態の総時間。table56 ulRunTimeCounterを参照。
- ３行目は、ターゲットがブートされてから、タスクがRunnnig状態であった割合を示す。切り捨てで計算されるので、合計は通常100%より小さい値となるとなる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540259/a8967b7e-d510-0c44-994f-defb95ddb0f1.png)

### ランタイム統計の出力例
この例では仮想的な16bitタイマーを32bitランタイム統計クロック生成に使う。16bitが最大値に達すると割り込みが入るようになっている。このISRはオーバーフロー回数をカウントする。

32bit値はオーバーフローの発生回数を使って生成され、32bitの上位2byteとして扱われる。16bitカウンタは下位2byteになる。疑似コードをListing170に示す

```c:Listing170
void TimerOverflowInterruptHandler( void ) 
{ 
    /* Just count the number of interrupts. */ 
    ulOverflowCount++; 
 
    /* Clear the interrupt. */ 
    ClearTimerInterrupt(); 
} 
```

Listing171はランタイム統計を有効にするために、FreeRTOSConfig.hに行を追加している。

```c:Listing171
/* Set configGENERATE_RUN_TIME_STATS to 1 to enable collection of run-time 
statistics.  When this is done, both portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() and 
portGET_RUN_TIME_COUNTER_VALUE() or portALT_GET_RUN_TIME_COUNTER_VALUE(x) must also 
be defined. */ 
#define configGENERATE_RUN_TIME_STATS  1 
 
/* portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() is defined to call the function that sets  
up the hypothetical 16-bit timer (the function’s implementation is not shown). */ 
void vSetupTimerForRunTimeStats( void ); 
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() vSetupTimerForRunTimeStats() 
 
/* portALT_GET_RUN_TIME_COUNTER_VALUE() is defined to set its parameter to the 
current run-time counter/time value.  The returned time value is 32-bits long, and is 
formed by shifting the count of 16-bit timer overflows into the top two bytes of a 
32-bit number, then bitwise ORing the result with the current 16-bit counter  
value. */ 
#define portALT_GET_RUN_TIME_COUNTER_VALUE( ulCountValue )                   \ 
    {                                                                        \ 
    extern volatile unsigned long ulOverflowCount;                           \ 
                                                                             \ 
        /* Disconnect the clock from the counter so it does not change       \ 
        while its value is being used. */                                    \ 
        PauseTimer();                                                        \ 
                                                                             \ 
        /* The number of overflows is shifted into the most significant      \ 
        two bytes of the returned 32-bit value. */                           \ 
        ulCountValue = ( ulOverflowCount << 16UL );                          \ 
                                                                             \ 
        /* The current counter value is used as the least significant        \ 
        two bytes of the returned 32-bit value. */                           \ 
        ulCountValue |= ( unsigned long ) ReadTimerCount();                  \ 
                                                                             \ 
        /* Reconnect the clock to the counter. */                            \ 
        ResumeTimer();                                                       \ 
    } 
```

Listing172のタスクは５行ごとにランタイム統計をプリントしている。

```c:Listing172
/* For clarity, calls to fflush() have been omitted from this code listing. */ 
static void prvStatsTask( void *pvParameters ) 
{ 
TickType_t xLastExecutionTime; 
 
/* The buffer used to hold the formatted run-time statistics text needs to be quite 
large.  It is therefore declared static to ensure it is not allocated on the task 
stack.  This makes this function non re-entrant. */ 
static signed char cStringBuffer[ 512 ];  
 
/* The task will run every 5 seconds. */ 
const TickType_t xBlockPeriod = pdMS_TO_TICKS( 5000 ); 
 
    /* Initialize xLastExecutionTime to the current time.  This is the only time this  
    variable needs to be written to explicitly.  Afterwards it is updated internally  
    within the vTaskDelayUntil() API function. */ 
    xLastExecutionTime = xTaskGetTickCount(); 
 
    /* As per most tasks, this task is implemented in an infinite loop. */ 
    for( ;; ) 
    { 
        /* Wait until it is time to run this task again. */ 
        vTaskDelayUntil( &xLastExecutionTime, xBlockPeriod ); 
 
        /* Generate a text table from the run-time stats.  This must fit into the  
        cStringBuffer array. */ 
        vTaskGetRunTimeStats( cStringBuffer ); 
         
        /* Print out column headings for the run-time stats table. */ 
        printf( "\nTask\t\tAbs\t\t\t%%\n" ); 
        printf( "-------------------------------------------------------------\n" ); 
         
        /* Print out the run-time stats themselves.  The table of data contains  
        multiple lines, so the vPrintMultipleLines() function is called instead of  
        calling printf() directly.  vPrintMultipleLines() simply calls printf() on  
        each line individually, to ensure the line buffering works as expected. */ 
        vPrintMultipleLines( cStringBuffer ); 
    } 
} 
```

## 11.6 Trace Hook Macros
トレースマクロは、FreeRTOSソースコード内に置かれているマクロである。デフォルトではマクロは空で、コードを生成しないためオーバーヘッドもない。実装をオーバーライドすることでアプリケーション開発者は以下が可能となる

- FreeRTOSソースコードを変更しないでコード挿入できる
- ターゲットハードウェアで利用可能な方法で、詳細な実行シーケンス情報を出力できる。トレースマクロはFreeRTOSソースコードに十分配置されており、詳細なスケジューラ動作トレース、プロファイリングログを生成することができる。

### Trace Hook Macros有効化
全てのマクロの詳細は説明しきれない。Table59にアプリケーション開発者にとって有効と思われるサブセットを示す。

Table59の多くの説明はpxCurrentTCBをコールする。pxCurrentTCBはFreeRTOSのプライベート変数で、Runnnig状態のタスクのhandleを保持すする。しそいて、FreeRTOS/Source/task.cのソースファイルから呼んだマクロで利用可能である。


|パラメータ名|説明|
|:--|:--|
|traceTASK_INCREMENT_TICI(xTickCount)|ティックカウントがインクリメントした後に、ティック割り込み中に呼ばれる。xTickCountはマクロに新しいティックカウントを渡す|
|traceTASK_SWITCHED_OUT()|動作する新しいタスクを選択するときに呼ばれる。この時にはpxCurrentTCBはRunning状態を抜けようとしているタスクのhandleを含む|
|traceTASK_SWITCHED_IN()|動作する新しいタスクを選択するときに呼ばれる。この時にはpxCurrentTCBはRunning状態になろうとするタスクのhandleを含む|
|traceBLOCKING_ON_QUEUE_RECEIVE(pxQueue)|現在実行されているタスクが空のキューをreadしようとする、または空のセマフォ、ミューテックスを取ろうとしてBlocked状態になる直前で呼ばれる。pxQueueパラメータは、そのqueue, semaphoreをマクロに渡す|
|traceBLOCKING_ON_QUEUE_SEND(pxQueue) |fullのqueueに送信しようとしてBlocked状態に入る直前に呼ばれる。pxQueueはそのqueueのhandleを渡す|
|traceQUEUE_SEND(pxQueue) |xQueueSend(), xQueueSendToFront(),xQueueSendToBack(), またはsemaphoreの ‘give’ functions内で呼ばれる。呼ばれるのはgiveが成功したときになる。pxQueueはそのhandleをマクロに渡す|
|traceQUEUE_SEND_FAILED(pxQueue) |xQueueSend(), xQueueSendToFront(),xQueueSendToBack(), またはsemaphoreの ‘give’ functions内で呼ばれる。呼ばれるのはgiveが失敗したときになる。pxQueueはそのhandleをマクロに渡す|
|traceQUEUE_RECEIVE(pxQueue) |xQueueRecieveや、semaphoreの 'take' 関数で呼ばれる。呼ばれるのはtakeが成功したときになる。pxQueueはそのhandleをマクロに渡す|
|traceQUEUE_RECEIVE_FAILED(pxQueue) |xQueueRecieveや、semaphoreの 'take' 関数で呼ばれる。呼ばれるのはtakeが失敗したときになる。pxQueueはそのhandleをマクロに渡す|
|traceQUEUE_SEND_FROM_ISR(pxQueue) |xQueueSendFromISRから呼ばれる。呼ばれるのはsendが成功したときにある。pxQueueはそのhandleをマクロに渡す|
|traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue) |xQueueSendFromISRから呼ばれる。呼ばれるのはsendが失敗したときにある。pxQueueはそのhandleをマクロに渡す|
|traceQUEUE_RECEIVE_FROM_ISR(pxQueue) |xQueueReceiveFromISRから呼ばれる。呼ばれるのはreceiveが成功したときにある。pxQueueはそのhandleをマクロに渡す|
|traceQUEUE_RECEIVE_FROM_ISR_FAILED(pxQueue)  |xQueueReceiveFromISRから呼ばれる。呼ばれるのはreceiveが失敗したときにある。pxQueueはそのhandleをマクロに渡す|
|traceTASK_DELAY_UNTIL() |vTaskDelayUntil()内で呼ばれる。呼んだタスクがBlocked状態に入る直前に呼ばれる|
|traceTASK_DELAY() |vTaskDelay()内で呼ばれる。呼んだタスクがBlocked状態に入る直前に呼ばれる|

### Trace Hook Macrosの定義
各トレースマクロは、デフォルトでは空実装となっている。FreeRTOSConfig.hで定義することでオーバーライドできる。トレースマクロが長く複雑になる場合は、新しいヘッダーに定義して、FreeRTOSConfig.hでincludeすればよい。

ソフトウェアエンジニアリングのベストプラクティスとしては、FreeRTOSは厳密なデータ隠ぺい方針を維持する必要がある。トレースマクロはユーザーにFreeRTOSへのソースコード追加を許すので、トレースマクロで見えているデータタイプとは、アプリケーションコードとは異なる。

- FreeRTOS/Source/tasks.cでは、タスクhandleはタスクを記述する構造体へのポインタとなる。tasks.cの外側では、タスクhandleはvoid型ポインタとなる。
- FreeRTOS/Source/queue.cでも同様

マクロから直接FreeRTOSのprivateなデータ構造にアクセスすることには、強い警告をする。privateデータ構造体はFreeRTOSのバージョンによって変更されることがあるからだ。

### FreeRTOS向けのデバッガプラグイン
プラグインは次のIDEで提供されている。
このリストは完全ではない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540259/0823cf47-7f20-ead7-fd55-62ab1e4ed9a4.png)