commit 46af0781e76eb24632c98f60cbb80adae1012a46 Author: Steve Markgraf Date: Sun May 5 15:17:01 2024 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe2f604 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +Makefile +Makefile.in +.deps +.libs +*.o +*.lo +*.la +*.pc +aclocal.m4 +acinclude.m4 +aminclude.am +m4/*.m4 +autom4te.cache +config.h* +config.sub +config.log +config.status +config.guess +configure +configure~ +compile +depcomp +missing +ltmain.sh +install-sh +stamp-h1 +libtool +Doxyfile + +.tarball-version +.version + +.*.swp + +doc/ + +src/hsdaoh_file +src/hsdaoh_tcp +src/hsdaoh_test + +CMakeCache.txt +*/CMakeFiles +CMakeFiles +*.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a41dca4 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,225 @@ +# Copyright 2012-2020 Osmocom Project +# +# This file is part of hsdaoh +# +# 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 2 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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +######################################################################## +# Project setup +######################################################################## +cmake_minimum_required(VERSION 3.7.2) + +# workaround for https://gitlab.kitware.com/cmake/cmake/issues/16967 +if(${CMAKE_VERSION} VERSION_LESS "3.12.0") + project(hsdaoh) +else() + project(hsdaoh C) +endif() + +#select the release build type by default to get optimization flags +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release") + message(STATUS "Build type not specified: defaulting to release.") +endif(NOT CMAKE_BUILD_TYPE) + +list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/Modules) + +include(GNUInstallDirs) +include(GenerateExportHeader) +include(CMakePackageConfigHelpers) + +# Set the version information here +set(VERSION_INFO_MAJOR_VERSION 0) # increment major on api compatibility changes +set(VERSION_INFO_MINOR_VERSION 1) # increment minor on feature-level changes +set(VERSION_INFO_PATCH_VERSION 0) # increment patch for bug fixes and docs +include(Version) # setup version info + +######################################################################## +# Compiler specific setup +######################################################################## +if(CMAKE_COMPILER_IS_GNUCC AND NOT WIN32) + ADD_DEFINITIONS(-Wall) + ADD_DEFINITIONS(-Wextra) + ADD_DEFINITIONS(-Wno-unused-parameter) + ADD_DEFINITIONS(-Wno-unused) + ADD_DEFINITIONS(-Wsign-compare) + #http://gcc.gnu.org/wiki/Visibility + add_definitions(-fvisibility=hidden) +elseif(MSVC14 OR MSVC14) +#pthread-w32 issue, timespec is now part of time.h + ADD_DEFINITIONS(-D_TIMESPEC_DEFINED) +endif() + +######################################################################## +# Find build dependencies +######################################################################## +find_package(Threads) +find_package(PkgConfig) + +if(PKG_CONFIG_FOUND) + pkg_check_modules(LIBUSB libusb-1.0 IMPORTED_TARGET) + if(LIBUSB_LINK_LIBRARIES) + set(LIBUSB_LIBRARIES "${LIBUSB_LINK_LIBRARIES}") + endif() +else() + set(LIBUSB_LIBRARIES "" CACHE STRING "manual libusb path") + set(LIBUSB_INCLUDE_DIRS "" CACHE STRING "manual libusb includepath") +endif() + +if(PKG_CONFIG_FOUND) + pkg_check_modules(LIBUVC libuvc IMPORTED_TARGET) + if(LIBUVC_LINK_LIBRARIES) + set(LIBUVC_LIBRARIES "${LIBUVC_LINK_LIBRARIES}") + endif() +else() + set(LIBUVC_LIBRARIES "" CACHE STRING "manual libuvc path") + set(LIBUVC_INCLUDE_DIRS "" CACHE STRING "manual libuvc includepath") +endif() + +if(MSVC) + set(THREADS_PTHREADS_LIBRARY "" CACHE STRING "manual pthread-win32 path") + set(THREADS_PTHREADS_INCLUDE_DIR "" CACHE STRING "manual pthread-win32 includepath") +else() + set(THREADS_PTHREADS_LIBRARY "" CACHE INTERNAL "manual pthread-win32 path") + set(THREADS_PTHREADS_INCLUDE_DIR "" CACHE INTERNAL "manual pthread-win32 includepath") +endif() + +if(PKG_CONFIG_FOUND AND NOT LIBUSB_FOUND) + message(FATAL_ERROR "LibUSB 1.0 required to compile hsdaoh") +endif() +if(PKG_CONFIG_FOUND AND NOT LIBUVC_FOUND) + message(FATAL_ERROR "LibUVC required to compile hsdaoh") +endif() +if(NOT THREADS_FOUND) + message(FATAL_ERROR "pthreads(-win32) required to compile hsdaoh") +endif() + +######################################################################## +# Create uninstall target +######################################################################## +configure_file( + ${PROJECT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake +@ONLY) + +add_custom_target(uninstall + ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake +) + +######################################################################## +# Install udev rules +######################################################################## +option(INSTALL_UDEV_RULES "Install udev rules" ON) +if (INSTALL_UDEV_RULES) + install ( + FILES hsdaoh.rules + DESTINATION "/etc/udev/rules.d" + COMPONENT "udev" + ) +else (INSTALL_UDEV_RULES) + message (STATUS "Udev rules not being installed, install them with -DINSTALL_UDEV_RULES=ON") +endif (INSTALL_UDEV_RULES) + +######################################################################## +# Install public header files +######################################################################## +install(FILES + include/hsdaoh.h + include/hsdaoh_export.h + DESTINATION include +) + +######################################################################## +# Add subdirectories +######################################################################## +add_subdirectory(src) + +######################################################################## +# Create Pkg Config File +######################################################################## +FOREACH(inc ${LIBUSB_INCLUDEDIR}) + LIST(APPEND hsdaoh_PC_CFLAGS "-I${inc}") +ENDFOREACH(inc) + +FOREACH(lib ${LIBUSB_LIBRARY_DIRS}) + LIST(APPEND hsdaoh_PC_LIBS "-L${lib}") +ENDFOREACH(lib) + +FOREACH(inc ${LIBUVC_INCLUDEDIR}) + LIST(APPEND hsdaoh_PC_CFLAGS "-I${inc}") +ENDFOREACH(inc) + +FOREACH(lib ${LIBUVC_LIBRARY_DIRS}) + LIST(APPEND hsdaoh_PC_LIBS "-L${lib}") +ENDFOREACH(lib) + +# use space-separation format for the pc file +STRING(REPLACE ";" " " hsdaoh_PC_CFLAGS "${hsdaoh_PC_CFLAGS}") +STRING(REPLACE ";" " " hsdaoh_PC_LIBS "${hsdaoh_PC_LIBS}") + +# unset these vars to avoid hard-coded paths to cross environment +IF(CMAKE_CROSSCOMPILING) + UNSET(hsdaoh_PC_CFLAGS) + UNSET(hsdaoh_PC_LIBS) +ENDIF(CMAKE_CROSSCOMPILING) + +set(prefix "${CMAKE_INSTALL_PREFIX}") +set(exec_prefix \${prefix}) +set(includedir \${prefix}/include) +set(libdir \${exec_prefix}/lib) + +CONFIGURE_FILE( + ${CMAKE_CURRENT_SOURCE_DIR}/libhsdaoh.pc.in + ${CMAKE_CURRENT_BINARY_DIR}/libhsdaoh.pc +@ONLY) + +INSTALL( + FILES ${CMAKE_CURRENT_BINARY_DIR}/libhsdaoh.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig +) + +######################################################################## +# Create CMake Config File +######################################################################## +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/hsdaoh/hsdaohConfigVersion.cmake" + VERSION ${VERSION} + COMPATIBILITY AnyNewerVersion + ) + +configure_file(cmake/hsdaohConfig.cmake + "${CMAKE_CURRENT_BINARY_DIR}/hsdaoh/hsdaohConfig.cmake" + COPYONLY + ) + +set(ConfigPackageLocation lib/cmake/hsdaoh) +install(EXPORT HSDAOH-export + FILE hsdaohTargets.cmake + NAMESPACE hsdaoh:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hsdaoh/ + ) +install( + FILES + cmake/hsdaohConfig.cmake + "${CMAKE_CURRENT_BINARY_DIR}/hsdaoh/hsdaohConfigVersion.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hsdaoh/ + COMPONENT Devel + ) + +######################################################################## +# Print Summary +######################################################################## +MESSAGE(STATUS "Building for version: ${VERSION} / ${LIBVER}") +MESSAGE(STATUS "Using install prefix: ${CMAKE_INSTALL_PREFIX}") diff --git a/cmake/cmake_uninstall.cmake.in b/cmake/cmake_uninstall.cmake.in new file mode 100644 index 0000000..9ae1ae4 --- /dev/null +++ b/cmake/cmake_uninstall.cmake.in @@ -0,0 +1,32 @@ +# http://www.vtk.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F + +IF(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") + MESSAGE(FATAL_ERROR "Cannot find install manifest: \"@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt\"") +ENDIF(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") + +FILE(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files) +STRING(REGEX REPLACE "\n" ";" files "${files}") +FOREACH(file ${files}) + MESSAGE(STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"") + IF(EXISTS "$ENV{DESTDIR}${file}") + EXEC_PROGRAM( + "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" + OUTPUT_VARIABLE rm_out + RETURN_VALUE rm_retval + ) + IF(NOT "${rm_retval}" STREQUAL 0) + MESSAGE(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"") + ENDIF(NOT "${rm_retval}" STREQUAL 0) + ELSEIF(IS_SYMLINK "$ENV{DESTDIR}${file}") + EXEC_PROGRAM( + "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" + OUTPUT_VARIABLE rm_out + RETURN_VALUE rm_retval + ) + IF(NOT "${rm_retval}" STREQUAL 0) + MESSAGE(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"") + ENDIF(NOT "${rm_retval}" STREQUAL 0) + ELSE(EXISTS "$ENV{DESTDIR}${file}") + MESSAGE(STATUS "File \"$ENV{DESTDIR}${file}\" does not exist.") + ENDIF(EXISTS "$ENV{DESTDIR}${file}") +ENDFOREACH(file) diff --git a/git-version-gen b/git-version-gen new file mode 100755 index 0000000..42cf3d2 --- /dev/null +++ b/git-version-gen @@ -0,0 +1,151 @@ +#!/bin/sh +# Print a version string. +scriptversion=2010-01-28.01 + +# Copyright (C) 2007-2010 Free Software Foundation, Inc. +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# This script is derived from GIT-VERSION-GEN from GIT: http://git.or.cz/. +# It may be run two ways: +# - from a git repository in which the "git describe" command below +# produces useful output (thus requiring at least one signed tag) +# - from a non-git-repo directory containing a .tarball-version file, which +# presumes this script is invoked like "./git-version-gen .tarball-version". + +# In order to use intra-version strings in your project, you will need two +# separate generated version string files: +# +# .tarball-version - present only in a distribution tarball, and not in +# a checked-out repository. Created with contents that were learned at +# the last time autoconf was run, and used by git-version-gen. Must not +# be present in either $(srcdir) or $(builddir) for git-version-gen to +# give accurate answers during normal development with a checked out tree, +# but must be present in a tarball when there is no version control system. +# Therefore, it cannot be used in any dependencies. GNUmakefile has +# hooks to force a reconfigure at distribution time to get the value +# correct, without penalizing normal development with extra reconfigures. +# +# .version - present in a checked-out repository and in a distribution +# tarball. Usable in dependencies, particularly for files that don't +# want to depend on config.h but do want to track version changes. +# Delete this file prior to any autoconf run where you want to rebuild +# files to pick up a version string change; and leave it stale to +# minimize rebuild time after unrelated changes to configure sources. +# +# It is probably wise to add these two files to .gitignore, so that you +# don't accidentally commit either generated file. +# +# Use the following line in your configure.ac, so that $(VERSION) will +# automatically be up-to-date each time configure is run (and note that +# since configure.ac no longer includes a version string, Makefile rules +# should not depend on configure.ac for version updates). +# +# AC_INIT([GNU project], +# m4_esyscmd([build-aux/git-version-gen .tarball-version]), +# [bug-project@example]) +# +# Then use the following lines in your Makefile.am, so that .version +# will be present for dependencies, and so that .tarball-version will +# exist in distribution tarballs. +# +# BUILT_SOURCES = $(top_srcdir)/.version +# $(top_srcdir)/.version: +# echo $(VERSION) > $@-t && mv $@-t $@ +# dist-hook: +# echo $(VERSION) > $(distdir)/.tarball-version + +case $# in + 1) ;; + *) echo 1>&2 "Usage: $0 \$srcdir/.tarball-version"; exit 1;; +esac + +tarball_version_file=$1 +nl=' +' + +# First see if there is a tarball-only version file. +# then try "git describe", then default. +if test -f $tarball_version_file +then + v=`cat $tarball_version_file` || exit 1 + case $v in + *$nl*) v= ;; # reject multi-line output + [0-9]*) ;; + *) v= ;; + esac + test -z "$v" \ + && echo "$0: WARNING: $tarball_version_file seems to be damaged" 1>&2 +fi + +if test -n "$v" +then + : # use $v +elif + v=`git describe --abbrev=4 --match='v*' HEAD 2>/dev/null \ + || git describe --abbrev=4 HEAD 2>/dev/null` \ + && case $v in + [0-9]*) ;; + v[0-9]*) ;; + *) (exit 1) ;; + esac +then + # Is this a new git that lists number of commits since the last + # tag or the previous older version that did not? + # Newer: v6.10-77-g0f8faeb + # Older: v6.10-g0f8faeb + case $v in + *-*-*) : git describe is okay three part flavor ;; + *-*) + : git describe is older two part flavor + # Recreate the number of commits and rewrite such that the + # result is the same as if we were using the newer version + # of git describe. + vtag=`echo "$v" | sed 's/-.*//'` + numcommits=`git rev-list "$vtag"..HEAD | wc -l` + v=`echo "$v" | sed "s/\(.*\)-\(.*\)/\1-$numcommits-\2/"`; + ;; + esac + + # Change the first '-' to a '.', so version-comparing tools work properly. + # Remove the "g" in git describe's output string, to save a byte. + v=`echo "$v" | sed 's/-/./;s/\(.*\)-g/\1-/'`; +else + v=UNKNOWN +fi + +v=`echo "$v" |sed 's/^v//'` + +# Don't declare a version "dirty" merely because a time stamp has changed. +git status > /dev/null 2>&1 + +dirty=`sh -c 'git diff-index --name-only HEAD' 2>/dev/null` || dirty= +case "$dirty" in + '') ;; + *) # Append the suffix only if there isn't one already. + case $v in + *-dirty) ;; + *) v="$v-dirty" ;; + esac ;; +esac + +# Omit the trailing newline, so that m4_esyscmd can use the result directly. +echo "$v" | tr -d '\012' + +# Local variables: +# eval: (add-hook 'write-file-hooks 'time-stamp) +# time-stamp-start: "scriptversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-end: "$" +# End: diff --git a/hsdaoh.rules b/hsdaoh.rules new file mode 100644 index 0000000..fee3dec --- /dev/null +++ b/hsdaoh.rules @@ -0,0 +1,25 @@ +# +# Copyright 2024 Steve Markgraf +# +# 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 2 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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# MS2130 +SUBSYSTEMS=="usb", ATTRS{idVendor}=="345f", ATTRS{idProduct}=="2130", ENV{ID_SOFTWARE_RADIO}="1", MODE="0660", GROUP="plugdev" + +# MS2130 OEM +SUBSYSTEMS=="usb", ATTRS{idVendor}=="534d", ATTRS{idProduct}=="2130", ENV{ID_SOFTWARE_RADIO}="1", MODE="0660", GROUP="plugdev" + +# MS2131 +SUBSYSTEMS=="usb", ATTRS{idVendor}=="345f", ATTRS{idProduct}=="2131", ENV{ID_SOFTWARE_RADIO}="1", MODE="0660", GROUP="plugdev" diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt new file mode 100644 index 0000000..8144cfb --- /dev/null +++ b/include/CMakeLists.txt @@ -0,0 +1,27 @@ +# Copyright 2012 OSMOCOM Project +# +# This file is part of hsdaoh +# +# GNU Radio 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, or (at your option) +# any later version. +# +# GNU Radio 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. +# +# You should have received a copy of the GNU General Public License +# along with GNU Radio; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. + +######################################################################## +# Install public header files +######################################################################## +install(FILES + hsdaoh.h + hsdaoh_export.h + DESTINATION include +) diff --git a/include/hsdaoh.h b/include/hsdaoh.h new file mode 100644 index 0000000..0663fa8 --- /dev/null +++ b/include/hsdaoh.h @@ -0,0 +1,137 @@ +/* + * hsdaoh - High Speed Data Acquisition over MS213x USB3 HDMI capture sticks + * + * Copyright (C) 2024 by Steve Markgraf + * + * based on librtlsdr: + * Copyright (C) 2012-2024 by Steve Markgraf + * Copyright (C) 2012 by Dimitri Stolnikov + * + * SPDX-License-Identifier: GPL-2.0+ + * + * 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 2 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __HSDAOH_H +#define __HSDAOH_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +typedef struct hsdaoh_dev hsdaoh_dev_t; + +HSDAOH_API uint32_t hsdaoh_get_device_count(void); + +HSDAOH_API const char* hsdaoh_get_device_name(uint32_t index); + +/*! + * Get USB device strings. + * + * NOTE: The string arguments must provide space for up to 256 bytes. + * + * \param index the device index + * \param manufact manufacturer name, may be NULL + * \param product product name, may be NULL + * \param serial serial number, may be NULL + * \return 0 on success + */ +HSDAOH_API int hsdaoh_get_device_usb_strings(uint32_t index, + char *manufact, + char *product, + char *serial); + +/*! + * Get device index by USB serial string descriptor. + * + * \param serial serial string of the device + * \return device index of first device where the name matched + * \return -1 if name is NULL + * \return -2 if no devices were found at all + * \return -3 if devices were found, but none with matching name + */ +HSDAOH_API int hsdaoh_get_index_by_serial(const char *serial); + +HSDAOH_API int hsdaoh_open(hsdaoh_dev_t **dev, uint32_t index); + +HSDAOH_API int hsdaoh_close(hsdaoh_dev_t *dev); + +/* configuration functions */ + +/*! + * Get USB device strings. + * + * NOTE: The string arguments must provide space for up to 256 bytes. + * + * \param dev the device handle given by hsdaoh_open() + * \param manufact manufacturer name, may be NULL + * \param product product name, may be NULL + * \param serial serial number, may be NULL + * \return 0 on success + */ +HSDAOH_API int hsdaoh_get_usb_strings(hsdaoh_dev_t *dev, char *manufact, + char *product, char *serial); + +/*! + * Set the sample rate for the device + * + * \param dev the device handle given by hsdaoh_open() + * \param samp_rate the sample rate to be set + * \param ext_clock if true, use the IFCLK input insteafd of internal clock source + * if a Si5351 is connected, it will be configured + * \return 0 on success, -EINVAL on invalid rate + */ +HSDAOH_API int hsdaoh_set_sample_rate(hsdaoh_dev_t *dev, uint32_t rate, bool ext_clock); + +/*! + * Get actual sample rate the device is configured to. + * + * \param dev the device handle given by hsdaoh_open() + * \return 0 on error, sample rate in Hz otherwise + */ +HSDAOH_API uint32_t hsdaoh_get_sample_rate(hsdaoh_dev_t *dev); + +/* streaming functions */ + +typedef void(*hsdaoh_read_cb_t)(unsigned char *buf, uint32_t len, void *ctx); + +/*! + * Start streaming data from the device. + * + * \param dev the device handle given by hsdaoh_open() + * \param cb callback function to return received data + * \param ctx user specific context to pass via the callback function + * \return 0 on success + */ +HSDAOH_API int hsdaoh_start_stream(hsdaoh_dev_t *dev, + hsdaoh_read_cb_t cb, + void *ctx); + +/*! + * Stop streaming data from the device. + * + * \param dev the device handle given by hsdaoh_open() + * \return 0 on success + */ +HSDAOH_API int hsdaoh_stop_stream(hsdaoh_dev_t *dev); + +#ifdef __cplusplus +} +#endif + +#endif /* __HSDAOH_H */ diff --git a/include/hsdaoh_export.h b/include/hsdaoh_export.h new file mode 100644 index 0000000..197b2f1 --- /dev/null +++ b/include/hsdaoh_export.h @@ -0,0 +1,48 @@ +/* + * hsdaoh - High Speed Data Acquisition over MS213x USB3 HDMI capture sticks + * + * Copyright (C) 2012 by Hoernchen + * + * 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 2 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef HSDAOH_EXPORT_H +#define HSDAOH_EXPORT_H + +#if defined __GNUC__ +# if __GNUC__ >= 4 +# define __HSDAOH_EXPORT __attribute__((visibility("default"))) +# define __HSDAOH_IMPORT __attribute__((visibility("default"))) +# else +# define __HSDAOH_EXPORT +# define __HSDAOH_IMPORT +# endif +#elif _MSC_VER +# define __HSDAOH_EXPORT __declspec(dllexport) +# define __HSDAOH_IMPORT __declspec(dllimport) +#else +# define __HSDAOH_EXPORT +# define __HSDAOH_IMPORT +#endif + +#ifndef hsdaoh_STATIC +# ifdef hsdaoh_EXPORTS +# define HSDAOH_API __HSDAOH_EXPORT +# else +# define HSDAOH_API __HSDAOH_IMPORT +# endif +#else +#define HSDAOH_API +#endif +#endif /* HSDAOH_EXPORT_H */ diff --git a/include/hsdaoh_i2c.h b/include/hsdaoh_i2c.h new file mode 100644 index 0000000..89d8193 --- /dev/null +++ b/include/hsdaoh_i2c.h @@ -0,0 +1,7 @@ +#ifndef __I2C_H +#define __I2C_H + +int hsdaoh_i2c_write_fn(void *dev, uint8_t i2c_addr, uint8_t *buffer, uint16_t len); +int hsdaoh_i2c_read_fn(void *dev, uint8_t i2c_addr, uint8_t *buffer, uint16_t len); + +#endif diff --git a/libhsdaoh.pc.in b/libhsdaoh.pc.in new file mode 100644 index 0000000..f4bee39 --- /dev/null +++ b/libhsdaoh.pc.in @@ -0,0 +1,11 @@ +prefix=@prefix@ +exec_prefix=@exec_prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: hsdaoh Library +Description: C Utility Library +Version: @VERSION@ +Cflags: -I${includedir}/ +Libs: -L${libdir} -lhsdaoh +Libs.private: -lusb-1.0 @HSDAOH_PC_LIBS@ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..a493583 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,128 @@ +# Copyright 2012-2024 Osmocom Project +# +# This file is part of hsdaoh +# +# 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 2 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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +######################################################################## +# Setup shared library variant +######################################################################## +add_library(hsdaoh SHARED libhsdaoh.c) +target_link_libraries(hsdaoh ${LIBUSB_LIBRARIES} ${LIBUVC_LIBRARIES} ${THREADS_PTHREADS_LIBRARY}) +target_include_directories(hsdaoh PUBLIC + $ + $ # /include + ${LIBUSB_INCLUDE_DIRS} + ${LIBUVC_INCLUDE_DIRS} + ${THREADS_PTHREADS_INCLUDE_DIR} + ) +set_target_properties(hsdaoh PROPERTIES DEFINE_SYMBOL "hsdaoh_EXPORTS") +set_target_properties(hsdaoh PROPERTIES OUTPUT_NAME hsdaoh) +set_target_properties(hsdaoh PROPERTIES SOVERSION ${MAJOR_VERSION}) +set_target_properties(hsdaoh PROPERTIES VERSION ${LIBVER}) +generate_export_header(hsdaoh) + +######################################################################## +# Setup static library variant +######################################################################## +add_library(hsdaoh_static STATIC libhsdaoh.c) +target_link_libraries(hsdaoh m ${LIBUSB_LIBRARIES} ${LIBUVC_LIBRARIES} ${THREADS_PTHREADS_LIBRARY}) +target_include_directories(hsdaoh_static PUBLIC + $ + $ # /include + ${LIBUSB_INCLUDE_DIRS} + ${LIBUVC_INCLUDE_DIRS} + ${THREADS_PTHREADS_INCLUDE_DIR} + ) +set_property(TARGET hsdaoh_static APPEND PROPERTY COMPILE_DEFINITIONS "hsdaoh_STATIC" ) +if(NOT WIN32) +# Force same library filename for static and shared variants of the library +set_target_properties(hsdaoh_static PROPERTIES OUTPUT_NAME hsdaoh) +endif() +generate_export_header(hsdaoh_static) + +######################################################################## +# Set up Windows DLL resource files +######################################################################## +IF(MSVC) + include(${CMAKE_SOURCE_DIR}/cmake/Modules/Version.cmake) + + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/hsdaoh.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/hsdaoh.rc + @ONLY) + target_sources(hsdaoh PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/hsdaoh.rc) + target_sources(hsdaoh_static PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/hsdaoh.rc) +ENDIF(MSVC) + +######################################################################## +# Setup libraries used in executables +######################################################################## +if(WIN32) +add_library(libgetopt_static STATIC + getopt/getopt.c +) +target_link_libraries( + hsdaoh +) +endif() + +######################################################################## +# Build utility +######################################################################## +add_executable(hsdaoh_file hsdaoh_file.c) +add_executable(hsdaoh_tcp hsdaoh_tcp.c) +add_executable(hsdaoh_test hsdaoh_test.c) +set(INSTALL_TARGETS hsdaoh hsdaoh_static hsdaoh_file hsdaoh_tcp hsdaoh_test) + +target_link_libraries(hsdaoh_file hsdaoh +# ${LIBUSB_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} +) +target_link_libraries(hsdaoh_tcp hsdaoh +# ${LIBUSB_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} +) +target_link_libraries(hsdaoh_test hsdaoh +# ${LIBUSB_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} +) +if(UNIX) +if(APPLE OR CMAKE_SYSTEM MATCHES "OpenBSD") + target_link_libraries(hsdaoh_test m) +else() + target_link_libraries(hsdaoh_test m rt) +endif() +endif() + +if(WIN32) +target_link_libraries(hsdaoh_file libgetopt_static) +target_link_libraries(hsdaoh_tcp ws2_32 libgetopt_static) +target_link_libraries(hsdaoh_test libgetopt_static) +set_property(TARGET hsdaoh_file APPEND PROPERTY COMPILE_DEFINITIONS "hsdaoh_STATIC" ) +set_property(TARGET hsdaoh_tcp APPEND PROPERTY COMPILE_DEFINITIONS "hsdaoh_STATIC" ) +set_property(TARGET hsdaoh_test APPEND PROPERTY COMPILE_DEFINITIONS "hsdaoh_STATIC" ) +endif() +######################################################################## +# Install built library files & utilities +######################################################################## +install(TARGETS hsdaoh EXPORT HSDAOH-export + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} # .so/.dylib file + ) +install(TARGETS hsdaoh_static EXPORT HSDAOH-export + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} # .so/.dylib file + ) +install(TARGETS hsdaoh_file hsdaoh_tcp hsdaoh_test + DESTINATION ${CMAKE_INSTALL_BINDIR} + ) diff --git a/src/getopt/getopt.c b/src/getopt/getopt.c new file mode 100644 index 0000000..f1d461a --- /dev/null +++ b/src/getopt/getopt.c @@ -0,0 +1,1059 @@ +/* Getopt for GNU. + NOTE: getopt is now part of the C library, so if you don't know what + "Keep this file name-space clean" means, talk to drepper@gnu.org + before changing it! + Copyright (C) 1987,88,89,90,91,92,93,94,95,96,98,99,2000,2001 + Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, write to the Free + Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + 02111-1307 USA. */ +/* This tells Alpha OSF/1 not to define a getopt prototype in . + Ditto for AIX 3.2 and . */ +#ifndef _NO_PROTO +# define _NO_PROTO +#endif + +#ifdef HAVE_CONFIG_H +# include +#endif + +#if !defined __STDC__ || !__STDC__ +/* This is a separate conditional since some stdc systems + reject `defined (const)'. */ +# ifndef const +# define const +# endif +#endif + +#include + +/* Comment out all this code if we are using the GNU C Library, and are not + actually compiling the library itself. This code is part of the GNU C + Library, but also included in many other GNU distributions. Compiling + and linking in this code is a waste when using the GNU C library + (especially if it is a shared library). Rather than having every GNU + program understand `configure --with-gnu-libc' and omit the object files, + it is simpler to just do this in the source for each such file. */ + +#define GETOPT_INTERFACE_VERSION 2 +#if !defined _LIBC && defined __GLIBC__ && __GLIBC__ >= 2 +# include +# if _GNU_GETOPT_INTERFACE_VERSION == GETOPT_INTERFACE_VERSION +# define ELIDE_CODE +# endif +#endif + +#ifndef ELIDE_CODE + + +/* This needs to come after some library #include + to get __GNU_LIBRARY__ defined. */ +#ifdef __GNU_LIBRARY__ +/* Don't include stdlib.h for non-GNU C libraries because some of them + contain conflicting prototypes for getopt. */ +# include +# include +#endif /* GNU C library. */ + +#ifdef VMS +# include +# if HAVE_STRING_H - 0 +# include +# endif +#endif + +#ifndef _ +/* This is for other GNU distributions with internationalized messages. */ +# if (HAVE_LIBINTL_H && ENABLE_NLS) || defined _LIBC +# include +# ifndef _ +# define _(msgid) gettext (msgid) +# endif +# else +# define _(msgid) (msgid) +# endif +#endif + +/* This version of `getopt' appears to the caller like standard Unix `getopt' + but it behaves differently for the user, since it allows the user + to intersperse the options with the other arguments. + + As `getopt' works, it permutes the elements of ARGV so that, + when it is done, all the options precede everything else. Thus + all application programs are extended to handle flexible argument order. + + Setting the environment variable POSIXLY_CORRECT disables permutation. + Then the behavior is completely standard. + + GNU application programs can use a third alternative mode in which + they can distinguish the relative order of options and other arguments. */ + +#include "getopt.h" + +/* For communication from `getopt' to the caller. + When `getopt' finds an option that takes an argument, + the argument value is returned here. + Also, when `ordering' is RETURN_IN_ORDER, + each non-option ARGV-element is returned here. */ + +char *optarg; + +/* Index in ARGV of the next element to be scanned. + This is used for communication to and from the caller + and for communication between successive calls to `getopt'. + + On entry to `getopt', zero means this is the first call; initialize. + + When `getopt' returns -1, this is the index of the first of the + non-option elements that the caller should itself scan. + + Otherwise, `optind' communicates from one call to the next + how much of ARGV has been scanned so far. */ + +/* 1003.2 says this must be 1 before any call. */ +int optind = 1; + +/* Formerly, initialization of getopt depended on optind==0, which + causes problems with re-calling getopt as programs generally don't + know that. */ + +int __getopt_initialized; + +/* The next char to be scanned in the option-element + in which the last option character we returned was found. + This allows us to pick up the scan where we left off. + + If this is zero, or a null string, it means resume the scan + by advancing to the next ARGV-element. */ + +static char *nextchar; + +/* Callers store zero here to inhibit the error message + for unrecognized options. */ + +int opterr = 1; + +/* Set to an option character which was unrecognized. + This must be initialized on some systems to avoid linking in the + system's own getopt implementation. */ + +int optopt = '?'; + +/* Describe how to deal with options that follow non-option ARGV-elements. + + If the caller did not specify anything, + the default is REQUIRE_ORDER if the environment variable + POSIXLY_CORRECT is defined, PERMUTE otherwise. + + REQUIRE_ORDER means don't recognize them as options; + stop option processing when the first non-option is seen. + This is what Unix does. + This mode of operation is selected by either setting the environment + variable POSIXLY_CORRECT, or using `+' as the first character + of the list of option characters. + + PERMUTE is the default. We permute the contents of ARGV as we scan, + so that eventually all the non-options are at the end. This allows options + to be given in any order, even with programs that were not written to + expect this. + + RETURN_IN_ORDER is an option available to programs that were written + to expect options and other ARGV-elements in any order and that care about + the ordering of the two. We describe each non-option ARGV-element + as if it were the argument of an option with character code 1. + Using `-' as the first character of the list of option characters + selects this mode of operation. + + The special argument `--' forces an end of option-scanning regardless + of the value of `ordering'. In the case of RETURN_IN_ORDER, only + `--' can cause `getopt' to return -1 with `optind' != ARGC. */ + +static enum +{ + REQUIRE_ORDER, PERMUTE, RETURN_IN_ORDER +} ordering; + +/* Value of POSIXLY_CORRECT environment variable. */ +static char *posixly_correct; + +#ifdef __GNU_LIBRARY__ +/* We want to avoid inclusion of string.h with non-GNU libraries + because there are many ways it can cause trouble. + On some systems, it contains special magic macros that don't work + in GCC. */ +# include +# define my_index strchr +#else + +# if 1 //HAVE_STRING_H +# include +# else +# include +# endif + +/* Avoid depending on library functions or files + whose names are inconsistent. */ + +#ifndef getenv +#ifdef _MSC_VER +// DDK will complain if you don't use the stdlib defined getenv +#include +#else +extern char *getenv (); +#endif +#endif + +static char * +my_index (str, chr) + const char *str; + int chr; +{ + while (*str) + { + if (*str == chr) + return (char *) str; + str++; + } + return 0; +} + +/* If using GCC, we can safely declare strlen this way. + If not using GCC, it is ok not to declare it. */ +#ifdef __GNUC__ +/* Note that Motorola Delta 68k R3V7 comes with GCC but not stddef.h. + That was relevant to code that was here before. */ +# if (!defined __STDC__ || !__STDC__) && !defined strlen +/* gcc with -traditional declares the built-in strlen to return int, + and has done so at least since version 2.4.5. -- rms. */ +extern int strlen (const char *); +# endif /* not __STDC__ */ +#endif /* __GNUC__ */ + +#endif /* not __GNU_LIBRARY__ */ + +/* Handle permutation of arguments. */ + +/* Describe the part of ARGV that contains non-options that have + been skipped. `first_nonopt' is the index in ARGV of the first of them; + `last_nonopt' is the index after the last of them. */ + +static int first_nonopt; +static int last_nonopt; + +#ifdef _LIBC +/* Stored original parameters. + XXX This is no good solution. We should rather copy the args so + that we can compare them later. But we must not use malloc(3). */ +extern int __libc_argc; +extern char **__libc_argv; + +/* Bash 2.0 gives us an environment variable containing flags + indicating ARGV elements that should not be considered arguments. */ + +# ifdef USE_NONOPTION_FLAGS +/* Defined in getopt_init.c */ +extern char *__getopt_nonoption_flags; + +static int nonoption_flags_max_len; +static int nonoption_flags_len; +# endif + +# ifdef USE_NONOPTION_FLAGS +# define SWAP_FLAGS(ch1, ch2) \ + if (nonoption_flags_len > 0) \ + { \ + char __tmp = __getopt_nonoption_flags[ch1]; \ + __getopt_nonoption_flags[ch1] = __getopt_nonoption_flags[ch2]; \ + __getopt_nonoption_flags[ch2] = __tmp; \ + } +# else +# define SWAP_FLAGS(ch1, ch2) +# endif +#else /* !_LIBC */ +# define SWAP_FLAGS(ch1, ch2) +#endif /* _LIBC */ + +/* Exchange two adjacent subsequences of ARGV. + One subsequence is elements [first_nonopt,last_nonopt) + which contains all the non-options that have been skipped so far. + The other is elements [last_nonopt,optind), which contains all + the options processed since those non-options were skipped. + + `first_nonopt' and `last_nonopt' are relocated so that they describe + the new indices of the non-options in ARGV after they are moved. */ + +#if defined __STDC__ && __STDC__ +static void exchange (char **); +#endif + +static void +exchange (argv) + char **argv; +{ + int bottom = first_nonopt; + int middle = last_nonopt; + int top = optind; + char *tem; + + /* Exchange the shorter segment with the far end of the longer segment. + That puts the shorter segment into the right place. + It leaves the longer segment in the right place overall, + but it consists of two parts that need to be swapped next. */ + +#if defined _LIBC && defined USE_NONOPTION_FLAGS + /* First make sure the handling of the `__getopt_nonoption_flags' + string can work normally. Our top argument must be in the range + of the string. */ + if (nonoption_flags_len > 0 && top >= nonoption_flags_max_len) + { + /* We must extend the array. The user plays games with us and + presents new arguments. */ + char *new_str = malloc (top + 1); + if (new_str == NULL) + nonoption_flags_len = nonoption_flags_max_len = 0; + else + { + memset (__mempcpy (new_str, __getopt_nonoption_flags, + nonoption_flags_max_len), + '\0', top + 1 - nonoption_flags_max_len); + nonoption_flags_max_len = top + 1; + __getopt_nonoption_flags = new_str; + } + } +#endif + + while (top > middle && middle > bottom) + { + if (top - middle > middle - bottom) + { + /* Bottom segment is the short one. */ + int len = middle - bottom; + register int i; + + /* Swap it with the top part of the top segment. */ + for (i = 0; i < len; i++) + { + tem = argv[bottom + i]; + argv[bottom + i] = argv[top - (middle - bottom) + i]; + argv[top - (middle - bottom) + i] = tem; + SWAP_FLAGS (bottom + i, top - (middle - bottom) + i); + } + /* Exclude the moved bottom segment from further swapping. */ + top -= len; + } + else + { + /* Top segment is the short one. */ + int len = top - middle; + register int i; + + /* Swap it with the bottom part of the bottom segment. */ + for (i = 0; i < len; i++) + { + tem = argv[bottom + i]; + argv[bottom + i] = argv[middle + i]; + argv[middle + i] = tem; + SWAP_FLAGS (bottom + i, middle + i); + } + /* Exclude the moved top segment from further swapping. */ + bottom += len; + } + } + + /* Update records for the slots the non-options now occupy. */ + + first_nonopt += (optind - last_nonopt); + last_nonopt = optind; +} + +/* Initialize the internal data when the first call is made. */ + +#if defined __STDC__ && __STDC__ +static const char *_getopt_initialize (int, char *const *, const char *); +#endif +static const char * +_getopt_initialize (argc, argv, optstring) + int argc; + char *const *argv; + const char *optstring; +{ + /* Start processing options with ARGV-element 1 (since ARGV-element 0 + is the program name); the sequence of previously skipped + non-option ARGV-elements is empty. */ + + first_nonopt = last_nonopt = optind; + + nextchar = NULL; + + posixly_correct = getenv ("POSIXLY_CORRECT"); + + /* Determine how to handle the ordering of options and nonoptions. */ + + if (optstring[0] == '-') + { + ordering = RETURN_IN_ORDER; + ++optstring; + } + else if (optstring[0] == '+') + { + ordering = REQUIRE_ORDER; + ++optstring; + } + else if (posixly_correct != NULL) + ordering = REQUIRE_ORDER; + else + ordering = PERMUTE; + +#if defined _LIBC && defined USE_NONOPTION_FLAGS + if (posixly_correct == NULL + && argc == __libc_argc && argv == __libc_argv) + { + if (nonoption_flags_max_len == 0) + { + if (__getopt_nonoption_flags == NULL + || __getopt_nonoption_flags[0] == '\0') + nonoption_flags_max_len = -1; + else + { + const char *orig_str = __getopt_nonoption_flags; + int len = nonoption_flags_max_len = strlen (orig_str); + if (nonoption_flags_max_len < argc) + nonoption_flags_max_len = argc; + __getopt_nonoption_flags = + (char *) malloc (nonoption_flags_max_len); + if (__getopt_nonoption_flags == NULL) + nonoption_flags_max_len = -1; + else + memset (__mempcpy (__getopt_nonoption_flags, orig_str, len), + '\0', nonoption_flags_max_len - len); + } + } + nonoption_flags_len = nonoption_flags_max_len; + } + else + nonoption_flags_len = 0; +#endif + + return optstring; +} + +/* Scan elements of ARGV (whose length is ARGC) for option characters + given in OPTSTRING. + + If an element of ARGV starts with '-', and is not exactly "-" or "--", + then it is an option element. The characters of this element + (aside from the initial '-') are option characters. If `getopt' + is called repeatedly, it returns successively each of the option characters + from each of the option elements. + + If `getopt' finds another option character, it returns that character, + updating `optind' and `nextchar' so that the next call to `getopt' can + resume the scan with the following option character or ARGV-element. + + If there are no more option characters, `getopt' returns -1. + Then `optind' is the index in ARGV of the first ARGV-element + that is not an option. (The ARGV-elements have been permuted + so that those that are not options now come last.) + + OPTSTRING is a string containing the legitimate option characters. + If an option character is seen that is not listed in OPTSTRING, + return '?' after printing an error message. If you set `opterr' to + zero, the error message is suppressed but we still return '?'. + + If a char in OPTSTRING is followed by a colon, that means it wants an arg, + so the following text in the same ARGV-element, or the text of the following + ARGV-element, is returned in `optarg'. Two colons mean an option that + wants an optional arg; if there is text in the current ARGV-element, + it is returned in `optarg', otherwise `optarg' is set to zero. + + If OPTSTRING starts with `-' or `+', it requests different methods of + handling the non-option ARGV-elements. + See the comments about RETURN_IN_ORDER and REQUIRE_ORDER, above. + + Long-named options begin with `--' instead of `-'. + Their names may be abbreviated as long as the abbreviation is unique + or is an exact match for some defined option. If they have an + argument, it follows the option name in the same ARGV-element, separated + from the option name by a `=', or else the in next ARGV-element. + When `getopt' finds a long-named option, it returns 0 if that option's + `flag' field is nonzero, the value of the option's `val' field + if the `flag' field is zero. + + The elements of ARGV aren't really const, because we permute them. + But we pretend they're const in the prototype to be compatible + with other systems. + + LONGOPTS is a vector of `struct option' terminated by an + element containing a name which is zero. + + LONGIND returns the index in LONGOPT of the long-named option found. + It is only valid when a long-named option has been found by the most + recent call. + + If LONG_ONLY is nonzero, '-' as well as '--' can introduce + long-named options. */ + +int +_getopt_internal (argc, argv, optstring, longopts, longind, long_only) + int argc; + char *const *argv; + const char *optstring; + const struct option *longopts; + int *longind; + int long_only; +{ + int print_errors = opterr; + if (optstring[0] == ':') + print_errors = 0; + + if (argc < 1) + return -1; + + optarg = NULL; + + if (optind == 0 || !__getopt_initialized) + { + if (optind == 0) + optind = 1; /* Don't scan ARGV[0], the program name. */ + optstring = _getopt_initialize (argc, argv, optstring); + __getopt_initialized = 1; + } + + /* Test whether ARGV[optind] points to a non-option argument. + Either it does not have option syntax, or there is an environment flag + from the shell indicating it is not an option. The later information + is only used when the used in the GNU libc. */ +#if defined _LIBC && defined USE_NONOPTION_FLAGS +# define NONOPTION_P (argv[optind][0] != '-' || argv[optind][1] == '\0' \ + || (optind < nonoption_flags_len \ + && __getopt_nonoption_flags[optind] == '1')) +#else +# define NONOPTION_P (argv[optind][0] != '-' || argv[optind][1] == '\0') +#endif + + if (nextchar == NULL || *nextchar == '\0') + { + /* Advance to the next ARGV-element. */ + + /* Give FIRST_NONOPT & LAST_NONOPT rational values if OPTIND has been + moved back by the user (who may also have changed the arguments). */ + if (last_nonopt > optind) + last_nonopt = optind; + if (first_nonopt > optind) + first_nonopt = optind; + + if (ordering == PERMUTE) + { + /* If we have just processed some options following some non-options, + exchange them so that the options come first. */ + + if (first_nonopt != last_nonopt && last_nonopt != optind) + exchange ((char **) argv); + else if (last_nonopt != optind) + first_nonopt = optind; + + /* Skip any additional non-options + and extend the range of non-options previously skipped. */ + + while (optind < argc && NONOPTION_P) + optind++; + last_nonopt = optind; + } + + /* The special ARGV-element `--' means premature end of options. + Skip it like a null option, + then exchange with previous non-options as if it were an option, + then skip everything else like a non-option. */ + + if (optind != argc && !strcmp (argv[optind], "--")) + { + optind++; + + if (first_nonopt != last_nonopt && last_nonopt != optind) + exchange ((char **) argv); + else if (first_nonopt == last_nonopt) + first_nonopt = optind; + last_nonopt = argc; + + optind = argc; + } + + /* If we have done all the ARGV-elements, stop the scan + and back over any non-options that we skipped and permuted. */ + + if (optind == argc) + { + /* Set the next-arg-index to point at the non-options + that we previously skipped, so the caller will digest them. */ + if (first_nonopt != last_nonopt) + optind = first_nonopt; + return -1; + } + + /* If we have come to a non-option and did not permute it, + either stop the scan or describe it to the caller and pass it by. */ + + if (NONOPTION_P) + { + if (ordering == REQUIRE_ORDER) + return -1; + optarg = argv[optind++]; + return 1; + } + + /* We have found another option-ARGV-element. + Skip the initial punctuation. */ + + nextchar = (argv[optind] + 1 + + (longopts != NULL && argv[optind][1] == '-')); + } + + /* Decode the current option-ARGV-element. */ + + /* Check whether the ARGV-element is a long option. + + If long_only and the ARGV-element has the form "-f", where f is + a valid short option, don't consider it an abbreviated form of + a long option that starts with f. Otherwise there would be no + way to give the -f short option. + + On the other hand, if there's a long option "fubar" and + the ARGV-element is "-fu", do consider that an abbreviation of + the long option, just like "--fu", and not "-f" with arg "u". + + This distinction seems to be the most useful approach. */ + + if (longopts != NULL + && (argv[optind][1] == '-' + || (long_only && (argv[optind][2] || !my_index (optstring, argv[optind][1]))))) + { + char *nameend; + const struct option *p; + const struct option *pfound = NULL; + int exact = 0; + int ambig = 0; + int indfound = -1; + int option_index; + + for (nameend = nextchar; *nameend && *nameend != '='; nameend++) + /* Do nothing. */ ; + + /* Test all long options for either exact match + or abbreviated matches. */ + for (p = longopts, option_index = 0; p->name; p++, option_index++) + if (!strncmp (p->name, nextchar, nameend - nextchar)) + { + if ((unsigned int) (nameend - nextchar) + == (unsigned int) strlen (p->name)) + { + /* Exact match found. */ + pfound = p; + indfound = option_index; + exact = 1; + break; + } + else if (pfound == NULL) + { + /* First nonexact match found. */ + pfound = p; + indfound = option_index; + } + else if (long_only + || pfound->has_arg != p->has_arg + || pfound->flag != p->flag + || pfound->val != p->val) + /* Second or later nonexact match found. */ + ambig = 1; + } + + if (ambig && !exact) + { + if (print_errors) + fprintf (stderr, _("%s: option `%s' is ambiguous\n"), + argv[0], argv[optind]); + nextchar += strlen (nextchar); + optind++; + optopt = 0; + return '?'; + } + + if (pfound != NULL) + { + option_index = indfound; + optind++; + if (*nameend) + { + /* Don't test has_arg with >, because some C compilers don't + allow it to be used on enums. */ + if (pfound->has_arg) + optarg = nameend + 1; + else + { + if (print_errors) + { + if (argv[optind - 1][1] == '-') + /* --option */ + fprintf (stderr, + _("%s: option `--%s' doesn't allow an argument\n"), + argv[0], pfound->name); + else + /* +option or -option */ + fprintf (stderr, + _("%s: option `%c%s' doesn't allow an argument\n"), + argv[0], argv[optind - 1][0], pfound->name); + } + + nextchar += strlen (nextchar); + + optopt = pfound->val; + return '?'; + } + } + else if (pfound->has_arg == 1) + { + if (optind < argc) + optarg = argv[optind++]; + else + { + if (print_errors) + fprintf (stderr, + _("%s: option `%s' requires an argument\n"), + argv[0], argv[optind - 1]); + nextchar += strlen (nextchar); + optopt = pfound->val; + return optstring[0] == ':' ? ':' : '?'; + } + } + nextchar += strlen (nextchar); + if (longind != NULL) + *longind = option_index; + if (pfound->flag) + { + *(pfound->flag) = pfound->val; + return 0; + } + return pfound->val; + } + + /* Can't find it as a long option. If this is not getopt_long_only, + or the option starts with '--' or is not a valid short + option, then it's an error. + Otherwise interpret it as a short option. */ + if (!long_only || argv[optind][1] == '-' + || my_index (optstring, *nextchar) == NULL) + { + if (print_errors) + { + if (argv[optind][1] == '-') + /* --option */ + fprintf (stderr, _("%s: unrecognized option `--%s'\n"), + argv[0], nextchar); + else + /* +option or -option */ + fprintf (stderr, _("%s: unrecognized option `%c%s'\n"), + argv[0], argv[optind][0], nextchar); + } + nextchar = (char *) ""; + optind++; + optopt = 0; + return '?'; + } + } + + /* Look at and handle the next short option-character. */ + + { + char c = *nextchar++; + char *temp = my_index (optstring, c); + + /* Increment `optind' when we start to process its last character. */ + if (*nextchar == '\0') + ++optind; + + if (temp == NULL || c == ':') + { + if (print_errors) + { + if (posixly_correct) + /* 1003.2 specifies the format of this message. */ + fprintf (stderr, _("%s: illegal option -- %c\n"), + argv[0], c); + else + fprintf (stderr, _("%s: invalid option -- %c\n"), + argv[0], c); + } + optopt = c; + return '?'; + } + /* Convenience. Treat POSIX -W foo same as long option --foo */ + if (temp[0] == 'W' && temp[1] == ';') + { + char *nameend; + const struct option *p; + const struct option *pfound = NULL; + int exact = 0; + int ambig = 0; + int indfound = 0; + int option_index; + + /* This is an option that requires an argument. */ + if (*nextchar != '\0') + { + optarg = nextchar; + /* If we end this ARGV-element by taking the rest as an arg, + we must advance to the next element now. */ + optind++; + } + else if (optind == argc) + { + if (print_errors) + { + /* 1003.2 specifies the format of this message. */ + fprintf (stderr, _("%s: option requires an argument -- %c\n"), + argv[0], c); + } + optopt = c; + if (optstring[0] == ':') + c = ':'; + else + c = '?'; + return c; + } + else + /* We already incremented `optind' once; + increment it again when taking next ARGV-elt as argument. */ + optarg = argv[optind++]; + + /* optarg is now the argument, see if it's in the + table of longopts. */ + + for (nextchar = nameend = optarg; *nameend && *nameend != '='; nameend++) + /* Do nothing. */ ; + + /* Test all long options for either exact match + or abbreviated matches. */ + for (p = longopts, option_index = 0; p->name; p++, option_index++) + if (!strncmp (p->name, nextchar, nameend - nextchar)) + { + if ((unsigned int) (nameend - nextchar) == strlen (p->name)) + { + /* Exact match found. */ + pfound = p; + indfound = option_index; + exact = 1; + break; + } + else if (pfound == NULL) + { + /* First nonexact match found. */ + pfound = p; + indfound = option_index; + } + else + /* Second or later nonexact match found. */ + ambig = 1; + } + if (ambig && !exact) + { + if (print_errors) + fprintf (stderr, _("%s: option `-W %s' is ambiguous\n"), + argv[0], argv[optind]); + nextchar += strlen (nextchar); + optind++; + return '?'; + } + if (pfound != NULL) + { + option_index = indfound; + if (*nameend) + { + /* Don't test has_arg with >, because some C compilers don't + allow it to be used on enums. */ + if (pfound->has_arg) + optarg = nameend + 1; + else + { + if (print_errors) + fprintf (stderr, _("\ + %s: option `-W %s' doesn't allow an argument\n"), + argv[0], pfound->name); + + nextchar += strlen (nextchar); + return '?'; + } + } + else if (pfound->has_arg == 1) + { + if (optind < argc) + optarg = argv[optind++]; + else + { + if (print_errors) + fprintf (stderr, + _("%s: option `%s' requires an argument\n"), + argv[0], argv[optind - 1]); + nextchar += strlen (nextchar); + return optstring[0] == ':' ? ':' : '?'; + } + } + nextchar += strlen (nextchar); + if (longind != NULL) + *longind = option_index; + if (pfound->flag) + { + *(pfound->flag) = pfound->val; + return 0; + } + return pfound->val; + } + nextchar = NULL; + return 'W'; /* Let the application handle it. */ + } + if (temp[1] == ':') + { + if (temp[2] == ':') + { + /* This is an option that accepts an argument optionally. */ + if (*nextchar != '\0') + { + optarg = nextchar; + optind++; + } + else + optarg = NULL; + nextchar = NULL; + } + else + { + /* This is an option that requires an argument. */ + if (*nextchar != '\0') + { + optarg = nextchar; + /* If we end this ARGV-element by taking the rest as an arg, + we must advance to the next element now. */ + optind++; + } + else if (optind == argc) + { + if (print_errors) + { + /* 1003.2 specifies the format of this message. */ + fprintf (stderr, + _("%s: option requires an argument -- %c\n"), + argv[0], c); + } + optopt = c; + if (optstring[0] == ':') + c = ':'; + else + c = '?'; + } + else + /* We already incremented `optind' once; + increment it again when taking next ARGV-elt as argument. */ + optarg = argv[optind++]; + nextchar = NULL; + } + } + return c; + } +} + +int +getopt (argc, argv, optstring) + int argc; + char *const *argv; + const char *optstring; +{ + return _getopt_internal (argc, argv, optstring, + (const struct option *) 0, + (int *) 0, + 0); +} + +#endif /* Not ELIDE_CODE. */ + +#ifdef TEST + +/* Compile with -DTEST to make an executable for use in testing + the above definition of `getopt'. */ + +int +main (argc, argv) + int argc; + char **argv; +{ + int c; + int digit_optind = 0; + + while (1) + { + int this_option_optind = optind ? optind : 1; + + c = getopt (argc, argv, "abc:d:0123456789"); + if (c == -1) + break; + + switch (c) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + if (digit_optind != 0 && digit_optind != this_option_optind) + printf ("digits occur in two different argv-elements.\n"); + digit_optind = this_option_optind; + printf ("option %c\n", c); + break; + + case 'a': + printf ("option a\n"); + break; + + case 'b': + printf ("option b\n"); + break; + + case 'c': + printf ("option c with value `%s'\n", optarg); + break; + + case '?': + break; + + default: + printf ("?? getopt returned character code 0%o ??\n", c); + } + } + + if (optind < argc) + { + printf ("non-option ARGV-elements: "); + while (optind < argc) + printf ("%s ", argv[optind++]); + printf ("\n"); + } + + exit (0); +} + +#endif /* TEST */ diff --git a/src/getopt/getopt.h b/src/getopt/getopt.h new file mode 100644 index 0000000..a1b8dd6 --- /dev/null +++ b/src/getopt/getopt.h @@ -0,0 +1,180 @@ +/* Declarations for getopt. + Copyright (C) 1989-1994, 1996-1999, 2001 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, write to the Free + Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + 02111-1307 USA. */ + +#ifndef _GETOPT_H + +#ifndef __need_getopt +# define _GETOPT_H 1 +#endif + +/* If __GNU_LIBRARY__ is not already defined, either we are being used + standalone, or this is the first header included in the source file. + If we are being used with glibc, we need to include , but + that does not exist if we are standalone. So: if __GNU_LIBRARY__ is + not defined, include , which will pull in for us + if it's from glibc. (Why ctype.h? It's guaranteed to exist and it + doesn't flood the namespace with stuff the way some other headers do.) */ +#if !defined __GNU_LIBRARY__ +# include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* For communication from `getopt' to the caller. + When `getopt' finds an option that takes an argument, + the argument value is returned here. + Also, when `ordering' is RETURN_IN_ORDER, + each non-option ARGV-element is returned here. */ + +extern char *optarg; + +/* Index in ARGV of the next element to be scanned. + This is used for communication to and from the caller + and for communication between successive calls to `getopt'. + + On entry to `getopt', zero means this is the first call; initialize. + + When `getopt' returns -1, this is the index of the first of the + non-option elements that the caller should itself scan. + + Otherwise, `optind' communicates from one call to the next + how much of ARGV has been scanned so far. */ + +extern int optind; + +/* Callers store zero here to inhibit the error message `getopt' prints + for unrecognized options. */ + +extern int opterr; + +/* Set to an option character which was unrecognized. */ + +extern int optopt; + +#ifndef __need_getopt +/* Describe the long-named options requested by the application. + The LONG_OPTIONS argument to getopt_long or getopt_long_only is a vector + of `struct option' terminated by an element containing a name which is + zero. + + The field `has_arg' is: + no_argument (or 0) if the option does not take an argument, + required_argument (or 1) if the option requires an argument, + optional_argument (or 2) if the option takes an optional argument. + + If the field `flag' is not NULL, it points to a variable that is set + to the value given in the field `val' when the option is found, but + left unchanged if the option is not found. + + To have a long-named option do something other than set an `int' to + a compiled-in constant, such as set a value from `optarg', set the + option's `flag' field to zero and its `val' field to a nonzero + value (the equivalent single-letter option character, if there is + one). For long options that have a zero `flag' field, `getopt' + returns the contents of the `val' field. */ + +struct option +{ +# if (defined __STDC__ && __STDC__) || defined __cplusplus + const char *name; +# else + char *name; +# endif + /* has_arg can't be an enum because some compilers complain about + type mismatches in all the code that assumes it is an int. */ + int has_arg; + int *flag; + int val; +}; + +/* Names for the values of the `has_arg' field of `struct option'. */ + +# define no_argument 0 +# define required_argument 1 +# define optional_argument 2 +#endif /* need getopt */ + + +/* Get definitions and prototypes for functions to process the + arguments in ARGV (ARGC of them, minus the program name) for + options given in OPTS. + + Return the option character from OPTS just read. Return -1 when + there are no more options. For unrecognized options, or options + missing arguments, `optopt' is set to the option letter, and '?' is + returned. + + The OPTS string is a list of characters which are recognized option + letters, optionally followed by colons, specifying that that letter + takes an argument, to be placed in `optarg'. + + If a letter in OPTS is followed by two colons, its argument is + optional. This behavior is specific to the GNU `getopt'. + + The argument `--' causes premature termination of argument + scanning, explicitly telling `getopt' that there are no more + options. + + If OPTS begins with `--', then non-option arguments are treated as + arguments to the option '\0'. This behavior is specific to the GNU + `getopt'. */ + +#if (defined __STDC__ && __STDC__) || defined __cplusplus +# ifdef __GNU_LIBRARY__ +/* Many other libraries have conflicting prototypes for getopt, with + differences in the consts, in stdlib.h. To avoid compilation + errors, only prototype getopt for the GNU C library. */ +extern int getopt (int __argc, char *const *__argv, const char *__shortopts); +# else /* not __GNU_LIBRARY__ */ +extern int getopt (); +# endif /* __GNU_LIBRARY__ */ + +# ifndef __need_getopt +extern int getopt_long (int __argc, char *const *__argv, const char *__shortopts, + const struct option *__longopts, int *__longind); +extern int getopt_long_only (int __argc, char *const *__argv, + const char *__shortopts, + const struct option *__longopts, int *__longind); + +/* Internal only. Users should not call this directly. */ +extern int _getopt_internal (int __argc, char *const *__argv, + const char *__shortopts, + const struct option *__longopts, int *__longind, + int __long_only); +# endif +#else /* not __STDC__ */ +extern int getopt (); +# ifndef __need_getopt +extern int getopt_long (); +extern int getopt_long_only (); + +extern int _getopt_internal (); +# endif +#endif /* __STDC__ */ + +#ifdef __cplusplus +} +#endif + +/* Make sure we later can get all the definitions and declarations. */ +#undef __need_getopt + +#endif /* getopt.h */ diff --git a/src/hsdaoh.rc.in b/src/hsdaoh.rc.in new file mode 100644 index 0000000..95e98fc --- /dev/null +++ b/src/hsdaoh.rc.in @@ -0,0 +1,34 @@ + +#include + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 0,0,0,0 + PRODUCTVERSION 0,0,0,0 + FILEFLAGSMASK 0x3fL +#ifndef NDEBUG + FILEFLAGS 0x0L +#else + FILEFLAGS 0x1L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE VFT2_DRV_INSTALLABLE + BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "FileDescription", "hsdaoh" + VALUE "FileVersion", "@VERSION@" + VALUE "InternalName", "hsdaoh.dll" + VALUE "LegalCopyright", "Licensed under GPLv2" + VALUE "OriginalFilename", "hsdaoh.dll" + VALUE "ProductName", "hsdaoh" + VALUE "ProductVersion", "@VERSION@" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END + END diff --git a/src/hsdaoh_file.c b/src/hsdaoh_file.c new file mode 100644 index 0000000..6413f65 --- /dev/null +++ b/src/hsdaoh_file.c @@ -0,0 +1,198 @@ +/* + * hsdaoh - High Speed Data Acquisition over MS213x USB3 HDMI capture sticks + * + * Copyright (C) 2024 by Steve Markgraf + * + * SPDX-License-Identifier: GPL-2.0+ + * + * 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 2 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#else +#include +#include +#include +#include "getopt/getopt.h" +#endif + +#include "hsdaoh.h" + +#define DEFAULT_SAMPLE_RATE 30000000 + +static int do_exit = 0; +static uint32_t bytes_to_read = 0; +static hsdaoh_dev_t *dev = NULL; + +void usage(void) +{ + fprintf(stderr, + "hsdaoh_file, HDMI data acquisition tool\n\n" + "Usage:\n" + "\t[-s samplerate (default: 30 MHz)]\n" + "\t[-d device_index (default: 0)]\n" + "\t[-p ppm_error (default: 0)]\n" + "\t[-n number of samples to read (default: 0, infinite)]\n" + "\tfilename (a '-' dumps samples to stdout)\n\n"); + exit(1); +} + +#ifdef _WIN32 +BOOL WINAPI +sighandler(int signum) +{ + if (CTRL_C_EVENT == signum) { + fprintf(stderr, "Signal caught, exiting!\n"); + do_exit = 1; + hsdaoh_stop_stream(dev); + return TRUE; + } + return FALSE; +} +#else +static void sighandler(int signum) +{ + signal(SIGPIPE, SIG_IGN); + fprintf(stderr, "Signal caught, exiting!\n"); + do_exit = 1; + hsdaoh_stop_stream(dev); +} +#endif + +static void hsdaoh_callback(unsigned char *buf, uint32_t len, void *ctx) +{ + if (ctx) { + if (do_exit) + return; + + if ((bytes_to_read > 0) && (bytes_to_read < len)) { + len = bytes_to_read; + do_exit = 1; + hsdaoh_stop_stream(dev); + } + + if (fwrite(buf, 1, len, (FILE*)ctx) != len) { + fprintf(stderr, "Short write, samples lost, exiting!\n"); + hsdaoh_stop_stream(dev); + } + + if (bytes_to_read > 0) + bytes_to_read -= len; + } +} + +int main(int argc, char **argv) +{ +#ifndef _WIN32 + struct sigaction sigact; +#endif + char *filename = NULL; + int n_read; + int r, opt; + int ppm_error = 0; + FILE *file; + int dev_index = 0; + uint32_t samp_rate = DEFAULT_SAMPLE_RATE; + + while ((opt = getopt(argc, argv, "d:s:n:p:d:")) != -1) { + switch (opt) { + case 'd': + dev_index = (uint32_t)atoi(optarg); + break; + case 's': + samp_rate = (uint32_t)atof(optarg); + break; + case 'p': + ppm_error = atoi(optarg); + break; + case 'n': + bytes_to_read = (uint32_t)atof(optarg) * 2; + break; + default: + usage(); + break; + } + } + + if (argc <= optind) { + usage(); + } else { + filename = argv[optind]; + } + + if (dev_index < 0) { + exit(1); + } + + r = hsdaoh_open(&dev, (uint32_t)dev_index); + if (r < 0) { + fprintf(stderr, "Failed to open hsdaoh device #%d.\n", dev_index); + exit(1); + } +#ifndef _WIN32 + sigact.sa_handler = sighandler; + sigemptyset(&sigact.sa_mask); + sigact.sa_flags = 0; + sigaction(SIGINT, &sigact, NULL); + sigaction(SIGTERM, &sigact, NULL); + sigaction(SIGQUIT, &sigact, NULL); + sigaction(SIGPIPE, &sigact, NULL); +#else + SetConsoleCtrlHandler( (PHANDLER_ROUTINE) sighandler, TRUE ); +#endif + + /* Set the sample rate */ + r = hsdaoh_set_sample_rate(dev, samp_rate, 0); + if (r < 0) + fprintf(stderr, "WARNING: Failed to set sample rate.\n"); + + if (strcmp(filename, "-") == 0) { /* Write samples to stdout */ + file = stdout; +#ifdef _WIN32 + _setmode(_fileno(stdin), _O_BINARY); +#endif + } else { + file = fopen(filename, "wb"); + if (!file) { + fprintf(stderr, "Failed to open %s\n", filename); + goto out; + } + } + + fprintf(stderr, "Reading samples...\n"); + r = hsdaoh_start_stream(dev, hsdaoh_callback, (void *)file); + + while (!do_exit) { + usleep(50000); + } + + if (do_exit) + fprintf(stderr, "\nUser cancel, exiting...\n"); + else + fprintf(stderr, "\nLibrary error %d, exiting...\n", r); + + if (file != stdout) + fclose(file); + + hsdaoh_close(dev); +out: + return r >= 0 ? r : -r; +} diff --git a/src/hsdaoh_tcp.c b/src/hsdaoh_tcp.c new file mode 100644 index 0000000..92accd4 --- /dev/null +++ b/src/hsdaoh_tcp.c @@ -0,0 +1,532 @@ +/* + * hsdaoh - High Speed Data Acquisition over MS213x USB3 HDMI capture sticks + * + * Copyright (C) 2012 by Steve Markgraf + * Copyright (C) 2012-2013 by Hoernchen + * + * SPDX-License-Identifier: GPL-2.0+ + * + * 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 2 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include "getopt/getopt.h" +#endif + +#include + +#include "hsdaoh.h" + +#ifdef _WIN32 +#pragma comment(lib, "ws2_32.lib") + +typedef int socklen_t; + +#else +#define closesocket close +#define SOCKADDR struct sockaddr +#define SOCKET int +#define SOCKET_ERROR -1 +#endif + +#define DEFAULT_PORT_STR "1234" +#define DEFAULT_SAMPLE_RATE_HZ 30000000 +#define DEFAULT_MAX_NUM_BUFFERS 500 + +static SOCKET s; + +static pthread_t tcp_worker_thread; +static pthread_t command_thread; +static pthread_cond_t exit_cond; +static pthread_mutex_t exit_cond_lock; + +static pthread_mutex_t ll_mutex; +static pthread_cond_t cond; + +struct llist { + char *data; + size_t len; + struct llist *next; +}; + +typedef struct { /* structure size must be multiple of 2 bytes */ + char magic[4]; + uint32_t tuner_type; + uint32_t tuner_gain_count; +} dongle_info_t; + +static hsdaoh_dev_t *dev = NULL; + +static int global_numq = 0; +static struct llist *ll_buffers = 0; +static int llbuf_num = DEFAULT_MAX_NUM_BUFFERS; + +static volatile int do_exit = 0; + + +void usage(void) +{ + fprintf(stderr, "hsdaoh_tcp, a sample data server for hsdaoh\n\n"); + fprintf(stderr, "Usage:\t[-a listen address]\n"); + fprintf(stderr, "\t[-p listen port (default: %s)]\n", DEFAULT_PORT_STR); + fprintf(stderr, "\t[-b number of buffers (default: 15, set by library)]\n"); + fprintf(stderr, "\t[-n max number of linked list buffers to keep (default: %d)]\n", DEFAULT_MAX_NUM_BUFFERS); + fprintf(stderr, "\t[-d device index (default: 0)]\n"); + fprintf(stderr, "\t[-P ppm_error (default: 0)]\n"); + exit(1); +} + +#ifdef _WIN32 +int gettimeofday(struct timeval *tv, void* ignored) +{ + FILETIME ft; + unsigned __int64 tmp = 0; + if (NULL != tv) { + GetSystemTimeAsFileTime(&ft); + tmp |= ft.dwHighDateTime; + tmp <<= 32; + tmp |= ft.dwLowDateTime; + tmp /= 10; +#ifdef _MSC_VER + tmp -= 11644473600000000Ui64; +#else + tmp -= 11644473600000000ULL; +#endif + tv->tv_sec = (long)(tmp / 1000000UL); + tv->tv_usec = (long)(tmp % 1000000UL); + } + return 0; +} + +BOOL WINAPI +sighandler(int signum) +{ + if (CTRL_C_EVENT == signum) { + fprintf(stderr, "Signal caught, exiting!\n"); + do_exit = 1; + hsdaoh_stop_stream(dev); + return TRUE; + } + return FALSE; +} +#else +static void sighandler(int signum) +{ + signal(SIGPIPE, SIG_IGN); + fprintf(stderr, "Signal caught, exiting!\n"); + hsdaoh_stop_stream(dev); + do_exit = 1; +} +#endif + +void hsdaoh_callback(unsigned char *buf, uint32_t len, void *ctx) +{ + if(!do_exit) { + struct llist *rpt = (struct llist*)malloc(sizeof(struct llist)); + rpt->data = (char*)malloc(len); + memcpy(rpt->data, buf, len); + rpt->len = len; + rpt->next = NULL; + + pthread_mutex_lock(&ll_mutex); + + if (ll_buffers == NULL) { + ll_buffers = rpt; + } else { + struct llist *cur = ll_buffers; + int num_queued = 0; + + while (cur->next != NULL) { + cur = cur->next; + num_queued++; + } + + if(llbuf_num && llbuf_num == num_queued-2){ + struct llist *curelem; + + free(ll_buffers->data); + curelem = ll_buffers->next; + free(ll_buffers); + ll_buffers = curelem; + } + + cur->next = rpt; + + if (num_queued > global_numq) + fprintf(stderr, "ll+, now %d\n", num_queued); + else if (num_queued < global_numq) + fprintf(stderr, "ll-, now %d\n", num_queued); + + global_numq = num_queued; + } + pthread_cond_signal(&cond); + pthread_mutex_unlock(&ll_mutex); + } +} + +static void *tcp_worker(void *arg) +{ + struct llist *curelem,*prev; + int bytesleft,bytessent, index; + struct timeval tv= {1,0}; + struct timespec ts; + struct timeval tp; + fd_set writefds; + int r = 0; + + while(1) { + if(do_exit) + pthread_exit(0); + + pthread_mutex_lock(&ll_mutex); + gettimeofday(&tp, NULL); + ts.tv_sec = tp.tv_sec+5; + ts.tv_nsec = tp.tv_usec * 1000; + r = pthread_cond_timedwait(&cond, &ll_mutex, &ts); + if(r == ETIMEDOUT) { + pthread_mutex_unlock(&ll_mutex); + fprintf(stderr, "worker cond timeout\n"); + sighandler(0); + pthread_exit(NULL); + } + + curelem = ll_buffers; + ll_buffers = 0; + pthread_mutex_unlock(&ll_mutex); + + while(curelem != 0) { + bytesleft = curelem->len; + index = 0; + bytessent = 0; + while(bytesleft > 0) { + FD_ZERO(&writefds); + FD_SET(s, &writefds); + tv.tv_sec = 1; + tv.tv_usec = 0; + r = select(s+1, NULL, &writefds, NULL, &tv); + if(r) { + bytessent = send(s, &curelem->data[index], bytesleft, 0); + bytesleft -= bytessent; + index += bytessent; + } + if(bytessent == SOCKET_ERROR || do_exit) { + fprintf(stderr, "worker socket bye\n"); + sighandler(0); + pthread_exit(NULL); + } + } + prev = curelem; + curelem = curelem->next; + free(prev->data); + free(prev); + } + } +} + +#ifdef _WIN32 +#define __attribute__(x) +#pragma pack(push, 1) +#endif +struct command{ + unsigned char cmd; + unsigned int param; +}__attribute__((packed)); +#ifdef _WIN32 +#pragma pack(pop) +#endif +static void *command_worker(void *arg) +{ + int left, received = 0; + fd_set readfds; + struct command cmd={0, 0}; + struct timeval tv= {1, 0}; + int r = 0; + uint32_t tmp; + + while(1) { + left=sizeof(cmd); + while(left >0) { + FD_ZERO(&readfds); + FD_SET(s, &readfds); + tv.tv_sec = 1; + tv.tv_usec = 0; + r = select(s+1, &readfds, NULL, NULL, &tv); + if(r) { + received = recv(s, (char*)&cmd+(sizeof(cmd)-left), left, 0); + left -= received; + } + if(received == SOCKET_ERROR || do_exit) { + fprintf(stderr, "comm recv bye\n"); + sighandler(0); + pthread_exit(NULL); + } + } + switch(cmd.cmd) { + case 0x01: + break; + case 0x02: + fprintf(stderr, "set sample rate %d\n", ntohl(cmd.param)); + hsdaoh_set_sample_rate(dev, ntohl(cmd.param), false); + break; + case 0x03: + fprintf(stderr, "set gain mode %d\n", ntohl(cmd.param)); + //hsdaoh_set_tuner_gain_mode(dev, ntohl(cmd.param)); + break; + case 0x04: + fprintf(stderr, "set gain %d\n", ntohl(cmd.param)); + // hsdaoh_set_tuner_gain(dev, ntohl(cmd.param)); + break; + default: + break; + } + cmd.cmd = 0xff; + } +} + +int main(int argc, char **argv) +{ + int r, opt, i; + char *addr = "127.0.0.1"; + const char *port = DEFAULT_PORT_STR; + uint32_t frequency = 100000000, samp_rate = DEFAULT_SAMPLE_RATE_HZ; + struct sockaddr_storage local, remote; + struct addrinfo *ai; + struct addrinfo *aiHead; + struct addrinfo hints = { 0 }; + char hostinfo[NI_MAXHOST]; + char portinfo[NI_MAXSERV]; + char remhostinfo[NI_MAXHOST]; + char remportinfo[NI_MAXSERV]; + int aiErr; + uint32_t buf_num = 0; + int dev_index = 0; + int ppm_error = 0; + struct llist *curelem,*prev; + pthread_attr_t attr; + void *status; + struct timeval tv = {1,0}; + struct linger ling = {1,0}; + SOCKET listensocket = 0; + socklen_t rlen; + fd_set readfds; + u_long blockmode = 1; + dongle_info_t dongle_info; + +#ifdef _WIN32 + WSADATA wsd; + i = WSAStartup(MAKEWORD(2,2), &wsd); +#else + struct sigaction sigact, sigign; +#endif + + while ((opt = getopt(argc, argv, "a:p:s:v:b:n:d:e")) != -1) { + switch (opt) { + case 'd': + dev_index = (uint32_t)atoi(optarg); + break; + case 's': + samp_rate = (uint32_t)atof(optarg); + break; + case 'a': + addr = strdup(optarg); + break; + case 'p': + port = strdup(optarg); + break; + case 'b': + buf_num = atoi(optarg); + break; + case 'n': + llbuf_num = atoi(optarg); + break; + default: + usage(); + break; + } + } + + if (argc < optind) + usage(); + + if (dev_index < 0) { + exit(1); + } + + hsdaoh_open(&dev, (uint32_t)dev_index); + if (NULL == dev) { + fprintf(stderr, "Failed to open hsdaoh device #%d.\n", dev_index); + exit(1); + } + +#ifndef _WIN32 + sigact.sa_handler = sighandler; + sigemptyset(&sigact.sa_mask); + sigact.sa_flags = 0; + sigign.sa_handler = SIG_IGN; + sigaction(SIGINT, &sigact, NULL); + sigaction(SIGTERM, &sigact, NULL); + sigaction(SIGQUIT, &sigact, NULL); + sigaction(SIGPIPE, &sigign, NULL); +#else + SetConsoleCtrlHandler( (PHANDLER_ROUTINE) sighandler, TRUE ); +#endif + + /* Set the sample rate */ + r = hsdaoh_set_sample_rate(dev, samp_rate, false); + if (r < 0) + fprintf(stderr, "WARNING: Failed to set sample rate.\n"); + + pthread_mutex_init(&exit_cond_lock, NULL); + pthread_mutex_init(&ll_mutex, NULL); + pthread_mutex_init(&exit_cond_lock, NULL); + pthread_cond_init(&cond, NULL); + pthread_cond_init(&exit_cond, NULL); + + hints.ai_flags = AI_PASSIVE; /* Server mode. */ + hints.ai_family = PF_UNSPEC; /* IPv4 or IPv6. */ + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + if ((aiErr = getaddrinfo(addr, + port, + &hints, + &aiHead )) != 0) + { + fprintf(stderr, "local address %s ERROR - %s.\n", + addr, gai_strerror(aiErr)); + return(-1); + } + memcpy(&local, aiHead->ai_addr, aiHead->ai_addrlen); + + for (ai = aiHead; ai != NULL; ai = ai->ai_next) { + aiErr = getnameinfo((struct sockaddr *)ai->ai_addr, ai->ai_addrlen, + hostinfo, NI_MAXHOST, + portinfo, NI_MAXSERV, NI_NUMERICSERV | NI_NUMERICHOST); + if (aiErr) + fprintf( stderr, "getnameinfo ERROR - %s.\n",hostinfo); + + listensocket = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (listensocket < 0) + continue; + + r = 1; + setsockopt(listensocket, SOL_SOCKET, SO_REUSEADDR, (char *)&r, sizeof(int)); + setsockopt(listensocket, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(ling)); + + if (bind(listensocket, (struct sockaddr *)&local, aiHead->ai_addrlen)) + fprintf(stderr, "rtl_tcp bind error: %s", strerror(errno)); + else + break; + } + +#ifdef _WIN32 + ioctlsocket(listensocket, FIONBIO, &blockmode); +#else + r = fcntl(listensocket, F_GETFL, 0); + r = fcntl(listensocket, F_SETFL, r | O_NONBLOCK); +#endif + + while(1) { + fprintf(stderr, "listening...\n"); + fprintf(stderr, "Use the device argument 'rtl_tcp=%s:%s' in OsmoSDR " + "(gr-osmosdr) source\n" + "to receive samples in GRC and control " + "rtl_tcp parameters (frequency, gain, ...).\n", + hostinfo, portinfo); + listen(listensocket,1); + + while(1) { + FD_ZERO(&readfds); + FD_SET(listensocket, &readfds); + tv.tv_sec = 1; + tv.tv_usec = 0; + r = select(listensocket+1, &readfds, NULL, NULL, &tv); + if(do_exit) { + goto out; + } else if(r) { + rlen = sizeof(remote); + s = accept(listensocket,(struct sockaddr *)&remote, &rlen); + break; + } + } + + setsockopt(s, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(ling)); + + getnameinfo((struct sockaddr *)&remote, rlen, + remhostinfo, NI_MAXHOST, + remportinfo, NI_MAXSERV, NI_NUMERICSERV); + fprintf(stderr, "client accepted! %s %s\n", remhostinfo, remportinfo); + + memset(&dongle_info, 0, sizeof(dongle_info)); + memcpy(&dongle_info.magic, "RTL0", 4); + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); + r = pthread_create(&tcp_worker_thread, &attr, tcp_worker, NULL); + r = pthread_create(&command_thread, &attr, command_worker, NULL); + pthread_attr_destroy(&attr); + + r = hsdaoh_start_stream(dev, hsdaoh_callback, NULL); + while (!do_exit) { + usleep(50000); + } + + pthread_join(tcp_worker_thread, &status); + pthread_join(command_thread, &status); + + closesocket(s); + + fprintf(stderr, "all threads dead..\n"); + curelem = ll_buffers; + ll_buffers = 0; + + while(curelem != 0) { + prev = curelem; + curelem = curelem->next; + free(prev->data); + free(prev); + } + + do_exit = 0; + global_numq = 0; + } + +out: + hsdaoh_close(dev); + closesocket(listensocket); + closesocket(s); +#ifdef _WIN32 + WSACleanup(); +#endif + fprintf(stderr, "bye!\n"); + return r >= 0 ? r : -r; +} diff --git a/src/hsdaoh_test.c b/src/hsdaoh_test.c new file mode 100644 index 0000000..49154b1 --- /dev/null +++ b/src/hsdaoh_test.c @@ -0,0 +1,298 @@ +/* + * hsdaoh - High Speed Data Acquisition over MS213x USB3 HDMI capture sticks + * hsdaoh_test, test and benchmark tool + * + * Copyright (C) 2024 by Steve Markgraf + * + * based on rtl_test: + * Copyright (C) 2012-2014 by Steve Markgraf + * Copyright (C) 2012-2014 by Kyle Keen + * Copyright (C) 2014 by Michael Tatarinov + * + * SPDX-License-Identifier: GPL-2.0+ + * + * 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 2 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#else +#include +#endif + +#ifndef _WIN32 +#include +#else +#include +#include "getopt/getopt.h" +#endif + +#include "hsdaoh.h" + +#define DEFAULT_SAMPLE_RATE 30000000 + +#define MHZ(x) ((x)*1000*1000) + +#define PPM_DURATION 10 +#define PPM_DUMP_TIME 5 + +struct time_generic +/* holds all the platform specific values */ +{ +#ifndef _WIN32 + time_t tv_sec; + long tv_nsec; +#else + long tv_sec; + long tv_nsec; + int init; + LARGE_INTEGER frequency; + LARGE_INTEGER ticks; +#endif +}; + +static int do_exit = 0; +static hsdaoh_dev_t *dev = NULL; + +static uint32_t samp_rate = DEFAULT_SAMPLE_RATE; + +static uint32_t total_samples = 0; +static uint32_t dropped_samples = 0; + +static unsigned int ppm_duration = PPM_DURATION; + +void usage(void) +{ + fprintf(stderr, + "hsdaoh_test, a test tool for hsdaoh\n\n" + "Usage:\n" + "\t[-d device_index (default: 0)]\n" + "\t[-p[seconds] enable PPM error measurement (default: 10 seconds)]\n"); + exit(1); +} + +#ifdef _WIN32 +BOOL WINAPI +sighandler(int signum) +{ + if (CTRL_C_EVENT == signum) { + fprintf(stderr, "Signal caught, exiting!\n"); + do_exit = 1; + hsdaoh_stop_stream(dev); + return TRUE; + } + return FALSE; +} +#else +static void sighandler(int signum) +{ + signal(SIGPIPE, SIG_IGN); + fprintf(stderr, "Signal caught, exiting!\n"); + do_exit = 1; + hsdaoh_stop_stream(dev); +} +#endif + +#ifndef _WIN32 +static int ppm_gettime(struct time_generic *tg) +{ + int rv = ENOSYS; + struct timespec ts; + +#ifdef __unix__ + rv = clock_gettime(CLOCK_MONOTONIC, &ts); + tg->tv_sec = ts.tv_sec; + tg->tv_nsec = ts.tv_nsec; +#elif __APPLE__ + struct timeval tv; + + rv = gettimeofday(&tv, NULL); + tg->tv_sec = tv.tv_sec; + tg->tv_nsec = tv.tv_usec * 1000; +#endif + return rv; +} +#endif + +#ifdef _WIN32 +static int ppm_gettime(struct time_generic *tg) +{ + int rv; + int64_t frac; + if (!tg->init) { + QueryPerformanceFrequency(&tg->frequency); + tg->init = 1; + } + rv = QueryPerformanceCounter(&tg->ticks); + tg->tv_sec = tg->ticks.QuadPart / tg->frequency.QuadPart; + frac = (int64_t)(tg->ticks.QuadPart - (tg->tv_sec * tg->frequency.QuadPart)); + tg->tv_nsec = (long)(frac * 1000000000L / (int64_t)tg->frequency.QuadPart); + return !rv; +} +#endif + +static int ppm_report(uint64_t nsamples, uint64_t interval) +{ + double real_rate, ppm; + + real_rate = nsamples * 1e9 / interval; + ppm = 1e6 * (real_rate / (double)samp_rate - 1.); + return (int)round(ppm); +} + +static void ppm_test(uint32_t len) +{ + static uint64_t nsamples = 0; + static uint64_t interval = 0; + static uint64_t nsamples_total = 0; + static uint64_t interval_total = 0; + struct time_generic ppm_now; + static struct time_generic ppm_recent; + static enum { + PPM_INIT_NO, + PPM_INIT_DUMP, + PPM_INIT_RUN + } ppm_init = PPM_INIT_NO; + + ppm_gettime(&ppm_now); + + if (ppm_init != PPM_INIT_RUN) { + /* + * Kyle Keen wrote: + * PPM_DUMP_TIME throws out the first N seconds of data. + * The dongle's PPM is usually very bad when first starting up, + * typically incorrect by more than twice the final value. + * Discarding the first few seconds allows the value to stabilize much faster. + */ + if (ppm_init == PPM_INIT_NO) { + ppm_recent.tv_sec = ppm_now.tv_sec + PPM_DUMP_TIME; + ppm_init = PPM_INIT_DUMP; + return; + } + if (ppm_init == PPM_INIT_DUMP && ppm_recent.tv_sec < ppm_now.tv_sec) + return; + ppm_recent = ppm_now; + ppm_init = PPM_INIT_RUN; + return; + } + + nsamples += (uint64_t)(len); + interval = (uint64_t)(ppm_now.tv_sec - ppm_recent.tv_sec); + if (interval < ppm_duration) + return; + interval *= 1000000000UL; + interval += (int64_t)(ppm_now.tv_nsec - ppm_recent.tv_nsec); + nsamples_total += nsamples; + interval_total += interval; + printf("real sample rate: %i current PPM: %i cumulative PPM: %i\n", + (int)((1000000000UL * nsamples) / interval), + ppm_report(nsamples, interval), + ppm_report(nsamples_total, interval_total)); + ppm_recent = ppm_now; + nsamples = 0; +} + +uint16_t last_value = 0; + +static void hsdaoh_callback(unsigned char *buf, uint32_t len, void *ctx) +{ + /* verify the counter value */ + uint16_t *cnt = (uint16_t *)buf; + int n = len / sizeof(uint16_t); + + for (int i = 0; i < n; i++) { + if (cnt[i] != ((last_value+1) & 0xffff) ) { + printf("Counter error: %02x != %02x\n", cnt[i], ((last_value+1) & 0xffff)); + } + + last_value = cnt[i]; + } + ppm_test(n); +} + +int main(int argc, char **argv) +{ +#ifndef _WIN32 + struct sigaction sigact; +#endif + int n_read, r, opt, i; + int dev_index = 0; + + while ((opt = getopt(argc, argv, "d:s:p:he")) != -1) { + switch (opt) { + case 'd': + dev_index = (uint32_t)atoi(optarg); + break; + case 's': + samp_rate = (uint32_t)atof(optarg); + break; + case 'p': + if (optarg) + ppm_duration = atoi(optarg); + break; + case 'h': + default: + usage(); + break; + } + } + + if (dev_index < 0) { + exit(1); + } + + r = hsdaoh_open(&dev, (uint32_t)dev_index); + if (r < 0) { + fprintf(stderr, "Failed to open hsdaoh device #%d.\n", dev_index); + exit(1); + } +#ifndef _WIN32 + sigact.sa_handler = sighandler; + sigemptyset(&sigact.sa_mask); + sigact.sa_flags = 0; + sigaction(SIGINT, &sigact, NULL); + sigaction(SIGTERM, &sigact, NULL); + sigaction(SIGQUIT, &sigact, NULL); + sigaction(SIGPIPE, &sigact, NULL); +#else + SetConsoleCtrlHandler( (PHANDLER_ROUTINE) sighandler, TRUE ); +#endif + + fprintf(stderr, "Reporting PPM error measurement every %u seconds...\n", ppm_duration); + fprintf(stderr, "Press ^C after a few minutes.\n"); + + r = hsdaoh_start_stream(dev, hsdaoh_callback, NULL); + + + while (!do_exit) { + usleep(50000); + } + + if (do_exit) + fprintf(stderr, "\nUser cancel, exiting...\n"); + else + fprintf(stderr, "\nLibrary error %d, exiting...\n", r); + +exit: + hsdaoh_close(dev); + + return r >= 0 ? r : -r; +} diff --git a/src/libhsdaoh.c b/src/libhsdaoh.c new file mode 100644 index 0000000..4ce1f57 --- /dev/null +++ b/src/libhsdaoh.c @@ -0,0 +1,737 @@ +/* + * hsdaoh - High Speed Data Acquisition over MS213x USB3 HDMI capture sticks + * + * Copyright (C) 2024 by Steve Markgraf + * + * portions based on librtlsdr: + * Copyright (C) 2012-2014 by Steve Markgraf + * Copyright (C) 2012 by Dimitri Stolnikov + * + * SPDX-License-Identifier: GPL-2.0+ + * + * 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 2 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#ifndef _WIN32 +#include +#endif + +#ifdef _MSC_VER +#include +#define usleep(t) Sleep((t)/1000) +#else +#include +#endif + +#include +#include +#include +//#include +#include +#include + +/* + * All libusb callback functions should be marked with the LIBUSB_CALL macro + * to ensure that they are compiled with the same calling convention as libusb. + * + * If the macro isn't available in older libusb versions, we simply define it. + */ +#ifndef LIBUSB_CALL +#define LIBUSB_CALL +#endif + +enum hsdaoh_async_status { + HSDAOH_INACTIVE = 0, + HSDAOH_CANCELING, + HSDAOH_RUNNING +}; + +struct hsdaoh_dev { + libusb_context *ctx; + struct libusb_device_handle *devh; + hsdaoh_read_cb_t cb; + void *cb_ctx; + enum hsdaoh_async_status async_status; + int async_cancel; + uint16_t vid; + uint16_t pid; + + /* UVC related */ + uvc_context_t *uvc_ctx; + uvc_device_t *uvc_dev; + uvc_device_handle_t *uvc_devh; + + uint32_t rate; /* Hz */ + + int hid_interface; + + int frames_since_error; + int discard_start_frames; + uint16_t last_frame_cnt; + uint16_t idle_cnt; + + unsigned int width, height, fps; + + /* status */ + int dev_lost; + int driver_active; + unsigned int xfer_errors; + char manufact[256]; + char product[256]; +}; + +typedef struct hsdaoh_adapter { + uint16_t vid; + uint16_t pid; + const char *name; +} hsdaoh_adapter_t; + +static hsdaoh_adapter_t known_devices[] = { + { 0x345f, 0x2130, "MS2130" }, + { 0x534d, 0x2130, "MS2130 OEM?" }, + { 0x345f, 0x2131, "MS2131" }, +}; + +typedef struct +{ + uint32_t magic; + uint16_t framecounter; + uint8_t pack_state; +} __attribute__((packed, aligned(1))) metadata_t; + +#define CTRL_TIMEOUT 300 + +int hsdaoh_get_hid_feature_report(hsdaoh_dev_t *dev, unsigned char *data, size_t length) +{ + int report_number = data[0]; + + return libusb_control_transfer(dev->devh, + LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE | LIBUSB_ENDPOINT_IN, + 0x01, (3 << 8) | report_number, + dev->hid_interface, + (unsigned char *)data, length, + CTRL_TIMEOUT); +} + +int hsdaoh_send_hid_feature_report(hsdaoh_dev_t *dev, const unsigned char *data, size_t length) +{ + int report_number = data[0]; + + return libusb_control_transfer(dev->devh, + LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE | LIBUSB_ENDPOINT_OUT, + 0x09, (3 << 8) | report_number, + dev->hid_interface, + (unsigned char *)data, length, + CTRL_TIMEOUT); +} + +int hsdaoh_ms_write_register(hsdaoh_dev_t *dev, uint16_t addr, uint8_t val) +{ + uint8_t cmd[8] = { 0xb6, (addr >> 8), (addr & 0xff), val, + 0x00, 0x00, 0x00, 0x00 }; + + return hsdaoh_send_hid_feature_report(dev, cmd, sizeof(cmd)); +} + +int hsdaoh_read_register(hsdaoh_dev_t *dev, uint16_t addr, uint8_t *val) +{ + int r = 0; + uint8_t cmd[8] = { 0xb5, (addr >> 8), (addr & 0xff), 0x00, + 0x00, 0x00, 0x00, 0x00 }; + uint8_t resp[8]; + + r = hsdaoh_send_hid_feature_report(dev, cmd, sizeof(cmd)); + if (r < 0) + return r; + + r = hsdaoh_get_hid_feature_report(dev, resp, sizeof(resp)); + + if (val) + *val = resp[3]; + + return r; +} + +/* Switch the MS2130 to a transparent mode, YUYV data received via HDMI + * will be passed through unmodified */ +void hsdaoh_ms_enable_transparent_mode(hsdaoh_dev_t *dev) +{ + /* Note: those registers and settings have been + * found by try and error and observing changes to the output: + * no warranty! */ + + /* force YCbCr 4:2:2/YUV input, default is 0x04 (RGB) */ + hsdaoh_ms_write_register(dev, 0xf039, 0x06); + + /* disable sharpening */ + hsdaoh_ms_write_register(dev, 0xf6b0, 0x00); + + /* disable luma processing -> UVC brightness/contrast control has no effect anymore */ + hsdaoh_ms_write_register(dev, 0xf6be, 0x11); + + /* disable chroma processing -> UVC hue/saturation control has no effect anymore */ + hsdaoh_ms_write_register(dev, 0xf6bf, 0x11); + + /* disable luma horizontal scaling/subpixel interpolation */ + hsdaoh_ms_write_register(dev, 0xf65c, 0x10); + + /* disable luma vertical scaling/subpixel interpolation */ + hsdaoh_ms_write_register(dev, 0xf65e, 0x10); + + /* disable chroma interpolation */ + hsdaoh_ms_write_register(dev, 0xf600, 0x80); +} + +int hsdaoh_i2c_write_fn(void *dev, uint8_t addr, uint8_t *buf, uint16_t len) +{ + + return -1; +} + +int hsdaoh_i2c_read_fn(void *dev, uint8_t addr, uint8_t *buf, uint16_t len) +{ + + return -1; +} + +int hsdaoh_get_usb_strings(hsdaoh_dev_t *dev, char *manufact, char *product, + char *serial) +{ + struct libusb_device_descriptor dd; + libusb_device *device = NULL; + const int buf_max = 256; + int r = 0; + + if (!dev || !dev->devh) + return -1; + + device = libusb_get_device(dev->devh); + + r = libusb_get_device_descriptor(device, &dd); + if (r < 0) + return -1; + + if (manufact) { + memset(manufact, 0, buf_max); + libusb_get_string_descriptor_ascii(dev->devh, dd.iManufacturer, + (unsigned char *)manufact, + buf_max); + } + + if (product) { + memset(product, 0, buf_max); + libusb_get_string_descriptor_ascii(dev->devh, dd.iProduct, + (unsigned char *)product, + buf_max); + } + + if (serial) { + memset(serial, 0, buf_max); + libusb_get_string_descriptor_ascii(dev->devh, dd.iSerialNumber, + (unsigned char *)serial, + buf_max); + } + + return 0; +} + +int hsdaoh_set_sample_rate(hsdaoh_dev_t *dev, uint32_t samp_rate, bool ext_clock) +{ + return 0; +} + +uint32_t hsdaoh_get_sample_rate(hsdaoh_dev_t *dev) +{ + if (!dev) + return 0; + + return dev->rate; +} + +static hsdaoh_adapter_t *find_known_device(uint16_t vid, uint16_t pid) +{ + unsigned int i; + hsdaoh_adapter_t *device = NULL; + + for (i = 0; i < sizeof(known_devices)/sizeof(hsdaoh_adapter_t); i++ ) { + if (known_devices[i].vid == vid && known_devices[i].pid == pid) { + device = &known_devices[i]; + break; + } + } + + return device; +} + +uint32_t hsdaoh_get_device_count(void) +{ + int i,r; + libusb_context *ctx; + libusb_device **list; + uint32_t device_count = 0; + struct libusb_device_descriptor dd; + ssize_t cnt; + + r = libusb_init(&ctx); + if (r < 0) + return 0; + + cnt = libusb_get_device_list(ctx, &list); + + for (i = 0; i < cnt; i++) { + libusb_get_device_descriptor(list[i], &dd); + + if (find_known_device(dd.idVendor, dd.idProduct)) + device_count++; + } + + libusb_free_device_list(list, 1); + + libusb_exit(ctx); + + return device_count; +} + +const char *hsdaoh_get_device_name(uint32_t index) +{ + int i,r; + libusb_context *ctx; + libusb_device **list; + struct libusb_device_descriptor dd; + hsdaoh_adapter_t *device = NULL; + uint32_t device_count = 0; + ssize_t cnt; + + r = libusb_init(&ctx); + if (r < 0) + return ""; + + cnt = libusb_get_device_list(ctx, &list); + + for (i = 0; i < cnt; i++) { + libusb_get_device_descriptor(list[i], &dd); + + device = find_known_device(dd.idVendor, dd.idProduct); + + if (device) { + device_count++; + + if (index == device_count - 1) + break; + } + } + + libusb_free_device_list(list, 1); + + libusb_exit(ctx); + + if (device) + return device->name; + else + return ""; +} + +/* This function is a workaround for the fact that libuvc does not clear + * the halted UVC endpoint. After we properly close the UVC device, + * the endpoint becomes stalled, so after a restart of our library, + * no data is received. + * Thus, we need to manually claim the interface and clear the halted EP + * before we open the device with libuvc... + */ +int hsdaoh_clear_endpoint_halt(hsdaoh_dev_t *dev) +{ + int r; + + if (libusb_kernel_driver_active(dev->devh, 1) == 1) { + r = libusb_detach_kernel_driver(dev->devh, 1); + if (r < 0) { + fprintf(stderr, "Failed to detach UVC Kernel driver: %d\n", r); + return r; + } + } + + r = libusb_claim_interface(dev->devh, dev->hid_interface); + if (r < 0) { + fprintf(stderr, "usb_claim_interface hid error %d\n", r); + return r; + } + + r = libusb_claim_interface(dev->devh, 1); + if (r < 0) { + fprintf(stderr, "usb_claim_interface 1 error %d\n", r); + return r; + } + + r = libusb_clear_halt(dev->devh, LIBUSB_ENDPOINT_IN + 3); + if (r < 0) { + fprintf(stderr, "error clearing endpoint halt %d\n", r); + return r; + } + + return libusb_release_interface(dev->devh, 1); +} + +int _hsdaoh_open_uvc_device(hsdaoh_dev_t *dev) +{ + uvc_error_t r; + + /* Initialize UVC context. In theory it should be possible to re-use our + * libusb context, but for whatever reason that does not work */ + r = uvc_init(&dev->uvc_ctx, NULL); + + if (r < 0) { + uvc_perror(r, "uvc_init"); + return r; + } + + /* Locates the first attached UVC device, stores in uvc_dev */ + r = uvc_find_device(dev->uvc_ctx, &dev->uvc_dev, dev->vid, dev->pid, NULL); + + if (r < 0) { + uvc_perror(r, "uvc_find_device"); /* no devices found */ + } else { + + /* Try to open the device: requires exclusive access */ + r = uvc_open(dev->uvc_dev, &dev->uvc_devh); + + if (r < 0) { + uvc_perror(r, "uvc_open"); /* unable to open device */ + } + } + + return (int)r; +} + +int hsdaoh_open(hsdaoh_dev_t **out_dev, uint32_t index) +{ + int r; + int i; + libusb_device **list; + hsdaoh_dev_t *dev = NULL; + libusb_device *device = NULL; + uint32_t device_count = 0; + struct libusb_device_descriptor dd; + uint8_t reg; + ssize_t cnt; + + dev = malloc(sizeof(hsdaoh_dev_t)); + if (NULL == dev) + return -ENOMEM; + + memset(dev, 0, sizeof(hsdaoh_dev_t)); + + r = libusb_init(&dev->ctx); + if(r < 0){ + free(dev); + return -1; + } + +#if LIBUSB_API_VERSION >= 0x01000106 + libusb_set_option(dev->ctx, LIBUSB_OPTION_LOG_LEVEL, 3); +#else + libusb_set_debug(dev->ctx, 3); +#endif + + dev->dev_lost = 1; + + cnt = libusb_get_device_list(dev->ctx, &list); + + for (i = 0; i < cnt; i++) { + device = list[i]; + + libusb_get_device_descriptor(list[i], &dd); + + if (find_known_device(dd.idVendor, dd.idProduct)) { + device_count++; + } + + if (index == device_count - 1) + break; + + device = NULL; + } + + if (!device) { + r = -1; + goto err; + } + + dev->vid = dd.idVendor; + dev->pid = dd.idProduct; + + r = libusb_open(device, &dev->devh); + libusb_free_device_list(list, 1); + if (r < 0) { + fprintf(stderr, "usb_open error %d\n", r); + if(r == LIBUSB_ERROR_ACCESS) + fprintf(stderr, "Please fix the device permissions, e.g. " + "by installing the udev rules file\n"); + goto err; + } + + dev->hid_interface = 4; + if (libusb_kernel_driver_active(dev->devh, dev->hid_interface) == 1) { + + r = libusb_detach_kernel_driver(dev->devh, dev->hid_interface); + if (r < 0) { + fprintf(stderr, "Failed to detach HID Kernel driver: %d\n", r); + goto err; + } + } + + if (hsdaoh_clear_endpoint_halt(dev) < 0) + goto err; + + _hsdaoh_open_uvc_device(dev); + + + //dev->rate = DEFAULT_SAMPLERATE; + dev->dev_lost = 0; + +found: + *out_dev = dev; + + return 0; +err: + if (dev) { + if (dev->ctx) + libusb_exit(dev->ctx); + + free(dev); + } + + return r; +} + +int hsdaoh_close(hsdaoh_dev_t *dev) +{ + if (!dev) + return -1; + + if (HSDAOH_INACTIVE != dev->async_status) + uvc_stop_streaming(dev->uvc_devh); + + + uvc_close(dev->uvc_devh); + uvc_unref_device(dev->uvc_dev); + uvc_exit(dev->uvc_ctx); + + libusb_release_interface(dev->devh, dev->hid_interface); + + //TODO: only re-attach kernel driver if it was attached previously + if (!libusb_attach_kernel_driver(dev->devh, 4)) + fprintf(stderr, "Reattached kernel driver\n"); + else + fprintf(stderr, "Reattaching kernel driver failed!\n"); + + libusb_close(dev->devh); + libusb_exit(dev->ctx); + free(dev); + + return 0; +} + +/* callback for idle/filler data */ +inline int hsdaoh_check_idle_cnt(hsdaoh_dev_t *dev, uint16_t *buf, size_t length) +{ + int idle_counter_errors = 0; + + if (length == 0) + return 0; + + for (unsigned int i = 0; i < length; i++) { + if (buf[i] != ((dev->idle_cnt+1) & 0xffff)) + idle_counter_errors++; + + dev->idle_cnt = buf[i]; + } + + return idle_counter_errors; +} + +/* Extract the metadata stored in the upper 4 bits of the last word of each line */ +inline void hsdaoh_extract_metadata(uint8_t *data, metadata_t *metadata, unsigned int width) +{ + int j = 0; + uint8_t *meta = (uint8_t *)metadata; + + for (unsigned i = 0; i < sizeof(metadata_t)*2; i += 2) + meta[j++] = (data[((i+1)*width*2) - 1] >> 4) | (data[((i+2)*width*2) - 1] & 0xf0); +} + +void hsdaoh_process_frame(hsdaoh_dev_t *dev, uint8_t *data, int size) +{ + uint32_t frame_payload_bytes = 0; + + metadata_t meta; + hsdaoh_extract_metadata(data, &meta, dev->width); + + if (meta.magic != 0xda7acab1) + return; + + /* drop duplicated frames */ + if (meta.framecounter == dev->last_frame_cnt) + return; + + // FIXME: calculate number of dropped frames + if (meta.framecounter != ((dev->last_frame_cnt + 1) & 0xffff)) + printf("Missed at least one frame, fcnt %d, expected %d!\n", meta.framecounter, ((dev->last_frame_cnt + 1) & 0xffff)); + + dev->last_frame_cnt = meta.framecounter; + + int frame_error = 0; + + for (unsigned int i = 0; i < dev->height; i++) { + uint8_t *line_dat = data + (dev->width * 2 * i); + + /* extract number of payload words from reserved field at end of line */ + uint16_t payload_len = (line_dat[dev->width*2-1] << 8) | line_dat[dev->width*2-2]; + + /* we only use 12 bits, the upper 4 bits are reserved for the metadata */ + payload_len &= 0x0fff; + + if (payload_len > dev->width-1) { + fprintf(stderr, "Invalid payload length: %d\n", payload_len); + + /* discard frame */ + return; + } + + uint16_t idle_len = (dev->width-1) - payload_len; + int r = hsdaoh_check_idle_cnt(dev, (uint16_t *)line_dat + payload_len, idle_len); + + if (r) + printf("%d idle counter errors in line %d\n", r, i); + + if (payload_len > 0) + memmove(data + frame_payload_bytes, line_dat, payload_len * sizeof(uint16_t)); + + frame_payload_bytes += payload_len*2; + } + + if (dev->cb) + dev->cb(data, frame_payload_bytes, dev->cb_ctx); + + if (frame_error) { + fprintf(stderr,"%d counter errors, %d frames since last error\n", frame_error, dev->frames_since_error); + dev->frames_since_error = 0; + } else + dev->frames_since_error++; +} + +void _uvc_callback(uvc_frame_t *frame, void *ptr) +{ + hsdaoh_dev_t *dev = (hsdaoh_dev_t *)ptr; + + if (frame->frame_format != UVC_COLOR_FORMAT_YUYV) { + fprintf(stderr, "Error: incorrect frame format!\n"); + return; + } + + /* discard frame if it is incomplete (can happen after streaming was started) */ + if (frame->data_bytes != (dev->width * dev->height * 2)) + return; + + if (dev->discard_start_frames) { + dev->discard_start_frames--; + + if (dev->discard_start_frames == 5) + hsdaoh_ms_enable_transparent_mode(dev); + + return; + } + + hsdaoh_process_frame(dev, (uint8_t *)frame->data, frame->data_bytes); +} + +int hsdaoh_start_stream(hsdaoh_dev_t *dev, hsdaoh_read_cb_t cb, void *ctx) +{ + int r = 0; + + if (!dev) + return -1; + + if (HSDAOH_INACTIVE != dev->async_status) + return -2; + + dev->async_status = HSDAOH_RUNNING; + dev->async_cancel = 0; + + dev->cb = cb; + dev->cb_ctx = ctx; + + uvc_error_t res; + uvc_stream_ctrl_t ctrl; + + const uvc_format_desc_t *format_desc = uvc_get_format_descs(dev->uvc_devh); + const uvc_frame_desc_t *frame_desc = format_desc->frame_descs; + enum uvc_frame_format frame_format = UVC_FRAME_FORMAT_YUYV; + + dev->width = 1920; + dev->height = 1080; + dev->fps = 60; + + res = uvc_get_stream_ctrl_format_size(dev->uvc_devh, &ctrl, frame_format, dev->width, dev->height, dev->fps); + + if (res < 0) { + uvc_perror(res, "get_mode"); /* device doesn't provide a matching stream */ + } else { + /* start the UVC stream */ + dev->discard_start_frames = 60; + res = uvc_start_streaming(dev->uvc_devh, &ctrl, _uvc_callback, (void *)dev, 0); + + if (res < 0) { + uvc_perror(res, "start_streaming"); /* unable to start stream */ + } else { + puts("Streaming..."); + } + } + + return r; +} + +int hsdaoh_stop_stream(hsdaoh_dev_t *dev) +{ + if (!dev) + return -1; + + /* if streaming, try to cancel gracefully */ + if (HSDAOH_RUNNING == dev->async_status) { + dev->async_status = HSDAOH_CANCELING; + dev->async_cancel = 1; + + + + /* End the stream. Blocks until last callback is serviced */ + // uvc_stop_streaming(dev->uvc_devh); + // puts("Done streaming."); + + return 0; + } + + /* if called while in pending state, change the state forcefully */ +#if 0 + if (HSDAOH_INACTIVE != dev->async_status) { + dev->async_status = HSDAOH_INACTIVE; + return 0; + } +#endif + return -2; +}