From 120989e3d66bb61d82b18b4f45de41f2c1b640ac Mon Sep 17 00:00:00 2001 From: Jeff Weston Date: Wed, 11 Sep 2019 12:29:48 -0400 Subject: [PATCH] update to latest --- README.md | 12 +- VERSION.TXT | 5 +- android/README.TXT | 20 + android/build.gradle | 16 + .../wrapper/LICENSE-for-gradlewrapper.txt | 202 ++++++++ android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 1 + android/gradlew | 160 ++++++ android/gradlew.bat | 90 ++++ android/settings.gradle | 1 + examples/normStreamer.cpp | 124 ++++- include/normApi.h | 36 ++ include/normMessage.h | 132 +++-- include/normNode.h | 51 +- include/normObject.h | 9 +- include/normSession.h | 45 +- include/normVersion.h | 2 +- makefiles/Makefile.common | 2 +- .../AndroidManifest.xml | 0 makefiles/{android => android-ndk}/README | 8 + .../{android => android-ndk}/ant.properties | 0 makefiles/{android => android-ndk}/build.xml | 0 .../{android => android-ndk}/jni/Android.mk | 2 +- .../jni/Application.mk | 0 .../proguard-project.txt | 0 .../project.properties | 0 norm.pc.in | 11 + src/common/normApi.cpp | 154 +++++- src/common/normApp.cpp | 61 ++- src/common/normEncoderRS16.cpp | 4 +- src/common/normMessage.cpp | 2 +- src/common/normNode.cpp | 473 ++++++++++++++---- src/common/normObject.cpp | 140 +++--- src/common/normSession.cpp | 149 +++++- src/java/jni/normInstanceJni.cpp | 2 +- .../navy/nrl/norm/enums/NormEventType.java | 55 +- src/pynorm/constants.py | 12 +- src/pynorm/event.py | 2 + wscript | 20 +- 39 files changed, 1704 insertions(+), 299 deletions(-) create mode 100644 android/README.TXT create mode 100644 android/build.gradle create mode 100644 android/gradle/wrapper/LICENSE-for-gradlewrapper.txt create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100755 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/settings.gradle rename makefiles/{android => android-ndk}/AndroidManifest.xml (100%) rename makefiles/{android => android-ndk}/README (85%) rename makefiles/{android => android-ndk}/ant.properties (100%) rename makefiles/{android => android-ndk}/build.xml (100%) rename makefiles/{android => android-ndk}/jni/Android.mk (96%) rename makefiles/{android => android-ndk}/jni/Application.mk (100%) rename makefiles/{android => android-ndk}/proguard-project.txt (100%) rename makefiles/{android => android-ndk}/project.properties (100%) create mode 100644 norm.pc.in diff --git a/README.md b/README.md index 48020f3..980a03a 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ purposes, the authors would prefer that the code not be re-distributed so that users will obtain the latest (and most debugged/improved) release from the official NORM web site: - + SOURCE CODE @@ -102,14 +102,12 @@ NormUserGuide.pdf - Guide to "norm" demo app usage VERSION.TXT - NORM version history notes -README.TXT - this file +README.md - this file NOTES: ====== -The NORM code depends upon the current "Protolib" release. The NORM -source code tarballs contain an appropriate release of "Protolib" as -part of the NORM source tree. If the NORM code is checked out from -our CVS server, it is necessary to also check out "protolib" separately -and provide paths to it for NORM to build. +The NORM code depends upon the current "Protolib" release: + https://www.nrl.navy.mil/itd/ncs/products/protolib + https://github.com/USNavalResearchLaboratory/protolib diff --git a/VERSION.TXT b/VERSION.TXT index b6e6f76..74874d6 100644 --- a/VERSION.TXT +++ b/VERSION.TXT @@ -1,9 +1,10 @@ NORM Version History + Version 1.5.8 ============= - - Corrections to eliminate compiler warnings and suport kfreebsd builds - (Thanks to Luca Buccassi) + - Modified waf build to build pkg-config file.cThanks to Luca Boccassi + for this and some other patches for Gnu/Hurd, etc.) Version 1.5.7 ============= diff --git a/android/README.TXT b/android/README.TXT new file mode 100644 index 0000000..4d5a342 --- /dev/null +++ b/android/README.TXT @@ -0,0 +1,20 @@ +This directoy contains Android Studio project files for building the NORM +library for Android. + +TO BUILD: + +./gradlew build + +The lib/build/ directory will contain the binary libraries built. + +The "./gradlew clean" command can be used to delete the files built. + +This directory may also be opened as an Android Studio project and built from +the IDE GUI. + +This will evolve as we update NORM and associated projects to the newer Android +Studio tool chain. + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..651d63d --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,16 @@ +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.1' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} diff --git a/android/gradle/wrapper/LICENSE-for-gradlewrapper.txt b/android/gradle/wrapper/LICENSE-for-gradlewrapper.txt new file mode 100644 index 0000000..e06d208 --- /dev/null +++ b/android/gradle/wrapper/LICENSE-for-gradlewrapper.txt @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d384d28 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip \ No newline at end of file diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..2b49d90 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +include ':lib' \ No newline at end of file diff --git a/examples/normStreamer.cpp b/examples/normStreamer.cpp index bcc23ae..81c2604 100644 --- a/examples/normStreamer.cpp +++ b/examples/normStreamer.cpp @@ -71,7 +71,16 @@ class NormStreamer loopback = state; if (NORM_SESSION_INVALID != norm_session) NormSetMulticastLoopback(norm_session, state); - } + } + void SetFtiInfo(bool state) + { + fti_info = state; + if (NORM_SESSION_INVALID != norm_session) + NormLimitObjectInfo(norm_session, state); + } + + void SetAckEx(bool state) + {ack_ex = state;} bool EnableUdpRelay(const char* relayAddr, unsigned short relayPort); bool EnableUdpListener(unsigned short thePort, const char* groupAddr, const char * interfaceName); @@ -302,6 +311,8 @@ class NormStreamer bool norm_acking; bool tx_ack_pending; NormFlushMode flush_mode; // TBD - allow for "none", "passive", "active" options + bool fti_info; + bool ack_ex; // Receive stream and state variables for writing received messages to output NormObjectHandle rx_stream; @@ -324,7 +335,7 @@ class NormStreamer // These are some options mainly for testing purposes bool omit_header; // if "true", receive message length header is _not_ written to output - //bool rx_silent; + bool rx_silent; //double tx_loss; unsigned long input_byte_count; unsigned long tx_byte_count; @@ -351,11 +362,11 @@ NormStreamer::NormStreamer() tx_stream (NORM_OBJECT_INVALID), tx_ready(true), tx_stream_buffer_max(0), tx_stream_buffer_count(0), tx_stream_bytes_remain(0), tx_watermark_pending(false), norm_acking(false), tx_ack_pending(false), flush_mode(NORM_FLUSH_ACTIVE), - rx_stream(NORM_OBJECT_INVALID), rx_ready(false), rx_needed(false), msg_sync(false), + fti_info(false), ack_ex(false), rx_stream(NORM_OBJECT_INVALID), rx_ready(false), rx_needed(false), msg_sync(false), output_bucket_rate(0.0), output_bucket_interval(0.0), output_bucket_depth(0), output_bucket_count(0), output_socket(ProtoSocket::UDP), output_file(stdout), output_fd(fileno(stdout)), output_ready(true), output_msg_length(0), output_index(0), - omit_header(false), input_byte_count(0), tx_byte_count(0), + omit_header(false), rx_silent(false), input_byte_count(0), tx_byte_count(0), segment_size(1398), block_size(64), num_parity(0), auto_parity(0), stream_buffer_size(2*1024*1024), tx_socket_buffer_size(0), rx_socket_buffer_size(0), @@ -626,6 +637,8 @@ bool NormStreamer::Start(bool sender, bool receiver) gettimeofday(¤tTime, NULL); srand(currentTime.tv_usec); // seed random number generator NormSessionId instanceId = (NormSessionId)rand(); + if (fti_info) + NormLimitObjectInfo(norm_session, true); if (!NormStartSender(norm_session, instanceId, bufferSize, segment_size, block_size, num_parity)) { fprintf(stderr, "normStreamer error: unable to start NORM sender\n"); @@ -758,7 +771,15 @@ void NormStreamer::ReadInput() NormStreamClose(tx_stream, true); if (norm_acking) { - NormSetWatermark(norm_session, tx_stream, true); + if (ack_ex) + { + const char* req = "Hello, acker"; + NormSetWatermarkEx(norm_session, tx_stream, req, strlen(req) + 1, true); + } + else + { + NormSetWatermark(norm_session, tx_stream, true); + } tx_ack_pending = false; } input_needed = false; @@ -840,7 +861,16 @@ unsigned int NormStreamer::WriteToStream(const char* buffer, unsigned int numByt { // Initiate flow control ACK request //fprintf(stderr, "write-initiated flow control ACK REQUEST\n"); - NormSetWatermark(norm_session, tx_stream); + if (ack_ex) + { + const char* req = "Hello, acker"; + NormSetWatermarkEx(norm_session, tx_stream, req, strlen(req) + 1); + } + else + { + NormSetWatermark(norm_session, tx_stream); + } + tx_watermark_pending = true; tx_ack_pending = false; } @@ -915,7 +945,15 @@ void NormStreamer::FlushStream(bool eom, NormFlushMode flushMode) } if (setWatermark) { - NormSetWatermark(norm_session, tx_stream, true); + if (ack_ex) + { + const char* req = "Hello, acker"; + NormSetWatermarkEx(norm_session, tx_stream, req, strlen(req) + 1, true); + } + else + { + NormSetWatermark(norm_session, tx_stream, true); + } tx_ack_pending = false; } } @@ -1109,6 +1147,7 @@ void NormStreamer::HandleNormEvent(const NormEvent& event) break; case NORM_TX_WATERMARK_COMPLETED: + TRACE("NORM_TX_WATERMARK_COMPLETED ...\n"); if (NORM_ACK_SUCCESS == NormGetAckingStatus(norm_session)) { //fprintf(stderr, "WATERMARK COMPLETED\n"); @@ -1120,7 +1159,15 @@ void NormStreamer::HandleNormEvent(const NormEvent& event) //fprintf(stderr, "flow control ACK completed\n"); if (tx_ack_pending) { - NormSetWatermark(norm_session, tx_stream, true); + if (ack_ex) + { + const char* req = "Hello, acker"; + NormSetWatermarkEx(norm_session, tx_stream, req, strlen(req) + 1, true); + } + else + { + NormSetWatermark(norm_session, tx_stream, true); + } tx_ack_pending = false; } } @@ -1137,7 +1184,15 @@ void NormStreamer::HandleNormEvent(const NormEvent& event) if (tx_ack_pending) { // May as well advance the ack request point - NormSetWatermark(norm_session, tx_stream, true); + if (ack_ex) + { + const char* req = "Hello, acker"; + NormSetWatermarkEx(norm_session, tx_stream, req, strlen(req) + 1, true); + } + else + { + NormSetWatermark(norm_session, tx_stream, true); + } tx_ack_pending = false; } else @@ -1145,6 +1200,26 @@ void NormStreamer::HandleNormEvent(const NormEvent& event) NormResetWatermark(norm_session); } } + if (ack_ex) + { + // This iterates through the acking nodes looking for responses + // to our application-defined NormSetWatermarkEx() request + NormAckingStatus ackingStatus; + NormNodeId nodeId = NORM_NODE_NONE; // this inits NormGetNextAckingNode() iteration + while (NormGetNextAckingNode(event.session, &nodeId, &ackingStatus)) + { + if (NORM_ACK_SUCCESS == ackingStatus) + { + // This node acked, so look for AckEx response + // In our example/test case here, we use strings for the content + char buffer[256]; + buffer[0] = '\0'; + unsigned int buflen = 256; + if (NormGetAckEx(event.session, nodeId, buffer, &buflen)) + fprintf(stderr, "Received APP_ACK from node>%u \"%s\"\n", nodeId, buffer); + } + } + } break; case NORM_TX_OBJECT_PURGED: @@ -1209,6 +1284,19 @@ void NormStreamer::HandleNormEvent(const NormEvent& event) rx_needed = false; break; + case NORM_RX_ACK_REQUEST: + { + char buffer[256]; + buffer[0] = '\0'; + unsigned int buflen = 256; + NormNodeGetWatermarkEx(event.sender, buffer, &buflen); + fprintf(stderr, "Received NORM_RX_ACK_REQUEST: \"%s\"\n", buffer); + // Send a reply + const char* ack = "Yes, master"; + NormNodeSendAckEx(event.sender, ack, strlen(ack) + 1); + break; + } + default: break; } @@ -1218,7 +1306,7 @@ void NormStreamer::HandleNormEvent(const NormEvent& event) void Usage() { - fprintf(stderr, "Usage: normStreamer id {send|recv} [addr [/]][interface ][loopback]\n" + fprintf(stderr, "Usage: normStreamer id {send|recv} [addr [/]][interface ][loopback][info]\n" " [cc|cce|ccl|rate ][ack [,,...][flush {none|passive|active}]\n" " [listen [/]][linterface ]\n" " [relay /][limit [/]][output ]\n" @@ -1244,6 +1332,7 @@ void PrintHelp() " interface -- Specifies the name of the network interface on which to conduct NORM protocol\n" " (e.g., 'eth0')\n" " loopback -- Enables 'loopback' sessions on the same host machine. Required for multicast loopback.\n" + " info -- Limits FTI header extension to NORM_INFO message only (reduced overhead)\n" " rate -- sets fixed sender rate (and receiver token bucket rate if 'limit' option is used)\n" " [cc|cce|ccl] -- Enables optional NORM congestion control mode (overrides 'rate')\n" " ack [] -- Instructs sender to request positive acknowledgement from listed receiver nodes\n" @@ -1301,6 +1390,8 @@ int main(int argc, char* argv[]) unsigned int ackingNodeCount = 0; bool loopback = false; + bool ftiInfo = false; + bool ackEx = false; int debugLevel = 0; bool trace = false; const char* logFile = NULL; @@ -1343,6 +1434,15 @@ int main(int argc, char* argv[]) { loopback = true; } + else if (0 == strncmp(cmd, "info", len)) + { + ftiInfo = true; + } + else if (0 == strncmp(cmd, "ex", len)) + { + // This enables testing/demonstrating NormSetWatermarkEx() operation + ackEx = true; + } else if (0 == strncmp(cmd, "addr", len)) { if (i >= argc) @@ -1881,6 +1981,8 @@ int main(int argc, char* argv[]) normStreamer.SetStreamBufferSize(streamBufferSize); normStreamer.SetLoopback(loopback); + normStreamer.SetFtiInfo(ftiInfo); + normStreamer.SetAckEx(ackEx); if (omitHeaderOnOutput) normStreamer.OmitHeader(true); @@ -2129,7 +2231,7 @@ int main(int argc, char* argv[]) } // for debugging to see if anything gets "stuck" - if ((thisTime.tv_sec - lastTime.tv_sec) >= 1) + if ((thisTime.tv_sec - lastTime.tv_sec) >= 100) { if (send) fprintf(stderr, "normStreamer: inputNeeded:%d inputReady:%d txPending:%d txReady:%d inputCount:%lu txCount:%lu fdMask:%d\n", diff --git a/include/normApi.h b/include/normApi.h index 3beaae0..412e20b 100644 --- a/include/normApi.h +++ b/include/normApi.h @@ -188,6 +188,7 @@ typedef enum NormEventType NORM_RX_OBJECT_UPDATED, NORM_RX_OBJECT_COMPLETED, NORM_RX_OBJECT_ABORTED, + NORM_RX_ACK_REQUEST, // upon receipt of app-extended watermark ack request NORM_GRTT_UPDATED, NORM_CC_ACTIVE, NORM_CC_INACTIVE, @@ -328,6 +329,16 @@ void NormSetTxOnly(NormSessionHandle sessionHandle, bool txOnly, bool connectToSessionAddress DEFAULT(false)); +NORM_API_LINKAGE +void NormLimitObjectInfo(NormSessionHandle sessionHandle, bool state); // if true, FEC OTI in NORM_INFO only + +NORM_API_LINKAGE +bool NormPresetObjectInfo(NormSessionHandle sessionHandle, // FEC OTI is preset and not sent + unsigned long objectSize, // (most useful for NORM_OBJECT_STREAM) + UINT16 segmentSize, + UINT16 numData, + UINT16 numParity); + NORM_API_LINKAGE void NormSetId(NormSessionHandle sessionHandle, NormNodeId normId); @@ -576,6 +587,15 @@ NORM_API_LINKAGE bool NormSetWatermark(NormSessionHandle sessionHandle, NormObjectHandle objectHandle, bool overrideFlush DEFAULT(false)); + +NORM_API_LINKAGE +bool NormSetWatermarkEx(NormSessionHandle sessionHandle, + NormObjectHandle objectHandle, + const char* buffer, + unsigned int numBytes, + bool overrideFlush DEFAULT(false)); + + NORM_API_LINKAGE bool NormResetWatermark(NormSessionHandle sessionHandle); @@ -607,6 +627,12 @@ bool NormGetNextAckingNode(NormSessionHandle sessionHandle, NormNodeId* nodeId, NormAckingStatus* ackingStatus DEFAULT(0)); +NORM_API_LINKAGE +bool NormGetAckEx(NormSessionHandle sessionHandle, + NormNodeId nodeId, + char* buffer, + unsigned int* buflen); + NORM_API_LINKAGE bool NormSendCommand(NormSessionHandle sessionHandle, const char* cmdBuffer, @@ -784,6 +810,16 @@ bool NormNodeGetCommand(NormNodeHandle remoteSender, char* buffer, unsigned int* buflen); +NORM_API_LINKAGE +bool NormNodeSendAckEx(NormNodeHandle remoteSender, + const char* buffer, + unsigned int numBytes); + +NORM_API_LINKAGE +bool NormNodeGetWatermarkEx(NormNodeHandle remoteSender, + char* buffer, + unsigned int* buflen); + NORM_API_LINKAGE void NormNodeFreeBuffers(NormNodeHandle remoteSender); diff --git a/include/normMessage.h b/include/normMessage.h index c1bf7be..ae68765 100644 --- a/include/normMessage.h +++ b/include/normMessage.h @@ -183,7 +183,7 @@ const NormNodeId NORM_NODE_ANY = 0xffffffff; class NormObjectId { public: - NormObjectId() {}; + NormObjectId() : value(0) {}; NormObjectId(UINT16 id) {value = id;} NormObjectId(const NormObjectId& id) {value = id.value;} operator UINT16() const {return value;} @@ -331,14 +331,16 @@ class NormHeaderExtension INVALID = 0, FTI = 64, // FEC Object Transmission Information (FTI) extension CC_FEEDBACK = 3, // NORM-CC Feedback extension - CC_RATE = 128 // NORM-CC Rate extension + CC_RATE = 128, // NORM-CC Rate extension + APP_ACK = 65 // app-defined ACK extension (see NormSetWatermarkEx()) }; NormHeaderExtension(); virtual ~NormHeaderExtension() {} - virtual void Init(UINT32* theBuffer) + virtual void Init(UINT32* theBuffer, UINT16 numBytes) { - buffer = theBuffer; + // TBD - should we confirm that 'numBytes' is sufficient + AttachBuffer(theBuffer, numBytes); SetType(INVALID); SetWords(0); } @@ -347,13 +349,17 @@ class NormHeaderExtension void SetWords(UINT8 words) {((UINT8*)buffer)[LENGTH_OFFSET] = words;} - void AttachBuffer(const UINT32* theBuffer) {buffer = (UINT32*)theBuffer;} - const UINT32* GetBuffer() {return buffer;} + void AttachBuffer(const UINT32* theBuffer, UINT16 bufferLength) + { + buffer = (UINT32*)theBuffer; + buffer_length = bufferLength; + } + const UINT32* GetBuffer() + {return buffer;} Type GetType() const - { - return buffer ? (Type)(((UINT8*)buffer)[TYPE_OFFSET]) : INVALID; - } + {return buffer ? (Type)(((UINT8*)buffer)[TYPE_OFFSET]) : INVALID;} + UINT16 GetLength() const { return (buffer ? @@ -361,14 +367,27 @@ class NormHeaderExtension ((((UINT8*)buffer)[LENGTH_OFFSET]) << 2) : 4) : 0); } + // These currently only used for APP_ACK extension + const char* GetContent() + {return (((const char*)buffer) + CONTENT_OFFSET);} + + UINT16 GetContentLength() const + { + UINT16 totalLen = GetLength(); + return ((totalLen > CONTENT_OFFSET) ? (totalLen - CONTENT_OFFSET) : 0); + } + static UINT16 GetContentOffset() + {return (UINT16)CONTENT_OFFSET;} protected: enum { - TYPE_OFFSET = 0, // UINT8 offset - LENGTH_OFFSET = TYPE_OFFSET + 1 // UINT8 offset + TYPE_OFFSET = 0, // UINT8 offset + LENGTH_OFFSET = TYPE_OFFSET + 1, // UINT8 offset + CONTENT_OFFSET = LENGTH_OFFSET + 1 // UINT8 offset }; UINT32* buffer; + UINT16 buffer_length; }; // end class NormHeaderExtension @@ -594,9 +613,15 @@ class NormMsg void AttachExtension(NormHeaderExtension& extension) { - extension.Init(buffer+(header_length/4)); + extension.Init(buffer+(header_length/4), MAX_SIZE - header_length); ExtendHeaderLength(extension.GetLength()); } + // Only use this for extensions that have content appended after attachment + // (Fixed-length extensions should set their length upon Init()) + void PackExtension(NormHeaderExtension& extension) + { + ExtendHeaderLength(2 + extension.GetContentLength()); + } // Message processing routines bool InitFromBuffer(UINT16 msgLength); @@ -610,7 +635,7 @@ class NormMsg {return (((UINT8*)buffer)[VERSION_OFFSET] >> 4);} NormMsg::Type GetType() const {return (Type)(((UINT8*)buffer)[TYPE_OFFSET] & 0x0f);} - UINT16 GetHeaderLength() + UINT16 GetHeaderLength() const {return ((UINT8*)buffer)[HDR_LEN_OFFSET] << 2;} UINT16 GetBaseHeaderLength() {return header_length_base;} @@ -633,12 +658,21 @@ class NormMsg bool HasExtensions() const {return (header_length > header_length_base);} bool GetNextExtension(NormHeaderExtension& ext) const { - const UINT32* currentBuffer = ext.GetBuffer(); + const UINT32* currentBuffer = ext.GetBuffer(); + // 'nextOffset' here is a UINT32 offset UINT16 nextOffset = (UINT16)(currentBuffer ? (currentBuffer - buffer + (ext.GetLength()/4)) : - (header_length_base/4)); + (header_length_base/4)); bool result = HasExtensions() ? (nextOffset < (header_length/4)) : false; - ext.AttachBuffer(result ? (buffer+nextOffset) : (UINT32*)NULL); + if (result) + { + UINT16 nextLength = ((UINT8*)(buffer + nextOffset))[1] << 2; + ext.AttachBuffer(buffer+nextOffset, nextLength); + } + else + { + ext.AttachBuffer((UINT32*)NULL, 0); + } return result; } @@ -753,9 +787,9 @@ class NormFtiExtension2 : public NormHeaderExtension { public: // To build the FTI Header Extension - virtual void Init(UINT32* theBuffer) + virtual void Init(UINT32* theBuffer, UINT16 numBytes) { - AttachBuffer(theBuffer); + AttachBuffer(theBuffer, numBytes); SetType(FTI); // HET = 64 SetWords(4); } @@ -814,6 +848,16 @@ class NormFtiData num_data(0), num_parity(0), fec_m(0), instance_id(0) {} ~NormFtiData() {} + bool IsValid() const + {return (0 != segment_size);} + + void Invalidate() + { + object_size = 0; + segment_size = num_data = num_parity = instance_id = 0; + fec_m = 0; + } + void SetObjectSize(const NormObjectSize& objectSize) {object_size = objectSize;} void SetSegmentSize(UINT16 segmentSize) @@ -850,16 +894,15 @@ class NormFtiData }; // end class NormFtiData - // This FEC Object Transmission Information assumes "fec_id" == 5 (RFC 5510) // (this is the fully-defined 8-bit Reed-Solomon codec) class NormFtiExtension5 : public NormHeaderExtension { public: // To build the fec_id=5 FTI Header Extension - virtual void Init(UINT32* theBuffer) + virtual void Init(UINT32* theBuffer, UINT16 numBytes) { - AttachBuffer(theBuffer); + AttachBuffer(theBuffer, numBytes); SetType(FTI); // HET = 64 SetWords(3); } @@ -891,7 +934,7 @@ class NormFtiExtension5 : public NormHeaderExtension private: enum { - OBJ_SIZE_MSB_OFFSET = (LENGTH_OFFSET + 1)/2, + OBJ_SIZE_MSB_OFFSET = (CONTENT_OFFSET)/2, // UINT16 offset OBJ_SIZE_LSB_OFFSET = ((OBJ_SIZE_MSB_OFFSET*2)+2)/4, SEG_SIZE_OFFSET = ((OBJ_SIZE_LSB_OFFSET*4)+4)/2, FEC_NDATA_OFFSET = ((SEG_SIZE_OFFSET+1)*2), @@ -899,6 +942,37 @@ class NormFtiExtension5 : public NormHeaderExtension }; }; // end class NormFtiExtension5 +class NormAppAckExtension : public NormHeaderExtension +{ + public: + virtual void Init(UINT32* theBuffer, UINT16 numBytes) + { + AttachBuffer(theBuffer, numBytes); + SetType(APP_ACK); + SetWords(0); + } + bool SetContent(const char* data, UINT16 dataLen) + { + if (dataLen > (buffer_length - CONTENT_OFFSET)) return false; + memcpy(((char*)buffer) + CONTENT_OFFSET, data, dataLen); + if (dataLen > 2) + { + // Pad out to get 32-bit alignment of extension + dataLen += CONTENT_OFFSET; + UINT16 padLen = dataLen % 4; + if (padLen) + padLen = 4 - padLen; + dataLen += padLen; + SetWords(dataLen/4); + } + else + { + SetWords(1); + } + return true; + } +}; // end class NormAppAckExtension + // This FEC Object Transmission Information assumes "fec_id" == 129 class NormFtiExtension129 : public NormHeaderExtension @@ -906,9 +980,9 @@ class NormFtiExtension129 : public NormHeaderExtension public: // To build the FTI Header Extension // (TBD) allow for different "fec_id" types in the future - virtual void Init(UINT32* theBuffer) + virtual void Init(UINT32* theBuffer, UINT16 numBytes) { - AttachBuffer(theBuffer); + AttachBuffer(theBuffer, numBytes); SetType(FTI); SetWords(4); } @@ -1204,7 +1278,7 @@ class NormCmdFlushMsg : public NormCmdMsg {length = header_length;} bool AppendAckingNode(NormNodeId nodeId, UINT16 segmentSize) { - if ((length-header_length+ 4) > segmentSize) return false; + if ((length-header_length + 4) > segmentSize) return false; buffer[length/4] = htonl((UINT32)nodeId); length += 4; return true; @@ -1448,9 +1522,9 @@ class NormCCRateExtension : public NormHeaderExtension { public: - virtual void Init(UINT32* theBuffer) + virtual void Init(UINT32* theBuffer, UINT16 numBytes) { - AttachBuffer(theBuffer); + AttachBuffer(theBuffer, numBytes); SetType(CC_RATE); ((UINT8*)buffer)[RESERVED_OFFSET] = 0; } @@ -1668,9 +1742,9 @@ class NormCmdRepairAdvMsg : public NormCmdMsg class NormCCFeedbackExtension : public NormHeaderExtension { public: - virtual void Init(UINT32* theBuffer) + virtual void Init(UINT32* theBuffer, UINT16 numBytes) { - AttachBuffer(theBuffer); + AttachBuffer(theBuffer, numBytes); SetType(CC_FEEDBACK); SetWords(3); ((UINT8*)buffer)[CC_FLAGS_OFFSET] = 0; diff --git a/include/normNode.h b/include/normNode.h index 320c712..cc24c5a 100644 --- a/include/normNode.h +++ b/include/normNode.h @@ -206,10 +206,21 @@ class NormAckingNode : public NormNode unsigned int GetReqCount() const {return req_count;} bool AckReceived() const {return ack_received;} void MarkAckReceived() {ack_received = true;} + + bool SetAckEx(const char* buffer, UINT16 numBytes); + bool GetAckEx(char* buffer, unsigned int* buflen); + + /* + const char* GetAppAckContent() const + {return (const char*)ack_ex_buffer;} + unsigned int GetAppAckLength() const + {return ack_ex_length;} */ private: bool ack_received; // was ack received? unsigned int req_count; // remaining request attempts + char* ack_ex_buffer; + unsigned int ack_ex_length; }; // end NormAckingNode @@ -297,7 +308,7 @@ class NormSenderNode : public NormNode, public ProtoTree::Item UINT16 segmentSize , UINT16 numData, UINT16 numParity); - bool GetFtiData(const NormObjectMsg& msg, NormFtiData& ftiData); + bool GetFtiData(const NormObjectMsg& msg, NormFtiData& ftiData); // Parameters NormObject::NackingMode GetDefaultNackingMode() const @@ -359,7 +370,7 @@ class NormSenderNode : public NormNode, public ProtoTree::Item UINT16 segmentSize, UINT16 numData, UINT16 numParity); - bool BuffersAllocated() {return (0 != segment_size);} + bool BuffersAllocated() {return (NULL != retrieval_pool);} void FreeBuffers(); void Activate(bool isObjectMsg); @@ -370,7 +381,7 @@ class NormSenderNode : public NormNode, public ProtoTree::Item UINT8 GetFecFieldSize() const - {return fec_m;} + {return fti_data.GetFecFieldSize();} bool GetFirstPending(NormObjectId& objectId) const { @@ -408,9 +419,9 @@ class NormSenderNode : public NormNode, public ProtoTree::Item return NULL; } - UINT16 SegmentSize() {return segment_size;} - UINT16 BlockSize() {return ndata;} - UINT16 NumParity() {return nparity;} + UINT16 SegmentSize() const {return fti_data.GetSegmentSize();} + UINT16 BlockSize() const {return fti_data.GetFecMaxBlockLen();} + UINT16 NumParity() const {return fti_data.GetFecNumParity();} NormBlock* GetFreeBlock(NormObjectId objectId, NormBlockId blockId); void PutFreeBlock(NormBlock* block) @@ -425,7 +436,7 @@ class NormSenderNode : public NormNode, public ProtoTree::Item void SetErasureLoc(UINT16 index, UINT16 value) { - ASSERT(index < nparity); + ASSERT(index < NumParity()); erasure_loc[index] = value; } UINT16 GetErasureLoc(UINT16 index) @@ -434,7 +445,7 @@ class NormSenderNode : public NormNode, public ProtoTree::Item } void SetRetrievalLoc(UINT16 index, UINT16 value) { - ASSERT(index < ndata); + ASSERT(index < BlockSize()); retrieval_loc[index] = value; } UINT16 GetRetrievalLoc(UINT16 index) @@ -444,7 +455,7 @@ class NormSenderNode : public NormNode, public ProtoTree::Item char* GetRetrievalSegment() { char* s = retrieval_pool[retrieval_index++]; - retrieval_index = (retrieval_index >= ndata) ? 0 : retrieval_index; + retrieval_index = (retrieval_index >= BlockSize()) ? 0 : retrieval_index; return s; } @@ -458,9 +469,9 @@ class NormSenderNode : public NormNode, public ProtoTree::Item // Statistics kept on sender unsigned long CurrentBufferUsage() const - {return (segment_size * segment_pool.CurrentUsage());} + {return (SegmentSize() * segment_pool.CurrentUsage());} unsigned long PeakBufferUsage() const - {return (segment_size * segment_pool.PeakUsage());} + {return (SegmentSize() * segment_pool.PeakUsage());} unsigned long BufferOverunCount() const {return segment_pool.OverunCount() + block_pool.OverrunCount();} @@ -530,6 +541,9 @@ class NormSenderNode : public NormNode, public ProtoTree::Item bool ReadNextCmd(char* buffer, unsigned int* buflen); + bool SendAckEx(const char* data, unsigned int numBytes); + bool GetWatermarkEx(char* buffer, unsigned int* buflen); + void SetAddress(const ProtoAddress& address) { unsigned int len = address.GetLength(); @@ -591,11 +605,15 @@ class NormSenderNode : public NormNode, public ProtoTree::Item UINT16 max_pending_range; // max range of pending objs allowed bool is_open; - UINT16 segment_size; + // TBD - embed the FTI parameters into a NormFtiData object UINT8 fec_id; - UINT8 fec_m; - unsigned int ndata; - unsigned int nparity; + NormFtiData fti_data; + bool preset_fti; + //UINT16 segment_size; + //UINT8 fec_id; + //UINT8 fec_m; + //unsigned int ndata; + //unsigned int nparity; NormStreamObject* preset_stream; NormObjectTable rx_table; @@ -622,6 +640,9 @@ class NormSenderNode : public NormNode, public ProtoTree::Item NormObjectId watermark_object_id; NormBlockId watermark_block_id; NormSegmentId watermark_segment_id; + bool ack_ex_pending; + char* ack_ex_buffer; + unsigned int ack_ex_length; // Remote sender grtt measurement state double grtt_estimate; diff --git a/include/normObject.h b/include/normObject.h index 7b366e9..7f0212f 100644 --- a/include/normObject.h +++ b/include/normObject.h @@ -89,7 +89,14 @@ class NormObject info_len = 0; pending_info = false; } - + // This is used to bootstrap reception sender + // is using FTI_INFO FtiMode + void SetPendingInfo(bool state, UINT8 fecId) + { + pending_info = true; + fec_id = fecId; + } + const char* GetInfo() const {return info_ptr;} UINT16 GetInfoLength() const {return info_len;} bool IsStream() const {return (STREAM == type);} diff --git a/include/normSession.h b/include/normSession.h index 379c4c5..03f076f 100644 --- a/include/normSession.h +++ b/include/normSession.h @@ -51,6 +51,7 @@ class NormController RX_OBJECT_UPDATED, RX_OBJECT_COMPLETED, RX_OBJECT_ABORTED, + RX_ACK_REQUEST, // upon receipt of app-extended watermark ack request GRTT_UPDATED, CC_ACTIVE, // posted when cc feedback is detected CC_INACTIVE, // posted when no cc feedback and min rate reached @@ -166,6 +167,14 @@ class NormSession TRACK_SENDERS = 0x02, TRACK_ALL = 0x03 }; + + // Object FEC Transport Information (FTI) mode + enum FtiMode + { + FTI_PRESET = 0, // Receivers have preset FTI, don't send + FTI_INFO = 1, // Send FTI in NORM_INFO messages only + FTI_ALWAYS = 2 // Send FTI in NORM_DATA and NORM_INFO messages + }; // General methods void SetNodeId(NormNodeId nodeId) @@ -384,11 +393,14 @@ class NormSession NormObject* SenderFindTxObject(NormObjectId objectId) {return tx_table.Find(objectId);} - // postive ack mgmnt - void SenderSetWatermark(NormObjectId objectId, + // postive ack mgmnt (can only fail when 'appAckReq' is set) + bool SenderSetWatermark(NormObjectId objectId, NormBlockId blockId, NormSegmentId segmentId, - bool overrideFlush = false); + bool overrideFlush = false, + const char* appAckReq = NULL, + unsigned int appAckReqLen = 0); + void SenderResetWatermark(); void SenderCancelWatermark(); @@ -400,6 +412,7 @@ class NormSession AckingStatus SenderGetAckingStatus(NormNodeId nodeId); // Set "prevNodeId = NORM_NODE_NONE" to init this iteration (returns "false" when done) bool SenderGetNextAckingNode(NormNodeId& prevNodeId, AckingStatus* ackingStatus = NULL); + bool SenderGetAckEx(NormNodeId nodeId, char* buffer, unsigned int* buflen); NormAckingNode* SenderFindAckingNode(NormNodeId nodeId) const { @@ -486,6 +499,11 @@ class NormSession gsize_advertised = NormUnquantizeGroupSize(gsize_quantized); } + FtiMode SenderFtiMode() const + {return fti_mode;} + void SenderSetFtiMode(FtiMode ftiMode) + {fti_mode = ftiMode;} + void SenderEncode(unsigned int segmentId, const char* segment, char** parityVectorList) {encoder->Encode(segmentId, segment, parityVectorList);} @@ -530,6 +548,21 @@ class NormSession UINT16 numParity, unsigned int streamBufferSize = 0); + bool SetPresetFtiData(unsigned int objectSize, + UINT16 segmentSize, + UINT16 numData, + UINT16 numParity); + + bool GetPresetFtiData(NormFtiData& ftiData) + { + if (preset_fti.IsValid()) + { + ftiData = preset_fti; + return true; + } + return false; + } + void ReceiverSetUnicastNacks(bool state) {unicast_nacks = state;} bool ReceiverGetUnicastNacks() const @@ -724,6 +757,7 @@ class NormSession bool sndr_emcon; bool tx_only; bool tx_connect; + FtiMode fti_mode; NormObjectTable tx_table; ProtoSlidingMask tx_pending_mask; @@ -813,6 +847,10 @@ class NormSession ProtoTimer cmd_timer; bool syn_status; + // Sender "app-defined" ACK_REQUEST state (for NormSetWatermarkEx()) + char* ack_ex_buffer; + unsigned int ack_ex_length; + // Receiver parameters bool is_receiver; int rx_robust_factor; @@ -828,6 +866,7 @@ class NormSession NormObject::NackingMode default_nacking_mode; NormSenderNode::SyncPolicy default_sync_policy; UINT16 rx_cache_count_max; + NormFtiData preset_fti; // For NormSocket server-listener support bool is_server_listener; diff --git a/include/normVersion.h b/include/normVersion.h index 4e969fa..4be5779 100644 --- a/include/normVersion.h +++ b/include/normVersion.h @@ -36,6 +36,6 @@ #ifndef _NORM_VERSION #define _NORM_VERSION -#define VERSION "1.5.8" +#define VERSION "1.5.9" #endif // _NORM_VERSION diff --git a/makefiles/Makefile.common b/makefiles/Makefile.common index e1bdf27..5c698d3 100644 --- a/makefiles/Makefile.common +++ b/makefiles/Makefile.common @@ -14,7 +14,7 @@ NS = ../src/sim/ns INCLUDES = $(SYSTEM_INCLUDES) -I$(UNIX) -I../include -I$(PROTOLIB)/include -CFLAGS = -g -DPROTO_DEBUG -DUNIX -D_FILE_OFFSET_BITS=64 -O $(SYSTEM_CFLAGS) $(SYSTEM_HAVES) $(INCLUDES) +CFLAGS = -g -DPROTO_DEBUG -DUNIX -D_FILE_OFFSET_BITS=64 -O $(SYSTEM_CFLAGS) $(SYSTEM_HAVES) $(INCLUDES) -Wno-attributes #CFLAGS = -g -DPROTO_DEBUG -DUNIX -D_FILE_OFFSET_BITS=64 $(SYSTEM_CFLAGS) $(SYSTEM_HAVES) $(INCLUDES) LDFLAGS = $(SYSTEM_LDFLAGS) diff --git a/makefiles/android/AndroidManifest.xml b/makefiles/android-ndk/AndroidManifest.xml similarity index 100% rename from makefiles/android/AndroidManifest.xml rename to makefiles/android-ndk/AndroidManifest.xml diff --git a/makefiles/android/README b/makefiles/android-ndk/README similarity index 85% rename from makefiles/android/README rename to makefiles/android-ndk/README index 738fd34..47e7fb4 100644 --- a/makefiles/android/README +++ b/makefiles/android-ndk/README @@ -1,3 +1,11 @@ + +IMPORTANT NOTE: This "norm/makefiles/android-ndk" directory contains build +files for the ndk-build command more commonly used in older Android SDK +installations. The newer "norm/android" directory has gradle/CMake files +that are compatible with Android Studio or for command-line builds using the +provided "./gradlew" script. + + This directory contains an Android NDK import module for NORM, which builds a shared library, and the JNI bindings. diff --git a/makefiles/android/ant.properties b/makefiles/android-ndk/ant.properties similarity index 100% rename from makefiles/android/ant.properties rename to makefiles/android-ndk/ant.properties diff --git a/makefiles/android/build.xml b/makefiles/android-ndk/build.xml similarity index 100% rename from makefiles/android/build.xml rename to makefiles/android-ndk/build.xml diff --git a/makefiles/android/jni/Android.mk b/makefiles/android-ndk/jni/Android.mk similarity index 96% rename from makefiles/android/jni/Android.mk rename to makefiles/android-ndk/jni/Android.mk index 6332712..319e077 100644 --- a/makefiles/android/jni/Android.mk +++ b/makefiles/android-ndk/jni/Android.mk @@ -60,4 +60,4 @@ LOCAL_SRC_FILES := \ ../../../examples/normMsgr.cpp include $(BUILD_EXECUTABLE) -$(call import-module,protolib/makefiles/android/jni) +$(call import-module,protolib/makefiles/android-ndk/jni) diff --git a/makefiles/android/jni/Application.mk b/makefiles/android-ndk/jni/Application.mk similarity index 100% rename from makefiles/android/jni/Application.mk rename to makefiles/android-ndk/jni/Application.mk diff --git a/makefiles/android/proguard-project.txt b/makefiles/android-ndk/proguard-project.txt similarity index 100% rename from makefiles/android/proguard-project.txt rename to makefiles/android-ndk/proguard-project.txt diff --git a/makefiles/android/project.properties b/makefiles/android-ndk/project.properties similarity index 100% rename from makefiles/android/project.properties rename to makefiles/android-ndk/project.properties diff --git a/norm.pc.in b/norm.pc.in new file mode 100644 index 0000000..d824ab6 --- /dev/null +++ b/norm.pc.in @@ -0,0 +1,11 @@ +prefix=@PREFIX@ +exec_prefix=${prefix} +libdir=@LIBDIR@ +includedir=${prefix}/include + +Name: norm +Version: @VERSION@ +Description: NACK-Oriented Reliable Multicast (NORM) library +Libs: -L${libdir} -lnorm +Libs.private: @STATIC_LIBS@ +Cflags: -I${includedir} diff --git a/src/common/normApi.cpp b/src/common/normApi.cpp index ff510aa..7379d96 100644 --- a/src/common/normApi.cpp +++ b/src/common/normApi.cpp @@ -1195,7 +1195,47 @@ void NormSetTxOnly(NormSessionHandle sessionHandle, } } // end NormSetTxOnly() -// These functions are used internally by the NormSocket API extensiont +NORM_API_LINKAGE +bool NormPresetObjectInfo(NormSessionHandle sessionHandle, + unsigned long objectSize, + UINT16 segmentSize, + UINT16 numData, + UINT16 numParity) +{ + bool result = false; + NormInstance* instance = NormInstance::GetInstanceFromSession(sessionHandle); + if (instance && instance->dispatcher.SuspendThread()) + { + NormSession* session = (NormSession*)sessionHandle; + if (session) + { + result = session->SetPresetFtiData(objectSize, segmentSize, numData, numParity); + if (result) session->SenderSetFtiMode(NormSession::FTI_PRESET); + } + instance->dispatcher.ResumeThread(); + } + return result; +} // end NormPresetObjectInfo() + +NORM_API_LINKAGE +void NormLimitObjectInfo(NormSessionHandle sessionHandle, bool state) +{ + NormInstance* instance = NormInstance::GetInstanceFromSession(sessionHandle); + if (instance && instance->dispatcher.SuspendThread()) + { + NormSession* session = (NormSession*)sessionHandle; + if (session) + { + if (state) + session->SenderSetFtiMode(NormSession::FTI_INFO); + else + session->SenderSetFtiMode(NormSession::FTI_ALWAYS); + } + instance->dispatcher.ResumeThread(); + } +} // end NormLimitObjectInfo() + +// These functions are used internally by the NormSocket API extension NORM_API_LINKAGE void NormSetId(NormSessionHandle sessionHandle, NormNodeId normId) @@ -2046,6 +2086,49 @@ bool NormSetWatermark(NormSessionHandle sessionHandle, return result; } // end NormSetWatermark() +NORM_API_LINKAGE +bool NormSetWatermarkEx(NormSessionHandle sessionHandle, + NormObjectHandle objectHandle, + const char* buffer, + unsigned int numBytes, + bool overrideFlush) +{ + bool result = false; + NormInstance* instance = NormInstance::GetInstanceFromSession(sessionHandle); + if (instance && instance->dispatcher.SuspendThread()) + { + NormSession* session = (NormSession*)sessionHandle; + NormObject* obj = (NormObject*)objectHandle; + if (session && obj) + { + // Purge any existing NORM_TX_WATERMARK_COMPLETED notifications to be safe + instance->PurgeNotifications(sessionHandle, NORM_TX_WATERMARK_COMPLETED); + // (segmentId doesn't matter for non-stream) + if (obj->IsStream()) + { + NormStreamObject* stream = static_cast(obj); + result = session->SenderSetWatermark(stream->GetId(), + stream->FlushBlockId(), + stream->FlushSegmentId(), + overrideFlush, + buffer, numBytes); + } + else + { + NormBlockId blockId = obj->GetFinalBlockId(); + NormSegmentId segmentId = obj->GetBlockSize(blockId) - 1; + result = session->SenderSetWatermark(obj->GetId(), + blockId, + segmentId, + overrideFlush, + buffer, numBytes); + } + } + instance->dispatcher.ResumeThread(); + } + return result; +} // end NormSetWatermarkEx() + NORM_API_LINKAGE bool NormResetWatermark(NormSessionHandle sessionHandle) { @@ -2188,6 +2271,25 @@ bool NormGetNextAckingNode(NormSessionHandle sessionHandle, } } // end NormGetNextAckingNode() +NORM_API_LINKAGE +bool NormGetAckEx(NormSessionHandle sessionHandle, + NormNodeId nodeId, + char* buffer, + unsigned int* buflen) +{ + NormInstance* instance = NormInstance::GetInstanceFromSession(sessionHandle); + if (instance && instance->dispatcher.SuspendThread()) + { + NormSession* session = (NormSession*)sessionHandle; + bool result = (NormAckingStatus)session->SenderGetAckEx(nodeId, buffer, buflen); + instance->dispatcher.ResumeThread(); + return result; + } + if (NULL != buflen) *buflen = 0; + return false; +} // end NormGetAckEx() + + NORM_API_LINKAGE bool NormSendCommand(NormSessionHandle sessionHandle, const char* cmdBuffer, @@ -2523,8 +2625,8 @@ UINT16 NormObjectGetInfoLength(NormObjectHandle objectHandle) NORM_API_LINKAGE UINT16 NormObjectGetInfo(NormObjectHandle objectHandle, - char* buffer, - UINT16 bufferLen) + char* buffer, + UINT16 bufferLen) { UINT16 result = 0; if (NORM_OBJECT_INVALID != objectHandle) @@ -2816,6 +2918,52 @@ bool NormNodeGetCommand(NormNodeHandle nodeHandle, return result; } // end NormNodeGetCommand() +NORM_API_LINKAGE +bool NormNodeSendAckEx(NormNodeHandle nodeHandle, + const char* buffer, + unsigned int numBytes) +{ + bool result = false; + if (NORM_NODE_INVALID != nodeHandle) + { + NormInstance* instance = NormInstance::GetInstanceFromNode(nodeHandle); + if (instance && instance->dispatcher.SuspendThread()) + { + NormNode* node = (NormNode*)nodeHandle; + if (NormNode::SENDER == node->GetType()) + { + NormSenderNode* sender = static_cast(node); + result = sender->SendAckEx(buffer, numBytes); + } + instance->dispatcher.ResumeThread(); + } + } + return result; +} // end NormNodeSendAckEx() + +NORM_API_LINKAGE +bool NormNodeGetWatermarkEx(NormNodeHandle nodeHandle, + char* buffer, + unsigned int* buflen) +{ + bool result = false; + if (NORM_NODE_INVALID != nodeHandle) + { + NormInstance* instance = NormInstance::GetInstanceFromNode(nodeHandle); + if (instance && instance->dispatcher.SuspendThread()) + { + NormNode* node = (NormNode*)nodeHandle; + if (NormNode::SENDER == node->GetType()) + { + NormSenderNode* sender = static_cast(node); + result = sender->GetWatermarkEx(buffer, buflen); + } + instance->dispatcher.ResumeThread(); + } + } + return result; +} // end NormNodeGetWatermarkEx() + NORM_API_LINKAGE void NormNodeFreeBuffers(NormNodeHandle nodeHandle) { diff --git a/src/common/normApp.cpp b/src/common/normApp.cpp index 3a9c527..98f9bd3 100644 --- a/src/common/normApp.cpp +++ b/src/common/normApp.cpp @@ -67,6 +67,14 @@ class NormApp : public NormController, public ProtoApp char file_path[PATH_MAX]; NormObjectId object_id; }; // end class NormApp::FileCacheItem + + class Notification : public ProtoList::Item + { + public: + NormController::Event event; + }; // end class NormApp::Notification + class NotificationQueue : public ProtoListTemplate {}; + Notification* GetNotification(); // get from pool or create as needed void ShowHelp(); void OnInputReady(); @@ -98,6 +106,9 @@ class NormApp : public NormController, public ProtoApp NormSession* session; NormStreamObject* tx_stream; NormStreamObject* rx_stream; + NotificationQueue notify_queue; + NotificationQueue notify_pool; + bool notify_reentry; // application parameters FILE* input; // input stream @@ -215,7 +226,7 @@ NormApp::FileCacheItem::~FileCacheItem() NormApp::NormApp() : control_pipe(ProtoPipe::MESSAGE), control_remote(false), session_mgr(GetTimerMgr(), GetSocketNotifier(), &dispatcher), - session(NULL), tx_stream(NULL), rx_stream(NULL), input(NULL), output(NULL), + session(NULL), tx_stream(NULL), rx_stream(NULL), notify_reentry(false), input(NULL), output(NULL), output_io_buffer(NULL), output_io_bufsize(0), input_index(0), input_length(0), input_active(false), push_mode(false), msg_flush_mode(NormStreamObject::FLUSH_PASSIVE), @@ -257,6 +268,8 @@ NormApp::NormApp() NormApp::~NormApp() { + notify_queue.Destroy(); + notify_pool.Destroy(); if (address) delete[] address; if (interface_name) delete[] interface_name; @@ -272,6 +285,20 @@ NormApp::~NormApp() } } +NormApp::Notification* NormApp::GetNotification() +{ + Notification* notification = notify_pool.RemoveHead(); + if (NULL == notification) + { + if (NULL == (notification = new Notification())) + { + PLOG(PL_ERROR, "NormApp::GetNotification() new Notification error: %s\n", GetErrorString()); + return NULL; + } + } + return notification; +} // end NormApp::GetNotification() + // NOTE on message flushing mode: // // "none" - messages are aggregated to make NORM_DATA @@ -1557,6 +1584,18 @@ void NormApp::Notify(NormController::Event event, class NormNode* sender, class NormObject* object) { + if (notify_reentry) + { + Notification* notification; + if (NULL == (notification = GetNotification())) + { + PLOG(PL_ERROR, "NormApp::Notify() error: unable to create Notification item!\n"); + return; + } + notification->event = event; + return; + } + notify_reentry = true; switch (event) { case TX_QUEUE_VACANCY: @@ -1804,7 +1843,7 @@ void NormApp::Notify(NormController::Event event, // Read the stream when it's updated ASSERT(NULL != output); bool reading = true; - bool findMsgSync; + bool seekMsgStart; while (reading) { unsigned int readLength; @@ -1825,17 +1864,17 @@ void NormApp::Notify(NormController::Event event, ASSERT(output_msg_length >= 2); readLength = output_msg_length - output_index; } - findMsgSync = output_msg_sync ? false : true; + seekMsgStart = output_msg_sync ? false : true; } else { output_index = 0; readLength = 512; - findMsgSync = false; + seekMsgStart = false; } if(!((NormStreamObject*)object)->Read(output_buffer+output_index, - &readLength, findMsgSync)) + &readLength, seekMsgStart)) { // The stream broke if (output_messaging) @@ -1844,8 +1883,8 @@ void NormApp::Notify(NormController::Event event, PLOG(PL_ERROR, "NormApp::Notify() detected broken stream ...\n"); output_msg_length = output_index = 0; output_msg_sync = false; - if (findMsgSync) - break; + if (seekMsgStart) + break; // didn't find msg start yet else continue; // try to re-sync } @@ -2019,6 +2058,14 @@ void NormApp::Notify(NormController::Event event, break; } } // end switch(event) + notify_reentry = false; + // Now handle any deferred, reentrant notifications + Notification* notification; + while (NULL != (notification = notify_queue.RemoveHead())) + { + Notify(notification->event, sessionMgr, session, sender, object); + notify_pool.Append(*notification); + } } // end NormApp::Notify() diff --git a/src/common/normEncoderRS16.cpp b/src/common/normEncoderRS16.cpp index 9788452..1aba353 100644 --- a/src/common/normEncoderRS16.cpp +++ b/src/common/normEncoderRS16.cpp @@ -784,7 +784,7 @@ bool NormDecoderRS16::InvertDecodingMatrix() icol = col ; goto found_piv ; } - for (int row = 0 ; row < k ; row++) + for (unsigned int row = 0 ; row < k ; row++) { if (inv_pivt[row] != 1) { @@ -879,7 +879,7 @@ bool NormDecoderRS16::InvertDecodingMatrix() } else if (inv_ndxr[col] != inv_ndxc[col] ) { - for (int row = 0 ; row < k ; row++ ) + for (unsigned int row = 0 ; row < k ; row++ ) SWAP( src[row*k + inv_ndxr[col]], src[row*k + inv_ndxc[col]], gf) ; } } diff --git a/src/common/normMessage.cpp b/src/common/normMessage.cpp index 1a474b2..900ecaf 100644 --- a/src/common/normMessage.cpp +++ b/src/common/normMessage.cpp @@ -49,7 +49,7 @@ bool NormMsg::InitFromBuffer(UINT16 msgLength) PLOG(PL_FATAL, "NormMsg::InitFromBuffer(DATA) unknown fec_id value: %u\n", fecId); return false; } - header_length_base = 4*NormCmdFlushMsg::FEC_ID_OFFSET + fecIdLen; + header_length_base = 4 * NormCmdFlushMsg::FEC_PAYLOAD_ID_OFFSET + fecIdLen; break; } case NormCmdMsg::EOT: diff --git a/src/common/normNode.cpp b/src/common/normNode.cpp index 505471f..2393a1d 100644 --- a/src/common/normNode.cpp +++ b/src/common/normNode.cpp @@ -9,12 +9,10 @@ NormNode::NormNode(Type nodeType, class NormSession& theSession, NormNodeId node : session(theSession), node_type(nodeType), id(nodeId), reference_count(1), user_data(NULL), parent(NULL), right(NULL), left(NULL) { - } NormNode::~NormNode() { - //TRACE("NormNode %lu dtor ...\n", (unsigned long)id); } @@ -68,9 +66,11 @@ const double NormSenderNode::ACTIVITY_INTERVAL_MIN = 1.0; // 1 second min activ NormSenderNode::NormSenderNode(class NormSession& theSession, NormNodeId nodeId) : NormNode(SENDER, theSession, nodeId), instance_id(0), robust_factor(session.GetRxRobustFactor()), synchronized(false), sync_id(0), - is_open(false), segment_size(0), fec_m(0), ndata(0), nparity(0), preset_stream(NULL), + is_open(false), preset_fti(false), preset_stream(NULL), repair_boundary(BLOCK_BOUNDARY), decoder(NULL), erasure_loc(NULL), - retrieval_loc(NULL), retrieval_pool(NULL), ack_pending(false), notify_on_grtt_update(true), + retrieval_loc(NULL), retrieval_pool(NULL), ack_pending(false), + ack_ex_pending(false), ack_ex_buffer(NULL), ack_ex_length(0), + notify_on_grtt_update(true), cc_sequence(0), cc_enable(false), cc_feedback_needed(false), cc_rate(0.0), rtt_confirmed(false), is_clr(false), is_plr(false), slow_start(true), send_rate(0.0), recv_rate(0.0), recv_rate_prev(0.0), @@ -164,6 +164,13 @@ void NormSenderNode::Close() if (ack_timer.IsActive()) ack_timer.Deactivate(); FreeBuffers(); + if (NULL != ack_ex_buffer) + { + delete[] ack_ex_buffer; + ack_ex_buffer = NULL; + ack_ex_length = 0; + } + // Delete any command buffers from cmd_buffer queue while (NULL != cmd_buffer_head) { @@ -238,8 +245,7 @@ bool NormSenderNode::AllocateBuffers(unsigned int bufferSpace, return false; } - // The extra byte of segments is used for marking segments - // which are "start segments" for messages encapsulated in a NormStreamObject + // Segment buffers include space for NORM_OBJECT_STREAM stream payload header if (!segment_pool.Init(numSegments, segmentSize+NormDataMsg::GetStreamPayloadHeaderLength())) { PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() segment_pool init error\n"); @@ -249,7 +255,7 @@ bool NormSenderNode::AllocateBuffers(unsigned int bufferSpace, // The "retrieval_pool" is used for FEC block decoding // These segments are temporarily used for "retrieved" source symbol segments - // needed for block decoding (new rx buffer mgmt scheme) + // that aren't still cached and needed for block decoding if (!(retrieval_pool = new char*[numData])) { PLOG(PL_FATAL, "NormSenderNode::AllocateBuffers() new retrieval_pool error: %s\n", GetErrorString()); @@ -366,12 +372,14 @@ bool NormSenderNode::AllocateBuffers(unsigned int bufferSpace, { decoder = NULL; } // end if/else (0 != numParity) - segment_size = segmentSize; + + fti_data.SetSegmentSize(segmentSize); nominal_packet_size = (double)segmentSize; + fec_id = fecId; - fec_m = fecM; - ndata = numData; - nparity = numParity; + fti_data.SetFecFieldSize(fecM); + fti_data.SetFecMaxBlockLen(numData); + fti_data.SetFecNumParity(numParity); IncrementResyncCount(); return true; } // end NormSenderNode::AllocateBuffers() @@ -396,7 +404,8 @@ void NormSenderNode::FreeBuffers() } if (retrieval_pool) { - for (unsigned int i = 0; i < ndata; i++) + UINT16 numData = BlockSize(); + for (unsigned int i = 0; i < numData; i++) { if (retrieval_pool[i]) { @@ -407,7 +416,6 @@ void NormSenderNode::FreeBuffers() delete[] retrieval_pool; retrieval_pool = NULL; } - NormObject* obj; while ((obj = rx_table.Find(rx_table.RangeLo()))) { @@ -418,7 +426,7 @@ void NormSenderNode::FreeBuffers() } segment_pool.Destroy(); block_pool.Destroy(); - segment_size = ndata = nparity = 0; + fti_data.Invalidate(); } // end NormSenderNode::FreeBuffers() unsigned long NormSenderNode::CurrentStreamBufferUsage() @@ -501,6 +509,74 @@ bool NormSenderNode::ReadNextCmd(char* buffer, unsigned int* buflen) } } // end NormSenderNode::ReadNextCmd() +bool NormSenderNode::SendAckEx(const char* appAck, unsigned int appAckLen) +{ + // First copy in the new appAck content for transmission + if (NULL != appAck) + { + if (appAckLen != ack_ex_length) + { + if (NULL != ack_ex_buffer) + { + delete[] ack_ex_buffer; + ack_ex_buffer = NULL; + ack_ex_length = 0; + } + // Make sure there is room for the header extension + if (appAckLen > SegmentSize()) + { + PLOG(PL_ERROR, "NormSenderNode::SendAckEx() error: application-defined ACK_REQ content too large!\n"); + ack_ex_pending = false; + return false; + } + else if (NULL == (ack_ex_buffer = new char[appAckLen])) + { + PLOG(PL_ERROR, "NormSenderNode::SendAckEx() new app_req_buffer error: %s\n", GetErrorString()); + ack_ex_pending = false; + return false; + } + } + memcpy(ack_ex_buffer, appAck, appAckLen); + ack_ex_length = appAckLen; + } + else if (NULL != ack_ex_buffer) + { + delete[] ack_ex_buffer; + ack_ex_buffer = NULL; + ack_ex_length = 0; + } + ack_ex_pending = false; + if (!ack_timer.IsActive()) + OnAckTimeout(ack_timer); + return true; +} // end NormSenderNode::SendAckEx() + +bool NormSenderNode::GetWatermarkEx(char* buffer, unsigned int* buflen) +{ + if (0 != ack_ex_length) + { + if (NULL != buflen) + { + if (*buflen < ack_ex_length) + { + *buflen = ack_ex_length; + return false; + } + *buflen = ack_ex_length; + if (NULL != buffer) + memcpy(buffer, ack_ex_buffer, ack_ex_length); + else + return false; + } + return true; + } + else + { + if (NULL != buflen) *buflen = 0; + return false; // no application-defined ACK request data + } +} // end NormSenderNode::GetWatermarkEx() + void NormSenderNode::SetRobustFactor(int value) { robust_factor = value; @@ -561,9 +637,9 @@ void NormSenderNode::HandleCommand(const struct timeval& currentTime, // build proper NACKs since we have no prior state fec_id = squelch.GetFecId(); if (2 == fec_id) // see comment in HandleObjectMessage() method on this - fec_m = 16; + fti_data.SetFecFieldSize(16); else - fec_m = 8; + fti_data.SetFecFieldSize(8); } else { @@ -576,9 +652,7 @@ void NormSenderNode::HandleCommand(const struct timeval& currentTime, NormObject* obj = rx_table.Find(objectId); if ((NULL != obj) && (NormObject::STREAM == obj->GetType())) { - NormBlockId blockId = squelch.GetFecBlockId(fec_m); - //NormStreamObject* stream = (NormStreamObject*)obj; - //TRACE("Prune(%u) 8 (read_index = %u.%hu)...\n", (UINT32)blockId, (UINT32)stream->GetNextBlockId(), stream->GetNextSegmentId()); + NormBlockId blockId = squelch.GetFecBlockId(fti_data.GetFecFieldSize()); static_cast(obj)->Prune(blockId, true); } // 3) Discard any invalidated objects (those listed in the squelch) @@ -674,7 +748,7 @@ void NormSenderNode::HandleCommand(const struct timeval& currentTime, } else { - double nominalSize = nominal_packet_size ? nominal_packet_size : segment_size; + double nominalSize = nominal_packet_size ? nominal_packet_size : SegmentSize(); cc_rate = NormSession::CalculateRate(nominalSize, rtt_estimate, ccLoss); r = cc_rate / send_rate; r = MIN(r, 0.9); @@ -724,14 +798,14 @@ void NormSenderNode::HandleCommand(const struct timeval& currentTime, // build proper NACKs since we have no prior state fec_id = flush.GetFecId(); if (2 == fec_id) // see comment in HandleObjectMessage() method on this - fec_m = 16; + fti_data.SetFecFieldSize(16); else - fec_m = 8; + fti_data.SetFecFieldSize(8); } else if (flush.GetFecId() == fec_id) { - blockId = flush.GetFecBlockId(fec_m); - symbolId = flush.GetFecSymbolId(fec_m); + blockId = flush.GetFecBlockId(fti_data.GetFecFieldSize()); + symbolId = flush.GetFecSymbolId(fti_data.GetFecFieldSize()); } else { @@ -759,7 +833,45 @@ void NormSenderNode::HandleCommand(const struct timeval& currentTime, watermark_object_id = objectId; watermark_block_id = blockId; watermark_segment_id = symbolId; - if (!ack_timer.IsActive()) + + // Check for application-extended watermark request (see NormSetWatermarkEx()) + const char* appAckReq = NULL; + unsigned int appAckReqLen = 0; + NormAppAckExtension ext; + while (flush.GetNextExtension(ext)) + { + if (NormHeaderExtension::APP_ACK == ext.GetType()) + { + appAckReq = ext.GetContent(); + appAckReqLen = ext.GetContentLength(); + } + } + if (NULL != appAckReq) + { + // We need to bubble this up to the application before we acknowledge watermark + // so app can set any extended ACK content in response + if (appAckReqLen != ack_ex_length) + { + if (NULL != ack_ex_buffer) delete[] ack_ex_buffer; + if (NULL == (ack_ex_buffer = new char[appAckReqLen])) + { + // TBD - notify app of allocation error + PLOG(PL_ERROR, "NormSenderNode::HandleCommand() new ack_ex_buffer error: %s\n", GetErrorString()); + ack_ex_length = 0; + } + else + { + ack_ex_length = appAckReqLen; + } + } + if (NULL != ack_ex_buffer) + { + memcpy(ack_ex_buffer, appAckReq, appAckReqLen); + ack_ex_pending = true; + session.Notify(NormController::RX_ACK_REQUEST, this, NULL); + } + } + else if (!ack_timer.IsActive()) { double ackBackoff = (session.Address().IsMulticast() && (backoff_factor > 0.0)) ? UniformRand(grtt_estimate) : 0.0; @@ -820,8 +932,8 @@ void NormSenderNode::HandleCommand(const struct timeval& currentTime, else { unsigned int cmdLength = appCmd.GetContentLength(); - if ((cmdLength <= segment_size) || - ((0 == segment_size) && (cmdLength < 8192))) + if ((cmdLength <= SegmentSize()) || + ((0 == SegmentSize()) && (cmdLength < 8192))) { // 2) Copy the app-defined command content into our buffer buf->SetContent(appCmd.GetContent(), appCmd.GetContentLength()); @@ -863,7 +975,7 @@ void NormSenderNode::HandleCCFeedback(UINT8 ccFlags, double ccRate) { // We're suppressed by non-CLR receivers with no RTT confirmed // and/or lower rate - double nominalSize = nominal_packet_size ? nominal_packet_size : segment_size; + double nominalSize = nominal_packet_size ? nominal_packet_size : SegmentSize(); double ccLoss = slow_start ? 0.0 : LossEstimate(); double localRate = (0.0 == ccLoss) ? (2.0*recv_rate) : @@ -988,7 +1100,7 @@ void NormSenderNode::HandleRepairContent(const UINT32* buffer, UINT16 bufferLen) } bool repairInfo = req.FlagIsSet(NormRepairRequest::INFO); - NormRepairRequest::Iterator iterator(req, fec_id, fec_m); // assumes constant "m" + NormRepairRequest::Iterator iterator(req, fec_id, fti_data.GetFecFieldSize()); // assumes constant "m" NormObjectId nextObjectId, lastObjectId; NormBlockId nextBlockId, lastBlockId; UINT16 nextBlockLen, lastBlockLen; @@ -1119,8 +1231,6 @@ void NormSenderNode::DeleteObject(NormObject* obj) NormBlock* NormSenderNode::GetFreeBlock(NormObjectId objectId, NormBlockId blockId) { NormBlock* b = block_pool.Get(); - //TRACE("enter NormSenderNode::GetFreeBlock() empty:%d\n", (NULL == b)); - if (NULL == b) { if (session.ReceiverIsSilent() || session.RcvrIsRealtime()) @@ -1259,7 +1369,7 @@ bool NormSenderNode::PreallocateRxStream(unsigned int bufferSize, return true; } // end NormSenderNode::PreallocateRxStream() - +// TBD - Move this method to NormObjectMsg class bool NormSenderNode::GetFtiData(const NormObjectMsg& msg, NormFtiData& ftiData) { UINT8 fecId = msg.GetFecId(); @@ -1358,31 +1468,36 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) bool allocateBuffers = true; bool gotFTI = false; NormFtiData ftiData; - - if (BuffersAllocated()) { // Validate that allocated buffers match object FEC params if (fecId == fec_id) { - if (GetFtiData(msg, ftiData)) + if (GetFtiData(msg, ftiData) || session.GetPresetFtiData(ftiData)) { gotFTI = true; - if ((ftiData.GetSegmentSize() != segment_size) || - (ftiData.GetFecFieldSize() != fec_m) || - (ftiData.GetFecMaxBlockLen() != ndata) || - (ftiData.GetFecNumParity() != nparity)) + if ((ftiData.GetSegmentSize() != SegmentSize()) || + (ftiData.GetFecFieldSize() != fti_data.GetFecFieldSize()) || + (ftiData.GetFecMaxBlockLen() != fti_data.GetFecMaxBlockLen()) || + (ftiData.GetFecNumParity() != fti_data.GetFecNumParity())) { FreeBuffers(); // force reallocation because fec params changed + fti_data = ftiData; } else { allocateBuffers = false; // FEC params match } } + else if ((NormMsg::INFO != msgType) && msg.FlagIsSet(NormObjectMsg::FLAG_INFO)) + { + // This handles case where only NORM_INFO carries FTI Info to reduce overhead + // We have to assume sender FTI hasn't changed ... + allocateBuffers = false; + } else { - // TBD - handle case where only NORM_INFO carries FTI Info to reduce overhead + PLOG(PL_ERROR, "NormSenderNode::HandleObjectMessage() node>%lu sender>%lu - no FTI provided!\n", (unsigned long)LocalNodeId(), (unsigned long)GetId()); return; // (TBD) notify app of error ?? @@ -1392,29 +1507,37 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) { FreeBuffers(); // force reallocation because fec id changed } - } + } // end if (BuffersAllocated()) NormBlockId blockId; NormSegmentId segmentId; - if (NormMsg::INFO == msgType) { - fec_id = fecId; - // This assumes fec_id 2 (Reed Solomon) is only used when - // 16-bit RS code is needed (otherwise fec_id 5 should have been used) - // This is a limitation of the current RS specification where fec_m is - // only provided in FEC OTI. (Note we could dig into FEC OTI of NORM_INFO - // message, but FLUSH/SQUELCH commands can't do this anyway and because we - // defer buffer allocation until NORM_DATA is received, it doesn't really matter - // (Any NACKs generate will have zero-value FEC blockId / symbolId anyway) - if (2 == fecId) - fec_m = 16; + if (!BuffersAllocated()) + { + fec_id = fecId; + // Go ahead and capture FTI from INFO + if (GetFtiData(msg, ftiData) || session.GetPresetFtiData(ftiData)) + { + gotFTI = true; + fti_data = ftiData; + } + else + { + + PLOG(PL_ERROR, "NormSenderNode::HandleObjectMessage() node>%lu sender>%lu - no FTI provided!\n", + (unsigned long)LocalNodeId(), (unsigned long)GetId()); + return; // (TBD) notify app of error ?? + } + } else - fec_m = 8; + { + ASSERT(gotFTI); + } blockId = 0; segmentId = 0; } - else + else // NormMsg::DATA { if (allocateBuffers) { @@ -1430,16 +1553,26 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) { gotFTI = true; } - else + else if (fti_data.IsValid()) + { + ftiData = fti_data; + gotFTI = true; + } + else if (session.GetPresetFtiData(ftiData)) + { + gotFTI = true; + } + else if ((NormMsg::INFO != msgType) && !msg.FlagIsSet(NormObjectMsg::FLAG_INFO)) { PLOG(PL_ERROR, "NormSenderNode::HandleObjectMessage() node>%lu sender>%lu - no FTI provided!\n", (unsigned long)LocalNodeId(), (unsigned long)GetId()); // (TBD) notify app of error ?? return; } + // else wait for NORM_INFO message with sender FTI } - if (!AllocateBuffers(session.RemoteSenderBufferSize(), fecId, ftiData.GetFecInstanceId(), ftiData.GetFecFieldSize(), - ftiData.GetSegmentSize(), ftiData.GetFecMaxBlockLen(), ftiData.GetFecNumParity())) + if (gotFTI && !AllocateBuffers(session.RemoteSenderBufferSize(), fecId, ftiData.GetFecInstanceId(), ftiData.GetFecFieldSize(), + ftiData.GetSegmentSize(), ftiData.GetFecMaxBlockLen(), ftiData.GetFecNumParity())) { PLOG(PL_ERROR, "NormSenderNode::HandleObjectMessage() node>%lu sender>%lu buffer allocation error\n", (unsigned long)LocalNodeId(), (unsigned long)GetId()); @@ -1448,10 +1581,23 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) } } // end if (allocateBuffers) - ASSERT(0 != fec_m); - const NormDataMsg& data = static_cast(msg); - blockId = data.GetFecBlockId(fec_m); - segmentId = data.GetFecSymbolId(fec_m); + if (fti_data.IsValid()) + { + ASSERT(0 != fti_data.GetFecFieldSize()); + const NormDataMsg& data = static_cast(msg); + blockId = data.GetFecBlockId(fti_data.GetFecFieldSize()); + segmentId = data.GetFecSymbolId(fti_data.GetFecFieldSize()); + } + else + { + // These won't come into play anyway + if (2 == fecId) + fti_data.SetFecFieldSize(16); + else + fti_data.SetFecFieldSize(8); + blockId = 0; + segmentId = 0; + } } // end if/else (NormMsg::INFO == msgType) ObjectStatus status; @@ -1485,16 +1631,41 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) } bool presetStream = false; NormObject* obj = NULL; + bool doInsert = true; switch (status) { case OBJ_PENDING: - if (NULL != (obj = rx_table.Find(objectId))) - break; + { + if (NULL != (obj = rx_table.Find(objectId))) + { + if (0 == obj->GetSize().GetOffset()) + { + // It's a seen object for which are awaiting FTI + if (GetFtiData(msg, ftiData)) + { + gotFTI = true; + obj->SetNackingMode(default_nacking_mode); + doInsert = false; + // Intentionally pass through to case OBJ_NEW + } + else + { + obj = NULL; // keep waiting for FTI + break; + } + } + else + { + break; // handle as normal pending object + } + } + // else intentionally pass through to case OBJ_NEW + } case OBJ_NEW: { if (msg.FlagIsSet(NormObjectMsg::FLAG_STREAM)) { - if (NULL != preset_stream) + if ((NULL != preset_stream) && ((NULL == obj) || (obj == static_cast(preset_stream)))) { obj = static_cast(preset_stream); // Validate FTI params @@ -1505,7 +1676,11 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) { gotFTI = true; } - else + else if (session.GetPresetFtiData(ftiData)) + { + gotFTI = true; + } + else if ((NormMsg::INFO != msgType) && !msg.FlagIsSet(NormObjectMsg::FLAG_INFO)) { PLOG(PL_ERROR, "NormSenderNode::HandleObjectMessage() node>%lu sender>%lu - no FTI provided!\n", (unsigned long)LocalNodeId(), (unsigned long)GetId()); @@ -1513,12 +1688,13 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) return; } } - if ((obj->GetSize() != ftiData.GetObjectSize()) || - (obj->GetFecId() != fecId) || - (obj->GetSegmentSize() != ftiData.GetSegmentSize()) || - (obj->GetFecMaxBlockLen() != ftiData.GetFecMaxBlockLen()) || - (obj->GetFecNumParity() != ftiData.GetFecNumParity()) || - (obj->GetFecFieldSize() != ftiData.GetFecFieldSize())) + if (gotFTI && + ((obj->GetSize() != ftiData.GetObjectSize()) || + (obj->GetFecId() != fecId) || + (obj->GetSegmentSize() != ftiData.GetSegmentSize()) || + (obj->GetFecMaxBlockLen() != ftiData.GetFecMaxBlockLen()) || + (obj->GetFecNumParity() != ftiData.GetFecNumParity()) || + (obj->GetFecFieldSize() != ftiData.GetFecFieldSize()))) { PLOG(PL_WARN, "NormSenderNode::HandleObjectMessage() node>%lu sender>%lu warning: " "FTI does not match preset_stream!\n", @@ -1543,7 +1719,7 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) } } } - else if (msg.FlagIsSet(NormObjectMsg::FLAG_FILE)) + else if (msg.FlagIsSet(NormObjectMsg::FLAG_FILE) && (NULL == obj)) { #ifdef SIMULATE if (!(obj = new NormSimObject(session, this, objectId))) @@ -1552,10 +1728,10 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) #endif { PLOG(PL_ERROR, "NormSenderNode::HandleObjectMessage() new NORM_OBJECT_FILE error: %s\n", - GetErrorString()); + GetErrorString()); } } - else + else if (NULL == obj) { if (!(obj = new NormDataObject(session, this, objectId, session.GetSessionMgr().GetDataFreeFunction()))) { @@ -1571,17 +1747,14 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) { ASSERT(rx_table.CanInsert(objectId)); ASSERT(rx_pending_mask.Test(objectId)); - rx_table.Insert(obj); + if (doInsert) rx_table.Insert(obj); // Pull out FTI parameters from header extension if we didn't get it above - if (!gotFTI && !GetFtiData(msg, ftiData)) + if (!gotFTI) { - PLOG(PL_ERROR, "NormSenderNode::HandleObjectMessage() node>%lu sender>%lu " - "new obj>%hu - no FTI provided!\n", (unsigned long)LocalNodeId(), - (unsigned long)GetId(), (UINT16)objectId); - if (!presetStream) DeleteObject(obj); - obj = NULL; + if (GetFtiData(msg, ftiData) || session.GetPresetFtiData(ftiData)) + gotFTI = true; } - else + if (gotFTI) { if (presetStream || obj->RxOpen(ftiData.GetObjectSize(), @@ -1641,6 +1814,23 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) obj = NULL; } } + else if ((NormMsg::INFO != msgType) && msg.FlagIsSet(NormObjectMsg::FLAG_INFO)) + { + // Open a zero-sized object in NACK_INFO_ONLY nacking mode until NORM_INFO w/ FTI arrives + obj->SetPendingInfo(true, fecId); + obj->SetNackingMode(NormObject::NACK_INFO_ONLY); + if (presetStream) preset_stream = NULL; // we're using it up + obj = NULL; // can't process NORM_DATA until we have FTI + // TBD - buffer received messages instead of discarding them??? + } + else + { + PLOG(PL_ERROR, "NormSenderNode::HandleObjectMessage() node>%lu sender>%lu " + "new obj>%hu - no FTI provided!\n", (unsigned long)LocalNodeId(), + (unsigned long)GetId(), (UINT16)objectId); + if (!presetStream) DeleteObject(obj); + obj = NULL; + } } break; } @@ -1692,7 +1882,7 @@ void NormSenderNode::HandleObjectMessage(const NormObjectMsg& msg) // Optional "object boundary repair check (non-streams only!) // (checks for repair needs for objects _prior_ to current objectId) // (also requests "info" for current objectId) - if (obj && obj->IsStream()) + if (NULL != obj && obj->IsStream()) RepairCheck(NormObject::TO_BLOCK, objectId, blockId, segmentId); else RepairCheck(NormObject::THRU_INFO, objectId, blockId, segmentId); @@ -1711,7 +1901,7 @@ bool NormSenderNode::SyncTest(const NormObjectMsg& msg) const bool result = msg.FlagIsSet(NormObjectMsg::FLAG_STREAM); // Allow sync on INFO or block zero DATA message result = result || (NormMsg::INFO == msg.GetType()) ? - true : (NormBlockId(0) == ((const NormDataMsg&)msg).GetFecBlockId(fec_m)); + true : (NormBlockId(0) == ((const NormDataMsg&)msg).GetFecBlockId(fti_data.GetFecFieldSize())); // Never sync on repair messages result = result && !msg.FlagIsSet(NormObjectMsg::FLAG_REPAIR); return result; @@ -2142,7 +2332,7 @@ bool NormSenderNode::OnRepairTimeout(ProtoTimer& /*theTimer*/) return false; } nack->Init(); - UINT16 payloadMax = 4*segment_size; + UINT16 payloadMax = 4*SegmentSize(); // If we sync'd to non-DATA, we don't yet know the sender segment_size if (0 == payloadMax) payloadMax = 4*NormNackMsg::DEFAULT_LENGTH_MAX; @@ -2176,7 +2366,7 @@ bool NormSenderNode::OnRepairTimeout(ProtoTimer& /*theTimer*/) } else { - double nominalSize = (nominal_packet_size > segment_size) ? nominal_packet_size : segment_size; + double nominalSize = (nominal_packet_size > SegmentSize()) ? nominal_packet_size : SegmentSize(); if (0 == nominalSize) nominalSize = 512; // TBD - what should this really be double ccRate = NormSession::CalculateRate(nominalSize, rtt_estimate, @@ -2273,13 +2463,13 @@ bool NormSenderNode::OnRepairTimeout(ProtoTimer& /*theTimer*/) switch (nextForm) { case NormRepairRequest::ITEMS: - req.AppendRepairItem(fec_id, fec_m, prevId, 0, ndata, 0); + req.AppendRepairItem(fec_id, fti_data.GetFecFieldSize(), prevId, 0, BlockSize(), 0); if (2 == consecutiveCount) - req.AppendRepairItem(fec_id, fec_m, prevId+1, 0, ndata, 0); + req.AppendRepairItem(fec_id, fti_data.GetFecFieldSize(), prevId+1, 0, BlockSize(), 0); break; case NormRepairRequest::RANGES: - req.AppendRepairRange(fec_id, fec_m, prevId, 0, ndata, 0, - prevId+consecutiveCount-1, 0, ndata, 0); + req.AppendRepairRange(fec_id, fti_data.GetFecFieldSize(), prevId, 0, BlockSize(), 0, + prevId+consecutiveCount-1, 0, BlockSize(), 0); break; default: break; @@ -2340,7 +2530,7 @@ bool NormSenderNode::OnRepairTimeout(ProtoTimer& /*theTimer*/) ASSERT(nack->GetRepairContentLength() > 0); if (!session.ReceiverIsSilent()) { - UINT16 singleNackSize = segment_size ? segment_size : NormNackMsg::DEFAULT_LENGTH_MAX; + UINT16 singleNackSize = SegmentSize() ? SegmentSize() : NormNackMsg::DEFAULT_LENGTH_MAX; if (nack->GetRepairContentLength() <= singleNackSize) { session.SendMessage(*nack); @@ -2394,7 +2584,7 @@ bool NormSenderNode::OnRepairTimeout(ProtoTimer& /*theTimer*/) } // Uncommenting the line below treats ((0 == nparity) && 0.0 == backoff_factor) // as a special case (assumes zero sender aggregateInterval) - holdoffInterval = ((0 != nparity) || (backoff_factor > 0.0)) ? holdoffInterval : grtt_estimate; + holdoffInterval = ((0 != NumParity()) || (backoff_factor > 0.0)) ? holdoffInterval : grtt_estimate; repair_timer.SetInterval(holdoffInterval); PLOG(PL_DEBUG, "NormSenderNode::OnRepairTimeout() node>%lu sender>%lu begin NACK hold-off: %lf sec ...\n", (unsigned long)LocalNodeId(), (unsigned long)GetId(), holdoffInterval); @@ -2442,23 +2632,23 @@ void NormSenderNode::FragmentNack(NormNackMsg& superNack) { const UINT16 REQ_HDR_LEN = 4; // TBD - get from normMessage.h instead requestOffset += requestLength; - if ((payloadLength + requestLength) <= segment_size) + if ((payloadLength + requestLength) <= SegmentSize()) { // Copy whole request over nack->AppendRepairRequest(superReq); payloadLength += requestLength; } - else if ((payloadLength + REQ_HDR_LEN) < segment_size) + else if ((payloadLength + REQ_HDR_LEN) < SegmentSize()) { // Duplicate request and add individual repair items NormRepairRequest::Form requestForm = superReq.GetForm(); NormRepairRequest req; - nack->AttachRepairRequest(req, segment_size); + nack->AttachRepairRequest(req, SegmentSize()); req.SetForm(requestForm); req.SetFlags(superReq.GetFlags()); payloadLength += REQ_HDR_LEN; - NormRepairRequest::Iterator iterator(superReq, fec_id, fec_m); + NormRepairRequest::Iterator iterator(superReq, fec_id, fti_data.GetFecFieldSize()); NormObjectId objectId, lastObjectId; NormBlockId blockId, lastBlockId; UINT16 blockLen, lastBlockLen; @@ -2471,24 +2661,24 @@ void NormSenderNode::FragmentNack(NormNackMsg& superNack) itemLength += iterator.NextRepairItem(&lastObjectId, &lastBlockId, &lastBlockLen, &lastSegmentId); } - if ((payloadLength + itemLength) > segment_size) + if ((payloadLength + itemLength) > SegmentSize()) { // We have filled the NACK, so pack, send, and reset request nack->PackRepairRequest(req); session.SendMessage(*nack); nack_count++; nack->ResetPayload(); - nack->AttachRepairRequest(req, segment_size); + nack->AttachRepairRequest(req, SegmentSize()); payloadLength = REQ_HDR_LEN; } if (NormRepairRequest::RANGES == requestForm) { - req.AppendRepairRange(fec_id, fec_m, objectId, blockId, blockLen, segmentId, + req.AppendRepairRange(fec_id, fti_data.GetFecFieldSize(), objectId, blockId, blockLen, segmentId, lastObjectId, lastBlockId, lastBlockLen, lastSegmentId); } else { - req.AppendRepairItem(fec_id, fec_m, objectId, blockId, blockLen, segmentId); + req.AppendRepairItem(fec_id, fti_data.GetFecFieldSize(), objectId, blockId, blockLen, segmentId); } payloadLength += itemLength; } @@ -2534,7 +2724,7 @@ void NormSenderNode::UpdateRecvRate(const struct timeval& currentTime, unsigned if ((interval >= measurementInterval) && (currentRecvRate < recv_rate)) { // Make sure we've allowed sufficient time for a measurement at low rates - double nominalSize = (nominal_packet_size > segment_size) ? nominal_packet_size : segment_size; + double nominalSize = (nominal_packet_size > SegmentSize()) ? nominal_packet_size : SegmentSize(); double minInterval = 4.0 * nominalSize / recv_rate; if (measurementInterval < minInterval) measurementInterval = minInterval; } @@ -2700,7 +2890,7 @@ bool NormSenderNode::UpdateLossEstimate(const struct timeval& currentTime, { // Calculate loss initialization based on current receive rate // and rtt estimation - double nominalSize = (nominal_packet_size > segment_size) ? nominal_packet_size : segment_size; + double nominalSize = (nominal_packet_size > SegmentSize()) ? nominal_packet_size : SegmentSize(); double lossInit = nominalSize / (recv_rate * rtt_estimate); lossInit *= lossInit; lossInit *= 3.0/2.0; @@ -2767,7 +2957,7 @@ void NormSenderNode::AttachCCFeedback(NormAckMsg& ack) else { //double nominalSize = (nominal_packet_size > segment_size) ? nominal_packet_size : segment_size; - double nominalSize = (0 != nominal_packet_size) ? nominal_packet_size : segment_size; + double nominalSize = (0 != nominal_packet_size) ? nominal_packet_size : SegmentSize(); double ccRate = NormSession::CalculateRate(nominalSize, rtt_estimate, ccLoss); #ifdef LIMIT_CC_RATE // Experimental modification to NORM-CC where congestion control rate is limited @@ -2795,7 +2985,7 @@ void NormSenderNode::AttachCCFeedback(NormAckMsg& ack) bool NormSenderNode::OnCCTimeout(ProtoTimer& /*theTimer*/) { // Build and send NORM_ACK(CC) - if (ack_pending && (1 == cc_timer.GetRepeatCount())) + if (ack_pending && !ack_ex_pending && (1 == cc_timer.GetRepeatCount())) { // Send ACK flush right away (CC feedback is included) if (ack_timer.IsActive()) ack_timer.Deactivate(); @@ -2869,6 +3059,8 @@ bool NormSenderNode::OnCCTimeout(ProtoTimer& /*theTimer*/) bool NormSenderNode::OnAckTimeout(ProtoTimer& /*theTimer*/) { // Build and send NORM_ACK(FLUSH) + if (ack_ex_pending) + return true; // Will acknowledge when application services RX_ACK_REQUEST notification NormAckFlushMsg* ack = (NormAckFlushMsg*)session.GetMessageFromPool(); if (NULL != ack) { @@ -2878,6 +3070,14 @@ bool NormSenderNode::OnAckTimeout(ProtoTimer& /*theTimer*/) ack->SetAckType(NormAck::FLUSH); ack->SetAckId(0); AttachCCFeedback(*ack); + if (0 != ack_ex_length) + { + NormAppAckExtension ext; + ack->AttachExtension(ext); + ext.SetContent(ack_ex_buffer, ack_ex_length); + ack->PackExtension(ext); + } + ack->SetObjectId(watermark_object_id); // _Attempt_ to set the fec_payload_id source block length field appropriately @@ -2885,11 +3085,12 @@ bool NormSenderNode::OnAckTimeout(ProtoTimer& /*theTimer*/) NormObject* obj = rx_table.Find(watermark_object_id); if (NULL != obj) blockLen = obj->GetBlockSize(watermark_block_id); - else if (watermark_segment_id < ndata) - blockLen = ndata; + else if (watermark_segment_id < BlockSize()) + blockLen = BlockSize(); else blockLen = watermark_segment_id; - ack->SetFecPayloadId(fec_id, watermark_block_id.GetValue(), watermark_segment_id, blockLen, fec_m); + + ack->SetFecPayloadId(fec_id, watermark_block_id.GetValue(), watermark_segment_id, blockLen, fti_data.GetFecFieldSize()); if (unicast_nacks) ack->SetDestination(GetAddress()); @@ -2932,15 +3133,67 @@ bool NormSenderNode::OnAckTimeout(ProtoTimer& /*theTimer*/) NormAckingNode::NormAckingNode(class NormSession& theSession, NormNodeId nodeId) - : NormNode(ACKER, theSession, nodeId), ack_received(false), req_count(theSession.GetTxRobustFactor()) -{ + : NormNode(ACKER, theSession, nodeId), + ack_received(false), req_count(theSession.GetTxRobustFactor()), + ack_ex_buffer(NULL), ack_ex_length(0) +{ } NormAckingNode::~NormAckingNode() { + if (NULL != ack_ex_buffer) + { + delete[] ack_ex_buffer; + ack_ex_buffer = NULL; + ack_ex_length = 0; + } } +bool NormAckingNode::SetAckEx(const char* buffer, UINT16 numBytes) +{ + if (numBytes != ack_ex_length) + { + if (NULL != ack_ex_buffer) delete[] ack_ex_buffer; + if (NULL == (ack_ex_buffer = new char[numBytes])) + { + // TBD - notify app of errror + PLOG(PL_ERROR, "NormAckingNode::SetAppAckContent() new ack_ex_buffer error: %s\n", GetErrorString()); + ack_ex_length = 0; + return false; + } + ack_ex_length = numBytes; + } + memcpy(ack_ex_buffer, buffer, numBytes); + return true; +} // end NormAckingNode::SetAckEx() + +bool NormAckingNode::GetAckEx(char* buffer, unsigned int* buflen) +{ + if (0 != ack_ex_length) + { + if (NULL != buflen) + { + if (*buflen < ack_ex_length) + { + *buflen = ack_ex_length; + return false; + } + *buflen = ack_ex_length; + if (NULL != buffer) + memcpy(buffer, ack_ex_buffer, ack_ex_length); + else + return false; + } + return true; + } + else + { + if (NULL != buflen) *buflen = 0; + return false; // no application-defined ACK request data + } +} // end NormAckingNode::GetAckEx() + NormNodeTree::NormNodeTree() : root(NULL) { @@ -3449,7 +3702,6 @@ bool NormLossEstimator2::Update(const struct timeval& currentTime, seeking_loss_event = CONFIRMING; // wait for more loss to confirm congestion event event_time = event_time_orig = currentTime; outageDepth = 0; - //TRACE("Deferring loss event for confirmation ...\n"); } } @@ -3466,7 +3718,6 @@ bool NormLossEstimator2::Update(const struct timeval& currentTime, discount[i] = discount[i-1]*current_discount; discount[0] = 1.0; current_discount = 1.0; - //TRACE("Confirmed loss event ...(tolerate:%d)\n", tolerate_loss); seeking_loss_event = CONFIRMED; event_time = event_time_orig = currentTime; newLossEvent = true; diff --git a/src/common/normObject.cpp b/src/common/normObject.cpp index 416c5dd..96d6226 100644 --- a/src/common/normObject.cpp +++ b/src/common/normObject.cpp @@ -50,8 +50,6 @@ void NormObject::Retain() void NormObject::Release() { - //if ((NULL != sender) && (reference_count == 1)) - // TRACE("NormObject final release node>%lu object>%hu\n", (unsigned long)sender->GetId(), (unsigned short)transport_id); if (sender) sender->Release(); if (reference_count) { @@ -93,18 +91,18 @@ bool NormObject::Open(const NormObjectSize& objectSize, if (infoLen > 0) { pending_info = true; - info_len = 0; if (!(info_ptr = new char[segmentSize])) { PLOG(PL_FATAL, "NormObject::Open() info allocation error\n"); return false; } } + info_len = 0; // will be set properly upon NORM_INFO arrival last_nack_time.GetCurrentTime(); // init to now } else { - if (infoPtr) + if (NULL != infoPtr) { if (NULL != info_ptr) delete[] info_ptr; if (infoLen > segmentSize) @@ -123,6 +121,14 @@ bool NormObject::Open(const NormObjectSize& objectSize, info_len = infoLen; pending_info = true; } + else + { + if (NormSession::FTI_INFO == session.SenderFtiMode()) + { + + pending_info = true; + } + } } // Compute number of segments and coding blocks for the object @@ -190,11 +196,11 @@ bool NormObject::Open(const NormObjectSize& objectSize, } else { - // Set everything pending + // Set everything pending (i.e, nothing sent or received yet) pending_mask.Clear(); pending_mask.SetBits(0, numBlocks.LSB()); // Compute FEC block structure per NORM Protocol Spec Section 5.1.1 - // (Note NormObjectSize divide operator always rounds _up_) + // (Note NormObjectSize divide operator always rounds _up_, i.e., ceil(numSegments/numBlocks)) NormObjectSize largeBlockSize = numSegments / numBlocks; ASSERT(0 == largeBlockSize.MSB()); large_block_size = largeBlockSize.LSB(); @@ -290,7 +296,7 @@ NormObjectSize NormObject::GetBytesPending() const bool NormObject::HandleInfoRequest(bool holdoff) { bool increasedRepair = false; - if (info_ptr) + if (info_ptr || (NormSession::FTI_INFO == session.SenderFtiMode())) { if (!repair_info) { @@ -379,7 +385,7 @@ bool NormObject::HandleBlockRequest(const NormBlockId& firstId, const NormBlockI bool NormObject::TxReset(NormBlockId firstBlock, bool requeue) { bool increasedRepair = false; - if (!pending_info && HaveInfo()) + if (!pending_info && (HaveInfo() || (NormSession::FTI_INFO == session.SenderFtiMode()))) { increasedRepair = true; pending_info = true; @@ -1387,9 +1393,22 @@ void NormObject::HandleObjectMessage(const NormObjectMsg& msg, "Warning! info too long.\n", (unsigned long)LocalNodeId(), (unsigned long)sender->GetId(), (UINT16)transport_id); } - memcpy(info_ptr, infoMsg.GetInfo(), info_len); - pending_info = false; - session.Notify(NormController::RX_OBJECT_INFO, sender, this); + else if (0 == info_len) + { + // NORM_INFO used to convey FTI only + if (NULL != info_ptr) + { + delete[] info_ptr; + info_ptr = NULL; + } + pending_info = false; + } + else + { + memcpy(info_ptr, infoMsg.GetInfo(), info_len); + pending_info = false; + session.Notify(NormController::RX_OBJECT_INFO, sender, this); + } } else { @@ -1538,7 +1557,7 @@ void NormObject::HandleObjectMessage(const NormObjectMsg& msg, if (nextErasure < numData) { // Use "NormObject::RetrieveSegment() method to "retrieve" - // source symbol segments already received which weren't cached. + // source symbol segments already received which aren't still cached. for (UINT16 nextSegment = 0; nextSegment < numData; nextSegment++) { if (block->IsPending(nextSegment)) @@ -1622,7 +1641,7 @@ void NormObject::HandleObjectMessage(const NormObjectMsg& msg, // so it's not called unnecessarily if (objectUpdated && notify_on_update) { - if ((NULL == stream) || stream->DetermineReadReadiness()) + if ((NULL == stream) || stream->DetermineReadReadiness() || session.RcvrIsLowDelay()) { notify_on_update = false; session.Notify(NormController::RX_OBJECT_UPDATED, sender, this); @@ -1780,50 +1799,55 @@ bool NormObject::NextSenderMsg(NormObjectMsg* msg) default: break; } - if (info_ptr) + NormSession::FtiMode ftiMode = session.SenderFtiMode(); + if ((NULL != info_ptr) || (NormSession::FTI_INFO == ftiMode)) msg->SetFlag(NormObjectMsg::FLAG_INFO); msg->SetObjectId(transport_id); - // We currently always apply the FTI extension - switch (fec_id) + // Apply FTI extension as needed + if ((NormSession::FTI_ALWAYS == ftiMode) || + (pending_info && (NormSession::FTI_INFO == ftiMode))) { - case 2: + switch (fec_id) { - NormFtiExtension2 fti; - msg->AttachExtension(fti); - fti.SetObjectSize(object_size); - fti.SetFecFieldSize(fec_m); - fti.SetFecGroupSize(1); - fti.SetSegmentSize(segment_size); - fti.SetFecMaxBlockLen(ndata); - fti.SetFecNumParity(nparity); - break; + case 2: + { + NormFtiExtension2 fti; + msg->AttachExtension(fti); + fti.SetObjectSize(object_size); + fti.SetFecFieldSize(fec_m); + fti.SetFecGroupSize(1); + fti.SetSegmentSize(segment_size); + fti.SetFecMaxBlockLen(ndata); + fti.SetFecNumParity(nparity); + break; + } + case 5: + { + NormFtiExtension5 fti; + msg->AttachExtension(fti); + fti.SetObjectSize(object_size); + fti.SetSegmentSize(segment_size); + fti.SetFecMaxBlockLen((UINT8)ndata); + fti.SetFecNumParity((UINT8)nparity); + break; + } + case 129: + { + NormFtiExtension129 fti; + msg->AttachExtension(fti); + fti.SetObjectSize(object_size); + fti.SetFecInstanceId(0); // ZERO is for legacy MDP/NORM FEC encoder (TBD - use appropriate instanceId) + fti.SetSegmentSize(segment_size); + fti.SetFecMaxBlockLen(ndata); + fti.SetFecNumParity(nparity); + break; + } + default: + ASSERT(0); + return false; } - case 5: - { - NormFtiExtension5 fti; - msg->AttachExtension(fti); - fti.SetObjectSize(object_size); - fti.SetSegmentSize(segment_size); - fti.SetFecMaxBlockLen((UINT8)ndata); - fti.SetFecNumParity((UINT8)nparity); - break; - } - case 129: - { - NormFtiExtension129 fti; - msg->AttachExtension(fti); - fti.SetObjectSize(object_size); - fti.SetFecInstanceId(0); // ZERO is for legacy MDP/NORM FEC encoder (TBD - use appropriate instanceId) - fti.SetSegmentSize(segment_size); - fti.SetFecMaxBlockLen(ndata); - fti.SetFecNumParity(nparity); - break; - } - default: - ASSERT(0); - return false; } if (pending_info) { @@ -1990,7 +2014,7 @@ bool NormObject::NextSenderMsg(NormObjectMsg* msg) block->ResetParityCount(nparity); pending_mask.Unset(blockId.GetValue()); // for EMCON sending, mark NORM_INFO for re-transmission, if applicable - if (session.SndrEmcon() && HaveInfo()) + if (session.SndrEmcon() && (HaveInfo() || (NormSession::FTI_INFO == session.SenderFtiMode()))) pending_info = true; } block = NULL; @@ -2052,7 +2076,7 @@ bool NormObject::NextSenderMsg(NormObjectMsg* msg) block->ResetParityCount(nparity); pending_mask.Unset(blockId.GetValue()); // for EMCON sending, mark NORM_INFO for re-transmission, if applicable - if (session.SndrEmcon() && HaveInfo()) + if (session.SndrEmcon() && (HaveInfo() || (NormSession::FTI_INFO == session.SenderFtiMode()))) pending_info = true; // Advance sender use of "max_pending_block" so we always // know when a block should be flagged as IN_REPAIR @@ -2506,7 +2530,6 @@ NormDataObject::NormDataObject(class NormSession& theSession, NormDataObject::~NormDataObject() { - //TRACE("NormDataObject destructor ...\n"); Close(); if (data_released) { @@ -3289,7 +3312,6 @@ bool NormStreamObject::WriteSegment(NormBlockId blockId, block->EmptyToPool(segment_pool); block_pool.Put(block); block = NULL; - //TRACE("Prune(%u) 6 ...\n", (unsigned int)read_index.block.GetValue()); Prune(read_index.block, false); break; } @@ -3315,7 +3337,6 @@ bool NormStreamObject::WriteSegment(NormBlockId blockId, Increment(read_index.block); read_index.segment = 0; read_index.offset = 0; - //TRACE("Prune(%lu) 7 ...\n", (unsigned long)read_index.block.GetValue()); Prune(read_index.block, false); } } @@ -3553,7 +3574,9 @@ bool NormStreamObject::ReadPrivate(char* buffer, unsigned int* buflen, bool seek { INT32 delta = (INT32)Difference(max_pending_block, read_index.block); if (delta > session.RcvrGetMaxDelay()) - forceForward = true; + { + forceForward = true; + } } } else @@ -3571,7 +3594,6 @@ bool NormStreamObject::ReadPrivate(char* buffer, unsigned int* buflen, bool seek Increment(read_index.block); read_index.segment = 0; read_index.offset = 0; - //TRACE("Prune(%u) 1 ...\n", (unsigned int)read_index.block.GetValue()); if (!seekMsgStart) brokenStream = true; Prune(read_index.block, false); continue; @@ -3638,7 +3660,6 @@ bool NormStreamObject::ReadPrivate(char* buffer, unsigned int* buflen, bool seek Increment(read_index.block); read_index.segment = 0; read_index.offset = 0; - //TRACE("Prune(%u) 2 ...\n", (unsigned int)read_index.block.GetValue()); Prune(read_index.block, false); // prevents repair requests for data we } // no longer care about (i.e, prior to this block) if (!seekMsgStart) brokenStream = true; @@ -3712,7 +3733,6 @@ bool NormStreamObject::ReadPrivate(char* buffer, unsigned int* buflen, bool seek Increment(read_index.block); read_index.segment = 0; read_index.offset = 0; - //TRACE("Prune(%u) 3 ...\n", (unsigned int)read_index.block.GetValue()); Prune(read_index.block, false); } *buflen = 0; @@ -3800,7 +3820,6 @@ bool NormStreamObject::ReadPrivate(char* buffer, unsigned int* buflen, bool seek block_pool.Put(block); Increment(read_index.block); read_index.segment = 0; - //TRACE("Prune(%u) 4 ...\n", (unsigned int)read_index.block.GetValue()); Prune(read_index.block, false); } continue; @@ -3843,7 +3862,6 @@ bool NormStreamObject::ReadPrivate(char* buffer, unsigned int* buflen, bool seek block_pool.Put(block); Increment(read_index.block); read_index.segment = 0; - //TRACE("Prune(%u) 5 ...\n", (unsigned int)read_index.block.GetValue()); Prune(read_index.block, false); if (0 == bytesToRead) read_ready = DetermineReadReadiness(); diff --git a/src/common/normSession.cpp b/src/common/normSession.cpp index 56161ed..490899c 100644 --- a/src/common/normSession.cpp +++ b/src/common/normSession.cpp @@ -48,7 +48,7 @@ NormSession::NormSession(NormSessionMgr& sessionMgr, NormNodeId localNodeId) backoff_factor(DEFAULT_BACKOFF_FACTOR), is_sender(false), tx_robust_factor(DEFAULT_ROBUST_FACTOR), instance_id(0), ndata(DEFAULT_NDATA), nparity(DEFAULT_NPARITY), auto_parity(0), extra_parity(0), - sndr_emcon(false), tx_only(false), tx_connect(false), encoder(NULL), + sndr_emcon(false), tx_only(false), tx_connect(false), fti_mode(FTI_ALWAYS), encoder(NULL), next_tx_object_id(0), tx_cache_count_min(DEFAULT_TX_CACHE_MIN), tx_cache_count_max(DEFAULT_TX_CACHE_MAX), @@ -67,6 +67,7 @@ NormSession::NormSession(NormSessionMgr& sessionMgr, NormNodeId localNodeId) cc_enable(false), cc_adjust(true), cc_sequence(0), cc_slow_start(true), cc_active(false), flow_control_factor(DEFAULT_FLOW_CONTROL_FACTOR), cmd_count(0), cmd_buffer(NULL), cmd_length(0), syn_status(false), + ack_ex_buffer(NULL), ack_ex_length(0), is_receiver(false), rx_robust_factor(DEFAULT_ROBUST_FACTOR), preset_sender(NULL), unicast_nacks(false), receiver_silent(false), rcvr_ignore_info(false), rcvr_max_delay(-1), rcvr_realtime(false), default_repair_boundary(NormSenderNode::BLOCK_BOUNDARY), @@ -721,6 +722,22 @@ bool NormSession::StartSender(UINT16 instanceId, UINT16 numParity, UINT8 fecId) { + UINT16 blockSize = numData + numParity; + if (blockSize <= 255) + fec_m = 8; + else + fec_m = 16; + if (preset_fti.IsValid()) + { + if ((preset_fti.GetSegmentSize() != segmentSize) || + (preset_fti.GetFecMaxBlockLen() != numData) || + (preset_fti.GetFecNumParity() != numParity) || + (preset_fti.GetFecFieldSize() != fec_m)) + { + PLOG(PL_FATAL, "NormSession::StartSender() preset FTI mismatch error!\n"); + return false; + } + } if (!IsOpen()) { if (!Open()) return false; @@ -745,7 +762,6 @@ bool NormSession::StartSender(UINT16 instanceId, } // Calculate how much memory each buffered block will require - UINT16 blockSize = numData + numParity; unsigned long maskSize = blockSize >> 3; if (0 != (blockSize & 0x07)) maskSize++; unsigned long blockSpace = sizeof(NormBlock) + @@ -917,6 +933,13 @@ void NormSession::StopSender() if (flow_control_timer.IsActive()) flow_control_timer.Deactivate(); + if (NULL != ack_ex_buffer) + { + delete[] ack_ex_buffer; + ack_ex_buffer = NULL; + ack_ex_length = 0; + } + if (NULL != cmd_buffer) { delete[] cmd_buffer; @@ -1045,6 +1068,37 @@ bool NormSession::PreallocateRemoteSender(unsigned int bufferSpace, return true; } // end NormSession::PreallocateRemoteSender() +bool NormSession::SetPresetFtiData(unsigned int objectSize, + UINT16 segmentSize, + UINT16 numData, + UINT16 numParity) +{ + UINT16 blockSize = numData + numParity; + UINT8 fecM; + if (blockSize > 255) + fecM = 16; + else + fecM = 8; + if (IsSender()) + { + if ((segmentSize != segment_size) || + (numData != ndata) || + (numParity != nparity) || + (fecM != fec_m)) + { + PLOG(PL_FATAL, "NormSession::SetPresetFtiData() sender FTI mismatch error!\n"); + return false; + } + } + preset_fti.SetObjectSize(NormObjectSize(objectSize)); + preset_fti.SetSegmentSize(segmentSize); + preset_fti.SetFecMaxBlockLen(numData); + preset_fti.SetFecNumParity(numParity); + preset_fti.SetFecFieldSize(fecM); + preset_fti.SetFecInstanceId(0); + return true; +} // end NormSession::SetPresetFtiData() + void NormSession::Serve() { // Only send new data when no other messages are queued for transmission @@ -1341,10 +1395,12 @@ void NormSession::Serve() } } // end NormSession::Serve() -void NormSession::SenderSetWatermark(NormObjectId objectId, +bool NormSession::SenderSetWatermark(NormObjectId objectId, NormBlockId blockId, NormSegmentId segmentId, - bool overrideFlush) + bool overrideFlush, + const char* appAckReq, + unsigned int appAckReqLen) { PLOG(PL_DEBUG, "NormSession::SenderSetWatermark() watermark>%hu:%lu:%hu\n", (UINT16)objectId, (unsigned long)blockId.GetValue(), (UINT16)segmentId); @@ -1361,7 +1417,42 @@ void NormSession::SenderSetWatermark(NormObjectId objectId, int robustFactor = GetTxRobustFactor(); while ((next = iterator.GetNextNode())) static_cast(next)->Reset(robustFactor); + + if (NULL != appAckReq) + { + if (appAckReqLen != ack_ex_length) + { + if (NULL != ack_ex_buffer) + { + delete[] ack_ex_buffer; + ack_ex_buffer = NULL; + ack_ex_length = 0; + } + // Make sure there is room left for at least one acker NormNodeId in + if ((NormHeaderExtension::GetContentOffset() + appAckReqLen) > (segment_size - sizeof(NormNodeId))) + { + PLOG(PL_ERROR, "NormSession::SenderSetWatermark() error: application-defined ACK_REQ content too large!\n"); + watermark_pending = false; + return false; + } + else if (NULL == (ack_ex_buffer = new char[appAckReqLen])) + { + PLOG(PL_ERROR, "NormSession::SenderSetWatermark() new app_req_buffer error: %s\n", GetErrorString()); + watermark_pending = false; + return false; + } + } + memcpy(ack_ex_buffer, appAckReq, appAckReqLen); + ack_ex_length = appAckReqLen; + } + else if (NULL != ack_ex_buffer) + { + delete[] ack_ex_buffer; + ack_ex_buffer = NULL; + ack_ex_length = 0; + } PromptSender(); + return true; } // end NormSession::SenderSetWatermark() void NormSession::SenderResetWatermark() @@ -1504,6 +1595,21 @@ bool NormSession::SenderGetNextAckingNode(NormNodeId& prevNodeId, AckingStatus* } } // end NormSession::SenderGetNextAckingNode() +bool NormSession::SenderGetAckEx(NormNodeId nodeId, char* buffer, unsigned int* buflen) +{ + NormAckingNode* theNode = + static_cast(acking_node_tree.FindNodeById(nodeId)); + if (NULL != theNode) + { + return theNode->GetAckEx(buffer, buflen); + } + else + { + if (NULL != buflen) *buflen = 0; + return false; + } +} // end NormSession::SenderGetAckEx() + bool NormSession::SenderQueueWatermarkFlush() { if (flush_timer.IsActive()) return false; @@ -1511,6 +1617,7 @@ bool NormSession::SenderQueueWatermarkFlush() if (flush) { flush->Init(); + flush->SetDestination(address); flush->SetGrtt(grtt_quantized); flush->SetBackoffFactor((unsigned char)backoff_factor); @@ -1527,6 +1634,14 @@ bool NormSession::SenderQueueWatermarkFlush() blockLen = watermark_segment_id; flush->SetFecPayloadId(fec_id, watermark_block_id.GetValue(), watermark_segment_id, blockLen, fec_m); + if (0 != ack_ex_length) + { + NormAppAckExtension ext; + flush->AttachExtension(ext); + ext.SetContent(ack_ex_buffer, ack_ex_length); + flush->PackExtension(ext); + } + NormNodeTreeIterator iterator(acking_node_tree); NormAckingNode* next; watermark_pending = false; @@ -1872,6 +1987,12 @@ bool NormSession::QueueTxObject(NormObject* obj) return false; } + if (preset_fti.IsValid() && (obj->GetSize() != preset_fti.GetObjectSize())) + { + PLOG(PL_FATAL, "NormSession::QueueTxObject() preset object info mismatch!\n"); + return false; + } + // Manage tx_table min/max count and max size bounds // Depending on tx cache bounds _and_ what has been // enqueued/dequeued, we may need to prune the @@ -2424,7 +2545,7 @@ void NormTrace(const struct timeval& currentTime, "RTT", "APP" }; - + NormMsg::Type msgType = msg.GetType(); UINT16 length = msg.GetLength(); const char* status = sent ? "dst" : "src"; @@ -3284,7 +3405,7 @@ void NormSession::SenderHandleCCFeedback(struct timeval currentTime, // 3) Replace candidate if this response is higher precedence if (candidate) { - bool haveRtt = (0 != (ccFlags && NormCC::RTT)); + bool haveRtt = (0 != (ccFlags & NormCC::RTT)); bool replace; if (candidate->GetId() == nodeId) replace = true; @@ -3377,6 +3498,19 @@ void NormSession::SenderHandleAckMessage(const struct timeval& currentTime, cons (watermark_block_id == flushAck.GetFecBlockId(fec_m)) && (watermark_segment_id == flushAck.GetFecSymbolId(fec_m))) { + // Cache any application-defined extended ACK content for this acker + NormAppAckExtension ext; + while (ack.GetNextExtension(ext)) + { + if (NormHeaderExtension::APP_ACK == ext.GetType()) + { + if (!acker->SetAckEx(ext.GetContent(), ext.GetContentLength())) + { + // TBD - notify app of error + PLOG(PL_ERROR, "NormSession::SenderHandleAckMessage() error: unable to cache application-defined ACK content!\n"); + } + } + } acker->MarkAckReceived(); /* This code was an attempt to expedite delivery of the TX_WATERMARK_COMPLETED notification to the application, but breaks some other desired behavior. @@ -3403,7 +3537,7 @@ void NormSession::SenderHandleAckMessage(const struct timeval& currentTime, cons { // This can happen when new watermarks are set when an old watermark is still // pending (i.e. receivers may still be in the process of replying) - PLOG(PL_DEBUG, "NormSession::SenderHandleAckMessage() received wrong watermark ACK?!\n"); + PLOG(PL_DEBUG, "NormSession::SenderHandleAckMessage() received old/wrong watermark ACK?!\n"); } } else @@ -4743,6 +4877,7 @@ NormSession::MessageStatus NormSession::SendMessage(NormMsg& msg) UINT16 msgSize = msg.GetLength(); // Possibly drop some tx messages for testing purposes + bool drop = (tx_loss_rate > 0.0) ? (UniformRand(100.0) < tx_loss_rate) : false; if (isReceiverMsg && receiver_silent) diff --git a/src/java/jni/normInstanceJni.cpp b/src/java/jni/normInstanceJni.cpp index aaef916..028b08e 100644 --- a/src/java/jni/normInstanceJni.cpp +++ b/src/java/jni/normInstanceJni.cpp @@ -182,7 +182,7 @@ JNIEXPORT jobject JNICALL PKGNAME(NormInstance_getNextEvent) if (env->GetArrayLength(array) <= event.type) { env->ThrowNew((jclass)env->NewLocalRef(jw_IOException), "Invalid NORM event type (NormEventType.java out of sync with NORM API event header?)"); - return false; + return NULL; } jobject type = env->GetObjectArrayElement(array, event.type); diff --git a/src/java/src/mil/navy/nrl/norm/enums/NormEventType.java b/src/java/src/mil/navy/nrl/norm/enums/NormEventType.java index 8ba2f56..64ee2fb 100644 --- a/src/java/src/mil/navy/nrl/norm/enums/NormEventType.java +++ b/src/java/src/mil/navy/nrl/norm/enums/NormEventType.java @@ -8,32 +8,33 @@ package mil.navy.nrl.norm.enums; * @author Jason Rush */ public enum NormEventType { - NORM_EVENT_INVALID, - NORM_TX_QUEUE_VACANCY, - NORM_TX_QUEUE_EMPTY, - NORM_TX_FLUSH_COMPLETED, - NORM_TX_WATERMARK_COMPLETED, - NORM_TX_CMD_SENT, - NORM_TX_OBJECT_SENT, - NORM_TX_OBJECT_PURGED, - NORM_TX_RATE_CHANGED, - NORM_LOCAL_SENDER_CLOSED, - NORM_REMOTE_SENDER_NEW, - NORM_REMOTE_SENDER_RESET, - NORM_REMOTE_SENDER_ADDRESS, - NORM_REMOTE_SENDER_ACTIVE, - NORM_REMOTE_SENDER_INACTIVE, - NORM_REMOTE_SENDER_PURGED, - NORM_RX_CMD_NEW, - NORM_RX_OBJECT_NEW, - NORM_RX_OBJECT_INFO, - NORM_RX_OBJECT_UPDATED, - NORM_RX_OBJECT_COMPLETED, - NORM_RX_OBJECT_ABORTED, - NORM_GRTT_UPDATED, - NORM_CC_ACTIVE, + NORM_EVENT_INVALID, + NORM_TX_QUEUE_VACANCY, + NORM_TX_QUEUE_EMPTY, + NORM_TX_FLUSH_COMPLETED, + NORM_TX_WATERMARK_COMPLETED, + NORM_TX_CMD_SENT, + NORM_TX_OBJECT_SENT, + NORM_TX_OBJECT_PURGED, + NORM_TX_RATE_CHANGED, + NORM_LOCAL_SENDER_CLOSED, + NORM_REMOTE_SENDER_NEW, + NORM_REMOTE_SENDER_RESET, + NORM_REMOTE_SENDER_ADDRESS, + NORM_REMOTE_SENDER_ACTIVE, + NORM_REMOTE_SENDER_INACTIVE, + NORM_REMOTE_SENDER_PURGED, + NORM_RX_CMD_NEW, + NORM_RX_OBJECT_NEW, + NORM_RX_OBJECT_INFO, + NORM_RX_OBJECT_UPDATED, + NORM_RX_OBJECT_COMPLETED, + NORM_RX_OBJECT_ABORTED, + NORM_RX_ACK_REQUEST, + NORM_GRTT_UPDATED, + NORM_CC_ACTIVE, NORM_CC_INACTIVE, - NORM_ACKING_NODE_NEW, - NORM_SEND_ERROR, - NORM_USER_TIMEOUT + NORM_ACKING_NODE_NEW, + NORM_SEND_ERROR, + NORM_USER_TIMEOUT } diff --git a/src/pynorm/constants.py b/src/pynorm/constants.py index c2ad5d0..f4d06d3 100644 --- a/src/pynorm/constants.py +++ b/src/pynorm/constants.py @@ -69,11 +69,13 @@ NORM_RX_OBJECT_INFO = 18 NORM_RX_OBJECT_UPDATED = 19 NORM_RX_OBJECT_COMPLETED = 20 NORM_RX_OBJECT_ABORTED = 21 -NORM_GRTT_UPDATED = 22 -NORM_CC_ACTIVE = 23 -NORM_CC_INACTIVE = 24 - - +NORM_RX_ACK_REQUEST = 22 +NORM_GRTT_UPDATED = 23 +NORM_CC_ACTIVE = 24 +NORM_CC_INACTIVE = 25 +NORM_ACKING_NODE_NEW = 26 +NORM_SEND_ERROR = 27 +NORM_USER_TIMEOUT = 28 NORM_INSTANCE_INVALID = ctypes.c_void_p.in_dll(libnorm, "NORM_INSTANCE_INVALID").value NORM_SESSION_INVALID = ctypes.c_void_p.in_dll(libnorm, "NORM_SESSION_INVALID").value diff --git a/src/pynorm/event.py b/src/pynorm/event.py index c3b1ba9..3db3f7b 100644 --- a/src/pynorm/event.py +++ b/src/pynorm/event.py @@ -85,6 +85,8 @@ class Event(object): return "NORM_RX_OBJECT_COMPLETED" elif self.type == c.NORM_RX_OBJECT_ABORTED: return "NORM_RX_OBJECT_ABORTED" + elif self.type == c.NORM_RX_ACK_REQUEST: + return "NORM_RX_ACK_REQUEST" elif self.type == c.NORM_GRTT_UPDATED: return "NORM_GRTT_UPDATED" elif self.type == c.NORM_CC_ACTIVE: diff --git a/wscript b/wscript index 8428899..b17bebc 100644 --- a/wscript +++ b/wscript @@ -65,8 +65,11 @@ def configure(ctx): ctx.env.DEFINES_BUILD_NORM += ['NORM_USE_DLL'] if ctx.env.COMPILER_CXX == 'g++' or ctx.env.COMPILER_CXX == 'clang++': - ctx.env.CFLAGS += ['-fvisibility=hidden'] - ctx.env.CXXFLAGS += ['-fvisibility=hidden'] + ctx.env.CFLAGS += ['-fvisibility=hidden', '-Wno-attributes'] + ctx.env.CXXFLAGS += ['-fvisibility=hidden', '-Wno-attributes'] + + # Will be used by the pkg-config generator + ctx.env.VERSION = VERSION def build(ctx): ctx.recurse('protolib') @@ -132,7 +135,8 @@ def build(ctx): if ctx.env.BUILD_JAVA: ctx.shlib( target = 'mil_navy_nrl_norm', - use = ['norm_shlib', 'JAVA'], + includes = ['include'], + use = ['norm', 'JAVA'], vnum = VERSION, source = ['src/java/jni/{0}.cpp'.format(x) for x in [ 'normJni', @@ -205,6 +209,16 @@ def build(ctx): # Enable example targets specified on the command line ctx._parse_targets() + # Generate pkg-config file + # Add additional static compilation dependencies based on the system. + # libpcap is used by protolib on GNU/Hurd based systems. + static_libs = '' + if ctx.options.enable_static_library: + static_libs += ' -lstdc++ -lprotolib' + if system == "gnu": + static_libs += ' -lpcap' + ctx(source='norm.pc.in', STATIC_LIBS = static_libs) + def _make_simple_example(ctx, name, path='examples'): '''Makes a task from a single source file in the examples directory.