locale helpers are now on their own file
This commit is contained in:
@@ -13,31 +13,13 @@
|
|||||||
|
|
||||||
import optparse
|
import optparse
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import locale
|
from locale_utils import print_with_c_locale
|
||||||
from scipy.signal import butter, filtfilt
|
from scipy.signal import butter, filtfilt
|
||||||
|
|
||||||
|
|
||||||
NUM_POINTS = 500
|
NUM_POINTS = 500
|
||||||
|
|
||||||
|
|
||||||
# Set the best locale for time and date formating (generation of the titles)
|
|
||||||
try:
|
|
||||||
current_locale = locale.getlocale(locale.LC_TIME)
|
|
||||||
if current_locale is None or current_locale[0] is None:
|
|
||||||
locale.setlocale(locale.LC_TIME, 'C')
|
|
||||||
except locale.Error:
|
|
||||||
locale.setlocale(locale.LC_TIME, 'C')
|
|
||||||
|
|
||||||
# Override the built-in print function to avoid problem in Klipper due to locale settings
|
|
||||||
original_print = print
|
|
||||||
def print_with_c_locale(*args, **kwargs):
|
|
||||||
original_locale = locale.setlocale(locale.LC_ALL, None)
|
|
||||||
locale.setlocale(locale.LC_ALL, 'C')
|
|
||||||
original_print(*args, **kwargs)
|
|
||||||
locale.setlocale(locale.LC_ALL, original_locale)
|
|
||||||
print = print_with_c_locale
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Computation
|
# Computation
|
||||||
######################################################################
|
######################################################################
|
||||||
@@ -162,7 +144,7 @@ def main():
|
|||||||
opts.error("Invalid acceleration value. It should be a numeric value.")
|
opts.error("Invalid acceleration value. It should be a numeric value.")
|
||||||
|
|
||||||
results = axesmap_calibration(args, accel_value)
|
results = axesmap_calibration(args, accel_value)
|
||||||
print(results)
|
print_with_c_locale(results)
|
||||||
|
|
||||||
if options.output is not None:
|
if options.output is not None:
|
||||||
with open(options.output, 'w') as f:
|
with open(options.output, 'w') as f:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import scipy
|
|||||||
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
||||||
import matplotlib.ticker, matplotlib.gridspec, matplotlib.colors
|
import matplotlib.ticker, matplotlib.gridspec, matplotlib.colors
|
||||||
import matplotlib.patches
|
import matplotlib.patches
|
||||||
import locale
|
from locale_utils import set_locale, print_with_c_locale
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
matplotlib.use('Agg')
|
matplotlib.use('Agg')
|
||||||
@@ -44,24 +44,6 @@ KLIPPAIN_COLORS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Set the best locale for time and date formating (generation of the titles)
|
|
||||||
try:
|
|
||||||
current_locale = locale.getlocale(locale.LC_TIME)
|
|
||||||
if current_locale is None or current_locale[0] is None:
|
|
||||||
locale.setlocale(locale.LC_TIME, 'C')
|
|
||||||
except locale.Error:
|
|
||||||
locale.setlocale(locale.LC_TIME, 'C')
|
|
||||||
|
|
||||||
# Override the built-in print function to avoid problem in Klipper due to locale settings
|
|
||||||
original_print = print
|
|
||||||
def print_with_c_locale(*args, **kwargs):
|
|
||||||
original_locale = locale.setlocale(locale.LC_ALL, None)
|
|
||||||
locale.setlocale(locale.LC_ALL, 'C')
|
|
||||||
original_print(*args, **kwargs)
|
|
||||||
locale.setlocale(locale.LC_ALL, original_locale)
|
|
||||||
print = print_with_c_locale
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Computation of the PSD graph
|
# Computation of the PSD graph
|
||||||
######################################################################
|
######################################################################
|
||||||
@@ -284,7 +266,7 @@ def plot_compare_frequency(ax, lognames, signal1, signal2, max_freq):
|
|||||||
signal1_belt += " (axis 1, 1)"
|
signal1_belt += " (axis 1, 1)"
|
||||||
signal2_belt += " (axis 1,-1)"
|
signal2_belt += " (axis 1,-1)"
|
||||||
else:
|
else:
|
||||||
print("Warning: belts doesn't seem to have the correct name A and B (extracted from the filename.csv)")
|
print_with_c_locale("Warning: belts doesn't seem to have the correct name A and B (extracted from the filename.csv)")
|
||||||
|
|
||||||
# Plot the two belts PSD signals
|
# Plot the two belts PSD signals
|
||||||
ax.plot(signal1.freqs, signal1.psd, label="Belt " + signal1_belt, color=KLIPPAIN_COLORS['purple'])
|
ax.plot(signal1.freqs, signal1.psd, label="Belt " + signal1_belt, color=KLIPPAIN_COLORS['purple'])
|
||||||
@@ -339,7 +321,7 @@ def plot_compare_frequency(ax, lognames, signal1, signal2, max_freq):
|
|||||||
similarity_factor = compute_curve_similarity_factor(signal1, signal2)
|
similarity_factor = compute_curve_similarity_factor(signal1, signal2)
|
||||||
ax2.plot([], [], ' ', label=f'Estimated similarity: {similarity_factor:.1f}%')
|
ax2.plot([], [], ' ', label=f'Estimated similarity: {similarity_factor:.1f}%')
|
||||||
ax2.plot([], [], ' ', label=f'Number of unpaired peaks: {unpaired_peak_count}')
|
ax2.plot([], [], ' ', label=f'Number of unpaired peaks: {unpaired_peak_count}')
|
||||||
print(f"Belts estimated similarity: {similarity_factor:.1f}%")
|
print_with_c_locale(f"Belts estimated similarity: {similarity_factor:.1f}%")
|
||||||
|
|
||||||
# Setting axis parameters, grid and graph title
|
# Setting axis parameters, grid and graph title
|
||||||
ax.set_xlabel('Frequency (Hz)')
|
ax.set_xlabel('Frequency (Hz)')
|
||||||
@@ -383,7 +365,7 @@ def plot_difference_spectrogram(ax, data1, data2, signal1, signal2, similarity_f
|
|||||||
# the similarity factor and the number or unpaired peaks from the belts frequency profile
|
# the similarity factor and the number or unpaired peaks from the belts frequency profile
|
||||||
# Be careful, this value is highly opinionated and is pretty experimental!
|
# Be careful, this value is highly opinionated and is pretty experimental!
|
||||||
mhi, textual_mhi = compute_mhi(combined_sum, similarity_factor, len(signal1.unpaired_peaks) + len(signal2.unpaired_peaks))
|
mhi, textual_mhi = compute_mhi(combined_sum, similarity_factor, len(signal1.unpaired_peaks) + len(signal2.unpaired_peaks))
|
||||||
print(f"[experimental] Mechanical Health Indicator: {textual_mhi.lower()} ({mhi:.1f}%)")
|
print_with_c_locale(f"[experimental] Mechanical Health Indicator: {textual_mhi.lower()} ({mhi:.1f}%)")
|
||||||
ax.set_title(f"Differential Spectrogram", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
ax.set_title(f"Differential Spectrogram", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||||
ax.plot([], [], ' ', label=f'{textual_mhi} (experimental)')
|
ax.plot([], [], ' ', label=f'{textual_mhi} (experimental)')
|
||||||
|
|
||||||
@@ -476,6 +458,7 @@ def setup_klipper_import(kdir):
|
|||||||
|
|
||||||
|
|
||||||
def belts_calibration(lognames, klipperdir="~/klipper", max_freq=200.):
|
def belts_calibration(lognames, klipperdir="~/klipper", max_freq=200.):
|
||||||
|
set_locale()
|
||||||
setup_klipper_import(klipperdir)
|
setup_klipper_import(klipperdir)
|
||||||
|
|
||||||
# Parse data
|
# Parse data
|
||||||
@@ -506,7 +489,7 @@ def belts_calibration(lognames, klipperdir="~/klipper", max_freq=200.):
|
|||||||
dt = datetime.strptime(f"{filename.split('_')[1]} {filename.split('_')[2]}", "%Y%m%d %H%M%S")
|
dt = datetime.strptime(f"{filename.split('_')[1]} {filename.split('_')[2]}", "%Y%m%d %H%M%S")
|
||||||
title_line2 = dt.strftime('%x %X')
|
title_line2 = dt.strftime('%x %X')
|
||||||
except:
|
except:
|
||||||
print("Warning: CSV filenames look to be different than expected (%s , %s)" % (lognames[0], lognames[1]))
|
print_with_c_locale("Warning: CSV filenames look to be different than expected (%s , %s)" % (lognames[0], lognames[1]))
|
||||||
title_line2 = lognames[0].split('/')[-1] + " / " + lognames[1].split('/')[-1]
|
title_line2 = lognames[0].split('/')[-1] + " / " + lognames[1].split('/')[-1]
|
||||||
fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import numpy as np
|
|||||||
import scipy
|
import scipy
|
||||||
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
||||||
import matplotlib.ticker, matplotlib.gridspec
|
import matplotlib.ticker, matplotlib.gridspec
|
||||||
import locale
|
from locale_utils import set_locale, print_with_c_locale
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
matplotlib.use('Agg')
|
matplotlib.use('Agg')
|
||||||
@@ -38,24 +38,6 @@ KLIPPAIN_COLORS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Set the best locale for time and date formating (generation of the titles)
|
|
||||||
try:
|
|
||||||
current_locale = locale.getlocale(locale.LC_TIME)
|
|
||||||
if current_locale is None or current_locale[0] is None:
|
|
||||||
locale.setlocale(locale.LC_TIME, 'C')
|
|
||||||
except locale.Error:
|
|
||||||
locale.setlocale(locale.LC_TIME, 'C')
|
|
||||||
|
|
||||||
# Override the built-in print function to avoid problem in Klipper due to locale settings
|
|
||||||
original_print = print
|
|
||||||
def print_with_c_locale(*args, **kwargs):
|
|
||||||
original_locale = locale.setlocale(locale.LC_ALL, None)
|
|
||||||
locale.setlocale(locale.LC_ALL, 'C')
|
|
||||||
original_print(*args, **kwargs)
|
|
||||||
locale.setlocale(locale.LC_ALL, original_locale)
|
|
||||||
print = print_with_c_locale
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Computation
|
# Computation
|
||||||
######################################################################
|
######################################################################
|
||||||
@@ -70,14 +52,14 @@ def calibrate_shaper_with_damping(datas, max_smoothing):
|
|||||||
|
|
||||||
calibration_data.normalize_to_frequencies()
|
calibration_data.normalize_to_frequencies()
|
||||||
|
|
||||||
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, print)
|
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, print_with_c_locale)
|
||||||
|
|
||||||
freqs = calibration_data.freq_bins
|
freqs = calibration_data.freq_bins
|
||||||
psd = calibration_data.psd_sum
|
psd = calibration_data.psd_sum
|
||||||
fr, zeta = compute_damping_ratio(psd, freqs)
|
fr, zeta = compute_damping_ratio(psd, freqs)
|
||||||
|
|
||||||
print("Recommended shaper is %s @ %.1f Hz" % (shaper.name, shaper.freq))
|
print_with_c_locale("Recommended shaper is %s @ %.1f Hz" % (shaper.name, shaper.freq))
|
||||||
print("Axis has a main resonant frequency at %.1fHz with an estimated damping ratio of %.3f" % (fr, zeta))
|
print_with_c_locale("Axis has a main resonant frequency at %.1fHz with an estimated damping ratio of %.3f" % (fr, zeta))
|
||||||
|
|
||||||
return shaper.name, all_shapers, calibration_data, fr, zeta
|
return shaper.name, all_shapers, calibration_data, fr, zeta
|
||||||
|
|
||||||
@@ -148,7 +130,7 @@ def detect_peaks(psd, freqs, window_size=5, vicinity=3):
|
|||||||
num_peaks = len(refined_peaks)
|
num_peaks = len(refined_peaks)
|
||||||
num_peaks_above_effect_threshold = np.sum(psd[refined_peaks] > effect_threshold)
|
num_peaks_above_effect_threshold = np.sum(psd[refined_peaks] > effect_threshold)
|
||||||
|
|
||||||
print("Peaks detected on the graph: %d @ %s Hz (%d above effect threshold)" % (num_peaks, ", ".join(map(str, peak_freqs)), num_peaks_above_effect_threshold))
|
print_with_c_locale("Peaks detected on the graph: %d @ %s Hz (%d above effect threshold)" % (num_peaks, ", ".join(map(str, peak_freqs)), num_peaks_above_effect_threshold))
|
||||||
|
|
||||||
return np.array(refined_peaks), num_peaks, num_peaks_above_effect_threshold
|
return np.array(refined_peaks), num_peaks, num_peaks_above_effect_threshold
|
||||||
|
|
||||||
@@ -319,6 +301,7 @@ def setup_klipper_import(kdir):
|
|||||||
|
|
||||||
|
|
||||||
def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, max_freq=200.):
|
def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, max_freq=200.):
|
||||||
|
set_locale()
|
||||||
setup_klipper_import(klipperdir)
|
setup_klipper_import(klipperdir)
|
||||||
|
|
||||||
# Parse data
|
# Parse data
|
||||||
@@ -340,7 +323,7 @@ def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, max
|
|||||||
dt = datetime.strptime(f"{filename_parts[1]} {filename_parts[2]}", "%Y%m%d %H%M%S")
|
dt = datetime.strptime(f"{filename_parts[1]} {filename_parts[2]}", "%Y%m%d %H%M%S")
|
||||||
title_line2 = dt.strftime('%x %X') + ' -- ' + filename_parts[3].upper().split('.')[0] + ' axis'
|
title_line2 = dt.strftime('%x %X') + ' -- ' + filename_parts[3].upper().split('.')[0] + ' axis'
|
||||||
except:
|
except:
|
||||||
print("Warning: CSV filename look to be different than expected (%s)" % (lognames[0]))
|
print_with_c_locale("Warning: CSV filename look to be different than expected (%s)" % (lognames[0]))
|
||||||
title_line2 = lognames[0].split('/')[-1]
|
title_line2 = lognames[0].split('/')[-1]
|
||||||
fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from collections import OrderedDict
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
|
||||||
import matplotlib.ticker, matplotlib.gridspec
|
import matplotlib.ticker, matplotlib.gridspec
|
||||||
import locale
|
from locale_utils import set_locale, print_with_c_locale
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
matplotlib.use('Agg')
|
matplotlib.use('Agg')
|
||||||
@@ -36,24 +36,6 @@ KLIPPAIN_COLORS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Set the best locale for time and date formating (generation of the titles)
|
|
||||||
try:
|
|
||||||
current_locale = locale.getlocale(locale.LC_TIME)
|
|
||||||
if current_locale is None or current_locale[0] is None:
|
|
||||||
locale.setlocale(locale.LC_TIME, 'C')
|
|
||||||
except locale.Error:
|
|
||||||
locale.setlocale(locale.LC_TIME, 'C')
|
|
||||||
|
|
||||||
# Override the built-in print function to avoid problem in Klipper due to locale settings
|
|
||||||
original_print = print
|
|
||||||
def print_with_c_locale(*args, **kwargs):
|
|
||||||
original_locale = locale.setlocale(locale.LC_ALL, None)
|
|
||||||
locale.setlocale(locale.LC_ALL, 'C')
|
|
||||||
original_print(*args, **kwargs)
|
|
||||||
locale.setlocale(locale.LC_ALL, original_locale)
|
|
||||||
print = print_with_c_locale
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Computation
|
# Computation
|
||||||
######################################################################
|
######################################################################
|
||||||
@@ -255,7 +237,7 @@ def plot_speed_profile(ax, speeds, power_total):
|
|||||||
low_energy_zones = identify_low_energy_zones(resampled_power_total)
|
low_energy_zones = identify_low_energy_zones(resampled_power_total)
|
||||||
|
|
||||||
peak_speeds = ["{:.1f}".format(resampled_speeds[i]) for i in peaks]
|
peak_speeds = ["{:.1f}".format(resampled_speeds[i]) for i in peaks]
|
||||||
print("Vibrations peaks detected: %d @ %s mm/s (avoid setting a speed near these values in your slicer print profile)" % (num_peaks, ", ".join(map(str, peak_speeds))))
|
print_with_c_locale("Vibrations peaks detected: %d @ %s mm/s (avoid setting a speed near these values in your slicer print profile)" % (num_peaks, ", ".join(map(str, peak_speeds))))
|
||||||
|
|
||||||
if peaks.size:
|
if peaks.size:
|
||||||
ax.plot(speed_array[peaks], power_total_sum[peaks], "x", color='black', markersize=8)
|
ax.plot(speed_array[peaks], power_total_sum[peaks], "x", color='black', markersize=8)
|
||||||
@@ -354,10 +336,10 @@ def plot_vibration_profile(ax, freqs, vibration_power):
|
|||||||
zeta = bandwidth / (2 * fr)
|
zeta = bandwidth / (2 * fr)
|
||||||
|
|
||||||
if fr > 20:
|
if fr > 20:
|
||||||
print("Motors have a main resonant frequency at %.1fHz with an estimated damping ratio of %.3f" % (fr, zeta))
|
print_with_c_locale("Motors have a main resonant frequency at %.1fHz with an estimated damping ratio of %.3f" % (fr, zeta))
|
||||||
else:
|
else:
|
||||||
print("The resonance frequency of the motors is too low (%.1fHz). This is probably due to the test run with too high acceleration!" % fr)
|
print_with_c_locale("The resonance frequency of the motors is too low (%.1fHz). This is probably due to the test run with too high acceleration!" % fr)
|
||||||
print("Try lowering the ACCEL value before restarting the macro to ensure that only constant speeds are recorded and that the dynamic behavior in the corners is not impacting the measurements.")
|
print_with_c_locale("Try lowering the ACCEL value before restarting the macro to ensure that only constant speeds are recorded and that the dynamic behavior in the corners is not impacting the measurements.")
|
||||||
|
|
||||||
ax.plot(vibr_power_array[max_power_index], freq_array[max_power_index], "x", color='black', markersize=8)
|
ax.plot(vibr_power_array[max_power_index], freq_array[max_power_index], "x", color='black', markersize=8)
|
||||||
fontcolor = KLIPPAIN_COLORS['purple']
|
fontcolor = KLIPPAIN_COLORS['purple']
|
||||||
@@ -428,6 +410,7 @@ def setup_klipper_import(kdir):
|
|||||||
|
|
||||||
|
|
||||||
def vibrations_calibration(lognames, klipperdir="~/klipper", axisname=None, max_freq=1000., remove=0):
|
def vibrations_calibration(lognames, klipperdir="~/klipper", axisname=None, max_freq=1000., remove=0):
|
||||||
|
set_locale()
|
||||||
setup_klipper_import(klipperdir)
|
setup_klipper_import(klipperdir)
|
||||||
|
|
||||||
# Parse the raw data and get them ready for analysis
|
# Parse the raw data and get them ready for analysis
|
||||||
@@ -456,7 +439,7 @@ def vibrations_calibration(lognames, klipperdir="~/klipper", axisname=None, max_
|
|||||||
dt = datetime.strptime(f"{filename_parts[1]} {filename_parts[2].split('-')[0]}", "%Y%m%d %H%M%S")
|
dt = datetime.strptime(f"{filename_parts[1]} {filename_parts[2].split('-')[0]}", "%Y%m%d %H%M%S")
|
||||||
title_line2 = dt.strftime('%x %X') + ' -- ' + axisname.upper() + ' axis'
|
title_line2 = dt.strftime('%x %X') + ' -- ' + axisname.upper() + ' axis'
|
||||||
except:
|
except:
|
||||||
print("Warning: CSV filename look to be different than expected (%s)" % (lognames[0]))
|
print_with_c_locale("Warning: CSV filename look to be different than expected (%s)" % (lognames[0]))
|
||||||
title_line2 = lognames[0].split('/')[-1]
|
title_line2 = lognames[0].split('/')[-1]
|
||||||
fig.text(0.075, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.075, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
|
|
||||||
|
|||||||
30
K-ShakeTune/scripts/locale_utils.py
Normal file
30
K-ShakeTune/scripts/locale_utils.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Special utility functions to manage locale settings and printing
|
||||||
|
# Written by Frix_x#0161 #
|
||||||
|
|
||||||
|
|
||||||
|
import locale
|
||||||
|
|
||||||
|
# Set the best locale for time and date formating (generation of the titles)
|
||||||
|
def set_locale():
|
||||||
|
try:
|
||||||
|
current_locale = locale.getlocale(locale.LC_TIME)
|
||||||
|
if current_locale is None or current_locale[0] is None:
|
||||||
|
locale.setlocale(locale.LC_TIME, 'C')
|
||||||
|
except locale.Error:
|
||||||
|
locale.setlocale(locale.LC_TIME, 'C')
|
||||||
|
|
||||||
|
# Print function to avoid problem in Klipper console (that doesn't support special characters) due to locale settings
|
||||||
|
def print_with_c_locale(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
original_locale = locale.getlocale()
|
||||||
|
locale.setlocale(locale.LC_ALL, 'C')
|
||||||
|
except locale.Error as e:
|
||||||
|
print("Warning: Failed to set a basic locale. Special characters may not display correctly in Klipper console:", e)
|
||||||
|
finally:
|
||||||
|
print(*args, **kwargs) # Proceed with printing regardless of locale setting success
|
||||||
|
try:
|
||||||
|
locale.setlocale(locale.LC_ALL, original_locale)
|
||||||
|
except locale.Error as e:
|
||||||
|
print("Warning: Failed to restore the original locale setting:", e)
|
||||||
Reference in New Issue
Block a user