sabato 11 dicembre 2021

Autoencoder anomaly detection con tensorlfow

 Terzo tentativo di analisi dati di estensimetro con Tensorflow (iniziato qui). Si tratta di un adattamento dell'esempio sul sito di Keras


In questo post si cerca di impostare una anomaly detection mediante autoencoder

La prima anomalia nella serie dati e' in corrispondenza del movimento indicato dalla freccia nel grafico soprastante

I dati sono stati tagliati in modo da includere solo l'inizio dell'anomalia in modo da non istruire troppo la rete 



Il modello converge rapidamente con valori di loss e validation loss similari



sovrapponendo il modello ai dati di train si nota una ottima corrispondenza




sottrando i dati reali dal modello si possono estrapolare le anomalie. Indicato dalla freccia l'anomalia derivante dal movimento



di seguito il codice


# -*- coding: utf-8 -*-
"""timeseries_anomaly_detection_detrend3

Automatically generated by Colaboratory.

Original file is located at
https://colab.research.google.com/drive/12Kkjp_xazCmO4HrK0tmzVPyHIoYxJsYo
"""

import numpy as np
import pandas as pd
from tensorflow import keras
from tensorflow.keras import layers
from matplotlib import pyplot as plt

!rm detrend.*
!wget http://c1p81.altervista.org/detrend3.zip
!rm *.csv
!unzip detrend3.zip
df_small_noise=pd.read_csv(r'detrend3.csv', sep=':', header=0, low_memory=False, infer_datetime_format=True, parse_dates={'datetime':[0]}, index_col=['datetime'],usecols=['Data','detrend'])

print(df_small_noise.head())
print(df_small_noise.shape)

#df_small_noise = df_small_noise[:9500]

plt.plot(df_small_noise['detrend'])

plt.show()

# Normalize and save the mean and std we get,
# for normalizing test data.
training_mean = df_small_noise.mean()
training_std = df_small_noise.std()
df_training_value = (df_small_noise - training_mean) / training_std
print("Number of training samples:", len(df_training_value))

TIME_STEPS = 1000

# Generated training sequences for use in the model.
def create_sequences(values, time_steps=TIME_STEPS):
output = []
for i in range(len(values) - time_steps + 1):
output.append(values[i : (i + time_steps)])
return np.stack(output)


x_train = create_sequences(df_training_value.values)
print("Training input shape: ", x_train.shape)

model = keras.Sequential(
[
layers.Input(shape=(x_train.shape[1], x_train.shape[2])),
layers.Conv1D(
filters=32, kernel_size=7, padding="same", strides=2, activation="relu"
),
layers.Dropout(rate=0.2),
layers.Conv1D(
filters=16, kernel_size=7, padding="same", strides=2, activation="relu"
),
layers.Conv1DTranspose(
filters=16, kernel_size=7, padding="same", strides=2, activation="relu"
),
layers.Dropout(rate=0.2),
layers.Conv1DTranspose(
filters=32, kernel_size=7, padding="same", strides=2, activation="relu"
),
layers.Conv1DTranspose(filters=1, kernel_size=7, padding="same"),
]
)
model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.001), loss="mse")
model.summary()

history = model.fit(
x_train,
x_train,
epochs=10,
batch_size=128,
validation_split=0.1,
callbacks=[
keras.callbacks.EarlyStopping(monitor="val_loss", patience=5, mode="min")
],
)


plt.plot(history.history["loss"], label="Training Loss")
plt.plot(history.history["val_loss"], label="Validation Loss")
plt.legend()
plt.show()

# Get train MAE loss.
x_train_pred = model.predict(x_train)
train_mae_loss = np.mean(np.abs(x_train_pred - x_train), axis=1)

plt.hist(train_mae_loss, bins=50)
plt.xlabel("Train MAE loss")
plt.ylabel("No of samples")
plt.show()

# Get reconstruction loss threshold.
threshold = np.max(train_mae_loss)

print("Reconstruction error threshold: ", threshold)

print(x_train.shape)
# Checking how the first sequence is learnt
plt.plot(x_train[288],label='Dati')
plt.plot(x_train_pred[288],label='Modello')
plt.legend()
plt.show()

