From 1f2648087f2e03791b168f9aa175555b1e9dc73f Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Thu, 30 Jul 2026 16:40:14 +0200 Subject: [PATCH 1/6] new low frequency DSP.... --- client/src/pm3_dsp.c | 1812 ++++++++++++++++++++++++++++++++++++++++++ client/src/pm3_dsp.h | 200 +++++ client/src/pm3_fit.c | 1066 +++++++++++++++++++++++++ client/src/pm3_fit.h | 119 +++ 4 files changed, 3197 insertions(+) create mode 100644 client/src/pm3_dsp.c create mode 100644 client/src/pm3_dsp.h create mode 100644 client/src/pm3_fit.c create mode 100644 client/src/pm3_fit.h diff --git a/client/src/pm3_dsp.c b/client/src/pm3_dsp.c new file mode 100644 index 000000000..36b3a3aff --- /dev/null +++ b/client/src/pm3_dsp.c @@ -0,0 +1,1812 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Host side DSP primitives - FFT, windows, power spectrum, STFT +//----------------------------------------------------------------------------- + +#include "pm3_dsp.h" + +#include +#include +#include + +#include "pm3_cmd.h" +#include "commonutil.h" // ARRAYLEN + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +// refuse to allocate a spectrogram larger than this, in doubles +#define PM3_DSP_MAX_STFT_CELLS (8 * 1024 * 1024) + +// peaks closer together than this many bins are treated as the same peak +#define PM3_DSP_PEAK_SEP 3 +// half width of the neighbourhood used to estimate the local noise floor +#define PM3_DSP_FLOOR_SPAN 48 +// a peak has to be at least this prominent to take part in comb finding +#define COMB_MIN_PROMINENCE 8.0 +// autocorrelation lag window when hunting for the message repeat period +#define PM3_DSP_ACORR_MIN_LAG 256 +#define PM3_DSP_ACORR_MAX_LAG 32768 +#define PM3_DSP_ACORR_MIN_STRENGTH 0.25 +// Schmitt trigger level for run length extraction, in standard deviations +#define PM3_DSP_RUN_HYST 0.35 +// longest run over chip, above which the code is carrying raw levels +#define PM3_DSP_RUN_TRANSITION 2.6 +// a run this many chips long or less counts as short +#define PM3_DSP_RUN_SHORT 2.5 +// Transition codes put essentially every run under that limit, raw levels +// leave a quarter of them above it. Measured over the T5577 corpus the two +// populations sit at 0.95..0.99 and 0.61..0.81, so the boundary goes in the +// middle of that gap rather than at the edge of either. +#define PM3_DSP_RUN_SHORT_FRAC 0.88 +// harmonics past this are not worth chasing +#define COMB_MAX_HARMONIC 12 +// symbol clocks outside this range are not something any LF tag uses +#define COMB_MIN_CLK 4.0 +#define COMB_MAX_CLK 600.0 + +size_t pm3_next_pow2(size_t v) { + size_t p = 1; + while (p < v && p < PM3_DSP_MAX_FFT) { + p <<= 1; + } + return p; +} + +static bool is_pow2(size_t v) { + return (v != 0) && ((v & (v - 1)) == 0); +} + +pm3_fft_plan_t *pm3_fft_plan_create(size_t n) { + + if (is_pow2(n) == false || n < 2 || n > PM3_DSP_MAX_FFT) { + return NULL; + } + + pm3_fft_plan_t *plan = calloc(1, sizeof(pm3_fft_plan_t)); + if (plan == NULL) { + return NULL; + } + + plan->n = n; + plan->twiddle = calloc(n / 2, sizeof(pm3_cplx_t)); + plan->rev = calloc(n, sizeof(uint32_t)); + if (plan->twiddle == NULL || plan->rev == NULL) { + pm3_fft_plan_destroy(plan); + return NULL; + } + + for (size_t k = 0; k < n / 2; k++) { + double ang = -2.0 * M_PI * (double)k / (double)n; + plan->twiddle[k].re = cos(ang); + plan->twiddle[k].im = sin(ang); + } + + // bit reversal permutation, built incrementally so we never need to know the width + size_t shift = 0; + while ((((size_t)1) << shift) < n) { + shift++; + } + + for (size_t i = 0; i < n; i++) { + + size_t r = 0; + for (size_t b = 0; b < shift; b++) { + + if (i & (((size_t)1) << b)) { + r |= ((size_t)1) << (shift - 1 - b); + } + } + plan->rev[i] = (uint32_t)r; + } + + return plan; +} + +void pm3_fft_plan_destroy(pm3_fft_plan_t *plan) { + if (plan == NULL) { + return; + } + free(plan->twiddle); + free(plan->rev); + free(plan); +} + +void pm3_fft(const pm3_fft_plan_t *plan, pm3_cplx_t *data, bool inverse) { + + if (plan == NULL || data == NULL) { + return; + } + + const size_t n = plan->n; + + // the inverse transform is the forward transform of the conjugate, + // conjugated again and scaled. Keeps a single twiddle table. + if (inverse) { + for (size_t i = 0; i < n; i++) { + data[i].im = -data[i].im; + } + } + + for (size_t i = 0; i < n; i++) { + size_t j = plan->rev[i]; + if (j > i) { + pm3_cplx_t tmp = data[i]; + data[i] = data[j]; + data[j] = tmp; + } + } + + for (size_t span = 2; span <= n; span <<= 1) { + + const size_t half = span / 2; + const size_t step = n / span; + + for (size_t base = 0; base < n; base += span) { + for (size_t k = 0; k < half; k++) { + + const pm3_cplx_t w = plan->twiddle[k * step]; + pm3_cplx_t *lo = &data[base + k]; + pm3_cplx_t *hi = &data[base + k + half]; + + const double tr = hi->re * w.re - hi->im * w.im; + const double ti = hi->re * w.im + hi->im * w.re; + + hi->re = lo->re - tr; + hi->im = lo->im - ti; + lo->re += tr; + lo->im += ti; + } + } + } + + if (inverse) { + const double scale = 1.0 / (double)n; + for (size_t i = 0; i < n; i++) { + data[i].re *= scale; + data[i].im *= -scale; + } + } +} + +bool pm3_window_from_str(const char *str, pm3_window_t *out) { + + if (str == NULL || out == NULL) { + return false; + } + + if (strcmp(str, "hann") == 0) { + *out = PM3_WIN_HANN; + } else if (strcmp(str, "hamming") == 0) { + *out = PM3_WIN_HAMMING; + } else if (strcmp(str, "blackman") == 0) { + *out = PM3_WIN_BLACKMAN; + } else if (strcmp(str, "rect") == 0) { + *out = PM3_WIN_RECT; + } else { + return false; + } + return true; +} + +const char *pm3_window_name(pm3_window_t win) { + switch (win) { + case PM3_WIN_HANN: + return "hann"; + case PM3_WIN_HAMMING: + return "hamming"; + case PM3_WIN_BLACKMAN: + return "blackman"; + case PM3_WIN_RECT: + return "rect"; + } + return "unknown"; +} + +void pm3_apply_window(double *data, size_t len, pm3_window_t win) { + + if (data == NULL || len == 0 || win == PM3_WIN_RECT) { + return; + } + + if (len == 1) { + data[0] = 0; + return; + } + + const double denom = (double)(len - 1); + + for (size_t i = 0; i < len; i++) { + + const double phase = 2.0 * M_PI * (double)i / denom; + double w = 1.0; + + switch (win) { + case PM3_WIN_HANN: { + w = 0.5 * (1.0 - cos(phase)); + break; + } + case PM3_WIN_HAMMING: { + w = 0.54 - 0.46 * cos(phase); + break; + } + case PM3_WIN_BLACKMAN:{ + w = 0.42 - 0.5 * cos(phase) + 0.08 * cos(2.0 * phase); + break; + } + case PM3_WIN_RECT: { + break; + } + } + data[i] *= w; + } +} + +void pm3_remove_mean(double *data, size_t len) { + + if (data == NULL || len == 0) { + return; + } + + double sum = 0.0; + for (size_t i = 0; i < len; i++) { + sum += data[i]; + } + + const double mean = sum / (double)len; + for (size_t i = 0; i < len; i++) { + data[i] -= mean; + } +} + +double *pm3_extract(const int32_t *src, size_t len, size_t start, size_t count) { + + if (src == NULL || count == 0 || start >= len || (len - start) < count) { + return NULL; + } + + double *out = calloc(count, sizeof(double)); + if (out == NULL) { + return NULL; + } + + for (size_t i = 0; i < count; i++) { + out[i] = (double)src[start + i]; + } + + // LF captures carry a large DC offset. Left in place it dominates bin 0 + // and its leakage skirt swamps everything we care about. + pm3_remove_mean(out, count); + + double energy = 0.0; + for (size_t i = 0; i < count; i++) { + energy += out[i] * out[i]; + } + + // a constant input leaves nothing behind, hand back the zeros and let the + // caller decide what to say about it + if (energy > 0.0) { + const double scale = 1.0 / sqrt(energy / (double)count); + for (size_t i = 0; i < count; i++) { + out[i] *= scale; + } + } + + return out; +} + +void pm3_delay_multiply(const double *in, size_t len, size_t lag, double *out) { + + if (in == NULL || out == NULL || len == 0) { + return; + } + + for (size_t i = 0; i < len; i++) { + out[i] = (i < lag) ? 0.0 : in[i] * in[i - lag]; + } + + pm3_remove_mean(out, len); +} + +int pm3_spectrum(const double *sig, size_t len, size_t n, pm3_window_t win, pm3_spectrum_t *out) { + + if (sig == NULL || out == NULL || len == 0 || is_pow2(n) == false || n < 2) { + return PM3_EINVARG; + } + + memset(out, 0, sizeof(pm3_spectrum_t)); + + pm3_fft_plan_t *plan = pm3_fft_plan_create(n); + if (plan == NULL) { + return PM3_EMALLOC; + } + + pm3_cplx_t *buf = calloc(n, sizeof(pm3_cplx_t)); + double *win_buf = calloc(n, sizeof(double)); + if (buf == NULL || win_buf == NULL) { + free(buf); + free(win_buf); + pm3_fft_plan_destroy(plan); + return PM3_EMALLOC; + } + + const size_t take = (len < n) ? len : n; + memcpy(win_buf, sig, take * sizeof(double)); + pm3_apply_window(win_buf, n, win); + + for (size_t i = 0; i < n; i++) { + buf[i].re = win_buf[i]; + buf[i].im = 0.0; + } + free(win_buf); + + pm3_fft(plan, buf, false); + pm3_fft_plan_destroy(plan); + + out->n = n; + out->nbins = (n / 2) + 1; + out->mag = calloc(out->nbins, sizeof(double)); + if (out->mag == NULL) { + free(buf); + memset(out, 0, sizeof(pm3_spectrum_t)); + return PM3_EMALLOC; + } + + for (size_t i = 0; i < out->nbins; i++) { + out->mag[i] = sqrt(buf[i].re * buf[i].re + buf[i].im * buf[i].im); + } + free(buf); + + out->peak = 0.0; + out->peak_bin = 0; + for (size_t i = PM3_DSP_DC_GUARD; i < out->nbins; i++) { + if (out->mag[i] > out->peak) { + out->peak = out->mag[i]; + out->peak_bin = i; + } + } + + return PM3_SUCCESS; +} + +void pm3_spectrum_free(pm3_spectrum_t *spec) { + if (spec == NULL) { + return; + } + free(spec->mag); + memset(spec, 0, sizeof(pm3_spectrum_t)); +} + +// noise floor around `bin`, ignoring the peak itself and its immediate skirt +static double local_floor(const pm3_spectrum_t *spec, size_t bin) { + + double sum = 0.0; + size_t cnt = 0; + + const size_t lo = (bin > PM3_DSP_FLOOR_SPAN) ? bin - PM3_DSP_FLOOR_SPAN : PM3_DSP_DC_GUARD; + size_t hi = bin + PM3_DSP_FLOOR_SPAN; + if (hi >= spec->nbins) { + hi = spec->nbins - 1; + } + + for (size_t i = lo; i <= hi; i++) { + if (i + PM3_DSP_PEAK_SEP >= bin && i <= bin + PM3_DSP_PEAK_SEP) { + continue; + } + sum += spec->mag[i]; + cnt++; + } + + if (cnt == 0) { + return 0.0; + } + return sum / (double)cnt; +} + +size_t pm3_find_peaks(const pm3_spectrum_t *spec, pm3_peak_t *peaks, size_t max_peaks) { + + if (spec == NULL || spec->mag == NULL || peaks == NULL || max_peaks == 0) { + return 0; + } + if (spec->nbins < PM3_DSP_DC_GUARD + 3) { + return 0; + } + + size_t found = 0; + + // greedy: repeatedly take the strongest local maximum that is not already + // covered by something we picked + for (size_t pick = 0; pick < max_peaks; pick++) { + + double best = 0.0; + size_t best_bin = 0; + bool have = false; + + for (size_t i = PM3_DSP_DC_GUARD + 1; i + 1 < spec->nbins; i++) { + + if (spec->mag[i] < spec->mag[i - 1] || spec->mag[i] < spec->mag[i + 1]) { + continue; + } + + bool taken = false; + for (size_t k = 0; k < found; k++) { + const size_t d = (peaks[k].bin > i) ? peaks[k].bin - i : i - peaks[k].bin; + if (d <= PM3_DSP_PEAK_SEP) { + taken = true; + break; + } + } + if (taken) { + continue; + } + + if (have == false || spec->mag[i] > best) { + best = spec->mag[i]; + best_bin = i; + have = true; + } + } + + if (have == false || best <= 0.0) { + break; + } + + // parabolic interpolation over the three bins around the peak, in dB. + // The fractional part is the whole point of this table. + const double a = 20.0 * log10(spec->mag[best_bin - 1] + 1e-30); + const double b = 20.0 * log10(spec->mag[best_bin] + 1e-30); + const double c = 20.0 * log10(spec->mag[best_bin + 1] + 1e-30); + const double denom = a - (2.0 * b) + c; + + double delta = 0.0; + if (fabs(denom) > 1e-12) { + delta = 0.5 * (a - c) / denom; + } + if (delta > 0.5) { + delta = 0.5; + } + if (delta < -0.5) { + delta = -0.5; + } + + peaks[found].bin = best_bin; + peaks[found].freq = ((double)best_bin + delta) / (double)spec->n; + peaks[found].clk = (peaks[found].freq > 0.0) ? 1.0 / peaks[found].freq : 0.0; + peaks[found].mag_db = 20.0 * log10((best / (spec->peak + 1e-30)) + 1e-30); + + const double floor_mag = local_floor(spec, best_bin); + peaks[found].prominence = 20.0 * log10((best / (floor_mag + 1e-30)) + 1e-30); + + found++; + } + + return found; +} + +int pm3_stft(const double *sig, size_t len, size_t n, size_t hop, pm3_window_t win, double anchor, pm3_stft_t *out) { + + if (sig == NULL || out == NULL || hop == 0 || is_pow2(n) == false || n < 2) { + return PM3_EINVARG; + } + if (len < n) { + return PM3_EINVARG; + } + + memset(out, 0, sizeof(pm3_stft_t)); + + const size_t nframes = ((len - n) / hop) + 1; + const size_t nbins = (n / 2) + 1; + + if (nframes == 0 || (nframes * nbins) > PM3_DSP_MAX_STFT_CELLS) { + return PM3_EINVARG; + } + + out->n = n; + out->hop = hop; + out->nframes = nframes; + out->nbins = nbins; + out->frame_start = calloc(nframes, sizeof(size_t)); + out->peak_freq = calloc(nframes, sizeof(double)); + out->peak_mag = calloc(nframes, sizeof(double)); + out->frame_energy = calloc(nframes, sizeof(double)); + out->ridge = calloc(nframes * nbins, sizeof(double)); + + if (out->frame_start == NULL || out->peak_freq == NULL || out->peak_mag == NULL || out->frame_energy == NULL || out->ridge == NULL) { + pm3_stft_free(out); + return PM3_EMALLOC; + } + + pm3_fft_plan_t *plan = pm3_fft_plan_create(n); + pm3_cplx_t *buf = calloc(n, sizeof(pm3_cplx_t)); + double *frame = calloc(n, sizeof(double)); + + if (plan == NULL || buf == NULL || frame == NULL) { + pm3_fft_plan_destroy(plan); + free(buf); + free(frame); + pm3_stft_free(out); + return PM3_EMALLOC; + } + + for (size_t f = 0; f < nframes; f++) { + + const size_t off = f * hop; + out->frame_start[f] = off; + + memcpy(frame, sig + off, n * sizeof(double)); + pm3_remove_mean(frame, n); + pm3_apply_window(frame, n, win); + + for (size_t i = 0; i < n; i++) { + buf[i].re = frame[i]; + buf[i].im = 0.0; + } + + pm3_fft(plan, buf, false); + + double best = 0.0; + size_t best_bin = 0; + double energy = 0.0; + + size_t search_lo = PM3_DSP_DC_GUARD; + size_t search_hi = nbins; + + if (anchor > 0.0) { + + const double lo = (anchor / 1.2) * (double)n; + const double hi = (anchor * 1.2) * (double)n; + + if (lo > (double)search_lo) { + search_lo = (size_t)lo; + } + + if (hi < (double)search_hi) { + search_hi = (size_t)hi + 1; + } + + if (search_hi <= search_lo + 2) { + search_lo = PM3_DSP_DC_GUARD; + search_hi = nbins; + } + } + + for (size_t i = 0; i < nbins; i++) { + + const double m = sqrt(buf[i].re * buf[i].re + buf[i].im * buf[i].im); + + out->ridge[(f * nbins) + i] = m; + + if (i >= PM3_DSP_DC_GUARD) { + energy += m * m; + } + + if (i >= search_lo && i < search_hi && m > best) { + best = m; + best_bin = i; + } + } + + // sub bin refine the dominant line so drift shows up as a smooth curve + // rather than a staircase of whole bins + double delta = 0.0; + if (best_bin > 0 && best_bin + 1 < nbins) { + + const double a = 20.0 * log10(out->ridge[(f * nbins) + best_bin - 1] + 1e-30); + const double b = 20.0 * log10(out->ridge[(f * nbins) + best_bin] + 1e-30); + const double c = 20.0 * log10(out->ridge[(f * nbins) + best_bin + 1] + 1e-30); + const double denom = a - (2.0 * b) + c; + + if (fabs(denom) > 1e-12) { + delta = 0.5 * (a - c) / denom; + } + + if (delta > 0.5) { + delta = 0.5; + } + + if (delta < -0.5) { + delta = -0.5; + } + } + + out->peak_mag[f] = best; + out->peak_freq[f] = ((double)best_bin + delta) / (double)n; + out->frame_energy[f] = sqrt(energy); + } + + pm3_fft_plan_destroy(plan); + free(buf); + free(frame); + return PM3_SUCCESS; +} + +void pm3_stft_free(pm3_stft_t *st) { + if (st == NULL) { + return; + } + free(st->frame_start); + free(st->peak_freq); + free(st->peak_mag); + free(st->frame_energy); + free(st->ridge); + memset(st, 0, sizeof(pm3_stft_t)); +} + +const char *pm3_family_name(pm3_family_t fam) { + switch (fam) { + case PM3_FAM_UNKNOWN: + return "unknown"; + case PM3_FAM_ASK: + return "ASK"; + case PM3_FAM_FSK: + return "FSK"; + case PM3_FAM_PSK: + return "PSK"; + case PM3_FAM_NRZ: + return "NRZ"; + case PM3_FAM_MANCHESTER: + return "MANCHESTER"; + } + return "unknown"; +} + +const char *pm3_confidence_name(pm3_confidence_t conf) { + switch (conf) { + case PM3_CONF_NONE: + return "none"; + case PM3_CONF_LOW: + return "low"; + case PM3_CONF_MEDIUM: + return "medium"; + case PM3_CONF_HIGH: + return "high"; + } + return "none"; +} + +// prominence of the plain spectrum at an arbitrary frequency, used to ask +// "is there a line here as well, or only after the non linearity?" +static double prominence_at(const pm3_spectrum_t *spec, double freq) { + + if (freq <= 0.0) { + return 0.0; + } + + size_t bin = (size_t)((freq * (double)spec->n) + 0.5); + if (bin < PM3_DSP_DC_GUARD || bin + 1 >= spec->nbins) { + return 0.0; + } + + // take the strongest of the three bins around the target, a fractional + // clock puts the line between bins + double best = spec->mag[bin]; + if (spec->mag[bin - 1] > best) { + best = spec->mag[bin - 1]; + } + + if (spec->mag[bin + 1] > best) { + best = spec->mag[bin + 1]; + } + + return 20.0 * log10((best / (local_floor(spec, bin) + 1e-30)) + 1e-30); +} + +// strongest peak whose implied clock falls inside [lo, hi] samples/symbol +static const pm3_peak_t *peak_in_range(const pm3_peak_t *peaks, size_t n, double lo, double hi) { + + const pm3_peak_t *best = NULL; + + for (size_t i = 0; i < n; i++) { + if (peaks[i].clk < lo || peaks[i].clk > hi) { + continue; + } + + if (best == NULL || peaks[i].prominence > best->prominence) { + best = &peaks[i]; + } + } + return best; +} + +static bool near(double got, double want, double tol) { + return fabs(got - want) <= (want * tol); +} + +// Look for the two field clock lines the existing FSK demodulators expect. +static bool find_fsk_pair(const pm3_peak_t *peaks, size_t n, double *lo, double *hi, double *ratio) { + + static const double pairs[][2] = { {8.0, 10.0}, {5.0, 8.0} }; + + for (size_t p = 0; p < ARRAYLEN(pairs); p++) { + + const pm3_peak_t *a = NULL; + const pm3_peak_t *b = NULL; + + for (size_t i = 0; i < n; i++) { + if (near(peaks[i].clk, pairs[p][0], 0.12) && (a == NULL || peaks[i].prominence > a->prominence)) { + a = &peaks[i]; + } + + if (near(peaks[i].clk, pairs[p][1], 0.12) && (b == NULL || peaks[i].prominence > b->prominence)) { + b = &peaks[i]; + } + } + + // Both tones have to be genuinely strong, and comparable to each + // other: an FSK stream spends time on each, so neither line dominates. + // A single PSK subcarrier plus one spurious neighbour would otherwise pass for a pair. + if (a != NULL && b != NULL + && a->prominence > 10.0 && b->prominence > 10.0 + && fabs(a->mag_db - b->mag_db) < 10.0) { + + *hi = a->clk; + *lo = b->clk; + *ratio = b->clk / a->clk; + return true; + } + } + return false; +} + +// Find the spacing of the harmonic comb that best explains a set of peaks. +// +// Squaring a symbol stream produces energy at every integer multiple of the symbol rate. +// Picking the tallest line therefore lands on a harmonic as often as not. +// Try each peak and each peak divided by a small integer as a candidate spacing +// and keep whichever one places the most peaks on a multiple of itself +static double comb_fundamental(const pm3_peak_t *peaks, size_t npeaks, size_t n, double min_clk) { + + double best = 0.0; + size_t best_count = 0; + + for (size_t i = 0; i < npeaks; i++) { + + if (peaks[i].prominence < COMB_MIN_PROMINENCE) { + continue; + } + + for (size_t div = 1; div <= 4; div++) { + + const double cand = peaks[i].freq / (double)div; + if (cand <= 0.0) { + continue; + } + + size_t count = 0; + for (size_t j = 0; j < npeaks; j++) { + + if (peaks[j].prominence < COMB_MIN_PROMINENCE) { + continue; + } + + const double k = floor((peaks[j].freq / cand) + 0.5); + if (k < 1.0 || k > (double)COMB_MAX_HARMONIC) { + continue; + } + + // a line may sit about a bin away from the ideal multiple. + // The tolerance is tied to the peak's own frequency, not to the candidate spacing + const double tol = (1.5 / (double)n) + (0.015 * peaks[j].freq); + if (fabs(peaks[j].freq - (k * cand)) <= tol) { + count++; + } + } + + if (count > best_count || (count == best_count && cand > best)) { + best_count = count; + best = cand; + } + } + } + + // a single line is not a comb + if (best_count < 2) { + return 0.0; + } + // and no LF tag clocks itself outside this range + if (best < (1.0 / COMB_MAX_CLK) || best > (1.0 / min_clk)) { + return 0.0; + } + return best; +} + +static pm3_confidence_t confidence_from_db(double db) { + if (db >= 12.0) { + return PM3_CONF_HIGH; + } + + if (db >= 6.0) { + return PM3_CONF_MEDIUM; + } + + if (db >= 2.0) { + return PM3_CONF_LOW; + } + + return PM3_CONF_NONE; +} + +static int cmp_double(const void *a, const void *b); + +// Rectified, low passed copy of `sig` with its own DC left in place. +// The window is centred so a switching edge keeps its position +static double *envelope_raw(const double *sig, size_t len, size_t smooth) { + + if (sig == NULL || len == 0) { + return NULL; + } + + if (smooth == 0) { + smooth = PM3_DSP_ENVELOPE_SMOOTH; + } + if (smooth > len) { + smooth = len; + } + + double *out = calloc(len, sizeof(double)); + double *ps = calloc(len + 1, sizeof(double)); + if (out == NULL || ps == NULL) { + free(out); + free(ps); + return NULL; + } + + double mean = 0.0; + for (size_t i = 0; i < len; i++) { + mean += sig[i]; + } + mean /= (double)len; + + for (size_t i = 0; i < len; i++) { + ps[i + 1] = ps[i] + fabs(sig[i] - mean); + } + + const size_t half = smooth / 2; + + for (size_t i = 0; i < len; i++) { + const size_t lo = (i > half) ? (i - half) : 0; + size_t hi = i + smooth - half; + if (hi > len) { + hi = len; + } + out[i] = (ps[hi] - ps[lo]) / (double)(hi - lo); + } + + free(ps); + return out; +} + +double *pm3_envelope(const double *sig, size_t len, size_t smooth) { + + double *out = envelope_raw(sig, len, smooth); + if (out == NULL) { + return NULL; + } + + pm3_remove_mean(out, len); + + double energy = 0.0; + for (size_t i = 0; i < len; i++) { + energy += out[i] * out[i]; + } + + if (energy > 0.0) { + const double scale = 1.0 / sqrt(energy / (double)len); + for (size_t i = 0; i < len; i++) { + out[i] *= scale; + } + } + + return out; +} + +double pm3_envelope_depth(const double *sig, size_t len) { + + if (sig == NULL || len < 32) { + return 0.0; + } + + double *env = envelope_raw(sig, len, 0); + if (env == NULL) { + return 0.0; + } + + // percentiles rather than min and max. + // one clipped sample or one dead spot in the capture does not impact + const size_t want = 8192; + const size_t stride = (len > want) ? (len / want) : 1; + + double *s = calloc((len / stride) + 1, sizeof(double)); + if (s == NULL) { + free(env); + return 0.0; + } + + size_t cnt = 0; + for (size_t i = 0; i < len; i += stride) { + s[cnt++] = env[i]; + } + free(env); + + if (cnt < 16) { + free(s); + return 0.0; + } + + qsort(s, cnt, sizeof(double), cmp_double); + + const double hi = s[(size_t)((double)cnt * 0.9)]; + const double lo = s[(size_t)((double)cnt * 0.1)]; + free(s); + + return ((hi + lo) > 0.0) ? ((hi - lo) / (hi + lo)) : 0.0; +} + +void pm3_slice_free(pm3_slice_t *out) { + + if (out == NULL) { + return; + } + free(out->bits); + out->bits = NULL; + out->nbits = 0; +} + +int pm3_ask_slice(const double *sig, size_t len, double clk, pm3_slice_t *out) { + + if (sig == NULL || out == NULL || clk < 2.0 || len < (size_t)(clk * 4.0)) { + return PM3_EINVARG; + } + + memset(out, 0, sizeof(pm3_slice_t)); + + // Prefix sums, so the average over a chip window costs two lookups no matter how wide the chip is. + double *ps = calloc(len + 1, sizeof(double)); + if (ps == NULL) { + return PM3_EMALLOC; + } + for (size_t i = 0; i < len; i++) { + ps[i + 1] = ps[i] + sig[i]; + } + + // Robust levels rather than min and max: one clipped sample does not set the threshold for the capture. + const size_t want = 4096; + const size_t stride = (len > want) ? (len / want) : 1; + + double *s = calloc((len / stride) + 1, sizeof(double)); + if (s == NULL) { + free(ps); + return PM3_EMALLOC; + } + + size_t cnt = 0; + for (size_t i = 0; i < len; i += stride) { + s[cnt++] = sig[i]; + } + qsort(s, cnt, sizeof(double), cmp_double); + + const double lo = s[(size_t)((double)cnt * 0.1)]; + const double hi = s[(size_t)((double)cnt * 0.9)]; + free(s); + + const double thr = (hi + lo) * 0.5; + const double span = (hi - lo) * 0.5; + + if (span <= 0.0) { + free(ps); + return PM3_ESOFT; + } + + const size_t nsym = (size_t)(((double)len - clk) / clk); + if (nsym < 4) { + free(ps); + return PM3_ESOFT; + } + + // The middle half of the chip. Wide enough to average the noise down, + // narrow enough that a late edge on either side never reaches it. + const size_t half = (size_t)(clk * 0.25); + const size_t nphase = (size_t)clk; + + double best_score = -1.0; + size_t best_phase = 0; + + for (size_t p = 0; p < nphase; p++) { + + double score = 0.0; + + for (size_t k = 0; k < nsym; k++) { + + const double centre = (double)p + ((double)k + 0.5) * clk; + const size_t a = (size_t)(centre - (double)half); + const size_t b = (size_t)(centre + (double)half); + + if (b >= len || b <= a) { + break; + } + + const double mean = (ps[b] - ps[a]) / (double)(b - a); + score += fabs(mean - thr); + } + + if (score > best_score) { + best_score = score; + best_phase = p; + } + } + + out->bits = calloc(nsym, sizeof(uint8_t)); + if (out->bits == NULL) { + free(ps); + return PM3_EMALLOC; + } + + const double dead = span * 0.15; + double eye = 0.0; + + for (size_t k = 0; k < nsym; k++) { + + const double centre = (double)best_phase + ((double)k + 0.5) * clk; + const size_t a = (size_t)(centre - (double)half); + const size_t b = (size_t)(centre + (double)half); + + if (b >= len || b <= a) { + break; + } + + const double mean = (ps[b] - ps[a]) / (double)(b - a); + const double dist = fabs(mean - thr); + + if (dist < dead) { + out->bits[out->nbits] = 7; + out->nerrors++; + } else { + out->bits[out->nbits] = (mean > thr) ? 1 : 0; + } + + eye += dist / span; + out->nbits++; + } + + free(ps); + + out->phase = best_phase; + out->eye = (out->nbits) ? (eye / (double)out->nbits) : 0.0; + + return (out->nbits > 0) ? PM3_SUCCESS : PM3_ESOFT; +} + +bool pm3_is_switched_carrier(const double *sig, size_t len) { + + if (sig == NULL || len < 256) { + return false; + } + + double mean = 0.0; + for (size_t i = 0; i < len; i++) { + mean += sig[i]; + } + mean /= (double)len; + + size_t flips = 0, pairs = 0; + int prev = 0; + + for (size_t i = 0; i < len; i++) { + + const double d = sig[i] - mean; + const int s = (d > 0.0) ? 1 : ((d < 0.0) ? -1 : 0); + + if (s == 0) { + continue; + } + + if (prev != 0) { + pairs++; + if (s != prev) { + flips++; + } + } + prev = s; + } + + if (pairs == 0) { + return false; + } + + if (((double)flips / (double)pairs) < PM3_DSP_FLIP_FRAC) { + return false; + } + + return (pm3_envelope_depth(sig, len) > PM3_DSP_ENVELOPE_DEPTH); +} + +static int analyse_once(const double *sig, size_t len, size_t n, pm3_window_t win, pm3_spec_analysis_t *out) { + + if (sig == NULL || out == NULL || len == 0) { + return PM3_EINVARG; + } + + memset(out, 0, sizeof(pm3_spec_analysis_t)); + + const size_t top = PM3_DSP_MAX_PEAKS; + + pm3_spectrum_t plain; + int res = pm3_spectrum(sig, len, n, win, &plain); + if (res != PM3_SUCCESS) { + return res; + } + + out->npeaks = pm3_find_peaks(&plain, out->peaks, top); + + double *work = calloc(len, sizeof(double)); + if (work == NULL) { + pm3_spectrum_free(&plain); + return PM3_EMALLOC; + } + + pm3_spectrum_t sq; + memset(&sq, 0, sizeof(sq)); + + pm3_delay_multiply(sig, len, 0, work); + if (pm3_spectrum(work, len, n, win, &sq) == PM3_SUCCESS) { + out->have_sq = true; + out->nsq_peaks = pm3_find_peaks(&sq, out->sq_peaks, top); + } + + static const size_t lags[PM3_DSP_NDELAYS] = { 1, 2, 4, 8 }; + out->ndelay = PM3_DSP_NDELAYS; + + for (size_t i = 0; i < PM3_DSP_NDELAYS; i++) { + + out->delay[i] = lags[i]; + pm3_delay_multiply(sig, len, lags[i], work); + + pm3_spectrum_t ds; + if (pm3_spectrum(work, len, n, win, &ds) != PM3_SUCCESS) { + continue; + } + + pm3_peak_t got[1]; + if (pm3_find_peaks(&ds, got, 1) == 1) { + out->delay_peak[i] = got[0]; + out->delay_valid[i] = true; + } + pm3_spectrum_free(&ds); + } + free(work); + + // low frequency energy relative to the strongest line. + // NRZ piles up at DC, + // Manchester and biphase have null. + double dc_sum = 0.0; + size_t dc_cnt = 0; + const size_t dc_hi = (plain.nbins < 32) ? plain.nbins : 32; + for (size_t i = PM3_DSP_DC_GUARD; i < dc_hi; i++) { + dc_sum += plain.mag[i]; + dc_cnt++; + } + out->dc_ratio = (dc_cnt && plain.peak > 0.0) ? (dc_sum / (double)dc_cnt) / plain.peak : 0.0; + + const pm3_peak_t *carrier = peak_in_range(out->peaks, out->npeaks, 1.8, 12.0); + if (carrier != NULL && carrier->prominence > 4.0) { + out->have_carrier = true; + out->carrier_clk = carrier->clk; + } + + if (out->have_carrier && out->carrier_clk <= PM3_DSP_NYQUIST_CLK) { + out->carrier_at_nyquist = true; + out->envelope_depth = pm3_envelope_depth(sig, len); + out->needs_envelope = (out->envelope_depth > PM3_DSP_ENVELOPE_DEPTH); + } + + const bool is_fsk = find_fsk_pair(out->peaks, out->npeaks, &out->fsk_clk_lo, &out->fsk_clk_hi, &out->fsk_ratio); + + const double min_clk = is_fsk ? (out->fsk_clk_lo * 2.0) : COMB_MIN_CLK; + + double fundamental = 0.0; + if (out->have_sq) { + fundamental = comb_fundamental(out->sq_peaks, out->nsq_peaks, sq.n, min_clk); + } + + if (fundamental > 0.0) { + + out->symbol_clk = 1.0 / fundamental; + out->have_symbol_clk = true; + out->sq_prominence = prominence_at(&sq, fundamental); + + const double second = prominence_at(&sq, fundamental * 2.0); + out->transition_coded = (second - out->sq_prominence) > 6.0; + + } else { + const pm3_peak_t *sym_plain = peak_in_range(out->peaks, out->npeaks, 12.0, 600.0); + if (sym_plain != NULL) { + out->symbol_clk = sym_plain->clk; + out->have_symbol_clk = true; + } + } + + if (out->have_symbol_clk) { + out->sym_prominence = prominence_at(&plain, 1.0 / out->symbol_clk); + } + + if (is_fsk) { + + out->family = PM3_FAM_FSK; + out->confidence = confidence_from_db( + 0.5 * (prominence_at(&plain, 1.0 / out->fsk_clk_lo) + prominence_at(&plain, 1.0 / out->fsk_clk_hi)) + ); + + } else if (out->have_carrier) { + + out->family = PM3_FAM_PSK; + out->confidence = confidence_from_db(prominence_at(&plain, 1.0 / out->carrier_clk)); + + } else if (out->transition_coded) { + + out->family = PM3_FAM_MANCHESTER; + out->confidence = confidence_from_db(out->sq_prominence); + + } else if (out->dc_ratio > 0.4) { + + out->family = PM3_FAM_NRZ; + out->confidence = confidence_from_db(out->sq_prominence); + + } else if (out->have_symbol_clk && out->sq_prominence > 6.0) { + + out->family = PM3_FAM_ASK; + out->confidence = confidence_from_db(out->sq_prominence); + + } else { + out->family = PM3_FAM_UNKNOWN; + out->confidence = PM3_CONF_NONE; + } + + pm3_spectrum_free(&plain); + if (out->have_sq) { + pm3_spectrum_free(&sq); + } + return PM3_SUCCESS; +} + +int pm3_analyse(const double *sig, size_t len, size_t n, pm3_window_t win, pm3_spec_analysis_t *out) { + + int res = analyse_once(sig, len, n, win, out); + if (res != PM3_SUCCESS || out->needs_envelope == false) { + return res; + } + + const double carrier_clk = out->carrier_clk; + const double depth = out->envelope_depth; + + double *env = pm3_envelope(sig, len, 0); + if (env == NULL) { + return res; + } + + pm3_spec_analysis_t inner; + const int inner_res = analyse_once(env, len, n, win, &inner); + free(env); + + if (inner_res != PM3_SUCCESS) { + return res; + } + + *out = inner; + out->carrier_at_nyquist = true; + out->needs_envelope = true; + out->envelope_depth = depth; + out->carrier_clk = carrier_clk; + out->have_carrier = true; + + return PM3_SUCCESS; +} + +static int cmp_double(const void *a, const void *b) { + + const double x = *(const double *)a; + const double y = *(const double *)b; + + if (x < y) { + return -1; + } + + if (x > y) { + return 1; + } + return 0; +} + +int pm3_signal_stats(const double *sig, size_t len, pm3_sigstat_t *out) { + + if (sig == NULL || out == NULL || len < 256) { + return PM3_EINVARG; + } + + memset(out, 0, sizeof(pm3_sigstat_t)); + + double sumsq = 0.0; + for (size_t i = 0; i < len; i++) { + sumsq += sig[i] * sig[i]; + } + + const double sd = sqrt(sumsq / (double)len); + if (sd <= 0.0) { + return PM3_ESOFT; + } + + const double hi_th = sd * PM3_DSP_RUN_HYST; + const double lo_th = -hi_th; + + double *runs = calloc(len + 1, sizeof(double)); + if (runs == NULL) { + return PM3_EMALLOC; + } + + size_t nruns = 0; + int state = 0; + size_t last_edge = 0; + + for (size_t i = 0; i < len; i++) { + + int next = state; + if (sig[i] > hi_th) { + next = 1; + } else if (sig[i] < lo_th) { + next = -1; + } + + if (state == 0) { + state = next; + last_edge = i; + continue; + } + + if (next != state) { + if (i > last_edge) { + runs[nruns++] = (double)(i - last_edge); + } + last_edge = i; + state = next; + } + } + + if (nruns < 16) { + free(runs); + return PM3_ESOFT; + } + + qsort(runs, nruns, sizeof(double), cmp_double); + + // The chip is the shortest run that actually repeats. Taking the outright + // minimum would pick up a glitch, so use a low percentile. + out->chip = runs[nruns / 10]; + out->long_run = runs[(nruns * 95) / 100]; + + if (out->chip <= 0.0) { + free(runs); + return PM3_ESOFT; + } + + out->run_ratio = out->long_run / out->chip; + + size_t shortish = 0; + const double limit = out->chip * PM3_DSP_RUN_SHORT; + for (size_t i = 0; i < nruns; i++) { + if (runs[i] <= limit) { + shortish++; + } + } + out->short_fraction = (double)shortish / (double)nruns; + out->transition_coded = (out->short_fraction > PM3_DSP_RUN_SHORT_FRAC); + out->valid = true; + + free(runs); + + out->period = pm3_autocorr_period(sig, len, &out->period_strength); + return PM3_SUCCESS; +} + +double pm3_autocorr_period(const double *sig, size_t len, double *strength) { + + if (strength != NULL) { + *strength = 0.0; + } + + if (sig == NULL || len < (PM3_DSP_ACORR_MIN_LAG * 4)) { + return 0.0; + } + + size_t n = pm3_next_pow2(len * 2); + if (n > PM3_DSP_MAX_FFT) { + n = PM3_DSP_MAX_FFT; + } + + pm3_fft_plan_t *plan = pm3_fft_plan_create(n); + pm3_cplx_t *buf = calloc(n, sizeof(pm3_cplx_t)); + if (plan == NULL || buf == NULL) { + pm3_fft_plan_destroy(plan); + free(buf); + return 0.0; + } + + const size_t take = (len < n / 2) ? len : n / 2; + for (size_t i = 0; i < take; i++) { + buf[i].re = sig[i]; + } + + pm3_fft(plan, buf, false); + + // Wiener-Khinchin: the autocorrelation is the inverse transform of the power spectrum + for (size_t i = 0; i < n; i++) { + const double p = (buf[i].re * buf[i].re) + (buf[i].im * buf[i].im); + buf[i].re = p; + buf[i].im = 0.0; + } + + pm3_fft(plan, buf, true); + pm3_fft_plan_destroy(plan); + + const double energy = buf[0].re; + if (energy <= 0.0) { + free(buf); + return 0.0; + } + + size_t hi = take / 2; + if (hi > PM3_DSP_ACORR_MAX_LAG) { + hi = PM3_DSP_ACORR_MAX_LAG; + } + + double best = 0.0; + size_t best_lag = 0; + + for (size_t lag = PM3_DSP_ACORR_MIN_LAG; lag < hi; lag++) { + + const double r = buf[lag].re / energy; + if (r <= best) { + continue; + } + // only interested in genuine local maxima + if (buf[lag].re < buf[lag - 1].re || buf[lag].re < buf[lag + 1].re) { + continue; + } + best = r; + best_lag = lag; + } + + double period = 0.0; + + if (best_lag > 0 && best >= PM3_DSP_ACORR_MIN_STRENGTH) { + + // sub sample refine + const double a = buf[best_lag - 1].re; + const double b = buf[best_lag].re; + const double c = buf[best_lag + 1].re; + const double denom = a - (2.0 * b) + c; + + double delta = 0.0; + if (fabs(denom) > 1e-30) { + delta = 0.5 * (a - c) / denom; + } + + if (delta > 0.5) { + delta = 0.5; + } + + if (delta < -0.5) { + delta = -0.5; + } + + period = (double)best_lag + delta; + if (strength != NULL) { + *strength = best; + } + } + + free(buf); + return period; +} + +double *pm3_resample(const double *in, size_t len, double ratio, size_t *out_len) { + + if (in == NULL || out_len == NULL || len < 2 || ratio <= 0.0) { + return NULL; + } + + const double want = (double)len * ratio; + if (want < 2.0 || want > (double)PM3_DSP_MAX_RESAMPLE) { + return NULL; + } + + const size_t n = (size_t)want; + double *out = calloc(n, sizeof(double)); + if (out == NULL) { + return NULL; + } + + for (size_t i = 0; i < n; i++) { + + const double pos = (double)i / ratio; + size_t idx = (size_t)pos; + + if (idx + 1 >= len) { + out[i] = in[len - 1]; + continue; + } + + const double frac = pos - (double)idx; + out[i] = (in[idx] * (1.0 - frac)) + (in[idx + 1] * frac); + } + + *out_len = n; + return out; +} + +// Coherent PSK receiver. +int pm3_psk_demod(const double *sig, size_t len, int fc, double clk, + uint8_t *bits, size_t *nbits, double *clk_out, int *phase_out, + double *score_out, uint8_t *abs_bits) { + + if (sig == NULL || bits == NULL || nbits == NULL) { + return PM3_EINVARG; + } + + const size_t w = (size_t)(clk + 0.5); + if (fc < 2 || clk < 4.0 || w < 4 || len < w * 8) { + return PM3_EINVARG; + } + + const size_t zlen = len - w + 1; + + double *zr = calloc(zlen, sizeof(double)); + double *zi = calloc(zlen, sizeof(double)); + if (zr == NULL || zi == NULL) { + free(zr); + free(zi); + return PM3_EMALLOC; + } + + // Mix down by the subcarrier and integrate over one symbol + { + double sr = 0.0, si = 0.0; + for (size_t i = 0; i < w; i++) { + const double a = -2.0 * M_PI * (double)i / (double)fc; + sr += sig[i] * cos(a); + si += sig[i] * sin(a); + } + zr[0] = sr; + zi[0] = si; + + for (size_t i = 1; i < zlen; i++) { + const double aout = -2.0 * M_PI * (double)(i - 1) / (double)fc; + const double ain = -2.0 * M_PI * (double)(i + w - 1) / (double)fc; + sr += (sig[i + w - 1] * cos(ain)) - (sig[i - 1] * cos(aout)); + si += (sig[i + w - 1] * sin(ain)) - (sig[i - 1] * sin(aout)); + zr[i] = sr; + zi[i] = si; + } + } + + // Symbol timing. + double best_clk = clk, best_score = -1.0; + int best_phase = 0; + + for (int step = -20; step <= 20; step++) { + + const double c = clk + ((double)step * 0.1); + if (c < 4.0) { + continue; + } + + for (size_t p = 0; p < w; p++) { + + double sum = 0.0; + size_t cnt = 0; + + for (double pos = (double)p; pos < (double)zlen; pos += c) { + const size_t k = (size_t)(pos + 0.5); + if (k >= zlen) { + break; + } + sum += sqrt((zr[k] * zr[k]) + (zi[k] * zi[k])); + cnt++; + } + + if (cnt < 8) { + continue; + } + + const double score = sum / (double)cnt; + if (score > best_score) { + best_score = score; + best_clk = c; + best_phase = (int)p; + } + } + } + + if (best_score < 0.0) { + free(zr); + free(zi); + return PM3_ESOFT; + } + + // Absolute phase reference, when the caller wants one. + double ref_r = 1.0, ref_i = 0.0; + + if (abs_bits != NULL) { + + double sr = 0.0, si = 0.0; + for (double pos = (double)best_phase; pos < (double)zlen; pos += best_clk) { + const size_t k = (size_t)(pos + 0.5); + if (k >= zlen) { + break; + } + sr += (zr[k] * zr[k]) - (zi[k] * zi[k]); + si += 2.0 * zr[k] * zi[k]; + } + + const double mag = sqrt((sr * sr) + (si * si)); + if (mag > 1e-12) { + const double half = 0.5 * atan2(si, sr); + ref_r = cos(half); + ref_i = sin(half); + } + } + + // Differential detection at the recovered timing. + size_t n = 0; + double prev_r = 0.0, prev_i = 0.0; + uint8_t phase = 0; + bool first = true; + + for (double pos = (double)best_phase; pos < (double)zlen && n < *nbits; pos += best_clk) { + + const size_t k = (size_t)(pos + 0.5); + if (k >= zlen) { + break; + } + + if (first == false) { + // Re( s * conj(s_prev) ) - negative means the phase reversed + if (((zr[k] * prev_r) + (zi[k] * prev_i)) < 0.0) { + phase ^= 1; + } + } + + prev_r = zr[k]; + prev_i = zi[k]; + first = false; + + if (abs_bits != NULL) { + // Re( z * conj(ref) ) - which side of the axis this symbol sits on + abs_bits[n] = (((zr[k] * ref_r) + (zi[k] * ref_i)) < 0.0) ? 1 : 0; + } + + bits[n++] = phase; + } + + free(zr); + free(zi); + + *nbits = n; + if (clk_out != NULL) { + *clk_out = best_clk; + } + if (phase_out != NULL) { + *phase_out = best_phase; + } + if (score_out != NULL) { + *score_out = best_score; + } + return (n >= 32) ? PM3_SUCCESS : PM3_ESOFT; +} + +// Coherent chip slicer for amplitude keyed traces. +int pm3_ask_chips(const double *sig, size_t len, double chip, + uint8_t *chips, size_t *nchips, double *chip_out, int *phase_out) { + + if (sig == NULL || chips == NULL || nchips == NULL) { + return PM3_EINVARG; + } + + const size_t w = (size_t)(chip + 0.5); + if (chip < 2.0 || w < 2 || len < w * 16) { + return PM3_EINVARG; + } + + const size_t clen = len - w + 1; + + double *c = calloc(clen, sizeof(double)); + if (c == NULL) { + return PM3_EMALLOC; + } + + double run = 0.0; + for (size_t i = 0; i < w; i++) { + run += sig[i]; + } + c[0] = run; + for (size_t i = 1; i < clen; i++) { + run += sig[i + w - 1] - sig[i - 1]; + c[i] = run; + } + + double best_chip = chip, best_score = -1.0; + int best_phase = 0; + + for (int step = -20; step <= 20; step++) { + + const double cc = chip + ((double)step * 0.05); + if (cc < 2.0) { + continue; + } + + for (size_t p = 0; p < w; p++) { + + double sum = 0.0; + size_t cnt = 0; + + for (double pos = (double)p; pos < (double)clen; pos += cc) { + const size_t k = (size_t)(pos + 0.5); + if (k >= clen) { + break; + } + sum += fabs(c[k]); + cnt++; + } + + if (cnt < 64) { + continue; + } + + const double score = sum / (double)cnt; + if (score > best_score) { + best_score = score; + best_chip = cc; + best_phase = (int)p; + } + } + } + + if (best_score < 0.0) { + free(c); + return PM3_ESOFT; + } + + size_t n = 0; + for (double pos = (double)best_phase; pos < (double)clen && n < *nchips; pos += best_chip) { + const size_t k = (size_t)(pos + 0.5); + if (k >= clen) { + break; + } + chips[n++] = (c[k] > 0.0) ? 1 : 0; + } + + free(c); + + *nchips = n; + if (chip_out != NULL) { + *chip_out = best_chip; + } + + if (phase_out != NULL) { + *phase_out = best_phase; + } + + return (n >= 64) ? PM3_SUCCESS : PM3_ESOFT; +} diff --git a/client/src/pm3_dsp.h b/client/src/pm3_dsp.h new file mode 100644 index 000000000..1920ac026 --- /dev/null +++ b/client/src/pm3_dsp.h @@ -0,0 +1,200 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Host side DSP primitives; FFT, windows, power spectrum, STFT +//----------------------------------------------------------------------------- + +#ifndef PM3_DSP_H__ +#define PM3_DSP_H__ + +#include +#include "common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define PM3_DSP_MAX_FFT 262144 +#define PM3_DSP_DC_GUARD 2 +#define PM3_DSP_MAX_RESAMPLE (40000 * 32) +#define PM3_DSP_NYQUIST_CLK 2.5 +#define PM3_DSP_ENVELOPE_SMOOTH 8 +#define PM3_DSP_ENVELOPE_DEPTH 0.6 + +#define PM3_DSP_FLIP_FRAC 0.7 + +typedef enum { + PM3_WIN_HANN = 0, + PM3_WIN_HAMMING, + PM3_WIN_BLACKMAN, + PM3_WIN_RECT, +} pm3_window_t; + +typedef struct { + double re; + double im; +} pm3_cplx_t; + +typedef struct { + size_t n; + pm3_cplx_t *twiddle; // n / 2 entries, exp(-2*pi*i*k/n) + uint32_t *rev; // n entries, bit reversal permutation +} pm3_fft_plan_t; + +typedef struct { + size_t n; // transform length + size_t nbins; // n / 2 + 1, input is real + double *mag; // linear magnitude per bin + double peak; // largest magnitude found + size_t peak_bin; // ... and where +} pm3_spectrum_t; + +typedef struct { + size_t bin; // integer bin the peak was found in + double freq; // cycles/sample, parabolic sub bin estimate + double clk; // samples/symbol, 1.0 / freq + double mag_db; // dB relative to the strongest bin in the spectrum + double prominence; // dB above the local noise floor +} pm3_peak_t; + +typedef struct { + size_t nframes; + size_t nbins; + size_t n; // window length + size_t hop; + size_t *frame_start; // nframes entries, offset of each frame + double *peak_freq; // nframes entries, cycles/sample of dominant bin + double *peak_mag; // nframes entries, magnitude of dominant bin + double *frame_energy; // nframes entries, total in band energy + double *ridge; // nframes * nbins, magnitude, row major +} pm3_stft_t; + + +#define PM3_DSP_MAX_PEAKS 16 +#define PM3_DSP_NDELAYS 4 + +typedef enum { + PM3_FAM_UNKNOWN = 0, + PM3_FAM_ASK, + PM3_FAM_FSK, + PM3_FAM_PSK, + PM3_FAM_NRZ, + PM3_FAM_MANCHESTER, +} pm3_family_t; + +typedef enum { + PM3_CONF_NONE = 0, + PM3_CONF_LOW, + PM3_CONF_MEDIUM, + PM3_CONF_HIGH, +} pm3_confidence_t; + +typedef struct { + + size_t npeaks; + pm3_peak_t peaks[PM3_DSP_MAX_PEAKS]; + bool have_sq; + size_t nsq_peaks; + pm3_peak_t sq_peaks[PM3_DSP_MAX_PEAKS]; + size_t ndelay; + size_t delay[PM3_DSP_NDELAYS]; + pm3_peak_t delay_peak[PM3_DSP_NDELAYS]; + bool delay_valid[PM3_DSP_NDELAYS]; + + pm3_family_t family; + pm3_confidence_t confidence; + + double symbol_clk; // best symbol rate estimate, samples/symbol + bool have_symbol_clk; + + double carrier_clk; // subcarrier / field clock, when one is present + bool have_carrier; + + double fsk_clk_hi; // the shorter of the two FSK field clocks + double fsk_clk_lo; + double fsk_ratio; + + bool transition_coded; + double dc_ratio; // low frequency energy relative to the peak + double sym_prominence; // prominence of the plain spectrum at symbol_clk + double sq_prominence; // prominence of the squaring line + + bool carrier_at_nyquist; + double envelope_depth; // 0.0 constant amplitude, 1.0 switched fully off + bool needs_envelope; +} pm3_spec_analysis_t; + +const char *pm3_family_name(pm3_family_t fam); +const char *pm3_confidence_name(pm3_confidence_t conf); + +int pm3_analyse(const double *sig, size_t len, size_t n, pm3_window_t win, pm3_spec_analysis_t *out); + +pm3_fft_plan_t *pm3_fft_plan_create(size_t n); +void pm3_fft_plan_destroy(pm3_fft_plan_t *plan); +void pm3_fft(const pm3_fft_plan_t *plan, pm3_cplx_t *data, bool inverse); + +size_t pm3_next_pow2(size_t v); +bool pm3_window_from_str(const char *str, pm3_window_t *out); +const char *pm3_window_name(pm3_window_t win); +void pm3_apply_window(double *data, size_t len, pm3_window_t win); + +double *pm3_extract(const int32_t *src, size_t len, size_t start, size_t count); +void pm3_remove_mean(double *data, size_t len); +double *pm3_envelope(const double *sig, size_t len, size_t smooth); + +typedef struct { + uint8_t *bits; + size_t nbits; + size_t nerrors; + size_t phase; // sample offset of the first chip centre + double eye; // mean distance from the threshold, 1.0 = fully open +} pm3_slice_t; + +int pm3_ask_slice(const double *sig, size_t len, double clk, pm3_slice_t *out); +void pm3_slice_free(pm3_slice_t *out); +bool pm3_is_switched_carrier(const double *sig, size_t len); +double pm3_envelope_depth(const double *sig, size_t len); +void pm3_delay_multiply(const double *in, size_t len, size_t lag, double *out); +int pm3_spectrum(const double *sig, size_t len, size_t n, pm3_window_t win, pm3_spectrum_t *out); +void pm3_spectrum_free(pm3_spectrum_t *spec); +size_t pm3_find_peaks(const pm3_spectrum_t *spec, pm3_peak_t *peaks, size_t max_peaks); +int pm3_stft(const double *sig, size_t len, size_t n, size_t hop, pm3_window_t win, double anchor, pm3_stft_t *out); +void pm3_stft_free(pm3_stft_t *st); + +typedef struct { + bool valid; + double chip; // shortest repeated run length, samples + double long_run; // 95th percentile run length, samples + double run_ratio; // long_run / chip + double short_fraction; // runs no longer than 2.5 chips, as a fraction + bool transition_coded; // runs never much exceed two chips + double period; // autocorrelation repeat period, 0 if none + double period_strength; // normalised autocorrelation at that lag +} pm3_sigstat_t; + + +int pm3_signal_stats(const double *sig, size_t len, pm3_sigstat_t *out); +double pm3_autocorr_period(const double *sig, size_t len, double *strength); +int pm3_psk_demod(const double *sig, size_t len, int fc, double clk, + uint8_t *bits, size_t *nbits, double *clk_out, int *phase_out, + double *score_out, uint8_t *abs_bits); + +int pm3_ask_chips(const double *sig, size_t len, double chip, uint8_t *chips, size_t *nchips, double *chip_out, int *phase_out); +double *pm3_resample(const double *in, size_t len, double ratio, size_t *out_len); + +#ifdef __cplusplus +} +#endif +#endif // PM3_DSP_H__ diff --git a/client/src/pm3_fit.c b/client/src/pm3_fit.c new file mode 100644 index 000000000..ea060f040 --- /dev/null +++ b/client/src/pm3_fit.c @@ -0,0 +1,1066 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Symbol template generation and matched filter hypothesis scoring +//----------------------------------------------------------------------------- + +#include "pm3_fit.h" + +#include +#include +#include + +#include "pm3_cmd.h" +#include "commonutil.h" // ARRAYLEN +#include "pm3_dsp.h" + +#define PM3_FIT_VAR_FLOOR 0.01 +#define PM3_FIT_MIN_CONTRAST 0.15 +#define PM3_FIT_OCTAVE_TOL 6.0 +#define PM3_FIT_MAX_MID 0.85 +#define PM3_FIT_MAX_MID_FSK 0.78 +#define PM3_FIT_MIN_TONE_SPLIT 0.30 +#define PM3_FIT_MIN_SURVIVAL 0.01 +#define PM3_FIT_MIN_TONE_BALANCE 0.15 +#define PM3_FIT_MIN_TONE_ENERGY 0.30 +#define PM3_FIT_FSK_MIN_CYCLES 4 +#define PM3_FIT_AGC_SYMBOLS 8 +#define PM3_FIT_CHIP_PAIR 0.90 +#define PM3_FIT_STRUCT_MIN_CLK 16.0 +#define PM3_FIT_STRUCT_TOL 0.25 + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +// Clock candidates. +static const int g_fit_clocks[] = { 8, 16, 32, 40, 50, 64, 100, 128, 256, 272, 384 }; + +// FSK field clock pairs, as fchigh / fclow. +static const int g_fsk_pairs[][2] = { {10, 8}, {8, 5} }; + +// PSK subcarrier periods. +static const int g_psk_fc[] = { 2, 4, 8 }; + +static const int g_fit_q[] = { 2, 8, 20, 50 }; + +const char *pm3_mod_name(pm3_mod_t mod) { + switch (mod) { + case PM3_MOD_ASK: + return "ASK"; + case PM3_MOD_FSK: + return "FSK"; + case PM3_MOD_PSK: + return "PSK"; + case PM3_MOD_NRZ: + return "NRZ"; + } + return "?"; +} + +const char *pm3_enc_name(pm3_enc_t enc) { + switch (enc) { + case PM3_ENC_RAW: + return "raw"; + case PM3_ENC_MANCHESTER: + return "manchester"; + case PM3_ENC_BIPHASE: + return "biphase"; + } + return "?"; +} + +size_t pm3_fit_clocks(const int **clocks) { + if (clocks != NULL) { + *clocks = g_fit_clocks; + } + return ARRAYLEN(g_fit_clocks); +} + +// Apply a single pole antenna response to a template, in place. +static void shape_for_q(double *tpl, size_t len, int q) { + + const double tau = (double)q / M_PI; + if (tau < 1.0) { + return; + } + + const double a = 1.0 / tau; + double y = 0.0; + + for (size_t i = 0; i < len; i++) { + y += a * (tpl[i] - y); + tpl[i] = y; + } +} + +static bool normalise_energy(double *tpl, size_t len) { + + double e = 0.0; + for (size_t i = 0; i < len; i++) { + e += tpl[i] * tpl[i]; + } + + if (e < (1e-6 * (double)len)) { + return false; + } + + const double scale = 1.0 / sqrt(e); + for (size_t i = 0; i < len; i++) { + tpl[i] *= scale; + } + return true; +} + +static bool build_template(const pm3_hyp_t *hyp, double *tpl, size_t len, int variant) { + + memset(tpl, 0, len * sizeof(double)); + + switch (hyp->tpl) { + + case PM3_TPL_FLAT: + for (size_t i = 0; i < len; i++) { + tpl[i] = 1.0; + } + break; + + case PM3_TPL_HALF: + for (size_t i = 0; i < len; i++) { + tpl[i] = (i < (len / 2)) ? 1.0 : -1.0; + } + break; + + case PM3_TPL_PSK: + for (size_t i = 0; i < len; i++) { + tpl[i] = ((i % (size_t)hyp->fc) < ((size_t)hyp->fc / 2)) ? 1.0 : -1.0; + } + break; + + case PM3_TPL_FSK: { + const size_t fc = (size_t)((variant == 0) ? hyp->fc_hi : hyp->fc_lo); + for (size_t i = 0; i < len; i++) { + tpl[i] = ((i % fc) < (fc / 2)) ? 1.0 : -1.0; + } + break; + } + } + + double raw_energy = 0.0; + for (size_t i = 0; i < len; i++) { + raw_energy += tpl[i] * tpl[i]; + } + + shape_for_q(tpl, len, hyp->q); + + double shaped_energy = 0.0; + for (size_t i = 0; i < len; i++) { + shaped_energy += tpl[i] * tpl[i]; + } + + if (raw_energy <= 0.0 || (shaped_energy / raw_energy) < PM3_FIT_MIN_SURVIVAL) { + return false; + } + + return normalise_energy(tpl, len); +} + +// Correlate the whole trace against a single symbol. +static int correlate(const pm3_fft_plan_t *plan, const pm3_cplx_t *sig_fft, + const double *tpl, size_t tpl_len, pm3_cplx_t *scratch, double *out) { + + const size_t n = plan->n; + + memset(scratch, 0, n * sizeof(pm3_cplx_t)); + for (size_t i = 0; i < tpl_len; i++) { + scratch[i].re = tpl[i]; + } + + pm3_fft(plan, scratch, false); + + for (size_t i = 0; i < n; i++) { + const double xr = sig_fft[i].re, xi = sig_fft[i].im; + const double tr = scratch[i].re, ti = -scratch[i].im; // conjugate + scratch[i].re = (xr * tr) - (xi * ti); + scratch[i].im = (xr * ti) + (xi * tr); + } + + pm3_fft(plan, scratch, true); + + for (size_t i = 0; i < n; i++) { + out[i] = scratch[i].re; + } + return PM3_SUCCESS; +} + +static double eye_mean(const double *work, size_t limit, double clk, double phase, size_t *count) { + + const double half = clk / 2.0; + double sum = 0.0; + size_t cnt = 0; + + for (double pos = phase; (pos + half) < (double)limit; pos += clk) { + sum += work[(size_t)(pos + 0.5)] - work[(size_t)(pos + half + 0.5)]; + cnt++; + } + + if (count != NULL) { + *count = cnt; + } + if (cnt == 0) { + return -1e30; + } + return sum / (double)cnt; +} + +static bool score_hypothesis(pm3_hyp_t *hyp, const double *stat_in, const double *aux, const double *sgn, + size_t valid, double *work, double *agc) { + + const double *stat = stat_in; + + const size_t clk = (size_t)hyp->clk; + if (clk < 2 || valid < clk * PM3_FIT_MIN_SYMBOLS) { + return false; + } + + // Flatten slow amplitude variation, for timing recovery only. + { + size_t win = clk * PM3_FIT_AGC_SYMBOLS; + if (win < 8) { + win = 8; + } + if (win > valid) { + win = valid; + } + + double run = 0.0; + size_t lo = 0, hi = 0; + while (hi < win) { + run += stat[hi++]; + } + + for (size_t i = 0; i < valid; i++) { + + const size_t want_lo = (i > win / 2) ? i - (win / 2) : 0; + size_t want_hi = want_lo + win; + if (want_hi > valid) { + want_hi = valid; + } + + while (hi < want_hi) { + run += stat[hi++]; + } + while (lo < want_lo) { + run -= stat[lo++]; + } + + const size_t cnt = hi - lo; + const double mean = (cnt > 0) ? (run / (double)cnt) : 0.0; + agc[i] = (mean > 1e-12) ? (stat[i] / mean) : 0.0; + } + } + + // standardise the flattened copy, this is what the timing search sees + { + double sum = 0.0, sumsq = 0.0; + for (size_t i = 0; i < valid; i++) { + sum += agc[i]; + sumsq += agc[i] * agc[i]; + } + const double mean = sum / (double)valid; + double var = (sumsq / (double)valid) - (mean * mean); + if (var < 1e-30) { + return false; + } + const double sd = sqrt(var); + for (size_t i = 0; i < valid; i++) { + agc[i] = (agc[i] - mean) / sd; + } + } + + // standardise the untouched statistic + double sum = 0.0, sumsq = 0.0; + for (size_t i = 0; i < valid; i++) { + sum += stat[i]; + sumsq += stat[i] * stat[i]; + } + + const double gmean = sum / (double)valid; + double gvar = (sumsq / (double)valid) - (gmean * gmean); + if (gvar < 1e-30) { + return false; + } + const double gsd = sqrt(gvar); + + for (size_t i = 0; i < valid; i++) { + work[i] = (stat[i] - gmean) / gsd; + } + + // Stage 1 - find the phase over a short prefix. + size_t prefix = clk * 32; + if (prefix > valid) { + prefix = valid; + } + + double best_phase = 0.0; + double best_mean = -1e30; + + for (size_t phase = 0; phase < clk; phase++) { + const double m = eye_mean(agc, prefix, (double)clk, (double)phase, NULL); + if (m > best_mean) { + best_mean = m; + best_phase = (double)phase; + } + } + + // Stage 2 - refine the clock as a fraction, over the full length. + double best_clk = (double)clk; + double full_best = eye_mean(agc, valid, best_clk, best_phase, NULL); + + for (int step = -30; step <= 30; step++) { + + const double c = (double)clk + ((double)step * 0.02); + if (c < 2.0) { + continue; + } + const double m = eye_mean(agc, valid, c, best_phase, NULL); + if (m > full_best) { + full_best = m; + best_clk = c; + } + } + + // Stage 3 - re-fit the phase now that the clock is right + best_mean = -1e30; + for (size_t phase = 0; phase < clk; phase++) { + const double m = eye_mean(agc, valid, best_clk, (double)phase, NULL); + if (m > best_mean) { + best_mean = m; + best_phase = (double)phase; + } + } + + if (best_mean < PM3_FIT_MIN_CONTRAST) { + return false; + } + + // Timing is settled; score the decision instants themselves. + const double half = best_clk / 2.0; + + // See the chip rate test after this loop. + size_t pair_diff[2] = { 0, 0 }; + size_t pair_tot[2] = { 0, 0 }; + bool prev_bit = false; + + double zsum = 0.0, zsumsq = 0.0; + double rsum = 0.0, rlow = 0.0, mid_sum = 0.0; + size_t cnt = 0; + + for (double pos = best_phase; (pos + half) < (double)valid; pos += best_clk) { + + const size_t idx = (size_t)(pos + 0.5); + const size_t mid = (size_t)(pos + half + 0.5); + + const double z = work[idx]; + zsum += z; + zsumsq += z * z; + + rsum += stat[idx]; + mid_sum += stat[mid]; + if (cnt == 0 || stat[idx] < rlow) { + rlow = stat[idx]; + } + + // running tally for the chip rate test below + const bool bit = (sgn[idx] >= 0.0); + if (cnt > 0) { + const size_t align = (cnt - 1) & 1; + pair_tot[align]++; + if (bit != prev_bit) { + pair_diff[align]++; + } + } + prev_bit = bit; + + cnt++; + } + + if (cnt < PM3_FIT_MIN_SYMBOLS) { + return false; + } + + const double zmean = zsum / (double)cnt; + double zvar = (zsumsq / (double)cnt) - (zmean * zmean); + + // Floor the variance. + if (zvar < PM3_FIT_VAR_FLOOR) { + zvar = PM3_FIT_VAR_FLOOR; + } + + const double rmean = rsum / (double)cnt; + + // Chip rate test. + hyp->chip_rate = false; + for (size_t align = 0; align < 2; align++) { + if (pair_tot[align] < 16) { + continue; + } + const double frac = (double)pair_diff[align] / (double)pair_tot[align]; + if (frac > PM3_FIT_CHIP_PAIR) { + hyp->chip_rate = true; + } + } + + hyp->phase = (int)best_phase; + hyp->clk_fine = best_clk; + hyp->nsym = cnt; + hyp->snr_dd = 10.0 * log10((zmean * zmean) / zvar); + hyp->eye = (rmean > 0.0) ? (rlow / rmean) : 0.0; + hyp->mid = mid_sum / (double)cnt; + hyp->mid_ratio = (rmean > 0.0) ? (hyp->mid / rmean) : 0.0; + hyp->margin = 0.0; + + const double mid_limit = (hyp->tpl == PM3_TPL_FSK) ? PM3_FIT_MAX_MID_FSK : PM3_FIT_MAX_MID; + if (hyp->mid_ratio > mid_limit) { + return false; + } + + // PSK only: the subcarrier has to actually be in the trace. + if (hyp->tpl == PM3_TPL_PSK && rmean < (PM3_FIT_MIN_TONE_ENERGY * sqrt(best_clk))) { + return false; + } + + // FSK only: the two tone correlations have to actually separate. + if (aux != NULL) { + + double asum = 0.0; + size_t acnt = 0; + + for (double pos = best_phase; (pos + half) < (double)valid; pos += best_clk) { + asum += aux[(size_t)(pos + 0.5)]; + acnt++; + } + + if (acnt == 0) { + return false; + } + const double amean = asum / (double)acnt; + if (amean <= 0.0 || (rmean / amean) < PM3_FIT_MIN_TONE_SPLIT) { + return false; + } + + if (amean < (PM3_FIT_MIN_TONE_ENERGY * sqrt(best_clk))) { + return false; + } + + // The winning tone has to actually change. + size_t hi_cnt = 0, lo_cnt = 0; + + for (double pos = best_phase; (pos + half) < (double)valid; pos += best_clk) { + if (sgn[(size_t)(pos + 0.5)] >= 0.0) { + hi_cnt++; + } else { + lo_cnt++; + } + } + + const size_t total = hi_cnt + lo_cnt; + const size_t rarer = (hi_cnt < lo_cnt) ? hi_cnt : lo_cnt; + if (total == 0 || ((double)rarer / (double)total) < PM3_FIT_MIN_TONE_BALANCE) { + return false; + } + } + + return true; +} + +static bool same_decode(const pm3_hyp_t *a, const pm3_hyp_t *b) { + + if (a->clk != b->clk || a->tpl != b->tpl) { + return false; + } + if (a->tpl == PM3_TPL_PSK && a->fc != b->fc) { + return false; + } + if (a->tpl == PM3_TPL_FSK && (a->fc_hi != b->fc_hi || a->fc_lo != b->fc_lo)) { + return false; + } + return true; +} + +static int cmp_hyp(const void *pa, const void *pb) { + + const pm3_hyp_t *a = (const pm3_hyp_t *)pa; + const pm3_hyp_t *b = (const pm3_hyp_t *)pb; + + if (a->snr_dd > b->snr_dd) { + return -1; + } + if (a->snr_dd < b->snr_dd) { + return 1; + } + + if (a->mod != b->mod) { + return (a->mod < b->mod) ? -1 : 1; + } + if (a->enc != b->enc) { + return (a->enc < b->enc) ? -1 : 1; + } + if (a->clk != b->clk) { + return (a->clk < b->clk) ? -1 : 1; + } + if (a->q != b->q) { + return (a->q < b->q) ? -1 : 1; + } + return 0; +} + +static size_t enumerate(const pm3_fit_opts_t *opts, pm3_hyp_t *list) { + + size_t n = 0; + + for (size_t ci = 0; ci < ARRAYLEN(g_fit_clocks); ci++) { + + const int clk = g_fit_clocks[ci]; + if (opts->clk_only && clk != opts->clk_only) { + continue; + } + + for (size_t qi = 0; qi < ARRAYLEN(g_fit_q); qi++) { + + const int q = g_fit_q[qi]; + + pm3_hyp_t h; + + if (opts->mod_mask == 0 || (opts->mod_mask & (1 << PM3_MOD_ASK))) { + for (int enc = PM3_ENC_RAW; enc <= PM3_ENC_BIPHASE; enc++) { + memset(&h, 0, sizeof(h)); + h.mod = PM3_MOD_ASK; + h.enc = (pm3_enc_t)enc; + h.tpl = (enc == PM3_ENC_RAW) ? PM3_TPL_FLAT : PM3_TPL_HALF; + h.clk = clk; + h.q = q; + if (list) { + list[n] = h; + } + n++; + } + } + + if (opts->mod_mask == 0 || (opts->mod_mask & (1 << PM3_MOD_NRZ))) { + memset(&h, 0, sizeof(h)); + h.mod = PM3_MOD_NRZ; + h.enc = PM3_ENC_RAW; + h.tpl = PM3_TPL_FLAT; + h.clk = clk; + h.q = q; + if (list) { + list[n] = h; + } + n++; + } + + if (opts->mod_mask == 0 || (opts->mod_mask & (1 << PM3_MOD_PSK))) { + for (size_t fi = 0; fi < ARRAYLEN(g_psk_fc); fi++) { + if (clk < g_psk_fc[fi] * 4) { + continue; + } + memset(&h, 0, sizeof(h)); + h.mod = PM3_MOD_PSK; + h.enc = PM3_ENC_RAW; + h.tpl = PM3_TPL_PSK; + h.clk = clk; + h.q = q; + h.fc = g_psk_fc[fi]; + if (list) { + list[n] = h; + } + n++; + } + } + + if (opts->mod_mask == 0 || (opts->mod_mask & (1 << PM3_MOD_FSK))) { + for (size_t fi = 0; fi < ARRAYLEN(g_fsk_pairs); fi++) { + + if (clk < g_fsk_pairs[fi][0] * PM3_FIT_FSK_MIN_CYCLES) { + continue; + } + + memset(&h, 0, sizeof(h)); + h.mod = PM3_MOD_FSK; + h.enc = PM3_ENC_RAW; + h.tpl = PM3_TPL_FSK; + h.clk = clk; + h.q = q; + h.fc_hi = g_fsk_pairs[fi][0]; + h.fc_lo = g_fsk_pairs[fi][1]; + if (list) { + list[n] = h; + } + n++; + } + } + } + } + + return n; +} + +int pm3_fit_run(const double *sig, size_t len, const pm3_fit_opts_t *opts, pm3_fit_t *out) { + + if (sig == NULL || opts == NULL || out == NULL || len < 64) { + return PM3_EINVARG; + } + + memset(out, 0, sizeof(pm3_fit_t)); + + const size_t nhyp = enumerate(opts, NULL); + if (nhyp == 0) { + return PM3_EINVARG; + } + + const size_t longest = (size_t)g_fit_clocks[ARRAYLEN(g_fit_clocks) - 1]; + const size_t n = pm3_next_pow2(len + longest); + if (n > PM3_DSP_MAX_FFT) { + return PM3_EINVARG; + } + + pm3_fft_plan_t *plan = pm3_fft_plan_create(n); + pm3_cplx_t *sig_fft = calloc(n, sizeof(pm3_cplx_t)); + pm3_cplx_t *scratch = calloc(n, sizeof(pm3_cplx_t)); + double *corr = calloc(n, sizeof(double)); + double *corr_b = calloc(n, sizeof(double)); + double *stat = calloc(n, sizeof(double)); + double *zwork = calloc(n, sizeof(double)); + double *aux = calloc(n, sizeof(double)); + double *sgn = calloc(n, sizeof(double)); + double *agcbuf = calloc(n, sizeof(double)); + double *tpl = calloc(longest + 1, sizeof(double)); + pm3_hyp_t *list = calloc(nhyp, sizeof(pm3_hyp_t)); + + if (plan == NULL || sig_fft == NULL || scratch == NULL || corr == NULL + || corr_b == NULL || stat == NULL || zwork == NULL || aux == NULL || sgn == NULL + || agcbuf == NULL || tpl == NULL || list == NULL) { + pm3_fft_plan_destroy(plan); + free(sig_fft); + free(scratch); + free(corr); + free(corr_b); + free(stat); + free(zwork); + free(aux); + free(sgn); + free(agcbuf); + free(tpl); + free(list); + return PM3_EMALLOC; + } + + pm3_signal_stats(sig, len, &out->stats); + + enumerate(opts, list); + + // FFT of the trace + for (size_t i = 0; i < len; i++) { + sig_fft[i].re = sig[i]; + } + pm3_fft(plan, sig_fft, false); + + size_t kept = 0; + + for (size_t i = 0; i < nhyp; i++) { + + pm3_hyp_t *h = &list[i]; + const size_t tlen = (size_t)h->clk; + const size_t valid = (len > tlen) ? (len - tlen) : 0; + + if (valid < tlen * PM3_FIT_MIN_SYMBOLS) { + continue; + } + + if (h->tpl == PM3_TPL_FSK) { + + if (build_template(h, tpl, tlen, 0) == false) { + continue; + } + correlate(plan, sig_fft, tpl, tlen, scratch, corr); + + if (build_template(h, tpl, tlen, 1) == false) { + continue; + } + correlate(plan, sig_fft, tpl, tlen, scratch, corr_b); + + for (size_t k = 0; k < valid; k++) { + const double d = fabs(corr[k]) - fabs(corr_b[k]); + stat[k] = fabs(d); + aux[k] = fabs(corr[k]) + fabs(corr_b[k]); + sgn[k] = d; + } + + } else { + + if (build_template(h, tpl, tlen, 0) == false) { + continue; + } + correlate(plan, sig_fft, tpl, tlen, scratch, corr); + + for (size_t k = 0; k < valid; k++) { + stat[k] = fabs(corr[k]); + sgn[k] = corr[k]; + } + } + + const bool is_fsk = (h->tpl == PM3_TPL_FSK); + if (score_hypothesis(h, stat, is_fsk ? aux : NULL, sgn, valid, zwork, agcbuf) == false) { + continue; + } + + list[kept++] = *h; + } + + if (kept == 0) { + pm3_fft_plan_destroy(plan); + free(sig_fft); + free(scratch); + free(corr); + free(corr_b); + free(stat); + free(zwork); + free(aux); + free(sgn); + free(agcbuf); + free(tpl); + free(list); + return PM3_ESOFT; + } + + qsort(list, kept, sizeof(pm3_hyp_t), cmp_hyp); + + // Octave correction. + for (bool moved = true; moved;) { + + moved = false; + + for (size_t j = 1; j < kept; j++) { + + bool candidate = (list[j].tpl == list[0].tpl + && list[j].mod == list[0].mod + && list[j].enc == list[0].enc); + + if (candidate == false + && list[0].tpl == PM3_TPL_FLAT + && list[j].tpl == PM3_TPL_HALF + && (list[0].mod == PM3_MOD_ASK || list[0].mod == PM3_MOD_NRZ) + && list[j].mod == PM3_MOD_ASK) { + candidate = true; + } + + if (candidate == false) { + continue; + } + + if (list[0].tpl != PM3_TPL_FLAT && list[0].tpl != PM3_TPL_HALF) { + break; + } + + if (list[0].chip_rate == false) { + break; + } + + const double ratio = (double)list[j].clk / (double)list[0].clk; + if (fabs(ratio - 2.0) > 0.01) { + continue; + } + + const pm3_hyp_t winner = list[j]; + memmove(&list[1], &list[0], j * sizeof(pm3_hyp_t)); + list[0] = winner; + out->promoted = true; + break; + } + } + + // Structural override. + if (out->stats.valid) { + + const double want_clk = out->stats.transition_coded + ? (2.0 * out->stats.chip) + : out->stats.chip; + + if (want_clk >= PM3_FIT_STRUCT_MIN_CLK) { + + for (size_t j = 0; j < kept; j++) { + + if (list[j].tpl != PM3_TPL_FLAT && list[j].tpl != PM3_TPL_HALF) { + continue; + } + + const bool tpl_ok = out->stats.transition_coded + ? (list[j].tpl == PM3_TPL_HALF) + : (list[j].tpl == PM3_TPL_FLAT); + if (tpl_ok == false) { + continue; + } + + if (fabs((double)list[j].clk - want_clk) > (PM3_FIT_STRUCT_TOL * want_clk)) { + continue; + } + + if (j > 0) { + const pm3_hyp_t winner = list[j]; + memmove(&list[1], &list[0], j * sizeof(pm3_hyp_t)); + list[0] = winner; + out->promoted = true; + } + break; + } + } + } + + for (size_t i = 0; i < kept; i++) { + list[i].margin = 0.0; + for (size_t j = i + 1; j < kept; j++) { + if (same_decode(&list[i], &list[j]) == false) { + list[i].margin = list[i].snr_dd - list[j].snr_dd; + break; + } + } + } + + if (opts->keep_corr) { + + const pm3_hyp_t *best = &list[0]; + const size_t tlen = (size_t)best->clk; + const size_t valid = len - tlen; + + if (best->tpl == PM3_TPL_FSK) { + build_template(best, tpl, tlen, 0); + correlate(plan, sig_fft, tpl, tlen, scratch, corr); + build_template(best, tpl, tlen, 1); + correlate(plan, sig_fft, tpl, tlen, scratch, corr_b); + for (size_t k = 0; k < valid; k++) { + stat[k] = fabs(fabs(corr[k]) - fabs(corr_b[k])); + } + } else { + build_template(best, tpl, tlen, 0); + correlate(plan, sig_fft, tpl, tlen, scratch, corr); + for (size_t k = 0; k < valid; k++) { + stat[k] = fabs(corr[k]); + sgn[k] = corr[k]; + } + } + + out->corr = calloc(valid, sizeof(double)); + if (out->corr != NULL) { + memcpy(out->corr, stat, valid * sizeof(double)); + out->corr_len = valid; + } + } + + out->items = list; + out->count = kept; + + pm3_fft_plan_destroy(plan); + free(sig_fft); + free(scratch); + free(corr); + free(corr_b); + free(stat); + free(zwork); + free(aux); + free(sgn); + free(agcbuf); + free(tpl); + return PM3_SUCCESS; +} + +void pm3_fit_free(pm3_fit_t *fit) { + if (fit == NULL) { + return; + } + free(fit->items); + free(fit->corr); + memset(fit, 0, sizeof(pm3_fit_t)); +} + +//----------------------------------------------------------------------------- +// Synthetic signal generator +//----------------------------------------------------------------------------- +static uint32_t rng_next(uint32_t *state) { + uint32_t x = *state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *state = x; + return x; +} + +static double rng_uniform(uint32_t *state) { + const uint32_t v = rng_next(state); + return (double)v / 4294967296.0; +} + +static double rng_gauss(uint32_t *state) { + double u1 = rng_uniform(state); + const double u2 = rng_uniform(state); + if (u1 < 1e-12) { + u1 = 1e-12; + } + return sqrt(-2.0 * log(u1)) * cos(2.0 * M_PI * u2); +} + +int pm3_fit_generate(const pm3_gen_opts_t *opts, double *out, size_t len) { + + if (opts == NULL || out == NULL || len < 64 || opts->clk < 2.0) { + return PM3_EINVARG; + } + + uint32_t state = opts->seed ? opts->seed : 1; + + const size_t nbits = (size_t)((double)len / opts->clk) + 2; + uint8_t *bits = calloc(nbits, sizeof(uint8_t)); + if (bits == NULL) { + return PM3_EMALLOC; + } + for (size_t i = 0; i < nbits; i++) { + bits[i] = rng_next(&state) & 1; + } + + if (opts->repeat > 0) { + for (size_t i = (size_t)opts->repeat; i < nbits; i++) { + bits[i] = bits[i % (size_t)opts->repeat]; + } + } + + double *edge = calloc(nbits + 1, sizeof(double)); + if (edge == NULL) { + free(bits); + return PM3_EMALLOC; + } + for (size_t i = 0; i <= nbits; i++) { + edge[i] = ((double)i * opts->clk) + (opts->jitter ? (rng_gauss(&state) * opts->jitter) : 0.0); + } + + int8_t *first = calloc(nbits, sizeof(int8_t)); + int8_t *second = calloc(nbits, sizeof(int8_t)); + if (first == NULL || second == NULL) { + free(bits); + free(edge); + free(first); + free(second); + return PM3_EMALLOC; + } + + { + int8_t level = 1; + for (size_t i = 0; i < nbits; i++) { + level = (int8_t)(-level); // boundary transition, always + first[i] = level; + if (bits[i]) { + level = (int8_t)(-level); // mid symbol transition, on a one + } + second[i] = level; + } + } + + for (size_t i = 0; i < len; i++) { + + size_t sym = (size_t)((double)i / opts->clk); + if (sym >= nbits) { + sym = nbits - 1; + } + while (sym + 1 < nbits && (double)i >= edge[sym + 1]) { + sym++; + } + while (sym > 0 && (double)i < edge[sym]) { + sym--; + } + + const double span = edge[sym + 1] - edge[sym]; + const double frac = (span > 0.0) ? (((double)i - edge[sym]) / span) : 0.0; + const int bit = bits[sym]; + + double sym_level; + + switch (opts->enc) { + case PM3_ENC_MANCHESTER: { + sym_level = ((frac < 0.5) == (bit != 0)) ? 1.0 : -1.0; + break; + } + case PM3_ENC_BIPHASE: { + sym_level = (frac < 0.5) ? (double)first[sym] : (double)second[sym]; + break; + } + case PM3_ENC_RAW: + default: { + sym_level = bit ? 1.0 : -1.0; + break; + } + } + + double v; + + switch (opts->mod) { + case PM3_MOD_FSK: { + const int fc = (sym_level > 0.0) ? opts->fc_hi : opts->fc_lo; + v = sin(2.0 * M_PI * (double)i / (double)(fc ? fc : 8)); + break; + } + case PM3_MOD_PSK: { + const int fc = opts->fc ? opts->fc : 4; + v = sym_level * sin(2.0 * M_PI * (double)i / (double)fc); + break; + } + case PM3_MOD_ASK: + case PM3_MOD_NRZ: + default: { + v = sym_level; + break; + } + } + + if (opts->envelope > 0.0) { + v *= 1.0 + (opts->envelope * ((double)i / (double)len - 0.5)); + } + + if (opts->noise > 0.0) { + v += rng_gauss(&state) * opts->noise; + } + + if (opts->drift != 0.0) { + v += opts->drift * ((double)i / (double)len); + } + + if (opts->clip > 0.0) { + if (v > opts->clip) { + v = opts->clip; + } + + if (v < -opts->clip) { + v = -opts->clip; + } + } + + out[i] = v; + } + + free(bits); + free(edge); + free(first); + free(second); + + shape_for_q(out, len, opts->q ? opts->q : 8); + pm3_remove_mean(out, len); + return PM3_SUCCESS; +} diff --git a/client/src/pm3_fit.h b/client/src/pm3_fit.h new file mode 100644 index 000000000..228642355 --- /dev/null +++ b/client/src/pm3_fit.h @@ -0,0 +1,119 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Symbol template generation and matched filter hypothesis scoring +//----------------------------------------------------------------------------- + +#ifndef PM3_FIT_H__ +#define PM3_FIT_H__ + +#include "common.h" +#include "pm3_dsp.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define PM3_FIT_MIN_SYMBOLS 64 + +typedef enum { + PM3_MOD_ASK = 0, + PM3_MOD_FSK, + PM3_MOD_PSK, + PM3_MOD_NRZ, +} pm3_mod_t; + +typedef enum { + PM3_ENC_RAW = 0, + PM3_ENC_MANCHESTER, + PM3_ENC_BIPHASE, +} pm3_enc_t; + +typedef enum { + PM3_TPL_FLAT = 0, // one level held for the whole symbol + PM3_TPL_HALF, // half high, half low - a mid symbol transition + PM3_TPL_PSK, // subcarrier burst + PM3_TPL_FSK, // one of two tones, scored non coherently +} pm3_tpl_t; + +typedef struct { + + pm3_mod_t mod; + pm3_enc_t enc; + pm3_tpl_t tpl; + + int clk; // samples per symbol + int q; // assumed antenna Q used to shape the template + int fc_hi; // FSK, the longer field clock ( pm3 `fchigh` ) + int fc_lo; // FSK, the shorter field clock ( pm3 `fclow` ) + int fc; // PSK subcarrier period + + // results + int phase; // best symbol phase, 0 .. clk-1 + double clk_fine; // refined clock, fractional - this is the useful one + size_t nsym; // decision instants the score was built from + double snr_dd; // decision directed SNR, dB + double eye; // min / mean at the decision instants, 0 .. 1 + double mid; // mean at the mid symbol instants + double mid_ratio; // mid / mean at the decision instants + bool chip_rate; // decoded bits pair up 01/10 almost always, so + double margin; // dB to the next best genuinely different decode +} pm3_hyp_t; + +typedef struct { + int mod_mask; // bitmask of (1 << pm3_mod_t), 0 means all + int clk_only; // restrict to this clock, 0 means all candidates + bool keep_corr; // hand back the rank 1 correlator output +} pm3_fit_opts_t; + +typedef struct { + pm3_hyp_t *items; // ranked, best first + size_t count; + pm3_sigstat_t stats; // structural facts measured from the waveform + bool promoted; // rank 1 was chosen on structural evidence rather + double *corr; // rank 1 decision statistic, when requested + size_t corr_len; +} pm3_fit_t; + +const char *pm3_mod_name(pm3_mod_t mod); +const char *pm3_enc_name(pm3_enc_t enc); + +size_t pm3_fit_clocks(const int **clocks); +int pm3_fit_run(const double *sig, size_t len, const pm3_fit_opts_t *opts, pm3_fit_t *out); +void pm3_fit_free(pm3_fit_t *fit); + +typedef struct { + pm3_mod_t mod; + pm3_enc_t enc; + double clk; // may be fractional + int fc_hi; + int fc_lo; + int fc; + double noise; + double drift; + double envelope; // amplitude swing across the capture, 0 = none + double jitter; + double clip; // 0 = no clipping + int q; // antenna Q to shape with, 0 for the default + int repeat; // message length in bits, 0 for endless random data. + uint32_t seed; +} pm3_gen_opts_t; + +int pm3_fit_generate(const pm3_gen_opts_t *opts, double *out, size_t len); + +#ifdef __cplusplus +} +#endif +#endif // PM3_FIT_H__ From 1dfc0cfef573ba36fae82f58ba5ff097594ba467 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Thu, 30 Jul 2026 16:41:07 +0200 Subject: [PATCH 2/6] add commands2 --- client/src/cmdlftrovan.c | 436 +++++++++++++++++++++++++++++++++++++++ client/src/cmdlftrovan.h | 29 +++ 2 files changed, 465 insertions(+) create mode 100644 client/src/cmdlftrovan.c create mode 100644 client/src/cmdlftrovan.h diff --git a/client/src/cmdlftrovan.c b/client/src/cmdlftrovan.c new file mode 100644 index 000000000..410bc97e5 --- /dev/null +++ b/client/src/cmdlftrovan.c @@ -0,0 +1,436 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Low frequency Trovan animal ID commands +// +// Trovan is not ISO 11784/11785 +// +// Differential biphase phase shift keying on an Fc/2 subcarrier, 16 carrier cycles to the bit. +// A telegram is 64 bits: +// +// 8 bits sync, 01111111. Cannot occur anywhere in the body without +// breaking row parity, which is what makes it a usable sync word. +// 13 rows 3 data bits and 1 odd row parity bit, 39 data bits in total +// 1 row 3 odd column parity bits and 1 odd row parity bit + +//----------------------------------------------------------------------------- + +#include "cmdlftrovan.h" + +#include +#include +#include + +#include "commonutil.h" // ARRAYLEN +#include "cmdparser.h" // command_t +#include "cliparser.h" +#include "comms.h" +#include "ui.h" +#include "cmddata.h" +#include "cmdlf.h" +#include "protocols.h" // T55xx config register definitions +#include "lfdemod.h" // psk1TOpsk2 +#include "cmdlft55xx.h" // clone_t55xx_tag +#include "cmdlfem4x05.h" // em4x05_clone_tag + +#define TROVAN_CLOCK 16 +#define TROVAN_TELEGRAM 64 +#define TROVAN_SYNC_LEN 8 +#define TROVAN_ROWS 14 +#define TROVAN_DATA_BITS 39 + +// samples for one telegram, times enough telegrams to survive a ragged start +#define TROVAN_READ_SAMPLES (TROVAN_TELEGRAM * TROVAN_CLOCK * 10) + +static int CmdHelp(const char *Cmd); + +static const uint8_t trovan_sync[TROVAN_SYNC_LEN] = { 0, 1, 1, 1, 1, 1, 1, 1 }; + +// Check one 64 bit telegram and pull the ID out of it. `bits` holds one bit +// per byte. Returns true and fills `id` when both parity planes hold. +static bool trovan_check(const uint8_t *bits, uint64_t *id) { + + const uint8_t *rows = bits + TROVAN_SYNC_LEN; + + // odd parity across each 4 bit row + for (uint8_t r = 0; r < TROVAN_ROWS; r++) { + + uint8_t ones = 0; + for (uint8_t c = 0; c < 4; c++) { + ones += rows[(r * 4) + c]; + } + + if ((ones & 1) == 0) { + return false; + } + } + + // odd parity down each of the three data columns, the fourth column is + // the row parity itself and is not covered + for (uint8_t c = 0; c < 3; c++) { + + uint8_t ones = 0; + for (uint8_t r = 0; r < TROVAN_ROWS; r++) { + ones += rows[(r * 4) + c]; + } + + if ((ones & 1) == 0) { + return false; + } + } + + // 39 data bits, most significant first, the last row being parity only + uint64_t v = 0; + for (uint8_t r = 0; r < TROVAN_ROWS - 1; r++) { + for (uint8_t c = 0; c < 3; c++) { + v = (v << 1) | rows[(r * 4) + c]; + } + } + + *id = v; + return true; +} + +// Hunt for a sync word and validate the telegram that follows it. +// Returns the bit offset the telegram started at, or -1. +static int trovan_find(const uint8_t *bits, size_t size, uint64_t *id) { + + if (size < TROVAN_TELEGRAM) { + return -1; + } + + for (size_t i = 0; i + TROVAN_TELEGRAM <= size; i++) { + + if (memcmp(bits + i, trovan_sync, TROVAN_SYNC_LEN) != 0) { + continue; + } + + if (trovan_check(bits + i, id)) { + return (int)i; + } + } + + return -1; +} + +// Conventional Trovan formatting is 10 hex digits in 2-4-4 groups. +static void trovan_print(uint64_t id) { + + PrintAndLogEx(SUCCESS, "Trovan animal ID " _GREEN_("%02X-%04X-%04X") + , (uint32_t)((id >> 32) & 0xFF) + , (uint32_t)((id >> 16) & 0xFFFF) + , (uint32_t)(id & 0xFFFF) + ); +} + +int demodTrovan(bool verbose) { + + if (PSKDemod(TROVAN_CLOCK, 0, 100, false) != PM3_SUCCESS) { + PrintAndLogEx(DEBUG, "DEBUG: Error - Trovan: PSK demod failed"); + return PM3_ESOFT; + } + + psk1TOpsk2(g_DemodBuffer, g_DemodBufferLen); + + if (g_DemodBufferLen < TROVAN_TELEGRAM) { + PrintAndLogEx(DEBUG, "DEBUG: Error - Trovan: too few bits, %zu", g_DemodBufferLen); + return PM3_ESOFT; + } + + // The demodulator settles on a polarity of its own choosing, and the parity checks cannot tell the two apart. + uint8_t *bits = calloc(g_DemodBufferLen, sizeof(uint8_t)); + if (bits == NULL) { + PrintAndLogEx(DEBUG, "DEBUG: Error - Trovan: out of memory"); + return PM3_EMALLOC; + } + + uint64_t id = 0; + int idx = -1; + bool inverted = false; + + for (uint8_t pass = 0; pass < 2 && idx < 0; pass++) { + + for (size_t i = 0; i < g_DemodBufferLen; i++) { + bits[i] = (pass == 0) ? (g_DemodBuffer[i] & 1) : (g_DemodBuffer[i] & 1) ^ 1; + } + + idx = trovan_find(bits, g_DemodBufferLen, &id); + inverted = (pass == 1); + } + + if (idx < 0) { + free(bits); + PrintAndLogEx(DEBUG, "DEBUG: Error - Trovan: no valid telegram found"); + return PM3_ESOFT; + } + + setDemodBuff(bits, TROVAN_TELEGRAM, (size_t)idx); + setClockGrid(g_DemodClock, g_DemodStartIdx + (idx * g_DemodClock)); + free(bits); + + trovan_print(id); + + if (verbose) { + PrintAndLogEx(INFO, " telegram at bit " _YELLOW_("%d") ", clock rf/%d, polarity %s" + , idx + , TROVAN_CLOCK + , inverted ? "inverted" : "normal" + ); + PrintAndLogEx(INFO, " raw " _YELLOW_("%010" PRIX64), id); + } + + return PM3_SUCCESS; +} + +int readTrovanUid(void) { + return (demodTrovan(false) == PM3_SUCCESS); +} + +// Build the 64 bit telegram for an ID. +static void trovan_encode(uint64_t id, uint8_t *bits) { + + memcpy(bits, trovan_sync, TROVAN_SYNC_LEN); + uint8_t *rows = bits + TROVAN_SYNC_LEN; + + // 13 rows of 3 data bits, most significant first, each closed with an odd row parity bit + for (uint8_t r = 0; r < TROVAN_ROWS - 1; r++) { + + uint8_t ones = 0; + + for (uint8_t c = 0; c < 3; c++) { + const uint8_t shift = (uint8_t)(TROVAN_DATA_BITS - 1 - ((r * 3) + c)); + const uint8_t b = (uint8_t)((id >> shift) & 1); + rows[(r * 4) + c] = b; + ones += b; + } + + rows[(r * 4) + 3] = (ones & 1) ? 0 : 1; + } + + // the last row carries the column parities, then its own row parity + uint8_t last = 0; + + for (uint8_t c = 0; c < 3; c++) { + + uint8_t ones = 0; + for (uint8_t r = 0; r < TROVAN_ROWS - 1; r++) { + ones += rows[(r * 4) + c]; + } + + const uint8_t p = (ones & 1) ? 0 : 1; + rows[((TROVAN_ROWS - 1) * 4) + c] = p; + last += p; + } + + rows[((TROVAN_ROWS - 1) * 4) + 3] = (last & 1) ? 0 : 1; +} + +static int CmdTrovanDemod(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "lf trovan demod", + "Demodulate a Trovan animal ID tag from the GraphBuffer", + "lf trovan demod\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_lit0("v", "verbose", "verbose output"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + bool verbose = arg_get_lit(ctx, 1); + CLIParserFree(ctx); + + const int res = demodTrovan(verbose); + + if (res != PM3_SUCCESS && verbose) { + PrintAndLogEx(FAILED, "Trovan demod failed ( looked for a %d bit frame at rf/%d )" + , TROVAN_TELEGRAM + , TROVAN_CLOCK + ); + } + + return res; +} + +static int CmdTrovanReader(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "lf trovan reader", + "read a Trovan animal ID tag", + "lf trovan reader -@ -> continuous reader mode" + ); + + void *argtable[] = { + arg_param_begin, + arg_lit0("@", NULL, "optional - continuous reader mode"), + arg_lit0("v", "verbose", "verbose output"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + bool cm = arg_get_lit(ctx, 1); + bool verbose = arg_get_lit(ctx, 2); + CLIParserFree(ctx); + + if (cm) { + PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); + } + + int res = PM3_SUCCESS; + + do { + lf_read(false, TROVAN_READ_SAMPLES); + res = demodTrovan(verbose && (cm == false)); + } while (cm && (kbd_enter_pressed() == false)); + + // continuous mode is expected to come up empty between tag presentations, + // so only a one shot read is worth reporting a miss for + if (res != PM3_SUCCESS && (cm == false) && verbose) { + PrintAndLogEx(FAILED, "Trovan demod failed ( looked for a %d bit frame at rf/%d )" + , TROVAN_TELEGRAM + , TROVAN_CLOCK + ); + } + + return res; +} + +static int CmdTrovanClone(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "lf trovan clone", + "clone a Trovan animal ID to a T55x7, Q5/T5555 or EM4305/4469 tag\n" + "The ID is the 10 hex digits printed on the tag, with or without dashes.", + "lf trovan clone --id 0007F9043A\n" + "lf trovan clone --id 00-07F9-043A\n" + "lf trovan clone --id 0007F9043A --q5" + ); + + void *argtable[] = { + arg_param_begin, + arg_str1(NULL, "id", "", "Trovan animal ID, 10 hex digits"), + arg_lit0(NULL, "q5", "specify writing to Q5/T5555 tag"), + arg_lit0(NULL, "em", "specify writing to EM4305/4469 tag"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + uint8_t raw[24] = {0}; + int rawlen = sizeof(raw) - 1; + CLIGetStrWithReturn(ctx, 1, raw, &rawlen); + bool q5 = arg_get_lit(ctx, 2); + bool em = arg_get_lit(ctx, 3); + CLIParserFree(ctx); + + if (q5 && em) { + PrintAndLogEx(FAILED, "Can't specify both Q5 and EM4305 at the same time"); + return PM3_EINVARG; + } + + // accept the dashed form the tags are printed in + char digits[16] = {0}; + int n = 0; + + for (int i = 0; i < rawlen; i++) { + + if (raw[i] == '-' || raw[i] == ' ') { + continue; + } + + if (n >= (int)sizeof(digits) - 1) { + n++; + break; + } + + digits[n++] = (char)raw[i]; + } + + if (n != 10) { + PrintAndLogEx(FAILED, "ID must be 10 hex digits, got " _RED_("%d"), n); + return PM3_EINVARG; + } + + char *end = NULL; + const uint64_t id = strtoull(digits, &end, 16); + + if (end == NULL || *end != '\0') { + PrintAndLogEx(FAILED, "ID is not hex: " _RED_("%s"), digits); + return PM3_EINVARG; + } + + // 10 hex digits is 40 bits but only 39 of them are carried + if (id > 0x7FFFFFFFFFULL) { + PrintAndLogEx(FAILED, "ID does not fit in the 39 bits a telegram carries"); + return PM3_EINVARG; + } + + uint8_t bits[TROVAN_TELEGRAM] = {0}; + trovan_encode(id, bits); + + uint32_t blocks[3] = {0}; + for (uint8_t i = 0; i < 64; i++) { + const uint8_t b = bits[i] ^ 1; + blocks[1 + (i / 32)] |= ((uint32_t)b) << (31 - (i % 32)); + } + + char cardtype[16] = {"T55x7"}; + + blocks[0] = T55x7_TESTMODE_DISABLED | T55x7_MODULATION_PSK2 | T55x7_PSKCF_RF_2 | T55x7_BITRATE_RF_16 | 2 << T55x7_MAXBLOCK_SHIFT; + + if (q5) { + blocks[0] = T5555_FIXED | T5555_MODULATION_PSK2 | T5555_SET_BITRATE(TROVAN_CLOCK) | T5555_PSK_RF_2 | 2 << T5555_MAXBLOCK_SHIFT; + snprintf(cardtype, sizeof(cardtype), "Q5/T5555"); + } + + if (em) { + PrintAndLogEx(FAILED, "EM4305/4469 does not support PSK, which Trovan needs"); + return PM3_EINVARG; + } + + PrintAndLogEx(INFO, "Preparing to clone Trovan to " _YELLOW_("%s") " with animal ID " _GREEN_("%02X-%04X-%04X") + , cardtype + , (uint32_t)((id >> 32) & 0xFF) + , (uint32_t)((id >> 16) & 0xFFFF) + , (uint32_t)(id & 0xFFFF) + ); + + print_blocks(blocks, ARRAYLEN(blocks)); + + int res = clone_t55xx_tag(blocks, ARRAYLEN(blocks)); + + PrintAndLogEx(SUCCESS, "Done!"); + PrintAndLogEx(HINT, "Hint: Try " _YELLOW_("`lf trovan reader`") " to verify"); + return res; +} + +static command_t CommandTable[] = { + {"help", CmdHelp, AlwaysAvailable, "This help"}, + {"demod", CmdTrovanDemod, AlwaysAvailable, "demodulate a Trovan tag from the GraphBuffer"}, + {"reader", CmdTrovanReader, IfPm3Lf, "attempt to read and extract tag data"}, + {"clone", CmdTrovanClone, IfPm3Lf, "clone Trovan tag to T55x7 or Q5/T5555"}, + {NULL, NULL, NULL, NULL} +}; + +static int CmdHelp(const char *Cmd) { + (void)Cmd; // Cmd is not used so far + CmdsHelp(CommandTable); + return PM3_SUCCESS; +} + +int CmdLFTrovan(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(CommandTable, Cmd); +} diff --git a/client/src/cmdlftrovan.h b/client/src/cmdlftrovan.h new file mode 100644 index 000000000..c4e2dc615 --- /dev/null +++ b/client/src/cmdlftrovan.h @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Low frequency Trovan animal ID commands +//----------------------------------------------------------------------------- + +#ifndef CMDLFTROVAN_H__ +#define CMDLFTROVAN_H__ + +#include "common.h" +#include + +int CmdLFTrovan(const char *Cmd); +int demodTrovan(bool verbose); +int readTrovanUid(void); + +#endif From 7e565198e11abdf39f8601cdf7eea54773db333b Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Thu, 30 Jul 2026 16:42:09 +0200 Subject: [PATCH 3/6] text --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd09d1f8..ebc560680 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] - +- Added `lf trovan` commands to support Trovan Animal ID (@iceman1001) - Added `hf mf gdmgetblk/gdmgethidblk/gdmsethidblk/gdmsetuid/gdmwipe/gdmsetsig` (@0x6r1an0y) - Changed `hf mf gdmparsecfg/gdmsetblk` (@0x6r1an0y) - Renamed `hf mf gdmcfg` (@0x6r1an0y) From 19cc98c53258b8528f5eba040ecbe9d6dd9b1cd9 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Thu, 30 Jul 2026 16:43:09 +0200 Subject: [PATCH 4/6] hooked up the new DSP code. Should improve the t55xx commands --- client/CMakeLists.txt | 4 + client/Makefile | 3 + client/src/cmddata.c | 1845 +++++++++++++++++++++++++++++-- client/src/cmdflashmemspiffs.c | 15 +- client/src/cmdhf.c | 2 +- client/src/cmdlf.c | 42 +- client/src/cmdlfcotag.c | 408 +++---- client/src/cmdlfem4x70.c | 3 +- client/src/cmdlfem4x70.h | 2 +- client/src/cmdlft55xx.c | 1123 ++++++++++++++----- client/src/pm3line_vocabulary.h | 10 +- doc/commands.json | 192 +++- doc/commands.md | 18 +- 13 files changed, 2990 insertions(+), 677 deletions(-) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 6ae302c17..43fc3288e 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -461,6 +461,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdlfsecurakey.c ${PM3_ROOT}/client/src/cmdlft55xx.c ${PM3_ROOT}/client/src/cmdlfti.c + ${PM3_ROOT}/client/src/cmdlftrovan.c ${PM3_ROOT}/client/src/cmdlfviking.c ${PM3_ROOT}/client/src/cmdlfvisa2000.c ${PM3_ROOT}/client/src/cmdlfzx8211.c @@ -491,6 +492,9 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/pm3.c ${PM3_ROOT}/client/src/pm3_binlib.c ${PM3_ROOT}/client/src/pm3_bitlib.c + ${PM3_ROOT}/client/src/pm3_dsp.c + + ${PM3_ROOT}/client/src/pm3_fit.c ${PM3_ROOT}/client/src/pm3line.c ${PM3_ROOT}/client/src/scandir.c ${PM3_ROOT}/client/src/scripting.c diff --git a/client/Makefile b/client/Makefile index 3ad261b0b..13bb06ff7 100644 --- a/client/Makefile +++ b/client/Makefile @@ -802,6 +802,7 @@ SRCS = mifare/aiddesfire.c \ cmdlfsecurakey.c \ cmdlft55xx.c \ cmdlfti.c \ + cmdlftrovan.c \ cmdlfviking.c \ cmdlfvisa2000.c \ cmdlfzx8211.c \ @@ -888,6 +889,8 @@ SRCS = mifare/aiddesfire.c \ pm3.c \ pm3_binlib.c \ pm3_bitlib.c \ + pm3_dsp.c \ + pm3_fit.c \ preferences.c \ pm3line.c \ proxmark3.c \ diff --git a/client/src/cmddata.c b/client/src/cmddata.c index 7700c4a40..0e5de7e5a 100644 --- a/client/src/cmddata.c +++ b/client/src/cmddata.c @@ -43,7 +43,11 @@ #include "atrs.h" // ATR lookup #include "crypto/libpcrypto.h" // Cryptography #include "qrcode/qrcode.h" // QR Code lib +#include "pm3_dsp.h" // FFT, windows, spectra +#include "pm3_fit.h" // matched filter hypothesis bank +#define FITSCORE_DEFAULT_WINDOW 16384 +#define FITSCORE_MIN_SYMBOLS 128 uint8_t g_DemodBuffer[MAX_DEMOD_BUF_LEN] = { 0x00 }; size_t g_DemodBufferLen = 0; @@ -52,47 +56,6 @@ int g_DemodClock = 0; static int CmdHelp(const char *Cmd); - -// https://www.eskimo.com/~scs/c-faq.com/stdio/commaprint.html -static char *commaprint(size_t n) { - - static int comma = '\0'; - static char retbuf[30]; - - char *p = &retbuf[sizeof(retbuf) - 1]; - int i = 0; - - if (comma == '\0') { - - struct lconv *lcp = localeconv(); - if (lcp != NULL) { - - if (lcp->thousands_sep != NULL && *lcp->thousands_sep != '\0') { - comma = *lcp->thousands_sep; - } else { - comma = ','; - } - } - } - - *p = '\0'; - - do { - if (i % 3 == 0 && i != 0) { - *--p = comma; - } - - *--p = '0' + n % 10; - - n /= 10; - - i++; - - } while (n != 0); - - return p; -} - // set the g_DemodBuffer with given array ofq binary (one bit per byte) void setDemodBuff(const uint8_t *buff, size_t size, size_t start_idx) { if (buff == NULL) { @@ -121,39 +84,6 @@ bool getDemodBuff(uint8_t *buff, size_t *size) { return true; } -// include -// Root mean square -/* -static double rms(double *v, size_t n) { - double sum = 0.0; - for (size_t i = 0; i < n; i++) - sum += v[i] * v[i]; - return sqrt(sum / n); -} - -static int cmp_int(const void *a, const void *b) { - if (*(const int *)a < * (const int *)b) - return -1; - else - return *(const int *)a > *(const int *)b; -} -static int cmp_uint8(const void *a, const void *b) { - if (*(const uint8_t *)a < * (const uint8_t *)b) - return -1; - else - return *(const uint8_t *)a > *(const uint8_t *)b; -} -// Median of a array of values - -static double median_int(int *src, size_t size) { - qsort(src, size, sizeof(int), cmp_int); - return 0.5 * (src[size / 2] + src[(size - 1) / 2]); -} -static double median_uint8(uint8_t *src, size_t size) { - qsort(src, size, sizeof(uint8_t), cmp_uint8); - return 0.5 * (src[size / 2] + src[(size - 1) / 2]); -} -*/ // function to compute mean for a series static double compute_mean(const int *data, size_t n) { double mean = 0.0; @@ -163,7 +93,7 @@ static double compute_mean(const int *data, size_t n) { return mean; } -// function to compute variance for a series +// function to compute variance for a series static double compute_variance(const int *data, size_t n) { double variance = 0.0; double mean = compute_mean(data, n); @@ -175,30 +105,6 @@ static double compute_variance(const int *data, size_t n) { return variance; } -// Function to compute autocorrelation for a series -// Author: Kenneth J. Christensen -// - Corrected divide by n to divide (n - lag) from Tobias Mueller -/* -static double compute_autoc(const int *data, size_t n, int lag) { - double autocv = 0.0; // Autocovariance value - double ac_value; // Computed autocorrelation value to be returned - double variance; // Computed variance - double mean; - - mean = compute_mean(data, n); - variance = compute_variance(data, n); - - for (size_t i=0; i < (n - lag); i++) - autocv += (data[i] - mean) * (data[i+lag] - mean); - - autocv = (1.0 / (n - lag)) * autocv; - - // Autocorrelation is autocovariance divided by variance - ac_value = autocv / variance; - return ac_value; -} -*/ - static int CmdSetDebugMode(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "data setdebugmode", @@ -1176,9 +1082,68 @@ static int CmdAskEdgeDetect(const char *Cmd) { return res; } -// Print our clock rate -// uses data from graphbuffer -// adjusted to take char parameter for type of modulation to find the clock - by marshmellow. +// Clock detection for `data detectclock`. +static int detectclock_fit(int mod_mask, pm3_hyp_t *hyp) { + + if (g_GraphTraceLen < 2048) { + return PM3_ESOFT; + } + + const size_t count = (g_GraphTraceLen < FITSCORE_DEFAULT_WINDOW) + ? g_GraphTraceLen + : FITSCORE_DEFAULT_WINDOW; + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, count); + if (sig == NULL) { + return PM3_EMALLOC; + } + + pm3_fit_opts_t opts = {0}; + opts.mod_mask = mod_mask; + + pm3_fit_t fit; + int res = pm3_fit_run(sig, count, &opts, &fit); + free(sig); + + if (res != PM3_SUCCESS) { + return res; + } + + *hyp = fit.items[0]; + pm3_fit_free(&fit); + return PM3_SUCCESS; +} + +static int detectclock_print(int mod_mask, bool verbose) { + + pm3_hyp_t hyp; + if (detectclock_fit(mod_mask, &hyp) != PM3_SUCCESS) { + return 0; + } + + const int clk = (int)(hyp.clk_fine + 0.5); + if (clk <= 0) { + return 0; + } + + if (verbose) { + if (hyp.mod == PM3_MOD_FSK) { + PrintAndLogEx(SUCCESS, "Detected Field Clocks: FC/%d, FC/%d - Bit Clock: RF/%d" + , hyp.fc_hi + , hyp.fc_lo + , clk + ); + } else if (hyp.mod == PM3_MOD_ASK) { + PrintAndLogEx(SUCCESS, "Auto-detected clock rate: %d, Best Starting Position: %d", clk, hyp.phase); + } else { + PrintAndLogEx(SUCCESS, "Auto-detected clock rate: %d", clk); + } + } + + setClockGrid(clk, hyp.phase); + return clk; +} + static int CmdDetectClockRate(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "data detectclock", @@ -1207,6 +1172,18 @@ static int CmdDetectClockRate(const char *Cmd) { return PM3_EINVARG; } else if (tmp == 0) { + // no modulation asked for, so report whichever one the signal is + pm3_hyp_t hyp; + if (detectclock_fit(0, &hyp) == PM3_SUCCESS) { + const int clk = (int)(hyp.clk_fine + 0.5); + if (clk > 0) { + PrintAndLogEx(SUCCESS, "%s Clock... %d", pm3_mod_name(hyp.mod), clk); + setClockGrid(clk, hyp.phase); + return PM3_SUCCESS; + } + } + + // nothing fit, fall back to what the old heuristics can offer int clock = GetFskClock("", false); if (clock > 0) { PrintAndLogEx(SUCCESS, "FSK Clock... %d", clock); @@ -1226,17 +1203,21 @@ static int CmdDetectClockRate(const char *Cmd) { return PM3_SUCCESS; } - if (a) + if (a && detectclock_print(1 << PM3_MOD_ASK, true) == 0) { GetAskClock("", true); + } - if (f) + if (f && detectclock_print(1 << PM3_MOD_FSK, true) == 0) { GetFskClock("", true); + } - if (n) + if (n && detectclock_print(1 << PM3_MOD_NRZ, true) == 0) { GetNrzClock("", true); + } - if (p) + if (p && detectclock_print(1 << PM3_MOD_PSK, true) == 0) { GetPskClock("", true); + } RepaintGraphWindow(); return PM3_SUCCESS; @@ -2031,7 +2012,7 @@ static int CmdLoad(const char *Cmd) { } fclose(f); - PrintAndLogEx(SUCCESS, "loaded " _YELLOW_("%s") " samples", commaprint(g_GraphTraceLen)); + PrintAndLogEx(SUCCESS, "loaded " _YELLOW_("%zu") " samples", g_GraphTraceLen); if (nofix == false) { uint8_t *bits = calloc(g_GraphTraceLen, sizeof(uint8_t)); @@ -4107,9 +4088,1642 @@ static int CmdQRcode(const char *Cmd) { return PM3_SUCCESS; } +//----------------------------------------------------------------------------- +// Frequency domain analysis +// +// These commands all read the graph buffer, never write to it unless asked, +// The transforms themselves live in pm3_dsp.c +//----------------------------------------------------------------------------- + +static size_t largest_pow2_le(size_t v) { + size_t p = 1; + while ((p << 1) <= v && (p << 1) <= PM3_DSP_MAX_FFT) { + p <<= 1; + } + return p; +} + +// Shared front end for the host side DSP commands. Validates the requested +// window against the graph buffer, rejects degenerate input, and hands back a +// mean removed, unit variance copy of the samples. Caller frees *sig. +// +// `size` == 0 means "the largest power of two that fits from start". When a +// size is given it is rounded up to a power of two for the transform length, +// which is returned in *n, while *count stays at the number of real samples. +static int dsp_prepare(int start, int size, double **sig, size_t *count, size_t *n) { + + *sig = NULL; + *count = 0; + *n = 0; + + if (g_GraphTraceLen == 0) { + PrintAndLogEx(WARNING, "GraphBuffer is empty"); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data load -f ") "` or `" _YELLOW_("lf read") "` to get samples"); + return PM3_EINVARG; + } + + if (start < 0) { + PrintAndLogEx(WARNING, "start must not be negative"); + return PM3_EINVARG; + } + + if ((size_t)start >= g_GraphTraceLen) { + PrintAndLogEx(WARNING, "start ( " _YELLOW_("%d") " ) is past the end of the trace ( " _YELLOW_("%zu") " samples )" + , start + , g_GraphTraceLen + ); + return PM3_EINVARG; + } + + if (size < 0) { + PrintAndLogEx(WARNING, "size must not be negative"); + return PM3_EINVARG; + } + + const size_t avail = g_GraphTraceLen - (size_t)start; + + if (avail < 4) { + PrintAndLogEx(WARNING, "not enough samples from start ( " _YELLOW_("%zu") " ), need at least 4", avail); + return PM3_EINVARG; + } + + if (size == 0) { + *n = largest_pow2_le(avail); + *count = *n; + } else { + if ((size_t)size > avail) { + PrintAndLogEx(WARNING, "size ( " _YELLOW_("%d") " ) is larger than the %zu samples available from start", size, avail); + return PM3_EINVARG; + } + if ((size_t)size > PM3_DSP_MAX_FFT) { + PrintAndLogEx(WARNING, "size ( " _YELLOW_("%d") " ) exceeds the maximum transform length of %d", size, PM3_DSP_MAX_FFT); + return PM3_EINVARG; + } + *count = (size_t)size; + *n = pm3_next_pow2(*count); + } + + if (*n < 4) { + PrintAndLogEx(WARNING, "transform length ( " _YELLOW_("%zu") " ) is too short to be useful", *n); + return PM3_EINVARG; + } + + // a flat window carries no information and every downstream statistic + // would divide by zero. Catch it here rather than printing NaN tables. + bool flat = true; + for (size_t i = 1; i < *count; i++) { + if (g_GraphBuffer[start + i] != g_GraphBuffer[start]) { + flat = false; + break; + } + } + if (flat) { + PrintAndLogEx(WARNING, "all %zu samples in the window are identical, nothing to analyse", *count); + return PM3_EINVARG; + } + + *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, (size_t)start, *count); + if (*sig == NULL) { + PrintAndLogEx(WARNING, "failed to allocate memory"); + return PM3_EMALLOC; + } + + return PM3_SUCCESS; +} + +// Frequencies are reported in cycles/sample as the primary unit. Hz is only +// printed when the user tells us the sample rate with --fs: the client keeps no +// record of the divisor a trace was captured with, and assuming 125 kHz would +// silently mislabel every HF and every non default LF capture. +static int CmdFFT(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "data fft", + "Fourier transform of the samples in the GraphBuffer.\n" + "Reports the magnitude spectrum of the first N/2+1 bins, the input being real.\n" + "The DC component is removed and the window is normalised to unit variance\n" + "before the transform, so magnitudes are comparable between captures.", + "data fft --> transform the whole buffer\n" + "data fft --size 4096 --win blackman --> 4096 point transform, blackman window\n" + "data fft --start 1000 --size 8192 --db --> magnitudes in dB relative to the peak\n" + "data fft --size 4096 --graph --> put the spectrum in the graph window\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "start", "", "first sample of the GraphBuffer to use (def 0)"), + arg_int0(NULL, "size", "", "transform length, rounded up to a power of two (def largest that fits)"), + arg_str0(NULL, "win", "", "window function (def hann)"), + arg_lit0(NULL, "db", "report magnitude in dB relative to the peak bin instead of linear"), + arg_lit0(NULL, "graph", "write the magnitude spectrum into the GraphBuffer and repaint"), + arg_int0(NULL, "fs", "", "sample rate in Hz, adds a frequency column in Hz"), + arg_int0(NULL, "bins", "", "how many bins to print (def 32, 0 for all)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int start = arg_get_int_def(ctx, 1, 0); + int size = arg_get_int_def(ctx, 2, 0); + + char win_str[16] = {0}; + int win_len = sizeof(win_str) - 1; + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)win_str, win_len, &win_len); + + bool use_db = arg_get_lit(ctx, 4); + bool to_graph = arg_get_lit(ctx, 5); + int fs = arg_get_int_def(ctx, 6, 0); + int want_bins = arg_get_int_def(ctx, 7, 32); + CLIParserFree(ctx); + + pm3_window_t win = PM3_WIN_HANN; + if (win_len > 0 && pm3_window_from_str(win_str, &win) == false) { + PrintAndLogEx(WARNING, "unknown window `%s`, expected hann, hamming, blackman or rect", win_str); + return PM3_EINVARG; + } + + if (want_bins < 0) { + PrintAndLogEx(WARNING, "bins must not be negative"); + return PM3_EINVARG; + } + + double *sig = NULL; + size_t count = 0, n = 0; + int res = dsp_prepare(start, size, &sig, &count, &n); + if (res != PM3_SUCCESS) { + return res; + } + + pm3_spectrum_t spec; + res = pm3_spectrum(sig, count, n, win, &spec); + free(sig); + + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "failed to compute the spectrum"); + return res; + } + + const double bin_width = 1.0 / (double)n; + const double peak_freq = (double)spec.peak_bin * bin_width; + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Fourier transform") " ---------------------------------"); + PrintAndLogEx(INFO, " samples....... " _YELLOW_("%zu") " from offset " _YELLOW_("%d"), count, start); + PrintAndLogEx(INFO, " transform..... " _YELLOW_("%zu") " point, %zu bins reported", n, spec.nbins); + PrintAndLogEx(INFO, " window........ " _YELLOW_("%s"), pm3_window_name(win)); + PrintAndLogEx(INFO, " bin width..... " _YELLOW_("%.6f") " cycles/sample", bin_width); + + if (fs > 0) { + PrintAndLogEx(INFO, " sample rate... " _YELLOW_("%d") " Hz ( %.2f Hz per bin )", fs, bin_width * (double)fs); + } else { + PrintAndLogEx(INFO, " sample rate... " _YELLOW_("unknown") ", use `--fs` to add a Hz column"); + } + + PrintAndLogEx(INFO, " peak bin...... " _GREEN_("%zu") " ( %.6f cycles/sample, clock %.2f samples/symbol )" + , spec.peak_bin + , peak_freq + , (peak_freq > 0.0) ? 1.0 / peak_freq : 0.0 + ); + if (fs > 0) { + PrintAndLogEx(INFO, " peak.......... " _GREEN_("%.1f") " Hz", peak_freq * (double)fs); + } + + size_t print_bins = (want_bins == 0) ? spec.nbins : (size_t)want_bins; + if (print_bins > spec.nbins) { + print_bins = spec.nbins; + } + + if (print_bins) { + + PrintAndLogEx(NORMAL, ""); + if (fs > 0) { + PrintAndLogEx(INFO, " bin cycles/sample Hz %s", use_db ? " mag (dB)" : " magnitude"); + PrintAndLogEx(INFO, "------+--------------+-----------+-----------"); + } else { + PrintAndLogEx(INFO, " bin cycles/sample %s", use_db ? " mag (dB)" : " magnitude"); + PrintAndLogEx(INFO, "------+--------------+-----------"); + } + + for (size_t i = 0; i < print_bins; i++) { + + const double f = (double)i * bin_width; + char val[32]; + + if (use_db) { + snprintf(val, sizeof(val), "%9.2f", 20.0 * log10((spec.mag[i] / (spec.peak + 1e-30)) + 1e-30)); + } else { + snprintf(val, sizeof(val), "%9.3f", spec.mag[i]); + } + + if (fs > 0) { + PrintAndLogEx(INFO, "%5zu | %12.6f | %9.1f | %s", i, f, f * (double)fs, val); + } else { + PrintAndLogEx(INFO, "%5zu | %12.6f | %s", i, f, val); + } + } + + if (print_bins < spec.nbins) { + PrintAndLogEx(INFO, "( %zu of %zu bins shown, use `--bins 0` for all )", print_bins, spec.nbins); + } + } + + if (to_graph) { + + // dB against the peak keeps the low level structure visible, a linear + // plot of an LF spectrum is one spike and a flat line + for (size_t i = 0; i < spec.nbins; i++) { + double db = 20.0 * log10((spec.mag[i] / (spec.peak + 1e-30)) + 1e-30); + if (db < -96.0) { + db = -96.0; + } + g_GraphBuffer[i] = (int32_t)((db + 96.0) * (255.0 / 96.0)) - 128; + } + g_GraphTraceLen = spec.nbins; + + // the x axis is now bins, not samples. Drive the existing cursor + // scale so a marker delta reads out directly in cycles/sample. + setClockGrid(0, 0); + g_CursorScaleFactor = (double)n; + snprintf(g_CursorScaleFactorUnit, sizeof(g_CursorScaleFactorUnit), "c/sample"); + RepaintGraphWindow(); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(WARNING, "GraphBuffer now holds a " _YELLOW_("magnitude spectrum")); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data load -f ") "` to get the original samples back"); + } + + pm3_spectrum_free(&spec); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; +} + +static void print_peak_table(const char *title, const pm3_peak_t *peaks, size_t n, int fs) { + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "%s", title); + + if (n == 0) { + PrintAndLogEx(INFO, " no peaks found above the noise floor"); + return; + } + + if (fs > 0) { + PrintAndLogEx(INFO, " bin cycles/sample Hz clock mag(dB) prom(dB)"); + PrintAndLogEx(INFO, "------+--------------+-----------+---------+----------+----------"); + } else { + PrintAndLogEx(INFO, " bin cycles/sample clock mag(dB) prom(dB)"); + PrintAndLogEx(INFO, "------+--------------+---------+----------+----------"); + } + + for (size_t i = 0; i < n; i++) { + if (fs > 0) { + PrintAndLogEx(INFO, "%5zu | %12.6f | %9.1f | %7.2f | %8.2f | %8.2f" + , peaks[i].bin + , peaks[i].freq + , peaks[i].freq * (double)fs + , peaks[i].clk + , peaks[i].mag_db + , peaks[i].prominence + ); + } else { + PrintAndLogEx(INFO, "%5zu | %12.6f | %7.2f | %8.2f | %8.2f" + , peaks[i].bin + , peaks[i].freq + , peaks[i].clk + , peaks[i].mag_db + , peaks[i].prominence + ); + } + } +} + +// Pick the analysis window when the user did not. A capture long enough to +// have a choice gets one cheap look at its spectrum first, because the right +// window depends on the symbol rate and nothing knows that yet. The spectral +// estimate can land on a harmonic of the true rate, so size for twice what it +// reports rather than for exactly it. +static int autodemod_window(int start, size_t avail) { + + size_t want = MIN(avail, (size_t)FITSCORE_DEFAULT_WINDOW); + + if (avail <= (size_t)FITSCORE_DEFAULT_WINDOW) { + return (int)want; + } + + double *probe = NULL; + size_t count = 0, n = 0; + if (dsp_prepare(start, (int)want, &probe, &count, &n) != PM3_SUCCESS) { + return (int)want; + } + + pm3_spec_analysis_t an; + const int res = pm3_analyse(probe, count, n, PM3_WIN_HANN, &an); + free(probe); + + if (res != PM3_SUCCESS || an.have_symbol_clk == false) { + return (int)want; + } + + const double need = an.symbol_clk * 2.0 * (double)FITSCORE_MIN_SYMBOLS; + if (need > (double)want) { + want = MIN(avail, (size_t)need); + want = MIN(want, (size_t)PM3_DSP_MAX_FFT); + } + + return (int)want; +} + +// Keep a copy of the graph buffer the first time something is about to +// overwrite it. `data autodemod` rectifies and resamples in place so the +// demodulators see what it decided on, and both have to be undoable. +static void autodemod_backup(int32_t **backup, size_t len) { + + if (*backup != NULL || len == 0) { + return; + } + + *backup = calloc(len, sizeof(int32_t)); + if (*backup != NULL) { + memcpy(*backup, g_GraphBuffer, len * sizeof(int32_t)); + } +} + +// Write `len` doubles into the graph buffer, scaled to the +/-100 the +// demodulators expect from a loaded trace. +static void autodemod_store(const double *src, size_t len) { + + double peak = 0.0; + for (size_t i = 0; i < len; i++) { + if (fabs(src[i]) > peak) { + peak = fabs(src[i]); + } + } + + if (peak <= 0.0) { + peak = 1.0; + } + + for (size_t i = 0; i < len; i++) { + g_GraphBuffer[i] = (int32_t)((src[i] / peak) * 100.0); + } + g_GraphTraceLen = len; +} + +static void print_family_hint(const pm3_spec_analysis_t *an) { + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Modulation family") " ---------------------------------"); + + if (an->needs_envelope) { + PrintAndLogEx(INFO, " carrier....... rf/" _YELLOW_("%.2f") ", switched on and off ( depth " _YELLOW_("%.2f") " )" + , an->carrier_clk + , an->envelope_depth + ); + PrintAndLogEx(INFO, " two samples per carrier period, so the data is in the envelope, not the phase"); + PrintAndLogEx(INFO, " everything below was measured on the rectified envelope"); + } + + if (an->family == PM3_FAM_UNKNOWN) { + PrintAndLogEx(WARNING, "no clear spectral signature, confidence " _RED_("none")); + PrintAndLogEx(INFO, "the capture may be noise, or the symbol rate may be outside the search range"); + return; + } + + if (an->confidence >= PM3_CONF_MEDIUM) { + PrintAndLogEx(SUCCESS, "looks like " _GREEN_("%s") ", confidence " _GREEN_("%s") + , pm3_family_name(an->family) + , pm3_confidence_name(an->confidence) + ); + } else { + PrintAndLogEx(WARNING, "looks like " _YELLOW_("%s") ", confidence " _YELLOW_("%s") + , pm3_family_name(an->family) + , pm3_confidence_name(an->confidence) + ); + } + + switch (an->family) { + case PM3_FAM_FSK: + PrintAndLogEx(INFO, " field clocks.. rf/" _YELLOW_("%.2f") " and rf/" _YELLOW_("%.2f") " ( ratio %.3f )" + , an->fsk_clk_hi + , an->fsk_clk_lo + , an->fsk_ratio + ); + PrintAndLogEx(INFO, " two strong lines at a stable ratio is the FSK signature"); + break; + case PM3_FAM_PSK: + PrintAndLogEx(INFO, " subcarrier.... rf/" _YELLOW_("%.2f"), an->carrier_clk); + PrintAndLogEx(INFO, " carrier is suppressed at the symbol rate, the line only appears after squaring"); + break; + case PM3_FAM_MANCHESTER: + PrintAndLogEx(INFO, " null at DC, energy concentrated at the symbol rate"); + break; + case PM3_FAM_ASK: + PrintAndLogEx(INFO, " a line at the symbol rate survives in the plain spectrum"); + break; + case PM3_FAM_NRZ: + PrintAndLogEx(INFO, " maximum at DC with a sinc squared skirt"); + break; + case PM3_FAM_UNKNOWN: + break; + } + + if (an->have_symbol_clk) { + PrintAndLogEx(INFO, " symbol clock.. " _GREEN_("%.2f") " samples/symbol", an->symbol_clk); + const double frac = fabs(an->symbol_clk - floor(an->symbol_clk + 0.5)); + if (frac > 0.05) { + PrintAndLogEx(WARNING, " clock is " _YELLOW_("%.2f") " samples off the nearest integer", frac); + PrintAndLogEx(INFO, " a fractional clock decodes cleanly at the start of a capture and drifts out later"); + } + } + + PrintAndLogEx(INFO, " this is a hint from spectral shape alone, no bits have been decoded"); +} + +// Renders the spectrogram as a character ramp. Rows are frequency bands, +// columns are time. Both are folded to fit a terminal. +static void print_heatmap(const pm3_stft_t *st) { + + static const char ramp[] = " .:-=+*#%@"; + const size_t nramp = sizeof(ramp) - 2; + + const size_t cols = (st->nframes < 96) ? st->nframes : 96; + const size_t rows = 20; + + // only the lower part of the spectrum carries the symbol rate, the top + // octave is almost always empty on an LF capture + const size_t band_hi = st->nbins; + + double peak = 0.0; + for (size_t i = 0; i < st->nframes * st->nbins; i++) { + if (st->ridge[i] > peak) { + peak = st->ridge[i]; + } + } + if (peak <= 0.0) { + return; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Spectrogram") " ---------------------------------------"); + PrintAndLogEx(INFO, "rows are frequency ( top = Nyquist ), columns are time, `%c` is the peak", ramp[nramp]); + + char line[128]; + + for (size_t r = 0; r < rows; r++) { + + // row 0 is the top of the spectrum + const size_t b_lo = ((rows - 1 - r) * band_hi) / rows; + size_t b_hi = ((rows - r) * band_hi) / rows; + if (b_hi <= b_lo) { + b_hi = b_lo + 1; + } + + for (size_t c = 0; c < cols; c++) { + + const size_t f_lo = (c * st->nframes) / cols; + size_t f_hi = ((c + 1) * st->nframes) / cols; + if (f_hi <= f_lo) { + f_hi = f_lo + 1; + } + + double best = 0.0; + for (size_t f = f_lo; f < f_hi && f < st->nframes; f++) { + for (size_t b = b_lo; b < b_hi && b < st->nbins; b++) { + const double v = st->ridge[(f * st->nbins) + b]; + if (v > best) { + best = v; + } + } + } + + size_t idx = (size_t)((best / peak) * (double)nramp); + if (idx > nramp) { + idx = nramp; + } + line[c] = ramp[idx]; + } + line[cols] = '\0'; + + const double f_top = (double)b_hi / (double)st->n; + PrintAndLogEx(INFO, "%8.4f |%s|", f_top, line); + } + + PrintAndLogEx(INFO, " +%.*s+", (int)cols, "--------------------------------------------------------------------------------------------------"); + PrintAndLogEx(INFO, " sample %-*zu%zu", (int)(cols - 8), st->frame_start[0], st->frame_start[st->nframes - 1] + st->n); +} + +static void report_stft(const pm3_stft_t *st, int start) { + + print_heatmap(st); + + // median of the dominant rate, used as the reference the ridge must stay close to + double *sorted = calloc(st->nframes, sizeof(double)); + if (sorted == NULL) { + PrintAndLogEx(WARNING, "failed to allocate memory"); + return; + } + memcpy(sorted, st->peak_freq, st->nframes * sizeof(double)); + for (size_t i = 1; i < st->nframes; i++) { + const double v = sorted[i]; + size_t j = i; + while (j > 0 && sorted[j - 1] > v) { + sorted[j] = sorted[j - 1]; + j--; + } + sorted[j] = v; + } + const double median = sorted[st->nframes / 2]; + + memcpy(sorted, st->peak_mag, st->nframes * sizeof(double)); + for (size_t i = 1; i < st->nframes; i++) { + const double v = sorted[i]; + size_t j = i; + while (j > 0 && sorted[j - 1] > v) { + sorted[j] = sorted[j - 1]; + j--; + } + sorted[j] = v; + } + const double median_mag = sorted[st->nframes / 2]; + free(sorted); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Ridge") " ---------------------------------------------"); + + if (median <= 0.0) { + PrintAndLogEx(WARNING, "no coherent rate found in any frame"); + return; + } + + // longest run of frames whose dominant rate stays within 5% of the median + size_t best_lo = 0, best_len = 0, run_lo = 0, run_len = 0; + for (size_t f = 0; f < st->nframes; f++) { + + // a frame counts as coherent when the clock is where we expect it and + // there is actually something there. Frames before the tag enters the + // field pass the first test on noise alone, hence the second. + const bool ok = ((fabs(st->peak_freq[f] - median) / median) < 0.10) + && (st->peak_mag[f] > (0.25 * median_mag)); + if (ok) { + if (run_len == 0) { + run_lo = f; + } + run_len++; + if (run_len > best_len) { + best_len = run_len; + best_lo = run_lo; + } + } else { + run_len = 0; + } + } + + if (best_len == 0) { + PrintAndLogEx(WARNING, "the dominant rate never settles, the capture may be all noise"); + return; + } + + const size_t s_lo = start + st->frame_start[best_lo]; + const size_t s_hi = start + st->frame_start[best_lo + best_len - 1] + st->n; + + PrintAndLogEx(INFO, " coherent...... samples " _GREEN_("%zu") " .. " _GREEN_("%zu") " ( %zu of %zu frames )" + , s_lo + , s_hi + , best_len + , st->nframes + ); + + if (best_len < st->nframes) { + if (s_hi < g_GraphTraceLen) { + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data rtrim -i %zu") "` to drop the incoherent tail", s_hi); + } + if (s_lo > 0) { + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data ltrim -i %zu") "` to drop the incoherent head", s_lo); + } + } else { + PrintAndLogEx(INFO, " the whole capture holds a coherent clock, no trimming needed"); + } + + // drift, measured over the coherent span only + const size_t edge = (best_len / 10) ? (best_len / 10) : 1; + + double f_start = 0.0, f_end = 0.0; + for (size_t i = 0; i < edge; i++) { + f_start += st->peak_freq[best_lo + i]; + f_end += st->peak_freq[best_lo + best_len - 1 - i]; + } + + f_start /= (double)edge; + f_end /= (double)edge; + + if (f_start > 0.0 && f_end > 0.0) { + + const double clk_start = 1.0 / f_start; + const double clk_end = 1.0 / f_end; + const double pct = ((clk_end - clk_start) / clk_start) * 100.0; + + PrintAndLogEx(INFO, " clock drift... " _YELLOW_("%.2f") " -> " _YELLOW_("%.2f") " samples/symbol ( %+.2f %% )" + , clk_start + , clk_end + , pct + ); + + if (fabs(pct) > 1.0) { + PrintAndLogEx(WARNING, " the clock moves across the capture, a single integer clock will not fit all of it"); + } + } + + // discontinuities + size_t jumps = 0; + for (size_t f = 1; f < st->nframes; f++) { + const double d = fabs(st->peak_freq[f] - st->peak_freq[f - 1]); + if ((d / median) > 0.15) { + if (jumps < 8) { + PrintAndLogEx(INFO, " rate jump..... at sample " _YELLOW_("%zu") " ( %.2f -> %.2f samples/symbol )" + , (size_t)start + st->frame_start[f] + , (st->peak_freq[f - 1] > 0.0) ? 1.0 / st->peak_freq[f - 1] : 0.0 + , (st->peak_freq[f] > 0.0) ? 1.0 / st->peak_freq[f] : 0.0 + ); + } + jumps++; + } + } + if (jumps > 8) { + PrintAndLogEx(INFO, " ( %zu more jumps not shown )", jumps - 8); + } + if (jumps == 0) { + PrintAndLogEx(INFO, " rate jumps.... " _GREEN_("none")); + } +} + +static int CmdSpectrum(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "data spectrum", + "Spectral analysis of the GraphBuffer.\n" + "Reports the strongest spectral peaks with a sub bin estimate of the symbol clock,\n" + "the squaring and delay-and-multiply spectra which expose rates the plain spectrum\n" + "nulls out, and a modulation family hint derived from spectral shape alone.\n" + "Run this on a trace that will not decode.", + "data spectrum --> peak table and family hint\n" + "data spectrum --top 8 --sq --> more peaks, plus the squaring spectrum\n" + "data spectrum --stft --size 2048 --> spectrogram over the whole capture\n" + "data spectrum --stft --size 2048 --hop 256 --> finer time resolution\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "start", "", "first sample of the GraphBuffer to use (def 0)"), + arg_int0(NULL, "size", "", "transform length, rounded up to a power of two (def largest that fits)"), + arg_str0(NULL, "win", "", "window function (def hann)"), + arg_int0(NULL, "top", "", "number of peaks to report (def 5)"), + arg_lit0(NULL, "sq", "also show the squaring and delay-and-multiply spectra"), + arg_lit0(NULL, "stft", "sliding window spectrogram, ridge tracking and drift"), + arg_int0(NULL, "hop", "", "STFT hop in samples (def size/4)"), + arg_int0(NULL, "fs", "", "sample rate in Hz, adds a frequency column in Hz"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int start = arg_get_int_def(ctx, 1, 0); + int size = arg_get_int_def(ctx, 2, 0); + + char win_str[16] = {0}; + int win_len = sizeof(win_str) - 1; + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)win_str, win_len, &win_len); + + int top = arg_get_int_def(ctx, 4, 5); + bool show_sq = arg_get_lit(ctx, 5); + bool do_stft = arg_get_lit(ctx, 6); + int hop = arg_get_int_def(ctx, 7, 0); + int fs = arg_get_int_def(ctx, 8, 0); + CLIParserFree(ctx); + + pm3_window_t win = PM3_WIN_HANN; + if (win_len > 0 && pm3_window_from_str(win_str, &win) == false) { + PrintAndLogEx(WARNING, "unknown window `%s`, expected hann, hamming, blackman or rect", win_str); + return PM3_EINVARG; + } + + if (top < 1 || top > PM3_DSP_MAX_PEAKS) { + PrintAndLogEx(WARNING, "top must be between 1 and %d", PM3_DSP_MAX_PEAKS); + return PM3_EINVARG; + } + + if (hop < 0) { + PrintAndLogEx(WARNING, "hop must not be negative"); + return PM3_EINVARG; + } + + double *sig = NULL; + size_t count = 0, n = 0; + int res = dsp_prepare(start, size, &sig, &count, &n); + if (res != PM3_SUCCESS) { + return res; + } + + pm3_spec_analysis_t an; + res = pm3_analyse(sig, count, n, win, &an); + if (res != PM3_SUCCESS) { + free(sig); + PrintAndLogEx(WARNING, "spectral analysis failed"); + return res; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Spectrum") " ------------------------------------------"); + PrintAndLogEx(INFO, " samples..... " _YELLOW_("%zu") " from offset " _YELLOW_("%d"), count, start); + PrintAndLogEx(INFO, " transform... " _YELLOW_("%zu") " point, %s window", n, pm3_window_name(win)); + PrintAndLogEx(INFO, " bin width... " _YELLOW_("%.6f") " cycles/sample", 1.0 / (double)n); + + print_peak_table("--- " _CYAN_("Peaks") " ---------------------------------------------", an.peaks, MIN(an.npeaks, (size_t)top), fs); + PrintAndLogEx(INFO, "clock is the reciprocal of the frequency"); + + if (show_sq) { + + print_peak_table("--- " _CYAN_("Squaring spectrum") " ( y = x * x ) -------------", an.sq_peaks, MIN(an.nsq_peaks, (size_t)top), fs); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Delay and multiply") " ( y[i] = x[i] * x[i-d] ) ---"); + PrintAndLogEx(INFO, " lag clock prom(dB)"); + PrintAndLogEx(INFO, "------+---------+----------"); + for (size_t i = 0; i < an.ndelay; i++) { + if (an.delay_valid[i] == false) { + continue; + } + PrintAndLogEx(INFO, "%5zu | %7.2f | %8.2f", an.delay[i], an.delay_peak[i].clk, an.delay_peak[i].prominence); + } + PrintAndLogEx(INFO, "squaring a BPSK subcarrier collapses the +/-180 modulation into a tone at twice the carrier"); + } + + print_family_hint(&an); + + if (do_stft) { + + size_t stft_hop = (hop > 0) ? (size_t)hop : (n / 4); + if (stft_hop == 0) { + stft_hop = 1; + } + + // the spectrogram wants the whole trace from start + const size_t avail = g_GraphTraceLen - (size_t)start; + double *full = pm3_extract(g_GraphBuffer, g_GraphTraceLen, (size_t)start, avail); + + if (full == NULL) { + PrintAndLogEx(WARNING, "failed to allocate memory"); + free(sig); + return PM3_EMALLOC; + } + + // anchor the ridge tracker on the symbol rate the full length analysis found, + // so drift is measured against the clock we care about rather than against whichever bin peaked in each frame + const double anchor = an.have_symbol_clk ? (1.0 / an.symbol_clk) : 0.0; + + pm3_stft_t st; + res = pm3_stft(full, avail, n, stft_hop, win, anchor, &st); + free(full); + + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "cannot build a spectrogram: need at least one full %zu sample window from offset %d", n, start); + PrintAndLogEx(HINT, "Hint: Try a smaller `" _YELLOW_("--size") "`"); + } else { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, " frames........ " _YELLOW_("%zu") " of %zu samples, hop " _YELLOW_("%zu"), st.nframes, st.n, st.hop); + report_stft(&st, start); + pm3_stft_free(&st); + } + } + + free(sig); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; +} + +static int parse_mod_mask(const char *str, int *mask) { + + *mask = 0; + if (str == NULL || str[0] == '\0') { + return PM3_SUCCESS; + } + + if (strcmp(str, "ask") == 0) { + *mask = 1 << PM3_MOD_ASK; + } else if (strcmp(str, "fsk") == 0) { + *mask = 1 << PM3_MOD_FSK; + } else if (strcmp(str, "psk") == 0) { + *mask = 1 << PM3_MOD_PSK; + } else if (strcmp(str, "nrz") == 0) { + *mask = 1 << PM3_MOD_NRZ; + } else { + return PM3_EINVARG; + } + return PM3_SUCCESS; +} + +static void print_fit_table(const pm3_fit_t *fit, size_t rows) { + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "rank mod encoding clk clk fine Q phase SNR_dd eye mid margin"); + PrintAndLogEx(INFO, "-----+-----+------------+-----+----------+----+-------+---------+-------+------+--------"); + + for (size_t i = 0; i < rows; i++) { + + const pm3_hyp_t *h = &fit->items[i]; + + char extra[24] = {0}; + if (h->tpl == PM3_TPL_FSK) { + snprintf(extra, sizeof(extra), " fc %d/%d", h->fc_hi, h->fc_lo); + } else if (h->tpl == PM3_TPL_PSK) { + snprintf(extra, sizeof(extra), " fc %d", h->fc); + } + + PrintAndLogEx(INFO, "%4zu | %-3s | %-10s | %3d | %8.2f | %2d | %5d | %7.2f | %5.3f | %4.2f | %6.2f%s" + , i + 1 + , pm3_mod_name(h->mod) + , pm3_enc_name(h->enc) + , h->clk + , h->clk_fine + , h->q + , h->phase + , h->snr_dd + , h->eye + , h->mid_ratio + , h->margin + , extra + ); + } +} + +static int CmdFitScore(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "data fitscore", + "Rank matched filter hypotheses against the GraphBuffer.\n" + "A hypothesis is a modulation, a symbol shape and a clock. Each one is scored by\n" + "correlating the trace against a single symbol template and measuring how tightly\n" + "the correlator output clusters at the decision instants. Phase is not searched:\n" + "the correlation is done with an FFT, which yields every phase offset at once.\n" + "Templates are shaped by an assumed antenna Q, because correlating a bandlimited\n" + "signal against square edges biases the ranking toward short clocks.", + "data fitscore --> rank the whole bank\n" + "data fitscore --mod ask --top 5 --> ASK hypotheses only\n" + "data fitscore --clk 64 --all --> everything at clock 64\n" + "data fitscore --verbose --> also put the rank 1 correlator output in the graph\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "start", "", "first sample of the GraphBuffer to use (def 0)"), + arg_int0(NULL, "size", "", "samples to analyse, rounded up to a power of two (def 16384)"), + arg_str0(NULL, "mod", "", "restrict to one modulation (def all)"), + arg_int0(NULL, "clk", "", "restrict to one clock (def all candidates)"), + arg_int0(NULL, "top", "", "rows to print (def 10)"), + arg_lit0(NULL, "all", "print every scored hypothesis"), + arg_lit0("v", "verbose", "write the rank 1 correlator output into the GraphBuffer"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int start = arg_get_int_def(ctx, 1, 0); + int size = arg_get_int_def(ctx, 2, 0); + + char mod_str[8] = {0}; + int mod_len = sizeof(mod_str) - 1; + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)mod_str, mod_len, &mod_len); + + int clk_only = arg_get_int_def(ctx, 4, 0); + int top = arg_get_int_def(ctx, 5, 10); + bool show_all = arg_get_lit(ctx, 6); + bool verbose = arg_get_lit(ctx, 7); + CLIParserFree(ctx); + + pm3_fit_opts_t opts = {0}; + + if (parse_mod_mask(mod_str, &opts.mod_mask) != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "unknown modulation `%s`, expected ask, fsk, psk or nrz", mod_str); + return PM3_EINVARG; + } + + if (clk_only < 0) { + PrintAndLogEx(WARNING, "clk must not be negative"); + return PM3_EINVARG; + } + if (clk_only > 0) { + const int *clocks = NULL; + const size_t nclk = pm3_fit_clocks(&clocks); + bool known = false; + for (size_t i = 0; i < nclk; i++) { + if (clocks[i] == clk_only) { + known = true; + break; + } + } + if (known == false) { + PrintAndLogEx(WARNING, "clock " _YELLOW_("%d") " is not one of the candidates the detectors search", clk_only); + PrintAndLogEx(INFO, "candidates are:"); + char buf[128] = {0}; + for (size_t i = 0; i < nclk; i++) { + snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "%s%d", i ? ", " : " ", clocks[i]); + } + PrintAndLogEx(INFO, "%s", buf); + return PM3_EINVARG; + } + } + opts.clk_only = clk_only; + opts.keep_corr = verbose; + + if (top < 1) { + PrintAndLogEx(WARNING, "top must be at least 1"); + return PM3_EINVARG; + } + + if (size == 0) { + const size_t avail = (g_GraphTraceLen > (size_t)MAX(start, 0)) ? g_GraphTraceLen - (size_t)MAX(start, 0) : 0; + size = autodemod_window(start, avail); + if (size <= 0) { + size = 0; + } + } + + double *sig = NULL; + size_t count = 0, n = 0; + int res = dsp_prepare(start, size, &sig, &count, &n); + if (res != PM3_SUCCESS) { + return res; + } + + pm3_fit_t fit; + res = pm3_fit_run(sig, count, &opts, &fit); + free(sig); + + if (res == PM3_ESOFT) { + PrintAndLogEx(WARNING, "no hypothesis had enough symbols to score"); + PrintAndLogEx(INFO, "a hypothesis needs at least %d decision instants, so a clock of C needs %d*C samples" + , PM3_FIT_MIN_SYMBOLS + , PM3_FIT_MIN_SYMBOLS + ); + return res; + } + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "failed to score the hypothesis bank"); + return res; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Matched filter bank") " -------------------------------"); + PrintAndLogEx(INFO, " samples....... " _YELLOW_("%zu") " from offset " _YELLOW_("%d"), count, start); + PrintAndLogEx(INFO, " hypotheses.... " _YELLOW_("%zu") " scored", fit.count); + + if (fit.stats.valid) { + PrintAndLogEx(INFO, " short runs.... " _YELLOW_("%.3f") " of runs are under 2.5 chips", fit.stats.short_fraction); + PrintAndLogEx(INFO, " chip.......... " _YELLOW_("%.1f") " samples, longest run " _YELLOW_("%.1f") " ( ratio %.2f, %s )" + , fit.stats.chip + , fit.stats.long_run + , fit.stats.run_ratio + , fit.stats.transition_coded ? "transition coded" : "raw levels" + ); + if (fit.stats.period > 0.0) { + PrintAndLogEx(INFO, " repeats every. " _YELLOW_("%.1f") " samples ( autocorrelation %.2f )" + , fit.stats.period + , fit.stats.period_strength + ); + } else { + PrintAndLogEx(INFO, " repeats every. " _YELLOW_("no clear period")); + } + } + + size_t rows = show_all ? fit.count : MIN((size_t)top, fit.count); + print_fit_table(&fit, rows); + + if (rows < fit.count) { + PrintAndLogEx(INFO, "( %zu of %zu rows shown, use `--all` for everything )", rows, fit.count); + } + + const pm3_hyp_t *best = &fit.items[0]; + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(SUCCESS, "best fit... " _GREEN_("%s / %s") " at clock " _GREEN_("%.2f") ", phase %d, Q %d" + , pm3_mod_name(best->mod) + , pm3_enc_name(best->enc) + , best->clk_fine + , best->phase + , best->q + ); + PrintAndLogEx(INFO, " SNR_dd........ " _YELLOW_("%.2f") " dB over %zu decision instants", best->snr_dd, best->nsym); + PrintAndLogEx(INFO, " eye opening... " _YELLOW_("%.3f") " ( min / mean at the decision instants )", best->eye); + PrintAndLogEx(INFO, " mid symbol.... " _YELLOW_("%.2f") " ( relative to the decision instants )", best->mid_ratio); + + if (best->margin >= 6.0) { + PrintAndLogEx(SUCCESS, " confidence.... " _GREEN_("%.2f") " dB ahead of the next different decode", best->margin); + } else if (best->margin >= 3.0) { + PrintAndLogEx(INFO, " confidence.... " _YELLOW_("%.2f") " dB ahead of the next different decode", best->margin); + } else { + PrintAndLogEx(WARNING, " confidence.... " _RED_("%.2f") " dB ahead of the next different decode, this is ambiguous", best->margin); + } + + PrintAndLogEx(INFO, ""); + + if (verbose && fit.corr != NULL) { + + size_t len = MIN(fit.corr_len, (size_t)MAX_GRAPH_TRACE_LEN); + + double peak = 0.0; + for (size_t i = 0; i < len; i++) { + if (fit.corr[i] > peak) { + peak = fit.corr[i]; + } + } + + if (peak > 0.0) { + for (size_t i = 0; i < len; i++) { + g_GraphBuffer[i] = (int32_t)((fit.corr[i] / peak) * 127.0); + } + g_GraphTraceLen = len; + setClockGrid(best->clk, best->phase); + RepaintGraphWindow(); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(WARNING, "GraphBuffer now holds the " _YELLOW_("rank 1 correlator output") ", not the original samples"); + PrintAndLogEx(INFO, "the grid is set to the winning clock and phase"); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data plot") "`"); + } + } + + pm3_fit_free(&fit); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; +} + +// Developer helper. +// Fills the GraphBuffer with a synthetic waveform whose modulation, encoding and clock are known +static int CmdGenSignal(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "data gensignal", + "Developer tool. Fill the GraphBuffer with a synthetic LF waveform.\n" + "Used with `data fitscore` and `data autodemod`", + "data gensignal --mod ask --enc manchester --clk 64\n" + "data gensignal --mod ask --enc manchester --clk 64.25 --noise 0.4 --len 20000\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_str0(NULL, "mod", "", "modulation (def ask)"), + arg_str0(NULL, "enc", "", "encoding (def raw)"), + arg_dbl0(NULL, "clk", "", "clock in samples/symbol, may be fractional (def 64)"), + arg_int0(NULL, "len", "", "samples to generate (def 16384)"), + arg_dbl0(NULL, "noise", "", "gaussian noise standard deviation (def 0)"), + arg_dbl0(NULL, "drift", "", "linear DC ramp across the capture (def 0)"), + arg_dbl0(NULL, "env", "", "amplitude envelope swing (def 0)"), + arg_dbl0(NULL, "jitter", "", "per symbol edge jitter, samples (def 0)"), + arg_dbl0(NULL, "clip", "", "hard clip level, 0 for none (def 0)"), + arg_int0(NULL, "fc", "", "PSK subcarrier period (def 4)"), + arg_int0(NULL, "fchigh", "", "FSK long field clock (def 10)"), + arg_int0(NULL, "fclow", "", "FSK short field clock (def 8)"), + arg_int0(NULL, "q", "", "antenna Q to shape the waveform with (def 8)"), + arg_int0(NULL, "repeat", "", "loop a message of this many bits, like a real tag (def 0, random)"), + arg_int0(NULL, "seed", "", "RNG seed, for reproducible runs (def 1)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + char mod_str[8] = {0}; + int mod_len = sizeof(mod_str) - 1; + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)mod_str, mod_len, &mod_len); + + char enc_str[16] = {0}; + int enc_len = sizeof(enc_str) - 1; + CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)enc_str, enc_len, &enc_len); + + pm3_gen_opts_t opts = {0}; + opts.mod = PM3_MOD_ASK; + opts.enc = PM3_ENC_RAW; + opts.clk = arg_get_dbl_def(ctx, 3, 64.0); + int len = arg_get_int_def(ctx, 4, 16384); + opts.noise = arg_get_dbl_def(ctx, 5, 0.0); + opts.drift = arg_get_dbl_def(ctx, 6, 0.0); + opts.envelope = arg_get_dbl_def(ctx, 7, 0.0); + opts.jitter = arg_get_dbl_def(ctx, 8, 0.0); + opts.clip = arg_get_dbl_def(ctx, 9, 0.0); + opts.fc = arg_get_int_def(ctx, 10, 4); + opts.fc_hi = arg_get_int_def(ctx, 11, 10); + opts.fc_lo = arg_get_int_def(ctx, 12, 8); + opts.q = arg_get_int_def(ctx, 13, 8); + opts.repeat = arg_get_int_def(ctx, 14, 0); + opts.seed = (uint32_t)arg_get_int_def(ctx, 15, 1); + CLIParserFree(ctx); + + if (mod_len > 0) { + if (strcmp(mod_str, "ask") == 0) { + opts.mod = PM3_MOD_ASK; + } else if (strcmp(mod_str, "fsk") == 0) { + opts.mod = PM3_MOD_FSK; + } else if (strcmp(mod_str, "psk") == 0) { + opts.mod = PM3_MOD_PSK; + } else if (strcmp(mod_str, "nrz") == 0) { + opts.mod = PM3_MOD_NRZ; + } else { + PrintAndLogEx(WARNING, "unknown modulation `%s`", mod_str); + return PM3_EINVARG; + } + } + + if (enc_len > 0) { + if (strcmp(enc_str, "raw") == 0) { + opts.enc = PM3_ENC_RAW; + } else if (strcmp(enc_str, "manchester") == 0) { + opts.enc = PM3_ENC_MANCHESTER; + } else if (strcmp(enc_str, "biphase") == 0) { + opts.enc = PM3_ENC_BIPHASE; + } else { + PrintAndLogEx(WARNING, "unknown encoding `%s`", enc_str); + return PM3_EINVARG; + } + } + + if (len < 256 || len > MAX_GRAPH_TRACE_LEN) { + PrintAndLogEx(WARNING, "len must be between 256 and %d", MAX_GRAPH_TRACE_LEN); + return PM3_EINVARG; + } + if (opts.clk < 4.0 || opts.clk > 1024.0) { + PrintAndLogEx(WARNING, "clk must be between 4 and 1024"); + return PM3_EINVARG; + } + + double *buf = calloc((size_t)len, sizeof(double)); + if (buf == NULL) { + PrintAndLogEx(WARNING, "failed to allocate memory"); + return PM3_EMALLOC; + } + + int res = pm3_fit_generate(&opts, buf, (size_t)len); + if (res != PM3_SUCCESS) { + free(buf); + PrintAndLogEx(WARNING, "failed to generate the signal"); + return res; + } + + double peak = 0.0; + for (int i = 0; i < len; i++) { + if (fabs(buf[i]) > peak) { + peak = fabs(buf[i]); + } + } + if (peak <= 0.0) { + peak = 1.0; + } + + for (int i = 0; i < len; i++) { + g_GraphBuffer[i] = (int32_t)((buf[i] / peak) * 100.0); + } + g_GraphTraceLen = (size_t)len; + free(buf); + + setClockGrid(0, 0); + RepaintGraphWindow(); + + PrintAndLogEx(SUCCESS, "generated " _GREEN_("%d") " samples, %s / %s at clock " _GREEN_("%.2f") + , len + , pm3_mod_name(opts.mod) + , pm3_enc_name(opts.enc) + , opts.clk + ); + return PM3_SUCCESS; +} + + +// Run whichever of the existing demodulators the hypothesis names. This +// command is a parameter suggester, not a decoder - the bit producing path +// stays exactly the one `data rawdemod` uses, so anything it outputs is as +// auditable as before. +static int autodemod_dispatch(const pm3_hyp_t *hyp, int clk, bool invert, bool amp, bool verbose) { + + const int max_err = 100; + + switch (hyp->mod) { + + case PM3_MOD_ASK: + if (hyp->enc == PM3_ENC_BIPHASE) { + return ASKbiphaseDemod(0, clk, invert ? 1 : 0, max_err, verbose); + } + if (hyp->enc == PM3_ENC_MANCHESTER) { + return ASKDemod(clk, invert ? 1 : 0, max_err, 0, amp, verbose, false, 1); + } + // askdemod_ext() halves the clock again for askType 0, so the raw + // path wants the half symbol rate, not the bit rate. See the note + // on detectclock_fit(). + return ASKDemod(clk * 2, invert ? 1 : 0, max_err, 0, amp, verbose, false, 0); + + case PM3_MOD_FSK: + return FSKrawDemod((uint8_t)clk, invert ? 1 : 0, (uint8_t)hyp->fc_hi, (uint8_t)hyp->fc_lo, verbose); + + case PM3_MOD_PSK: + return PSKDemod(clk, invert ? 1 : 0, max_err, verbose); + + case PM3_MOD_NRZ: + return NRZrawDemod(clk, invert ? 1 : 0, max_err, verbose); + } + return PM3_ESOFT; +} + +// Count the chips the demodulator could not call. +static size_t autodemod_markers(void) { + + size_t markers = 0; + for (size_t i = 0; i < g_DemodBufferLen; i++) { + if (g_DemodBuffer[i] > 1) { + markers++; + } + } + return markers; +} + +// Second opinion on an ASK/raw decode, taken at the chip centres. +// +// The shared demodulator recovers its phase from edge positions, which is the +// only option while the clock is unknown. It does mean any asymmetry in the +// waveform lands directly on that phase: an LF tank rings down slower than it +// charges, so on a COTAG capture every falling edge arrives 10 samples late +// and every rising edge 10 samples early against a chip of 384. The chip +// centres are the furthest points from either edge, so once the clock is +// settled they are the better place to decide. +// +// Only ASK/raw, because only there does the demod buffer hold chips at the +// hypothesis clock. The Manchester and biphase paths pair and decode inside +// the shared demodulator, and raw chips are not interchangeable with that. +static bool autodemod_reslice(const pm3_hyp_t *hyp, size_t before) { + + if (hyp->mod != PM3_MOD_ASK || hyp->enc != PM3_ENC_RAW || before == 0) { + return false; + } + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, g_GraphTraceLen); + if (sig == NULL) { + return false; + } + + pm3_slice_t sl; + const int res = pm3_ask_slice(sig, g_GraphTraceLen, hyp->clk_fine, &sl); + free(sig); + + if (res != PM3_SUCCESS) { + return false; + } + + // Keep whichever demodulation had to guess less often. Both ran at the + // same clock, so the bit counts are comparable and a lower marker count + // cannot be bought by decoding fewer chips. + if (sl.nbits == 0 || sl.nerrors >= before) { + pm3_slice_free(&sl); + return false; + } + + const size_t n = MIN(sl.nbits, (size_t)MAX_DEMOD_BUF_LEN); + setDemodBuff(sl.bits, n, 0); + setClockGrid((uint32_t)(hyp->clk_fine + 0.5), (int)sl.phase); + + PrintAndLogEx(INFO, " resliced...... at the chip centres, %zu demod errors instead of %zu ( eye %.3f )" + , sl.nerrors + , before + , sl.eye + ); + + pm3_slice_free(&sl); + return true; +} + +// The equivalent hand typed command, so the tool teaches rather than replaces +// and the result can go in a script. +static void autodemod_hint(const pm3_hyp_t *hyp, int clk, bool invert, bool amp) { + + const char *flag = "--ar"; + + switch (hyp->mod) { + case PM3_MOD_ASK: + flag = (hyp->enc == PM3_ENC_BIPHASE) ? "--ab" + : (hyp->enc == PM3_ENC_MANCHESTER) ? "--am" : "--ar"; + break; + case PM3_MOD_FSK: + flag = "--fs"; + break; + case PM3_MOD_PSK: + flag = "--p1"; + break; + case PM3_MOD_NRZ: + flag = "--nr"; + break; + } + + const int shown = (hyp->mod == PM3_MOD_ASK && hyp->enc == PM3_ENC_RAW) ? (clk * 2) : clk; + + PrintAndLogEx(HINT, "Hint: the same thing by hand is `" _YELLOW_("data rawdemod %s -c %d%s%s") "`" + , flag + , shown + , invert ? " -i" : "" + , amp ? " -a" : "" + ); +} + +// How clean the demodulation came out. This is about the demodulation, not +// about what the bits mean - putting a format to them is `lf search`'s job and +// this command deliberately stops short of it. +static void autodemod_quality(void) { + + if (g_DemodBufferLen == 0) { + return; + } + + size_t markers = 0; + for (size_t i = 0; i < g_DemodBufferLen; i++) { + if (g_DemodBuffer[i] > 1) { + markers++; + } + } + + if (markers) { + PrintAndLogEx(WARNING, " quality....... %zu of %zu bits are demod errors", markers, g_DemodBufferLen); + } else { + PrintAndLogEx(SUCCESS, " quality....... no demod errors in %zu bits", g_DemodBufferLen); + } +} + +static int CmdAutoDemod(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "data autodemod", + "Tries to work out the modulation, encoding and clock of the wave in the GraphBuffer then run the matching demodulator\n" + "Adds no signal processing of its own, the analysis is `data spectrum` and `data fitscore`\n" + "A fractional clock is resampled onto an integer grid first", + "data autodemod --> analyse and demodulate\n" + "data autodemod --dry-run --> decide and print, demodulate nothing\n" + "data autodemod --thres 6 --> insist on a 6 dB margin before trusting rank 1\n" + "data autodemod --invert --amp --> pass invert and amplify through to the demod\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "start", "", "first sample of the GraphBuffer to use (def 0)"), + arg_int0(NULL, "size", "", "samples to analyse (def 16384)"), + arg_dbl0(NULL, "thres", "", "margin below which rank 1 is called ambiguous (def 3.0)"), + arg_lit0(NULL, "dry-run", "analyse and decide, but do not demodulate"), + arg_lit0("a", "amp", "amplify the signal before ASK demodulation"), + arg_lit0("i", "invert", "invert the demodulated output"), + arg_lit0("v", "verbose", "show the demodulator's own output"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int start = arg_get_int_def(ctx, 1, 0); + int size = arg_get_int_def(ctx, 2, 0); + double thres = arg_get_dbl_def(ctx, 3, 3.0); + bool dry_run = arg_get_lit(ctx, 4); + bool amp = arg_get_lit(ctx, 5); + bool invert = arg_get_lit(ctx, 6); + bool verbose = arg_get_lit(ctx, 7); + CLIParserFree(ctx); + + if (size == 0) { + const size_t avail = (g_GraphTraceLen > (size_t)MAX(start, 0)) ? g_GraphTraceLen - (size_t)MAX(start, 0) : 0; + size = autodemod_window(start, avail); + } + + double *sig = NULL; + size_t count = 0, n = 0; + int res = dsp_prepare(start, size, &sig, &count, &n); + if (res != PM3_SUCCESS) { + return res; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Auto demodulate") " -----------------------------------"); + + // the graph buffer is put back the way we found it unless we get bits out + int32_t *backup = NULL; + size_t backup_len = g_GraphTraceLen; + + // 1 - spectral analysis, to prune the bank. Zeroed up front because the + // steps below read it whether or not the analysis had anything to say. + pm3_spec_analysis_t an = {0}; + int mod_mask = 0; + + if (pm3_analyse(sig, count, n, PM3_WIN_HANN, &an) == PM3_SUCCESS) { + + PrintAndLogEx(INFO, " spectrum...... %s, confidence %s" + , pm3_family_name(an.family) + , pm3_confidence_name(an.confidence) + ); + + // only prune on a hint worth trusting, otherwise search everything + if (an.confidence >= PM3_CONF_MEDIUM) { + switch (an.family) { + case PM3_FAM_FSK: + mod_mask = 1 << PM3_MOD_FSK; + break; + case PM3_FAM_PSK: + mod_mask = 1 << PM3_MOD_PSK; + break; + case PM3_FAM_ASK: + case PM3_FAM_MANCHESTER: + mod_mask = (1 << PM3_MOD_ASK) | (1 << PM3_MOD_NRZ); + break; + case PM3_FAM_NRZ: + mod_mask = (1 << PM3_MOD_NRZ) | (1 << PM3_MOD_ASK); + break; + case PM3_FAM_UNKNOWN: + break; + } + } + } + + // 1b - a switched carrier has to be rectified before anything downstream + // will work. The demodulators threshold the samples as they stand, and a + // capture with two samples per carrier period alternates sign on every + // one of them, so every symbol averages to zero and no demodulator can + // ever produce a bit. Replace the buffer with its envelope and carry on. + if (an.needs_envelope) { + + PrintAndLogEx(INFO, " carrier....... rf/%.2f switched on and off ( envelope depth %.2f )" + , an.carrier_clk + , an.envelope_depth + ); + + double *whole = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, g_GraphTraceLen); + double *env = (whole != NULL) ? pm3_envelope(whole, g_GraphTraceLen, 0) : NULL; + free(whole); + + if (env != NULL) { + + autodemod_backup(&backup, backup_len); + autodemod_store(env, g_GraphTraceLen); + free(env); + + double *re = pm3_extract(g_GraphBuffer, g_GraphTraceLen, (size_t)start, count); + if (re != NULL) { + free(sig); + sig = re; + } + + PrintAndLogEx(INFO, " rectified..... the data is in the envelope, demodulating that instead"); + + } else { + PrintAndLogEx(WARNING, " rectified..... failed to allocate, carrying on with the raw samples"); + } + } + + if (mod_mask) { + PrintAndLogEx(INFO, " bank.......... pruned to %s", pm3_family_name(an.family)); + } else { + PrintAndLogEx(INFO, " bank.......... full, the spectral hint was not strong enough to prune on"); + } + + // 2 - rank the hypotheses + pm3_fit_opts_t opts = {0}; + opts.mod_mask = mod_mask; + + pm3_fit_t fit; + res = pm3_fit_run(sig, count, &opts, &fit); + + // a pruned bank that finds nothing is worse than no pruning + if (res != PM3_SUCCESS && mod_mask != 0) { + PrintAndLogEx(INFO, " bank.......... pruned search came up empty, retrying with everything"); + opts.mod_mask = 0; + res = pm3_fit_run(sig, count, &opts, &fit); + } + free(sig); + + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "no hypothesis fit the signal"); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data spectrum --sq") "` to see what is actually there"); + return res; + } + + const pm3_hyp_t *best = &fit.items[0]; + + PrintAndLogEx(SUCCESS, " decision...... " _GREEN_("%s / %s") " at clock " _GREEN_("%.2f") + , pm3_mod_name(best->mod) + , pm3_enc_name(best->enc) + , best->clk_fine + ); + PrintAndLogEx(INFO, " margin........ %.2f dB, SNR %.2f dB, eye %.3f", best->margin, best->snr_dd, best->eye); + + // 3 - an ambiguous choice is reported, not refused. + // + // A structural promotion is not a score race, so a negative margin there + // is expected and says nothing about confidence - the run lengths settled + // it, and they are not a matter of degree. + if (fit.promoted) { + PrintAndLogEx(INFO, " chosen on..... run length evidence, not on score"); + } else if (best->margin < thres) { + PrintAndLogEx(WARNING, "margin %.2f dB is below the %.2f dB threshold, this choice is ambiguous" + , best->margin + , thres + ); + const size_t show = MIN((size_t)3, fit.count); + for (size_t i = 0; i < show; i++) { + PrintAndLogEx(INFO, " %zu. %s / %s at clock %.2f ( SNR %.2f dB )" + , i + 1 + , pm3_mod_name(fit.items[i].mod) + , pm3_enc_name(fit.items[i].enc) + , fit.items[i].clk_fine + , fit.items[i].snr_dd + ); + } + } + + if (dry_run) { + autodemod_hint(best, (int)(best->clk_fine + 0.5), invert, amp); + PrintAndLogEx(INFO, "dry run, nothing was demodulated and the DemodBuffer is untouched"); + pm3_fit_free(&fit); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; + } + + // 4 - put a fractional clock onto an integer grid before dispatching. + // + // This is the whole point of the exercise. A clock of 64.19 walks a full + // symbol out of step after 340 symbols, so the demodulator gets the start + // of the capture right and turns the rest to mush - which presents to the + // user as a noisy tag rather than as a wrong clock. + const double frac = fabs(best->clk_fine - floor(best->clk_fine + 0.5)); + + if (frac > 0.05) { + + const double target = floor(best->clk_fine + 0.5); + const double ratio = target / best->clk_fine; + + double *whole = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, g_GraphTraceLen); + if (whole != NULL) { + + size_t out_len = 0; + double *rs = pm3_resample(whole, g_GraphTraceLen, ratio, &out_len); + free(whole); + + if (rs != NULL) { + + const size_t was = g_GraphTraceLen; + + autodemod_backup(&backup, g_GraphTraceLen); + autodemod_store(rs, out_len); + free(rs); + + PrintAndLogEx(INFO, " resampled..... clock %.2f -> %.0f, buffer %zu -> %zu samples" + , best->clk_fine + , target + , was + , out_len + ); + PrintAndLogEx(INFO, " a fractional clock drifts out of step across a long capture, this fixes that"); + } + } + } + + // 5..7 - dispatch, and fall through to the next ranked hypothesis on failure + const size_t tries = MIN((size_t)3, fit.count); + bool ok = false; + + for (size_t i = 0; i < tries; i++) { + + const pm3_hyp_t *h = &fit.items[i]; + const int clk = (int)(h->clk_fine + 0.5); + + PrintAndLogEx(INFO, " attempt %zu..... %s / %s at clock %d", i + 1, pm3_mod_name(h->mod), pm3_enc_name(h->enc), clk); + + if (autodemod_dispatch(h, clk, invert, amp, verbose) == PM3_SUCCESS && g_DemodBufferLen > 0) { + + autodemod_reslice(h, autodemod_markers()); + + PrintAndLogEx(SUCCESS, " demodulated... " _GREEN_("%zu") " bits into the DemodBuffer", g_DemodBufferLen); + autodemod_quality(); + autodemod_hint(h, clk, invert, amp); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("lf search -1") "` to identify what the bits are"); + ok = true; + break; + } + + PrintAndLogEx(INFO, " no bits, moving on"); + } + + if (ok == false) { + + PrintAndLogEx(WARNING, "none of the top %zu hypotheses produced bits", tries); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data fitscore --all") "` to see the whole ranking"); + + // leave the user's buffer as we found it if we changed it for nothing + if (backup != NULL) { + memcpy(g_GraphBuffer, backup, backup_len * sizeof(int32_t)); + g_GraphTraceLen = backup_len; + PrintAndLogEx(INFO, "the graph buffer has been put back the way it was"); + } + } + + free(backup); + pm3_fit_free(&fit); + PrintAndLogEx(NORMAL, ""); + return ok ? PM3_SUCCESS : PM3_ESOFT; +} + static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, - {"-----------", CmdHelp, AlwaysAvailable, "------------------------- " _CYAN_("General") "-------------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "------------------------------ " _CYAN_("General") " -------------------------------"}, {"clear", CmdBuffClear, AlwaysAvailable, "Clears various buffers used by the graph window"}, {"hide", CmdHide, AlwaysAvailable, "Hide the graph window"}, {"load", CmdLoad, AlwaysAvailable, "Load contents of file into graph window"}, @@ -4120,7 +5734,7 @@ static command_t CommandTable[] = { {"setdebugmode", CmdSetDebugMode, AlwaysAvailable, "Set Debugging Level on client side"}, {"xor", CmdXor, AlwaysAvailable, "Xor a input string"}, - {"-----------", CmdHelp, AlwaysAvailable, "------------------------- " _CYAN_("Modulation") "-------------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "----------------------------- " _CYAN_("Modulation") " -----------------------------"}, {"biphaserawdecode", CmdBiphaseDecodeRaw, AlwaysAvailable, "Biphase decode bin stream in DemodBuffer"}, {"detectclock", CmdDetectClockRate, AlwaysAvailable, "Detect ASK, FSK, NRZ, PSK clock rate of wave in GraphBuffer"}, {"fsktonrz", CmdFSKToNRZ, AlwaysAvailable, "Convert fsk2 to nrz wave for alternate fsk demodulating (for weak fsk)"}, @@ -4128,7 +5742,13 @@ static command_t CommandTable[] = { {"modulation", CmdDataModulationSearch, AlwaysAvailable, "Identify LF signal for clock and modulation"}, {"rawdemod", CmdRawDemod, AlwaysAvailable, "Demodulate the data in the GraphBuffer and output binary"}, - {"-----------", CmdHelp, AlwaysAvailable, "------------------------- " _CYAN_("Graph") "-------------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "-------------------------- " _CYAN_("Frequency domain") " --------------------------"}, + {"autodemod", CmdAutoDemod, AlwaysAvailable, "Detect modulation, encoding and clock, then demodulate"}, + {"fft", CmdFFT, AlwaysAvailable, "Fourier transform of the GraphBuffer"}, + {"fitscore", CmdFitScore, AlwaysAvailable, "Rank matched filter hypotheses for modulation, encoding and clock"}, + {"spectrum", CmdSpectrum, AlwaysAvailable, "Spectral peaks, symbol rate and modulation family hint"}, + + {"-----------", CmdHelp, AlwaysAvailable, "------------------------------- " _CYAN_("Graph") " --------------------------------"}, {"askedgedetect", CmdAskEdgeDetect, AlwaysAvailable, "Adjust Graph for manual ASK demod"}, {"autocorr", CmdAutoCorr, AlwaysAvailable, "Autocorrelation over window"}, {"convertbitstream", CmdConvertBitStream, AlwaysAvailable, "Convert GraphBuffer's 0/1 values to 127 / -127"}, @@ -4150,7 +5770,7 @@ static command_t CommandTable[] = { {"undecimate", CmdUndecimate, AlwaysAvailable, "Un-decimate samples"}, {"zerocrossings", CmdZerocrossings, AlwaysAvailable, "Count time between zero-crossings"}, - {"-----------", CmdHelp, AlwaysAvailable, "------------------------- " _CYAN_("Operations") "-------------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "---------------------------- " _CYAN_("Operations") " ------------------------------"}, {"asn1", CmdAsn1Decoder, AlwaysAvailable, "ASN1 decoder"}, {"atr", CmdAtrLookup, AlwaysAvailable, "ATR lookup"}, {"bitsamples", CmdBitsamples, IfPm3Present, "Get raw samples as bitstring"}, @@ -4161,7 +5781,8 @@ static command_t CommandTable[] = { {"samples", CmdSamples, IfPm3Present, "Get raw samples for graph window ( GraphBuffer )"}, {"qrcode", CmdQRcode, AlwaysAvailable, "Create a QR code"}, - {"-----------", CmdHelp, IfClientDebugEnabled, "------------------------- " _CYAN_("Debug") "-------------------------"}, + {"-----------", CmdHelp, IfClientDebugEnabled, "------------------------------- " _CYAN_("Debug") " --------------------------------"}, + {"gensignal", CmdGenSignal, IfClientDebugEnabled, "Generate a synthetic LF waveform into the GraphBuffer"}, {"test_ss8", CmdTestSaveState8, IfClientDebugEnabled, "Test the implementation of Buffer Save States (8-bit buffer)"}, {"test_ss32", CmdTestSaveState32, IfClientDebugEnabled, "Test the implementation of Buffer Save States (32-bit buffer)"}, {"test_ss32s", CmdTestSaveState32S, IfClientDebugEnabled, "Test the implementation of Buffer Save States (32-bit signed buffer)"}, diff --git a/client/src/cmdflashmemspiffs.c b/client/src/cmdflashmemspiffs.c index 170724695..ad27753a4 100644 --- a/client/src/cmdflashmemspiffs.c +++ b/client/src/cmdflashmemspiffs.c @@ -467,7 +467,7 @@ static int CmdFlashMemSpiFFSWipe(const char *Cmd) { CLIParserInit(&ctx, "mem spiffs wipe", _RED_("* * * Warning * * *") " \n" _CYAN_("This command wipes all files on the device SPIFFS file system"), - "mem spiffs wipe"); + "mem spiffs wipe"); void *argtable[] = { arg_param_begin, @@ -512,11 +512,11 @@ static int CmdFlashMemSpiFFSUpload(const char *Cmd) { CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)src, FILE_PATH_SIZE, &slen); int dlen = 0; - char dest[32] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)dest, 32, &dlen); + char dst[32] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)dst, 32, &dlen); CLIParserFree(ctx); - PrintAndLogEx(DEBUG, "Upload `" _YELLOW_("%s") "` -> `" _YELLOW_("%s") "`", src, dest); + PrintAndLogEx(DEBUG, "Upload `" _YELLOW_("%s") "` -> `" _YELLOW_("%s") "`", src, dst); size_t datalen = 0; uint8_t *data = NULL; @@ -527,11 +527,12 @@ static int CmdFlashMemSpiFFSUpload(const char *Cmd) { return PM3_EFILE; } - res = flashmem_spiffs_load(dest, data, datalen); + res = flashmem_spiffs_load(dst, data, datalen); free(data); - if (res == PM3_SUCCESS) - PrintAndLogEx(SUCCESS, "Wrote "_GREEN_("%zu") " bytes to file "_GREEN_("%s"), datalen, dest); + if (res == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "Wrote "_GREEN_("%zu") " bytes to file "_GREEN_("%s"), datalen, dst); + } PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("mem spiffs tree") "` to verify"); return res; diff --git a/client/src/cmdhf.c b/client/src/cmdhf.c index eb7ac64dc..b58a7066b 100644 --- a/client/src/cmdhf.c +++ b/client/src/cmdhf.c @@ -395,7 +395,7 @@ int CmdHFTune(const char *Cmd) { PrintAndLogEx(WARNING, "timeout while waiting for Proxmark HF shutdown, aborting"); return PM3_ETIMEOUT; } - PrintAndLogEx(NORMAL, "\x1b%c[2K\r", 30); + PrintAndLogEx(NORMAL, _CLR_LINE_ "\r"); if (verbose) { PrintAndLogEx(INFO, "Min....... %u mV", v_min); diff --git a/client/src/cmdlf.c b/client/src/cmdlf.c index 4c3372f39..56d6e71eb 100644 --- a/client/src/cmdlf.c +++ b/client/src/cmdlf.c @@ -49,6 +49,7 @@ #include "cmdlfidteck.h" // for idteck menu #include "cmdlfio.h" // for ioprox menu #include "cmdlfcotag.h" // for COTAG menu +#include "pm3_dsp.h" // pm3_extract, pm3_is_switched_carrier #include "cmdlfdestron.h" // for FDX-A FECAVA Destron menu #include "cmdlffdxb.h" // for FDX-B menu #include "cmdlfgallagher.h" // for GALLAGHER menu @@ -68,6 +69,7 @@ #include "cmdlfsecurakey.h" // for securakey menu #include "cmdlft55xx.h" // for t55xx menu #include "cmdlfti.h" // for ti menu +#include "cmdlftrovan.h" // for trovan menu #include "cmdlfviking.h" // for viking menu #include "cmdlfvisa2000.h" // for VISA2000 menu #include "cmdlfzx8211.h" // for ZX8211 menu @@ -239,7 +241,7 @@ static int CmdLFTune(const char *Cmd) { return PM3_ETIMEOUT; } - PrintAndLogEx(NORMAL, "\x1b%c[2K\r", 30); + PrintAndLogEx(NORMAL, _CLR_LINE_ "\r"); if (verbose) { PrintAndLogEx(INFO, "Min....... %u mV", v_min); PrintAndLogEx(INFO, "Max....... %u mV", v_max); @@ -1834,11 +1836,11 @@ int CmdLFRelay(const char *Cmd) { " --rdr : Reading device, act as IP client and reads LF tag and sends data\n" " --tag : Simulation device, act as IP server and simulates relayed data\n", _WHITE_("Device A, reading LF tag, client") "\n" - "lf relay --rdr --ip 192.168.1.141 -> Client, connect to IP 192.168.1.141:8000\n" - "lf relay --rdr --ip 192.168.1.141 -p 18111 -> Client, connect to IP 192.168.1.141:18111 \n\n" + "lf relay --rdr --ip 192.168.1.141 -> Client, connect to IP 192.168.1.141:8000\n" + "lf relay --rdr --ip 192.168.1.141 -p 18111 -> Client, connect to IP 192.168.1.141:18111 \n\n" _WHITE_("Device B, simulate LF tag, server") "\n" - "lf relay --tag -p 8111 -> Server listening port 8111, recv 40000 samples\n" - "lf relay --tag -s 10000 -> Server listening port 8000, recv 10000 samples\n" + "lf relay --tag -p 8111 -> Server listening port 8111, recv 40000 samples\n" + "lf relay --tag -s 10000 -> Server listening port 8000, recv 10000 samples\n" ); void *argtable[] = { @@ -2211,6 +2213,35 @@ int CmdLFfind(const char *Cmd) { } } */ + if (demodTrovan(false) == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "\nValid " _GREEN_("Trovan ID") " found!"); + if (search_cont) { + found++; + } else { + goto out; + } + } + + // COTAG last, and only when the capture actually looks like one. + if (found == 0) { + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, g_GraphTraceLen); + if (sig != NULL) { + + const bool switched = pm3_is_switched_carrier(sig, g_GraphTraceLen); + free(sig); + + if (switched && demodCOTAG(false, -1, -1) == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "\nValid " _GREEN_("COTAG ID") " found!"); + if (search_cont) { + found++; + } else { + goto out; + } + } + } + } + if (found == 0) { PrintAndLogEx(FAILED, _RED_("No known 125/134 kHz tags found!")); } @@ -2351,6 +2382,7 @@ static command_t CommandTable[] = { {"securakey", CmdLFSecurakey, AlwaysAvailable, "{ Securakey RFIDs... }"}, {"ti", CmdLFTI, AlwaysAvailable, "{ TI CHIPs... }"}, {"t55xx", CmdLFT55XX, AlwaysAvailable, "{ T55xx CHIPs... }"}, + {"trovan", CmdLFTrovan, AlwaysAvailable, "{ Trovan animal IDs... }"}, {"viking", CmdLFViking, AlwaysAvailable, "{ Viking RFIDs... }"}, {"visa2000", CmdLFVisa2k, AlwaysAvailable, "{ Visa2000 RFIDs... }"}, // {"zx", CmdLFZx8211, AlwaysAvailable, "{ ZX8211 RFIDs... }"}, diff --git a/client/src/cmdlfcotag.c b/client/src/cmdlfcotag.c index 6bf01b82b..759d6ac09 100644 --- a/client/src/cmdlfcotag.c +++ b/client/src/cmdlfcotag.c @@ -54,75 +54,6 @@ static void find_avg_high_low(const int32_t *samples, int num_samples, double *out_high_level, double *out_low_level); static double trimmed_mean_abs(const int32_t *samples, int start, int count); -#if 0 -// TODO: With the new cotag implementation shall we remove this old version -// or is there anything we could use from it? -// COTAG demod should be able to use g_GraphBuffer, -// when data load samples -int demodCOTAG(bool verbose) { - (void) verbose; // unused so far - - uint8_t bits[COTAG_BITS] = {0}; - size_t bitlen = COTAG_BITS; - memcpy(bits, g_DemodBuffer, COTAG_BITS); - - uint8_t inv_bits[COTAG_BITS] = {0}; - memcpy(inv_bits, g_DemodBuffer, COTAG_BITS); - - uint8_t alignPos = 0; - uint16_t err = manrawdecode(bits, &bitlen, 1, &alignPos); - if (err > 50) { - PrintAndLogEx(DEBUG, "DEBUG: Error - COTAG too many errors: %d", err); - return PM3_ESOFT; - } - - setDemodBuff(bits, bitlen, 0); - - //got a good demod - uint16_t cn = bytebits_to_byteLSBF(bits + 1, 16); - uint32_t fc = bytebits_to_byteLSBF(bits + 1 + 16, 8); - - uint32_t raw1 = bytebits_to_byteLSBF(bits, 32); - uint32_t raw2 = bytebits_to_byteLSBF(bits + 32, 32); - uint32_t raw3 = bytebits_to_byteLSBF(bits + 64, 32); - uint32_t raw4 = bytebits_to_byteLSBF(bits + 96, 32); - - - /* - fc 161: 1010 0001 -> LSB 1000 0101 - cn 33593 1000 0011 0011 1001 -> LSB 1001 1100 1100 0001 - cccc cccc cccc cccc ffffffff - 0 1001 1100 1100 0001 1000 0101 0000 0000 100001010000000001111011100000011010000010000000000000000000000000000000000000000000000000000000100111001100000110000101000 - 1001 1100 1100 0001 10000101 - - COTAG FC/272 - 1 7 7 D E 2 0 0 8 0 0 0 3 9 2 0 D 0 4 0000000000000 - 0001 0111 0111 1101 1110 0010 0000 0000 1000 0000 0000 0000 0011 1001 0010 0000 1101 0000 0100 0000000000000000000000000000000000000000000000000000000 - 0001 0111 0111 1101 1110 001 0010 1001 0011 1000 0110 0100 - - */ - PrintAndLogEx(SUCCESS, "COTAG Found: FC " _GREEN_("%u")", CN: " _GREEN_("%u")" Raw: %08X%08X%08X%08X", fc, cn, raw1, raw2, raw3, raw4); - - bitlen = COTAG_BITS; - err = manrawdecode(inv_bits, &bitlen, 0, &alignPos); - if (err < 50) { - uint32_t cn_large = bytebits_to_byte(inv_bits + 1, 23); - cn_large = reflect32(cn_large) >> 9; - uint8_t a = bytebits_to_byte(inv_bits + 48, 4); - uint8_t b = bytebits_to_byte(inv_bits + 52, 4); - uint8_t c = bytebits_to_byte(inv_bits + 56, 4); - uint16_t fc_large = NIBBLE_LOW(c) << 8 | NIBBLE_LOW(b) << 4 | NIBBLE_LOW(a); - - raw1 = bytebits_to_byte(inv_bits, 32); - raw2 = bytebits_to_byte(inv_bits + 32, 32); - raw3 = bytebits_to_byte(inv_bits + 64, 32); - raw4 = bytebits_to_byte(inv_bits + 96, 32); - PrintAndLogEx(SUCCESS, " FC " _GREEN_("%u")", CN: " _GREEN_("%u")" Raw: %08X%08X%08X%08X", fc_large, cn_large, raw1, raw2, raw3, raw4); - } - return PM3_SUCCESS; -} -#endif - /** * Demodulate COTAG samples. * @@ -141,64 +72,80 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s uint8_t *manchester_demod = NULL; uint8_t *manchester_demod_reversed = NULL; - /* Calculate min, max, average */ + int manchester_count = 0; + const char *fail = NULL; + int64_t sum = 0; min = max = samples[0]; + for (int i = 0; i < num_samples; i++) { sum += samples[i]; - if (samples[i] < min) min = samples[i]; - if (samples[i] > max) max = samples[i]; + if (samples[i] < min) { + min = samples[i]; + } + + if (samples[i] > max) { + max = samples[i]; + } } + avg = (double)sum / (double)num_samples; if (verbose) { PrintAndLogEx(INFO, " Clock: %d", clock); - if (threshold < 0) + if (threshold < 0) { PrintAndLogEx(INFO, " Threshold: auto"); - else + } else { PrintAndLogEx(INFO, " Threshold: %.2f", threshold); + } PrintAndLogEx(INFO, " Min : %" PRId32, min); PrintAndLogEx(INFO, " Max : %" PRId32, max); - PrintAndLogEx(INFO, " Avg : %.2f\n", avg); - printf("\n"); + PrintAndLogEx(INFO, " Avg : %.2f", avg); + PrintAndLogEx(NORMAL, ""); } - /* DC offset removal: subtract average from every sample */ + // DC offset removal: subtract average from every sample for (int i = 0; i < num_samples; i++) { double v = round((double)samples[i] - avg); samples[i] = (int32_t)v; } - /* Auto threshold estimation: */ + // Auto threshold estimation if (threshold < 0) { double high_level, low_level; find_avg_high_low(samples, num_samples, clock, &high_level, &low_level); threshold = (low_level + high_level) * 0.5; - if (verbose) - PrintAndLogEx(INFO, " Auto threshold: low_level=%.2f, high_level=%.2f --> threshold=%.2f", low_level, high_level, threshold); + if (verbose) { + PrintAndLogEx(INFO, " Auto threshold: low_level = %.2f, high_level = %.2f --> threshold = %.2f", low_level, high_level, threshold); + } } - /* Auto clock-start detection (first edge detection) */ + // Auto clock-start detection (first edge detection) if (clock_start == -1) { clock_start = detect_edge(samples, num_samples, 0, threshold); - if (verbose) + if (verbose) { PrintAndLogEx(INFO, " Detected clock start candidate: sample #%d", clock_start); + } } - /* High/low raw demodulation of clock-half cycles */ + // High/low raw demodulation of clock-half cycles int high_low_demod_01_len = (num_samples - clock_start) / clock_half + 16; + high_low_demod_01 = calloc(high_low_demod_01_len, sizeof(uint8_t)); - if (!high_low_demod_01) { + if (high_low_demod_01 == NULL) { PrintAndLogEx(ERR, "Error: out of memory"); rv = PM3_EMALLOC; goto end; } + int high_low_demod_01_count = 0; for (int idx = clock_start; idx + clock <= num_samples; idx += clock) { - /* Trimmed mean of absolute values of first half: */ + + // Trimmed mean of absolute values of first half double clock_half1_val = trimmed_mean_abs(samples, idx, clock_half); - /* Trimmed mean of absolute values of second half: */ + + // Trimmed mean of absolute values of second half double clock_half2_val = trimmed_mean_abs(samples, idx + clock_half, clock_half); uint8_t half1 = (clock_half1_val >= threshold) ? 1 : 0; @@ -209,38 +156,52 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s high_low_demod_01_count += 2; } - /* Manchester demodulation buffer: */ + // Manchester demodulation buffer int manchester_demod_len = high_low_demod_01_count / 2; manchester_demod = calloc(manchester_demod_len, sizeof(uint8_t)); - if (!manchester_demod) { + if (manchester_demod == NULL) { PrintAndLogEx(ERR, "Error: out of memory"); rv = PM3_EMALLOC; goto end; } - /* Manchester demodulation (Thomas) from raw high/low demod (high_low_demod_01) */ + // Manchester demodulation (Thomas) from raw high/low demod (high_low_demod_01) const int MAX_MANDEMOD_ERRORS = 64; bool demod_success = true; - int manchester_count = 0; int mandemod_err_count = 0; + for (int i = 0; i + 1 < high_low_demod_01_count;) { + uint8_t half1 = high_low_demod_01[i]; uint8_t half2 = high_low_demod_01[i + 1]; if (half1 == 0 && half2 == 1) { + manchester_demod[manchester_count++] = 0; i += 2; + } else if (half1 == 1 && half2 == 0) { + manchester_demod[manchester_count++] = 1; i += 2; + } else { - if (verbose) - PrintAndLogEx(INFO, " Manchester demod error: index %d (sample #%" PRId32 "): half1=%u, half2=%u --> clock align by half cycle", - i, (int32_t)(clock_start + i * clock_half), (unsigned)half1, (unsigned)half2); - i += 1; /* re-align by one half-clock forward */ + + if (verbose) { + PrintAndLogEx(INFO, " Manchester demod error: index %d (sample #%" PRId32 "): half1=%u, half2=%u --> clock align by half cycle" + , i + , (int32_t)(clock_start + i * clock_half) + , (unsigned)half1, (unsigned)half2 + ); + } + + // re-align by one half-clock forward + i += 1; + mandemod_err_count++; + if (mandemod_err_count >= MAX_MANDEMOD_ERRORS) { - PrintAndLogEx(ERR, " Manchester demod: too many errors (%d), giving up", mandemod_err_count); + fail = "too many Manchester errors"; demod_success = false; break; } @@ -248,31 +209,34 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s } if (demod_success) { - if (verbose) + if (verbose) { PrintAndLogEx(INFO, " Manchester demod: %d bits", manchester_count); - } else + } + } else { goto end; - printf("\n"); + } - /* Reverse order of bits */ + // Reverse order of bits manchester_demod_reversed = calloc(manchester_demod_len, sizeof(uint8_t)); - if (!manchester_demod_reversed) { + if (manchester_demod_reversed == NULL) { PrintAndLogEx(ERR, "Error: out of memory"); rv = PM3_EMALLOC; goto end; } - for (int i = 0; i < manchester_count; i++) - manchester_demod_reversed[i] = manchester_demod[manchester_count - 1 - i]; - { + for (int i = 0; i < manchester_count; i++) { + manchester_demod_reversed[i] = manchester_demod[manchester_count - 1 - i]; + } + + if (verbose) { char manchester_demod_reversed_str[manchester_count + 1]; - for (int i = 0; i < manchester_count; i++) + for (int i = 0; i < manchester_count; i++) { manchester_demod_reversed_str[i] = '0' + manchester_demod_reversed[i]; + } manchester_demod_reversed_str[manchester_count] = '\0'; - PrintAndLogEx(SUCCESS, " Manchester demod reversed:"); - PrintAndLogEx(SUCCESS, " %s", manchester_demod_reversed_str); - printf("\n"); - printf("\n"); + PrintAndLogEx(INFO, " Manchester demod reversed:"); + PrintAndLogEx(INFO, " %s", manchester_demod_reversed_str); + PrintAndLogEx(NORMAL, ""); } /* @@ -284,21 +248,21 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s * Card number: 0x24A88F */ - /* Find preamble. */ + // Find preamble. static const uint8_t preamble_a[] = { - /* type A: 62 zeros followed by 1,0,1,0,0,0 */ + // type A: 62 zeros followed by 1,0,1,0,0,0 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0 }; static const uint8_t preamble_p[] = { - /* type P: 55 zeros followed by 1,0,0,0,0,0,1 */ + // type P: 55 zeros followed by 1,0,0,0,0,0,1 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1 }; static const uint8_t preamble_p0[] = { - /* type P-0: 61 zeros followed by 1 */ + // type P-0: 61 zeros followed by 1 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 @@ -320,8 +284,8 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s for (int i = 0; i < manchester_count && preamble_index < 0; i++) { for (int p = 0; preamble_patterns[p].pat != NULL; p++) { - if (i + preamble_patterns[p].len <= manchester_count && - memcmp(&manchester_demod_reversed[i], preamble_patterns[p].pat, preamble_patterns[p].len) == 0) { + if ( ((i + preamble_patterns[p].len) <= manchester_count) + && (memcmp(&manchester_demod_reversed[i], preamble_patterns[p].pat, preamble_patterns[p].len) == 0)) { preamble_index = i; preamble_type = preamble_patterns[p].name; break; @@ -330,51 +294,64 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s } if (preamble_index < 0) { - PrintAndLogEx(INFO, " Preamble not found in manchester_demod_reversed"); + fail = "no preamble found"; goto end; } - PrintAndLogEx(SUCCESS, " Preamble found (type %s) in manchester_demod_reversed at index %d", - preamble_type, preamble_index); + if (verbose) { + PrintAndLogEx(INFO, " Preamble found (type %s) in manchester_demod_reversed at index %d", preamble_type, preamble_index); + } - /* data_bits: 128 bits starting at preamble */ + // data_bits: 128 bits starting at preamble if (preamble_index + LF_COTAG_DATA_LEN > manchester_count) { - PrintAndLogEx(INFO, " Not enough bits after preamble for full 128-bit data block"); + fail = "preamble found but the capture ends before a full 128 bit block"; rv = PM3_EPARTIAL; goto end; } const uint8_t *data_bits = &manchester_demod_reversed[preamble_index]; - /* Print raw 128 bits */ - { + // Print raw 128 bits + if (verbose) { char str[LF_COTAG_DATA_LEN + 1]; - for (int i = 0; i < LF_COTAG_DATA_LEN; i++) + for (int i = 0; i < LF_COTAG_DATA_LEN; i++) { str[i] = '0' + data_bits[i]; + } str[LF_COTAG_DATA_LEN] = '\0'; PrintAndLogEx(SUCCESS, " data bits: %s", str); } - /* Print bits grouped by 4, space-separated */ - { + + // Print bits grouped by 4, space-separated + if (verbose) { char str[LF_COTAG_DATA_LEN + LF_COTAG_DATA_LEN / 4]; + int p = 0; for (int i = 0; i < LF_COTAG_DATA_LEN; i += 4) { - if (i > 0) str[p++] = ' '; - for (int j = 0; j < 4; j++) + + if (i > 0) { + str[p++] = ' '; + } + + for (int j = 0; j < 4; j++) { str[p++] = '0' + data_bits[i + j]; + } } + str[p] = '\0'; PrintAndLogEx(SUCCESS, " data bits: %s", str); } - /* Print bits as hex nibbles */ - { + + // Print bits as hex nibbles + if (verbose) { char str[LF_COTAG_DATA_LEN / 4 * 5 + 1]; int p = 0; for (int i = 0; i < LF_COTAG_DATA_LEN; i += 4) { - int nibble = (data_bits[i] << 3) - | (data_bits[i + 1] << 2) - | (data_bits[i + 2] << 1) - | data_bits[i + 3]; + + int nibble = (data_bits[i] << 3) + | (data_bits[i + 1] << 2) + | (data_bits[i + 2] << 1) + | data_bits[i + 3]; + str[p++] = ' '; str[p++] = ' '; str[p++] = ' '; @@ -385,17 +362,19 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s PrintAndLogEx(SUCCESS, " data hex: %s", str); } - /* Card number: last 24 bits of data_bits as an integer */ + // Card number: last 24 bits of data_bits as an integer uint32_t c_num = 0; - for (int i = LF_COTAG_DATA_LEN - 24; i < LF_COTAG_DATA_LEN; i++) + for (int i = LF_COTAG_DATA_LEN - 24; i < LF_COTAG_DATA_LEN; i++) { c_num = (c_num << 1) | data_bits[i]; - PrintAndLogEx(SUCCESS, " card number: 0x%X == %u", c_num, c_num); + } - /* Count how many subsequent 128-bit blocks equal data_bits */ + // Count how many subsequent 128-bit blocks equal data_bits int repeat_count = 0; bool fully_repeats = true; int pos = preamble_index + LF_COTAG_DATA_LEN; + while (pos + LF_COTAG_DATA_LEN <= manchester_count) { + if (memcmp(&manchester_demod_reversed[pos], data_bits, LF_COTAG_DATA_LEN) == 0) { repeat_count++; } else { @@ -405,14 +384,28 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s pos += LF_COTAG_DATA_LEN; } - if (fully_repeats && repeat_count > 0) - PrintAndLogEx(INFO, " Sequence fully repeats until the end %d time(s)", repeat_count); - else - PrintAndLogEx(INFO, " Sequence does NOT match at index %d (repeat count = %d)", pos, repeat_count); - printf("\n"); + if (verbose) { + if (fully_repeats && repeat_count > 0) { + PrintAndLogEx(INFO, " Sequence fully repeats until the end %d time(s)", repeat_count); + } else { + PrintAndLogEx(INFO, " Sequence does NOT match at index %d (repeat count = %d)", pos, repeat_count); + } + } + + PrintAndLogEx(SUCCESS, "COTAG - Card number " _GREEN_("%u") " ( 0x%06X )", c_num, c_num); rv = PM3_SUCCESS; end: + // One line when there is no card number to show, so a failed demod says + // how far it got rather than either going silent or dumping every stage. + if (rv != PM3_SUCCESS && fail != NULL) { + PrintAndLogEx(FAILED, "COTAG demod failed - %s ( %d Manchester bits at rf/%d )" + , fail + , manchester_count + , clock + ); + } + free(manchester_demod_reversed); free(manchester_demod); free(high_low_demod_01); @@ -431,7 +424,8 @@ int demodCOTAG(bool verbose, int clock, int threshold) { static int CmdCOTAGDemod(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "lf cotag demod", - "Demodulate COTAG samples from g_GraphBuffer. Try to find COTAG preamble, if found decode / descramble data.", + "Demodulate COTAG samples from g_GraphBuffer.\n" + "Try to find COTAG preamble, if found decode / descramble data.", "lf cotag demod" ); @@ -490,8 +484,9 @@ static int CmdCOTAGReader(const char *Cmd) { return PM3_EINVARG; } - if (g_session.pm3_present == false) + if (g_session.pm3_present == false) { return PM3_ENOTTY; + } uint8_t effective_divisor = (divisor > -1) ? (uint8_t)divisor : LF_COTAG_DIVISOR; @@ -542,97 +537,13 @@ static int CmdCOTAGReader(const char *Cmd) { return ret; } -#if 0 // TODO: Remove this implementation? -// When reading a COTAG. -// 0 = HIGH/LOW signal - maxlength bigbuff -// 1 = translation for HI/LO into bytes with manchester 0,1 - length 300 -// 2 = raw signal - maxlength bigbuff -int CmdCOTAGReader_old(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "lf cotag reader", - "read a COTAG tag, the current support for COTAG is limited. ", - "lf cotag reader -2" - ); - - void *argtable[] = { - arg_param_begin, - arg_lit0("1", NULL, "HIGH/LOW signal; maxlength bigbuff"), - arg_lit0("2", NULL, "translation of HIGH/LOW into bytes with manchester 0,1"), - arg_lit0("3", NULL, "raw signal; maxlength bigbuff"), - arg_param_end - }; - - CLIExecWithReturn(ctx, Cmd, argtable, false); - bool mode0 = arg_get_lit(ctx, 1); - bool mode1 = arg_get_lit(ctx, 2); - bool mode2 = arg_get_lit(ctx, 3); - CLIParserFree(ctx); - - if ((mode0 + mode1 + mode2) > 1) { - PrintAndLogEx(ERR, "You can only use one option at a time"); - return PM3_EINVARG; - } - uint8_t mode = 0xFF; - if (mode0) - mode = 0; - if (mode1) - mode = 1; - if (mode2) - mode = 2; - - struct p { - uint8_t mode; - } PACKED payload; - payload.mode = mode; - - PacketResponseNG resp; - clearCommandBuffer(); - SendCommandNG(CMD_LF_COTAG_READ, (uint8_t *)&payload, sizeof(payload)); - - uint8_t timeout = 3; - int res = PM3_SUCCESS; - while (WaitForResponseTimeout(CMD_LF_COTAG_READ, &resp, 1000) == false) { - timeout--; - if (timeout == 0) { - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(WARNING, "command execution time out"); - SendCommandNG(CMD_BREAK_LOOP, NULL, 0); - res = PM3_ETIMEOUT; - } - } - - if (res != PM3_SUCCESS) { - return res; - } - - if (timeout != 3) - PrintAndLogEx(NORMAL, ""); - - switch (payload.mode) { - case 0: - case 2: { - CmdPlot(""); - CmdGrid("-x 384"); - getSamples(0, false); - break; - } - case 1: { - memcpy(g_DemodBuffer, resp.data.asBytes, resp.length); - g_DemodBufferLen = resp.length; - return demodCOTAG(true); - } - } - return PM3_SUCCESS; -} -#endif - static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"demod", CmdCOTAGDemod, AlwaysAvailable, "demodulate a COTAG tag"}, {"reader", CmdCOTAGReader, IfPm3Lf, "attempt to read and extract tag data"}, {NULL, NULL, NULL, NULL} }; + static int CmdHelp(const char *Cmd) { (void)Cmd; // Cmd is not used so far CmdsHelp(CommandTable); @@ -662,22 +573,25 @@ static int cmp_int32_asc(const void *a, const void *b) { * resolution for very low amplitude captures. */ static double trimmed_mean_abs(const int32_t *samples, int start, int count) { - /* The values are sorted and the top 1/TRIM_DROP_DEN are discarded before averaging */ + /// The values are sorted and the top 1/TRIM_DROP_DEN are discarded before averaging const int TRIM_DROP_DEN = 4; // trimmed mean denominator int32_t buf[count]; - for (int k = 0; k < count; k++) + for (int k = 0; k < count; k++) { buf[k] = abs(samples[start + k]); + } qsort(buf, count, sizeof(int32_t), cmp_int32_asc); int keep = count - count / TRIM_DROP_DEN; - if (keep < 1) + if (keep < 1) { keep = 1; + } int64_t sum = 0; - for (int k = 0; k < keep; k++) + for (int k = 0; k < keep; k++) { sum += buf[k]; + } return (double)sum / (double)keep; } @@ -691,24 +605,26 @@ static double trimmed_mean_abs(const int32_t *samples, int start, int count) { * @param out_high_level the highest window trimmed mean seen * @param out_low_level the lowest window trimmed mean seen */ -static void find_avg_high_low(const int32_t *samples, int num_samples, - int clock, - double *out_high_level, double *out_low_level) { +static void find_avg_high_low(const int32_t *samples, int num_samples, int clock, double *out_high_level, double *out_low_level) { const int WINDOW = 256; double high_level = -DBL_MAX; double low_level = DBL_MAX; int scan_end = 8 * clock; - if (scan_end > num_samples) + if (scan_end > num_samples) { scan_end = num_samples; + } for (int i = 0; i + WINDOW <= scan_end; i++) { double window_val = trimmed_mean_abs(samples, i, WINDOW); - if (window_val < low_level) + if (window_val < low_level) { low_level = window_val; - if (window_val > high_level) + } + + if (window_val > high_level) { high_level = window_val; + } } *out_high_level = high_level; @@ -726,12 +642,13 @@ static void find_avg_high_low(const int32_t *samples, int num_samples, * * @return Index of the detected edge, or 0 if none found. */ -static int detect_edge(const int32_t *samples, int num_samples, - int index_start, double threshold) { +static int detect_edge(const int32_t *samples, int num_samples, int index_start, double threshold) { + const int GLITCH_WINDOW = 10; - if (num_samples <= 0 || index_start < 0 || index_start >= num_samples) + if (num_samples <= 0 || index_start < 0 || index_start >= num_samples) { return 0; + } bool prev_high = abs(samples[index_start]) >= threshold; @@ -739,18 +656,18 @@ static int detect_edge(const int32_t *samples, int num_samples, bool curr_high = abs(samples[i]) >= threshold; if (curr_high != prev_high) { - /* Need enough samples on both sides for glitch check */ + // Need enough samples on both sides for glitch check if ((int)i < GLITCH_WINDOW || i + GLITCH_WINDOW > num_samples) { prev_high = curr_high; continue; } - /* Sum of GLITCH_WINDOW absolute values before the crossing */ + // Sum of GLITCH_WINDOW absolute values before the crossing int64_t before_sum = 0; for (int k = 0; k < GLITCH_WINDOW; k++) before_sum += abs(samples[i - GLITCH_WINDOW + k]); - /* Sum of GLITCH_WINDOW absolute values after the crossing */ + // Sum of GLITCH_WINDOW absolute values after the crossing int64_t after_sum = 0; for (int k = 0; k < GLITCH_WINDOW; k++) after_sum += abs(samples[i + k]); @@ -758,8 +675,9 @@ static int detect_edge(const int32_t *samples, int num_samples, bool before_high = (double)before_sum >= threshold * GLITCH_WINDOW; bool after_high = (double)after_sum >= threshold * GLITCH_WINDOW; - if (before_high != after_high) + if (before_high != after_high) { return (int)i; + } } prev_high = curr_high; diff --git a/client/src/cmdlfem4x70.c b/client/src/cmdlfem4x70.c index 8fd80e3d7..a1b0c5b32 100644 --- a/client/src/cmdlfem4x70.c +++ b/client/src/cmdlfem4x70.c @@ -310,7 +310,6 @@ static int brute_em4x70(const em4x70_cmd_input_brute_t *opts, em4x70_cmd_output_ // Lowers the cognitive load AND makes it easier to understand. // opts structure stored value in BIG ENDIAN // Note that the FIRMWARE side will swap the byte order back to BIG ENDIAN. - // (yes, this is a bit of a mess, but it is what it is for now...) uint16_t start_key_be = (opts->partial_key_start[0] << 8) | opts->partial_key_start[1]; etd.start_key = start_key_be; @@ -994,7 +993,7 @@ static int CmdEM4x70Recover(const char *Cmd) { alt_grn.grn[2] ); } - printf("\n"); + PrintAndLogEx(NORMAL, ""); // which of those keys actually validates? if (recover_ctx.opts.verify) { diff --git a/client/src/cmdlfem4x70.h b/client/src/cmdlfem4x70.h index 7b5afdae8..18f066155 100644 --- a/client/src/cmdlfem4x70.h +++ b/client/src/cmdlfem4x70.h @@ -20,12 +20,12 @@ #define CMDLFEM4X70_H__ #include "common.h" +#include #define TIMEOUT 2000 int CmdLFEM4X70(const char *Cmd); -// for `lf search`: bool detect_4x70_block(void); #endif diff --git a/client/src/cmdlft55xx.c b/client/src/cmdlft55xx.c index b07c4e015..728d296b5 100644 --- a/client/src/cmdlft55xx.c +++ b/client/src/cmdlft55xx.c @@ -22,6 +22,8 @@ #endif #include "cmdlft55xx.h" +#include "pm3_dsp.h" // pm3_extract +#include "pm3_fit.h" // matched filter hypothesis bank #include #include // MingW #include "cmdparser.h" // command_t @@ -51,7 +53,11 @@ #define T55XX_PrintConfig true #define T55XX_DontPrintConfig false -//static uint8_t bit_rates[9] = {8, 16, 32, 40, 50, 64, 100, 128, 0}; +#define T55XX_PSK3_MAX_CAND 32 + +static size_t t55xx_psk3_block0_candidates(uint32_t observed, uint8_t clk, uint32_t *out, size_t max); +static bool t55xx_config_psk3_ambiguous(void); +static bool t55xx_psk3_probe(bool usepwd, uint32_t password, uint8_t downlink_mode); // Default configuration static t55xx_conf_block_t config = { @@ -66,265 +72,6 @@ static t55xx_conf_block_t config = { }; static t55xx_memory_item_t cardmem[T55x7_BLOCK_COUNT] = {{0}}; -/* -#define DC(x) ((x) + 128) - -static bool t55xx_is_valid_block0(uint32_t block, uint8_t rfclk, uint8_t pskcf) { - - if (block == 0x00) { - return false; - } - - // Master key = 6 or 9 - if ((((block >> 28)& 0xF) != 0x0) && - (((block >> 28)& 0xF) != 0x6) && - (((block >> 28)& 0xF) != 0x9)) { - return false; - } - - // X Mode - if ( ((block >> 17) & 1) && ((((block >> 28) & 0xf) == 0x6) || (((block >> 28) & 0xf) == 0x9)) ) { - // X mode fixed 0 bits - if ((block & 0x0F000000) != 0x00) { - return false; - } - } else { - // / Basic Mode fixed 0 bits - if ((block & 0x0FE00106) != 0x00) { - return false; - } - } - - // Modulation - if ( (((block >> 12) & 0x1F) != 0x00) && // Direct - (((block >> 12) & 0x1F) != 0x01) && // PSK1 - (((block >> 12) & 0x1F) != 0x02) && // PSK2 - (((block >> 12) & 0x1F) != 0x03) && // PSK3 - (((block >> 12) & 0x1F) != 0x04) && // FSK1 - (((block >> 12) & 0x1F) != 0x05) && // FSK2 - (((block >> 12) & 0x1F) != 0x06) && // FSK1a - (((block >> 12) & 0x1F) != 0x07) && // FSK2a - (((block >> 12) & 0x1F) != 0x08) && // Manchester - (((block >> 12) & 0x1F) != 0x10) && // Bi-phase - (((block >> 12) & 0x1F) != 0x18) ) { // Reserved - return false; - } - - PrintAndLogEx(DEBUG, "suggested block... %08x", block); - - // check pskcf - if ((pskcf <= 3) && (((block >> 10) & 0x3) != pskcf)) { - PrintAndLogEx(DEBUG, "fail 6 - %u %u", pskcf, (block >> 10) & 0x3); - return false; - } - - uint8_t testSpeed; - - // check rfclk - if ((((block >> 17) & 1) == 1) && ((((block >> 28) & 0xf) == 0x6) || (((block >> 28) & 0xf) == 0x9)) ){ // X mode speedBits - testSpeed = (((block >> 18) & 0x3F) * 2) + 2; - } else { - uint8_t basicSpeeds[] = {8,16,32,40,50,64,100,128}; - testSpeed = basicSpeeds[(block >> 18) & 0x7]; - } - - if (testSpeed != rfclk) { - PrintAndLogEx(DEBUG, "fail 7 - %u %u ", testSpeed , rfclk); - return false; - } - return true; -} - -static void t55xx_psk1_demod (int *data, uint8_t rfclk, uint8_t pskcf, uint32_t *block) { - - if ((rfclk < 8) || (rfclk > 128)) { - return; - } - - switch (pskcf) { - case 0: { - pskcf = 2; - break; - } - case 1: { - pskcf = 4; - break; - } - case 2: { - pskcf = 8; - break; - } - default: { - break; - } - } - - int startOffset = 1; // where to start reading data samples - int sampleCount = 0; // Counter for 1 bit of samples - int samples0, samples1; // Number of High even and odd bits in a sample. - int startBitOffset = 1; // which bit to start at e.g. for rf/32 1 33 65 ... - int bitCount = 0; - uint32_t myblock = 0; - int offset; - uint8_t drift = 0; - uint8_t tuneOffset = 0; - - drift = (rfclk % pskcf); // 50 2 = 1 50 4 = 2 - - // locate first "0" - high transisiton for correct start offset - while (DC(data[startOffset]) <= (DC(data[startOffset - 1]) + 5)) { - // sampleToggle ^= 1; - startOffset++; - } - - // Start sample may be 1 off due to sample alignment with chip modulation - // so seach for the first lower value, and adjust as needed - if (pskcf == 2) { - - tuneOffset = startOffset + 1; - - while (DC(data[tuneOffset]) >= (DC(data[tuneOffset - 1]) + 5)) { - tuneOffset++; - } - - if ((tuneOffset - startOffset - 1) % 2) { - startOffset++; - } - } - - uint8_t pskcfidx = 0; - - // Get the offset to the first sample of the data block - offset = (rfclk * startBitOffset) + startOffset; - - pskcfidx = (drift / 2); - pskcfidx = pskcfidx % pskcf; - - // while data my be in the settle period of sampling - // First 18 - 24 bits not usable for reference only - while (offset < 20) { - offset += (32 * rfclk); - } - - // Read 1 block of data - for (bitCount = 0; bitCount < 32; bitCount++) { - - samples0 = 0; - samples1 = 0; - - // Get 1 bit of data - for (sampleCount = 0; sampleCount < rfclk; sampleCount++){ - // Count number of even and odd high bits at center to edge - switch (pskcf) { - case 2: { - - // if current sample is high - if (DC(data[offset]) > DC(data[offset + 1])) { - if (pskcfidx == 0) { - samples0++; - } else { - samples1++; - } - } - break; - } - case 4: { - - // only check pskcf 2nd bit x 1 x x - if (pskcfidx == 1) { - - // if current sample is high - if (DC(data[offset]) > DC(data[offset + 2])) { - samples0++; - } else { - samples1++; - } - } - break; - } - case 8: { - - if (pskcfidx == 3) { // x x x 1 x x x x // 00041840 : FFFBE7BF - - // if current sample is high - if (DC(data[offset]) > DC(data[offset + 4])) { - samples0++; - } else { - samples1++; - } - } - break; - } - default: { - break; - } - } - - // If at bit boundary (after first bit) then adjust phase check for drift - if ((sampleCount > 0) && (sampleCount % rfclk) == 0) { - pskcfidx -= drift; - } - - offset++; - pskcfidx++; - pskcfidx = pskcfidx % pskcf; - } - - myblock <<= 1; - if (samples1 > samples0) { - myblock++; - } - } - - *block = myblock; -} - -static void t55xx_psk2_demod (int *data, uint8_t rfclk, uint8_t pskcf, uint32_t *block) { - // decode PSK - t55xx_psk1_demod (data, rfclk, pskcf, block); - - uint32_t new_block = 0; - uint8_t prev_phase = 1; - - // Convert to PSK2 - for (int8_t bit = 31; bit >= 0; bit--) { - - new_block <<= 1; - - if (((*block >> bit) & 1) != prev_phase) { - new_block++; - } - - prev_phase = ((*block >> bit) & 1); - } - - *block = new_block; -} - -static void t55xx_search_config_psk(int *d, int pskV) { - - for (uint8_t pskcf = 0; pskcf < 3; pskcf++) { - - for (uint8_t speedBits = 0; speedBits < 64; speedBits++) { - - uint8_t rfclk = rfclk = (2 * speedBits) + 2; - uint32_t block = 0; - - if (pskV == 1) { - t55xx_psk1_demod (d, rfclk, pskcf, &block); - } - - if (pskV == 2) { - t55xx_psk2_demod (d, rfclk, pskcf, &block); - } - - if (t55xx_is_valid_block0(block, rfclk, pskcf)) { - PrintAndLogEx(SUCCESS, "Valid config block [%08X] - rfclk [%d] - pskcf [%d]", block, rfclk, pskcf); - } - } - } -} -*/ t55xx_conf_block_t Get_t55xx_Config(void) { return config; @@ -420,6 +167,7 @@ static int CmdT55xxCloneHelp(const char *Cmd) { PrintAndLogEx(NORMAL, _GREEN_("lf presco clone")); PrintAndLogEx(NORMAL, _GREEN_("lf pyramid clone")); PrintAndLogEx(NORMAL, _GREEN_("lf securakey clone")); + PrintAndLogEx(NORMAL, _GREEN_("lf trovan clone")); PrintAndLogEx(NORMAL, _GREEN_("lf viking clone")); PrintAndLogEx(NORMAL, _GREEN_("lf visa2000 clone")); return PM3_SUCCESS; @@ -442,6 +190,7 @@ int clone_t55xx_tag(uint32_t *blockdata, uint8_t numblocks) { if (blockdata == NULL) return PM3_EINVARG; + if (numblocks < 1 || numblocks > 8) return PM3_EINVARG; @@ -477,12 +226,14 @@ int clone_t55xx_tag(uint32_t *blockdata, uint8_t numblocks) { if (i == 0) { SetConfigWithBlock0(blockdata[0]); - if (t55xxAcquireAndCompareBlock0(false, 0, blockdata[0], false)) + if (t55xxAcquireAndCompareBlock0(false, 0, blockdata[0], false)) { continue; + } } - if (t55xxVerifyWrite(i, 0, false, false, 0, 0xFF, blockdata[i]) == false) + if (t55xxVerifyWrite(i, 0, false, false, 0, 0xFF, blockdata[i]) == false) { res++; + } } if (res == 0) @@ -1301,6 +1052,24 @@ static int CmdT55xxDetect(const char *Cmd) { found = t55xxTryDetectModulation(downlink_mode, T55XX_PrintConfig); } + // With a tag on the antenna the psk2 / psk3 ambiguity can be settled rather + // than merely reported: read a few data blocks and apply the adjacent ones + // invariant. It needs the card, so it is skipped offline, where the single + // saved block 0 buffer cannot answer the question either way. + if (found && use_gb == false && t55xx_config_psk3_ambiguous()) { + + if (t55xx_psk3_probe(config.usepwd, config.pwd, config.downlink_mode)) { + config.modulation = DEMOD_PSK3; + PrintAndLogEx(SUCCESS, "Data blocks agree this is " _GREEN_("psk3") ", not psk2 - block 0 is one of the words listed above"); + } + + // put the configuration block back in the demod buffer, so anything + // reading it after us sees block 0 rather than the last probed block + if (AcquireData(T55x7_PAGE0, T55x7_CONFIGURATION_BLOCK, config.usepwd, config.pwd, config.downlink_mode)) { + DecodeT55xxBlock(); + } + } + if (found == false) { config.usepwd = false; config.pwd = 0x00; @@ -1315,6 +1084,494 @@ bool t55xxTryDetectModulation(uint8_t downlink_mode, bool print_config) { return t55xxTryDetectModulationEx(downlink_mode, print_config, 0, -1); } +#define PM3_T55_FALLBACK_MAXERR 100 + +static bool block0_repeats_at_stride(uint8_t offset) { + + if ((size_t)offset + 64 > g_DemodBufferLen || offset > 255 - 32) { + return false; + } + return (PackBits(offset, 32, g_DemodBuffer) == PackBits((uint8_t)(offset + 32), 32, g_DemodBuffer)); +} + +static void t55xx_psk_coherent(int fitclk, uint8_t clk, t55xx_conf_block_t *tests, uint8_t *hits, uint8_t downlink_mode) { + + static const int subcarriers[] = { 2, 4, 8 }; + + if (g_GraphTraceLen < 2048 || fitclk < 4) { + return; + } + + size_t count = g_GraphTraceLen; + if (count > 16384) { + count = 16384; + } + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, count); + if (sig == NULL) { + return; + } + + uint8_t *raw = calloc(MAX_DEMOD_BUF_LEN, sizeof(uint8_t)); + uint8_t *work = calloc(MAX_DEMOD_BUF_LEN, sizeof(uint8_t)); + if (raw == NULL || work == NULL) { + free(sig); + free(raw); + free(work); + return; + } + + const uint8_t before = *hits; + + size_t n = 0; + double got_clk = 0.0, best_score = -1.0; + int got_phase = 0; + + for (size_t s = 0; s < ARRAYLEN(subcarriers); s++) { + + size_t got_n = MAX_DEMOD_BUF_LEN; + double this_clk = 0.0, score = 0.0; + int this_phase = 0; + + if (pm3_psk_demod(sig, count, subcarriers[s], (double)fitclk, work, &got_n, &this_clk, &this_phase, &score, NULL) != PM3_SUCCESS) { + continue; + } + + if (score > best_score) { + best_score = score; + n = got_n; + got_clk = this_clk; + got_phase = this_phase; + memcpy(raw, work, got_n); + } + } + + if (n >= 32) { + + for (int variant = 0; variant < 4 && *hits == before; variant++) { + + const bool inverted = ((variant & 1) != 0); + + for (size_t i = 0; i < n; i++) { + work[i] = (inverted) ? (raw[i] ^ 1) : raw[i]; + } + + if (variant >= 2) { + psk1TOpsk2(work, n); + } + + setDemodBuff(work, n, 0); + setClockGrid((uint32_t)(got_clk + 0.5), got_phase); + + static const uint8_t modes[4] = { DEMOD_PSK1, DEMOD_PSK1, DEMOD_PSK2, DEMOD_PSK3 }; + + int bitRate = 0; + if (test(modes[variant], &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5) == false) { + continue; + } + + tests[*hits].modulation = modes[variant]; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = inverted; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + } + } + + free(sig); + free(raw); + free(work); +} + +static void t55xx_ask_coherent(int fitclk, uint8_t clk, t55xx_conf_block_t *tests, uint8_t *hits, uint8_t downlink_mode) { + + if (g_GraphTraceLen < 2048 || fitclk < 4) { + return; + } + + size_t count = g_GraphTraceLen; + if (count > 16384) { + count = 16384; + } + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, count); + if (sig == NULL) { + return; + } + + uint8_t *raw = calloc(MAX_DEMOD_BUF_LEN, sizeof(uint8_t)); + uint8_t *work = calloc(MAX_DEMOD_BUF_LEN, sizeof(uint8_t)); + if (raw == NULL || work == NULL) { + free(sig); + free(raw); + free(work); + return; + } + + const uint8_t before = *hits; + + // a transition code carries two chips per bit + size_t n = MAX_DEMOD_BUF_LEN; + double got_chip = 0.0; + int got_phase = 0; + + if (pm3_ask_chips(sig, count, (double)fitclk / 2.0, raw, &n, &got_chip, &got_phase) == PM3_SUCCESS) { + + // manchester first, then biphase at both pair alignments, each way up + for (int variant = 0; variant < 6 && *hits == before; variant++) { + + const int invert = (variant & 1); + size_t size = n; + uint8_t mode; + + memcpy(work, raw, n); + + if (variant < 2) { + uint8_t align = 0; + if (manrawdecode(work, &size, (uint8_t)invert, &align) == 0xFFFF) { + continue; + } + mode = DEMOD_ASK; + } else { + int offset = (variant < 4) ? 0 : 1; + if (BiphaseRawDecode(work, &size, &offset, invert) < 0) { + continue; + } + mode = invert ? DEMOD_BIa : DEMOD_BI; + } + + if (size < 32) { + continue; + } + + setDemodBuff(work, size, 0); + setClockGrid((uint32_t)((got_chip * 2.0) + 0.5), got_phase); + + int bitRate = 0; + if (test(mode, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5) == false) { + continue; + } + + tests[*hits].modulation = mode; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = (invert != 0); + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + } + } + + free(sig); + free(raw); + free(work); +} + +static bool t55xx_fallback_try(pm3_mod_t mod, pm3_enc_t enc, int fc_hi, int fc_lo, int fitclk, + t55xx_conf_block_t *tests, uint8_t *hits, uint8_t downlink_mode, + bool coherent_ok) { + + const uint8_t before = *hits; + int bitRate = 0; + + const uint8_t clk = (uint8_t)((fitclk > 0 && fitclk < 256) ? fitclk : 0); + + if (mod == PM3_MOD_FSK) { + + static const uint8_t rates[] = { 32, 40, 50, 64, 100, 128 }; + + const uint8_t pairs[3][2] = { + { (uint8_t)fc_hi, (uint8_t)fc_lo }, { 8, 5 }, { 10, 8 } + }; + + for (int strict = 1; strict >= 0; strict--) { + for (size_t p = 0; p < ARRAYLEN(pairs); p++) { + + if (pairs[p][0] == 0 || pairs[p][1] == 0) { + continue; + } + + for (size_t r = 0; r < ARRAYLEN(rates); r++) { + for (int inv = 0; inv < 2; inv++) { + + if (FSKrawDemod(rates[r], (uint8_t)inv, pairs[p][0], pairs[p][1], false) != PM3_SUCCESS) { + continue; + } + if (test(DEMOD_FSK, &tests[*hits].offset, &bitRate, rates[r], &tests[*hits].Q5) == false) { + continue; + } + + if (strict && block0_repeats_at_stride(tests[*hits].offset) == false) { + continue; + } + + uint8_t m = DEMOD_FSK; + if (pairs[p][0] == 8 && pairs[p][1] == 5) { + m = inv ? DEMOD_FSK1 : DEMOD_FSK1a; + } else if (pairs[p][0] == 10 && pairs[p][1] == 8) { + m = inv ? DEMOD_FSK2a : DEMOD_FSK2; + } + + tests[*hits].modulation = m; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = (inv != 0); + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + return true; + } + } + } + } + return false; + } + + if (mod == PM3_MOD_PSK) { + + buffer_savestate_t saveState = save_bufferS32(g_GraphBuffer, g_GraphTraceLen); + saveState.offset = g_GridOffset; + CmdLtrim("-i 160"); + + for (int inv = 0; inv < 2; inv++) { + if (PSKDemod(fitclk, inv, PM3_T55_FALLBACK_MAXERR, false) != PM3_SUCCESS) { + continue; + } + if (test(DEMOD_PSK1, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5) == false) { + continue; + } + tests[*hits].modulation = DEMOD_PSK1; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = (inv != 0); + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + break; + } + + // PSK2 and PSK3 are PSK1 put through psk1TOpsk2() + if (*hits == before && PSKDemod(fitclk, 0, PM3_T55_FALLBACK_MAXERR, false) == PM3_SUCCESS) { + psk1TOpsk2(g_DemodBuffer, g_DemodBufferLen); + if (test(DEMOD_PSK2, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5)) { + tests[*hits].modulation = DEMOD_PSK2; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = false; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + } + } + + restore_bufferS32(saveState, g_GraphBuffer); + g_GridOffset = saveState.offset; + + if (*hits == before && coherent_ok) { + t55xx_psk_coherent(fitclk, clk, tests, hits, downlink_mode); + } + return (*hits != before); + } + + if (mod == PM3_MOD_NRZ || (mod == PM3_MOD_ASK && enc == PM3_ENC_RAW)) { + + if (fitclk <= 8) { + return false; + } + + for (int inv = 0; inv < 2; inv++) { + if (NRZrawDemod(fitclk, inv, PM3_T55_FALLBACK_MAXERR, false) != PM3_SUCCESS) { + continue; + } + if (test(DEMOD_NRZ, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5) == false) { + continue; + } + tests[*hits].modulation = DEMOD_NRZ; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = (inv != 0); + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + return true; + } + return false; + } + + tests[*hits].ST = true; + if ((ASKDemod_ext(fitclk, 0, PM3_T55_FALLBACK_MAXERR, 0, false, false, false, 1, &tests[*hits].ST) == PM3_SUCCESS) + && test(DEMOD_ASK, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5)) { + + tests[*hits].modulation = DEMOD_ASK; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = false; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + return true; + } + + tests[*hits].ST = true; + if ((ASKDemod_ext(fitclk, 1, PM3_T55_FALLBACK_MAXERR, 0, false, false, false, 1, &tests[*hits].ST) == PM3_SUCCESS) + && test(DEMOD_ASK, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5)) { + + tests[*hits].modulation = DEMOD_ASK; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = true; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + return true; + } + + if ((ASKbiphaseDemod(0, fitclk, 0, PM3_T55_FALLBACK_MAXERR, false) == PM3_SUCCESS) + && test(DEMOD_BI, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5)) { + + tests[*hits].modulation = DEMOD_BI; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = false; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + return true; + } + + if ((ASKbiphaseDemod(0, fitclk, 1, PM3_T55_FALLBACK_MAXERR, false) == PM3_SUCCESS) + && test(DEMOD_BIa, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5)) { + + tests[*hits].modulation = DEMOD_BIa; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = true; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + } + + if (*hits == before && coherent_ok) { + t55xx_ask_coherent(fitclk, clk, tests, hits, downlink_mode); + } + + return (*hits != before); +} + + +#define PM3_T55_FALLBACK_HYPS 4 + +static void t55xx_detect_fallback(t55xx_conf_block_t *tests, uint8_t *hits, uint8_t downlink_mode) { + + if (g_GraphTraceLen < 2048) { + return; + } + + size_t count = g_GraphTraceLen; + if (count > 16384) { + count = 16384; + } + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, count); + if (sig == NULL) { + return; + } + + pm3_spec_analysis_t an; + pm3_fit_opts_t opts = {0}; + + if (pm3_analyse(sig, count, pm3_next_pow2(count), PM3_WIN_HANN, &an) == PM3_SUCCESS + && an.confidence >= PM3_CONF_MEDIUM) { + + switch (an.family) { + case PM3_FAM_FSK: + opts.mod_mask = 1 << PM3_MOD_FSK; + break; + case PM3_FAM_PSK: + opts.mod_mask = 1 << PM3_MOD_PSK; + break; + case PM3_FAM_ASK: + case PM3_FAM_MANCHESTER: + opts.mod_mask = (1 << PM3_MOD_ASK) | (1 << PM3_MOD_NRZ); + break; + case PM3_FAM_NRZ: + opts.mod_mask = (1 << PM3_MOD_NRZ) | (1 << PM3_MOD_ASK); + break; + case PM3_FAM_UNKNOWN: + break; + } + } + + const int first_mask = opts.mod_mask; + + for (int round = 0; round < 3; round++) { + + if (round == 1) { + if (first_mask == 0) { + continue; + } + opts.mod_mask = 0; + } + if (round == 2) { + opts.mod_mask = 0; + } + + pm3_fit_t fit; + if (pm3_fit_run(sig, count, &opts, &fit) != PM3_SUCCESS) { + continue; + } + + int seen_mod[PM3_T55_FALLBACK_HYPS], seen_enc[PM3_T55_FALLBACK_HYPS], seen_clk[PM3_T55_FALLBACK_HYPS]; + size_t nseen = 0; + bool done = false; + + for (size_t i = 0; i < fit.count && nseen < PM3_T55_FALLBACK_HYPS; i++) { + + const int fitclk = (int)(fit.items[i].clk_fine + 0.5); + if (fitclk <= 0 || fitclk > 255) { + continue; + } + + bool dup = false; + for (size_t j = 0; j < nseen; j++) { + if ((seen_mod[j] == (int)fit.items[i].mod) && (seen_enc[j] == (int)fit.items[i].enc) && (seen_clk[j] == fitclk)) { + dup = true; + break; + } + } + if (dup) { + continue; + } + + seen_mod[nseen] = (int)fit.items[i].mod; + seen_enc[nseen] = (int)fit.items[i].enc; + seen_clk[nseen] = fitclk; + nseen++; + + if (t55xx_fallback_try(fit.items[i].mod + , fit.items[i].enc + , fit.items[i].fc_hi + , fit.items[i].fc_lo + , fitclk + , tests + , hits + , downlink_mode + , (round == 2))) { + done = true; + break; + } + } + + pm3_fit_free(&fit); + + if (done) { + break; + } + } + + free(sig); +} + bool t55xxTryDetectModulationEx(uint8_t downlink_mode, bool print_config, uint32_t wanted_conf, uint64_t pwd) { t55xx_conf_block_t tests[15]; @@ -1482,6 +1739,11 @@ bool t55xxTryDetectModulationEx(uint8_t downlink_mode, bool print_config, uint32 // t55xx_search_config_psk(g_GraphBuffer, 2); } } + + if (hits == 0) { + t55xx_detect_fallback(tests, &hits, downlink_mode); + } + if (hits == 1) { config.modulation = tests[0].modulation; config.bitrate = tests[0].bitrate; @@ -1589,6 +1851,71 @@ bool GetT55xxBlockData(uint32_t *blockdata) { return true; } +static bool t55xx_has_adjacent_ones(uint32_t v) { + const uint32_t rot = (v >> 1) | ((v & 1) << 31); + return ((v & rot) != 0); +} + +static bool t55xx_config_psk3_ambiguous(void) { + + if (config.modulation != DEMOD_PSK2 || config.Q5) { + return false; + } + + static const uint8_t basic[] = {8, 16, 32, 40, 50, 64, 100, 128}; + const uint8_t clk = basic[config.bitrate & 0x07]; + + uint32_t cand[T55XX_PSK3_MAX_CAND]; + return (t55xx_psk3_block0_candidates(config.block0, clk, cand, ARRAYLEN(cand)) > 0); +} + +#define T55XX_PSK3_PROBE_BLOCKS 7 +#define T55XX_PSK3_PROBE_TRIES 3 +#define T55XX_PSK3_PROBE_MIN 3 + +static bool t55xx_psk3_probe(bool usepwd, uint32_t password, uint8_t downlink_mode) { + + size_t usable = 0, clean = 0; + + for (uint8_t b = 1; b <= T55XX_PSK3_PROBE_BLOCKS; b++) { + + bool got = false, ok = false; + + for (uint8_t t = 0; t < T55XX_PSK3_PROBE_TRIES; t++) { + + if (AcquireData(T55x7_PAGE0, b, usepwd, password, downlink_mode) == false) { + continue; + } + if (DecodeT55xxBlock() == false) { + continue; + } + + uint32_t val = 0; + if (GetT55xxBlockData(&val) == false) { + continue; + } + + got = true; + + if (t55xx_has_adjacent_ones(val) == false) { + ok = true; + break; + } + } + + if (got == false) { + continue; + } + + usable++; + if (ok) { + clean++; + } + } + + return (usable >= T55XX_PSK3_PROBE_MIN && clean == usable); +} + void printT55xxBlock(uint8_t blockNum, bool page1) { uint32_t val = 0; @@ -1600,7 +1927,9 @@ void printT55xxBlock(uint8_t blockNum, bool page1) { T55x7_SaveBlockData((page1) ? blockNum + 8 : blockNum, val); - PrintAndLogEx(SUCCESS, " %02d | %08X | %s | %s", blockNum, val, sprint_bytebits_bin(g_DemodBuffer + config.offset, 32), sprint_ascii(bytes, 4)); + const char *note = t55xx_config_psk3_ambiguous() ? _YELLOW_(" <- psk2/psk3 ambiguous") : ""; + + PrintAndLogEx(SUCCESS, " %02d | %08X | %s | %s%s", blockNum, val, sprint_bytebits_bin(g_DemodBuffer + config.offset, 32), sprint_ascii(bytes, 4), note); } static bool testModulation(uint8_t mode, uint8_t modread) { @@ -1714,7 +2043,7 @@ static bool testQ5(uint8_t mode, uint8_t *offset, int *fndBitRate, uint8_t clk) *fndBitRate = convertQ5bitRate(bitRate); if (*fndBitRate < 0) continue; - *offset = idx; + *offset = (uint8_t)idx; return true; } @@ -1729,31 +2058,69 @@ static bool testBitRate(uint8_t readRate, uint8_t clk) { return false; } -bool test(uint8_t mode, uint8_t *offset, int *fndBitRate, uint8_t clk, bool *Q5) { +typedef struct { + uint16_t last; // highest offset a whole window fits at + uint32_t val[256]; + uint16_t count[256]; // offsets in the buffer holding this same value + bool stride[256]; // and the same value again one block further on +} t55_windows_t; - if (g_DemodBufferLen < 64) { - return false; +static void windows_build(t55_windows_t *w) { + + memset(w, 0, sizeof(*w)); + + // offset is stored in a uint8_t, hence the 255 bound + w->last = (g_DemodBufferLen - 32 > 255) ? 255 : (uint16_t)(g_DemodBufferLen - 32); + + for (uint16_t i = 0; i <= w->last; i++) { + w->val[i] = PackBits((uint8_t)i, 32, g_DemodBuffer); } - for (uint8_t idx = 28; idx < 64; idx++) { + for (uint16_t i = 0; i <= w->last; i++) { - uint8_t si = idx; + for (uint16_t j = 0; j <= w->last; j++) { + if (w->val[j] == w->val[i]) { + w->count[i]++; + } + } + + if (i + 32 <= w->last) { + w->stride[i] = (w->val[i] == w->val[i + 32]); + } + } +} + +static bool test_scan(uint8_t mode, uint8_t *offset, int *fndBitRate, uint8_t clk, + uint16_t start, uint16_t end, const t55_windows_t *w, + uint16_t need, bool need_stride) { + + for (uint16_t idx = start; idx <= end; idx++) { + + uint8_t si = (uint8_t)idx; if (PackBits(si, 28, g_DemodBuffer) == 0x00) { continue; } + if (w->count[idx] < need || (need_stride && w->stride[idx] == false)) { + continue; + } + uint8_t safer = PackBits(si, 4, g_DemodBuffer); si += 4; //master key uint8_t resv = PackBits(si, 4, g_DemodBuffer); - si += 4; //was 7 & +=7+3 //should be only 4 bits if extended mode + si += 4; //was 7 & +=7+3 // should be only 4 bits if extended mode // 2nibble must be zeroed. - // moved test to here, since this gets most faults first. if (resv > 0x00) { continue; } + // The master key is 0, or 6 or 9 to select extended mode + if (safer != 0x0 && safer != 0x6 && safer != 0x9) { + continue; + } + int bitRate = PackBits(si, 6, g_DemodBuffer); si += 6; //bit rate (includes extended mode part of rate) uint8_t extend = PackBits(si, 1, g_DemodBuffer); @@ -1789,11 +2156,67 @@ bool test(uint8_t mode, uint8_t *offset, int *fndBitRate, uint8_t clk, bool *Q5) } *fndBitRate = bitRate; - *offset = idx; - *Q5 = false; + *offset = (uint8_t)idx; return true; } + return false; +} + +bool test(uint8_t mode, uint8_t *offset, int *fndBitRate, uint8_t clk, bool *Q5) { + + if (g_debugMode) { + PrintAndLogEx(DEBUG, "DEBUG (test) mode %u clk %u dclk %d len %zu : %s", mode, clk, g_DemodClock, g_DemodBufferLen, + sprint_bytebits_bin(g_DemodBuffer, (g_DemodBufferLen > 512) ? 512 : g_DemodBufferLen)); + } + + // One block is all it takes to carry a configuration. The old floor of 64 + // threw away every short demodulation unread, and a manchester rf/128 + // block read demodulates to 49 bits - the whole capture is only 93 bit + // periods long. + if (g_DemodBufferLen < 32) { + return false; + } + + // Scan as far as the buffer allows rather than stopping at bit 64. + // + // The tag repeats its configuration every 32 bits, so a window of 36 + // offsets covers barely one period - and if the first clean copy happens + // to start later than that, because the demodulator dropped a bit early on + // or the buffer opens mid block, the config is simply never looked at. + // A psk1 rf/32 capture had a perfectly good copy sitting past bit 64 while + // detection failed. offset is a uint8_t, hence the 255 bound. + const uint16_t limit = (g_DemodBufferLen - 32 > 255) ? 255 : (uint16_t)(g_DemodBufferLen - 32); + + // Where to start. + // + // Starting at 28 skips the ragged first copy, which is the right thing to + // do as long as what is left still spans a whole 32 bit period - that + // needs offsets up to 59, so 91 bits of buffer. Below that, starting at + // 28 means some phases are never looked at at all, and on the manchester + // rf/128 read the one that is never looked at is offset 0, where the + // configuration actually sits. + const uint16_t start = (g_DemodBufferLen >= 92) ? 28 : 0; + + t55_windows_t w; + windows_build(&w); + + // A word that had every chance to show itself twice and did not is a + // coincidence, and answering with it is worse than not answering. Only a + // buffer too short to have held a second copy gets to fall back on a + // single sighting. + const uint16_t need = (g_DemodBufferLen >= 64) ? 2 : 1; + + // Strongest corroboration first: the block stride, then a bare repeat. + const bool stride_pass[2] = { true, false }; + + for (uint8_t r = 0; r < 2; r++) { + if (test_scan(mode, offset, fndBitRate, clk, start, limit, &w, need, stride_pass[r])) { + *Q5 = false; + return true; + } + } + if (testQ5(mode, offset, fndBitRate, clk)) { *Q5 = true; return true; @@ -1833,6 +2256,121 @@ int CmdT55xxSpecial(const char *Cmd) { return PM3_SUCCESS; } +// Is `b` a word a T55x7 block 0 could actually hold, with modulation psk3 and +// the bit rate the demodulation settled on? +// +// Only rules that are certainly true are applied - master key, the fixed zero +// bits, the bit rate, modulation field 3 and a psk carrier that exists. A +// candidate list one entry too long is harmless; one that has dropped the real +// word is not, so nothing merely probable is tested here. +static bool t55xx_psk3_block0_plausible(uint32_t b, uint8_t clk) { + + const uint8_t master = (uint8_t)((b >> 28) & 0x0F); + const bool xmode = (((b >> 17) & 1) != 0) && (master == 0x6 || master == 0x9); + + if (xmode) { + if (b & 0x0F000000) { + return false; + } + } else { + if (master != 0x0) { + return false; + } + if (b & 0x0FE00106) { + return false; + } + } + + // modulation field 3 is psk3, which is the whole point of the enumeration + if (((b >> 12) & 0x1F) != 0x03) { + return false; + } + + // psk carrier 11 is reserved, so a word claiming it is not a real config + if (((b >> 10) & 0x03) == 0x03) { + return false; + } + + if (xmode) { + return (EM4x05_GET_BITRATE((b >> 18) & 0x3F) == clk); + } + + static const uint8_t basic[] = {8, 16, 32, 40, 50, 64, 100, 128}; + return (basic[(b >> 18) & 0x07] == clk); +} + +static size_t t55xx_psk3_block0_candidates(uint32_t observed, uint8_t clk, uint32_t *out, size_t max) { + + if (out == NULL || max == 0 || observed == 0) { + return 0; + } + + uint8_t edge[32], nedge = 0; + for (uint8_t i = 0; i < 32; i++) { + if ((observed >> (31 - i)) & 1) { + edge[nedge++] = i; + } + } + + uint8_t gap[32]; + for (uint8_t k = 0; k < nedge; k++) { + const uint8_t nxt = edge[(k + 1) % nedge]; + gap[k] = (nedge == 1) ? 32 : (uint8_t)((32 + nxt - edge[k]) % 32); + if (gap[k] < 2) { + return 0; + } + } + + uint8_t len[32]; + for (uint8_t k = 0; k < nedge; k++) { + len[k] = 1; + } + + size_t found = 0; + + for (;;) { + + uint32_t d = 0; + for (uint8_t k = 0; k < nedge; k++) { + for (uint8_t t = 0; t < len[k]; t++) { + d |= 1u << (31 - ((edge[k] + t) % 32)); + } + } + + if (t55xx_psk3_block0_plausible(d, clk)) { + + bool dup = false; + for (size_t i = 0; i < found; i++) { + if (out[i] == d) { + dup = true; + break; + } + } + if (dup == false) { + out[found++] = d; + if (found == max) { + return found; + } + } + } + + uint8_t k = 0; + while (k < nedge) { + len[k]++; + if (len[k] < gap[k]) { + break; + } + len[k] = 1; + k++; + } + if (k == nedge) { + break; + } + } + + return found; +} + int printConfiguration(t55xx_conf_block_t b) { PrintAndLogEx(INFO, " Chip type......... " _GREEN_("%s"), (b.Q5) ? "Q5/T5555" : "T55x7"); PrintAndLogEx(INFO, " Modulation........ " _GREEN_("%s"), GetSelectedModulationStr(b.modulation)); @@ -1841,6 +2379,33 @@ int printConfiguration(t55xx_conf_block_t b) { PrintAndLogEx(INFO, " Offset............ %d", b.offset); PrintAndLogEx(INFO, " Seq. terminator... %s", (b.ST) ? _GREEN_("Yes") : "No"); PrintAndLogEx(INFO, " Block0............ %08X %s", b.block0, GetConfigBlock0Source(b.block0Status)); + + if (b.modulation == DEMOD_PSK2 && b.Q5 == false) { + + static const uint8_t basic[] = {8, 16, 32, 40, 50, 64, 100, 128}; + const uint8_t clk = basic[b.bitrate & 0x07]; + + uint32_t cand[T55XX_PSK3_MAX_CAND]; + size_t n = t55xx_psk3_block0_candidates(b.block0, clk, cand, ARRAYLEN(cand)); + + for (size_t i = 1; i < n; i++) { + uint32_t v = cand[i]; + size_t j = i; + while (j > 0 && cand[j - 1] > v) { + cand[j] = cand[j - 1]; + j--; + } + cand[j] = v; + } + + if (n > 0) { + PrintAndLogEx(INFO, " psk3 ambiguity.... this read is also consistent with a " _YELLOW_("psk3") " tag holding"); + for (size_t i = 0; i < n; i++) { + PrintAndLogEx(INFO, " %08X", cand[i]); + } + } + } + PrintAndLogEx(INFO, " Downlink mode..... %s", GetDownlinkModeStr(b.downlink_mode)); PrintAndLogEx(INFO, " Password set...... %s", (b.usepwd) ? _RED_("Yes") : _GREEN_("No")); if (b.usepwd) { @@ -4771,12 +5336,12 @@ static int CmdT55xxView(const char *Cmd) { } static command_t CommandTable[] = { - {"-----------", CmdHelp, AlwaysAvailable, "---------------------------- " _CYAN_("notice") " -----------------------------"}, - {"", CmdHelp, AlwaysAvailable, "Remember to run `" _YELLOW_("lf t55xx detect") "` first whenever a new card"}, - {"", CmdHelp, AlwaysAvailable, "is placed on the Proxmark3 or the config block changed."}, + {"-----------", CmdHelp, AlwaysAvailable, "------------------------------- " _CYAN_("notice") " ---------------------------------"}, + {"", CmdHelp, AlwaysAvailable, "Always run `" _YELLOW_("lf t55xx detect") "` first whenever a new card is placed"}, + {"", CmdHelp, AlwaysAvailable, "on the Proxmark3 or the config block changed."}, {"", CmdHelp, AlwaysAvailable, ""}, {"help", CmdHelp, AlwaysAvailable, "This help"}, - {"-----------", CmdHelp, AlwaysAvailable, "--------------------- " _CYAN_("operations") " ---------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "----------------------------- " _CYAN_("operations") " -------------------------------"}, {"clonehelp", CmdT55xxCloneHelp, IfPm3Lf, "Shows the available clone commands"}, {"config", CmdT55xxSetConfig, AlwaysAvailable, "Set/Get T55XX configuration (modulation, inverted, offset, rate)"}, {"dangerraw", CmdT55xxDangerousRaw, IfPm3Lf, "Sends raw bitstream. Dangerous, do not use!!"}, @@ -4786,13 +5351,13 @@ static command_t CommandTable[] = { {"info", CmdT55xxInfo, AlwaysAvailable, "Show T55x7 configuration data (page 0/ blk 0)"}, {"p1detect", CmdT55xxDetectPage1, IfPm3Lf, "Try detecting if this is a t55xx tag by reading page 1"}, {"read", CmdT55xxReadBlock, IfPm3Lf, "Read T55xx block data"}, - {"resetread", CmdResetRead, IfPm3Lf, "Send Reset Cmd then lf read the stream to attempt to identify the start of it"}, + {"resetread", CmdResetRead, IfPm3Lf, "Send Reset Cmd then lf read the stream to attempt to identify the start"}, {"restore", CmdT55xxRestore, IfPm3Lf, "Restore T55xx card Page 0 / Page 1 blocks"}, {"trace", CmdT55xxReadTrace, AlwaysAvailable, "Show T55x7 traceability data (page 1/ blk 0-1)"}, {"wakeup", CmdT55xxWakeUp, IfPm3Lf, "Send AOR wakeup command"}, {"view", CmdT55xxView, AlwaysAvailable, "Display content from tag dump file"}, {"write", CmdT55xxWriteBlock, IfPm3Lf, "Write T55xx block data"}, - {"-----------", CmdHelp, AlwaysAvailable, "--------------------- " _CYAN_("recovery") " ---------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "------------------------------ " _CYAN_("recovery") " --------------------------------"}, {"bruteforce", CmdT55xxBruteForce, IfPm3Lf, "Simple bruteforce attack to find password"}, {"chk", CmdT55xxChkPwds, IfPm3Lf, "Check passwords"}, {"protect", CmdT55xxProtect, IfPm3Lf, "Password protect tag"}, diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 5237f7b57..d9063a2b1 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -95,6 +95,10 @@ const static vocabulary_t vocabulary[] = { { 1, "data manrawdecode" }, { 1, "data modulation" }, { 1, "data rawdemod" }, + { 1, "data autodemod" }, + { 1, "data fft" }, + { 1, "data fitscore" }, + { 1, "data spectrum" }, { 1, "data askedgedetect" }, { 1, "data autocorr" }, { 1, "data convertbitstream" }, @@ -124,6 +128,7 @@ const static vocabulary_t vocabulary[] = { { 0, "data hexsamples" }, { 0, "data samples" }, { 1, "data qrcode" }, + { 0, "data gensignal" }, { 0, "data test_ss8" }, { 0, "data test_ss32" }, { 0, "data test_ss32s" }, @@ -217,7 +222,6 @@ const static vocabulary_t vocabulary[] = { { 0, "hf 15 writeafi" }, { 0, "hf 15 writedsfid" }, { 0, "hf 15 csetuid" }, - { 0, "hf 15 cfinalize" }, { 1, "hf aliro help" }, { 1, "hf aliro list" }, { 0, "hf aliro info" }, @@ -923,6 +927,10 @@ const static vocabulary_t vocabulary[] = { { 1, "lf t55xx sniff" }, { 0, "lf t55xx special" }, { 0, "lf t55xx wipe" }, + { 1, "lf trovan help" }, + { 1, "lf trovan demod" }, + { 0, "lf trovan reader" }, + { 0, "lf trovan clone" }, { 1, "lf viking help" }, { 1, "lf viking demod" }, { 0, "lf viking reader" }, diff --git a/doc/commands.json b/doc/commands.json index 73cdca9ff..2a403e7ad 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -239,6 +239,28 @@ ], "usage": "data autocorr [-hg] [-w ]" }, + "data autodemod": { + "command": "data autodemod", + "description": "Tries to work out the modulation, encoding and clock of the wave in the GraphBuffer then run the matching demodulator Adds no signal processing of its own, the analysis is `data spectrum` and `data fitscore` A fractional clock is resampled onto an integer grid first", + "notes": [ + "data autodemod -> analyse and demodulate", + "data autodemod --dry-run -> decide and print, demodulate nothing", + "data autodemod --thres 6 -> insist on a 6 dB margin before trusting rank 1", + "data autodemod --invert --amp -> pass invert and amplify through to the demod" + ], + "offline": true, + "options": [ + "-h, --help This help", + "--start first sample of the GraphBuffer to use (def 0)", + "--size samples to analyse (def 16384)", + "--thres margin below which rank 1 is called ambiguous (def 3.0)", + "--dry-run analyse and decide, but do not demodulate", + "-a, --amp amplify the signal before ASK demodulation", + "-i, --invert invert the demodulated output", + "-v, --verbose show the demodulator's own output" + ], + "usage": "data autodemod [-haiv] [--start ] [--size ] [--thres ] [--dry-run]" + }, "data biphaserawdecode": { "command": "data biphaserawdecode", "description": "Biphase decode binary stream in DemodBuffer Converts 10 or 01 -> 1 and 11 or 00 -> 0 - must have binary sequence in DemodBuffer (run `data rawdemod --ar` before) - invert for Conditional Dephase Encoding (CDP) AKA Differential Manchester", @@ -407,6 +429,50 @@ ], "usage": "data envelope [-h]" }, + "data fft": { + "command": "data fft", + "description": "Fourier transform of the samples in the GraphBuffer. Reports the magnitude spectrum of the first N/2+1 bins, the input being real. The DC component is removed and the window is normalised to unit variance before the transform, so magnitudes are comparable between captures.", + "notes": [ + "data fft -> transform the whole buffer", + "data fft --size 4096 --win blackman -> 4096 point transform, blackman window", + "data fft --start 1000 --size 8192 --db -> magnitudes in dB relative to the peak", + "data fft --size 4096 --graph -> put the spectrum in the graph window" + ], + "offline": true, + "options": [ + "-h, --help This help", + "--start first sample of the GraphBuffer to use (def 0)", + "--size transform length, rounded up to a power of two (def largest that fits)", + "--win window function (def hann)", + "--db report magnitude in dB relative to the peak bin instead of linear", + "--graph write the magnitude spectrum into the GraphBuffer and repaint", + "--fs sample rate in Hz, adds a frequency column in Hz", + "--bins how many bins to print (def 32, 0 for all)" + ], + "usage": "data fft [-h] [--start ] [--size ] [--win ] [--db] [--graph] [--fs ] [--bins ]" + }, + "data fitscore": { + "command": "data fitscore", + "description": "Rank matched filter hypotheses against the GraphBuffer. A hypothesis is a modulation, a symbol shape and a clock. Each one is scored by correlating the trace against a single symbol template and measuring how tightly the correlator output clusters at the decision instants. Phase is not searched: the correlation is done with an FFT, which yields every phase offset at once. Templates are shaped by an assumed antenna Q, because correlating a bandlimited signal against square edges biases the ranking toward short clocks.", + "notes": [ + "data fitscore -> rank the whole bank", + "data fitscore --mod ask --top 5 -> ASK hypotheses only", + "data fitscore --clk 64 --all -> everything at clock 64", + "data fitscore --verbose -> also put the rank 1 correlator output in the graph" + ], + "offline": true, + "options": [ + "-h, --help This help", + "--start first sample of the GraphBuffer to use (def 0)", + "--size samples to analyse, rounded up to a power of two (def 16384)", + "--mod restrict to one modulation (def all)", + "--clk restrict to one clock (def all candidates)", + "--top rows to print (def 10)", + "--all print every scored hypothesis", + "-v, --verbose write the rank 1 correlator output into the GraphBuffer" + ], + "usage": "data fitscore [-hv] [--start ] [--size ] [--mod ] [--clk ] [--top ] [--all]" + }, "data fsktonrz": { "command": "data fsktonrz", "description": "Convert fsk2 to nrz wave for alternate fsk demodulating (for weak fsk) Omitted values are autodetect instead", @@ -423,6 +489,34 @@ ], "usage": "data fsktonrz [-h] [-c ] [--low ] [--hi ]" }, + "data gensignal": { + "command": "data gensignal", + "description": "Developer tool. Fill the GraphBuffer with a synthetic LF waveform. Used with `data fitscore` and `data autodemod`", + "notes": [ + "data gensignal --mod ask --enc manchester --clk 64", + "data gensignal --mod ask --enc manchester --clk 64.25 --noise 0.4 --len 20000" + ], + "offline": false, + "options": [ + "-h, --help This help", + "--mod modulation (def ask)", + "--enc encoding (def raw)", + "--clk clock in samples/symbol, may be fractional (def 64)", + "--len samples to generate (def 16384)", + "--noise gaussian noise standard deviation (def 0)", + "--drift linear DC ramp across the capture (def 0)", + "--env amplitude envelope swing (def 0)", + "--jitter per symbol edge jitter, samples (def 0)", + "--clip hard clip level, 0 for none (def 0)", + "--fc PSK subcarrier period (def 4)", + "--fchigh FSK long field clock (def 10)", + "--fclow FSK short field clock (def 8)", + "--q antenna Q to shape the waveform with (def 8)", + "--repeat loop a message of this many bits, like a real tag (def 0, random)", + "--seed RNG seed, for reproducible runs (def 1)" + ], + "usage": "data gensignal [-h] [--mod ] [--enc ] [--clk ] [--len ] [--noise ] [--drift ] [--env ] [--jitter ] [--clip ] [--fc ] [--fchigh ] [--fclow ] [--q ] [--repeat ] [--seed ]" + }, "data getbitstream": { "command": "data getbitstream", "description": "Convert GraphBuffer's value accordingly - larger or equal to ONE becomes ONE - less than ONE becomes ZERO", @@ -452,7 +546,7 @@ }, "data help": { "command": "data help", - "description": "help This help ----------- ------------------------- General------------------------- clear Clears various buffers used by the graph window hide Hide the graph window load Load contents of file into graph window num Converts dec/hex/bin plot Show the graph window print Print the data in the DemodBuffer save Save signal trace data setdebugmode Set Debugging Level on client side xor Xor a input string ----------- ------------------------- Modulation------------------------- biphaserawdecode Biphase decode bin stream in DemodBuffer detectclock Detect ASK, FSK, NRZ, PSK clock rate of wave in GraphBuffer fsktonrz Convert fsk2 to nrz wave for alternate fsk demodulating (for weak fsk) manrawdecode Manchester decode binary stream in DemodBuffer modulation Identify LF signal for clock and modulation rawdemod Demodulate the data in the GraphBuffer and output binary ----------- ------------------------- Graph------------------------- askedgedetect Adjust Graph for manual ASK demod autocorr Autocorrelation over window convertbitstream Convert GraphBuffer's 0/1 values to 127 / -127 cthreshold Average out all values between dirthreshold Max rising higher up-thres/ Min falling lower down-thres decimate Decimate samples envelope Generate square envelope of samples grid overlay grid on graph window getbitstream Convert GraphBuffer's >=1 values to 1 and <1 to 0 hpf Remove DC offset from trace iir Apply IIR buttersworth filter on plot data ltrim Trim samples from left of trace mtrim Trim out samples from the specified start to the specified stop norm Normalize max/min to +/-128 rtrim Trim samples from right of trace setgraphmarkers Set the markers in the graph window shiftgraphzero Shift 0 for Graphed wave + or - shift value timescale Set cursor display timescale undecimate Un-decimate samples zerocrossings Count time between zero-crossings ----------- ------------------------- Operations------------------------- asn1 ASN1 decoder atr ATR lookup bmap Convert hex value according a binary template crypto Encrypt and decrypt data diff Diff of input files qrcode Create a QR code --------------------------------------------------------------------------------------- data clear available offline: yes This function clears the BigBuf on device side and graph window ( graphbuffer )", + "description": "help This help ----------- ------------------------------ General ------------------------------- clear Clears various buffers used by the graph window hide Hide the graph window load Load contents of file into graph window num Converts dec/hex/bin plot Show the graph window print Print the data in the DemodBuffer save Save signal trace data setdebugmode Set Debugging Level on client side xor Xor a input string ----------- ----------------------------- Modulation ----------------------------- biphaserawdecode Biphase decode bin stream in DemodBuffer detectclock Detect ASK, FSK, NRZ, PSK clock rate of wave in GraphBuffer fsktonrz Convert fsk2 to nrz wave for alternate fsk demodulating (for weak fsk) manrawdecode Manchester decode binary stream in DemodBuffer modulation Identify LF signal for clock and modulation rawdemod Demodulate the data in the GraphBuffer and output binary ----------- -------------------------- Frequency domain -------------------------- autodemod Detect modulation, encoding and clock, then demodulate fft Fourier transform of the GraphBuffer fitscore Rank matched filter hypotheses for modulation, encoding and clock spectrum Spectral peaks, symbol rate and modulation family hint ----------- ------------------------------- Graph -------------------------------- askedgedetect Adjust Graph for manual ASK demod autocorr Autocorrelation over window convertbitstream Convert GraphBuffer's 0/1 values to 127 / -127 cthreshold Average out all values between dirthreshold Max rising higher up-thres/ Min falling lower down-thres decimate Decimate samples envelope Generate square envelope of samples grid overlay grid on graph window getbitstream Convert GraphBuffer's >=1 values to 1 and <1 to 0 hpf Remove DC offset from trace iir Apply IIR buttersworth filter on plot data ltrim Trim samples from left of trace mtrim Trim out samples from the specified start to the specified stop norm Normalize max/min to +/-128 rtrim Trim samples from right of trace setgraphmarkers Set the markers in the graph window shiftgraphzero Shift 0 for Graphed wave + or - shift value timescale Set cursor display timescale undecimate Un-decimate samples zerocrossings Count time between zero-crossings ----------- ---------------------------- Operations ------------------------------ asn1 ASN1 decoder atr ATR lookup bmap Convert hex value according a binary template crypto Encrypt and decrypt data diff Diff of input files qrcode Create a QR code --------------------------------------------------------------------------------------- data clear available offline: yes This function clears the BigBuf on device side and graph window ( graphbuffer )", "notes": [ "data clear" ], @@ -774,6 +868,29 @@ ], "usage": "data shiftgraphzero [-h] -n " }, + "data spectrum": { + "command": "data spectrum", + "description": "Spectral analysis of the GraphBuffer. Reports the strongest spectral peaks with a sub bin estimate of the symbol clock, the squaring and delay-and-multiply spectra which expose rates the plain spectrum nulls out, and a modulation family hint derived from spectral shape alone. Run this on a trace that will not decode.", + "notes": [ + "data spectrum -> peak table and family hint", + "data spectrum --top 8 --sq -> more peaks, plus the squaring spectrum", + "data spectrum --stft --size 2048 -> spectrogram over the whole capture", + "data spectrum --stft --size 2048 --hop 256 -> finer time resolution" + ], + "offline": true, + "options": [ + "-h, --help This help", + "--start first sample of the GraphBuffer to use (def 0)", + "--size transform length, rounded up to a power of two (def largest that fits)", + "--win window function (def hann)", + "--top number of peaks to report (def 5)", + "--sq also show the squaring and delay-and-multiply spectra", + "--stft sliding window spectrogram, ridge tracking and drift", + "--hop STFT hop in samples (def size/4)", + "--fs sample rate in Hz, adds a frequency column in Hz" + ], + "usage": "data spectrum [-h] [--start ] [--size ] [--win ] [--top ] [--sq] [--stft] [--hop ] [--fs ]" + }, "data test_ss32": { "command": "data test_ss32", "description": "Tests the implementation of Buffer Save States (32-bit buffer)", @@ -1774,35 +1891,20 @@ ], "usage": "hf 14b wrbl [-h] [-b ] -d [--512] [--4k] [--sb] [--force]" }, - "hf 15 cfinalize": { - "command": "hf 15 cfinalize", - "description": "Finalize a magic ISO15693 'V3' tag. This operation is irreversible. After finalize the configuration area is erased and the UID can no longer be changed. Set the UID with `hf 15 csetuid --v3` first, then lock it in with this command.", - "notes": [ - "hf 15 cfinalize -y" - ], - "offline": false, - "options": [ - "-h, --help This help", - "-y, --yes Confirm the irreversible finalize operation" - ], - "usage": "hf 15 cfinalize [-hy]" - }, "hf 15 csetuid": { "command": "hf 15 csetuid", - "description": "Set UID for magic Chinese card (only works with such cards) For magic 'V3' tags this writes the UID configuration only and is repeatable; run `hf 15 cfinalize` afterwards to lock the UID permanently.", + "description": "Set UID for magic Chinese card (only works with such cards)", "notes": [ "hf 15 csetuid -u E011223344556677 -> use gen1 command", - "hf 15 csetuid -u E011223344556677 --v2 -> use gen2 command", - "hf 15 csetuid -u E011223344556677 --v3 -> use gen3 (V3) magic tag" + "hf 15 csetuid -u E011223344556677 --v2 -> use gen2 command" ], "offline": false, "options": [ "-h, --help This help", "-u, --uid UID, 8 hex bytes", - "-2, --v2 Use gen2 magic command", - "-3, --v3 Use gen3 (V3) magic tag (repeatable, needs cfinalize)" + "-2, --v2 Use gen2 magic command" ], - "usage": "hf 15 csetuid [-h23] -u " + "usage": "hf 15 csetuid [-h2] -u " }, "hf 15 demod": { "command": "hf 15 demod", @@ -11997,7 +12099,7 @@ }, "lf help": { "command": "lf help", - "description": "help This help ----------- -------------- Low Frequency -------------- awid { AWID RFIDs... } cotag { COTAG CHIPs... } destron { FDX-A Destron RFIDs... } em { EM CHIPs & RFIDs... } fdxb { FDX-B RFIDs... } gallagher { GALLAGHER RFIDs... } gproxii { Guardall Prox II RFIDs... } hid { HID Prox RFIDs... } hitag { Hitag CHIPs... } idteck { Idteck RFIDs... } indala { Indala RFIDs... } io { ioProx RFIDs... } jablotron { Jablotron RFIDs... } keri { KERI RFIDs... } motorola { Motorola Flexpass RFIDs... } nedap { Nedap RFIDs... } nexwatch { NexWatch RFIDs... } noralsy { Noralsy RFIDs... } pac { PAC/Stanley RFIDs... } paradox { Paradox RFIDs... } pcf7931 { PCF7931 CHIPs... } presco { Presco RFIDs... } pyramid { Farpointe/Pyramid RFIDs... } securakey { Securakey RFIDs... } ti { TI CHIPs... } t55xx { T55xx CHIPs... } viking { Viking RFIDs... } visa2000 { Visa2000 RFIDs... } ----------- --------------------- General --------------------- search Read and Search for valid known tag --------------------------------------------------------------------------------------- lf config available offline: no Get/Set config for LF sampling, bit/sample, decimation, frequency These changes are temporary, will be reset after a power cycle. - use `lf read` performs a read (active field) - use `lf sniff` performs a sniff (no active field)", + "description": "help This help ----------- -------------- Low Frequency -------------- awid { AWID RFIDs... } cotag { COTAG CHIPs... } destron { FDX-A Destron RFIDs... } em { EM CHIPs & RFIDs... } fdxb { FDX-B RFIDs... } gallagher { GALLAGHER RFIDs... } gproxii { Guardall Prox II RFIDs... } hid { HID Prox RFIDs... } hitag { Hitag CHIPs... } idteck { Idteck RFIDs... } indala { Indala RFIDs... } io { ioProx RFIDs... } jablotron { Jablotron RFIDs... } keri { KERI RFIDs... } motorola { Motorola Flexpass RFIDs... } nedap { Nedap RFIDs... } nexwatch { NexWatch RFIDs... } noralsy { Noralsy RFIDs... } pac { PAC/Stanley RFIDs... } paradox { Paradox RFIDs... } pcf7931 { PCF7931 CHIPs... } presco { Presco RFIDs... } pyramid { Farpointe/Pyramid RFIDs... } securakey { Securakey RFIDs... } ti { TI CHIPs... } t55xx { T55xx CHIPs... } trovan { Trovan animal IDs... } viking { Viking RFIDs... } visa2000 { Visa2000 RFIDs... } ----------- --------------------- General --------------------- search Read and Search for valid known tag --------------------------------------------------------------------------------------- lf config available offline: no Get/Set config for LF sampling, bit/sample, decimation, frequency These changes are temporary, will be reset after a power cycle. - use `lf read` performs a read (active field) - use `lf sniff` performs a sniff (no active field)", "notes": [ "lf config -> shows current config", "lf config -b 8 --125 -> samples at 125 kHz, 8 bps", @@ -14222,6 +14324,50 @@ ], "usage": "lf ti write [-h] -r [--crc ]" }, + "lf trovan clone": { + "command": "lf trovan clone", + "description": "clone a Trovan animal ID to a T55x7, Q5/T5555 or EM4305/4469 tag The ID is the 10 hex digits printed on the tag, with or without dashes.", + "notes": [ + "lf trovan clone --id 0007F9043A", + "lf trovan clone --id 00-07F9-043A", + "lf trovan clone --id 0007F9043A --q5" + ], + "offline": false, + "options": [ + "-h, --help This help", + "--id Trovan animal ID, 10 hex digits", + "--q5 specify writing to Q5/T5555 tag", + "--em specify writing to EM4305/4469 tag" + ], + "usage": "lf trovan clone [-h] --id [--q5] [--em]" + }, + "lf trovan help": { + "command": "lf trovan help", + "description": "help This help demod demodulate a Trovan tag from the GraphBuffer --------------------------------------------------------------------------------------- lf trovan demod available offline: yes Demodulate a Trovan animal ID tag from the GraphBuffer", + "notes": [ + "lf trovan demod" + ], + "offline": true, + "options": [ + "-h, --help This help", + "-v, --verbose verbose output" + ], + "usage": "lf trovan demod [-hv]" + }, + "lf trovan reader": { + "command": "lf trovan reader", + "description": "read a Trovan animal ID tag", + "notes": [ + "lf trovan reader -@ -> continuous reader mode" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-@ optional - continuous reader mode", + "-v, --verbose verbose output" + ], + "usage": "lf trovan reader [-h@v]" + }, "lf tune": { "command": "lf tune", "description": "Continuously measure LF antenna tuning. Press button or to interrupt.", @@ -15873,8 +16019,8 @@ } }, "metadata": { - "commands_extracted": 889, + "commands_extracted": 896, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-07-26T21:37:31+00:00" + "extracted_on": "2026-07-28T11:00:12+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index c63ddb6c7..d56072456 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -114,6 +114,10 @@ Check column "offline" for their availability. |`data manrawdecode `|Y |`Manchester decode binary stream in DemodBuffer` |`data modulation `|Y |`Identify LF signal for clock and modulation` |`data rawdemod `|Y |`Demodulate the data in the GraphBuffer and output binary` +|`data autodemod `|Y |`Detect modulation, encoding and clock, then demodulate` +|`data fft `|Y |`Fourier transform of the GraphBuffer` +|`data fitscore `|Y |`Rank matched filter hypotheses for modulation, encoding and clock` +|`data spectrum `|Y |`Spectral peaks, symbol rate and modulation family hint` |`data askedgedetect `|Y |`Adjust Graph for manual ASK demod` |`data autocorr `|Y |`Autocorrelation over window` |`data convertbitstream `|Y |`Convert GraphBuffer's 0/1 values to 127 / -127` @@ -143,6 +147,7 @@ Check column "offline" for their availability. |`data hexsamples `|N |`Dump big buffer as hex bytes` |`data samples `|N |`Get raw samples for graph window ( GraphBuffer )` |`data qrcode `|Y |`Create a QR code` +|`data gensignal `|N |`Generate a synthetic LF waveform into the GraphBuffer` |`data test_ss8 `|N |`Test the implementation of Buffer Save States (8-bit buffer)` |`data test_ss32 `|N |`Test the implementation of Buffer Save States (32-bit buffer)` |`data test_ss32s `|N |`Test the implementation of Buffer Save States (32-bit signed buffer)` @@ -276,7 +281,6 @@ Check column "offline" for their availability. |`hf 15 writeafi `|N |`Writes the AFI on an ISO-15693 tag` |`hf 15 writedsfid `|N |`Writes the DSFID on an ISO-15693 tag` |`hf 15 csetuid `|N |`Set UID for magic card` -|`hf 15 cfinalize `|N |`Finalize a magic V3 tag (irreversible)` ### hf aliro @@ -1521,6 +1525,18 @@ Check column "offline" for their availability. |`lf t55xx wipe `|N |`Wipe a T55xx tag and set defaults (will destroy any data on tag)` +### lf trovan + + { Trovan animal IDs... } + +|command |offline |description +|------- |------- |----------- +|`lf trovan help `|Y |`This help` +|`lf trovan demod `|Y |`demodulate a Trovan tag from the GraphBuffer` +|`lf trovan reader `|N |`attempt to read and extract tag data` +|`lf trovan clone `|N |`clone Trovan tag to T55x7 or Q5/T5555` + + ### lf viking { Viking RFIDs... } From 92969c5173556b2a3c467c7cf1a6e519628d33f7 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Thu, 30 Jul 2026 17:06:19 +0200 Subject: [PATCH 5/6] fix test --- tools/pm3_tests.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/pm3_tests.sh b/tools/pm3_tests.sh index 9b1190d21..170abb922 100755 --- a/tools/pm3_tests.sh +++ b/tools/pm3_tests.sh @@ -511,13 +511,13 @@ while true; do echo -e "\n${C_BLUE}Testing LF:${C_NC}" if ! CheckExecute "lf hitag2 test" "$CLIENTBIN -c 'lf hitag test'" "Tests \( ok"; then break; fi - if ! CheckExecute "lf cotag demod test 1/4" "$CLIENTBIN -c 'data load -f traces/lf_cotag_220_8331.pm3; lf cotag demod -c 272'" \ + if ! CheckExecute "lf cotag demod test 1/4" "$CLIENTBIN -c 'data load -f traces/lf_cotag_220_8331.pm3; lf cotag demod -c 272 -v'" \ "data hex: 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 5 8 2 4 E 0 0 0 0 8 0 2 3 D F 7 7"; then break; fi - if ! CheckExecute "lf cotag demod test 2/4" "$CLIENTBIN -c 'data load -f traces/cotag/lf_cotag_passive_02402447_700000.pm3; lf cotag demod'" \ + if ! CheckExecute "lf cotag demod test 2/4" "$CLIENTBIN -c 'data load -f traces/cotag/lf_cotag_passive_02402447_700000.pm3; lf cotag demod -v'" \ "data hex: 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 5 8 2 4 E 0 0 0 0 8 0 2 4 A 8 8 F"; then break; fi - if ! CheckExecute "lf cotag demod test 3/4" "$CLIENTBIN -c 'data load -f traces/cotag/lf_cotag_passive_bewator_01576814_700000.pm3; lf cotag demod'" \ + if ! CheckExecute "lf cotag demod test 3/4" "$CLIENTBIN -c 'data load -f traces/cotag/lf_cotag_passive_bewator_01576814_700000.pm3; lf cotag demod -v'" \ "data hex: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7 C B B 7 0 0 0 0 8 0 1 8 0 F 6 E"; then break; fi - if ! CheckExecute "lf cotag demod test 4/4" "$CLIENTBIN -c 'data load -f traces/cotag/lf_cotag_active_00001577_700000.pm3; lf cotag demod'" \ + if ! CheckExecute "lf cotag demod test 4/4" "$CLIENTBIN -c 'data load -f traces/cotag/lf_cotag_active_00001577_700000.pm3; lf cotag demod -v'" \ "data hex: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 8 2 7 B 0 4 8 E 0 0 0 0 0 6 2 9"; then break; fi if ! CheckExecute "lf AWID test" "$CLIENTBIN -c 'data load -f traces/lf_AWID-15-259.pm3;lf search -1'" "AWID ID found"; then break; fi if ! CheckExecute "lf EM410x test" "$CLIENTBIN -c 'data load -f traces/lf_EM4102-1.pm3;lf search -1'" "EM410x ID found"; then break; fi From 6411ba1ee450c565c6a6e302a4d653d367dcc04d Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Thu, 30 Jul 2026 17:06:36 +0200 Subject: [PATCH 6/6] add --- client/src/cmddata.c | 4 +-- client/src/cmdflashmemspiffs.c | 2 +- client/src/cmdhf14b.c | 2 +- client/src/cmdhf15.c | 36 ++++++++++++------------- client/src/cmdhflegic.c | 24 ++++++++--------- client/src/cmdlf.c | 8 +++--- client/src/cmdlfcotag.c | 12 ++++----- client/src/cmdlft55xx.c | 20 +++++++------- client/src/cmdlftrovan.c | 6 ++--- client/src/flash.c | 4 +-- client/src/frame_progress.c | 48 +++++++++++++++++----------------- client/src/pm3_dsp.c | 14 +++++----- client/src/proxmark3.c | 2 +- doc/commands.json | 18 ++++++++----- doc/commands.md | 2 +- 15 files changed, 103 insertions(+), 99 deletions(-) diff --git a/client/src/cmddata.c b/client/src/cmddata.c index 0e5de7e5a..de406785f 100644 --- a/client/src/cmddata.c +++ b/client/src/cmddata.c @@ -4874,7 +4874,7 @@ static int CmdSpectrum(const char *Cmd) { return PM3_EMALLOC; } - // anchor the ridge tracker on the symbol rate the full length analysis found, + // anchor the ridge tracker on the symbol rate the full length analysis found, // so drift is measured against the clock we care about rather than against whichever bin peaked in each frame const double anchor = an.have_symbol_clk ? (1.0 / an.symbol_clk) : 0.0; @@ -5153,7 +5153,7 @@ static int CmdFitScore(const char *Cmd) { return PM3_SUCCESS; } -// Developer helper. +// Developer helper. // Fills the GraphBuffer with a synthetic waveform whose modulation, encoding and clock are known static int CmdGenSignal(const char *Cmd) { diff --git a/client/src/cmdflashmemspiffs.c b/client/src/cmdflashmemspiffs.c index ad27753a4..2176913aa 100644 --- a/client/src/cmdflashmemspiffs.c +++ b/client/src/cmdflashmemspiffs.c @@ -467,7 +467,7 @@ static int CmdFlashMemSpiFFSWipe(const char *Cmd) { CLIParserInit(&ctx, "mem spiffs wipe", _RED_("* * * Warning * * *") " \n" _CYAN_("This command wipes all files on the device SPIFFS file system"), - "mem spiffs wipe"); + "mem spiffs wipe"); void *argtable[] = { arg_param_begin, diff --git a/client/src/cmdhf14b.c b/client/src/cmdhf14b.c index 204ce2477..5d3eda8bb 100644 --- a/client/src/cmdhf14b.c +++ b/client/src/cmdhf14b.c @@ -3132,7 +3132,7 @@ static int CmdHF14BAPDU(const char *Cmd) { arg_lit0("t", "tlv", "executes TLV decoder if it possible"), arg_lit0(NULL, "decode", "decode apdu request if it possible"), arg_str0("m", "make", "", "make apdu with head from this field and data from data field.\n" - " must be 4 bytes: "), + " must be 4 bytes: "), arg_lit0("e", "extended", "make extended length apdu if `m` parameter included"), arg_int0("l", "le", "", "Le apdu parameter if `m` parameter included"), arg_str1("d", "data", "", " if `m` parameter included"), diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index b43cde96e..1711e784c 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -1383,22 +1383,22 @@ static void iso15_render_presence_table(const iso15_seen_tag_t *seen, int n, for (int i = 0; i < n; i++) { if (seen[i].present) { PrintAndLogEx(INFO, "%3d %-25s %02X %spresent%s" - , i + 1 - , iso15693_sprintUID(NULL, (uint8_t *)seen[i].uid) - , seen[i].dsfid - , c_grn - , c_off - ); + , i + 1 + , iso15693_sprintUID(NULL, (uint8_t *)seen[i].uid) + , seen[i].dsfid + , c_grn + , c_off + ); } else { uint32_t secs = (uint32_t)((now - seen[i].last_seen_ms) / 1000); PrintAndLogEx(INFO, "%3d %-25s %02X %sgone%s (%us ago)" - , i + 1 - , iso15693_sprintUID(NULL, (uint8_t *)seen[i].uid) - , seen[i].dsfid - , c_yel - , c_off - , secs - ); + , i + 1 + , iso15693_sprintUID(NULL, (uint8_t *)seen[i].uid) + , seen[i].dsfid + , c_yel + , c_off + , secs + ); } lines++; } @@ -3791,12 +3791,12 @@ static int CmdHF15SlixProtectPage(const char *Cmd) { PrintAndLogEx(INFO, "Trying to set page protection pointer to " _YELLOW_("%d"), payload.divide_ptr); PrintAndLogEx(INFO, _YELLOW_("LO") " page access %s%s" - , (payload.prot_status & 0x01) ? _RED_("r") : _GREEN_("r") - , (payload.prot_status & 0x02) ? _RED_("w") : _GREEN_("w") - ); + , (payload.prot_status & 0x01) ? _RED_("r") : _GREEN_("r") + , (payload.prot_status & 0x02) ? _RED_("w") : _GREEN_("w") + ); PrintAndLogEx(INFO, _YELLOW_("HI") " page access %s%s" - , (payload.prot_status & 0x10) ? _RED_("r") : _GREEN_("r") - , (payload.prot_status & 0x20) ? _RED_("w") : _GREEN_("w")); + , (payload.prot_status & 0x10) ? _RED_("r") : _GREEN_("r") + , (payload.prot_status & 0x20) ? _RED_("w") : _GREEN_("w")); PacketResponseNG resp; clearCommandBuffer(); diff --git a/client/src/cmdhflegic.c b/client/src/cmdhflegic.c index 5090438e1..8d90fa6f6 100644 --- a/client/src/cmdhflegic.c +++ b/client/src/cmdhflegic.c @@ -201,11 +201,11 @@ static int legic_write_dump_to_tag(uint8_t *dump, size_t bytes_read) { static int CmdLegicMigrate(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf legic migrate", - "Clone a LEGIC Prime dump to the currently attached tag.\n" - "Optionally apply a DCF update after cloning, but only when explicitly requested.", - "hf legic migrate -f src.bin\n" - "hf legic migrate -f src.bin --kgh\n" - "hf legic migrate -f src.bin --dcf 60EA --danger"); + "Clone a LEGIC Prime dump to the currently attached tag.\n" + "Optionally apply a DCF update after cloning, but only when explicitly requested.", + "hf legic migrate -f src.bin\n" + "hf legic migrate -f src.bin --kgh\n" + "hf legic migrate -f src.bin --dcf 60EA --danger"); void *argtable[] = { arg_param_begin, @@ -1352,13 +1352,13 @@ static int CmdLegicRestore(const char *Cmd) { static int CmdLegicClone(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf legic clone", - "Rewrite a LEGIC dump for a new target tag or MCC.\n" - "Use --mcc to rewrite only the dump obfuscation, or --write to clone to the current tag.\n" - "Use --kgh with --write to recalculate UID-bound KGH payload CRCs.", - "hf legic clone -f src.bin -c 39 -o clone.bin\n" - "hf legic clone -f src.bin --write\n" - "hf legic clone -f src.bin --write --kgh\n" - "hf legic clone -f src.bin --write --kgh -o clone.bin"); + "Rewrite a LEGIC dump for a new target tag or MCC.\n" + "Use --mcc to rewrite only the dump obfuscation, or --write to clone to the current tag.\n" + "Use --kgh with --write to recalculate UID-bound KGH payload CRCs.", + "hf legic clone -f src.bin -c 39 -o clone.bin\n" + "hf legic clone -f src.bin --write\n" + "hf legic clone -f src.bin --write --kgh\n" + "hf legic clone -f src.bin --write --kgh -o clone.bin"); void *argtable[] = { arg_param_begin, diff --git a/client/src/cmdlf.c b/client/src/cmdlf.c index 56d6e71eb..9d6b59cad 100644 --- a/client/src/cmdlf.c +++ b/client/src/cmdlf.c @@ -1836,11 +1836,11 @@ int CmdLFRelay(const char *Cmd) { " --rdr : Reading device, act as IP client and reads LF tag and sends data\n" " --tag : Simulation device, act as IP server and simulates relayed data\n", _WHITE_("Device A, reading LF tag, client") "\n" - "lf relay --rdr --ip 192.168.1.141 -> Client, connect to IP 192.168.1.141:8000\n" - "lf relay --rdr --ip 192.168.1.141 -p 18111 -> Client, connect to IP 192.168.1.141:18111 \n\n" + "lf relay --rdr --ip 192.168.1.141 -> Client, connect to IP 192.168.1.141:8000\n" + "lf relay --rdr --ip 192.168.1.141 -p 18111 -> Client, connect to IP 192.168.1.141:18111 \n\n" _WHITE_("Device B, simulate LF tag, server") "\n" - "lf relay --tag -p 8111 -> Server listening port 8111, recv 40000 samples\n" - "lf relay --tag -s 10000 -> Server listening port 8000, recv 10000 samples\n" + "lf relay --tag -p 8111 -> Server listening port 8111, recv 40000 samples\n" + "lf relay --tag -s 10000 -> Server listening port 8000, recv 10000 samples\n" ); void *argtable[] = { diff --git a/client/src/cmdlfcotag.c b/client/src/cmdlfcotag.c index 759d6ac09..f016e3ed8 100644 --- a/client/src/cmdlfcotag.c +++ b/client/src/cmdlfcotag.c @@ -192,7 +192,7 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s , i , (int32_t)(clock_start + i * clock_half) , (unsigned)half1, (unsigned)half2 - ); + ); } // re-align by one half-clock forward @@ -284,7 +284,7 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s for (int i = 0; i < manchester_count && preamble_index < 0; i++) { for (int p = 0; preamble_patterns[p].pat != NULL; p++) { - if ( ((i + preamble_patterns[p].len) <= manchester_count) + if (((i + preamble_patterns[p].len) <= manchester_count) && (memcmp(&manchester_demod_reversed[i], preamble_patterns[p].pat, preamble_patterns[p].len) == 0)) { preamble_index = i; preamble_type = preamble_patterns[p].name; @@ -348,9 +348,9 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s for (int i = 0; i < LF_COTAG_DATA_LEN; i += 4) { int nibble = (data_bits[i] << 3) - | (data_bits[i + 1] << 2) - | (data_bits[i + 2] << 1) - | data_bits[i + 3]; + | (data_bits[i + 1] << 2) + | (data_bits[i + 2] << 1) + | data_bits[i + 3]; str[p++] = ' '; str[p++] = ' '; @@ -573,7 +573,7 @@ static int cmp_int32_asc(const void *a, const void *b) { * resolution for very low amplitude captures. */ static double trimmed_mean_abs(const int32_t *samples, int start, int count) { - /// The values are sorted and the top 1/TRIM_DROP_DEN are discarded before averaging + /// The values are sorted and the top 1/TRIM_DROP_DEN are discarded before averaging const int TRIM_DROP_DEN = 4; // trimmed mean denominator int32_t buf[count]; diff --git a/client/src/cmdlft55xx.c b/client/src/cmdlft55xx.c index 728d296b5..9a51b567e 100644 --- a/client/src/cmdlft55xx.c +++ b/client/src/cmdlft55xx.c @@ -1280,9 +1280,9 @@ static bool t55xx_fallback_try(pm3_mod_t mod, pm3_enc_t enc, int fc_hi, int fc_l if (mod == PM3_MOD_FSK) { - static const uint8_t rates[] = { 32, 40, 50, 64, 100, 128 }; + static const uint8_t rates[] = { 32, 40, 50, 64, 100, 128 }; - const uint8_t pairs[3][2] = { + const uint8_t pairs[3][2] = { { (uint8_t)fc_hi, (uint8_t)fc_lo }, { 8, 5 }, { 10, 8 } }; @@ -1549,14 +1549,14 @@ static void t55xx_detect_fallback(t55xx_conf_block_t *tests, uint8_t *hits, uint nseen++; if (t55xx_fallback_try(fit.items[i].mod - , fit.items[i].enc - , fit.items[i].fc_hi - , fit.items[i].fc_lo - , fitclk - , tests - , hits - , downlink_mode - , (round == 2))) { + , fit.items[i].enc + , fit.items[i].fc_hi + , fit.items[i].fc_lo + , fitclk + , tests + , hits + , downlink_mode + , (round == 2))) { done = true; break; } diff --git a/client/src/cmdlftrovan.c b/client/src/cmdlftrovan.c index 410bc97e5..c60c75285 100644 --- a/client/src/cmdlftrovan.c +++ b/client/src/cmdlftrovan.c @@ -17,7 +17,7 @@ // // Trovan is not ISO 11784/11785 // -// Differential biphase phase shift keying on an Fc/2 subcarrier, 16 carrier cycles to the bit. +// Differential biphase phase shift keying on an Fc/2 subcarrier, 16 carrier cycles to the bit. // A telegram is 64 bits: // // 8 bits sync, 01111111. Cannot occur anywhere in the body without @@ -103,7 +103,7 @@ static bool trovan_check(const uint8_t *bits, uint64_t *id) { return true; } -// Hunt for a sync word and validate the telegram that follows it. +// Hunt for a sync word and validate the telegram that follows it. // Returns the bit offset the telegram started at, or -1. static int trovan_find(const uint8_t *bits, size_t size, uint64_t *id) { @@ -135,7 +135,7 @@ static void trovan_print(uint64_t id) { ); } -int demodTrovan(bool verbose) { +int demodTrovan(bool verbose) { if (PSKDemod(TROVAN_CLOCK, 0, 100, false) != PM3_SUCCESS) { PrintAndLogEx(DEBUG, "DEBUG: Error - Trovan: PSK demod failed"); diff --git a/client/src/flash.c b/client/src/flash.c index 156ccff7f..b1c557897 100644 --- a/client/src/flash.c +++ b/client/src/flash.c @@ -680,7 +680,7 @@ int flash_write(flash_file_t *ctx) { int pct = 0; if (blocks > 50) { - + signal(SIGINT, hadouken_on_sigint); #ifndef _WIN32 signal(SIGWINCH, hadouken_on_sigwinch); @@ -690,7 +690,7 @@ int flash_write(flash_file_t *ctx) { } while (length) { - + uint32_t block_size = length; if (block_size > BLOCK_SIZE) { block_size = BLOCK_SIZE; diff --git a/client/src/frame_progress.c b/client/src/frame_progress.c index d09ff7944..d6b4b99eb 100644 --- a/client/src/frame_progress.c +++ b/client/src/frame_progress.c @@ -36,10 +36,10 @@ static int terminal_width(void) { return csbi.srWindow.Right - csbi.srWindow.Left + 1; } const char *c = getenv("COLUMNS"); - if (c) { - int w = atoi(c); + if (c) { + int w = atoi(c); if (w > 0) { - return w; + return w; } } return 80; @@ -53,7 +53,7 @@ static int terminal_width(void) { return (int)ws.ws_col; } const char *c = getenv("COLUMNS"); - if (c) { + if (c) { int w = atoi(c); if (w > 0) { return w; @@ -63,7 +63,7 @@ static int terminal_width(void) { } #endif -// --- wave parameters (unchanged from hadouken_wave.c) ------------------- +// --- wave parameters (unchanged from hadouken_wave.c) ------------------- #define ORIGIN_X 64.0 #define ORIGIN_Y 8.0 #define ASPECT 2.0 @@ -91,7 +91,7 @@ static const char *GLYPHS = " .:;+xX#"; // --- shared state -------------------------------------------------------- static volatile sig_atomic_t g_resize = 0; // set by SIGWINCH static volatile int g_running = 0; -static volatile int g_progress = 0; // 0..100 +static volatile int g_progress = 0; // 0..100 static pthread_t g_thread; // animation config – written once before hadouken_start() @@ -99,9 +99,9 @@ static double g_fps = 30.0; static int g_glyph_mode = 0; static int g_wave_mode = 0; // 0 = radial, 1 = beam -void hadouken_on_sigint(int s) { - (void)s; - g_running = 0; +void hadouken_on_sigint(int s) { + (void)s; + g_running = 0; } #ifndef _WIN32 @@ -111,9 +111,9 @@ void hadouken_on_sigwinch(int s) { } #endif -/* +/* * Compute the inner fill width for the progress bar from the current terminal - * width, clamped to [BAR_MIN, BAR_MAX]. + * width, clamped to [BAR_MIN, BAR_MAX]. */ static int bar_inner_width(void) { int w = terminal_width() - BAR_OVERHEAD; @@ -126,7 +126,7 @@ static int bar_inner_width(void) { return w; } -// --- colour helpers ------------------------------------------------------ +// --- colour helpers ------------------------------------------------------ static inline int clamp8(double v) { if (v < 0) { return 0; @@ -175,11 +175,11 @@ static int render_anim_line(char *dst, int y, double t, int glyph_mode, int mode int ch = c->ch; int has = c->has; int r = 0; - int g = 0; + int g = 0; int b = 0; - if (ch == ' ' && !has) { - pending_blanks++; + if (ch == ' ' && !has) { + pending_blanks++; continue; } @@ -192,7 +192,7 @@ static int render_anim_line(char *dst, int y, double t, int glyph_mode, int mode double lum = (r + g + b) / 765.0; - int idx = (int)(lum * 8); + int idx = (int)(lum * 8); if (idx > 7) { idx = 7; } @@ -203,8 +203,8 @@ static int render_anim_line(char *dst, int y, double t, int glyph_mode, int mode } } } else if (has) { - r = c->r; - g = c->g; + r = c->r; + g = c->g; b = c->b; } @@ -219,10 +219,10 @@ static int render_anim_line(char *dst, int y, double t, int glyph_mode, int mode len += sprintf(dst + len, ANSI_FG_RGB, r, g, b); } else { len += sprintf(dst + len, AEND); - } - last_has = has; - last_r = r; - last_g = g; + } + last_has = has; + last_r = r; + last_g = g; last_b = b; } dst[len++] = (char)ch; @@ -285,7 +285,7 @@ static void *anim_thread(void *arg) { static char prev[FH][FW * 24 + 16]; static char out [FH * (FW * 24 + 32) + 512]; - // bar buf: BAR_MAX fill chars + ANSI escapes per char + fixed overhead + // bar buf: BAR_MAX fill chars + ANSI escapes per char + fixed overhead static char bar [BAR_MAX + 256]; int have_prev = 0; @@ -303,7 +303,7 @@ static void *anim_thread(void *arg) { have_prev = 0; } - // dynamic: re-queried every frame + // dynamic: re-queried every frame int bw = bar_inner_width(); int n = 0; diff --git a/client/src/pm3_dsp.c b/client/src/pm3_dsp.c index 36b3a3aff..f0d85efca 100644 --- a/client/src/pm3_dsp.c +++ b/client/src/pm3_dsp.c @@ -244,7 +244,7 @@ void pm3_apply_window(double *data, size_t len, pm3_window_t win) { w = 0.54 - 0.46 * cos(phase); break; } - case PM3_WIN_BLACKMAN:{ + case PM3_WIN_BLACKMAN: { w = 0.42 - 0.5 * cos(phase) + 0.08 * cos(2.0 * phase); break; } @@ -765,8 +765,8 @@ static bool find_fsk_pair(const pm3_peak_t *peaks, size_t n, double *lo, double // Find the spacing of the harmonic comb that best explains a set of peaks. // // Squaring a symbol stream produces energy at every integer multiple of the symbol rate. -// Picking the tallest line therefore lands on a harmonic as often as not. -// Try each peak and each peak divided by a small integer as a candidate spacing +// Picking the tallest line therefore lands on a harmonic as often as not. +// Try each peak and each peak divided by a small integer as a candidate spacing // and keep whichever one places the most peaks on a multiple of itself static double comb_fundamental(const pm3_peak_t *peaks, size_t npeaks, size_t n, double min_clk) { @@ -1101,7 +1101,7 @@ bool pm3_is_switched_carrier(const double *sig, size_t len) { mean += sig[i]; } mean /= (double)len; - + size_t flips = 0, pairs = 0; int prev = 0; @@ -1189,7 +1189,7 @@ static int analyse_once(const double *sig, size_t len, size_t n, pm3_window_t wi } free(work); - // low frequency energy relative to the strongest line. + // low frequency energy relative to the strongest line. // NRZ piles up at DC, // Manchester and biphase have null. double dc_sum = 0.0; @@ -1247,8 +1247,8 @@ static int analyse_once(const double *sig, size_t len, size_t n, pm3_window_t wi out->family = PM3_FAM_FSK; out->confidence = confidence_from_db( - 0.5 * (prominence_at(&plain, 1.0 / out->fsk_clk_lo) + prominence_at(&plain, 1.0 / out->fsk_clk_hi)) - ); + 0.5 * (prominence_at(&plain, 1.0 / out->fsk_clk_lo) + prominence_at(&plain, 1.0 / out->fsk_clk_hi)) + ); } else if (out->have_carrier) { diff --git a/client/src/proxmark3.c b/client/src/proxmark3.c index 28a541c20..fa1d6c75d 100644 --- a/client/src/proxmark3.c +++ b/client/src/proxmark3.c @@ -891,7 +891,7 @@ static int dumpmem_pm3(char *serial_port_name, const char *filename, uint32_t ad PrintAndLogEx(INFO, "Device is running the bootloader."); } else { PrintAndLogEx(ERR, "Device is running the bootloader, but the bootloader" - " doesn't understand the READ MEM command."); + " doesn't understand the READ MEM command."); goto finish2; } } diff --git a/doc/commands.json b/doc/commands.json index 2a403e7ad..6e0e1657c 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -5197,11 +5197,12 @@ }, "hf legic clone": { "command": "hf legic clone", - "description": "Rewrite a LEGIC dump for a new target tag or MCC. Use --mcc to rewrite only the dump obfuscation, or --write to clone to the current tag.", + "description": "Rewrite a LEGIC dump for a new target tag or MCC. Use --mcc to rewrite only the dump obfuscation, or --write to clone to the current tag. Use --kgh with --write to recalculate UID-bound KGH payload CRCs.", "notes": [ "hf legic clone -f src.bin -c 39 -o clone.bin", "hf legic clone -f src.bin --write", - "hf legic clone -f src.bin --write -o clone.bin" + "hf legic clone -f src.bin --write --kgh", + "hf legic clone -f src.bin --write --kgh -o clone.bin" ], "offline": false, "options": [ @@ -5209,9 +5210,10 @@ "-f, --file Source dump file", "--mcc Target MCC byte for output-only cloning", "-o, --output Output cloned dump file", - "-w, --write Write cloned dump to the currently attached tag" + "-w, --write Write cloned dump to the currently attached tag", + "--kgh Recalculate KGH payload CRCs" ], - "usage": "hf legic clone [-hw] -f [--mcc ] [-o ]" + "usage": "hf legic clone [-hw] -f [--mcc ] [-o ] [--kgh]" }, "hf legic crc": { "command": "hf legic crc", @@ -5351,16 +5353,18 @@ "description": "Clone a LEGIC Prime dump to the currently attached tag. Optionally apply a DCF update after cloning, but only when explicitly requested.", "notes": [ "hf legic migrate -f src.bin", + "hf legic migrate -f src.bin --kgh", "hf legic migrate -f src.bin --dcf 60EA --danger" ], "offline": false, "options": [ "-h, --help This help", "-f, --file Source dump file", + "--kgh Recalculate KGH payload CRCs", "--dcf Optional DCF bytes to write after clone", "--danger Allow the explicit DCF write" ], - "usage": "hf legic migrate [-h] -f [--dcf ] [--danger]" + "usage": "hf legic migrate [-h] -f [--kgh] [--dcf ] [--danger]" }, "hf legic rdbl": { "command": "hf legic rdbl", @@ -14012,7 +14016,7 @@ }, "lf t55xx help": { "command": "lf t55xx help", - "description": "----------- ---------------------------- notice ----------------------------- Remember to run `lf t55xx detect` first whenever a new card is placed on the Proxmark3 or the config block changed. help This help ----------- --------------------- operations --------------------- config Set/Get T55XX configuration (modulation, inverted, offset, rate) detect Try detecting the tag modulation from reading the configuration block info Show T55x7 configuration data (page 0/ blk 0) trace Show T55x7 traceability data (page 1/ blk 0-1) view Display content from tag dump file ----------- --------------------- recovery --------------------- sniff Attempt to recover T55xx commands from sample buffer --------------------------------------------------------------------------------------- lf t55xx clonehelp available offline: no Display a list of available commands for cloning specific techs on T5xx tags", + "description": "----------- ------------------------------- notice --------------------------------- Always run `lf t55xx detect` first whenever a new card is placed on the Proxmark3 or the config block changed. help This help ----------- ----------------------------- operations ------------------------------- config Set/Get T55XX configuration (modulation, inverted, offset, rate) detect Try detecting the tag modulation from reading the configuration block info Show T55x7 configuration data (page 0/ blk 0) trace Show T55x7 traceability data (page 1/ blk 0-1) view Display content from tag dump file ----------- ------------------------------ recovery -------------------------------- sniff Attempt to recover T55xx commands from sample buffer --------------------------------------------------------------------------------------- lf t55xx clonehelp available offline: no Display a list of available commands for cloning specific techs on T5xx tags", "notes": [ "lf t55xx clonehelp" ], @@ -16021,6 +16025,6 @@ "metadata": { "commands_extracted": 896, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-07-28T11:00:12+00:00" + "extracted_on": "2026-07-30T14:44:57+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index d56072456..07bed6008 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -1510,7 +1510,7 @@ Check column "offline" for their availability. |`lf t55xx info `|Y |`Show T55x7 configuration data (page 0/ blk 0)` |`lf t55xx p1detect `|N |`Try detecting if this is a t55xx tag by reading page 1` |`lf t55xx read `|N |`Read T55xx block data` -|`lf t55xx resetread `|N |`Send Reset Cmd then lf read the stream to attempt to identify the start of it` +|`lf t55xx resetread `|N |`Send Reset Cmd then lf read the stream to attempt to identify the start` |`lf t55xx restore `|N |`Restore T55xx card Page 0 / Page 1 blocks` |`lf t55xx trace `|Y |`Show T55x7 traceability data (page 1/ blk 0-1)` |`lf t55xx wakeup `|N |`Send AOR wakeup command`