diff options
| -rw-r--r-- | util/libreboot-utils/lottery.c | 3 | ||||
| -rw-r--r-- | util/libreboot-utils/mkhtemp.c | 3 | ||||
| -rw-r--r-- | util/libreboot-utils/nvmutil.c | 3 | ||||
| -rw-r--r-- | util/nvmutil/nvmutil.c | 361 | ||||
| -rw-r--r-- | util/spkmodem_decode/.gitignore | 2 | ||||
| -rw-r--r-- | util/spkmodem_decode/Makefile | 30 | ||||
| -rw-r--r-- | util/spkmodem_decode/spkmodem-decode.c | 725 | ||||
| -rw-r--r-- | util/spkmodem_recv/.gitignore | 1 | ||||
| -rw-r--r-- | util/spkmodem_recv/Makefile | 14 | ||||
| -rw-r--r-- | util/spkmodem_recv/spkmodem-recv.c | 124 |
10 files changed, 455 insertions, 811 deletions
diff --git a/util/libreboot-utils/lottery.c b/util/libreboot-utils/lottery.c index 38407512..3ac4d135 100644 --- a/util/libreboot-utils/lottery.c +++ b/util/libreboot-utils/lottery.c @@ -16,6 +16,9 @@ exit_cleanup(void); int main(int argc, char **argv) { +#ifndef __linux__ +#error This code is currently buggy on BSD systems. Only use on Linux. +#endif int same = 0; char *buf; size_t size = BUFSIZ; diff --git a/util/libreboot-utils/mkhtemp.c b/util/libreboot-utils/mkhtemp.c index d6300f16..9ff70328 100644 --- a/util/libreboot-utils/mkhtemp.c +++ b/util/libreboot-utils/mkhtemp.c @@ -41,6 +41,9 @@ exit_cleanup(void); int main(int argc, char *argv[]) { +#ifndef __linux__ +#error This code is currently buggy on BSD systems. Only use on Linux. +#endif size_t len; size_t tlen; size_t xc = 0; diff --git a/util/libreboot-utils/nvmutil.c b/util/libreboot-utils/nvmutil.c index 09801585..67b01ae7 100644 --- a/util/libreboot-utils/nvmutil.c +++ b/util/libreboot-utils/nvmutil.c @@ -27,6 +27,9 @@ exit_cleanup(void); int main(int argc, char *argv[]) { +#ifndef __linux__ +#error This code is currently buggy on BSD systems. Only use on Linux. +#endif struct xstate *x; struct commands *cmd; struct xfile *f; diff --git a/util/nvmutil/nvmutil.c b/util/nvmutil/nvmutil.c index 05459bb7..1f211d6c 100644 --- a/util/nvmutil/nvmutil.c +++ b/util/nvmutil/nvmutil.c @@ -2,24 +2,38 @@ /* Copyright (c) 2022-2025 Leah Rowe <leah@libreboot.org> */ /* Copyright (c) 2023 Riku Viitanen <riku.viitanen@protonmail.com> */ +#include <sys/types.h> #include <sys/stat.h> -#include <dirent.h> #include <err.h> #include <errno.h> #include <fcntl.h> -#include <stdint.h> +#include <stdarg.h> +#include <stddef.h> #include <stdio.h> -#include <stdlib.h> #include <string.h> +#include <stdlib.h> #include <unistd.h> +#include <limits.h> +#include <stdint.h> void cmd_setchecksum(void), cmd_brick(void), swap(int partnum), writeGbe(void), - cmd_dump(void), cmd_setmac(void), readGbe(void), checkdir(const char *path), + cmd_dump(void), cmd_setmac(void), readGbe(void), macf(int partnum), hexdump(int partnum), openFiles(const char *path), cmd_copy(void), parseMacString(const char *strMac, uint16_t *mac), - cmd_swap(void); -int goodChecksum(int partnum); + cmd_swap(void), xclose(int *fd); +int goodChecksum(int partnum), open_on_eintr(const char *pathname, int flags), + fs_retry(int saved_errno, int rval), + rw_retry(int saved_errno, ssize_t rval), if_err(int condition, int errval), + if_err_sys(int condition); +ssize_t rw_exact(int fd, unsigned char *mem, size_t nrw, + off_t off, int rw_type); +ssize_t rw(int fd, void *mem, size_t nrw, + off_t off, int rw_type); +int io_args(int fd, void *mem, size_t nrw, + off_t off, int rw_type); +int with_fallback_errno(int fallback); +ssize_t rw_over_nrw(ssize_t r, size_t nrw); uint8_t hextonum(char chs), rhex(void); #define COMMAND argv[2] @@ -34,6 +48,11 @@ uint8_t hextonum(char chs), rhex(void); #define SIZE_16KB 0x4000 #define SIZE_128KB 0x20000 +#define IO_READ 0 +#define IO_WRITE 1 +#define IO_PREAD 2 +#define IO_PWRITE 3 + uint16_t mac[3] = {0, 0, 0}; ssize_t nf; size_t partsize, gbe[2]; @@ -57,16 +76,28 @@ op_t op[] = { }; void (*cmd)(void) = NULL; -#define ERR() errno = errno ? errno : ECANCELED -#define err_if(x) if (x) err(ERR(), "%s", filename) +#define err_if(x) if (x) err(EXIT_FAILURE, "%s", filename) -#define xopen(f,l,p) if ((f = open(l, p)) == -1) err(ERR(), "%s", l); \ - if (fstat(f, &st) == -1) err(ERR(), "%s", l) +#define xopen(f,l,p) \ + do { \ + if ((f = open_on_eintr(l, p)) == -1) \ + err(EXIT_FAILURE, "%s", l); \ + if (fstat(f, &st) == -1) \ + err(EXIT_FAILURE, "%s", l); \ + } while(0) #define word(pos16, partnum) ((uint16_t *) gbe[partnum])[pos16] #define setWord(pos16, p, val16) if (word(pos16, p) != val16) \ nvmPartChanged[p] = 1 | (word(pos16, p) = val16) +#define SUCCESS(x) ((x) >= 0) + +#define reset_caller_errno(return_value) \ + do { \ + if (SUCCESS(return_value) && (!errno)) \ + errno = saved_errno; \ + } while (0) + int main(int argc, char *argv[]) { @@ -88,7 +119,8 @@ main(int argc, char *argv[]) fprintf(stderr, " %s FILE copy 0|1\n", argv[0]); fprintf(stderr, " %s FILE brick 0|1\n", argv[0]); fprintf(stderr, " %s FILE setchecksum 0|1\n", argv[0]); - err(errno = ECANCELED, "Too few arguments"); + errno = EINVAL; + err(EXIT_FAILURE, "Too few arguments"); } filename = argv[1]; @@ -104,9 +136,6 @@ main(int argc, char *argv[]) } } - checkdir("/dev/urandom"); - checkdir(filename); - #ifdef __OpenBSD__ err_if(unveil("/dev/urandom", "r") == -1); @@ -134,7 +163,8 @@ main(int argc, char *argv[]) cmd = op[i].cmd; break; } - err(errno = EINVAL, "Too few args on command '%s'", + errno = EINVAL; + err(EXIT_FAILURE, "Too few args on command '%s'", op[i].str); } } else { @@ -150,26 +180,18 @@ main(int argc, char *argv[]) strMac = MAC_ADDRESS; } else if ((cmd != NULL) && (argc > 3)) { /* user-supplied partnum */ err_if((errno = (!((part = PARTN[0] - '0') == 0 || part == 1)) - || PARTN[1] ? EINVAL : errno)); /* only allow '0' or '1' */ + || PARTN[1] ? EINVAL : 0)); /* only allow '0' or '1' */ + } + if (cmd == NULL) { + errno = EINVAL; + err(EXIT_FAILURE, "Bad command"); } - err_if((errno = (cmd == NULL) ? EINVAL : errno)); readGbe(); (*cmd)(); writeGbe(); - err_if((errno != 0) && (cmd != cmd_dump)); - return errno; -} - -void -checkdir(const char *path) -{ - if (opendir(path) != NULL) - err(errno = EISDIR, "%s", path); - if (errno == ENOTDIR) - errno = 0; - err_if(errno); + return EXIT_SUCCESS; } void @@ -186,7 +208,8 @@ openFiles(const char *path) partsize = st.st_size >> 1; break; default: - err(errno = ECANCELED, "Invalid file size (not 8/16/128KiB)"); + errno = ECANCELED; + err(EXIT_FAILURE, "Invalid file size (not 8/16/128KiB)"); break; } @@ -206,7 +229,7 @@ readGbe(void) char *buf = malloc(nf << (do_read[0] & do_read[1])); if (buf == NULL) - err(errno, NULL); + err(EXIT_FAILURE, "malloc"); gbe[0] = (size_t) buf; gbe[1] = gbe[0] + (nf * (do_read[0] & do_read[1])); @@ -217,10 +240,11 @@ readGbe(void) if (!do_read[p]) continue; - ssize_t nr = pread(fd, (uint8_t *) gbe[p], nf, p * partsize); + ssize_t nr = rw_exact(fd, (uint8_t *) gbe[p], + nf, p * partsize, IO_PREAD); err_if(nr == -1); if (nr != nf) - err(errno == ECANCELED, + err(EXIT_FAILURE, "%ld bytes read from '%s', expected %ld bytes\n", nr, filename, nf); @@ -254,29 +278,38 @@ cmd_setmac(void) } if (mac_updated) - errno = 0; + return; + + errno = EINVAL; + err(EXIT_FAILURE, "Error updating MAC address"); } void parseMacString(const char *strMac, uint16_t *mac) { uint64_t total = 0; - if (strnlen(strMac, 20) != 17) - err(errno = EINVAL, "Invalid MAC address string length"); + if (strnlen(strMac, 20) != 17) { + errno = EINVAL; + err(EXIT_FAILURE, "Invalid MAC address string length"); + } for (uint8_t h, i = 0; i < 16; i += 3) { if (i != 15) - if (strMac[i + 2] != ':') - err(errno = EINVAL, + if (strMac[i + 2] != ':') { + errno = EINVAL; + err(EXIT_FAILURE, "Invalid MAC address separator '%c'", strMac[i + 2]); + } int byte = i / 3; for (int nib = 0; nib < 2; nib++, total += h) { - if ((h = hextonum(strMac[i + nib])) > 15) - err(errno = EINVAL, "Invalid character '%c'", + if ((h = hextonum(strMac[i + nib])) > 15) { + errno = EINVAL; + err(EXIT_FAILURE, "Invalid character '%c'", strMac[i + nib]); + } /* If random, ensure that local/unicast bits are set */ if ((byte == 0) && (nib == 1)) @@ -290,10 +323,15 @@ parseMacString(const char *strMac, uint16_t *mac) } } + if (!((total == 0) || (mac[0] & 1))) + return; + + errno = EINVAL; + if (total == 0) - err(errno = EINVAL, "Invalid MAC (all-zero MAC address)"); + err(EXIT_FAILURE, "Invalid MAC (all-zero MAC address)"); if (mac[0] & 1) - err(errno = EINVAL, "Invalid MAC (multicast bit set)"); + err(EXIT_FAILURE, "Invalid MAC (multicast bit set)"); } uint8_t @@ -316,7 +354,8 @@ rhex(void) { static uint8_t n = 0, rnum[16]; if (!n) - err_if(pread(rfd, (uint8_t *) &rnum, (n = 15) + 1, 0) == -1); + err_if(rw_exact(rfd, (uint8_t *) &rnum, + (n = 15) + 1, 0, IO_READ) == -1); return rnum[n--] & 0xf; } @@ -334,9 +373,6 @@ cmd_dump(void) printf("MAC (part %d): ", partnum); macf(partnum); hexdump(partnum); - - if ((numInvalid < 2) && (partnum)) - errno = 0; } } @@ -415,7 +451,6 @@ goodChecksum(int partnum) return 1; fprintf(stderr, "WARNING: BAD checksum in part %d\n", partnum); - errno = ECANCELED; return 0; } @@ -429,12 +464,15 @@ writeGbe(void) continue; swap(p); /* swap bytes on big-endian host CPUs */ - ssize_t nw = pwrite(fd, (uint8_t *) gbe[p], nf, p * partsize); + ssize_t nw = rw_exact(fd, (uint8_t *) gbe[p], nf, + p * partsize, IO_PWRITE); err_if(nw == -1); - if (nw != nf) - err(errno == ECANCELED, + if (nw != nf) { + errno = ECANCELED; + err(EXIT_SUCCESS, "%ld bytes written to '%s', expected %ld bytes\n", nw, filename, nf); + } tnw += nf; } @@ -445,15 +483,37 @@ writeGbe(void) cmd_dump(); } - if ((!tnw) && (flags != O_RDONLY) && (!errno)) + if ((!tnw) && (flags != O_RDONLY)) fprintf(stderr, "No changes needed on file '%s'\n", filename); else if (tnw) printf("%ld bytes written to file '%s'\n", tnw, filename); - if (tnw) - errno = 0; + xclose(&fd); +} + +void +xclose(int *fd) +{ + int saved_errno = errno; + int rval = 0; + + if (fd == NULL) + err(EXIT_FAILURE, "xclose: null pointer"); + if (*fd < 0) + return; + + errno = 0; + if ((rval = close(*fd)) < 0) { + if (errno != EINTR) + err(EXIT_FAILURE, "xclose: could not close"); + + /* regard EINTR as a successful close */ + rval = 0; + } + + *fd = -1; - err_if(close(fd) == -1); + reset_caller_errno(rval); } void @@ -470,3 +530,196 @@ swap(int partnum) n[w] ^= n[x]; } } + +int +open_on_eintr(const char *pathname, + int flags) +{ + int saved_errno = errno; + int rval = 0; + errno = 0; + + while (fs_retry(saved_errno, + rval = open(pathname, flags))); + + reset_caller_errno(rval); + return rval; +} + + +ssize_t +rw_exact(int fd, unsigned char *mem, size_t nrw, + off_t off, int rw_type) +{ + int saved_errno = errno; + ssize_t rval = 0; + ssize_t rc = 0; + size_t nrw_cur; + off_t off_cur; + void *mem_cur; + errno = 0; + + if (io_args(fd, mem, nrw, off, rw_type) == -1) + goto err_rw_exact; + + while (1) { + + /* Prevent theoretical overflow */ + if (if_err(rval >= 0 && (size_t)rval > (nrw - (size_t)rc), + EOVERFLOW)) + goto err_rw_exact; + + rc += rval; + if ((size_t)rc >= nrw) + break; + + mem_cur = (void *)(mem + (size_t)rc); + nrw_cur = (size_t)(nrw - (size_t)rc); + + if (if_err(off < 0, EOVERFLOW)) + goto err_rw_exact; + + off_cur = off + (off_t)rc; + + if ((rval = rw(fd, mem_cur, nrw_cur, off_cur, rw_type)) <= 0) + goto err_rw_exact; + } + + if (if_err((size_t)rc != nrw, EIO) || + (rval = rw_over_nrw(rc, nrw)) < 0) + goto err_rw_exact; + + reset_caller_errno(rval); + return rval; + +err_rw_exact: + return with_fallback_errno(EIO); +} + +ssize_t +rw(int fd, void *mem, size_t nrw, + off_t off, int rw_type) +{ + ssize_t rval = 0; + ssize_t r = -1; + int saved_errno = errno; + errno = 0; + + if (io_args(fd, mem, nrw, off, rw_type) == -1 || + if_err(mem == NULL, EFAULT) || + if_err(fd < 0, EBADF) || + if_err(off < 0, EFAULT) || + if_err(nrw == 0, EINVAL)) + return with_fallback_errno(EIO); + + do { + switch (rw_type) { + case IO_READ: + r = read(fd, mem, nrw); + break; + case IO_WRITE: + r = write(fd, mem, nrw); + break; + case IO_PREAD: + r = pread(fd, mem, nrw, off); + break; + case IO_PWRITE: + r = pwrite(fd, mem, nrw, off); + break; + default: + errno = EINVAL; + break; + } + + } while (rw_retry(saved_errno, r)); + + if ((rval = rw_over_nrw(r, nrw)) < 0) + return with_fallback_errno(EIO); + + reset_caller_errno(rval); + return rval; +} + +int +io_args(int fd, void *mem, size_t nrw, + off_t off, int rw_type) +{ + int saved_errno = errno; + errno = 0; + + if (if_err(mem == NULL, EFAULT) || + if_err(fd < 0, EBADF) || + if_err(off < 0, ERANGE) || + if_err(!nrw, EPERM) || /* TODO: toggle zero-byte check */ + if_err(nrw > (size_t)SSIZE_MAX, ERANGE) || + if_err(((size_t)off + nrw) < (size_t)off, ERANGE) || + if_err(rw_type > IO_PWRITE, EINVAL)) + goto err_io_args; + + reset_caller_errno(0); + return 0; + +err_io_args: + return with_fallback_errno(EINVAL); +} + +ssize_t +rw_over_nrw(ssize_t r, size_t nrw) +{ + if (if_err(!nrw, EIO) || + (r == -1) || + if_err((size_t)r > SSIZE_MAX, ERANGE) || + if_err((size_t)r > nrw, ERANGE)) + return with_fallback_errno(EIO); + + return r; +} + +int +with_fallback_errno(int fallback) +{ + if (!errno) + errno = fallback; + return -1; +} + +/* two functions that reduce sloccount by + * two hundred lines */ +int +if_err(int condition, int errval) +{ + if (!condition) + return 0; + if (errval) + errno = errval; + return 1; +} +int +if_err_sys(int condition) +{ + if (!condition) + return 0; + return 1; +} + +#define fs_err_retry() \ + do { \ + if ((rval == -1) && \ + (errno == EINTR)) \ + return 1; \ + if (rval >= 0 && !errno) \ + errno = saved_errno; \ + return 0; \ + } while(0) + +int +fs_retry(int saved_errno, int rval) +{ + fs_err_retry(); +} + +int +rw_retry(int saved_errno, ssize_t rval) +{ + fs_err_retry(); +} diff --git a/util/spkmodem_decode/.gitignore b/util/spkmodem_decode/.gitignore deleted file mode 100644 index 42814fe4..00000000 --- a/util/spkmodem_decode/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -spkmodem-recv -spkmodem-decode diff --git a/util/spkmodem_decode/Makefile b/util/spkmodem_decode/Makefile deleted file mode 100644 index b00c4f43..00000000 --- a/util/spkmodem_decode/Makefile +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-License-Identifier: MIT -# SPDX-FileCopyrightText: 2022,2026 Leah Rowe <leah@libreboot.org> -# SPDX-FileCopyrightText: 2023 Riku Viitanen <riku.viitanen@protonmail.com> - -CC?=cc -CFLAGS?=-Os -Wall -Wextra -Werror -pedantic -std=c90 -DESTDIR?= -PREFIX?=/usr/local -INSTALL?=install - -PROG=spkmodem-decode - -all: $(PROG) - -$(PROG): spkmodem-decode.c - $(CC) $(CFLAGS) spkmodem-decode.c -o $(PROG) - -install: $(PROG) - mkdir -p $(DESTDIR)$(PREFIX)/bin/ - install -c $(PROG) $(DESTDIR)$(PREFIX)/bin/ - -uninstall: - rm -f $(DESTDIR)$(PREFIX)/bin/$(PROG) - -clean: - rm -f $(PROG) - -distclean: clean - -.PHONY: all install uninstall clean distclean diff --git a/util/spkmodem_decode/spkmodem-decode.c b/util/spkmodem_decode/spkmodem-decode.c deleted file mode 100644 index 3b3b33f8..00000000 --- a/util/spkmodem_decode/spkmodem-decode.c +++ /dev/null @@ -1,725 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-2.0-or-later - * Copyright (c) 2013 Free Software Foundation, Inc. - * Copyright (c) 2023, 2026 Leah Rowe <leah@libreboot.org> - * - * This program receives text encoded as pulses on the PC speaker, - * and decodes them via simple FSK (Frequency Shift Keying) - * demodulation and FIR (Finite Impulse Response) frequency - * discriminator. - * - * It waits for specific tones at specific intervals. - * It detects tones within the audio stream and reconstructs - * characters bit-by-bit as the encoded modem signal is received. - * This is performance-efficient on most CPUs, and has relatively - * high tolerance for noisy signals (similar to techniques used - * for data stored on audio cassette tapes). - * - * This is a special interface provided by coreboot and GNU GRUB, - * for computers that lack serial ports (useful for debugging). - * Note that GRUB and coreboot can both send these signals; this - * tool merely decodes them. This tool does not *encode*, only - * decode. - * - * Usage example (NOTE: little endian!): - * parec --channels=1 --rate=48000 --format=s16le | ./spkmodem-decode - * - * Originally provided by GNU GRUB, this version is a heavily - * modified fork that complies with the OpenBSD Kernel Source - * File Style Guide (KNF) instead of GNU coding standards; it - * emphasises strict error handling, portability and code - * quality, as characterised by OpenBSD projects. Several magic - * numbers have been tidied up, calculated (not hardcoded) and - * thoroughly explained, unlike in the original version. - * - * The original version was essentially a blob, masquerading as - * source code. This forked source code is therefore the result - * of extensive reverse engineering (of the GNU source code)! - * This cleaned up code and extensive commenting will thoroughly - * explain how the decoding works. This was done as an academic - * exercise in 2023, continuing in 2026. - * - * This fork of spkmodem-recv, called spkmodem-decode, is provided - * with Libreboot releases: - * https://libreboot.org/ - * - * The original GNU version is here, if you're morbidly curious: - * https://cgit.git.savannah.gnu.org/cgit/grub.git/plain/util/spkmodem-recv.c?id=3dce38eb196f47bdf86ab028de74be40e13f19fd - * - * Libreboot's version was renamed to spkmodem-decode on 12 March 2026, - * since Libreboot's version no longer closely resembles the GNU - * version at all; ergo, a full rename was in order. GNU's version - * was called spkmodem-recv. - */ - -#define _POSIX_SOURCE - -/* - * For OpenBSD define, to detect version - * for deciding whether to use pledge(2) - */ -#ifdef __OpenBSD__ -#include <sys/param.h> -#endif - -#include <errno.h> -#include <limits.h> -#include <stdio.h> -#include <stdarg.h> -#include <stdlib.h> -#include <string.h> -#include <unistd.h> - -/* - * spkmodem is essentially using FSK (Frequency Shift Keying) - * with two primary tones representing encoded bits, - * separated by a framing tone. - * Very cheap on CPU cycles and avoids needing something more - * expensive like FFT or Goertzel filters, and tolerates - * weak/noisy signals. - */ - -/* - * Frequency of audio in Hz - * WARNING: if changing, make sure to adjust - * SAMPLES_PER_FRAME accordingly (see maths below) - */ -#define SAMPLE_RATE 48000 - -/* - * One analysis frame spans 5 ms. - * - * frame_time = SAMPLES_PER_FRAME / SAMPLE_RATE - * - * With the default sample rate (48 kHz): - * - * frame_time = N / 48000 - * 0.005 s = N / 48000 - * N = 0.005 × 48000 = 240 samples - */ -#define SAMPLES_PER_FRAME 240 - -/* - * Number of analysis frames per second. - * - * Each increment in the frequency counters corresponds - * roughly to this many Hertz of tone frequency. - * - * With the default values: - * FRAME_RATE = 48000 / 240 = 200 Hz - */ -#define FRAME_RATE ((SAMPLE_RATE) / (SAMPLES_PER_FRAME)) - -/* - * Two FIR windows are maintained; one for data tone, - * and one for the separator tone. They are positioned - * one frame apart in the ring buffer. - */ -#define MAX_SAMPLES (2 * (SAMPLES_PER_FRAME)) - -/* - * Approx byte offset for ring buffer span, just for - * easier debug output correlating to the audio stream. - */ -#define SAMPLE_OFFSET ((MAX_SAMPLES) * (sizeof(short))) - -/* - * Expected tone ranges (approximate, derived from spkmodem). - * These values are intentionally wide because real-world setups - * often involve microphones, room acoustics, and cheap ADCs. - */ -#define SEP_TONE_MIN_HZ 1000 -#define SEP_TONE_MAX_HZ 3000 - -#define SEP_TOLERANCE_PULSES \ - (((SEP_TONE_MAX_HZ) - (SEP_TONE_MIN_HZ)) / (2 * (FRAME_RATE))) - -#define DATA_TONE_MIN_HZ 3000 -#define DATA_TONE_MAX_HZ 12000 - -/* Mid point used to distinguish the two data tones. */ -#define DATA_TONE_THRESHOLD_HZ 5000 - -/* - * Convert tone frequency ranges into pulse counts within the - * sliding analysis window. - * - * pulse_count = tone_frequency / FRAME_RATE - * where FRAME_RATE = SAMPLE_RATE / SAMPLES_PER_FRAME. - */ -#define FREQ_SEP_MIN ((SEP_TONE_MIN_HZ) / (FRAME_RATE)) -#define FREQ_SEP_MAX ((SEP_TONE_MAX_HZ) / (FRAME_RATE)) - -#define FREQ_DATA_MIN ((DATA_TONE_MIN_HZ) / (FRAME_RATE)) -#define FREQ_DATA_MAX ((DATA_TONE_MAX_HZ) / (FRAME_RATE)) - -#define FREQ_DATA_THRESHOLD ((DATA_TONE_THRESHOLD_HZ) / (FRAME_RATE)) - -/* - * These determine how long the program will wait during - * tone auto-detection, before shifting to defaults. - * It is done every LEARN_FRAMES number of frames. - */ -#define LEARN_SECONDS 1 -#define LEARN_FRAMES ((LEARN_SECONDS) * (FRAME_RATE)) - -/* - * Sample amplitude threshold used to convert the waveform - * into a pulse stream. Values near zero are regarded as noise. - */ -#define THRESHOLD 500 - -#define READ_BUF 4096 - -struct decoder_state { - unsigned char pulse[MAX_SAMPLES]; - - signed short inbuf[READ_BUF]; - size_t inpos; - size_t inlen; - - int ringpos; - int sep_pos; - - /* - * Sliding window pulse counters - * used to detect modem tones - */ - int freq_data; - int freq_separator; - int sample_count; - - int ascii_bit; - unsigned char ascii; - - int debug; - int swap_bytes; - - /* dynamic separator calibration */ - int sep_sum; - int sep_samples; - int sep_min; - int sep_max; - - /* for automatic tone detection */ - int freq_min; - int freq_max; - int freq_threshold; - int learn_frames; - - /* previous sample used for edge detection */ - signed short prev_sample; -}; - -static const char *argv0; - -/* - * 16-bit little endian words are read - * continuously. we will swap them, if - * the host cpu is big endian. - */ -static int host_is_big_endian(void); - -/* main loop */ -static void handle_audio(struct decoder_state *st); - -/* separate tone tolerances */ -static void select_separator_tone(struct decoder_state *st); -static int is_valid_signal(struct decoder_state *st); - -/* output to terminal */ -static int set_ascii_bit(struct decoder_state *st); -static void print_char(struct decoder_state *st); -static void reset_char(struct decoder_state *st); - -/* process samples/frames */ -static void decode_pulse(struct decoder_state *st); -static signed short read_sample(struct decoder_state *st); -static void read_words(struct decoder_state *st); - -/* continually adjust tone */ -static void detect_tone(struct decoder_state *st); -static int silent_signal(struct decoder_state *st); -static void select_low_tone(struct decoder_state *st); - -/* debug */ -static void print_stats(struct decoder_state *st); - -/* error handling / usage */ -static void err(int errval, const char *msg, ...); -static void usage(void); -static const char *progname(void); - -/* portability (old systems) */ -int getopt(int, char * const *, const char *); -extern char *optarg; -extern int optind; -extern int opterr; -extern int optopt; - -#ifndef CHAR_BIT -#define CHAR_BIT 8 -#endif - -typedef char static_assert_char_is_8_bits[(CHAR_BIT == 8) ? 1 : -1]; -typedef char static_assert_char_is_1[(sizeof(char) == 1) ? 1 : -1]; -typedef char static_assert_short[(sizeof(short) == 2) ? 1 : -1]; -typedef char static_assert_int_is_4[(sizeof(int) >= 4) ? 1 : -1]; -typedef char static_assert_twos_complement[ - ((-1 & 3) == 3) ? 1 : -1 -]; - -int -main(int argc, char **argv) -{ - struct decoder_state st; - int c; - - argv0 = argv[0]; - -#if defined (__OpenBSD__) && defined(OpenBSD) -#if OpenBSD >= 509 - if (pledge("stdio", NULL) == -1) - err(errno, "pledge"); -#endif -#endif - - memset(&st, 0, sizeof(st)); - - while ((c = getopt(argc, argv, "d")) != -1) { - if (c != 'd') - usage(); - st.debug = 1; - break; - } - - /* fallback in case tone detection fails */ - st.freq_min = 100000; - st.freq_max = 0; - st.freq_threshold = FREQ_DATA_THRESHOLD; - - /* - * Used for separator calibration - */ - st.sep_min = FREQ_SEP_MIN; - st.sep_max = FREQ_SEP_MAX; - - st.ascii_bit = 7; - - st.ringpos = 0; - st.sep_pos = SAMPLES_PER_FRAME; - - if (host_is_big_endian()) - st.swap_bytes = 1; - - setvbuf(stdout, NULL, _IONBF, 0); - - for (;;) - handle_audio(&st); - - return EXIT_SUCCESS; -} - -static int -host_is_big_endian(void) -{ - unsigned int x = 1; - return (*(unsigned char *)&x == 0); -} - -static void -handle_audio(struct decoder_state *st) -{ - int sample; - - /* - * If the modem signal disappears for several (read: 3) - * frames, discard the partially assembled character. - */ - if (st->sample_count >= (3 * SAMPLES_PER_FRAME) || - st->freq_separator <= 0) - reset_char(st); - - st->sample_count = 0; - - /* process exactly one frame */ - for (sample = 0; sample < SAMPLES_PER_FRAME; sample++) - decode_pulse(st); - - select_separator_tone(st); - - if (set_ascii_bit(st) < 0) - print_char(st); - - /* Detect tone per each frame */ - detect_tone(st); -} - -/* - * collect separator tone statistics - * (and auto-adjust tolerances) - */ -static void -select_separator_tone(struct decoder_state *st) -{ - int avg; - - if (!is_valid_signal(st)) - return; - - st->sep_sum += st->freq_separator; - st->sep_samples++; - - if (st->sep_samples != 50) - return; - - avg = st->sep_sum / st->sep_samples; - - st->sep_min = avg - SEP_TOLERANCE_PULSES; - st->sep_max = avg + SEP_TOLERANCE_PULSES; - - /* reset calibration accumulators */ - st->sep_sum = 0; - st->sep_samples = 0; - - if (st->debug) - printf("separator calibrated: %dHz\n", - avg * FRAME_RATE); -} - -/* - * Verify that the observed pulse densities fall within the - * expected ranges for spkmodem tones. This prevents random noise - * from being misinterpreted as data. - */ -static int -is_valid_signal(struct decoder_state *st) -{ - if (st->freq_data <= 0) - return 0; - - if (st->freq_separator < st->sep_min || - st->freq_separator > st->sep_max) - return 0; - - return 1; -} - -/* - * Each validated frame contributes one bit of modem data. - * Bits are accumulated MSB-first into the ASCII byte. - */ -static int -set_ascii_bit(struct decoder_state *st) -{ - if (st->debug) - print_stats(st); - - if (!is_valid_signal(st)) - return st->ascii_bit; - - if (st->freq_data < st->freq_threshold) - st->ascii |= (1 << st->ascii_bit); - - st->ascii_bit--; - - return st->ascii_bit; -} - -static void -print_char(struct decoder_state *st) -{ - if (st->debug) - printf("<%c,%x>", st->ascii, st->ascii); - else - putchar(st->ascii); - - reset_char(st); -} - -static void -reset_char(struct decoder_state *st) -{ - st->ascii = 0; - st->ascii_bit = 7; -} - -/* - * Main demodulation step (moving-sum FIR filter). - */ -static void -decode_pulse(struct decoder_state *st) -{ - unsigned char old_ring, old_sep; - unsigned char new_pulse; - signed short sample; - int ringpos; - int sep_pos; - int diff_edge; - int diff_amp; - - ringpos = st->ringpos; - sep_pos = st->sep_pos; - - /* - * Sliding rectangular FIR (Finite Impulse Response) filter. - * - * After thresholding, the signal becomes a stream of 0/1 pulses. - * The decoder measures pulse density over two windows: - * - * freq_data: pulses in the "data" window - * freq_separator: pulses in the "separator" window - * - * Instead of calculating each window every time (O(N) per frame), we - * update the window sums incrementally: - * - * sum_new = sum_old - pulse_leaving + pulse_entering - * - * This keeps the filter O(1) per sample instead of O(N). - * The technique is equivalent to a rectangular FIR filter - * implemented as a sliding moving sum. - * - * The two windows are exactly SAMPLES_PER_FRAME apart in the ring - * buffer, so the pulse leaving the data window is simultaneously - * entering the separator window. - */ - old_ring = st->pulse[ringpos]; - old_sep = st->pulse[sep_pos]; - st->freq_data -= old_ring; - st->freq_data += old_sep; - st->freq_separator -= old_sep; - - sample = read_sample(st); - - /* - * Avoid startup edge. Since - * it's zero at startup, this - * may wrongly produce a pulse - */ - if (st->sample_count == 0) - st->prev_sample = sample; - - /* - * Detect edges instead of amplitude. - * This is more tolerant of weak microphones - * and speaker distortion.. - * - * However, we check both slope edges and - * amplitude, to mitagate noise. - */ - diff_amp = sample; - diff_edge = sample - st->prev_sample; - if (diff_edge < 0) - diff_edge = -diff_edge; - if (diff_amp < 0) - diff_amp = -diff_amp; - if (diff_edge > THRESHOLD && - diff_amp > THRESHOLD) - new_pulse = 1; - else - new_pulse = 0; - st->prev_sample = sample; - - st->pulse[ringpos] = new_pulse; - st->freq_separator += new_pulse; - - /* - * Advance both FIR windows through the ring buffer. - * The separator window always stays one frame ahead - * of the data window. - */ - if (++ringpos >= MAX_SAMPLES) - ringpos = 0; - if (++sep_pos >= MAX_SAMPLES) - sep_pos = 0; - - st->ringpos = ringpos; - st->sep_pos = sep_pos; - - st->sample_count++; -} - -static signed short -read_sample(struct decoder_state *st) -{ - signed short sample; - unsigned short u; - - while (st->inpos >= st->inlen) - read_words(st); - - sample = st->inbuf[st->inpos++]; - - if (st->swap_bytes) { - u = (unsigned short)sample; - u = (u >> 8) | (u << 8); - - sample = (signed short)u; - } - - return sample; -} - -static void -read_words(struct decoder_state *st) -{ - size_t n; - - n = fread(st->inbuf, sizeof(st->inbuf[0]), - READ_BUF, stdin); - - if (n != 0) { - st->inpos = 0; - st->inlen = n; - - return; - } - - if (ferror(stdin)) - err(errno, "stdin read"); - if (feof(stdin)) - exit(EXIT_SUCCESS); -} - -/* - * Automatically detect spkmodem tone - */ -static void -detect_tone(struct decoder_state *st) -{ - if (st->learn_frames >= LEARN_FRAMES) - return; - - st->learn_frames++; - - if (silent_signal(st)) - return; - - select_low_tone(st); - - if (st->learn_frames != LEARN_FRAMES) - return; - - /* - * If the observed frequencies are too close, - * learning likely failed (only one tone seen). - * Keep the default threshold. - */ - if (st->freq_max - st->freq_min < 2) - return; - - st->freq_threshold = - (st->freq_min + st->freq_max) / 2; - - if (st->debug) - printf("auto threshold: %dHz\n", - st->freq_threshold * FRAME_RATE); -} - -/* - * Ignore silence / near silence. - * Both FIR windows will be near zero when no signal exists. - */ -static int -silent_signal(struct decoder_state *st) -{ - return (st->freq_data <= 2 && - st->freq_separator <= 2); -} - -/* - * Choose the lowest active tone. - * Separator frames carry tone in the separator window, - * data frames carry tone in the data window. - */ -static void -select_low_tone(struct decoder_state *st) -{ - int f; - - f = st->freq_data; - - if (f <= 0 || (st->freq_separator > 0 && - st->freq_separator < f)) - f = st->freq_separator; - - if (f <= 0) - return; - - if (f < st->freq_min) - st->freq_min = f; - - if (f > st->freq_max) - st->freq_max = f; -} - -static void -print_stats(struct decoder_state *st) -{ - long pos; - - int data_hz = st->freq_data * FRAME_RATE; - int sep_hz = st->freq_separator * FRAME_RATE; - int sep_hz_min = st->sep_min * FRAME_RATE; - int sep_hz_max = st->sep_max * FRAME_RATE; - - if ((pos = ftell(stdin)) == -1) { - printf("%d %d %d data=%dHz sep=%dHz(min %dHz %dHz)\n", - st->freq_data, - st->freq_separator, - st->freq_threshold, - data_hz, - sep_hz, - sep_hz_min, - sep_hz_max); - return; - } - - printf("%d %d %d @%ld data=%dHz sep=%dHz(min %dHz %dHz)\n", - st->freq_data, - st->freq_separator, - st->freq_threshold, - pos - SAMPLE_OFFSET, - data_hz, - sep_hz, - sep_hz_min, - sep_hz_max); -} - -static void -err(int errval, const char *msg, ...) -{ - va_list ap; - - fprintf(stderr, "%s: ", progname()); - - va_start(ap, msg); - vfprintf(stderr, msg, ap); - va_end(ap); - - fprintf(stderr, ": %s\n", strerror(errval)); - exit(EXIT_FAILURE); -} - -static void -usage(void) -{ - fprintf(stderr, "usage: %s [-d]\n", progname()); - exit(EXIT_FAILURE); -} - -static const char * -progname(void) -{ - const char *p; - - if (argv0 == NULL || *argv0 == '\0') - return ""; - - p = strrchr(argv0, '/'); - - if (p) - return p + 1; - else - return argv0; -} diff --git a/util/spkmodem_recv/.gitignore b/util/spkmodem_recv/.gitignore new file mode 100644 index 00000000..2f5c946c --- /dev/null +++ b/util/spkmodem_recv/.gitignore @@ -0,0 +1 @@ +spkmodem-recv diff --git a/util/spkmodem_recv/Makefile b/util/spkmodem_recv/Makefile new file mode 100644 index 00000000..3c5dc51f --- /dev/null +++ b/util/spkmodem_recv/Makefile @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +CC?=cc +CFLAGS?=-Os -Wall -Wextra -Werror -pedantic +DESTDIR?= +PREFIX?=/usr/local +INSTALL?=install + +spkmodem-recv: + $(CC) $(CFLAGS) -o $@ $@.c +install: spkmodem-recv + $(INSTALL) -d $(DESTDIR)$(PREFIX)/bin/ + $(INSTALL) $< -t $(DESTDIR)$(PREFIX)/bin/ +clean: + rm -f spkmodem-recv diff --git a/util/spkmodem_recv/spkmodem-recv.c b/util/spkmodem_recv/spkmodem-recv.c new file mode 100644 index 00000000..4467282d --- /dev/null +++ b/util/spkmodem_recv/spkmodem-recv.c @@ -0,0 +1,124 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* SPDX-FileCopyrightText: 2013 Free Software Foundation, Inc. */ +/* Usage: parec --channels=1 --rate=48000 --format=s16le | ./spkmodem-recv */ + +/* Forked from coreboot's version, at util/spkmodem_recv/ in coreboot.git, + * revision 5c2b5fcf2f9c9259938fd03cfa3ea06b36a007f0 as of 3 January 2022. + * This version is heavily modified, re-written based on OpenBSD Kernel Source + * File Style Guide (KNF); this change is Copyright 2023 Leah Rowe. */ + +#include <err.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define SAMPLES_PER_FRAME 240 +#define MAX_SAMPLES (2 * SAMPLES_PER_FRAME) +#define FREQ_SEP_MIN 5 +#define FREQ_SEP_MAX 15 +#define FREQ_DATA_MIN 15 +#define FREQ_DATA_THRESHOLD 25 +#define FREQ_DATA_MAX 60 +#define THRESHOLD 500 + +#define ERR() (errno = errno ? errno : ECANCELED) +#define reset_char() ascii = 0, ascii_bit = 7 + +signed short frame[MAX_SAMPLES], pulse[MAX_SAMPLES]; +int ringpos, debug, freq_data, freq_separator, sample_count, ascii_bit = 7; +char ascii = 0; + +void handle_audio(void); +void decode_pulse(void); +int set_ascii_bit(void); +void print_char(void); +void print_stats(void); + +int +main(int argc, char *argv[]) +{ + int c; +#ifdef __OpenBSD__ + if (pledge("stdio", NULL) == -1) + err(ERR(), "pledge"); +#endif + while ((c = getopt(argc, argv, "d")) != -1) + if (!(debug = (c == 'd'))) + err(errno = EINVAL, NULL); + setvbuf(stdout, NULL, _IONBF, 0); + while (!feof(stdin)) + handle_audio(); + if (errno && debug) + err(errno, "Unhandled error, errno %d", errno); + return errno; +} + +void +handle_audio(void) +{ + if (sample_count > (3 * SAMPLES_PER_FRAME)) + sample_count = reset_char(); + if ((freq_separator <= FREQ_SEP_MIN) || (freq_separator >= FREQ_SEP_MAX) + || (freq_data <= FREQ_DATA_MIN) || (freq_data >= FREQ_DATA_MAX)) { + decode_pulse(); + return; + } + + if (set_ascii_bit() < 0) + print_char(); + sample_count = 0; + for (int sample = 0; sample < SAMPLES_PER_FRAME; sample++) + decode_pulse(); +} + +void +decode_pulse(void) +{ + int next_ringpos = (ringpos + SAMPLES_PER_FRAME) % MAX_SAMPLES; + freq_data -= pulse[ringpos]; + freq_data += pulse[next_ringpos]; + freq_separator -= pulse[next_ringpos]; + + fread(frame + ringpos, 1, sizeof(frame[0]), stdin); + if (ferror(stdin) != 0) + err(ERR(), "Could not read from frame."); + + if ((pulse[ringpos] = (abs(frame[ringpos]) > THRESHOLD) ? 1 : 0)) + ++freq_separator; + ++ringpos; + ringpos %= MAX_SAMPLES; + ++sample_count; +} + +int +set_ascii_bit(void) +{ + if (debug) + print_stats(); + if (freq_data < FREQ_DATA_THRESHOLD) + ascii |= (1 << ascii_bit); + --ascii_bit; + return ascii_bit; +} + +void +print_char(void) +{ + if (debug) + printf("<%c, %x>", ascii, ascii); + else + printf("%c", ascii); + reset_char(); +} + +void +print_stats(void) +{ + long stdin_pos; + if ((stdin_pos = ftell(stdin)) == -1) + err(ERR(), NULL); + printf ("%d %d %d @%ld\n", freq_data, freq_separator, + FREQ_DATA_THRESHOLD, stdin_pos - sizeof(frame)); +} |