anomalia = x_train[288] - x_train_pred[288]
plt.plot(anomalia)
plt.show()

venerdì 10 dicembre 2021

AutoKeras LSTM per estensimetro

Questo e' un tentativo di applicazione di AutoKeras (AutoML ovvero machine learning automatizzato basato su Keras) ai dati del post precedente

La differenza sostanziale e' che la dimensione del train set deve essere un multiplo intero della batch size  (in questo caso 42 e 4200)




# -*- coding: utf-8 -*-
"""Autokeras timeseries_forecaster

Automatically generated by Colaboratory.

Original file is located at
https://colab.research.google.com/drive/1mQHWTqwyRKhShtTrwNXBvPHgOkU4ghsX
"""

!pip install autokeras

import pandas as pd
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

import autokeras as ak

!rm detrend2.csv
!wget http://c1p81.altervista.org/detrend2.zip
!unzip detrend2.zip

dataset=pd.read_csv(r'detrend2.csv', sep=',')
# cancella la prima colonna del tempo
#dataset_c.drop('Data',axis=1,inplace=True)
dataset= dataset_c[1:6001]
dataset.head()
dataset.shape

val_split = int(len(dataset) * 0.7)
data_train = dataset[:val_split]
validation_data = dataset[val_split:]

data_x = data_train[
[
"Est.[mm]"
]
].astype("float64")

data_x_val = validation_data[
[
"Est.[mm]"
]
].astype("float64")

# Data with train data and the unseen data from subsequent time steps.
data_x_test = dataset[
[
"Est.[mm]"
]
].astype("float64")

data_y = data_train["Est.[mm]"].astype("float64")

data_y_val = validation_data["Est.[mm]"].astype("float64")

##print(data_x.shape)
#print(data_y.shape)
#print(data_y)

#la batch size deve essere un divisore
#della serie di train altrimenti genera errore

predict_from = 1
#lunghezza dei dati nel futuro
predict_until = 42
lookback = 30
clf = ak.TimeseriesForecaster(
lookback=lookback,
predict_from=predict_from,
predict_until=predict_until,
max_trials=4,
objective="val_loss",
)
clf.fit(
x=data_x,
y=data_y,
validation_data=(data_x_val, data_y_val),
batch_size=42,
epochs=20
)

# Predizione
predictions = clf.predict(data_x_test)
print(predictions.shape)
print(data_x_val.shape)
print(data_y_val.shape)

loss,acc = clf.evaluate(data_x_val, data_y_val, batch_size=42, verbose=1)
print('Accuracy: %.3f' % acc)

plt.plot(predictions)

plt.title("Estensimetro")
plt.xlabel('')
plt.ylabel('')
plt.show()

model = clf.export_model()
# summarize the loaded model
model.summary()
# save the best performing model to file
model.save('model_sonar.h5')

giovedì 9 dicembre 2021

LSTM Time Series con Tensorflow per la geologia

Questo post e' un tentativo di applicazione delle reti neurali LSTM per l'analisi di dati (sulla base di questo articolo) derivanti da un sensore ad applicazione geologica, nello specifico un estensimetro. I dati sono dati reali che sono stati anonimizzati ma sono relativi ad un movimento di versante attivo. Sono originali sono acquisiti con passo di 20 minuti e coprono circa 10 mesi e prevedono misure anche meteo

Il primo tentativo e' stato quello di esaminare la curva completa ma presto mi sono reso conto che la rete neurale non riusciva mai a convergere. Ho limitato i dati quindi dall'inizio fino alla primo movimento avvenuto l'8 maggio

Come si osserva dal grafico ridotto sottostante oltre ad un trend generale in crescita dei valori dell'estensimetro vi sono variazioni cicliche a scala corta

in generale le variazioni cicliche a breve periodo di un estensimetro sono relative a dilatazioni termiche
Plottando i dati si ha comunque una scarsa correlazione



