mirror of
https://github.com/espressif/esp-idf.git
synced 2024-10-05 20:47:46 -04:00
feat(apptrace): Added sensor data visualization example
Added example and python script to show how to transfer and visualize sensor data.
This commit is contained in:
parent
9f5271031d
commit
804c12646b
8
examples/system/app_trace_to_plot/CMakeLists.txt
Normal file
8
examples/system/app_trace_to_plot/CMakeLists.txt
Normal file
@ -0,0 +1,8 @@
|
||||
# For more information about build system see
|
||||
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
|
||||
# The following five lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(app_trace_to_plot)
|
138
examples/system/app_trace_to_plot/README.md
Normal file
138
examples/system/app_trace_to_plot/README.md
Normal file
@ -0,0 +1,138 @@
|
||||
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 |
|
||||
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- |
|
||||
|
||||
# Application Level Tracing Example (Plotting)
|
||||
|
||||
(See the README.md file in the upper level 'examples' directory for more information about examples.)
|
||||
|
||||
This example demonstrates how to use the [Application Level Tracing Library](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/app_trace.html#) (henceforth referred to as **App Trace**) to send and plot dummy sensor data to a host via JTAG instead of the normal method of logging via UART.
|
||||
|
||||
UART logs are time consuming and can significantly slow down the function that calls it. Therefore, it is generally a bad idea to use UART logs in time-critical functions. Logging to host via JTAG is significantly faster and can be used in time-critical functions. For more details regarding logging to host via JTAG, refer to the [Logging to Host Documentation](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/app_trace.html#app-trace-logging-to-host).
|
||||
|
||||
### Hardware Required
|
||||
|
||||
To run this example, you need a supported target dev board connected to a JTAG adapter, which can come in the following forms:
|
||||
|
||||
* [ESP-WROVER-KIT](https://docs.espressif.com/projects/esp-idf/en/latest/hw-reference/modules-and-boards.html#esp-wrover-kit-v4-1) which integrates an on-board JTAG adapter. Ensure that the [required jumpers to enable JTAG are connected](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/get-started-wrover-kit.html#setup-options) on the WROVER-KIT.
|
||||
* ESP32, ESP32-S2 or ESP32-C2 core board (e.g. ESP32-DevKitC, [ESP32-S2-Saola-1](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/hw-reference/esp32s2/user-guide-saola-1-v1.2.html)) can also work as long as you connect it to an external JTAG adapter (e.g. FT2232H, J-LINK).
|
||||
- For ESP32-C3 or ESP32-S3, any board with the built-in USB interface (USB_SERIAL_JTAG).
|
||||
|
||||
This example will assume that an ESP-WROVER-KIT is used.
|
||||
|
||||
#### Connections:
|
||||
|
||||
1. Connect the JTAG interface to the target board. For details about how to set up JTAG interface, please see [JTAG Debugging](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/jtag-debugging/index.html). Power up both the JTAG debugger and target board.
|
||||
|
||||
2. To start the tcp socket server, you need to run `read_trace.py` tool under the `esp-idf/examples/system/app_trace_to_plot` path.
|
||||
|
||||
3. After connecting JTAG interface and starting the tcp socket server, you need to [Run OpenOCD](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/jtag-debugging/index.html#run-openocd).
|
||||
|
||||
|
||||
### Configure the project
|
||||
|
||||
```
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||
* To enable application tracing, select the `(X) Trace memory` option under `Component config > Application Level Tracing`. This option should have been selected by default.
|
||||
|
||||
### Build, Flash, and Run
|
||||
|
||||
Build the project and flash it to the board, then run monitor tool to view serial output:
|
||||
|
||||
```
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(Replace PORT with the name of the serial port to use.)
|
||||
|
||||
**Run Plotting Tool** To plot data and open TCP socket, there is a tool named `read_trace.py` under the `examples/system/app_trace_to_plot` path. Run this tool in the terminal session with configured IDF development environment by entering the command below. This command opens a TCP socket and plots the given data when OpenOCD triggered to start App Trace. If you are running tool at first time, you need to install dash with `pip install dash` in the same terminal session after running configuring IDF development environment.
|
||||
|
||||
```bash
|
||||
python read_trace.py --plot-config data.json --source tcp://localhost:53535 --output-file data.log
|
||||
```
|
||||
|
||||
**Start App Trace:** Start OpenOCD and App Trace on the target by entering the command below. This command will start OpenOCD and collect all of the bytes from JTAG log data and send them to the tcp socket `tcp://localhost:53535` (note `tcp://` depends on which IP address and port number openned). Assuming that OpenOCD was started in this example's directory, `data.log` will be saved here as well.
|
||||
|
||||
After running the plotting tool and starting apptrace with OpenOCD separately, you need access plotting on related socket address. Default address is `http://127.0.0.1:8055/` and also address can be seen from `read_trace.py` output. You can see the plotting result by accessing the address from browser.
|
||||
|
||||
|
||||
```bash
|
||||
idf.py openocd --openocd-commands 'reset;esp apptrace start tcp://localhost:53535 0 -1 5'
|
||||
```
|
||||
|
||||
**Note:** data.json file is an example for plot config file. It can be changed or modified.
|
||||
|
||||
**Note:** For more details on OpenOCD commands regarding App Trace, refer to the [OpenOCD Application Level Tracing Commands](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/app_trace.html#openocd-application-level-tracing-commands)
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
|
||||
|
||||
### Configration file and data format
|
||||
|
||||
#### General format of json file is:
|
||||
|
||||
```JSON
|
||||
"Subplot Name": {
|
||||
"data_streams"
|
||||
"Name of the sensor": {
|
||||
"id": int
|
||||
"x_axis_data_size": int
|
||||
"x_axis_timestamp": bool
|
||||
"y_axis_data_size": int
|
||||
"data_type": "string"
|
||||
["precision": int]
|
||||
["type": "string"]
|
||||
["mode": "string"]
|
||||
["any_plot_config": Type defined in plotly]
|
||||
}
|
||||
}
|
||||
"xaxis_title": "string",
|
||||
"yaxis_title": "string"
|
||||
}
|
||||
```
|
||||
|
||||
#### Explanations of JSON keys are:
|
||||
|
||||
| JSON Keys | Explanation |
|
||||
|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| id | ID value of the incoming packet (e.g. 1, 2, 3) |
|
||||
| x_axis_data_size | Data size of x axis of the incoming packet in bytes (e.g 2, 4, 8) |
|
||||
| x_axis_timestamp | Will x axis data be timestamp value (e.g. true, false) |
|
||||
| y_axis_data_size | Data size of y axis of the incoming packet in bytes (e.g 2, 4, 8) |
|
||||
| data_type | Data type of y axis of the incoming packet (e.g. "int", "float", "double") |
|
||||
| precision | Optional. If y axis data of the incoming packet is a floating point number, you need to send it in integer form by multiplying a power of 10 and adjust this field (e.g. 1, 2, 3) |
|
||||
| type | Optional. Determines the plotting type of the graph (e.g. "scatter", "bar"). Default is "scatter" |
|
||||
| mode | Optional. Determines how values will look like (e.g. "lines", "markers", "lines+markers"). Default is "lines" |
|
||||
| xaxis_title | Title of x axis in the plot (e.g "time") |
|
||||
| yaxis_title | Title of y axis in the plot (e.g "values", "sensor data") |
|
||||
|
||||
**Note:** Plotting works with plotly subplot feature. `id`, `timestamp_flag`, `data_size` and `data_type` are the mandatory elements. In addition to these elements, you can config your plot with [properties are passed to the constructor of the specified trace type](https://plotly.com/python/reference/scatter/). Data transfering is in {'x': ..., 'y': ..., ...} format. Graph types like Pie or Scatter3D won't work. Scatter, histogram, box, line and similar graph types are working.
|
||||
|
||||
#### Data format in microcontroller side is:
|
||||
|
||||
```C
|
||||
struct format {
|
||||
char STX[5] = "esp32",
|
||||
uint32_t id,
|
||||
any_size y_axis_value,
|
||||
any_size x_axis_value
|
||||
char EXT = 0x03,
|
||||
}
|
||||
```
|
||||
**Note** STX and EXT fields are mandatory and constant to check data validity in visualizer side. You cannot change or remove these fields.
|
||||
|
||||
**Note** y_axis_value and x_axis_value could be in any value and different sizes like long, short, and int. The only thing to do is enter the correct data size in the config file.
|
||||
|
||||
**Note:** If you want to send a floating point number, send it in integer form by multiplying a power of 10 and filling the precision field in the config file.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Unable to flash when OpenOCD is connected to the target
|
||||
|
||||
On ESP32 boards, one likely cause would be an incorrect SPI flash voltage when starting OpenOCD. Suppose a target board/module with a 3.3 V powered SPI flash is being used, but the configuration file (ex. `board/esp32-wrover.cfg` for ESP32) is selected when starting OpenOCD which can set the SPI flash voltage to 1.8 V. In this situation, the SPI flash will not work after OpenOCD connects to the target as OpenOCD has changed the SPI flash voltage. Therefore, you might not be able to flash to the target when OpenOCD is connected.
|
||||
|
||||
To work around this issue, users are suggested to use `board/esp32-wrover.cfg` for ESP32 boards/modules operating with an SPI flash voltage of 1.8 V, and `board/esp-wroom-32.cfg` for 3.3 V. Refer to [ESP32 Modules and Boards](https://docs.espressif.com/projects/esp-idf/en/latest/hw-reference/modules-and-boards.html) and [Set SPI Flash Voltage](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/jtag-debugging/tips-and-quirks.html#why-to-set-spi-flash-voltage-in-openocd-configuration) for more details.
|
||||
|
||||
(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.)
|
72
examples/system/app_trace_to_plot/data.json
Normal file
72
examples/system/app_trace_to_plot/data.json
Normal file
@ -0,0 +1,72 @@
|
||||
{
|
||||
"//data.json": "Apptrace plotting config file for 'read_trace.py'",
|
||||
"//Summary and usage": "This file is used for apptrace plotting. You can use this json file with '--plot-config' parameter of the 'read_trace.py' file is to configure graphs for visualizing sensor data",
|
||||
"//More information": "To get more information about apptrace plotting and configuration file, please check 'app_trace_to_plot' example in '../examples/system' and 'read_trace.py' file",
|
||||
"Temperature sensors": {
|
||||
"data_streams" : {
|
||||
"Outside temperature": {
|
||||
"id": 1,
|
||||
"x_axis_data_size": 4,
|
||||
"x_axis_timestamp":true,
|
||||
"y_axis_data_size": 1,
|
||||
"data_type": "int",
|
||||
"opacity": 0.5,
|
||||
"line_color":"crimson",
|
||||
"marker_line_width":2,
|
||||
"marker_size":9,
|
||||
"mode": "lines+markers"
|
||||
},
|
||||
"Engine temperature": {
|
||||
"id": 2,
|
||||
"x_axis_data_size": 4,
|
||||
"x_axis_timestamp":true,
|
||||
"y_axis_data_size": 2,
|
||||
"data_type": "int",
|
||||
"line_color": "#0d0887",
|
||||
"mode": "lines+markers"
|
||||
}
|
||||
},
|
||||
"xaxis_title": "Timestamp",
|
||||
"yaxis_title": "Temperature in Celcius"
|
||||
},
|
||||
"Altitude sensors": {
|
||||
"data_streams" : {
|
||||
"Altitude": {
|
||||
"id": 3,
|
||||
"x_axis_data_size": 4,
|
||||
"x_axis_timestamp":true,
|
||||
"y_axis_data_size": 4,
|
||||
"data_type": "int",
|
||||
"fill": "tozeroy",
|
||||
"fillcolor": "gray",
|
||||
"hovertext": "(in meter)",
|
||||
"type": "scatter",
|
||||
"fillpattern": {
|
||||
"shape": "x"
|
||||
},
|
||||
"error_y": {
|
||||
"type":"percent",
|
||||
"value": 10
|
||||
},
|
||||
"mode": "markers"
|
||||
}
|
||||
},
|
||||
"xaxis_title": "Timestamp",
|
||||
"yaxis_title": "Altitude in meters"
|
||||
},
|
||||
"Pressure sensors": {
|
||||
"data_streams" : {
|
||||
"Pressure": {
|
||||
"id": 4,
|
||||
"x_axis_data_size": 4,
|
||||
"x_axis_timestamp":true,
|
||||
"y_axis_data_size": 8,
|
||||
"data_type": "float",
|
||||
"precision": 2,
|
||||
"type": "bar"
|
||||
}
|
||||
},
|
||||
"xaxis_title": "Timestamp",
|
||||
"yaxis_title": "Pressure in milibar"
|
||||
}
|
||||
}
|
2
examples/system/app_trace_to_plot/main/CMakeLists.txt
Normal file
2
examples/system/app_trace_to_plot/main/CMakeLists.txt
Normal file
@ -0,0 +1,2 @@
|
||||
idf_component_register(SRCS "app_trace_to_plot.c"
|
||||
INCLUDE_DIRS ".")
|
167
examples/system/app_trace_to_plot/main/app_trace_to_plot.c
Normal file
167
examples/system/app_trace_to_plot/main/app_trace_to_plot.c
Normal file
@ -0,0 +1,167 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Application Trace to Plot Example
|
||||
|
||||
This example code is in the Public Domain (or CC0 licensed, at your option.)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, this
|
||||
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_app_trace.h"
|
||||
#include "esp_log.h"
|
||||
#include <math.h>
|
||||
#include <limits.h>
|
||||
|
||||
#define CYCLE_PERIOD 51
|
||||
#define STX "esp32"
|
||||
#define STX_LENGTH 5
|
||||
#define ETX 0x03
|
||||
#define MAX_PACKAGE_SIZE 25
|
||||
|
||||
static const char *TAG = "example";
|
||||
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
uint32_t timestamp;
|
||||
} __attribute__((packed)) package_header_t;
|
||||
|
||||
typedef struct {
|
||||
package_header_t header;
|
||||
int8_t value;
|
||||
} __attribute__((packed)) sensor_data_pkt_t;
|
||||
|
||||
typedef struct {
|
||||
package_header_t header;
|
||||
int16_t value;
|
||||
} __attribute__((packed)) sensor2_data_pkt_t;
|
||||
|
||||
typedef struct {
|
||||
package_header_t header;
|
||||
int32_t value;
|
||||
} __attribute__((packed)) sensor3_data_pkt_t;
|
||||
|
||||
typedef struct {
|
||||
package_header_t header;
|
||||
int64_t value;
|
||||
} __attribute__((packed)) sensor4_data_pkt_t;
|
||||
|
||||
int16_t get_engine_temperature(int cnt)
|
||||
{
|
||||
/* A healhty engine should work between 90 to 105 degree Celcius */
|
||||
const int32_t temp_arr[] = {1, 1, 2, 3, 4, 6, 8, 10, 13, 16, 19, 24, 29, 34, 40, 47, 53,
|
||||
60, 68, 75, 82, 88, 94, 99, 103, 105, 107, 107, 106, 104, 101, 96, 91, 85, 78, 71, 64,
|
||||
57, 50, 43, 37, 31, 26, 21, 17, 14, 11, 9, 7, 5, 4, 3};
|
||||
return temp_arr[cnt];
|
||||
}
|
||||
|
||||
int8_t get_outside_temperature(int cnt)
|
||||
{
|
||||
/* Recorded hightest temperature was around 57 degree and lowest was -89 degree Celcius */
|
||||
return (rand() % 146) - 89;
|
||||
}
|
||||
|
||||
int32_t get_altitude(void)
|
||||
{
|
||||
/* Very high altidude is around 4000 meter */
|
||||
return rand() % 4000;
|
||||
}
|
||||
|
||||
int32_t get_pressure(int cnt)
|
||||
{
|
||||
/* Highest pressure recorded around 1.084b and lowest was 0.870b on earth */
|
||||
return (int)(((sin(cnt) / 10) + 1) * 100);
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Waiting for OpenOCD connection");
|
||||
while (!esp_apptrace_host_is_connected(ESP_APPTRACE_DEST_JTAG)) {
|
||||
vTaskDelay(1);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Sending data to the host...");
|
||||
uint32_t cnt = 0;
|
||||
uint8_t buf[MAX_PACKAGE_SIZE] = {0};
|
||||
sensor_data_pkt_t sensor;
|
||||
sensor2_data_pkt_t sensor2;
|
||||
sensor3_data_pkt_t sensor3;
|
||||
sensor4_data_pkt_t sensor4;
|
||||
|
||||
memcpy(buf, (uint8_t *)&STX, STX_LENGTH);
|
||||
|
||||
while (esp_apptrace_host_is_connected(ESP_APPTRACE_DEST_JTAG)) {
|
||||
sensor.header.id = 1;
|
||||
sensor.header.timestamp = esp_log_timestamp();
|
||||
sensor.value = get_outside_temperature(cnt);
|
||||
|
||||
buf[STX_LENGTH] = sizeof(sensor);
|
||||
memcpy(buf + STX_LENGTH + 1, (uint8_t *)&sensor, sizeof(sensor));
|
||||
buf[STX_LENGTH + 1 + sizeof(sensor)] = ETX;
|
||||
esp_err_t res = esp_apptrace_write(ESP_APPTRACE_DEST_JTAG, buf, sizeof(sensor) + STX_LENGTH + 1/*sensor pkt len*/ + 1/*ETX len*/, ESP_APPTRACE_TMO_INFINITE);
|
||||
if (res != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write data to host [0x%x] (%s)", res, esp_err_to_name(res));
|
||||
break;
|
||||
}
|
||||
/* Delay added for represent the data better on the plot */
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
sensor2.header.id = 2;
|
||||
sensor2.header.timestamp = esp_log_timestamp();
|
||||
sensor2.value = get_engine_temperature(cnt);
|
||||
|
||||
buf[STX_LENGTH] = sizeof(sensor2);
|
||||
memcpy(buf + STX_LENGTH + 1, (uint8_t *)&sensor2, sizeof(sensor2));
|
||||
buf[STX_LENGTH + 1 + sizeof(sensor2)] = ETX;
|
||||
res = esp_apptrace_write(ESP_APPTRACE_DEST_JTAG, buf, sizeof(sensor2) + STX_LENGTH + 1/*sensor pkt len*/ + 1/*ETX len*/, ESP_APPTRACE_TMO_INFINITE);
|
||||
if (res != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write data to host [0x%x] (%s)", res, esp_err_to_name(res));
|
||||
break;
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
if (cnt % 2 == 0) {
|
||||
sensor3.header.id = 3;
|
||||
sensor3.header.timestamp = esp_log_timestamp();
|
||||
sensor3.value = get_altitude();
|
||||
|
||||
buf[STX_LENGTH] = sizeof(sensor3);
|
||||
memcpy(buf + STX_LENGTH + 1, (uint8_t *)&sensor3, sizeof(sensor3));
|
||||
buf[STX_LENGTH + 1 + sizeof(sensor3)] = ETX;
|
||||
res = esp_apptrace_write(ESP_APPTRACE_DEST_JTAG, buf, sizeof(sensor3) + STX_LENGTH + 1/*sensor pkt len*/ + 1/*ETX len*/, ESP_APPTRACE_TMO_INFINITE);
|
||||
if (res != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write data to host [0x%x] (%s)", res, esp_err_to_name(res));
|
||||
break;
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
sensor4.header.id = 4;
|
||||
sensor4.header.timestamp = esp_log_timestamp();
|
||||
sensor4.value = get_pressure(cnt);
|
||||
|
||||
buf[STX_LENGTH] = sizeof(sensor4);
|
||||
memcpy(buf + STX_LENGTH + 1, (uint8_t *)&sensor4, sizeof(sensor4));
|
||||
buf[STX_LENGTH + 1 + sizeof(sensor4)] = ETX;
|
||||
res = esp_apptrace_write(ESP_APPTRACE_DEST_JTAG, buf, sizeof(sensor4) + STX_LENGTH + 1/*sensor pkt len*/ + 1/*ETX len*/, ESP_APPTRACE_TMO_INFINITE);
|
||||
if (res != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write data to host [0x%x] (%s)", res, esp_err_to_name(res));
|
||||
break;
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
esp_apptrace_flush(ESP_APPTRACE_DEST_JTAG, 1000);
|
||||
cnt = (cnt + 1) % CYCLE_PERIOD;
|
||||
}
|
||||
ESP_LOGE(TAG, "Apptrace connection lost");
|
||||
ESP_LOGI(TAG, "Data sent and getting back to the UART...");
|
||||
ESP_LOGI(TAG, "Done!");
|
||||
}
|
264
examples/system/app_trace_to_plot/read_trace.py
Normal file
264
examples/system/app_trace_to_plot/read_trace.py
Normal file
@ -0,0 +1,264 @@
|
||||
# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
from typing import Any, List
|
||||
|
||||
try:
|
||||
import espytrace.apptrace
|
||||
except ImportError: # cheat and use IDF's copy of espytrace if available
|
||||
idf_path = os.getenv('IDF_PATH')
|
||||
if not idf_path or not os.path.exists(idf_path):
|
||||
print('IDF not found. Please export idf')
|
||||
raise SystemExit(1)
|
||||
sys.path.insert(0, os.path.join(idf_path, 'tools', 'esp_app_trace'))
|
||||
import espytrace.apptrace
|
||||
|
||||
try:
|
||||
import dash
|
||||
from dash import dcc, html
|
||||
from dash.dependencies import Input, Output
|
||||
from plotly.subplots import make_subplots
|
||||
except ImportError:
|
||||
print('Dash not found. Try to run \'pip install dash\'')
|
||||
raise SystemExit(1)
|
||||
|
||||
plots = [] # type: List[Any]
|
||||
output_lines = [] # type: List[Any]
|
||||
COMMENT_LINE = '//'
|
||||
|
||||
|
||||
class States(Enum):
|
||||
STX_WAIT = 1
|
||||
LENGTH_WAIT = 2
|
||||
DATA_WAIT = 3
|
||||
ETX_WAIT = 4
|
||||
|
||||
|
||||
app = dash.Dash(__name__)
|
||||
app.layout = html.Div(
|
||||
html.Div([
|
||||
html.H2('Telemetry Data'),
|
||||
html.Div(id='live-update-data'),
|
||||
dcc.Graph(id='live-update-graph', style={'height': 800}), # Height of the plotting area setted to 800px
|
||||
dcc.Interval(
|
||||
id='interval-component',
|
||||
interval=5 * 100, # Graph will be updated every 500 ms
|
||||
n_intervals=0
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
# Multiple components can update everytime interval gets fired.
|
||||
@app.callback(Output('live-update-graph', 'figure'),
|
||||
Input('interval-component', 'n_intervals'))
|
||||
def update_graph_live(_n: Any) -> Any: # pylint: disable=undefined-argument
|
||||
excluded_keys_for_plot = {'id', 'x_axis_data_size','y_axis_data_size', 'data_type', 'precision', 'x_axis_timestamp'}
|
||||
fig = make_subplots(rows=len(plots), cols=1, vertical_spacing=0.2, subplot_titles=[each_plot['title'] for each_plot in plots])
|
||||
|
||||
for i, each_plot in enumerate(plots, start=1):
|
||||
for each_subplot in each_plot['plots']:
|
||||
plot_dict = {k: each_subplot[k] for k in each_subplot.keys() - excluded_keys_for_plot}
|
||||
fig.append_trace(plot_dict, i, 1)
|
||||
fig['layout']['xaxis{}'.format(i)]['title'] = each_plot['xaxis_title']
|
||||
fig['layout']['yaxis{}'.format(i)]['title'] = each_plot['yaxis_title']
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def get_value_from_key(input_id: int, key: str) -> Any:
|
||||
for each_plot in plots:
|
||||
for each_subplot in each_plot['plots']:
|
||||
if each_subplot['id'] == input_id:
|
||||
return each_subplot[key]
|
||||
return None
|
||||
|
||||
|
||||
def parse_data_and_print(packet: bytes, offset_time: Any) -> None:
|
||||
ID_LENGTH = 4
|
||||
x_axis_data_length = 4
|
||||
y_axis_data_length = 4
|
||||
|
||||
input_id = int.from_bytes(packet[:ID_LENGTH], 'little')
|
||||
data_size = get_value_from_key(input_id, 'x_axis_data_size')
|
||||
x_axis_data_length = data_size
|
||||
x_axis_raw_data = int.from_bytes(packet[ID_LENGTH:ID_LENGTH + x_axis_data_length], 'little')
|
||||
is_timestamp = get_value_from_key(input_id, 'x_axis_timestamp')
|
||||
if is_timestamp:
|
||||
x_axis_data = offset_time + datetime.timedelta(seconds=x_axis_raw_data / 1000)
|
||||
else:
|
||||
x_axis_data = float(x_axis_raw_data)
|
||||
data_size = get_value_from_key(input_id, 'y_axis_data_size')
|
||||
y_axis_data_length = data_size
|
||||
y_axis_data = int.from_bytes(packet[x_axis_data_length + ID_LENGTH:x_axis_data_length + ID_LENGTH + y_axis_data_length], 'little', signed=True)
|
||||
if get_value_from_key(input_id, 'data_type') in ['float', 'double']:
|
||||
precision = get_value_from_key(input_id, 'precision')
|
||||
y_axis_data = y_axis_data / (10 ** precision)
|
||||
|
||||
ct = datetime.datetime.now()
|
||||
str_ctr = ct.strftime('%x-%X.%f')
|
||||
|
||||
output_str = str_ctr + '-> ' + str(packet)
|
||||
output_lines.append(output_str)
|
||||
|
||||
arr = [x_axis_data, y_axis_data]
|
||||
update_specific_graph_list(int(input_id), arr)
|
||||
|
||||
|
||||
class CustomRequestHandler(espytrace.apptrace.TCPRequestHandler):
|
||||
"""
|
||||
Handler for incoming TCP connections
|
||||
"""
|
||||
def handle(self) -> None:
|
||||
STX = b'esp32'
|
||||
ETX = b'\x03'
|
||||
STX_LEN = 5
|
||||
|
||||
ETX_LEN = 1
|
||||
DATA_LENGTH_LEN = 1
|
||||
PACKET_TIMEOUT_SECOND = 3
|
||||
|
||||
state = States.STX_WAIT
|
||||
val_to_parse = b''
|
||||
length = 0
|
||||
offset_time = datetime.datetime.now()
|
||||
start_time = datetime.datetime.now()
|
||||
data = b''
|
||||
while not self.server.need_stop:
|
||||
data += self.request.recv(1024)
|
||||
if state == States.STX_WAIT:
|
||||
if len(data) >= STX_LEN and data.find(STX) != -1:
|
||||
data = data[data.find(STX) + STX_LEN:]
|
||||
state = States.LENGTH_WAIT
|
||||
start_time = datetime.datetime.now()
|
||||
else:
|
||||
data = data[-STX_LEN:]
|
||||
if state == States.LENGTH_WAIT:
|
||||
if len(data) > 0:
|
||||
state = States.DATA_WAIT
|
||||
length = int.from_bytes(data[:DATA_LENGTH_LEN], 'little')
|
||||
data = data[DATA_LENGTH_LEN:]
|
||||
if state == States.DATA_WAIT:
|
||||
if len(data) >= length:
|
||||
state = States.ETX_WAIT
|
||||
val_to_parse = data[:length]
|
||||
data = data[length:]
|
||||
if state == States.ETX_WAIT:
|
||||
if len(data) >= ETX_LEN and data[:ETX_LEN] == ETX:
|
||||
state = States.STX_WAIT
|
||||
parse_data_and_print(val_to_parse, offset_time)
|
||||
data = data[ETX_LEN:]
|
||||
if state != States.STX_WAIT and (datetime.datetime.now() - start_time).seconds > PACKET_TIMEOUT_SECOND:
|
||||
print('Packet timed out. Dropping!')
|
||||
state = States.STX_WAIT
|
||||
|
||||
|
||||
def read_json(file_path: str) -> Any:
|
||||
with open(file_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def save_data(file_path: str) -> None:
|
||||
with open(file_path, 'w') as f:
|
||||
f.writelines(output_lines)
|
||||
|
||||
|
||||
def signal_handler(output_file_path: str, reader: Any, sig: Any, frame: Any) -> None:
|
||||
del sig, frame
|
||||
reader.cleanup()
|
||||
if output_file_path is not None:
|
||||
save_data(output_file_path)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def update_specific_graph_list(input_id: int, val: List[Any]) -> None:
|
||||
for each_plot in plots:
|
||||
for each_subplot in each_plot['plots']:
|
||||
if each_subplot['id'] == input_id:
|
||||
each_subplot['x'].append((val[0]))
|
||||
each_subplot['y'].append(float(val[1]))
|
||||
return
|
||||
|
||||
|
||||
def check_entry_and_get_struct(entry: dict, data_label: str) -> dict:
|
||||
mandatory_key = 'id'
|
||||
other_keys = {'x': [], 'y': [], 'type': 'scatter'}
|
||||
|
||||
ret_dict = entry
|
||||
if ret_dict.get(mandatory_key) is None:
|
||||
raise KeyError('ID key is missing')
|
||||
|
||||
ret_dict['name'] = data_label
|
||||
for each_key, each_value in other_keys.items():
|
||||
if ret_dict.get(each_key) is None:
|
||||
ret_dict[each_key] = each_value
|
||||
|
||||
return ret_dict
|
||||
|
||||
|
||||
def validate_json(json_file: Any) -> None:
|
||||
mandatory_keys = {'id':int, 'x_axis_data_size': int, 'y_axis_data_size': int, 'data_type': str, 'x_axis_timestamp': bool}
|
||||
for each_plot in json_file:
|
||||
if each_plot.startswith(COMMENT_LINE):
|
||||
continue
|
||||
try:
|
||||
each_subplot = json_file[each_plot]['data_streams']
|
||||
except KeyError:
|
||||
print('data_streams key not found. Aborting')
|
||||
raise SystemExit(1)
|
||||
for each_subplot in json_file[each_plot]['data_streams']:
|
||||
for key, value in mandatory_keys.items():
|
||||
try:
|
||||
val = json_file[each_plot]['data_streams'][each_subplot][key]
|
||||
if not isinstance(val, value):
|
||||
print('[{}][data_streams][{}][{}] expected {} found {}'.format(each_plot, each_subplot, key, mandatory_keys[key], type(val)))
|
||||
raise SystemExit(1)
|
||||
except KeyError:
|
||||
print('[{}][data_streams][{}][{}] key not found. Aborting'.format(each_plot, each_subplot, key))
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def configure_plots(json_file: Any) -> None:
|
||||
for each_plot in json_file:
|
||||
if each_plot.startswith(COMMENT_LINE):
|
||||
continue
|
||||
data_struct = {'title': each_plot, 'plots': [], 'xaxis_title': json_file[each_plot]['xaxis_title'], 'yaxis_title': json_file[each_plot]['yaxis_title']}
|
||||
for each_subplot in json_file[each_plot]['data_streams']:
|
||||
subplot_items = json_file[each_plot]['data_streams'][each_subplot]
|
||||
plot_data_struct = check_entry_and_get_struct(subplot_items, each_subplot)
|
||||
data_struct['plots'].append(plot_data_struct)
|
||||
plots.append(data_struct)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description='Apptrace Visualizing Tool')
|
||||
parser.add_argument('--plot-config', help='Path to json file', required=False, type=str,
|
||||
default=os.path.realpath(os.path.join(os.path.dirname(__file__), 'data.json')))
|
||||
parser.add_argument('--source', help='Data source path', required=True, type=str)
|
||||
parser.add_argument('--output-file', help='Path to program output file in txt format', type=str)
|
||||
|
||||
args = parser.parse_args()
|
||||
output_file_path = args.output_file
|
||||
json_file_name = args.plot_config
|
||||
data_source = args.source
|
||||
|
||||
json_file = read_json(json_file_name)
|
||||
validate_json(json_file)
|
||||
configure_plots(json_file)
|
||||
reader = espytrace.apptrace.reader_create(data_source, 1, CustomRequestHandler)
|
||||
signal.signal(signal.SIGINT, partial(signal_handler, output_file_path, reader))
|
||||
|
||||
app.run_server(debug=True, use_reloader=False, port=8055)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
3
examples/system/app_trace_to_plot/sdkconfig.defaults
Normal file
3
examples/system/app_trace_to_plot/sdkconfig.defaults
Normal file
@ -0,0 +1,3 @@
|
||||
# Enable application tracing by default
|
||||
CONFIG_APPTRACE_DEST_JTAG=y
|
||||
CONFIG_APPTRACE_ENABLE=y
|
@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import json
|
||||
|
@ -40,6 +40,7 @@ examples/build_system/cmake/idf_as_lib/run.sh
|
||||
examples/common_components/protocol_examples_tapif_io/make_tap_netif
|
||||
examples/storage/parttool/parttool_example.py
|
||||
examples/storage/parttool/parttool_example.sh
|
||||
examples/system/app_trace_to_plot/read_trace.py
|
||||
examples/system/ota/otatool/get_running_partition.py
|
||||
examples/system/ota/otatool/otatool_example.py
|
||||
examples/system/ota/otatool/otatool_example.sh
|
||||
|
@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import print_function
|
||||
|
||||
@ -288,7 +288,7 @@ class TCPReader(NetReader, SocketServer.TCPServer):
|
||||
"""
|
||||
TCP socket reader class
|
||||
"""
|
||||
def __init__(self, host, port, tmo):
|
||||
def __init__(self, host, port, tmo, handler=TCPRequestHandler):
|
||||
"""
|
||||
Constructor
|
||||
|
||||
@ -301,7 +301,7 @@ class TCPReader(NetReader, SocketServer.TCPServer):
|
||||
tmo : int
|
||||
see Reader.__init__()
|
||||
"""
|
||||
SocketServer.TCPServer.__init__(self, (host, port), TCPRequestHandler)
|
||||
SocketServer.TCPServer.__init__(self, (host, port), handler)
|
||||
NetReader.__init__(self, tmo)
|
||||
|
||||
|
||||
@ -316,7 +316,7 @@ class UDPReader(NetReader, SocketServer.UDPServer):
|
||||
"""
|
||||
UDP socket reader class
|
||||
"""
|
||||
def __init__(self, host, port, tmo):
|
||||
def __init__(self, host, port, tmo, handler=UDPRequestHandler):
|
||||
"""
|
||||
Constructor
|
||||
|
||||
@ -329,11 +329,11 @@ class UDPReader(NetReader, SocketServer.UDPServer):
|
||||
tmo : int
|
||||
see Reader.__init__()
|
||||
"""
|
||||
SocketServer.UDPServer.__init__(self, (host, port), UDPRequestHandler)
|
||||
SocketServer.UDPServer.__init__(self, (host, port), handler)
|
||||
NetReader.__init__(self, tmo)
|
||||
|
||||
|
||||
def reader_create(trc_src, tmo):
|
||||
def reader_create(trc_src, tmo, handler=None):
|
||||
"""
|
||||
Creates trace reader.
|
||||
|
||||
@ -357,8 +357,12 @@ def reader_create(trc_src, tmo):
|
||||
else:
|
||||
return FileReader(url.path, tmo)
|
||||
if url.scheme == 'tcp':
|
||||
if handler is not None:
|
||||
return TCPReader(url.hostname, url.port, tmo, handler)
|
||||
return TCPReader(url.hostname, url.port, tmo)
|
||||
if url.scheme == 'udp':
|
||||
if handler is not None:
|
||||
return UDPReader(url.hostname, url.port, tmo, handler)
|
||||
return UDPReader(url.hostname, url.port, tmo)
|
||||
return None
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user