Un approccio di analisi base prevede di trovare una curva che approssima i dati (in questo caso la migliore approssimazione e' stata una polinomio di secondo grado)

facendo uno zoom si ha vede che le variazioni a breve periodo non sono sinusoidali (in pratica si osserva solo dei picchi di discesa ) e che approssimando con la curva di interpolazione si ha una fascia di incertezza dell'ordine di 1 mm

Vediamo se analizzando la serie tempo con una rete LSTM in Tensorflow si riesce ad ottenere un modello che fitta meglio con i dati

Per prima cosa devo dire che i primi tentativi sono stati piuttosto deludenti..mi sono trovato spesso in situazioni in cui la loss dei dati di training era ottima mentre era pessima  (e spesso in crescita con le epochs) quella del set di validazione. 
In altri casi mi sono trovato ad avere un modello decente che aveva un offset costante rispetto ai dati di controllo (vedi grafico sottostante)


Una soluzione e' stata quella di inserire dei layer di dropout nella rete e quella di effettuare un detrending (sottrarre ai dati grezzi il valore della funzione che approssima la tendenza generale)

Un altro aspetto determinante per un modello ottimale consiste nell'individuazione della corretta batch size (nel caso specifico batch size inferiore a 50 non riusciva a seguire le variazioni dei dati)

un dettaglio dei dati dopo il detrend



il miglior risultato che sono riuscito ad ottenere e' il seguente
questi sono i dati di train contro dati reali. Si nota che il modello segue l'andamento dei dati reali ma mostra un ritardo nei minimi relativi del trend generale
 

questi sono invece i dati di validazione. Si osserva una buona sincronia sulle variazioni a breve periodo


========================================
# -*- coding: utf-8 -*-
"""Quincineto_stima_errore_detrend2.ipynb

Automatically generated by Colaboratory.

Original file is located at
https://colab.research.google.com/drive/1fFEfuUj5iwNucKDOfVpEzvIYK_1KebH8

### Elaborazione Quincineto
"""

import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
import matplotlib.pyplot as plt
import tensorflow as tf
import os

# Commented out IPython magic to ensure Python compatibility.
#prepara tensorboard
# %load_ext tensorboard

"""
Download dati"""

!wget http://c1p81.altervista.org/detrend2.zip

!unzip detrend2.zip

"""Carica i dati in matrice"""

df=pd.read_csv(r'detrend2.csv', sep=',', header=0,index_col=['Data'])
df.head()

plt.plot(df['Est.[mm]'])
plt.title("Estensimetro")
plt.xlabel('Datetime')
plt.ylabel('Est')
plt.show()

#finestra mobile dei dati
n_past = 300
n_future = 100
n_features = 1

# divide il dataset in 75% train, 25 % test
righe = df.shape[0]
t = np.round(righe*0.75,0)
print(t)
train_df,test_df = df[1:6000], df[6000:]

# il dataset di test non entra nel calcolo della rete neurale
# e' lo stato futuro che la rete deve prevedere
# qui si trova la rottura della tendenza
plt.plot(test_df['Est.[mm]'])
plt.title("Train dataset estensimetro Dettaglio")
plt.xlabel('Datetime')
plt.ylabel('Est')
plt.show()

#riscala i dati per il calcolo della rete
train = train_df
scalers={}

for i in train_df.columns:
scaler = MinMaxScaler(feature_range=(-1,1))
s_s = scaler.fit_transform(train[i].values.reshape(-1,1))
s_s=np.reshape(s_s,len(s_s))
scalers['scaler_'+ i] = scaler
train[i]=s_s

test = test_df
for i in train_df.columns:
scaler = scalers['scaler_'+i]
s_s = scaler.transform(test[i].values.reshape(-1,1))
s_s=np.reshape(s_s,len(s_s))
scalers['scaler_'+i] = scaler
test[i]=s_s

"""**Converting the series to samples for supervised learning**"""

def split_series(series, n_past, n_future):
#
# n_past ==> no of past observations
#
# n_future ==> no of future observations
#
X, y = list(), list()
for window_start in range(len(series)):
past_end = window_start + n_past
future_end = past_end + n_future
if future_end > len(series):
break
# slicing the past and future parts of the window
past, future = series[window_start:past_end, :], series[past_end:future_end, :]
X.append(past)
y.append(future)

return np.array(X), np.array(y)

X_train, y_train = split_series(train.values,n_past, n_future)
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1],n_features))
y_train = y_train.reshape((y_train.shape[0], y_train.shape[1], n_features))

X_test, y_test = split_series(test.values,n_past, n_future)
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1],n_features))
y_test = y_test.reshape((y_test.shape[0], y_test.shape[1], n_features))

X_test.shape
print(train)

# E1D1
# n_features ==> no of features at each timestep in the data.
#
encoder_inputs = tf.keras.layers.Input(shape=(n_past, n_features))
encoder_l1 = tf.keras.layers.LSTM(300, return_state=True)
encoder_outputs1 = encoder_l1(encoder_inputs)
encoder_states1 = encoder_outputs1[1:]
#
decoder_inputs = tf.keras.layers.RepeatVector(n_future)(encoder_outputs1[0])
#
decoder_l1 = tf.keras.layers.LSTM(300, return_sequences=True,dropout=0.5,recurrent_dropout=0.5)(decoder_inputs,initial_state = encoder_states1)
decoder_outputs1 = tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(n_features))(decoder_l1)
#
model_e1d1 = tf.keras.models.Model(encoder_inputs,decoder_outputs1)

model_e1d1.summary()

reduce_lr = tf.keras.callbacks.LearningRateScheduler(lambda x: 1e-3 * 0.90 ** x)

epoche = 15

#tensorboard start
import datetime
log_dir = "Logs/fit/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1)
#tensorboard start

model_e1d1.compile(optimizer=tf.keras.optimizers.Adam(), loss=tf.keras.losses.Huber())
history_e1d1=model_e1d1.fit(X_train,y_train,epochs=epoche,validation_data=(X_test,y_test),batch_size=164,verbose=1,callbacks=[reduce_lr,tensorboard_callback])

plt.plot(history_e1d1.history['loss'])
plt.plot(history_e1d1.history['val_loss'])
plt.title("Model Loss")
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend(['Train', 'Valid'])
plt.show()

plt.plot(history_e1d1.history['lr'])
plt.title("Model Lr")
plt.xlabel('Epochs')
plt.ylabel('Lr')
plt.show()

"""Predizione dei dati"""

pred1_e1d1=model_e1d1.predict(X_test)
pred_e1d1=model_e1d1.predict(X_train)

"""Ritorna dai valori scalati ai valori reali"""

for index,i in enumerate(train_df.columns):
scaler = scalers['scaler_'+i]
pred1_e1d1[:,:,index]=scaler.inverse_transform(pred1_e1d1[:,:,index])
pred_e1d1[:,:,index]=scaler.inverse_transform(pred_e1d1[:,:,index])
y_train[:,:,index]=scaler.inverse_transform(y_train[:,:,index])
y_test[:,:,index]=scaler.inverse_transform(y_test[:,:,index])

#print(y_train)
np.savetxt('array_ytrain_X.csv', y_train[:,:,0], delimiter=';', fmt='%f')
np.savetxt('array_ytrain_Y.csv', y_train[:,:,1], delimiter=';', fmt='%f')
np.savetxt('array_ytrain_Est.csv', y_train[:,:,2], delimiter=';', fmt='%f')
np.savetxt('array_ytrain_T.csv', y_train[:,:,3], delimiter=';', fmt='%f')
np.savetxt('array_ytrain_bat.csv', y_train[:,:,4], delimiter=';', fmt='%f')

np.savetxt('array_ytest_X.csv', y_test[:,:,0], delimiter=';', fmt='%f')
np.savetxt('array_ytest_Y.csv', y_test[:,:,1], delimiter=';', fmt='%f')
np.savetxt('array_ytest_Est.csv', y_test[:,:,2], delimiter=';', fmt='%f')
np.savetxt('array_ytest_T.csv', y_test[:,:,3], delimiter=';', fmt='%f')
np.savetxt('array_ytest_bat.csv', y_test[:,:,4], delimiter=';', fmt='%f')

print(pred1_e1d1[:,:,0])

"""**Checking Error** """

#errore medio tra modello e dati reali
from sklearn.metrics import mean_absolute_error
print(y_test.shape)
#print(pred1_e1d1[:,:,2])
#print(y_test[:,:,2])
#print(y_test.shape[0])
diff = pred1_e1d1[:,:,0]-y_test[:,:,0]
print("Diff")
#print(diff)
print(diff.shape)
#print(np.square(diff))
sommaq=np.sum(np.square(diff[0]))
print(sommaq.shape)
s_err1=np.sqrt(sommaq/y_test.shape[0]*y_test.shape[1])
print()
print(sommaq)
#err1 = np.square(pred1_e1d1[:,:,2]-y_test[:,:,2])
#s_err1 = np.sqrt(np.sum(err1, axis=0))
#print("Errore quadratico medio (mm)")
print(s_err1)

#print(y_test)
plt.plot(y_test[:,99,0])
plt.plot(pred1_e1d1[:,99,0])
plt.title("Modello vs monitoraggio")
plt.xlabel('Tempo')
plt.ylabel('Est')
plt.legend(['Dati reali', 'Modello'])
plt.show()

# Modello contro monitoraggio nei dati di addestramento
plt.plot(y_train[:,99,2])
plt.plot(pred_e1d1[:,99,2])
plt.title("")
plt.xlabel('Tempo')
plt.ylabel('Est (mm)')
plt.legend(['Monitoraggio', 'Modello'])
plt.show()

# dettaglio del grafico Modello contro dati reali
# si nota come il modello copia in modo idoneo anche
# le variazioni a corto periodo
plt.plot(y_train[1:1000,99,2])
plt.plot(pred_e1d1[1:1000,99,2])
plt.title("")
plt.xlabel('Tempo')
plt.ylabel('Est (mm)')
plt.legend(['Monitoraggio', 'Modello'])
plt.show()

# Commented out IPython magic to ensure Python compatibility.
# %tensorboard --logdir Logs/fit


Utilizzando l'esempio del Time Series Forecasting direttamente dal sito di Tensorflow si hanno risultati similari




giovedì 2 dicembre 2021

Dante e Tensorflow

 Questa storia viene da lontano. Al liceo il professore di italiano mi diede un floppy disk da 5 1/4 con una versione digitale della Divina Commedia dicendomi che era possibile una analisi delle ricorrenze delle parole per vedere come cambiava lo stile di scrittura di Dante Alighieri passando da Inferno, Purgatorio e Paradiso


Un lettore umano capisce subito la differenza tra i vari canti...ma un computer?

Dopo tanti anni e' arrivato il momento di fare un test con una rete neurale.

La prova e' stata quella di utilizzare l'esempio di Text Classification di Tensoriflow per vedere se una rete neurale e' capace di distinguere lo stile dantesco (i dati di base sono scaricabili da qui)

La prima prova e' stata quella di usare come train data files che contenevano un intero canto (34 per Inferno e 33 per Paradiso) ma per la scarsita' di dati il modello matematico non convergeva

Sono stati creati quindi files di testo di una sola riga (un solo verso) con un train di 4291 versi dell'Inferno e 4477 versi del Paradiso (i canti XIII e XIV di Inferno e Paradiso sono stati utilizzati come test data)





La rete non ha una accuratezza eccessiva ma e' ben al di sopra del 50% (valore che si otterrebbe tirando a caso nella classificazione) 

se la divisione del testo avviene per terzine il sistema migliora fino ad una accuratezza del 80%










Maratona FIrenze 2021

 Anche quest'anno un posto in prima fila (omino con giacca celeste)




martedì 30 novembre 2021

Mandelbrot M1 Metal Python

 Dopo il test di Cuda e' arrivato il momento di Metal, il linguaggio per la GPU di Apple

Gli ersempi in C++ sono un po' complicati per una semplice prova ed ho optato per la versione in Python in cui il kernel e' comunque scritto in C++ MetalCompute (negli esempi della libreria c'e' gia' una implementazione di Mandelbrot, l'ho scritta da zero per imparare...sicuramente la mia versione e' meno efficiente)


La logica seguita e' quella di Cuda, ogni thread si occupa di una riga

Si puo' solo passare un solo parametro come array di float al kernel e si ha in uscita un solo array di float. L'array di input e' generato con NumPy con le coordinate X,Y dei punti dell'insieme 


import metalcompute as mc
from PIL import Image as im

mc.init()

code = """
#include <metal_stdlib>
using namespace metal;

kernel void mandelbrot(
const device float* arr [[ buffer(0) ]],
device float* out [[ buffer(1) ]],
uint id [[ thread_position_in_grid ]]) {
float a = arr[id];
float b;
float x_new,y_new,x,y;
int iterazioni = 255;

for (int s=0; s<640;s++)
{
x=0.0;
y=0.0;
b = arr[640+s];

for (int k=0;k<iterazioni;k++)
{
x_new = (x*x)-(y*y)+a;
y_new = (2.0*x*y)+b;
if (((x_new*x_new)+(y_new*y_new))>4)
{
out[id*640+s] = (float)(k%2)*255;
k=iterazioni;
}
x = x_new;
y = y_new;
}
}
}
"""
mc.compile(code, "mandelbrot")
import numpy as np
from time import time as now

dimensioni = 640
x_start = -2.0
x_stop = 0.75
x_res = (x_stop-x_start)/dimensioni
x = np.arange(start=x_start, stop=x_stop,step = x_res,dtype='f')

y_start = -1.5
y_stop = 1.5
y_res = (y_stop-y_start)/dimensioni
y = np.arange(start=y_start, stop=y_stop,step = y_res,dtype='f')

arr = np.concatenate((x,y))
print(arr.shape)

out_buf = np.empty(dimensioni*dimensioni,dtype='f')

start = now()
mc.run(arr, out_buf, dimensioni)
end = now()

immagine = out_buf.reshape(dimensioni,dimensioni)
print(end-start)

immagine = np.int_(immagine)
np.savetxt("dati.txt",immagine.astype(int),fmt='%i')

img = im.fromarray(np.uint8(immagine) , 'L')
img.save('m1.png')
img.show()

lunedì 29 novembre 2021

Twitter query API 2

 Con un account sviluppatore di Twitter si possono effettuare query sul DB Twitter

Vi sono limitazioni temporrali (non si puo' andare piu' indietro di una settimana con l'account base) e si possono richiedere da 10 a 100 risultati (questa limitazione si supera utilizzando il valore di next_token contenuto nel risultato della query che permette di effettuare una ulteriori query sui risultati successivi)


import requests
import json
import re
import random

BEARER_TOKEN = "AAAAAAAAAAAAAAAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

def write_2_file(stringa):
textfile = open(str(random.randrange(1,10000000000))+".txt", "w")
textfile.write(stringa)
textfile.close()

def search_twitter(query, tweet_fields, max_results,next_token, bearer_token = BEARER_TOKEN):
headers = {"Authorization": "Bearer {}".format(bearer_token)}
if len(next_token)>13:
url = "https://api.twitter.com/2/tweets/search/recent?query={}&{}&{}&{}".format(query, tweet_fields,max_results,next_token)
else:
url = "https://api.twitter.com/2/tweets/search/recent?query={}&{}&{}".format(query, tweet_fields,max_results)
response = requests.request("GET", url, headers=headers)
if response.status_code != 200:
raise Exception(response.status_code, response.text)
return response.json()

nr = 100 #numero risultati per query
query = "frana"
tweet_fields = "tweet.fields=text"
max_results = "max_results="+str(nr) #valori compresi tra 10 e 500

start_time = "" # si possono inserire intervalli temporali negli ultimi 7 giorni
end_time = "" # formato YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339)

next_token = ""


json_response = search_twitter(query=query, tweet_fields=tweet_fields, max_results=max_results, next_token=next_token, bearer_token=BEARER_TOKEN)
for t in range(nr):
print(re.sub('[^A-Za-z0-9 ]+', '', json_response['data'][t]['text']))
write_2_file(re.sub('[^A-Za-z0-9 ]+', '', json_response['data'][t]['text']))

for i in range(50):
next_token = "next_token="+json_response['meta']['next_token']
json_response = search_twitter(query=query, tweet_fields=tweet_fields, max_results=max_results, next_token=next_token, bearer_token=BEARER_TOKEN)
for t in range(nr):
print(re.sub('[^A-Za-z0-9 ]+', '', json_response['data'][t]['text']))
write_2_file(re.sub('[^A-Za-z0-9 ]+', '', json_response['data'][t]['text']))

Debugger integrato ESP32S3

Aggiornamento In realta' il Jtag USB funziona anche sui moduli cinesi Il problema risiede  nell'ID USB della porta Jtag. Nel modulo...