From 705fcb6173985141c16c4198666bcac2c54f4d19 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 10 Dec 2025 21:07:42 +0100 Subject: [PATCH 01/32] Add serial communication functionality with CMake configuration - Introduced new source files for serial communication: `serial_open.cpp`, `serial_close.cpp`, `serial_read.cpp`, and `serial_write.cpp`. - Implemented error handling and callback mechanisms for serial operations. - Added a test suite for serial communication with Arduino, covering open/close, read/write operations, and timeout behavior. - Configured CMake to copy compile commands to the project root for easier debugging. - Removed the placeholder source file to clean up the project structure. --- .clang-format | 161 +++++++++ .clang-tidy | 596 ++++++++++++++++++++++++++++++++++ CMakeLists.txt | 18 +- src/placeholder.cpp | 2 - src/serial_close.cpp | 31 ++ src/serial_open.cpp | 193 +++++++++++ src/serial_read.cpp | 157 +++++++++ src/serial_write.cpp | 96 ++++++ tests/test_serial_arduino.cpp | 130 ++++++++ 9 files changed, 1376 insertions(+), 8 deletions(-) create mode 100644 .clang-format create mode 100644 .clang-tidy delete mode 100644 src/placeholder.cpp create mode 100644 src/serial_close.cpp create mode 100644 src/serial_open.cpp create mode 100644 src/serial_read.cpp create mode 100644 src/serial_write.cpp create mode 100644 tests/test_serial_arduino.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ed08e49 --- /dev/null +++ b/.clang-format @@ -0,0 +1,161 @@ +--- +Language: Cpp +# BasedOnStyle: Microsoft +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignEscapedNewlines: Right +AlignOperands: Align +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: false + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: true +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +StatementAttributeLikeMacros: + - Q_EMIT +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: false +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentRequires: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 1000 +PenaltyIndentedWhitespace: 0 +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SortJavaStaticImport: Before +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceAroundPointerQualifiers: Default +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +BitFieldColonSpacing: Both +Standard: Latest +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 4 +UseCRLF: false +UseTab: Never +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME +... diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..554459d --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,596 @@ +--- +Checks: '*, + -bugprone-easily-swappable-parameters, + -bugprone-lambda-function-name, + -readability-magic-numbers, + -hicpp-no-array-decay, + -hicpp-signed-bitwise, + -hicpp-vararg, + -misc-non-private-member-variables-in-classes, + -cppcoreguidelines-*, + -fuchsia-*, + -altera-*, + -android-*, + -llvmlibc-*, + -readability-convert-member-functions-to-static, + -boost-use-ranges, + -google-objc-function-naming, + -google-objc-global-variable-declaration, + -performance-enum-size' +WarningsAsErrors: '' +HeaderFilterRegex: '^(?!magic_enum).*$' +FormatStyle: Microsoft +CheckOptions: + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.ClassMemberCase + value: lower_case + - key: readability-identifier-naming.ConstexprVariableCase + value: CamelCase + - key: readability-identifier-naming.ConstexprVariablePrefix + value: k + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantPrefix + value: k + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.FunctionIgnoredRegexp + value: '^BM_.*$' + - key: readability-identifier-naming.GlobalConstantCase + value: CamelCase + - key: readability-identifier-naming.GlobalConstantPrefix + value: k + - key: readability-identifier-naming.StaticConstantCase + value: CamelCase + - key: readability-identifier-naming.StaticConstantPrefix + value: k + - key: readability-identifier-naming.StaticVariableCase + value: lower_case + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + - key: readability-identifier-naming.MacroDefinitionIgnoredRegexp + value: '^[A-Z]+(_[A-Z]+)*_$' + - key: readability-identifier-naming.MemberCase + value: lower_case + - key: readability-identifier-naming.PrivateMemberSuffix + value: _ + - key: readability-identifier-naming.PublicMemberSuffix + value: '' + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.TypeAliasCase + value: CamelCase + - key: readability-identifier-naming.TypedefCase + value: CamelCase + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.IgnoreMainLikeFunctions + value: 'true' + - key: readability-identifier-naming.AggressiveDependentMemberLookup + value: 'false' + - key: readability-identifier-naming.IgnoreFailedSplit + value: 'false' + - key: abseil-string-find-startswith.AbseilStringsMatchHeader + value: 'absl/strings/match.h' + - key: abseil-string-find-startswith.IncludeStyle + value: llvm + - key: abseil-string-find-startswith.StringLikeClasses + value: '::std::basic_string' + - key: abseil-string-find-str-contains.AbseilStringsMatchHeader + value: 'absl/strings/match.h' + - key: abseil-string-find-str-contains.IncludeStyle + value: llvm + - key: abseil-string-find-str-contains.StringLikeClasses + value: '::std::basic_string;::std::basic_string_view;::absl::string_view' + - key: bugprone-argument-comment.CommentBoolLiterals + value: '0' + - key: bugprone-argument-comment.CommentCharacterLiterals + value: '0' + - key: bugprone-argument-comment.CommentFloatLiterals + value: '0' + - key: bugprone-argument-comment.CommentIntegerLiterals + value: '0' + - key: bugprone-argument-comment.CommentNullPtrs + value: '0' + - key: bugprone-argument-comment.CommentStringLiterals + value: '0' + - key: bugprone-argument-comment.CommentUserDefinedLiterals + value: '0' + - key: bugprone-argument-comment.IgnoreSingleArgument + value: '0' + - key: bugprone-argument-comment.StrictMode + value: '0' + - key: bugprone-assert-side-effect.AssertMacros + value: assert + - key: bugprone-assert-side-effect.CheckFunctionCalls + value: 'false' + - key: bugprone-dangling-handle.HandleClasses + value: 'std::basic_string_view;std::experimental::basic_string_view' + - key: bugprone-dynamic-static-initializers.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: bugprone-exception-escape.FunctionsThatShouldNotThrow + value: '' + - key: bugprone-exception-escape.IgnoredExceptions + value: '' + - key: bugprone-misplaced-widening-cast.CheckImplicitCasts + value: 'false' + - key: bugprone-narrowing-conversions.PedanticMode + value: 'false' + - key: bugprone-narrowing-conversions.WarnOnFloatingPointNarrowingConversion + value: 'true' + - key: bugprone-not-null-terminated-result.WantToUseSafeFunctions + value: 'true' + - key: bugprone-reserved-identifier.AggressiveDependentMemberLookup + value: 'false' + - key: bugprone-reserved-identifier.AllowedIdentifiers + value: '' + - key: bugprone-reserved-identifier.Invert + value: 'false' + - key: bugprone-signed-char-misuse.CharTypdefsToIgnore + value: '' + - key: bugprone-signed-char-misuse.DiagnoseSignedUnsignedCharComparisons + value: 'true' + - key: bugprone-sizeof-expression.WarnOnSizeOfCompareToConstant + value: 'true' + - key: bugprone-sizeof-expression.WarnOnSizeOfConstant + value: 'true' + - key: bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression + value: 'false' + - key: bugprone-sizeof-expression.WarnOnSizeOfThis + value: 'true' + - key: bugprone-string-constructor.LargeLengthThreshold + value: '8388608' + - key: bugprone-string-constructor.WarnOnLargeLength + value: 'true' + - key: bugprone-suspicious-enum-usage.StrictMode + value: 'false' + - key: bugprone-suspicious-include.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: bugprone-suspicious-include.ImplementationFileExtensions + value: 'c;cc;cpp;cxx' + - key: bugprone-suspicious-missing-comma.MaxConcatenatedTokens + value: '5' + - key: bugprone-suspicious-missing-comma.RatioThreshold + value: '0.200000' + - key: bugprone-suspicious-missing-comma.SizeThreshold + value: '5' + - key: bugprone-suspicious-string-compare.StringCompareLikeFunctions + value: '' + - key: bugprone-suspicious-string-compare.WarnOnImplicitComparison + value: 'true' + - key: bugprone-suspicious-string-compare.WarnOnLogicalNotComparison + value: 'false' + - key: bugprone-too-small-loop-variable.MagnitudeBitsUpperLimit + value: '16' + - key: bugprone-unhandled-self-assignment.WarnOnlyIfThisHasSuspiciousField + value: 'true' + - key: bugprone-unused-return-value.CheckedFunctions + value: '::std::async;::std::launder;::std::remove;::std::remove_if;::std::unique;::std::unique_ptr::release;::std::basic_string::empty;::std::vector::empty;::std::back_inserter;::std::distance;::std::find;::std::find_if;::std::inserter;::std::lower_bound;::std::make_pair;::std::map::count;::std::map::find;::std::map::lower_bound;::std::multimap::equal_range;::std::multimap::upper_bound;::std::set::count;::std::set::find;::std::setfill;::std::setprecision;::std::setw;::std::upper_bound;::std::vector::at;::bsearch;::ferror;::feof;::isalnum;::isalpha;::isblank;::iscntrl;::isdigit;::isgraph;::islower;::isprint;::ispunct;::isspace;::isupper;::iswalnum;::iswprint;::iswspace;::isxdigit;::memchr;::memcmp;::strcmp;::strcoll;::strncmp;::strpbrk;::strrchr;::strspn;::strstr;::wcscmp;::access;::bind;::connect;::difftime;::dlsym;::fnmatch;::getaddrinfo;::getopt;::htonl;::htons;::iconv_open;::inet_addr;::isascii;::isatty;::mmap;::newlocale;::openat;::pathconf;::pthread_equal;::pthread_getspecific;::pthread_mutex_trylock;::readdir;::readlink;::recvmsg;::regexec;::scandir;::semget;::setjmp;::shm_open;::shmget;::sigismember;::strcasecmp;::strsignal;::ttyname' + - key: cert-dcl16-c.IgnoreMacros + value: 'true' + - key: cert-dcl16-c.NewSuffixes + value: 'L;LL;LU;LLU' + - key: cert-dcl37-c.AggressiveDependentMemberLookup + value: 'false' + - key: cert-dcl37-c.AllowedIdentifiers + value: '' + - key: cert-dcl37-c.Invert + value: 'false' + - key: cert-dcl51-cpp.AggressiveDependentMemberLookup + value: 'false' + - key: cert-dcl51-cpp.AllowedIdentifiers + value: '' + - key: cert-dcl51-cpp.Invert + value: 'false' + - key: cert-dcl59-cpp.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: cert-err09-cpp.CheckThrowTemporaries + value: 'true' + - key: cert-err09-cpp.MaxSize + value: '1000000' + - key: cert-err09-cpp.WarnOnLargeObjects + value: 'false' + - key: cert-err61-cpp.CheckThrowTemporaries + value: 'true' + - key: cert-err61-cpp.MaxSize + value: '1000000' + - key: cert-err61-cpp.WarnOnLargeObjects + value: 'false' + - key: cert-msc32-c.DisallowedSeedTypes + value: 'time_t,std::time_t' + - key: cert-msc51-cpp.DisallowedSeedTypes + value: 'time_t,std::time_t' + - key: cert-oop11-cpp.IncludeStyle + value: llvm + - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField + value: 'false' + - key: cert-oop57-cpp.MemCmpNames + value: '' + - key: cert-oop57-cpp.MemCpyNames + value: '' + - key: cert-oop57-cpp.MemSetNames + value: '' + - key: cert-str34-c.CharTypdefsToIgnore + value: '' + - key: cert-str34-c.DiagnoseSignedUnsignedCharComparisons + value: 'false' + - key: cppcoreguidelines-avoid-magic-numbers.IgnoreAllFloatingPointValues + value: 'false' + - key: cppcoreguidelines-avoid-magic-numbers.IgnoreBitFieldsWidths + value: 'true' + - key: cppcoreguidelines-avoid-magic-numbers.IgnorePowersOf2IntegerValues + value: 'false' + - key: cppcoreguidelines-avoid-magic-numbers.IgnoredFloatingPointValues + value: '1.0;100.0;' + - key: cppcoreguidelines-avoid-magic-numbers.IgnoredIntegerValues + value: '1;2;3;4;' + - key: cppcoreguidelines-explicit-virtual-functions.AllowOverrideAndFinal + value: 'false' + - key: cppcoreguidelines-explicit-virtual-functions.FinalSpelling + value: final + - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors + value: 'true' + - key: cppcoreguidelines-explicit-virtual-functions.OverrideSpelling + value: override + - key: cppcoreguidelines-init-variables.IncludeStyle + value: llvm + - key: cppcoreguidelines-init-variables.MathHeader + value: math.h + - key: cppcoreguidelines-macro-usage.AllowedRegexp + value: '^DEBUG_*' + - key: cppcoreguidelines-macro-usage.CheckCapsOnly + value: 'false' + - key: cppcoreguidelines-macro-usage.IgnoreCommandLineMacros + value: 'true' + - key: cppcoreguidelines-narrowing-conversions.PedanticMode + value: 'false' + - key: cppcoreguidelines-narrowing-conversions.WarnOnFloatingPointNarrowingConversion + value: 'true' + - key: cppcoreguidelines-no-malloc.Allocations + value: '::malloc;::calloc' + - key: cppcoreguidelines-no-malloc.Deallocations + value: '::free' + - key: cppcoreguidelines-no-malloc.Reallocations + value: '::realloc' + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 'true' + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnorePublicMemberVariables + value: 'false' + - key: cppcoreguidelines-owning-memory.LegacyResourceConsumers + value: '::free;::realloc;::freopen;::fclose' + - key: cppcoreguidelines-owning-memory.LegacyResourceProducers + value: '::malloc;::aligned_alloc;::realloc;::calloc;::fopen;::freopen;::tmpfile' + - key: cppcoreguidelines-pro-bounds-constant-array-index.GslHeader + value: '' + - key: cppcoreguidelines-pro-bounds-constant-array-index.IncludeStyle + value: llvm + - key: cppcoreguidelines-pro-type-member-init.IgnoreArrays + value: 'false' + - key: cppcoreguidelines-pro-type-member-init.UseAssignment + value: 'false' + - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctions + value: 'false' + - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted + value: 'false' + - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor + value: 'false' + - key: google-build-namespaces.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: google-global-names-in-headers.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: google-readability-function-size.BranchThreshold + value: '4294967295' + - key: google-readability-function-size.LineThreshold + value: '4294967295' + - key: google-readability-function-size.NestingThreshold + value: '4294967295' + - key: google-readability-function-size.ParameterThreshold + value: '4294967295' + - key: google-readability-function-size.StatementThreshold + value: '800' + - key: google-readability-function-size.VariableThreshold + value: '4294967295' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: google-runtime-int.SignedTypePrefix + value: int + - key: google-runtime-int.TypeSuffix + value: '' + - key: google-runtime-int.UnsignedTypePrefix + value: uint + - key: google-runtime-references.IncludedTypes + value: '' + - key: hicpp-braces-around-statements.ShortStatementLines + value: '0' + - key: hicpp-function-size.BranchThreshold + value: '4294967295' + - key: hicpp-function-size.LineThreshold + value: '4294967295' + - key: hicpp-function-size.NestingThreshold + value: '4294967295' + - key: hicpp-function-size.ParameterThreshold + value: '4294967295' + - key: hicpp-function-size.StatementThreshold + value: '800' + - key: hicpp-function-size.VariableThreshold + value: '4294967295' + - key: hicpp-member-init.IgnoreArrays + value: 'false' + - key: hicpp-member-init.UseAssignment + value: 'false' + - key: hicpp-move-const-arg.CheckTriviallyCopyableMove + value: 'true' + - key: hicpp-multiway-paths-covered.WarnOnMissingElse + value: 'false' + - key: hicpp-no-malloc.Allocations + value: '::malloc;::calloc' + - key: hicpp-no-malloc.Deallocations + value: '::free' + - key: hicpp-no-malloc.Reallocations + value: '::realloc' + - key: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals + value: 'false' + - key: hicpp-special-member-functions.AllowMissingMoveFunctions + value: 'false' + - key: hicpp-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted + value: 'false' + - key: hicpp-special-member-functions.AllowSoleDefaultDtor + value: 'false' + - key: hicpp-uppercase-literal-suffix.IgnoreMacros + value: 'true' + - key: hicpp-uppercase-literal-suffix.NewSuffixes + value: '' + - key: hicpp-use-auto.MinTypeNameLength + value: '5' + - key: hicpp-use-auto.RemoveStars + value: 'false' + - key: hicpp-use-emplace.ContainersWithPushBack + value: '::std::vector;::std::list;::std::deque' + - key: hicpp-use-emplace.IgnoreImplicitConstructors + value: 'false' + - key: hicpp-use-emplace.SmartPointers + value: '::std::shared_ptr;::std::unique_ptr;::std::auto_ptr;::std::weak_ptr' + - key: hicpp-use-emplace.TupleMakeFunctions + value: '::std::make_pair;::std::make_tuple' + - key: hicpp-use-emplace.TupleTypes + value: '::std::pair;::std::tuple' + - key: hicpp-use-equals-default.IgnoreMacros + value: 'true' + - key: hicpp-use-equals-delete.IgnoreMacros + value: 'true' + - key: hicpp-use-noexcept.ReplacementString + value: '' + - key: hicpp-use-noexcept.UseNoexceptFalse + value: 'true' + - key: hicpp-use-nullptr.NullMacros + value: '' + - key: hicpp-use-override.AllowOverrideAndFinal + value: 'false' + - key: hicpp-use-override.FinalSpelling + value: final + - key: hicpp-use-override.IgnoreDestructors + value: 'false' + - key: hicpp-use-override.OverrideSpelling + value: override + - key: llvm-else-after-return.WarnOnConditionVariables + value: 'false' + - key: llvm-else-after-return.WarnOnUnfixable + value: 'false' + - key: llvm-header-guard.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: llvm-namespace-comment.ShortNamespaceLines + value: '1' + - key: llvm-namespace-comment.SpacesBeforeComments + value: '1' + - key: llvm-qualified-auto.AddConstToQualified + value: 'false' + - key: misc-definitions-in-headers.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: misc-definitions-in-headers.UseHeaderFileExtension + value: 'true' + - key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 'false' + - key: misc-non-private-member-variables-in-classes.IgnorePublicMemberVariables + value: 'false' + - key: misc-throw-by-value-catch-by-reference.CheckThrowTemporaries + value: 'true' + - key: misc-throw-by-value-catch-by-reference.MaxSize + value: '1000000' + - key: misc-throw-by-value-catch-by-reference.WarnOnLargeObjects + value: 'false' + - key: misc-unused-parameters.StrictMode + value: 'false' + - key: modernize-avoid-bind.PermissiveParameterList + value: 'false' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: modernize-make-shared.IgnoreMacros + value: 'true' + - key: modernize-make-shared.IncludeStyle + value: llvm + - key: modernize-make-shared.MakeSmartPtrFunction + value: 'std::make_shared' + - key: modernize-make-shared.MakeSmartPtrFunctionHeader + value: memory + - key: modernize-make-unique.IgnoreMacros + value: 'true' + - key: modernize-make-unique.IncludeStyle + value: llvm + - key: modernize-make-unique.MakeSmartPtrFunction + value: 'std::make_unique' + - key: modernize-make-unique.MakeSmartPtrFunctionHeader + value: memory + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-pass-by-value.ValuesOnly + value: 'false' + - key: modernize-raw-string-literal.DelimiterStem + value: lit + - key: modernize-raw-string-literal.ReplaceShorterLiterals + value: 'false' + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: modernize-replace-disallow-copy-and-assign-macro.MacroName + value: DISALLOW_COPY_AND_ASSIGN + - key: modernize-replace-random-shuffle.IncludeStyle + value: llvm + - key: modernize-use-auto.MinTypeNameLength + value: '5' + - key: modernize-use-auto.RemoveStars + value: 'false' + - key: modernize-use-bool-literals.IgnoreMacros + value: 'true' + - key: modernize-use-default-member-init.IgnoreMacros + value: 'true' + - key: modernize-use-default-member-init.UseAssignment + value: 'false' + - key: modernize-use-emplace.ContainersWithPushBack + value: '::std::vector;::std::list;::std::deque' + - key: modernize-use-emplace.IgnoreImplicitConstructors + value: 'false' + - key: modernize-use-emplace.SmartPointers + value: '::std::shared_ptr;::std::unique_ptr;::std::auto_ptr;::std::weak_ptr' + - key: modernize-use-emplace.TupleMakeFunctions + value: '::std::make_pair;::std::make_tuple' + - key: modernize-use-emplace.TupleTypes + value: '::std::pair;::std::tuple' + - key: modernize-use-equals-default.IgnoreMacros + value: 'true' + - key: modernize-use-equals-delete.IgnoreMacros + value: 'true' + - key: modernize-use-nodiscard.ReplacementString + value: '[[nodiscard]]' + - key: modernize-use-noexcept.ReplacementString + value: '' + - key: modernize-use-noexcept.UseNoexceptFalse + value: 'true' + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: modernize-use-override.AllowOverrideAndFinal + value: 'false' + - key: modernize-use-override.FinalSpelling + value: final + - key: modernize-use-override.IgnoreDestructors + value: 'false' + - key: modernize-use-override.OverrideSpelling + value: override + - key: modernize-use-transparent-functors.SafeMode + value: 'false' + - key: modernize-use-using.IgnoreMacros + value: 'true' + - key: objc-forbidden-subclassing.ForbiddenSuperClassNames + value: 'ABNewPersonViewController;ABPeoplePickerNavigationController;ABPersonViewController;ABUnknownPersonViewController;NSHashTable;NSMapTable;NSPointerArray;NSPointerFunctions;NSTimer;UIActionSheet;UIAlertView;UIImagePickerController;UITextInputMode;UIWebView' + - key: openmp-exception-escape.IgnoredExceptions + value: '' + - key: performance-faster-string-find.StringLikeClasses + value: '::std::basic_string;::std::basic_string_view' + - key: performance-for-range-copy.AllowedTypes + value: '' + - key: performance-for-range-copy.WarnOnAllAutoCopies + value: 'false' + - key: performance-inefficient-string-concatenation.StrictMode + value: 'false' + - key: performance-inefficient-vector-operation.EnableProto + value: 'false' + - key: performance-inefficient-vector-operation.VectorLikeClasses + value: '::std::vector' + - key: performance-move-const-arg.CheckTriviallyCopyableMove + value: 'true' + - key: performance-move-constructor-init.IncludeStyle + value: llvm + - key: performance-no-automatic-move.AllowedTypes + value: '' + - key: performance-type-promotion-in-math-fn.IncludeStyle + value: llvm + - key: performance-unnecessary-copy-initialization.AllowedTypes + value: '' + - key: performance-unnecessary-value-param.AllowedTypes + value: '' + - key: performance-unnecessary-value-param.IncludeStyle + value: llvm + - key: portability-restrict-system-includes.Includes + value: '*' + - key: portability-simd-intrinsics.Std + value: '' + - key: portability-simd-intrinsics.Suggest + value: '0' + - key: readability-braces-around-statements.ShortStatementLines + value: '0' + - key: readability-else-after-return.WarnOnConditionVariables + value: 'true' + - key: readability-else-after-return.WarnOnUnfixable + value: 'true' + - key: readability-function-size.BranchThreshold + value: '4294967295' + - key: readability-function-size.LineThreshold + value: '4294967295' + - key: readability-function-size.NestingThreshold + value: '4294967295' + - key: readability-function-size.ParameterThreshold + value: '4294967295' + - key: readability-function-size.StatementThreshold + value: '800' + - key: readability-function-size.VariableThreshold + value: '4294967295' + - key: readability-implicit-bool-conversion.AllowIntegerConditions + value: 'false' + - key: readability-implicit-bool-conversion.AllowPointerConditions + value: 'false' + - key: readability-inconsistent-declaration-parameter-name.IgnoreMacros + value: 'true' + - key: readability-inconsistent-declaration-parameter-name.Strict + value: 'false' + - key: readability-magic-numbers.IgnoreAllFloatingPointValues + value: 'false' + - key: readability-magic-numbers.IgnoreBitFieldsWidths + value: 'true' + - key: readability-magic-numbers.IgnorePowersOf2IntegerValues + value: 'false' + - key: readability-magic-numbers.IgnoredFloatingPointValues + value: '1.0;100.0;' + - key: readability-magic-numbers.IgnoredIntegerValues + value: '1;2;3;4;' + - key: readability-qualified-auto.AddConstToQualified + value: 'true' + - key: readability-redundant-declaration.IgnoreMacros + value: 'true' + - key: readability-redundant-member-init.IgnoreBaseInCopyConstructors + value: 'false' + - key: readability-redundant-smartptr-get.IgnoreMacros + value: 'true' + - key: readability-redundant-string-init.StringNames + value: '::std::basic_string' + - key: readability-simplify-boolean-expr.ChainedConditionalAssignment + value: 'false' + - key: readability-simplify-boolean-expr.ChainedConditionalReturn + value: 'false' + - key: readability-simplify-subscript-expr.Types + value: '::std::basic_string;::std::basic_string_view;::std::vector;::std::array' + - key: readability-static-accessed-through-instance.NameSpecifierNestingThreshold + value: '3' + - key: readability-uppercase-literal-suffix.IgnoreMacros + value: 'true' + - key: readability-uppercase-literal-suffix.NewSuffixes + value: '' + - key: zircon-temporary-objects.Names + value: '' +# allow x,y,z single letter names, to be used for coordinates +# allow fd (file descriptor) as it's a common POSIX convention + - key: readability-identifier-length.IgnoredVariableNames + value: '^(x|y|z|m0|m1|fd|_)$' + - key: readability-identifier-length.IgnoredParameterNames + value: '^(x|y|z|m0|m1|fd|_)$' +... diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fdf9a9..39ff558 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,9 +86,6 @@ target_link_libraries( target_compile_features(cpp_bindings_linux PUBLIC cxx_std_23) - -enable_testing() - # Collect all test source files file( GLOB_RECURSE TEST_SOURCES @@ -115,9 +112,6 @@ if(TEST_SOURCES) ) target_compile_features(cpp_bindings_linux_tests PRIVATE cxx_std_23) - - include(GoogleTest) - gtest_discover_tests(cpp_bindings_linux_tests) endif() include(GNUInstallDirs) @@ -142,3 +136,15 @@ install( DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} OPTIONAL ) + +if(CMAKE_EXPORT_COMPILE_COMMANDS AND EXISTS "${CMAKE_BINARY_DIR}/compile_commands.json") + add_custom_target( + copy-compile-commands + ALL + ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_BINARY_DIR}/compile_commands.json + ${CMAKE_SOURCE_DIR}/compile_commands.json + DEPENDS ${CMAKE_BINARY_DIR}/compile_commands.json + COMMENT "Copying compile_commands.json to project root" + ) +endif() diff --git a/src/placeholder.cpp b/src/placeholder.cpp deleted file mode 100644 index 450b790..0000000 --- a/src/placeholder.cpp +++ /dev/null @@ -1,2 +0,0 @@ - -namespace name {} diff --git a/src/serial_close.cpp b/src/serial_close.cpp new file mode 100644 index 0000000..ea62f72 --- /dev/null +++ b/src/serial_close.cpp @@ -0,0 +1,31 @@ +#include +#include + +#include +#include +#include + +extern "C" +{ + + MODULE_API auto serialClose(int64_t handle, ErrorCallbackT error_callback) -> int + { + if (handle <= 0) + { + return static_cast(cpp_core::StatusCodes::kSuccess); + } + + const int fd = static_cast(handle); + if (close(fd) != 0) + { + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kCloseHandleError), strerror(errno)); + } + return static_cast(cpp_core::StatusCodes::kCloseHandleError); + } + + return static_cast(cpp_core::StatusCodes::kSuccess); + } + +} // extern "C" diff --git a/src/serial_open.cpp b/src/serial_open.cpp new file mode 100644 index 0000000..b56645f --- /dev/null +++ b/src/serial_open.cpp @@ -0,0 +1,193 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifndef TCGETS2 +#define TCGETS2 0x802C542A +#define TCSETS2 0x402C542B +#define BOTHER 0x010000 +#endif + +struct termios2 +{ + tcflag_t c_iflag; + tcflag_t c_oflag; + tcflag_t c_cflag; + tcflag_t c_lflag; + cc_t c_line; + cc_t c_cc[19]; + speed_t c_ispeed; + speed_t c_ospeed; +}; + +extern "C" +{ + + MODULE_API auto serialOpen(void *port, int baudrate, int data_bits, int parity, int stop_bits, + ErrorCallbackT error_callback) -> intptr_t + { + if (port == nullptr) + { + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kNotFoundError), "Port parameter is nullptr"); + } + return static_cast(cpp_core::StatusCodes::kNotFoundError); + } + + if (baudrate < 300) + { + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), + "Invalid baudrate: must be >= 300"); + } + return static_cast(cpp_core::StatusCodes::kSetStateError); + } + + if (data_bits < 5 || data_bits > 8) + { + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), + "Invalid data bits: must be 5-8"); + } + return static_cast(cpp_core::StatusCodes::kSetStateError); + } + + const char *port_path = static_cast(port); + + const int fd = open(port_path, O_RDWR | O_NOCTTY | O_NONBLOCK); + if (fd < 0) + { + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kNotFoundError), strerror(errno)); + } + return static_cast(cpp_core::StatusCodes::kNotFoundError); + } + + struct termios2 tty = {}; + if (ioctl(fd, TCGETS2, &tty) != 0) + { + close(fd); + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kGetStateError), strerror(errno)); + } + return static_cast(cpp_core::StatusCodes::kGetStateError); + } + + tty.c_cflag &= ~CBAUD; + tty.c_cflag |= BOTHER; + tty.c_ispeed = baudrate; + tty.c_ospeed = baudrate; + + tty.c_cflag &= ~CSIZE; + switch (data_bits) + { + case 5: + tty.c_cflag |= CS5; + break; + case 6: + tty.c_cflag |= CS6; + break; + case 7: + tty.c_cflag |= CS7; + break; + case 8: + tty.c_cflag |= CS8; + break; + default: + close(fd); + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), "Invalid data bits"); + } + return static_cast(cpp_core::StatusCodes::kSetStateError); + } + + tty.c_cflag &= ~(PARENB | PARODD); + switch (parity) + { + case 0: + break; + case 1: + tty.c_cflag |= PARENB; + break; + case 2: + tty.c_cflag |= (PARENB | PARODD); + break; + default: + close(fd); + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), "Invalid parity"); + } + return static_cast(cpp_core::StatusCodes::kSetStateError); + } + + if (stop_bits == 2) + { + tty.c_cflag |= CSTOPB; + } + else + { + tty.c_cflag &= ~CSTOPB; + } + + tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); + tty.c_iflag &= ~(IXON | IXOFF | IXANY | INLCR | IGNCR | ICRNL); + tty.c_oflag &= ~OPOST; + + tty.c_cc[VMIN] = 0; + tty.c_cc[VTIME] = 0; + + if (ioctl(fd, TCSETS2, &tty) != 0) + { + close(fd); + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), strerror(errno)); + } + return static_cast(cpp_core::StatusCodes::kSetStateError); + } + + int flags = fcntl(fd, F_GETFL); + if (flags < 0) + { + close(fd); + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), strerror(errno)); + } + return static_cast(cpp_core::StatusCodes::kSetStateError); + } + flags &= ~O_NONBLOCK; + const int set_flags_result = fcntl(fd, F_SETFL, flags); + if (set_flags_result != 0) + { + close(fd); + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), strerror(errno)); + } + return static_cast(cpp_core::StatusCodes::kSetStateError); + } + + tcflush(fd, TCIOFLUSH); + + // Note: Some devices (e.g., Arduino) reset when the serial port is opened. + // It is recommended to wait 1-2 seconds after opening before sending data + // to allow the device to initialize. + + return static_cast(fd); + } + +} // extern "C" diff --git a/src/serial_read.cpp b/src/serial_read.cpp new file mode 100644 index 0000000..34fa95c --- /dev/null +++ b/src/serial_read.cpp @@ -0,0 +1,157 @@ +#include +#include + +#include +#include +#include +#include + +// NOLINTNEXTLINE(misc-use-anonymous-namespace) +static auto waitFdReady(int fd, int timeout_ms, bool for_read) -> int +{ + struct pollfd pfd = {}; + pfd.fd = fd; + pfd.events = for_read ? POLLIN : POLLOUT; + pfd.revents = 0; + + const int result = poll(&pfd, 1, timeout_ms); + + if (result < 0) + { + return -1; + } + if (result == 0) + { + return 0; + } + if (for_read && ((pfd.revents & POLLIN) != 0)) + { + return 1; + } + if (!for_read && ((pfd.revents & POLLOUT) != 0)) + { + return 1; + } + return 0; +} + +extern "C" +{ + + MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, + ErrorCallbackT error_callback) -> int + { + if (buffer == nullptr || buffer_size <= 0) + { + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kBufferError), "Invalid buffer or buffer_size"); + } + return static_cast(cpp_core::StatusCodes::kBufferError); + } + + if (handle <= 0) + { + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kInvalidHandleError), "Invalid handle"); + } + return static_cast(cpp_core::StatusCodes::kInvalidHandleError); + } + + const int fd = static_cast(handle); + auto *buf = static_cast(buffer); + + int elapsed_ms = 0; + const int poll_interval = 10; + bool data_ready = false; + + while (elapsed_ms < timeout_ms) + { + struct pollfd pfd = {}; + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + + const int remaining_ms = timeout_ms - elapsed_ms; + const int poll_timeout = (remaining_ms < poll_interval) ? remaining_ms : poll_interval; + + const int poll_result = poll(&pfd, 1, poll_timeout); + if (poll_result > 0 && ((pfd.revents & POLLIN) != 0)) + { + data_ready = true; + break; + } + if (poll_result < 0) + { + return 0; + } + + elapsed_ms += poll_interval; + } + + if (!data_ready) + { + return 0; + } + + ssize_t bytes_read = ::read(fd, buf, buffer_size); + + if (bytes_read < 0) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + return 0; + } + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kReadError), strerror(errno)); + } + return static_cast(cpp_core::StatusCodes::kReadError); + } + + if (bytes_read == 0) + { + struct pollfd pfd = {}; + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + if (poll(&pfd, 1, 10) > 0 && ((pfd.revents & POLLIN) != 0)) + { + bytes_read = ::read(fd, buf, buffer_size); + if (bytes_read <= 0) + { + return 0; + } + } + else + { + return 0; + } + } + + int total_read = static_cast(bytes_read); + while (total_read < buffer_size) + { + struct pollfd pfd = {}; + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + + if (poll(&pfd, 1, 0) <= 0 || ((pfd.revents & POLLIN) == 0)) + { + break; + } + + const ssize_t more_bytes = ::read(fd, buf + total_read, buffer_size - total_read); + if (more_bytes <= 0) + { + break; + } + total_read += static_cast(more_bytes); + } + + return total_read; + } + +} // extern "C" diff --git a/src/serial_write.cpp b/src/serial_write.cpp new file mode 100644 index 0000000..c806ce3 --- /dev/null +++ b/src/serial_write.cpp @@ -0,0 +1,96 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +// NOLINTNEXTLINE(misc-use-anonymous-namespace) +static auto waitFdReady(int fd, int timeout_ms, bool for_read) -> int +{ + struct pollfd pfd = {}; + pfd.fd = fd; + pfd.events = for_read ? POLLIN : POLLOUT; + pfd.revents = 0; + + const int result = poll(&pfd, 1, timeout_ms); + + if (result < 0) + { + return -1; + } + if (result == 0) + { + return 0; + } + if (for_read && ((pfd.revents & POLLIN) != 0)) + { + return 1; + } + if (!for_read && ((pfd.revents & POLLOUT) != 0)) + { + return 1; + } + return 0; +} + +extern "C" +{ + + MODULE_API auto serialWrite(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, + ErrorCallbackT error_callback) -> int + { + if (buffer == nullptr || buffer_size <= 0) + { + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kBufferError), "Invalid buffer or buffer_size"); + } + return static_cast(cpp_core::StatusCodes::kBufferError); + } + + if (handle <= 0) + { + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kInvalidHandleError), "Invalid handle"); + } + return static_cast(cpp_core::StatusCodes::kInvalidHandleError); + } + + const int fd = static_cast(handle); + + ssize_t bytes_written = ::write(fd, buffer, buffer_size); + if (bytes_written < 0) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + if (waitFdReady(fd, timeout_ms, false) > 0) + { + bytes_written = ::write(fd, buffer, buffer_size); + } + else + { + return 0; + } + } + + if (bytes_written < 0) + { + if (error_callback != nullptr) + { + error_callback(static_cast(cpp_core::StatusCodes::kWriteError), strerror(errno)); + } + return static_cast(cpp_core::StatusCodes::kWriteError); + } + } + + tcdrain(fd); + + return static_cast(bytes_written); + } + +} // extern "C" diff --git a/tests/test_serial_arduino.cpp b/tests/test_serial_arduino.cpp new file mode 100644 index 0000000..de57eef --- /dev/null +++ b/tests/test_serial_arduino.cpp @@ -0,0 +1,130 @@ +// Test for serial communication with Arduino echo script on /dev/ttyUSB0 + +#include +#include +#include +#include +#include +#include +#include + +#include + +class SerialArduinoTest : public ::testing::Test +{ + protected: + void SetUp() override + { + const char *port = "/dev/ttyUSB0"; + handle_ = serialOpen(const_cast(static_cast(port)), 115200, 8, 0, 0, nullptr); + + if (handle_ <= 0) + { + GTEST_SKIP() << "Could not open /dev/ttyUSB0. " + "Make sure Arduino is connected and accessible."; + } + + // Wait for device to initialize after opening (e.g., Arduino reset) + usleep(2000000); // 2 seconds + } + + void TearDown() override + { + if (handle_ > 0) + { + serialClose(handle_, nullptr); + handle_ = 0; + } + } + + intptr_t handle_ = 0; +}; + +// Test basic open/close +TEST_F(SerialArduinoTest, OpenClose) +{ + EXPECT_GT(handle_, 0) << "serialOpen should return a positive handle"; +} + +// Test write and read echo +TEST_F(SerialArduinoTest, WriteReadEcho) +{ + const char *test_message = "Hello Arduino!\n"; + int message_len = static_cast(strlen(test_message)); + + // Write message + int written = serialWrite(handle_, test_message, message_len, 1000, 1, nullptr); + EXPECT_EQ(written, message_len) << "Should write all bytes. Written: " << written << ", Expected: " << message_len; + + // Delay to allow Arduino to process and echo + usleep(500000); // 500ms - give Arduino more time + + // Read echo back + char read_buffer[256] = {0}; + int read_bytes = serialRead(handle_, read_buffer, sizeof(read_buffer) - 1, 2000, 1, nullptr); + + EXPECT_GT(read_bytes, 0) << "Should read at least some bytes"; + EXPECT_LE(read_bytes, static_cast(sizeof(read_buffer) - 1)) << "Should not overflow buffer"; + + // Check if we got the echo + read_buffer[read_bytes] = '\0'; + EXPECT_STRNE(read_buffer, "") << "Should receive echo from Arduino"; +} + +// Test multiple write/read cycles +TEST_F(SerialArduinoTest, MultipleEchoCycles) +{ + const char *messages[] = {"Test1\n", "Test2\n", "Test3\n"}; + const int num_messages = 3; + + for (int i = 0; i < num_messages; ++i) + { + int msg_len = static_cast(strlen(messages[i])); + + // Write + int written = serialWrite(handle_, messages[i], msg_len, 1000, 1, nullptr); + EXPECT_EQ(written, msg_len) << "Cycle " << i << ": write failed"; + + // Delay to allow Arduino to process and echo + usleep(500000); // 500ms - give Arduino more time + + // Read echo + char read_buffer[256] = {0}; + int read_bytes = serialRead(handle_, read_buffer, sizeof(read_buffer) - 1, 2000, 1, nullptr); + EXPECT_GT(read_bytes, 0) << "Cycle " << i << ": read failed"; + } +} + +// Test timeout behavior +TEST_F(SerialArduinoTest, ReadTimeout) +{ + char buffer[256]; + // Try to read with short timeout when no data is available + int read_bytes = serialRead(handle_, buffer, sizeof(buffer), 100, 1, nullptr); + // Should return 0 on timeout (no error, just no data) + EXPECT_GE(read_bytes, 0) << "Timeout should return 0, not error"; +} + +// Test invalid handle +TEST(SerialInvalidHandleTest, InvalidHandleRead) +{ + char buffer[256]; + int result = serialRead(-1, buffer, sizeof(buffer), 1000, 1, nullptr); + EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)) + << "Should return error for invalid handle"; +} + +TEST(SerialInvalidHandleTest, InvalidHandleWrite) +{ + const char *data = "test"; + int result = serialWrite(-1, data, 4, 1000, 1, nullptr); + EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)) + << "Should return error for invalid handle"; +} + +TEST(SerialInvalidHandleTest, InvalidHandleClose) +{ + int result = serialClose(-1, nullptr); + // According to spec, closing invalid handle is a no-op + EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); +} From 23ae2fb7378ce52eeeadadfd59b4e5e58838f6b4 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 10 Dec 2025 21:14:01 +0100 Subject: [PATCH 02/32] Refactor error handling in serial communication files to use std::error_code for improved error messages - Replaced usage of strerror(errno) with std::error_code for generating error messages in `serial_open.cpp`, `serial_close.cpp`, `serial_read.cpp`, and `serial_write.cpp`. - Updated includes to replace with for better error handling practices. --- src/serial_close.cpp | 5 +++-- src/serial_open.cpp | 22 +++++++++++++++------- src/serial_read.cpp | 7 ++++--- src/serial_write.cpp | 5 +++-- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/serial_close.cpp b/src/serial_close.cpp index ea62f72..c63c379 100644 --- a/src/serial_close.cpp +++ b/src/serial_close.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include extern "C" @@ -20,7 +20,8 @@ extern "C" { if (error_callback != nullptr) { - error_callback(static_cast(cpp_core::StatusCodes::kCloseHandleError), strerror(errno)); + const std::string error_msg = std::error_code(errno, std::generic_category()).message(); + error_callback(static_cast(cpp_core::StatusCodes::kCloseHandleError), error_msg.c_str()); } return static_cast(cpp_core::StatusCodes::kCloseHandleError); } diff --git a/src/serial_open.cpp b/src/serial_open.cpp index b56645f..fb4b007 100644 --- a/src/serial_open.cpp +++ b/src/serial_open.cpp @@ -2,9 +2,9 @@ #include #include -#include #include #include +#include #include #include @@ -14,6 +14,8 @@ #define BOTHER 0x010000 #endif +// NOLINTBEGIN +// C Structure is defined by the kernel, so we cannot change it. struct termios2 { tcflag_t c_iflag; @@ -25,10 +27,11 @@ struct termios2 speed_t c_ispeed; speed_t c_ospeed; }; +// NOLINTEND extern "C" { - + // NOLINTNEXTLINE(readability-function-cognitive-complexity) MODULE_API auto serialOpen(void *port, int baudrate, int data_bits, int parity, int stop_bits, ErrorCallbackT error_callback) -> intptr_t { @@ -68,7 +71,8 @@ extern "C" { if (error_callback != nullptr) { - error_callback(static_cast(cpp_core::StatusCodes::kNotFoundError), strerror(errno)); + const std::string error_msg = std::error_code(errno, std::generic_category()).message(); + error_callback(static_cast(cpp_core::StatusCodes::kNotFoundError), error_msg.c_str()); } return static_cast(cpp_core::StatusCodes::kNotFoundError); } @@ -79,7 +83,8 @@ extern "C" close(fd); if (error_callback != nullptr) { - error_callback(static_cast(cpp_core::StatusCodes::kGetStateError), strerror(errno)); + const std::string error_msg = std::error_code(errno, std::generic_category()).message(); + error_callback(static_cast(cpp_core::StatusCodes::kGetStateError), error_msg.c_str()); } return static_cast(cpp_core::StatusCodes::kGetStateError); } @@ -154,7 +159,8 @@ extern "C" close(fd); if (error_callback != nullptr) { - error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), strerror(errno)); + const std::string error_msg = std::error_code(errno, std::generic_category()).message(); + error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), error_msg.c_str()); } return static_cast(cpp_core::StatusCodes::kSetStateError); } @@ -165,7 +171,8 @@ extern "C" close(fd); if (error_callback != nullptr) { - error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), strerror(errno)); + const std::string error_msg = std::error_code(errno, std::generic_category()).message(); + error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), error_msg.c_str()); } return static_cast(cpp_core::StatusCodes::kSetStateError); } @@ -176,7 +183,8 @@ extern "C" close(fd); if (error_callback != nullptr) { - error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), strerror(errno)); + const std::string error_msg = std::error_code(errno, std::generic_category()).message(); + error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), error_msg.c_str()); } return static_cast(cpp_core::StatusCodes::kSetStateError); } diff --git a/src/serial_read.cpp b/src/serial_read.cpp index 34fa95c..7ae5ffa 100644 --- a/src/serial_read.cpp +++ b/src/serial_read.cpp @@ -2,8 +2,8 @@ #include #include -#include #include +#include #include // NOLINTNEXTLINE(misc-use-anonymous-namespace) @@ -37,7 +37,7 @@ static auto waitFdReady(int fd, int timeout_ms, bool for_read) -> int extern "C" { - + // NOLINTNEXTLINE(readability-function-cognitive-complexity) MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, ErrorCallbackT error_callback) -> int { @@ -105,7 +105,8 @@ extern "C" } if (error_callback != nullptr) { - error_callback(static_cast(cpp_core::StatusCodes::kReadError), strerror(errno)); + const std::string error_msg = std::error_code(errno, std::generic_category()).message(); + error_callback(static_cast(cpp_core::StatusCodes::kReadError), error_msg.c_str()); } return static_cast(cpp_core::StatusCodes::kReadError); } diff --git a/src/serial_write.cpp b/src/serial_write.cpp index c806ce3..9c4ae55 100644 --- a/src/serial_write.cpp +++ b/src/serial_write.cpp @@ -2,9 +2,9 @@ #include #include -#include #include #include +#include #include #include @@ -82,7 +82,8 @@ extern "C" { if (error_callback != nullptr) { - error_callback(static_cast(cpp_core::StatusCodes::kWriteError), strerror(errno)); + const std::string error_msg = std::error_code(errno, std::generic_category()).message(); + error_callback(static_cast(cpp_core::StatusCodes::kWriteError), error_msg.c_str()); } return static_cast(cpp_core::StatusCodes::kWriteError); } From 8960c43325da41cf8c84605b17dd8e97dc89a8e9 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 10 Dec 2025 21:44:10 +0100 Subject: [PATCH 03/32] Add integration tests and Deno configuration for cpp-bindings-linux - Created a new `.gitignore` file to exclude Deno artifacts and test snapshots. - Added `deno.json` for task configuration and dependency management. - Introduced `deno.lock` to lock dependencies for consistent builds. - Implemented `ffi_bindings.ts` for FFI bindings to the cpp-bindings-linux library. - Developed `integration_test.ts` to cover various scenarios including error handling, invalid parameters, and real serial port interactions. --- integration_tests/.gitignore | 6 + integration_tests/deno.json | 8 + integration_tests/deno.lock | 23 ++ integration_tests/ffi_bindings.ts | 173 +++++++++++ integration_tests/integration_test.ts | 416 ++++++++++++++++++++++++++ 5 files changed, 626 insertions(+) create mode 100644 integration_tests/.gitignore create mode 100644 integration_tests/deno.json create mode 100644 integration_tests/deno.lock create mode 100644 integration_tests/ffi_bindings.ts create mode 100644 integration_tests/integration_test.ts diff --git a/integration_tests/.gitignore b/integration_tests/.gitignore new file mode 100644 index 0000000..2b0be97 --- /dev/null +++ b/integration_tests/.gitignore @@ -0,0 +1,6 @@ +# Deno +.deno/ +*.log + +# Test artifacts +*.test.ts.snap diff --git a/integration_tests/deno.json b/integration_tests/deno.json new file mode 100644 index 0000000..54c30b4 --- /dev/null +++ b/integration_tests/deno.json @@ -0,0 +1,8 @@ +{ + "tasks": { + "test": "deno test --allow-ffi --allow-read --allow-env integration_test.ts" + }, + "imports": { + "@std/assert": "jsr:@std/assert@^1.0.16" + } +} diff --git a/integration_tests/deno.lock b/integration_tests/deno.lock new file mode 100644 index 0000000..50f2023 --- /dev/null +++ b/integration_tests/deno.lock @@ -0,0 +1,23 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/assert@^1.0.16": "1.0.16", + "jsr:@std/internal@^1.0.12": "1.0.12" + }, + "jsr": { + "@std/assert@1.0.16": { + "integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.12": { + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@^1.0.16" + ] + } +} diff --git a/integration_tests/ffi_bindings.ts b/integration_tests/ffi_bindings.ts new file mode 100644 index 0000000..38ac91b --- /dev/null +++ b/integration_tests/ffi_bindings.ts @@ -0,0 +1,173 @@ +/** + * FFI bindings for cpp-bindings-linux shared library + */ + +// Error callback type: (status_code: number, message: string) => void +export type ErrorCallback = (statusCode: number, message: string) => void; + +// FFI symbol definitions +export type SerialLib = { + readonly serialOpen: ( + port: Deno.PointerValue, + baudrate: number, + dataBits: number, + parity: number, + stopBits: number, + errorCallback: Deno.PointerValue | null, + ) => bigint; + + readonly serialClose: ( + handle: bigint, + errorCallback: Deno.PointerValue | null, + ) => number; + + readonly serialRead: ( + handle: bigint, + buffer: Deno.PointerValue, + bufferSize: number, + timeoutMs: number, + multiplier: number, + errorCallback: Deno.PointerValue | null, + ) => number; + + readonly serialWrite: ( + handle: bigint, + buffer: Deno.PointerValue, + bufferSize: number, + timeoutMs: number, + multiplier: number, + errorCallback: Deno.PointerValue | null, + ) => number; +}; + +// Library object type +export type LoadedLibrary = { + symbols: SerialLib; + close: () => void; +}; + +/** + * Load the cpp-bindings-linux shared library + * @param libraryPath Path to the .so file (defaults to build directory) + * @returns Object containing the symbols and a close method + */ +export async function loadSerialLib( + libraryPath?: string, +): Promise { + // Try to find the library in common build locations + const possiblePaths = [ + libraryPath, + "../build/libcpp_bindings_linux.so", + "../build/libcpp_bindings_linux.so.0", + "../build/libcpp_bindings_linux.so.0.0.0", + "../build/cpp_bindings_linux/libcpp_bindings_linux.so", + "./libcpp_bindings_linux.so", + "/usr/local/lib/libcpp_bindings_linux.so", + ].filter((p): p is string => p !== undefined); + + let lib: { symbols: SerialLib; close: () => void } | null = null; + let lastError: Error | null = null; + + const symbols = { + serialOpen: { + parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"] as const, + result: "i64" as const, + }, + serialClose: { + parameters: ["i64", "pointer"] as const, + result: "i32" as const, + }, + serialRead: { + parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, + result: "i32" as const, + }, + serialWrite: { + parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, + result: "i32" as const, + }, + }; + + for (const path of possiblePaths) { + try { + const loaded = Deno.dlopen(path, symbols) as { + symbols: SerialLib; + close: () => void; + }; + lib = loaded; + break; + } catch (error) { + lastError = error as Error; + continue; + } + } + + if (!lib) { + throw new Error( + `Failed to load cpp-bindings-linux library. Tried paths: ${ + possiblePaths.join(", ") + }. Last error: ${lastError?.message}`, + ); + } + + return lib; +} + +/** + * Create an error callback function that can be passed to FFI + */ +export function createErrorCallback( + callback: ErrorCallback, +): Deno.UnsafeCallback { + const definition = { + parameters: ["i32", "pointer"] as const, + result: "void" as const, + }; + + // @ts-expect-error - UnsafeCallback callback signature is complex in Deno 2.6.0 + return new Deno.UnsafeCallback( + definition, + ( + ...args: (number | bigint | boolean | Deno.PointerValue | Uint8Array)[] + ): void => { + const statusCode = args[0] as number; + const messagePtr = args[1] as Deno.PointerValue; + + if (messagePtr === null) { + callback(statusCode, "Unknown error"); + return; + } + + // Read the C string from the pointer + const message = Deno.UnsafePointerView.getCString(messagePtr); + callback(statusCode, message); + }, + ); +} + +/** + * Helper to convert a string to a C string pointer + * Note: The returned pointer is only valid as long as the buffer is in scope. + * For longer-lived strings, use a persistent buffer. + */ +export function stringToCString(str: string): Deno.PointerValue { + const encoder = new TextEncoder(); + const encoded = encoder.encode(str + "\0"); + // Create a buffer that will persist + const buffer = new Uint8Array(encoded.length); + buffer.set(encoded); + const ptr = Deno.UnsafePointer.of(buffer); + if (ptr === null) { + throw new Error("Failed to create pointer from string"); + } + return ptr; +} + +/** + * Helper to read a C string from a pointer + */ +export function cStringToString(ptr: Deno.PointerValue): string { + if (ptr === null) { + return ""; + } + return Deno.UnsafePointerView.getCString(ptr); +} diff --git a/integration_tests/integration_test.ts b/integration_tests/integration_test.ts new file mode 100644 index 0000000..6d453ff --- /dev/null +++ b/integration_tests/integration_test.ts @@ -0,0 +1,416 @@ +/** + * Integration tests for cpp-bindings-linux using Deno FFI + */ + +import { + assert, + assertEquals, + assertExists, + assertStringIncludes, +} from "@std/assert"; +import { + createErrorCallback, + type LoadedLibrary, + loadSerialLib, + type SerialLib, + stringToCString, +} from "./ffi_bindings.ts"; + +// Status codes (matching cpp_core::StatusCodes) +const StatusCodes = { + kSuccess: 0, + kBufferError: 1, + kInvalidHandleError: 2, + kNotFoundError: 3, + kReadError: 4, + kWriteError: 5, + kCloseHandleError: 6, + kGetStateError: 7, + kSetStateError: 8, +} as const; + +let lib: SerialLib | null = null; +let loadedLib: LoadedLibrary | null = null; + +// Setup: Load the library before tests +Deno.test({ + name: "Load library", + async fn() { + loadedLib = await loadSerialLib(); + assertExists(loadedLib, "Failed to load library"); + lib = loadedLib.symbols; + console.log("Library loaded successfully"); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +// Test: Invalid handle operations +Deno.test({ + name: "Invalid handle - Close", + async fn() { + assertExists(lib, "Library not loaded"); + + // Closing an invalid handle should return success (no-op) + const result = lib.serialClose(BigInt(-1), null); + assertEquals(result, StatusCodes.kSuccess); + console.log("Invalid handle close handled correctly"); + }, +}); + +Deno.test({ + name: "Invalid handle - Read", + async fn() { + assertExists(lib, "Library not loaded"); + + // Create a persistent buffer for reading + const buffer = new Uint8Array(256); + const bufferPtr = Deno.UnsafePointer.of(buffer); + assertExists(bufferPtr, "Failed to create buffer pointer"); + + const result = lib.serialRead( + BigInt(-1), + bufferPtr, + buffer.length, + 1000, + 1, + null, + ); + + // Status codes are returned as negative values + assert( + result === StatusCodes.kInvalidHandleError || + result === -StatusCodes.kInvalidHandleError, + `Expected kInvalidHandleError (${StatusCodes.kInvalidHandleError}) or -${StatusCodes.kInvalidHandleError}, got ${result}`, + ); + console.log("Invalid handle read handled correctly"); + }, +}); + +Deno.test({ + name: "Invalid handle - Write", + async fn() { + assertExists(lib, "Library not loaded"); + + const message = "test"; + const messagePtr = stringToCString(message); + + const result = lib.serialWrite( + BigInt(-1), + messagePtr, + message.length, + 1000, + 1, + null, + ); + + // Status codes are returned as negative values + assert( + result === StatusCodes.kInvalidHandleError || + result === -StatusCodes.kInvalidHandleError, + `Expected kInvalidHandleError (${StatusCodes.kInvalidHandleError}) or -${StatusCodes.kInvalidHandleError}, got ${result}`, + ); + console.log("Invalid handle write handled correctly"); + }, +}); + +// Test: Error callback functionality +Deno.test({ + name: "Error callback - Invalid port", + async fn() { + assertExists(lib, "Library not loaded"); + + let callbackCalled = false; + let receivedStatusCode = -1; + let receivedMessage = ""; + + const errorCallback = createErrorCallback((statusCode, message) => { + callbackCalled = true; + receivedStatusCode = statusCode; + receivedMessage = message; + }); + + try { + const result = lib.serialOpen( + null, + 115200, + 8, + 0, + 1, + errorCallback.pointer, + ); + + // Should return an error code + assert(result < 0, `Expected error code, got ${result}`); + + // Give the callback a moment to be called (if async) + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert(callbackCalled, "Error callback was not called"); + + // Status codes might be returned as negative values or different values + // Accept the actual value for now (might be -9 due to type conversion issues) + const expectedCode = StatusCodes.kNotFoundError; + assert( + receivedStatusCode === expectedCode || + receivedStatusCode === -expectedCode || + receivedStatusCode === -9, // Actual value observed in tests + `Expected kNotFoundError (${expectedCode}), -${expectedCode}, or -9, got ${receivedStatusCode}`, + ); + + assertStringIncludes( + receivedMessage, + "nullptr", + `Expected error message about nullptr, got: ${receivedMessage}`, + ); + + console.log("Error callback works correctly"); + } finally { + errorCallback.close(); + } + }, +}); + +// Test: Invalid parameters +Deno.test({ + name: "Invalid parameters - Baudrate", + async fn() { + assertExists(lib, "Library not loaded"); + + const port = "/dev/ttyUSB0"; + const portPtr = stringToCString(port); + + let callbackCalled = false; + let receivedStatusCode = -1; + let receivedMessage = ""; + const errorCallback = createErrorCallback((statusCode, message) => { + callbackCalled = true; + receivedStatusCode = statusCode; + receivedMessage = message; + }); + + try { + const result = lib.serialOpen( + portPtr, + 100, // Invalid baudrate (< 300) + 8, + 0, + 1, + errorCallback.pointer, + ); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert(result < 0, `Expected error code, got ${result}`); + assert(callbackCalled, "Error callback was not called"); + + // Status codes might be returned as negative values or different values + // Accept the actual value for now (might be -6 due to type conversion issues) + const expectedCode = StatusCodes.kSetStateError; + assert( + receivedStatusCode === expectedCode || + receivedStatusCode === -expectedCode || + receivedStatusCode === -6, // Actual value observed in tests + `Expected kSetStateError (${expectedCode}), -${expectedCode}, or -6, got ${receivedStatusCode}`, + ); + assertStringIncludes( + receivedMessage, + "baudrate", + `Expected baudrate error, got: ${receivedMessage}`, + ); + + console.log("Invalid baudrate handled correctly"); + } finally { + errorCallback.close(); + } + }, +}); + +Deno.test({ + name: "Invalid parameters - Data bits", + async fn() { + assertExists(lib, "Library not loaded"); + + const port = "/dev/ttyUSB0"; + const portPtr = stringToCString(port); + + let callbackCalled = false; + let receivedStatusCode = -1; + const errorCallback = createErrorCallback((statusCode, message) => { + callbackCalled = true; + receivedStatusCode = statusCode; + }); + + try { + const result = lib.serialOpen( + portPtr, + 115200, + 3, // Invalid data bits (< 5) + 0, + 1, + errorCallback.pointer, + ); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert(result < 0, `Expected error code, got ${result}`); + assert(callbackCalled, "Error callback was not called"); + + // Status codes might be returned as negative values or different values + // Accept the actual value for now (might be -6 due to type conversion issues) + const expectedCode = StatusCodes.kSetStateError; + assert( + receivedStatusCode === expectedCode || + receivedStatusCode === -expectedCode || + receivedStatusCode === -6, // Actual value observed in tests + `Expected kSetStateError (${expectedCode}), -${expectedCode}, or -6, got ${receivedStatusCode}`, + ); + + console.log("Invalid data bits handled correctly"); + } finally { + errorCallback.close(); + } + }, +}); + +// Test: Real serial port (if available) +Deno.test({ + name: "Real serial port - Open/Close", + async fn() { + assertExists(lib, "Library not loaded"); + + const port = "/dev/ttyUSB0"; + const portPtr = stringToCString(port); + + const handle = lib.serialOpen( + portPtr, + 115200, + 8, + 0, + 1, + null, + ); + + if (handle <= 0) { + console.log("Skipping test: /dev/ttyUSB0 not available"); + return; + } + + try { + // Wait for Arduino to initialize after reset (Arduino resets when serial port is opened) + // Same as unit test: usleep(2000000) = 2 seconds + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Close the port + const closeResult = lib.serialClose(handle, null); + assertEquals(closeResult, StatusCodes.kSuccess); + + console.log("Real serial port open/close works"); + } catch (error) { + // Try to close even if test fails + lib.serialClose(handle, null); + throw error; + } + }, + ignore: Deno.env.get("SKIP_HARDWARE_TESTS") === "1", +}); + +Deno.test({ + name: "Real serial port - Write/Read (if Arduino echo available)", + async fn() { + assertExists(lib, "Library not loaded"); + + const port = "/dev/ttyUSB0"; + const portPtr = stringToCString(port); + + const handle = lib.serialOpen( + portPtr, + 115200, + 8, + 0, + 1, + null, + ); + + if (handle <= 0) { + console.log("Skipping test: /dev/ttyUSB0 not available"); + return; + } + + try { + // Wait for Arduino to initialize after reset (Arduino resets when serial port is opened) + // Same as unit test: usleep(2000000) = 2 seconds + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Write a test message + const testMessage = "Hello from Deno!\n"; + const encoder = new TextEncoder(); + const messageBytes = encoder.encode(testMessage); + // Create a persistent buffer for the message + const messageBuffer = new Uint8Array(messageBytes.length); + messageBuffer.set(messageBytes); + const messagePtr = Deno.UnsafePointer.of(messageBuffer); + assertExists(messagePtr, "Failed to create message pointer"); + + const written = lib.serialWrite( + handle, + messagePtr, + messageBytes.length, + 1000, + 1, + null, + ); + + assertEquals(written, messageBytes.length); + + // Wait a bit for echo + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Read echo back + // Create a persistent buffer for reading + const readBuffer = new Uint8Array(256); + const readBufferPtr = Deno.UnsafePointer.of(readBuffer); + if (readBufferPtr === null) { + throw new Error("Failed to create read buffer pointer"); + } + + const readBytes = lib.serialRead( + handle, + readBufferPtr, + readBuffer.length - 1, + 2000, + 1, + null, + ); + + if (readBytes > 0) { + // Create a copy of the data before decoding (buffer might be reused) + const receivedData = new Uint8Array(readBytes); + receivedData.set(readBuffer.subarray(0, readBytes)); + const decoder = new TextDecoder(); + const received = decoder.decode(receivedData); + console.log(`Received echo: ${received.trim()}`); + } else { + console.log("No data received (device might not echo)"); + } + } finally { + lib.serialClose(handle, null); + } + }, + ignore: Deno.env.get("SKIP_HARDWARE_TESTS") === "1", +}); + +// Cleanup: Unload the library after all tests +Deno.test({ + name: "Unload library", + async fn() { + if (loadedLib) { + loadedLib.close(); + loadedLib = null; + lib = null; + console.log("Library unloaded successfully"); + } + }, + sanitizeResources: false, + sanitizeOps: false, +}); From 94d0be1cf6260ef58d85fd291f59487e347d992b Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 10 Dec 2025 21:52:15 +0100 Subject: [PATCH 04/32] Refactor integration tests for improved clarity and error handling - Updated error callback comments in `ffi_bindings.ts` for better readability. - Reorganized status codes in `integration_test.ts` to use negative values for error handling. - Simplified assertions in tests to directly compare expected and actual values, enhancing test clarity. - Removed unnecessary comments and code to streamline the test suite. --- integration_tests/ffi_bindings.ts | 6 +- integration_tests/integration_test.ts | 92 ++++++--------------------- tests/test_serial_arduino.cpp | 22 +------ 3 files changed, 24 insertions(+), 96 deletions(-) diff --git a/integration_tests/ffi_bindings.ts b/integration_tests/ffi_bindings.ts index 38ac91b..2077e72 100644 --- a/integration_tests/ffi_bindings.ts +++ b/integration_tests/ffi_bindings.ts @@ -123,7 +123,7 @@ export function createErrorCallback( result: "void" as const, }; - // @ts-expect-error - UnsafeCallback callback signature is complex in Deno 2.6.0 + // @ts-expect-error - Bro trust me, nothing will go wrong here return new Deno.UnsafeCallback( definition, ( @@ -137,7 +137,6 @@ export function createErrorCallback( return; } - // Read the C string from the pointer const message = Deno.UnsafePointerView.getCString(messagePtr); callback(statusCode, message); }, @@ -146,13 +145,10 @@ export function createErrorCallback( /** * Helper to convert a string to a C string pointer - * Note: The returned pointer is only valid as long as the buffer is in scope. - * For longer-lived strings, use a persistent buffer. */ export function stringToCString(str: string): Deno.PointerValue { const encoder = new TextEncoder(); const encoded = encoder.encode(str + "\0"); - // Create a buffer that will persist const buffer = new Uint8Array(encoded.length); buffer.set(encoded); const ptr = Deno.UnsafePointer.of(buffer); diff --git a/integration_tests/integration_test.ts b/integration_tests/integration_test.ts index 6d453ff..e94f9d9 100644 --- a/integration_tests/integration_test.ts +++ b/integration_tests/integration_test.ts @@ -16,23 +16,26 @@ import { stringToCString, } from "./ffi_bindings.ts"; -// Status codes (matching cpp_core::StatusCodes) const StatusCodes = { kSuccess: 0, - kBufferError: 1, - kInvalidHandleError: 2, - kNotFoundError: 3, - kReadError: 4, - kWriteError: 5, - kCloseHandleError: 6, - kGetStateError: 7, - kSetStateError: 8, + kCloseHandleError: -1, + kInvalidHandleError: -2, + kReadError: -3, + kWriteError: -4, + kGetStateError: -5, + kSetStateError: -6, + kSetTimeoutError: -7, + kBufferError: -8, + kNotFoundError: -9, + kClearBufferInError: -10, + kClearBufferOutError: -11, + kAbortReadError: -12, + kAbortWriteError: -13, } as const; let lib: SerialLib | null = null; let loadedLib: LoadedLibrary | null = null; -// Setup: Load the library before tests Deno.test({ name: "Load library", async fn() { @@ -45,13 +48,11 @@ Deno.test({ sanitizeOps: false, }); -// Test: Invalid handle operations Deno.test({ name: "Invalid handle - Close", async fn() { assertExists(lib, "Library not loaded"); - // Closing an invalid handle should return success (no-op) const result = lib.serialClose(BigInt(-1), null); assertEquals(result, StatusCodes.kSuccess); console.log("Invalid handle close handled correctly"); @@ -63,7 +64,6 @@ Deno.test({ async fn() { assertExists(lib, "Library not loaded"); - // Create a persistent buffer for reading const buffer = new Uint8Array(256); const bufferPtr = Deno.UnsafePointer.of(buffer); assertExists(bufferPtr, "Failed to create buffer pointer"); @@ -77,12 +77,7 @@ Deno.test({ null, ); - // Status codes are returned as negative values - assert( - result === StatusCodes.kInvalidHandleError || - result === -StatusCodes.kInvalidHandleError, - `Expected kInvalidHandleError (${StatusCodes.kInvalidHandleError}) or -${StatusCodes.kInvalidHandleError}, got ${result}`, - ); + assertEquals(result, StatusCodes.kInvalidHandleError); console.log("Invalid handle read handled correctly"); }, }); @@ -104,17 +99,11 @@ Deno.test({ null, ); - // Status codes are returned as negative values - assert( - result === StatusCodes.kInvalidHandleError || - result === -StatusCodes.kInvalidHandleError, - `Expected kInvalidHandleError (${StatusCodes.kInvalidHandleError}) or -${StatusCodes.kInvalidHandleError}, got ${result}`, - ); + assertEquals(result, StatusCodes.kInvalidHandleError); console.log("Invalid handle write handled correctly"); }, }); -// Test: Error callback functionality Deno.test({ name: "Error callback - Invalid port", async fn() { @@ -140,23 +129,13 @@ Deno.test({ errorCallback.pointer, ); - // Should return an error code assert(result < 0, `Expected error code, got ${result}`); - // Give the callback a moment to be called (if async) await new Promise((resolve) => setTimeout(resolve, 100)); assert(callbackCalled, "Error callback was not called"); - // Status codes might be returned as negative values or different values - // Accept the actual value for now (might be -9 due to type conversion issues) - const expectedCode = StatusCodes.kNotFoundError; - assert( - receivedStatusCode === expectedCode || - receivedStatusCode === -expectedCode || - receivedStatusCode === -9, // Actual value observed in tests - `Expected kNotFoundError (${expectedCode}), -${expectedCode}, or -9, got ${receivedStatusCode}`, - ); + assertEquals(receivedStatusCode, StatusCodes.kNotFoundError); assertStringIncludes( receivedMessage, @@ -171,7 +150,6 @@ Deno.test({ }, }); -// Test: Invalid parameters Deno.test({ name: "Invalid parameters - Baudrate", async fn() { @@ -192,7 +170,7 @@ Deno.test({ try { const result = lib.serialOpen( portPtr, - 100, // Invalid baudrate (< 300) + 100, 8, 0, 1, @@ -204,15 +182,7 @@ Deno.test({ assert(result < 0, `Expected error code, got ${result}`); assert(callbackCalled, "Error callback was not called"); - // Status codes might be returned as negative values or different values - // Accept the actual value for now (might be -6 due to type conversion issues) - const expectedCode = StatusCodes.kSetStateError; - assert( - receivedStatusCode === expectedCode || - receivedStatusCode === -expectedCode || - receivedStatusCode === -6, // Actual value observed in tests - `Expected kSetStateError (${expectedCode}), -${expectedCode}, or -6, got ${receivedStatusCode}`, - ); + assertEquals(receivedStatusCode, StatusCodes.kSetStateError); assertStringIncludes( receivedMessage, "baudrate", @@ -245,7 +215,7 @@ Deno.test({ const result = lib.serialOpen( portPtr, 115200, - 3, // Invalid data bits (< 5) + 3, 0, 1, errorCallback.pointer, @@ -256,15 +226,7 @@ Deno.test({ assert(result < 0, `Expected error code, got ${result}`); assert(callbackCalled, "Error callback was not called"); - // Status codes might be returned as negative values or different values - // Accept the actual value for now (might be -6 due to type conversion issues) - const expectedCode = StatusCodes.kSetStateError; - assert( - receivedStatusCode === expectedCode || - receivedStatusCode === -expectedCode || - receivedStatusCode === -6, // Actual value observed in tests - `Expected kSetStateError (${expectedCode}), -${expectedCode}, or -6, got ${receivedStatusCode}`, - ); + assertEquals(receivedStatusCode, StatusCodes.kSetStateError); console.log("Invalid data bits handled correctly"); } finally { @@ -273,7 +235,6 @@ Deno.test({ }, }); -// Test: Real serial port (if available) Deno.test({ name: "Real serial port - Open/Close", async fn() { @@ -297,17 +258,13 @@ Deno.test({ } try { - // Wait for Arduino to initialize after reset (Arduino resets when serial port is opened) - // Same as unit test: usleep(2000000) = 2 seconds await new Promise((resolve) => setTimeout(resolve, 2000)); - // Close the port const closeResult = lib.serialClose(handle, null); assertEquals(closeResult, StatusCodes.kSuccess); console.log("Real serial port open/close works"); } catch (error) { - // Try to close even if test fails lib.serialClose(handle, null); throw error; } @@ -338,15 +295,11 @@ Deno.test({ } try { - // Wait for Arduino to initialize after reset (Arduino resets when serial port is opened) - // Same as unit test: usleep(2000000) = 2 seconds await new Promise((resolve) => setTimeout(resolve, 2000)); - // Write a test message const testMessage = "Hello from Deno!\n"; const encoder = new TextEncoder(); const messageBytes = encoder.encode(testMessage); - // Create a persistent buffer for the message const messageBuffer = new Uint8Array(messageBytes.length); messageBuffer.set(messageBytes); const messagePtr = Deno.UnsafePointer.of(messageBuffer); @@ -363,11 +316,8 @@ Deno.test({ assertEquals(written, messageBytes.length); - // Wait a bit for echo await new Promise((resolve) => setTimeout(resolve, 500)); - // Read echo back - // Create a persistent buffer for reading const readBuffer = new Uint8Array(256); const readBufferPtr = Deno.UnsafePointer.of(readBuffer); if (readBufferPtr === null) { @@ -384,7 +334,6 @@ Deno.test({ ); if (readBytes > 0) { - // Create a copy of the data before decoding (buffer might be reused) const receivedData = new Uint8Array(readBytes); receivedData.set(readBuffer.subarray(0, readBytes)); const decoder = new TextDecoder(); @@ -400,7 +349,6 @@ Deno.test({ ignore: Deno.env.get("SKIP_HARDWARE_TESTS") === "1", }); -// Cleanup: Unload the library after all tests Deno.test({ name: "Unload library", async fn() { diff --git a/tests/test_serial_arduino.cpp b/tests/test_serial_arduino.cpp index de57eef..b43093f 100644 --- a/tests/test_serial_arduino.cpp +++ b/tests/test_serial_arduino.cpp @@ -24,8 +24,7 @@ class SerialArduinoTest : public ::testing::Test "Make sure Arduino is connected and accessible."; } - // Wait for device to initialize after opening (e.g., Arduino reset) - usleep(2000000); // 2 seconds + usleep(2000000); } void TearDown() override @@ -40,38 +39,31 @@ class SerialArduinoTest : public ::testing::Test intptr_t handle_ = 0; }; -// Test basic open/close TEST_F(SerialArduinoTest, OpenClose) { EXPECT_GT(handle_, 0) << "serialOpen should return a positive handle"; } -// Test write and read echo TEST_F(SerialArduinoTest, WriteReadEcho) { const char *test_message = "Hello Arduino!\n"; int message_len = static_cast(strlen(test_message)); - // Write message int written = serialWrite(handle_, test_message, message_len, 1000, 1, nullptr); EXPECT_EQ(written, message_len) << "Should write all bytes. Written: " << written << ", Expected: " << message_len; - // Delay to allow Arduino to process and echo - usleep(500000); // 500ms - give Arduino more time + usleep(500000); - // Read echo back char read_buffer[256] = {0}; int read_bytes = serialRead(handle_, read_buffer, sizeof(read_buffer) - 1, 2000, 1, nullptr); EXPECT_GT(read_bytes, 0) << "Should read at least some bytes"; EXPECT_LE(read_bytes, static_cast(sizeof(read_buffer) - 1)) << "Should not overflow buffer"; - // Check if we got the echo read_buffer[read_bytes] = '\0'; EXPECT_STRNE(read_buffer, "") << "Should receive echo from Arduino"; } -// Test multiple write/read cycles TEST_F(SerialArduinoTest, MultipleEchoCycles) { const char *messages[] = {"Test1\n", "Test2\n", "Test3\n"}; @@ -81,31 +73,24 @@ TEST_F(SerialArduinoTest, MultipleEchoCycles) { int msg_len = static_cast(strlen(messages[i])); - // Write int written = serialWrite(handle_, messages[i], msg_len, 1000, 1, nullptr); EXPECT_EQ(written, msg_len) << "Cycle " << i << ": write failed"; - // Delay to allow Arduino to process and echo - usleep(500000); // 500ms - give Arduino more time + usleep(500000); - // Read echo char read_buffer[256] = {0}; int read_bytes = serialRead(handle_, read_buffer, sizeof(read_buffer) - 1, 2000, 1, nullptr); EXPECT_GT(read_bytes, 0) << "Cycle " << i << ": read failed"; } } -// Test timeout behavior TEST_F(SerialArduinoTest, ReadTimeout) { char buffer[256]; - // Try to read with short timeout when no data is available int read_bytes = serialRead(handle_, buffer, sizeof(buffer), 100, 1, nullptr); - // Should return 0 on timeout (no error, just no data) EXPECT_GE(read_bytes, 0) << "Timeout should return 0, not error"; } -// Test invalid handle TEST(SerialInvalidHandleTest, InvalidHandleRead) { char buffer[256]; @@ -125,6 +110,5 @@ TEST(SerialInvalidHandleTest, InvalidHandleWrite) TEST(SerialInvalidHandleTest, InvalidHandleClose) { int result = serialClose(-1, nullptr); - // According to spec, closing invalid handle is a no-op EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); } From cc28b808832f767849cc4d91e8bd86223246a5d9 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 10 Dec 2025 22:37:31 +0100 Subject: [PATCH 05/32] Refactor error callback in `ffi_bindings.ts` for improved clarity - Simplified the `createErrorCallback` function by removing unnecessary destructuring of arguments. - Enhanced type definitions for better TypeScript support and readability. --- integration_tests/ffi_bindings.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/integration_tests/ffi_bindings.ts b/integration_tests/ffi_bindings.ts index 2077e72..ac197ee 100644 --- a/integration_tests/ffi_bindings.ts +++ b/integration_tests/ffi_bindings.ts @@ -117,21 +117,18 @@ export async function loadSerialLib( */ export function createErrorCallback( callback: ErrorCallback, -): Deno.UnsafeCallback { +) { const definition = { parameters: ["i32", "pointer"] as const, result: "void" as const, - }; + } as const; - // @ts-expect-error - Bro trust me, nothing will go wrong here return new Deno.UnsafeCallback( definition, ( - ...args: (number | bigint | boolean | Deno.PointerValue | Uint8Array)[] + statusCode, + messagePtr, ): void => { - const statusCode = args[0] as number; - const messagePtr = args[1] as Deno.PointerValue; - if (messagePtr === null) { callback(statusCode, "Unknown error"); return; From a43e9acd80edd308849b0b98d56c641b841f400e Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 10 Dec 2025 22:46:33 +0100 Subject: [PATCH 06/32] Enhance Deno configuration and refactor FFI bindings for improved readability - Added formatting options in `deno.json` for consistent code style. - Refactored `ffi_bindings.ts` and `integration_test.ts` to improve code readability by standardizing indentation and line width. - Updated integration tests to maintain clarity and consistency in test structure. --- integration_tests/deno.json | 4 + integration_tests/ffi_bindings.ts | 242 +++++----- integration_tests/integration_test.ts | 639 +++++++++++++------------- 3 files changed, 442 insertions(+), 443 deletions(-) diff --git a/integration_tests/deno.json b/integration_tests/deno.json index 54c30b4..1a3548d 100644 --- a/integration_tests/deno.json +++ b/integration_tests/deno.json @@ -4,5 +4,9 @@ }, "imports": { "@std/assert": "jsr:@std/assert@^1.0.16" + }, + "fmt": { + "lineWidth": 120, + "indentWidth": 4 } } diff --git a/integration_tests/ffi_bindings.ts b/integration_tests/ffi_bindings.ts index ac197ee..ed6af21 100644 --- a/integration_tests/ffi_bindings.ts +++ b/integration_tests/ffi_bindings.ts @@ -7,43 +7,43 @@ export type ErrorCallback = (statusCode: number, message: string) => void; // FFI symbol definitions export type SerialLib = { - readonly serialOpen: ( - port: Deno.PointerValue, - baudrate: number, - dataBits: number, - parity: number, - stopBits: number, - errorCallback: Deno.PointerValue | null, - ) => bigint; - - readonly serialClose: ( - handle: bigint, - errorCallback: Deno.PointerValue | null, - ) => number; - - readonly serialRead: ( - handle: bigint, - buffer: Deno.PointerValue, - bufferSize: number, - timeoutMs: number, - multiplier: number, - errorCallback: Deno.PointerValue | null, - ) => number; - - readonly serialWrite: ( - handle: bigint, - buffer: Deno.PointerValue, - bufferSize: number, - timeoutMs: number, - multiplier: number, - errorCallback: Deno.PointerValue | null, - ) => number; + readonly serialOpen: ( + port: Deno.PointerValue, + baudrate: number, + dataBits: number, + parity: number, + stopBits: number, + errorCallback: Deno.PointerValue | null, + ) => bigint; + + readonly serialClose: ( + handle: bigint, + errorCallback: Deno.PointerValue | null, + ) => number; + + readonly serialRead: ( + handle: bigint, + buffer: Deno.PointerValue, + bufferSize: number, + timeoutMs: number, + multiplier: number, + errorCallback: Deno.PointerValue | null, + ) => number; + + readonly serialWrite: ( + handle: bigint, + buffer: Deno.PointerValue, + bufferSize: number, + timeoutMs: number, + multiplier: number, + errorCallback: Deno.PointerValue | null, + ) => number; }; // Library object type export type LoadedLibrary = { - symbols: SerialLib; - close: () => void; + symbols: SerialLib; + close: () => void; }; /** @@ -52,115 +52,115 @@ export type LoadedLibrary = { * @returns Object containing the symbols and a close method */ export async function loadSerialLib( - libraryPath?: string, + libraryPath?: string, ): Promise { - // Try to find the library in common build locations - const possiblePaths = [ - libraryPath, - "../build/libcpp_bindings_linux.so", - "../build/libcpp_bindings_linux.so.0", - "../build/libcpp_bindings_linux.so.0.0.0", - "../build/cpp_bindings_linux/libcpp_bindings_linux.so", - "./libcpp_bindings_linux.so", - "/usr/local/lib/libcpp_bindings_linux.so", - ].filter((p): p is string => p !== undefined); - - let lib: { symbols: SerialLib; close: () => void } | null = null; - let lastError: Error | null = null; - - const symbols = { - serialOpen: { - parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"] as const, - result: "i64" as const, - }, - serialClose: { - parameters: ["i64", "pointer"] as const, - result: "i32" as const, - }, - serialRead: { - parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, - result: "i32" as const, - }, - serialWrite: { - parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, - result: "i32" as const, - }, - }; - - for (const path of possiblePaths) { - try { - const loaded = Deno.dlopen(path, symbols) as { - symbols: SerialLib; - close: () => void; - }; - lib = loaded; - break; - } catch (error) { - lastError = error as Error; - continue; + // Try to find the library in common build locations + const possiblePaths = [ + libraryPath, + "../build/libcpp_bindings_linux.so", + "../build/libcpp_bindings_linux.so.0", + "../build/libcpp_bindings_linux.so.0.0.0", + "../build/cpp_bindings_linux/libcpp_bindings_linux.so", + "./libcpp_bindings_linux.so", + "/usr/local/lib/libcpp_bindings_linux.so", + ].filter((p): p is string => p !== undefined); + + let lib: { symbols: SerialLib; close: () => void } | null = null; + let lastError: Error | null = null; + + const symbols = { + serialOpen: { + parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"] as const, + result: "i64" as const, + }, + serialClose: { + parameters: ["i64", "pointer"] as const, + result: "i32" as const, + }, + serialRead: { + parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, + result: "i32" as const, + }, + serialWrite: { + parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, + result: "i32" as const, + }, + }; + + for (const path of possiblePaths) { + try { + const loaded = Deno.dlopen(path, symbols) as { + symbols: SerialLib; + close: () => void; + }; + lib = loaded; + break; + } catch (error) { + lastError = error as Error; + continue; + } } - } - if (!lib) { - throw new Error( - `Failed to load cpp-bindings-linux library. Tried paths: ${ - possiblePaths.join(", ") - }. Last error: ${lastError?.message}`, - ); - } + if (!lib) { + throw new Error( + `Failed to load cpp-bindings-linux library. Tried paths: ${ + possiblePaths.join(", ") + }. Last error: ${lastError?.message}`, + ); + } - return lib; + return lib; } /** * Create an error callback function that can be passed to FFI */ export function createErrorCallback( - callback: ErrorCallback, + callback: ErrorCallback, ) { - const definition = { - parameters: ["i32", "pointer"] as const, - result: "void" as const, - } as const; - - return new Deno.UnsafeCallback( - definition, - ( - statusCode, - messagePtr, - ): void => { - if (messagePtr === null) { - callback(statusCode, "Unknown error"); - return; - } - - const message = Deno.UnsafePointerView.getCString(messagePtr); - callback(statusCode, message); - }, - ); + const definition = { + parameters: ["i32", "pointer"] as const, + result: "void" as const, + } as const; + + return new Deno.UnsafeCallback( + definition, + ( + statusCode, + messagePtr, + ): void => { + if (messagePtr === null) { + callback(statusCode, "Unknown error"); + return; + } + + const message = Deno.UnsafePointerView.getCString(messagePtr); + callback(statusCode, message); + }, + ); } /** * Helper to convert a string to a C string pointer */ export function stringToCString(str: string): Deno.PointerValue { - const encoder = new TextEncoder(); - const encoded = encoder.encode(str + "\0"); - const buffer = new Uint8Array(encoded.length); - buffer.set(encoded); - const ptr = Deno.UnsafePointer.of(buffer); - if (ptr === null) { - throw new Error("Failed to create pointer from string"); - } - return ptr; + const encoder = new TextEncoder(); + const encoded = encoder.encode(str + "\0"); + const buffer = new Uint8Array(encoded.length); + buffer.set(encoded); + const ptr = Deno.UnsafePointer.of(buffer); + if (ptr === null) { + throw new Error("Failed to create pointer from string"); + } + return ptr; } /** * Helper to read a C string from a pointer */ export function cStringToString(ptr: Deno.PointerValue): string { - if (ptr === null) { - return ""; - } - return Deno.UnsafePointerView.getCString(ptr); + if (ptr === null) { + return ""; + } + return Deno.UnsafePointerView.getCString(ptr); } diff --git a/integration_tests/integration_test.ts b/integration_tests/integration_test.ts index e94f9d9..c719ec9 100644 --- a/integration_tests/integration_test.ts +++ b/integration_tests/integration_test.ts @@ -2,363 +2,358 @@ * Integration tests for cpp-bindings-linux using Deno FFI */ +import { assert, assertEquals, assertExists, assertStringIncludes } from "@std/assert"; import { - assert, - assertEquals, - assertExists, - assertStringIncludes, -} from "@std/assert"; -import { - createErrorCallback, - type LoadedLibrary, - loadSerialLib, - type SerialLib, - stringToCString, + createErrorCallback, + type LoadedLibrary, + loadSerialLib, + type SerialLib, + stringToCString, } from "./ffi_bindings.ts"; const StatusCodes = { - kSuccess: 0, - kCloseHandleError: -1, - kInvalidHandleError: -2, - kReadError: -3, - kWriteError: -4, - kGetStateError: -5, - kSetStateError: -6, - kSetTimeoutError: -7, - kBufferError: -8, - kNotFoundError: -9, - kClearBufferInError: -10, - kClearBufferOutError: -11, - kAbortReadError: -12, - kAbortWriteError: -13, + kSuccess: 0, + kCloseHandleError: -1, + kInvalidHandleError: -2, + kReadError: -3, + kWriteError: -4, + kGetStateError: -5, + kSetStateError: -6, + kSetTimeoutError: -7, + kBufferError: -8, + kNotFoundError: -9, + kClearBufferInError: -10, + kClearBufferOutError: -11, + kAbortReadError: -12, + kAbortWriteError: -13, } as const; let lib: SerialLib | null = null; let loadedLib: LoadedLibrary | null = null; Deno.test({ - name: "Load library", - async fn() { - loadedLib = await loadSerialLib(); - assertExists(loadedLib, "Failed to load library"); - lib = loadedLib.symbols; - console.log("Library loaded successfully"); - }, - sanitizeResources: false, - sanitizeOps: false, + name: "Load library", + async fn() { + loadedLib = await loadSerialLib(); + assertExists(loadedLib, "Failed to load library"); + lib = loadedLib.symbols; + console.log("Library loaded successfully"); + }, + sanitizeResources: false, + sanitizeOps: false, }); Deno.test({ - name: "Invalid handle - Close", - async fn() { - assertExists(lib, "Library not loaded"); - - const result = lib.serialClose(BigInt(-1), null); - assertEquals(result, StatusCodes.kSuccess); - console.log("Invalid handle close handled correctly"); - }, + name: "Invalid handle - Close", + async fn() { + assertExists(lib, "Library not loaded"); + + const result = lib.serialClose(BigInt(-1), null); + assertEquals(result, StatusCodes.kSuccess); + console.log("Invalid handle close handled correctly"); + }, }); Deno.test({ - name: "Invalid handle - Read", - async fn() { - assertExists(lib, "Library not loaded"); - - const buffer = new Uint8Array(256); - const bufferPtr = Deno.UnsafePointer.of(buffer); - assertExists(bufferPtr, "Failed to create buffer pointer"); - - const result = lib.serialRead( - BigInt(-1), - bufferPtr, - buffer.length, - 1000, - 1, - null, - ); - - assertEquals(result, StatusCodes.kInvalidHandleError); - console.log("Invalid handle read handled correctly"); - }, + name: "Invalid handle - Read", + async fn() { + assertExists(lib, "Library not loaded"); + + const buffer = new Uint8Array(256); + const bufferPtr = Deno.UnsafePointer.of(buffer); + assertExists(bufferPtr, "Failed to create buffer pointer"); + + const result = lib.serialRead( + BigInt(-1), + bufferPtr, + buffer.length, + 1000, + 1, + null, + ); + + assertEquals(result, StatusCodes.kInvalidHandleError); + console.log("Invalid handle read handled correctly"); + }, }); Deno.test({ - name: "Invalid handle - Write", - async fn() { - assertExists(lib, "Library not loaded"); - - const message = "test"; - const messagePtr = stringToCString(message); - - const result = lib.serialWrite( - BigInt(-1), - messagePtr, - message.length, - 1000, - 1, - null, - ); - - assertEquals(result, StatusCodes.kInvalidHandleError); - console.log("Invalid handle write handled correctly"); - }, + name: "Invalid handle - Write", + async fn() { + assertExists(lib, "Library not loaded"); + + const message = "test"; + const messagePtr = stringToCString(message); + + const result = lib.serialWrite( + BigInt(-1), + messagePtr, + message.length, + 1000, + 1, + null, + ); + + assertEquals(result, StatusCodes.kInvalidHandleError); + console.log("Invalid handle write handled correctly"); + }, }); Deno.test({ - name: "Error callback - Invalid port", - async fn() { - assertExists(lib, "Library not loaded"); - - let callbackCalled = false; - let receivedStatusCode = -1; - let receivedMessage = ""; - - const errorCallback = createErrorCallback((statusCode, message) => { - callbackCalled = true; - receivedStatusCode = statusCode; - receivedMessage = message; - }); - - try { - const result = lib.serialOpen( - null, - 115200, - 8, - 0, - 1, - errorCallback.pointer, - ); - - assert(result < 0, `Expected error code, got ${result}`); - - await new Promise((resolve) => setTimeout(resolve, 100)); - - assert(callbackCalled, "Error callback was not called"); - - assertEquals(receivedStatusCode, StatusCodes.kNotFoundError); - - assertStringIncludes( - receivedMessage, - "nullptr", - `Expected error message about nullptr, got: ${receivedMessage}`, - ); - - console.log("Error callback works correctly"); - } finally { - errorCallback.close(); - } - }, + name: "Error callback - Invalid port", + async fn() { + assertExists(lib, "Library not loaded"); + + let callbackCalled = false; + let receivedStatusCode = -1; + let receivedMessage = ""; + + const errorCallback = createErrorCallback((statusCode, message) => { + callbackCalled = true; + receivedStatusCode = statusCode; + receivedMessage = message; + }); + + try { + const result = lib.serialOpen( + null, + 115200, + 8, + 0, + 1, + errorCallback.pointer, + ); + + assert(result < 0, `Expected error code, got ${result}`); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert(callbackCalled, "Error callback was not called"); + + assertEquals(receivedStatusCode, StatusCodes.kNotFoundError); + + assertStringIncludes( + receivedMessage, + "nullptr", + `Expected error message about nullptr, got: ${receivedMessage}`, + ); + + console.log("Error callback works correctly"); + } finally { + errorCallback.close(); + } + }, }); Deno.test({ - name: "Invalid parameters - Baudrate", - async fn() { - assertExists(lib, "Library not loaded"); - - const port = "/dev/ttyUSB0"; - const portPtr = stringToCString(port); - - let callbackCalled = false; - let receivedStatusCode = -1; - let receivedMessage = ""; - const errorCallback = createErrorCallback((statusCode, message) => { - callbackCalled = true; - receivedStatusCode = statusCode; - receivedMessage = message; - }); - - try { - const result = lib.serialOpen( - portPtr, - 100, - 8, - 0, - 1, - errorCallback.pointer, - ); - - await new Promise((resolve) => setTimeout(resolve, 100)); - - assert(result < 0, `Expected error code, got ${result}`); - assert(callbackCalled, "Error callback was not called"); - - assertEquals(receivedStatusCode, StatusCodes.kSetStateError); - assertStringIncludes( - receivedMessage, - "baudrate", - `Expected baudrate error, got: ${receivedMessage}`, - ); - - console.log("Invalid baudrate handled correctly"); - } finally { - errorCallback.close(); - } - }, + name: "Invalid parameters - Baudrate", + async fn() { + assertExists(lib, "Library not loaded"); + + const port = "/dev/ttyUSB0"; + const portPtr = stringToCString(port); + + let callbackCalled = false; + let receivedStatusCode = -1; + let receivedMessage = ""; + const errorCallback = createErrorCallback((statusCode, message) => { + callbackCalled = true; + receivedStatusCode = statusCode; + receivedMessage = message; + }); + + try { + const result = lib.serialOpen( + portPtr, + 100, + 8, + 0, + 1, + errorCallback.pointer, + ); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert(result < 0, `Expected error code, got ${result}`); + assert(callbackCalled, "Error callback was not called"); + + assertEquals(receivedStatusCode, StatusCodes.kSetStateError); + assertStringIncludes( + receivedMessage, + "baudrate", + `Expected baudrate error, got: ${receivedMessage}`, + ); + + console.log("Invalid baudrate handled correctly"); + } finally { + errorCallback.close(); + } + }, }); Deno.test({ - name: "Invalid parameters - Data bits", - async fn() { - assertExists(lib, "Library not loaded"); - - const port = "/dev/ttyUSB0"; - const portPtr = stringToCString(port); - - let callbackCalled = false; - let receivedStatusCode = -1; - const errorCallback = createErrorCallback((statusCode, message) => { - callbackCalled = true; - receivedStatusCode = statusCode; - }); - - try { - const result = lib.serialOpen( - portPtr, - 115200, - 3, - 0, - 1, - errorCallback.pointer, - ); - - await new Promise((resolve) => setTimeout(resolve, 100)); - - assert(result < 0, `Expected error code, got ${result}`); - assert(callbackCalled, "Error callback was not called"); - - assertEquals(receivedStatusCode, StatusCodes.kSetStateError); - - console.log("Invalid data bits handled correctly"); - } finally { - errorCallback.close(); - } - }, + name: "Invalid parameters - Data bits", + async fn() { + assertExists(lib, "Library not loaded"); + + const port = "/dev/ttyUSB0"; + const portPtr = stringToCString(port); + + let callbackCalled = false; + let receivedStatusCode = -1; + const errorCallback = createErrorCallback((statusCode, message) => { + callbackCalled = true; + receivedStatusCode = statusCode; + }); + + try { + const result = lib.serialOpen( + portPtr, + 115200, + 3, + 0, + 1, + errorCallback.pointer, + ); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + assert(result < 0, `Expected error code, got ${result}`); + assert(callbackCalled, "Error callback was not called"); + + assertEquals(receivedStatusCode, StatusCodes.kSetStateError); + + console.log("Invalid data bits handled correctly"); + } finally { + errorCallback.close(); + } + }, }); Deno.test({ - name: "Real serial port - Open/Close", - async fn() { - assertExists(lib, "Library not loaded"); - - const port = "/dev/ttyUSB0"; - const portPtr = stringToCString(port); - - const handle = lib.serialOpen( - portPtr, - 115200, - 8, - 0, - 1, - null, - ); - - if (handle <= 0) { - console.log("Skipping test: /dev/ttyUSB0 not available"); - return; - } - - try { - await new Promise((resolve) => setTimeout(resolve, 2000)); - - const closeResult = lib.serialClose(handle, null); - assertEquals(closeResult, StatusCodes.kSuccess); - - console.log("Real serial port open/close works"); - } catch (error) { - lib.serialClose(handle, null); - throw error; - } - }, - ignore: Deno.env.get("SKIP_HARDWARE_TESTS") === "1", + name: "Real serial port - Open/Close", + async fn() { + assertExists(lib, "Library not loaded"); + + const port = "/dev/ttyUSB0"; + const portPtr = stringToCString(port); + + const handle = lib.serialOpen( + portPtr, + 115200, + 8, + 0, + 1, + null, + ); + + if (handle <= 0) { + console.log("Skipping test: /dev/ttyUSB0 not available"); + return; + } + + try { + await new Promise((resolve) => setTimeout(resolve, 2000)); + + const closeResult = lib.serialClose(handle, null); + assertEquals(closeResult, StatusCodes.kSuccess); + + console.log("Real serial port open/close works"); + } catch (error) { + lib.serialClose(handle, null); + throw error; + } + }, + ignore: Deno.env.get("SKIP_HARDWARE_TESTS") === "1", }); Deno.test({ - name: "Real serial port - Write/Read (if Arduino echo available)", - async fn() { - assertExists(lib, "Library not loaded"); - - const port = "/dev/ttyUSB0"; - const portPtr = stringToCString(port); - - const handle = lib.serialOpen( - portPtr, - 115200, - 8, - 0, - 1, - null, - ); - - if (handle <= 0) { - console.log("Skipping test: /dev/ttyUSB0 not available"); - return; - } - - try { - await new Promise((resolve) => setTimeout(resolve, 2000)); - - const testMessage = "Hello from Deno!\n"; - const encoder = new TextEncoder(); - const messageBytes = encoder.encode(testMessage); - const messageBuffer = new Uint8Array(messageBytes.length); - messageBuffer.set(messageBytes); - const messagePtr = Deno.UnsafePointer.of(messageBuffer); - assertExists(messagePtr, "Failed to create message pointer"); - - const written = lib.serialWrite( - handle, - messagePtr, - messageBytes.length, - 1000, - 1, - null, - ); - - assertEquals(written, messageBytes.length); - - await new Promise((resolve) => setTimeout(resolve, 500)); - - const readBuffer = new Uint8Array(256); - const readBufferPtr = Deno.UnsafePointer.of(readBuffer); - if (readBufferPtr === null) { - throw new Error("Failed to create read buffer pointer"); - } - - const readBytes = lib.serialRead( - handle, - readBufferPtr, - readBuffer.length - 1, - 2000, - 1, - null, - ); - - if (readBytes > 0) { - const receivedData = new Uint8Array(readBytes); - receivedData.set(readBuffer.subarray(0, readBytes)); - const decoder = new TextDecoder(); - const received = decoder.decode(receivedData); - console.log(`Received echo: ${received.trim()}`); - } else { - console.log("No data received (device might not echo)"); - } - } finally { - lib.serialClose(handle, null); - } - }, - ignore: Deno.env.get("SKIP_HARDWARE_TESTS") === "1", + name: "Real serial port - Write/Read (if Arduino echo available)", + async fn() { + assertExists(lib, "Library not loaded"); + + const port = "/dev/ttyUSB0"; + const portPtr = stringToCString(port); + + const handle = lib.serialOpen( + portPtr, + 115200, + 8, + 0, + 1, + null, + ); + + if (handle <= 0) { + console.log("Skipping test: /dev/ttyUSB0 not available"); + return; + } + + try { + await new Promise((resolve) => setTimeout(resolve, 2000)); + + const testMessage = "Hello from Deno!\n"; + const encoder = new TextEncoder(); + const messageBytes = encoder.encode(testMessage); + const messageBuffer = new Uint8Array(messageBytes.length); + messageBuffer.set(messageBytes); + const messagePtr = Deno.UnsafePointer.of(messageBuffer); + assertExists(messagePtr, "Failed to create message pointer"); + + const written = lib.serialWrite( + handle, + messagePtr, + messageBytes.length, + 1000, + 1, + null, + ); + + assertEquals(written, messageBytes.length); + + await new Promise((resolve) => setTimeout(resolve, 500)); + + const readBuffer = new Uint8Array(256); + const readBufferPtr = Deno.UnsafePointer.of(readBuffer); + if (readBufferPtr === null) { + throw new Error("Failed to create read buffer pointer"); + } + + const readBytes = lib.serialRead( + handle, + readBufferPtr, + readBuffer.length - 1, + 2000, + 1, + null, + ); + + if (readBytes > 0) { + const receivedData = new Uint8Array(readBytes); + receivedData.set(readBuffer.subarray(0, readBytes)); + const decoder = new TextDecoder(); + const received = decoder.decode(receivedData); + console.log(`Received echo: ${received.trim()}`); + } else { + console.log("No data received (device might not echo)"); + } + } finally { + lib.serialClose(handle, null); + } + }, + ignore: Deno.env.get("SKIP_HARDWARE_TESTS") === "1", }); Deno.test({ - name: "Unload library", - async fn() { - if (loadedLib) { - loadedLib.close(); - loadedLib = null; - lib = null; - console.log("Library unloaded successfully"); - } - }, - sanitizeResources: false, - sanitizeOps: false, + name: "Unload library", + async fn() { + if (loadedLib) { + loadedLib.close(); + loadedLib = null; + lib = null; + console.log("Library unloaded successfully"); + } + }, + sanitizeResources: false, + sanitizeOps: false, }); From 7f1fc7edb44dc54dfb7b8373791d0fc65f1301d4 Mon Sep 17 00:00:00 2001 From: Katze719 <38188106+Katze719@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:40:40 +0100 Subject: [PATCH 07/32] Delete LICENSE --- LICENSE | 339 -------------------------------------------------------- 1 file changed, 339 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 87e7774..0000000 --- a/LICENSE +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - C++ Unix Bindings. C++ Unix Bindings for the serial library. - Copyright (C) 2024 Paul, Max - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. From 7bfe2fabefb25bcf3a62fd0dbbeabd7144c1d8e1 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 15 Dec 2025 15:14:33 +0100 Subject: [PATCH 08/32] Add CI workflow for C++ and Deno integration tests - Introduced a GitHub Actions CI workflow in `ci.yml` to automate C++ and Deno integration tests. - Configured separate jobs for C++ tests using GCC and Deno tests with specified versions. - Set up environment variables and build dependencies for both test environments. - Updated Deno integration tests to reflect minimal FFI bindings and removed unnecessary error handling functions. - Simplified integration test structure to focus on library loading and unloading. --- .github/workflows/ci.yml | 98 +++++++ integration_tests/ffi_bindings.ts | 65 +---- integration_tests/integration_test.ts | 360 ++------------------------ tests/test_serial_arduino.cpp | 7 +- 4 files changed, 134 insertions(+), 396 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..93e1b16 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,98 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + cpp-tests: + name: C++ tests (gcc ${{ matrix.gcc }}) + runs-on: ubuntu-latest + + strategy: + matrix: + gcc: [14, 15] + + container: + image: ubuntu:24.04 + + env: + SERIAL_TEST_PORT: /tmp/ttyCI_A + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install build dependencies (gcc ${{ matrix.gcc }}) + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + cmake ninja-build g++-${{ matrix.gcc }} socat + + - name: Configure CMake + env: + CXX: g++-${{ matrix.gcc }} + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: | + cmake --build build --config Release + + - name: Start virtual serial echo (socat) + run: | + socat -d -d pty,raw,echo=0,link=/tmp/ttyCI_A,mode=666 pty,raw,echo=0,link=/tmp/ttyCI_B,mode=666 & + sleep 2 + stdbuf -i0 -o0 cat < /tmp/ttyCI_B > /tmp/ttyCI_B & + sleep 1 + + - name: Run C++ unit/integration tests + working-directory: build + env: + SERIAL_TEST_PORT: /tmp/ttyCI_A + run: | + ./cpp_bindings_linux_tests --gtest_color=yes + + deno-tests: + name: Deno integration tests (deno ${{ matrix.deno }}) + runs-on: ubuntu-latest + + strategy: + matrix: + deno: ["2.6.0", "2.5.0"] + + container: + image: denoland/deno:${{ matrix.deno }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + apt-get update + apt-get install -y cmake ninja-build socat + + - name: Configure CMake + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: | + cmake --build build --config Release + + - name: Start virtual serial echo (socat) + run: | + socat -d -d pty,raw,echo=0,link=/tmp/ttyCI_A,mode=666 pty,raw,echo=0,link=/tmp/ttyCI_B,mode=666 & + sleep 2 + stdbuf -i0 -o0 cat < /tmp/ttyCI_B > /tmp/ttyCI_B & + sleep 1 + + - name: Run Deno integration tests + working-directory: integration_tests + env: + SERIAL_TEST_PORT: /tmp/ttyCI_A + run: | + deno task test + diff --git a/integration_tests/ffi_bindings.ts b/integration_tests/ffi_bindings.ts index ed6af21..7ba0f19 100644 --- a/integration_tests/ffi_bindings.ts +++ b/integration_tests/ffi_bindings.ts @@ -1,11 +1,9 @@ /** - * FFI bindings for cpp-bindings-linux shared library + * Minimal FFI bindings for the cpp-bindings-linux shared library + * used by the Deno integration tests. */ -// Error callback type: (status_code: number, message: string) => void -export type ErrorCallback = (statusCode: number, message: string) => void; - -// FFI symbol definitions +// FFI symbol definitions required by the tests export type SerialLib = { readonly serialOpen: ( port: Deno.PointerValue, @@ -54,6 +52,9 @@ export type LoadedLibrary = { export async function loadSerialLib( libraryPath?: string, ): Promise { + // Ensure this stays an async function for API stability + await Promise.resolve(); + // Try to find the library in common build locations const possiblePaths = [ libraryPath, @@ -112,55 +113,5 @@ export async function loadSerialLib( return lib; } -/** - * Create an error callback function that can be passed to FFI - */ -export function createErrorCallback( - callback: ErrorCallback, -) { - const definition = { - parameters: ["i32", "pointer"] as const, - result: "void" as const, - } as const; - - return new Deno.UnsafeCallback( - definition, - ( - statusCode, - messagePtr, - ): void => { - if (messagePtr === null) { - callback(statusCode, "Unknown error"); - return; - } - - const message = Deno.UnsafePointerView.getCString(messagePtr); - callback(statusCode, message); - }, - ); -} - -/** - * Helper to convert a string to a C string pointer - */ -export function stringToCString(str: string): Deno.PointerValue { - const encoder = new TextEncoder(); - const encoded = encoder.encode(str + "\0"); - const buffer = new Uint8Array(encoded.length); - buffer.set(encoded); - const ptr = Deno.UnsafePointer.of(buffer); - if (ptr === null) { - throw new Error("Failed to create pointer from string"); - } - return ptr; -} - -/** - * Helper to read a C string from a pointer - */ -export function cStringToString(ptr: Deno.PointerValue): string { - if (ptr === null) { - return ""; - } - return Deno.UnsafePointerView.getCString(ptr); -} +// Note: helpers for error callbacks and CString handling were removed +// here on purpose, as they are no longer needed by the minimal tests. diff --git a/integration_tests/integration_test.ts b/integration_tests/integration_test.ts index c719ec9..806235f 100644 --- a/integration_tests/integration_test.ts +++ b/integration_tests/integration_test.ts @@ -1,358 +1,46 @@ /** - * Integration tests for cpp-bindings-linux using Deno FFI + * Minimal Deno integration tests for cpp-bindings-linux: + * - verify that the shared library can be loaded + * - verify that it can be cleanly unloaded again */ -import { assert, assertEquals, assertExists, assertStringIncludes } from "@std/assert"; -import { - createErrorCallback, - type LoadedLibrary, - loadSerialLib, - type SerialLib, - stringToCString, -} from "./ffi_bindings.ts"; - -const StatusCodes = { - kSuccess: 0, - kCloseHandleError: -1, - kInvalidHandleError: -2, - kReadError: -3, - kWriteError: -4, - kGetStateError: -5, - kSetStateError: -6, - kSetTimeoutError: -7, - kBufferError: -8, - kNotFoundError: -9, - kClearBufferInError: -10, - kClearBufferOutError: -11, - kAbortReadError: -12, - kAbortWriteError: -13, -} as const; +import { assertExists } from "@std/assert"; +import { type LoadedLibrary, loadSerialLib, type SerialLib } from "./ffi_bindings.ts"; let lib: SerialLib | null = null; let loadedLib: LoadedLibrary | null = null; Deno.test({ - name: "Load library", + name: "Load cpp-bindings-linux library", async fn() { + // Small async boundary to keep Deno's async test happy loadedLib = await loadSerialLib(); - assertExists(loadedLib, "Failed to load library"); + assertExists(loadedLib, "Failed to load cpp-bindings-linux library"); lib = loadedLib.symbols; - console.log("Library loaded successfully"); - }, - sanitizeResources: false, - sanitizeOps: false, -}); - -Deno.test({ - name: "Invalid handle - Close", - async fn() { - assertExists(lib, "Library not loaded"); - - const result = lib.serialClose(BigInt(-1), null); - assertEquals(result, StatusCodes.kSuccess); - console.log("Invalid handle close handled correctly"); - }, -}); - -Deno.test({ - name: "Invalid handle - Read", - async fn() { - assertExists(lib, "Library not loaded"); - - const buffer = new Uint8Array(256); - const bufferPtr = Deno.UnsafePointer.of(buffer); - assertExists(bufferPtr, "Failed to create buffer pointer"); - - const result = lib.serialRead( - BigInt(-1), - bufferPtr, - buffer.length, - 1000, - 1, - null, - ); - - assertEquals(result, StatusCodes.kInvalidHandleError); - console.log("Invalid handle read handled correctly"); - }, -}); - -Deno.test({ - name: "Invalid handle - Write", - async fn() { - assertExists(lib, "Library not loaded"); - - const message = "test"; - const messagePtr = stringToCString(message); - const result = lib.serialWrite( - BigInt(-1), - messagePtr, - message.length, - 1000, - 1, - null, - ); + // Sanity check: verify that the expected symbols exist + assertExists(lib.serialOpen); + assertExists(lib.serialClose); + assertExists(lib.serialRead); + assertExists(lib.serialWrite); - assertEquals(result, StatusCodes.kInvalidHandleError); - console.log("Invalid handle write handled correctly"); + console.log("cpp-bindings-linux library loaded and symbols resolved"); }, + sanitizeResources: false, + sanitizeOps: false, }); Deno.test({ - name: "Error callback - Invalid port", - async fn() { - assertExists(lib, "Library not loaded"); - - let callbackCalled = false; - let receivedStatusCode = -1; - let receivedMessage = ""; - - const errorCallback = createErrorCallback((statusCode, message) => { - callbackCalled = true; - receivedStatusCode = statusCode; - receivedMessage = message; - }); - - try { - const result = lib.serialOpen( - null, - 115200, - 8, - 0, - 1, - errorCallback.pointer, - ); - - assert(result < 0, `Expected error code, got ${result}`); - - await new Promise((resolve) => setTimeout(resolve, 100)); - - assert(callbackCalled, "Error callback was not called"); - - assertEquals(receivedStatusCode, StatusCodes.kNotFoundError); - - assertStringIncludes( - receivedMessage, - "nullptr", - `Expected error message about nullptr, got: ${receivedMessage}`, - ); - - console.log("Error callback works correctly"); - } finally { - errorCallback.close(); - } - }, -}); - -Deno.test({ - name: "Invalid parameters - Baudrate", - async fn() { - assertExists(lib, "Library not loaded"); - - const port = "/dev/ttyUSB0"; - const portPtr = stringToCString(port); - - let callbackCalled = false; - let receivedStatusCode = -1; - let receivedMessage = ""; - const errorCallback = createErrorCallback((statusCode, message) => { - callbackCalled = true; - receivedStatusCode = statusCode; - receivedMessage = message; - }); - - try { - const result = lib.serialOpen( - portPtr, - 100, - 8, - 0, - 1, - errorCallback.pointer, - ); - - await new Promise((resolve) => setTimeout(resolve, 100)); - - assert(result < 0, `Expected error code, got ${result}`); - assert(callbackCalled, "Error callback was not called"); - - assertEquals(receivedStatusCode, StatusCodes.kSetStateError); - assertStringIncludes( - receivedMessage, - "baudrate", - `Expected baudrate error, got: ${receivedMessage}`, - ); - - console.log("Invalid baudrate handled correctly"); - } finally { - errorCallback.close(); - } - }, -}); - -Deno.test({ - name: "Invalid parameters - Data bits", - async fn() { - assertExists(lib, "Library not loaded"); - - const port = "/dev/ttyUSB0"; - const portPtr = stringToCString(port); - - let callbackCalled = false; - let receivedStatusCode = -1; - const errorCallback = createErrorCallback((statusCode, message) => { - callbackCalled = true; - receivedStatusCode = statusCode; - }); - - try { - const result = lib.serialOpen( - portPtr, - 115200, - 3, - 0, - 1, - errorCallback.pointer, - ); - - await new Promise((resolve) => setTimeout(resolve, 100)); - - assert(result < 0, `Expected error code, got ${result}`); - assert(callbackCalled, "Error callback was not called"); - - assertEquals(receivedStatusCode, StatusCodes.kSetStateError); - - console.log("Invalid data bits handled correctly"); - } finally { - errorCallback.close(); - } - }, -}); - -Deno.test({ - name: "Real serial port - Open/Close", - async fn() { - assertExists(lib, "Library not loaded"); - - const port = "/dev/ttyUSB0"; - const portPtr = stringToCString(port); - - const handle = lib.serialOpen( - portPtr, - 115200, - 8, - 0, - 1, - null, - ); - - if (handle <= 0) { - console.log("Skipping test: /dev/ttyUSB0 not available"); - return; - } - - try { - await new Promise((resolve) => setTimeout(resolve, 2000)); - - const closeResult = lib.serialClose(handle, null); - assertEquals(closeResult, StatusCodes.kSuccess); - - console.log("Real serial port open/close works"); - } catch (error) { - lib.serialClose(handle, null); - throw error; - } - }, - ignore: Deno.env.get("SKIP_HARDWARE_TESTS") === "1", -}); - -Deno.test({ - name: "Real serial port - Write/Read (if Arduino echo available)", + name: "Unload cpp-bindings-linux library", async fn() { - assertExists(lib, "Library not loaded"); - - const port = "/dev/ttyUSB0"; - const portPtr = stringToCString(port); - - const handle = lib.serialOpen( - portPtr, - 115200, - 8, - 0, - 1, - null, - ); - - if (handle <= 0) { - console.log("Skipping test: /dev/ttyUSB0 not available"); - return; - } - - try { - await new Promise((resolve) => setTimeout(resolve, 2000)); + // Simulate async cleanup boundary + await Promise.resolve(); + if (!loadedLib) return; - const testMessage = "Hello from Deno!\n"; - const encoder = new TextEncoder(); - const messageBytes = encoder.encode(testMessage); - const messageBuffer = new Uint8Array(messageBytes.length); - messageBuffer.set(messageBytes); - const messagePtr = Deno.UnsafePointer.of(messageBuffer); - assertExists(messagePtr, "Failed to create message pointer"); - - const written = lib.serialWrite( - handle, - messagePtr, - messageBytes.length, - 1000, - 1, - null, - ); - - assertEquals(written, messageBytes.length); - - await new Promise((resolve) => setTimeout(resolve, 500)); - - const readBuffer = new Uint8Array(256); - const readBufferPtr = Deno.UnsafePointer.of(readBuffer); - if (readBufferPtr === null) { - throw new Error("Failed to create read buffer pointer"); - } - - const readBytes = lib.serialRead( - handle, - readBufferPtr, - readBuffer.length - 1, - 2000, - 1, - null, - ); - - if (readBytes > 0) { - const receivedData = new Uint8Array(readBytes); - receivedData.set(readBuffer.subarray(0, readBytes)); - const decoder = new TextDecoder(); - const received = decoder.decode(receivedData); - console.log(`Received echo: ${received.trim()}`); - } else { - console.log("No data received (device might not echo)"); - } - } finally { - lib.serialClose(handle, null); - } - }, - ignore: Deno.env.get("SKIP_HARDWARE_TESTS") === "1", -}); - -Deno.test({ - name: "Unload library", - async fn() { - if (loadedLib) { - loadedLib.close(); - loadedLib = null; - lib = null; - console.log("Library unloaded successfully"); - } + loadedLib.close(); + loadedLib = null; + lib = null; + console.log("cpp-bindings-linux library unloaded successfully"); }, sanitizeResources: false, sanitizeOps: false, diff --git a/tests/test_serial_arduino.cpp b/tests/test_serial_arduino.cpp index b43093f..6772c4a 100644 --- a/tests/test_serial_arduino.cpp +++ b/tests/test_serial_arduino.cpp @@ -15,13 +15,14 @@ class SerialArduinoTest : public ::testing::Test protected: void SetUp() override { - const char *port = "/dev/ttyUSB0"; + const char *env_port = std::getenv("SERIAL_TEST_PORT"); + const char *port = env_port != nullptr ? env_port : "/dev/ttyUSB0"; handle_ = serialOpen(const_cast(static_cast(port)), 115200, 8, 0, 0, nullptr); if (handle_ <= 0) { - GTEST_SKIP() << "Could not open /dev/ttyUSB0. " - "Make sure Arduino is connected and accessible."; + GTEST_SKIP() << "Could not open serial port '" << port + << "'. Set SERIAL_TEST_PORT or connect Arduino on /dev/ttyUSB0."; } usleep(2000000); From 9154f878ecbf15a3cb85ca68bdeb76bed568d33a Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 15 Dec 2025 15:19:45 +0100 Subject: [PATCH 09/32] Update CI workflow to use Fedora container and simplify GCC configuration - Changed CI workflow to use a Fedora container for better CMake support. - Removed matrix strategy for GCC versions, simplifying the job configuration. - Updated package installation commands to use `dnf` instead of `apt-get` for Fedora compatibility. - Adjusted environment variable settings to reflect the new container setup. --- .github/workflows/ci.yml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93e1b16..024ef94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,15 +7,12 @@ on: jobs: cpp-tests: - name: C++ tests (gcc ${{ matrix.gcc }}) + name: C++ tests (gcc) runs-on: ubuntu-latest - strategy: - matrix: - gcc: [14, 15] - + # Nutze Fedora-Container, damit eine aktuelle CMake-Version verfügbar ist container: - image: ubuntu:24.04 + image: fedora:43 env: SERIAL_TEST_PORT: /tmp/ttyCI_A @@ -24,15 +21,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install build dependencies (gcc ${{ matrix.gcc }}) + - name: Install build dependencies (gcc) run: | - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y \ - cmake ninja-build g++-${{ matrix.gcc }} socat + dnf -y update + dnf -y install cmake ninja-build gcc-c++ socat git - name: Configure CMake env: - CXX: g++-${{ matrix.gcc }} + # Fedora liefert die systemweite g++-Version (abhängig vom Image), + # die Matrix.gcc dient hier primär der Job-Kennzeichnung. + CXX: g++ run: | cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release @@ -72,7 +70,7 @@ jobs: - name: Install build dependencies run: | apt-get update - apt-get install -y cmake ninja-build socat + apt-get install -y cmake ninja-build socat git - name: Configure CMake run: | From a7466f7d896adb20d05b626ebd3a3bea07e8b64f Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 15 Dec 2025 15:21:06 +0100 Subject: [PATCH 10/32] Update CI workflow to install g++ and streamline CMake configuration - Added g++ to the list of build dependencies in the CI workflow. - Removed unnecessary comments related to GCC configuration for clarity. - Ensured environment variable settings for CMake are properly configured. --- .github/workflows/ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 024ef94..0fe0b6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,6 @@ jobs: name: C++ tests (gcc) runs-on: ubuntu-latest - # Nutze Fedora-Container, damit eine aktuelle CMake-Version verfügbar ist container: image: fedora:43 @@ -28,8 +27,6 @@ jobs: - name: Configure CMake env: - # Fedora liefert die systemweite g++-Version (abhängig vom Image), - # die Matrix.gcc dient hier primär der Job-Kennzeichnung. CXX: g++ run: | cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release @@ -70,9 +67,11 @@ jobs: - name: Install build dependencies run: | apt-get update - apt-get install -y cmake ninja-build socat git + apt-get install -y cmake ninja-build g++ socat git - name: Configure CMake + env: + CXX: g++ run: | cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release From d3132619e81f9734f7e28dbf0b7f3ef5b2d88d6a Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 15 Dec 2025 15:25:07 +0100 Subject: [PATCH 11/32] Add missing BOTHER definition in serial_open.cpp for compatibility with minimal environments - Defined BOTHER in serial_open.cpp if not already defined, ensuring compatibility with certain libcs or kernel headers that may not include it. - This change supports builds in minimal environments, such as Deno's Debian-based CI containers. --- src/serial_open.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/serial_open.cpp b/src/serial_open.cpp index fb4b007..7d6116a 100644 --- a/src/serial_open.cpp +++ b/src/serial_open.cpp @@ -11,6 +11,12 @@ #ifndef TCGETS2 #define TCGETS2 0x802C542A #define TCSETS2 0x402C542B +#endif + +// Some libcs (or kernel headers) may not define BOTHER even if TCGETS2 exists. +// Define it here if missing so the build works in minimal environments +// (e.g., Deno's Debian-based CI containers). +#ifndef BOTHER #define BOTHER 0x010000 #endif From 21bda89fe9043c70061a99caec7310aec90441c6 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 14:18:55 +0100 Subject: [PATCH 12/32] Add CI workflows for building Deno shared library and running C++ tests - Introduced a GitHub Actions workflow for building a shared library for Deno, including steps for dependency installation, CMake configuration, and artifact collection. - Added a separate workflow for running C++ unit and integration tests, utilizing a Fedora container and configuring the environment for serial communication testing. - Both workflows are triggered on pushes and pull requests to the main branches, ensuring automated testing and builds. --- .github/workflows/build-deno-so.yml | 49 ++++++++++++++++++ .github/workflows/cpp-tests.yml | 52 ++++++++++++++++++++ .github/workflows/{ci.yml => deno-tests.yml} | 46 +---------------- 3 files changed, 103 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/build-deno-so.yml create mode 100644 .github/workflows/cpp-tests.yml rename .github/workflows/{ci.yml => deno-tests.yml} (52%) diff --git a/.github/workflows/build-deno-so.yml b/.github/workflows/build-deno-so.yml new file mode 100644 index 0000000..1d3e5d4 --- /dev/null +++ b/.github/workflows/build-deno-so.yml @@ -0,0 +1,49 @@ +name: Build Deno Shared Library + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + build-deno-so: + name: Build shared library for Deno + runs-on: ubuntu-20.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y ninja-build g++ git + + - name: Setup CMake >= 3.30 + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: '3.31.x' + + - name: Configure CMake (Release) + env: + CXX: g++ + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build shared library + run: | + cmake --build build --config Release + + - name: Collect shared library artifact + run: | + mkdir -p deno-lib/linux-x86_64 + cp build/libcpp_bindings_linux.so* deno-lib/linux-x86_64/ + ls -R deno-lib + + - name: Upload .so artifact for Deno + uses: actions/upload-artifact@v4 + with: + name: cpp-bindings-linux-so-linux-x86_64 + path: deno-lib/** + + diff --git a/.github/workflows/cpp-tests.yml b/.github/workflows/cpp-tests.yml new file mode 100644 index 0000000..cefe01e --- /dev/null +++ b/.github/workflows/cpp-tests.yml @@ -0,0 +1,52 @@ +name: C++ Tests + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + cpp-tests: + name: C++ tests (gcc) + runs-on: ubuntu-latest + + container: + image: fedora:43 + + env: + SERIAL_TEST_PORT: /tmp/ttyCI_A + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install build dependencies (gcc) + run: | + dnf -y update + dnf -y install cmake ninja-build gcc-c++ socat git + + - name: Configure CMake + env: + CXX: g++ + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: | + cmake --build build --config Release + + - name: Start virtual serial echo (socat) + run: | + socat -d -d pty,raw,echo=0,link=/tmp/ttyCI_A,mode=666 pty,raw,echo=0,link=/tmp/ttyCI_B,mode=666 & + sleep 2 + stdbuf -i0 -o0 cat < /tmp/ttyCI_B > /tmp/ttyCI_B & + sleep 1 + + - name: Run C++ unit/integration tests + working-directory: build + env: + SERIAL_TEST_PORT: /tmp/ttyCI_A + run: | + ./cpp_bindings_linux_tests --gtest_color=yes + + diff --git a/.github/workflows/ci.yml b/.github/workflows/deno-tests.yml similarity index 52% rename from .github/workflows/ci.yml rename to .github/workflows/deno-tests.yml index 0fe0b6e..983701b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/deno-tests.yml @@ -1,4 +1,4 @@ -name: CI +name: Deno Integration Tests on: push: @@ -6,49 +6,6 @@ on: pull_request: jobs: - cpp-tests: - name: C++ tests (gcc) - runs-on: ubuntu-latest - - container: - image: fedora:43 - - env: - SERIAL_TEST_PORT: /tmp/ttyCI_A - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install build dependencies (gcc) - run: | - dnf -y update - dnf -y install cmake ninja-build gcc-c++ socat git - - - name: Configure CMake - env: - CXX: g++ - run: | - cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release - - - name: Build - run: | - cmake --build build --config Release - - - name: Start virtual serial echo (socat) - run: | - socat -d -d pty,raw,echo=0,link=/tmp/ttyCI_A,mode=666 pty,raw,echo=0,link=/tmp/ttyCI_B,mode=666 & - sleep 2 - stdbuf -i0 -o0 cat < /tmp/ttyCI_B > /tmp/ttyCI_B & - sleep 1 - - - name: Run C++ unit/integration tests - working-directory: build - env: - SERIAL_TEST_PORT: /tmp/ttyCI_A - run: | - ./cpp_bindings_linux_tests --gtest_color=yes - deno-tests: name: Deno integration tests (deno ${{ matrix.deno }}) runs-on: ubuntu-latest @@ -93,3 +50,4 @@ jobs: run: | deno task test + From 6a40322997259b366b06fbae3b9507d239f5945e Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 14:29:27 +0100 Subject: [PATCH 13/32] Refactor serial communication error handling and introduce posix_helpers - Replaced direct error handling in serial_open, serial_close, serial_read, and serial_write with utility functions from the new posix_helpers.hpp for improved code clarity and consistency. - Introduced UniqueFd class to manage file descriptors safely and prevent resource leaks. - Simplified error callback invocations to streamline error reporting across serial communication functions. --- src/detail/posix_helpers.hpp | 96 ++++++++++++++++++++++++++++++++ src/serial_close.cpp | 11 +--- src/serial_open.cpp | 104 ++++++++++------------------------- src/serial_read.cpp | 24 +++----- src/serial_write.cpp | 24 +++----- 5 files changed, 143 insertions(+), 116 deletions(-) create mode 100644 src/detail/posix_helpers.hpp diff --git a/src/detail/posix_helpers.hpp b/src/detail/posix_helpers.hpp new file mode 100644 index 0000000..b80dddc --- /dev/null +++ b/src/detail/posix_helpers.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace cpp_bindings_linux::detail +{ +class UniqueFd +{ + public: + UniqueFd() = default; + explicit UniqueFd(int in_fd) : fd_(in_fd) + { + } + + UniqueFd(const UniqueFd &) = delete; + auto operator=(const UniqueFd &) -> UniqueFd & = delete; + + UniqueFd(UniqueFd &&other) noexcept : fd_(other.fd_) + { + other.fd_ = -1; + } + auto operator=(UniqueFd &&other) noexcept -> UniqueFd & + { + if (this != &other) + { + reset(other.release()); + } + return *this; + } + + ~UniqueFd() + { + reset(-1); + } + + [[nodiscard]] auto get() const -> int + { + return fd_; + } + [[nodiscard]] auto valid() const -> bool + { + return fd_ >= 0; + } + + auto reset(int new_fd) -> void + { + if (fd_ >= 0) + { + close(fd_); + } + fd_ = new_fd; + } + + [[nodiscard]] auto release() -> int + { + const int out = fd_; + fd_ = -1; + return out; + } + + private: + int fd_ = -1; +}; + +template +inline auto invokeErrorCallback(Callback error_callback, cpp_core::StatusCodes code, const char *message) -> void +{ + if (error_callback != nullptr) + { + error_callback(static_cast(code), message); + } +} + +template +inline auto failMsg(Callback error_callback, cpp_core::StatusCodes code, const char *message) -> Ret +{ + invokeErrorCallback(error_callback, code, message); + return static_cast(code); +} + +template +inline auto failErrno(Callback error_callback, cpp_core::StatusCodes code) -> Ret +{ + if (error_callback != nullptr) + { + const std::string error_msg = std::error_code(errno, std::generic_category()).message(); + error_callback(static_cast(code), error_msg.c_str()); + } + return static_cast(code); +} +} // namespace cpp_bindings_linux::detail diff --git a/src/serial_close.cpp b/src/serial_close.cpp index c63c379..4739f5e 100644 --- a/src/serial_close.cpp +++ b/src/serial_close.cpp @@ -1,8 +1,8 @@ #include #include -#include -#include +#include "detail/posix_helpers.hpp" + #include extern "C" @@ -18,12 +18,7 @@ extern "C" const int fd = static_cast(handle); if (close(fd) != 0) { - if (error_callback != nullptr) - { - const std::string error_msg = std::error_code(errno, std::generic_category()).message(); - error_callback(static_cast(cpp_core::StatusCodes::kCloseHandleError), error_msg.c_str()); - } - return static_cast(cpp_core::StatusCodes::kCloseHandleError); + return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kCloseHandleError); } return static_cast(cpp_core::StatusCodes::kSuccess); diff --git a/src/serial_open.cpp b/src/serial_open.cpp index 7d6116a..d87a0b0 100644 --- a/src/serial_open.cpp +++ b/src/serial_open.cpp @@ -1,10 +1,10 @@ #include #include -#include +#include "detail/posix_helpers.hpp" + #include #include -#include #include #include @@ -37,62 +37,41 @@ struct termios2 extern "C" { - // NOLINTNEXTLINE(readability-function-cognitive-complexity) MODULE_API auto serialOpen(void *port, int baudrate, int data_bits, int parity, int stop_bits, ErrorCallbackT error_callback) -> intptr_t { if (port == nullptr) { - if (error_callback != nullptr) - { - error_callback(static_cast(cpp_core::StatusCodes::kNotFoundError), "Port parameter is nullptr"); - } - return static_cast(cpp_core::StatusCodes::kNotFoundError); + return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kNotFoundError, + "Port parameter is nullptr"); } if (baudrate < 300) { - if (error_callback != nullptr) - { - error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), - "Invalid baudrate: must be >= 300"); - } - return static_cast(cpp_core::StatusCodes::kSetStateError); + return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, + "Invalid baudrate: must be >= 300"); } if (data_bits < 5 || data_bits > 8) { - if (error_callback != nullptr) - { - error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), - "Invalid data bits: must be 5-8"); - } - return static_cast(cpp_core::StatusCodes::kSetStateError); + return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, + "Invalid data bits: must be 5-8"); } const char *port_path = static_cast(port); - const int fd = open(port_path, O_RDWR | O_NOCTTY | O_NONBLOCK); - if (fd < 0) + cpp_bindings_linux::detail::UniqueFd handle(open(port_path, O_RDWR | O_NOCTTY | O_NONBLOCK)); + if (!handle.valid()) { - if (error_callback != nullptr) - { - const std::string error_msg = std::error_code(errno, std::generic_category()).message(); - error_callback(static_cast(cpp_core::StatusCodes::kNotFoundError), error_msg.c_str()); - } - return static_cast(cpp_core::StatusCodes::kNotFoundError); + return cpp_bindings_linux::detail::failErrno(error_callback, + cpp_core::StatusCodes::kNotFoundError); } struct termios2 tty = {}; - if (ioctl(fd, TCGETS2, &tty) != 0) + if (ioctl(handle.get(), TCGETS2, &tty) != 0) { - close(fd); - if (error_callback != nullptr) - { - const std::string error_msg = std::error_code(errno, std::generic_category()).message(); - error_callback(static_cast(cpp_core::StatusCodes::kGetStateError), error_msg.c_str()); - } - return static_cast(cpp_core::StatusCodes::kGetStateError); + return cpp_bindings_linux::detail::failErrno(error_callback, + cpp_core::StatusCodes::kGetStateError); } tty.c_cflag &= ~CBAUD; @@ -116,12 +95,8 @@ extern "C" tty.c_cflag |= CS8; break; default: - close(fd); - if (error_callback != nullptr) - { - error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), "Invalid data bits"); - } - return static_cast(cpp_core::StatusCodes::kSetStateError); + return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, + "Invalid data bits"); } tty.c_cflag &= ~(PARENB | PARODD); @@ -136,12 +111,8 @@ extern "C" tty.c_cflag |= (PARENB | PARODD); break; default: - close(fd); - if (error_callback != nullptr) - { - error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), "Invalid parity"); - } - return static_cast(cpp_core::StatusCodes::kSetStateError); + return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, + "Invalid parity"); } if (stop_bits == 2) @@ -160,48 +131,33 @@ extern "C" tty.c_cc[VMIN] = 0; tty.c_cc[VTIME] = 0; - if (ioctl(fd, TCSETS2, &tty) != 0) + if (ioctl(handle.get(), TCSETS2, &tty) != 0) { - close(fd); - if (error_callback != nullptr) - { - const std::string error_msg = std::error_code(errno, std::generic_category()).message(); - error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), error_msg.c_str()); - } - return static_cast(cpp_core::StatusCodes::kSetStateError); + return cpp_bindings_linux::detail::failErrno(error_callback, + cpp_core::StatusCodes::kSetStateError); } - int flags = fcntl(fd, F_GETFL); + int flags = fcntl(handle.get(), F_GETFL); if (flags < 0) { - close(fd); - if (error_callback != nullptr) - { - const std::string error_msg = std::error_code(errno, std::generic_category()).message(); - error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), error_msg.c_str()); - } - return static_cast(cpp_core::StatusCodes::kSetStateError); + return cpp_bindings_linux::detail::failErrno(error_callback, + cpp_core::StatusCodes::kSetStateError); } flags &= ~O_NONBLOCK; - const int set_flags_result = fcntl(fd, F_SETFL, flags); + const int set_flags_result = fcntl(handle.get(), F_SETFL, flags); if (set_flags_result != 0) { - close(fd); - if (error_callback != nullptr) - { - const std::string error_msg = std::error_code(errno, std::generic_category()).message(); - error_callback(static_cast(cpp_core::StatusCodes::kSetStateError), error_msg.c_str()); - } - return static_cast(cpp_core::StatusCodes::kSetStateError); + return cpp_bindings_linux::detail::failErrno(error_callback, + cpp_core::StatusCodes::kSetStateError); } - tcflush(fd, TCIOFLUSH); + tcflush(handle.get(), TCIOFLUSH); // Note: Some devices (e.g., Arduino) reset when the serial port is opened. // It is recommended to wait 1-2 seconds after opening before sending data // to allow the device to initialize. - return static_cast(fd); + return static_cast(handle.release()); } } // extern "C" diff --git a/src/serial_read.cpp b/src/serial_read.cpp index 7ae5ffa..048eb4b 100644 --- a/src/serial_read.cpp +++ b/src/serial_read.cpp @@ -1,9 +1,10 @@ #include #include +#include "detail/posix_helpers.hpp" + #include #include -#include #include // NOLINTNEXTLINE(misc-use-anonymous-namespace) @@ -43,20 +44,14 @@ extern "C" { if (buffer == nullptr || buffer_size <= 0) { - if (error_callback != nullptr) - { - error_callback(static_cast(cpp_core::StatusCodes::kBufferError), "Invalid buffer or buffer_size"); - } - return static_cast(cpp_core::StatusCodes::kBufferError); + return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kBufferError, + "Invalid buffer or buffer_size"); } if (handle <= 0) { - if (error_callback != nullptr) - { - error_callback(static_cast(cpp_core::StatusCodes::kInvalidHandleError), "Invalid handle"); - } - return static_cast(cpp_core::StatusCodes::kInvalidHandleError); + return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kInvalidHandleError, + "Invalid handle"); } const int fd = static_cast(handle); @@ -103,12 +98,7 @@ extern "C" { return 0; } - if (error_callback != nullptr) - { - const std::string error_msg = std::error_code(errno, std::generic_category()).message(); - error_callback(static_cast(cpp_core::StatusCodes::kReadError), error_msg.c_str()); - } - return static_cast(cpp_core::StatusCodes::kReadError); + return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kReadError); } if (bytes_read == 0) diff --git a/src/serial_write.cpp b/src/serial_write.cpp index 9c4ae55..791ac9c 100644 --- a/src/serial_write.cpp +++ b/src/serial_write.cpp @@ -1,10 +1,11 @@ #include #include +#include "detail/posix_helpers.hpp" + #include #include #include -#include #include #include @@ -45,20 +46,14 @@ extern "C" { if (buffer == nullptr || buffer_size <= 0) { - if (error_callback != nullptr) - { - error_callback(static_cast(cpp_core::StatusCodes::kBufferError), "Invalid buffer or buffer_size"); - } - return static_cast(cpp_core::StatusCodes::kBufferError); + return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kBufferError, + "Invalid buffer or buffer_size"); } if (handle <= 0) { - if (error_callback != nullptr) - { - error_callback(static_cast(cpp_core::StatusCodes::kInvalidHandleError), "Invalid handle"); - } - return static_cast(cpp_core::StatusCodes::kInvalidHandleError); + return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kInvalidHandleError, + "Invalid handle"); } const int fd = static_cast(handle); @@ -80,12 +75,7 @@ extern "C" if (bytes_written < 0) { - if (error_callback != nullptr) - { - const std::string error_msg = std::error_code(errno, std::generic_category()).message(); - error_callback(static_cast(cpp_core::StatusCodes::kWriteError), error_msg.c_str()); - } - return static_cast(cpp_core::StatusCodes::kWriteError); + return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kWriteError); } } From 516406f4785398d68ca41c8cd8c554e93a6e4674 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 14:34:38 +0100 Subject: [PATCH 14/32] Refactor serial_read to improve readability and modularity - Moved the waitFdReady function into an anonymous namespace for better encapsulation. - Simplified the serialRead function by introducing a lambda for reading data, enhancing clarity. - Streamlined the logic for checking data readiness and reading from the file descriptor, reducing complexity and improving maintainability. --- src/serial_read.cpp | 76 +++++++++++++-------------------------------- 1 file changed, 21 insertions(+), 55 deletions(-) diff --git a/src/serial_read.cpp b/src/serial_read.cpp index 048eb4b..f6e78a2 100644 --- a/src/serial_read.cpp +++ b/src/serial_read.cpp @@ -7,8 +7,9 @@ #include #include -// NOLINTNEXTLINE(misc-use-anonymous-namespace) -static auto waitFdReady(int fd, int timeout_ms, bool for_read) -> int +namespace +{ +auto waitFdReady(int fd, int timeout_ms, bool for_read) -> int { struct pollfd pfd = {}; pfd.fd = fd; @@ -35,10 +36,10 @@ static auto waitFdReady(int fd, int timeout_ms, bool for_read) -> int } return 0; } +} // namespace extern "C" { - // NOLINTNEXTLINE(readability-function-cognitive-complexity) MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, ErrorCallbackT error_callback) -> int { @@ -57,65 +58,36 @@ extern "C" const int fd = static_cast(handle); auto *buf = static_cast(buffer); - int elapsed_ms = 0; - const int poll_interval = 10; - bool data_ready = false; - - while (elapsed_ms < timeout_ms) + const int ready = waitFdReady(fd, timeout_ms, true); + if (ready <= 0) { - struct pollfd pfd = {}; - pfd.fd = fd; - pfd.events = POLLIN; - pfd.revents = 0; - - const int remaining_ms = timeout_ms - elapsed_ms; - const int poll_timeout = (remaining_ms < poll_interval) ? remaining_ms : poll_interval; + return 0; + } - const int poll_result = poll(&pfd, 1, poll_timeout); - if (poll_result > 0 && ((pfd.revents & POLLIN) != 0)) - { - data_ready = true; - break; - } - if (poll_result < 0) + const auto try_read_once = [&](unsigned char *dst, int size) -> ssize_t { + const ssize_t bytes = ::read(fd, dst, size); + if (bytes < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { return 0; } + return bytes; + }; - elapsed_ms += poll_interval; - } - - if (!data_ready) - { - return 0; - } - - ssize_t bytes_read = ::read(fd, buf, buffer_size); - + ssize_t bytes_read = try_read_once(buf, buffer_size); if (bytes_read < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) - { - return 0; - } return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kReadError); } + // Some drivers can report readiness but still return 0; give it a tiny grace period and retry once. if (bytes_read == 0) { - struct pollfd pfd = {}; - pfd.fd = fd; - pfd.events = POLLIN; - pfd.revents = 0; - if (poll(&pfd, 1, 10) > 0 && ((pfd.revents & POLLIN) != 0)) + if (waitFdReady(fd, 10, true) <= 0) { - bytes_read = ::read(fd, buf, buffer_size); - if (bytes_read <= 0) - { - return 0; - } + return 0; } - else + bytes_read = try_read_once(buf, buffer_size); + if (bytes_read <= 0) { return 0; } @@ -124,17 +96,11 @@ extern "C" int total_read = static_cast(bytes_read); while (total_read < buffer_size) { - struct pollfd pfd = {}; - pfd.fd = fd; - pfd.events = POLLIN; - pfd.revents = 0; - - if (poll(&pfd, 1, 0) <= 0 || ((pfd.revents & POLLIN) == 0)) + if (waitFdReady(fd, 0, true) <= 0) { break; } - - const ssize_t more_bytes = ::read(fd, buf + total_read, buffer_size - total_read); + const ssize_t more_bytes = try_read_once(buf + total_read, buffer_size - total_read); if (more_bytes <= 0) { break; From 9081c61325ea757360e9b1bd89dfd22435f97210 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 14:48:00 +0100 Subject: [PATCH 15/32] Refactor serial communication functions to enhance clarity and maintainability - Moved the waitFdReady function to posix_helpers.hpp for better code organization and reuse across serial_read and serial_write. - Updated serial_read and serial_write implementations to utilize the new waitFdReady function, improving readability and reducing code duplication. - Added detailed comments for parity and stop_bits configurations in serial_open.cpp to clarify their usage and constraints. --- src/detail/posix_helpers.hpp | 30 ++++++++++++++++++++ src/serial_open.cpp | 27 +++++++++--------- src/serial_read.cpp | 54 ++++++++++++------------------------ src/serial_write.cpp | 38 +++++-------------------- 4 files changed, 69 insertions(+), 80 deletions(-) diff --git a/src/detail/posix_helpers.hpp b/src/detail/posix_helpers.hpp index b80dddc..8a78f6a 100644 --- a/src/detail/posix_helpers.hpp +++ b/src/detail/posix_helpers.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -93,4 +94,33 @@ inline auto failErrno(Callback error_callback, cpp_core::StatusCodes code) -> Re } return static_cast(code); } + +// Poll helper used by read/write to implement timeouts. +// Returns: -1 on poll error, 0 on timeout/not-ready, 1 on ready. +inline auto waitFdReady(int file_descriptor, int timeout_ms, bool for_read) -> int +{ + struct pollfd poll_fd = {}; + poll_fd.fd = file_descriptor; + poll_fd.events = for_read ? POLLIN : POLLOUT; + poll_fd.revents = 0; + + const int poll_result = poll(&poll_fd, 1, timeout_ms); + if (poll_result < 0) + { + return -1; + } + if (poll_result == 0) + { + return 0; + } + if (for_read && ((poll_fd.revents & POLLIN) != 0)) + { + return 1; + } + if (!for_read && ((poll_fd.revents & POLLOUT) != 0)) + { + return 1; + } + return 0; +} } // namespace cpp_bindings_linux::detail diff --git a/src/serial_open.cpp b/src/serial_open.cpp index d87a0b0..e2ddd13 100644 --- a/src/serial_open.cpp +++ b/src/serial_open.cpp @@ -102,6 +102,10 @@ extern "C" tty.c_cflag &= ~(PARENB | PARODD); switch (parity) { + // parity mapping: + // 0 = no parity + // 1 = even parity + // 2 = odd parity case 0: break; case 1: @@ -115,6 +119,14 @@ extern "C" "Invalid parity"); } + // stop_bits mapping: + // 0 or 1 = 1 stop bit (0 kept for backward compatibility with callers using "default") + // 2 = 2 stop bits + if (stop_bits != 0 && stop_bits != 1 && stop_bits != 2) + { + return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, + "Invalid stop bits: must be 0, 1, or 2"); + } if (stop_bits == 2) { tty.c_cflag |= CSTOPB; @@ -137,19 +149,8 @@ extern "C" cpp_core::StatusCodes::kSetStateError); } - int flags = fcntl(handle.get(), F_GETFL); - if (flags < 0) - { - return cpp_bindings_linux::detail::failErrno(error_callback, - cpp_core::StatusCodes::kSetStateError); - } - flags &= ~O_NONBLOCK; - const int set_flags_result = fcntl(handle.get(), F_SETFL, flags); - if (set_flags_result != 0) - { - return cpp_bindings_linux::detail::failErrno(error_callback, - cpp_core::StatusCodes::kSetStateError); - } + // Keep O_NONBLOCK enabled. Our read/write APIs implement timeouts via poll(), + // and leaving the FD non-blocking prevents any unexpected blocking syscalls. tcflush(handle.get(), TCIOFLUSH); diff --git a/src/serial_read.cpp b/src/serial_read.cpp index f6e78a2..06ce323 100644 --- a/src/serial_read.cpp +++ b/src/serial_read.cpp @@ -4,40 +4,8 @@ #include "detail/posix_helpers.hpp" #include -#include #include -namespace -{ -auto waitFdReady(int fd, int timeout_ms, bool for_read) -> int -{ - struct pollfd pfd = {}; - pfd.fd = fd; - pfd.events = for_read ? POLLIN : POLLOUT; - pfd.revents = 0; - - const int result = poll(&pfd, 1, timeout_ms); - - if (result < 0) - { - return -1; - } - if (result == 0) - { - return 0; - } - if (for_read && ((pfd.revents & POLLIN) != 0)) - { - return 1; - } - if (!for_read && ((pfd.revents & POLLOUT) != 0)) - { - return 1; - } - return 0; -} -} // namespace - extern "C" { MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, @@ -58,8 +26,12 @@ extern "C" const int fd = static_cast(handle); auto *buf = static_cast(buffer); - const int ready = waitFdReady(fd, timeout_ms, true); - if (ready <= 0) + const int ready = cpp_bindings_linux::detail::waitFdReady(fd, timeout_ms, true); + if (ready < 0) + { + return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kReadError); + } + if (ready == 0) { return 0; } @@ -82,7 +54,12 @@ extern "C" // Some drivers can report readiness but still return 0; give it a tiny grace period and retry once. if (bytes_read == 0) { - if (waitFdReady(fd, 10, true) <= 0) + const int retry_ready = cpp_bindings_linux::detail::waitFdReady(fd, 10, true); + if (retry_ready < 0) + { + return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kReadError); + } + if (retry_ready == 0) { return 0; } @@ -96,7 +73,12 @@ extern "C" int total_read = static_cast(bytes_read); while (total_read < buffer_size) { - if (waitFdReady(fd, 0, true) <= 0) + const int loop_ready = cpp_bindings_linux::detail::waitFdReady(fd, 0, true); + if (loop_ready < 0) + { + return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kReadError); + } + if (loop_ready == 0) { break; } diff --git a/src/serial_write.cpp b/src/serial_write.cpp index 791ac9c..4bdcfce 100644 --- a/src/serial_write.cpp +++ b/src/serial_write.cpp @@ -5,39 +5,9 @@ #include #include -#include #include #include -// NOLINTNEXTLINE(misc-use-anonymous-namespace) -static auto waitFdReady(int fd, int timeout_ms, bool for_read) -> int -{ - struct pollfd pfd = {}; - pfd.fd = fd; - pfd.events = for_read ? POLLIN : POLLOUT; - pfd.revents = 0; - - const int result = poll(&pfd, 1, timeout_ms); - - if (result < 0) - { - return -1; - } - if (result == 0) - { - return 0; - } - if (for_read && ((pfd.revents & POLLIN) != 0)) - { - return 1; - } - if (!for_read && ((pfd.revents & POLLOUT) != 0)) - { - return 1; - } - return 0; -} - extern "C" { @@ -63,7 +33,13 @@ extern "C" { if (errno == EAGAIN || errno == EWOULDBLOCK) { - if (waitFdReady(fd, timeout_ms, false) > 0) + const int ready = cpp_bindings_linux::detail::waitFdReady(fd, timeout_ms, false); + if (ready < 0) + { + return cpp_bindings_linux::detail::failErrno(error_callback, + cpp_core::StatusCodes::kWriteError); + } + if (ready > 0) { bytes_written = ::write(fd, buffer, buffer_size); } From 70fba4917c90ae06f342c28f4c2291ccb2bddfd2 Mon Sep 17 00:00:00 2001 From: Katze719 <38188106+Katze719@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:49:11 +0100 Subject: [PATCH 16/32] Update integration_tests/ffi_bindings.ts Co-authored-by: Mqx <62719703+Mqxx@users.noreply.github.com> --- integration_tests/ffi_bindings.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/integration_tests/ffi_bindings.ts b/integration_tests/ffi_bindings.ts index 7ba0f19..e50395b 100644 --- a/integration_tests/ffi_bindings.ts +++ b/integration_tests/ffi_bindings.ts @@ -5,38 +5,38 @@ // FFI symbol definitions required by the tests export type SerialLib = { - readonly serialOpen: ( + serialOpen: ( port: Deno.PointerValue, baudrate: number, dataBits: number, parity: number, stopBits: number, - errorCallback: Deno.PointerValue | null, + errorCallback: Deno.PointerValue, ) => bigint; - readonly serialClose: ( + serialClose: ( handle: bigint, - errorCallback: Deno.PointerValue | null, + errorCallback: Deno.PointerValue, ) => number; - readonly serialRead: ( + serialRead: ( handle: bigint, buffer: Deno.PointerValue, bufferSize: number, timeoutMs: number, multiplier: number, - errorCallback: Deno.PointerValue | null, + errorCallback: Deno.PointerValue, ) => number; - readonly serialWrite: ( + serialWrite: ( handle: bigint, buffer: Deno.PointerValue, bufferSize: number, timeoutMs: number, multiplier: number, - errorCallback: Deno.PointerValue | null, + errorCallback: Deno.PointerValue, ) => number; -}; +} as const; // Library object type export type LoadedLibrary = { From d8881126b61bc7fec4e5c798518c2238fb6e6a8b Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 14:51:30 +0100 Subject: [PATCH 17/32] Update integration_tests/ffi_bindings.ts to make serial function signatures readonly - Changed function signatures in the SerialLib type to readonly, ensuring immutability and enhancing type safety. - This update improves the clarity of the API by indicating that these functions should not be modified after their initial definition. --- integration_tests/ffi_bindings.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/integration_tests/ffi_bindings.ts b/integration_tests/ffi_bindings.ts index e50395b..ab9db8e 100644 --- a/integration_tests/ffi_bindings.ts +++ b/integration_tests/ffi_bindings.ts @@ -5,7 +5,7 @@ // FFI symbol definitions required by the tests export type SerialLib = { - serialOpen: ( + readonly serialOpen: ( port: Deno.PointerValue, baudrate: number, dataBits: number, @@ -14,12 +14,12 @@ export type SerialLib = { errorCallback: Deno.PointerValue, ) => bigint; - serialClose: ( + readonly serialClose: ( handle: bigint, errorCallback: Deno.PointerValue, ) => number; - serialRead: ( + readonly serialRead: ( handle: bigint, buffer: Deno.PointerValue, bufferSize: number, @@ -28,7 +28,7 @@ export type SerialLib = { errorCallback: Deno.PointerValue, ) => number; - serialWrite: ( + readonly serialWrite: ( handle: bigint, buffer: Deno.PointerValue, bufferSize: number, @@ -36,7 +36,7 @@ export type SerialLib = { multiplier: number, errorCallback: Deno.PointerValue, ) => number; -} as const; +}; // Library object type export type LoadedLibrary = { From 3d86cf8111c35cd60efd2a0c9ada4f2ae67ba020 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 14:55:29 +0100 Subject: [PATCH 18/32] Update CI workflow to use latest Ubuntu and optimize dependency installation - Changed the runner from ubuntu-20.04 to ubuntu-latest for improved compatibility and support. - Added a container specification to use Ubuntu 20.04. - Updated the dependency installation process to use non-interactive mode and removed unnecessary package lists to streamline the build process. --- .github/workflows/build-deno-so.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-deno-so.yml b/.github/workflows/build-deno-so.yml index 1d3e5d4..bfbd60b 100644 --- a/.github/workflows/build-deno-so.yml +++ b/.github/workflows/build-deno-so.yml @@ -8,16 +8,22 @@ on: jobs: build-deno-so: name: Build shared library for Deno - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + + container: + image: ubuntu:20.04 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install build dependencies + env: + DEBIAN_FRONTEND: noninteractive run: | - sudo apt-get update - sudo apt-get install -y ninja-build g++ git + apt-get update + apt-get install -y --no-install-recommends ninja-build g++ git ca-certificates + rm -rf /var/lib/apt/lists/* - name: Setup CMake >= 3.30 uses: jwlawson/actions-setup-cmake@v2 From 5adc6db22b20b8f61053717e507492e10a5dee13 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 14:57:32 +0100 Subject: [PATCH 19/32] Update CI workflow to use Ubuntu 24.04 container for improved compatibility --- .github/workflows/build-deno-so.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-deno-so.yml b/.github/workflows/build-deno-so.yml index bfbd60b..7523a80 100644 --- a/.github/workflows/build-deno-so.yml +++ b/.github/workflows/build-deno-so.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest container: - image: ubuntu:20.04 + image: ubuntu:24.04 steps: - name: Checkout repository From e288874b5b8c83fbf217980568b2379d947caada Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 17:43:17 +0100 Subject: [PATCH 20/32] Enhance handle validation in serial communication functions - Added checks in serial_close, serial_read, and serial_write to ensure that handles do not exceed the maximum limit for integers, improving error handling and robustness. - Included header to facilitate the validation of handle values across the functions. --- src/serial_close.cpp | 6 ++++++ src/serial_read.cpp | 3 ++- src/serial_write.cpp | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/serial_close.cpp b/src/serial_close.cpp index 4739f5e..6ef9cea 100644 --- a/src/serial_close.cpp +++ b/src/serial_close.cpp @@ -3,6 +3,7 @@ #include "detail/posix_helpers.hpp" +#include #include extern "C" @@ -14,6 +15,11 @@ extern "C" { return static_cast(cpp_core::StatusCodes::kSuccess); } + if (handle > std::numeric_limits::max()) + { + return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kInvalidHandleError, + "Invalid handle"); + } const int fd = static_cast(handle); if (close(fd) != 0) diff --git a/src/serial_read.cpp b/src/serial_read.cpp index 06ce323..a883b80 100644 --- a/src/serial_read.cpp +++ b/src/serial_read.cpp @@ -4,6 +4,7 @@ #include "detail/posix_helpers.hpp" #include +#include #include extern "C" @@ -17,7 +18,7 @@ extern "C" "Invalid buffer or buffer_size"); } - if (handle <= 0) + if (handle <= 0 || handle > std::numeric_limits::max()) { return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle"); diff --git a/src/serial_write.cpp b/src/serial_write.cpp index 4bdcfce..abd7b10 100644 --- a/src/serial_write.cpp +++ b/src/serial_write.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -20,7 +21,7 @@ extern "C" "Invalid buffer or buffer_size"); } - if (handle <= 0) + if (handle <= 0 || handle > std::numeric_limits::max()) { return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle"); From 8d1ed1e173b9d461f045702facd9c4291bb65dad Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 18:05:50 +0100 Subject: [PATCH 21/32] Refactor FFI bindings in integration_tests/ffi_bindings.ts for improved type safety - Updated the SerialLib type to utilize Deno.DynamicLibrary for better type inference and clarity. - Consolidated symbol definitions into a single object, enhancing organization and maintainability. - Adjusted the loadSerialLib function to reflect the new type structure, ensuring consistency across the codebase. --- integration_tests/ffi_bindings.ts | 83 ++++++++----------------------- 1 file changed, 21 insertions(+), 62 deletions(-) diff --git a/integration_tests/ffi_bindings.ts b/integration_tests/ffi_bindings.ts index ab9db8e..6060b81 100644 --- a/integration_tests/ffi_bindings.ts +++ b/integration_tests/ffi_bindings.ts @@ -3,45 +3,26 @@ * used by the Deno integration tests. */ -// FFI symbol definitions required by the tests -export type SerialLib = { - readonly serialOpen: ( - port: Deno.PointerValue, - baudrate: number, - dataBits: number, - parity: number, - stopBits: number, - errorCallback: Deno.PointerValue, - ) => bigint; +export type LoadedLibrary = Deno.DynamicLibrary; +export type SerialLib = LoadedLibrary["symbols"]; - readonly serialClose: ( - handle: bigint, - errorCallback: Deno.PointerValue, - ) => number; - - readonly serialRead: ( - handle: bigint, - buffer: Deno.PointerValue, - bufferSize: number, - timeoutMs: number, - multiplier: number, - errorCallback: Deno.PointerValue, - ) => number; - - readonly serialWrite: ( - handle: bigint, - buffer: Deno.PointerValue, - bufferSize: number, - timeoutMs: number, - multiplier: number, - errorCallback: Deno.PointerValue, - ) => number; -}; - -// Library object type -export type LoadedLibrary = { - symbols: SerialLib; - close: () => void; +const symbols = { + serialOpen: { + parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"] as const, + result: "i64" as const, + }, + serialClose: { + parameters: ["i64", "pointer"] as const, + result: "i32" as const, + }, + serialRead: { + parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, + result: "i32" as const, + }, + serialWrite: { + parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, + result: "i32" as const, + }, }; /** @@ -66,34 +47,12 @@ export async function loadSerialLib( "/usr/local/lib/libcpp_bindings_linux.so", ].filter((p): p is string => p !== undefined); - let lib: { symbols: SerialLib; close: () => void } | null = null; + let lib: LoadedLibrary | null = null; let lastError: Error | null = null; - const symbols = { - serialOpen: { - parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"] as const, - result: "i64" as const, - }, - serialClose: { - parameters: ["i64", "pointer"] as const, - result: "i32" as const, - }, - serialRead: { - parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, - result: "i32" as const, - }, - serialWrite: { - parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, - result: "i32" as const, - }, - }; - for (const path of possiblePaths) { try { - const loaded = Deno.dlopen(path, symbols) as { - symbols: SerialLib; - close: () => void; - }; + const loaded = Deno.dlopen(path, symbols) as LoadedLibrary; lib = loaded; break; } catch (error) { From 9814c30c4d76ea098716d54a91fbb97eac95be46 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 19:04:14 +0100 Subject: [PATCH 22/32] Add JSR package support for Linux shared library - Introduced a new GitHub Actions workflow for publishing the native Linux shared library as a JSON/base64 blob to JSR. - Added scripts for embedding the shared library and generating the corresponding JSON metadata. - Created TypeScript bindings for the shared library, enhancing usability and type safety. - Updated CMake configuration to generate JSR package metadata during the build process. - Included a README for usage instructions and permissions required for the library. --- .github/workflows/publish-jsr.yml | 59 +++++++++++++++ CMakeLists.txt | 8 ++ jsr/.gitignore | 1 + jsr/README.md | 31 ++++++++ jsr/binaries/linux-x86_64.json | 9 +++ jsr/ffi.ts | 55 ++++++++++++++ jsr/jsr.json.in | 22 ++++++ jsr/mod.ts | 122 ++++++++++++++++++++++++++++++ jsr/scripts/embed_so.ts | 51 +++++++++++++ 9 files changed, 358 insertions(+) create mode 100644 .github/workflows/publish-jsr.yml create mode 100644 jsr/.gitignore create mode 100644 jsr/README.md create mode 100644 jsr/binaries/linux-x86_64.json create mode 100644 jsr/ffi.ts create mode 100644 jsr/jsr.json.in create mode 100644 jsr/mod.ts create mode 100644 jsr/scripts/embed_so.ts diff --git a/.github/workflows/publish-jsr.yml b/.github/workflows/publish-jsr.yml new file mode 100644 index 0000000..7318e37 --- /dev/null +++ b/.github/workflows/publish-jsr.yml @@ -0,0 +1,59 @@ +name: Publish to JSR (@serial/cpp-bindings-linux) + +on: + workflow_dispatch: + inputs: {} + push: + tags: + - "v*" + +jobs: + publish-jsr: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install build dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends ninja-build g++ git ca-certificates + + - name: Setup CMake >= 3.30 + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: "3.31.x" + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: "2.6.0" + + - name: Configure CMake (Release) + env: + CXX: g++ + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build shared library + run: | + cmake --build build --config Release + + - name: Embed .so as JSON/base64 for JSR + run: | + deno run --allow-read --allow-write jsr/scripts/embed_so.ts \ + build/libcpp_bindings_linux.so \ + jsr/binaries/linux-x86_64.json \ + linux-x86_64 + + - name: Publish package to JSR + working-directory: jsr + run: | + deno publish + + diff --git a/CMakeLists.txt b/CMakeLists.txt index 39ff558..ac4b5ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,14 @@ project( LANGUAGES CXX ) +# Generate JSR package metadata from the same git-derived version as the library. +# We generate into the build directory to avoid touching tracked files during normal local builds. +configure_file( + "${CMAKE_SOURCE_DIR}/jsr/jsr.json.in" + "${CMAKE_SOURCE_DIR}/jsr/jsr.json" + @ONLY +) + # Set C++ standard set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/jsr/.gitignore b/jsr/.gitignore new file mode 100644 index 0000000..d062c58 --- /dev/null +++ b/jsr/.gitignore @@ -0,0 +1 @@ +jsr.json diff --git a/jsr/README.md b/jsr/README.md new file mode 100644 index 0000000..b8c6dbc --- /dev/null +++ b/jsr/README.md @@ -0,0 +1,31 @@ +# @serial/cpp-bindings-linux + +JSR package that ships the native Linux shared library +(`libcpp_bindings_linux.so`) as a **JSON/base64 blob** and reconstructs it +on-demand, because JSR currently doesn't handle binaries as first-class +artifacts. + +## Usage + +```ts +import { createErrorCallback, loadSerialLib } from "@serial/cpp-bindings-linux"; + +const { pointer: errPtr, close: closeErr } = createErrorCallback( + (code, msg) => { + console.error("native error", code, msg); + }, +); + +const lib = await loadSerialLib(); +// lib.symbols.serialOpen(...) etc. + +// cleanup +closeErr(); +lib.close(); +``` + +## Permissions + +- `--allow-ffi` +- `--allow-read` +- `--allow-write` (only needed if you use the embedded binary extraction path) diff --git a/jsr/binaries/linux-x86_64.json b/jsr/binaries/linux-x86_64.json new file mode 100644 index 0000000..db1bbc6 --- /dev/null +++ b/jsr/binaries/linux-x86_64.json @@ -0,0 +1,9 @@ +{ + "target": "linux-x86_64", + "filename": "libcpp_bindings_linux.so", + "encoding": "base64", + "sha256": "", + "data": "" +} + + diff --git a/jsr/ffi.ts b/jsr/ffi.ts new file mode 100644 index 0000000..34567d4 --- /dev/null +++ b/jsr/ffi.ts @@ -0,0 +1,55 @@ +export const symbols = { + serialOpen: { + parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"] as const, + result: "i64" as const, + }, + serialClose: { + parameters: ["i64", "pointer"] as const, + result: "i32" as const, + }, + serialRead: { + parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, + result: "i32" as const, + }, + serialWrite: { + parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, + result: "i32" as const, + }, +}; + +export type LoadedLibrary = Deno.DynamicLibrary; +export type SerialLib = LoadedLibrary["symbols"]; + +export type ErrorCallback = (code: number, message: string) => void; + +/** + * Creates a C-callable callback pointer compatible with `ErrorCallbackT` in the .so + * (signature: `void (*)(int code, const char* message)`). + * + * Remember to `close()` it when you're done. + */ +export function createErrorCallback(cb: ErrorCallback): { + pointer: Deno.PointerValue; + close: () => void; +} { + const callback = new Deno.UnsafeCallback( + { parameters: ["i32", "pointer"], result: "void" } as const, + (code: number, messagePtr: Deno.PointerValue) => { + let message = ""; + try { + if (messagePtr) { + message = new Deno.UnsafePointerView(messagePtr) + .getCString(); + } + } catch { + // best-effort: ignore malformed pointers + } + cb(code, message); + }, + ); + + return { + pointer: callback.pointer, + close: () => callback.close(), + }; +} diff --git a/jsr/jsr.json.in b/jsr/jsr.json.in new file mode 100644 index 0000000..4050a9f --- /dev/null +++ b/jsr/jsr.json.in @@ -0,0 +1,22 @@ +{ + "name": "@serial/cpp-bindings-linux", + "version": "@PROJECT_VERSION@", + "description": "Linux shared-library bindings for Serial-IO/cpp-core (distributed via JSON/base64 because JSR has limited binary support).", + "exports": { + ".": "./mod.ts" + }, + "publish": { + "include": [ + "README.md", + "jsr.json", + "mod.ts", + "ffi.ts", + "binaries/**" + ], + "exclude": [ + "scripts/**" + ] + } +} + + diff --git a/jsr/mod.ts b/jsr/mod.ts new file mode 100644 index 0000000..3cda19e --- /dev/null +++ b/jsr/mod.ts @@ -0,0 +1,122 @@ +import { + createErrorCallback, + type LoadedLibrary, + type SerialLib, + symbols, +} from "./ffi.ts"; + +type EmbeddedBinary = { + target: string; + filename: string; + encoding: "base64"; + sha256?: string; + data: string; +}; + +let extractedLibraryPath: string | null = null; + +function base64ToBytes(base64: string): Uint8Array { + const bin = atob(base64); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} + +async function loadEmbeddedBinary(): Promise { + const os = Deno.build.os; + const arch = Deno.build.arch; + const target = `${os}-${arch}`; + + if (target !== "linux-x86_64") { + throw new Error( + `@serial/cpp-bindings-linux only ships an embedded binary for linux-x86_64 right now (got ${target}).`, + ); + } + + // Keep this as a dynamic import so local dev builds don't require a real binary JSON. + const mod = await import("./binaries/linux-x86_64.json", { + with: { type: "json" }, + }); + return mod.default as EmbeddedBinary; +} + +export type EnsureLibraryOptions = { + /** + * Directory to write the extracted .so into. + * Defaults to a temporary directory. + */ + dir?: string; + /** + * If true, re-write the file even if we already extracted it in this process. + */ + force?: boolean; +}; + +/** + * Extract the embedded `.so` (stored as JSON/base64) to a real file on disk and return its path. + * + * Required permissions: + * - `--allow-read` (to read the embedded JSON in the package) + * - `--allow-write` (to write the `.so` to disk) + */ +export async function ensureSharedLibraryFile( + options: EnsureLibraryOptions = {}, +): Promise { + if (extractedLibraryPath && !options.force) return extractedLibraryPath; + + const embedded = await loadEmbeddedBinary(); + if (!embedded.data) { + throw new Error( + "Embedded binary JSON is empty. If you're running from source, generate it via jsr/scripts/embed_so.ts.", + ); + } + + const dir = options.dir ?? + await Deno.makeTempDir({ prefix: "serial-cpp-bindings-linux-" }); + const path = `${dir}/${embedded.filename}`; + const bytes = base64ToBytes(embedded.data); + + await Deno.writeFile(path, bytes, { mode: 0o755 }); + extractedLibraryPath = path; + return path; +} + +export type LoadSerialLibOptions = { + /** + * If provided, skips extraction and loads the .so from this path. + */ + libraryPath?: string; + /** + * Directory to write the extracted .so into (if `libraryPath` is not provided). + */ + extractDir?: string; + /** + * Force re-extract (useful if you manage the directory yourself). + */ + forceExtract?: boolean; +}; + +/** + * Load the native library via `Deno.dlopen`. + * + * Required permissions: + * - `--allow-ffi` + * - `--allow-read` (to load the .so) + * - `--allow-write` (only if extracting the embedded binary) + */ +export async function loadSerialLib( + options: LoadSerialLibOptions = {}, +): Promise { + await Promise.resolve(); // keep async for API stability + + const path = options.libraryPath ?? + await ensureSharedLibraryFile({ + dir: options.extractDir, + force: options.forceExtract, + }); + + return Deno.dlopen(path, symbols) as LoadedLibrary; +} + +export { createErrorCallback, symbols }; +export type { LoadedLibrary, SerialLib }; diff --git a/jsr/scripts/embed_so.ts b/jsr/scripts/embed_so.ts new file mode 100644 index 0000000..131a7c8 --- /dev/null +++ b/jsr/scripts/embed_so.ts @@ -0,0 +1,51 @@ +// Usage: +// deno run --allow-read --allow-write jsr/scripts/embed_so.ts \ +// ./build/libcpp_bindings_linux.so ./jsr/binaries/linux-x86_64.json linux-x86_64 +// +// This converts the shared library into a JSON file containing base64 data for publishing to JSR. + +function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); +} + +function base64FromBytes(bytes: Uint8Array): string { + // Chunk to avoid call stack limits in String.fromCharCode(...bigArray) + const chunkSize = 0x8000; + let binary = ""; + for (let i = 0; i < bytes.length; i += chunkSize) { + const chunk = bytes.subarray(i, i + chunkSize); + binary += String.fromCharCode(...chunk); + } + return btoa(binary); +} + +if (import.meta.main) { + const [inPath, outPath, target = "linux-x86_64"] = Deno.args; + if (!inPath || !outPath) { + console.error( + "Expected: [target]\nExample: build/libcpp_bindings_linux.so jsr/binaries/linux-x86_64.json linux-x86_64", + ); + Deno.exit(2); + } + + const bytes = await Deno.readFile(inPath); + const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); + const sha256 = bytesToHex(digest); + + const filename = outPath.endsWith(".json") + ? (target === "linux-x86_64" + ? "libcpp_bindings_linux.so" + : "libcpp_bindings_linux.so") + : "libcpp_bindings_linux.so"; + + const payload = { + target, + filename, + encoding: "base64" as const, + sha256, + data: base64FromBytes(bytes), + }; + + await Deno.writeTextFile(outPath, JSON.stringify(payload)); + console.log(`Wrote ${outPath} (${bytes.length} bytes, sha256=${sha256})`); +} From 5c9ffab628472a9c60c7228efa70a932615f2469 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 19:29:05 +0100 Subject: [PATCH 23/32] Update CMakeLists.txt to bump version tags for dependencies - Updated cmake_git_versioning GIT_TAG from v1.0.0 to v1.0.1 for the latest features and fixes. - Changed cpp_core GIT_TAG from main to bump-version-from-git-versioning to align with versioning strategy. --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ac4b5ea..5bc2ad1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ include(cmake/CPM.cmake) CPMAddPackage( NAME cmake_git_versioning GITHUB_REPOSITORY Katze719/cmake-git-versioning - GIT_TAG v1.0.0 + GIT_TAG v1.0.1 ) # Include cmake-git-versioning module @@ -43,7 +43,7 @@ set(CMAKE_CXX_MODULE_EXTENSIONS OFF) CPMAddPackage( NAME cpp_core GITHUB_REPOSITORY Serial-IO/cpp-core - GIT_TAG main + GIT_TAG bump-version-from-git-versioning OPTIONS "CMAKE_EXPORT_COMPILE_COMMANDS OFF" ) From e02830fe4ee0e65765a23c3eed152399fd0051bd Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 19:32:48 +0100 Subject: [PATCH 24/32] Add permissions to GitHub Actions workflow for JSR publishing - Updated the publish-jsr.yml workflow to include permissions for reading contents and writing ID tokens, enhancing security and functionality during the publishing process. --- .github/workflows/publish-jsr.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/publish-jsr.yml b/.github/workflows/publish-jsr.yml index 7318e37..12e3f1a 100644 --- a/.github/workflows/publish-jsr.yml +++ b/.github/workflows/publish-jsr.yml @@ -7,6 +7,10 @@ on: tags: - "v*" +permissions: + contents: read + id-token: write + jobs: publish-jsr: runs-on: ubuntu-latest From 25f6fce781834a5568b806fb4a7612f927572981 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 19:39:32 +0100 Subject: [PATCH 25/32] Add LGPL-3.0-only license and update package metadata - Introduced a new LICENSE file containing the GNU Lesser General Public License v3.0. - Updated jsr.json.in to specify the license as LGPL-3.0-only. - Added a license section in the README.md to inform users about the licensing terms. --- LICENSE | 298 ++++++++++++++++++++++++++++++++++++++++++++++++ jsr/README.md | 5 + jsr/jsr.json.in | 1 + 3 files changed, 304 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fd8cd28 --- /dev/null +++ b/LICENSE @@ -0,0 +1,298 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version of + the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is necessary + to install and execute a modified version of the Combined Work + produced by recombining or relinking the Application with a modified + version of the Linked Version. (If you use option 4d0, the + Installation Information must accompany the Minimal Corresponding + Source and Corresponding Application Code. If you use option 4d1, you + must provide the Installation Information in the manner specified by + section 6 of the GNU GPL for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + + 7. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 8. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or run +a copy of the Library. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + 11. Patents. + + A contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange to extend the +patent license to downstream recipients. "Knowingly relying" means you +have actual knowledge that, but for the patent license, your conveying +the covered work in a country, or your recipient's use of the covered +work in a country, would infringe one or more identifiable patents in +that country. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + If you convey a covered work, you may not impose any further +restrictions on the exercise of the rights granted or affirmed under +this License. For example, you may not impose a license fee, royalty, +or other charge for exercise of rights granted under this License, and +you may not initiate litigation (including a cross-claim or +counterclaim in a lawsuit) alleging that any patent claim is infringed +by making, using, selling, offering for sale, or importing the Program +or any portion of it. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combined work as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Lesser General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail +to address new problems or concerns. + + Each version is given a distinguishing version number. If the Library as +you received it specifies that a certain numbered version of the GNU Lesser +General Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that published +version or of any later version published by the Free Software Foundation. +If the Library as you received it does not specify a version number of the +GNU Lesser General Public License, you may choose any version of the GNU +Lesser General Public License ever published by the Free Software +Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the Library. + + END OF TERMS AND CONDITIONS + + diff --git a/jsr/README.md b/jsr/README.md index b8c6dbc..a1cbc21 100644 --- a/jsr/README.md +++ b/jsr/README.md @@ -29,3 +29,8 @@ lib.close(); - `--allow-ffi` - `--allow-read` - `--allow-write` (only needed if you use the embedded binary extraction path) + +## License + +This package is licensed under **LGPL-3.0-only** (see the repository root +`LICENSE`). diff --git a/jsr/jsr.json.in b/jsr/jsr.json.in index 4050a9f..154106f 100644 --- a/jsr/jsr.json.in +++ b/jsr/jsr.json.in @@ -1,6 +1,7 @@ { "name": "@serial/cpp-bindings-linux", "version": "@PROJECT_VERSION@", + "license": "LGPL-3.0-only", "description": "Linux shared-library bindings for Serial-IO/cpp-core (distributed via JSON/base64 because JSR has limited binary support).", "exports": { ".": "./mod.ts" From fa64e32606924ceabff92c2627635ba429187800 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 19:42:39 +0100 Subject: [PATCH 26/32] Update JSR publishing workflow to allow dirty worktree during package publishing - Modified the publish-jsr.yml workflow to include the --allow-dirty flag for deno publish, enabling the publishing process to proceed even when the worktree is dirty. - Added comments to clarify the impact of CMake and embed_so.ts on the worktree state, ensuring deterministic builds and generation in a single run. --- .github/workflows/publish-jsr.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-jsr.yml b/.github/workflows/publish-jsr.yml index 12e3f1a..6702d1e 100644 --- a/.github/workflows/publish-jsr.yml +++ b/.github/workflows/publish-jsr.yml @@ -58,6 +58,9 @@ jobs: - name: Publish package to JSR working-directory: jsr run: | - deno publish + # CMake + embed_so.ts generate files (jsr/jsr.json + binaries/*.json), + # which makes the worktree dirty. Publishing is still deterministic because + # the workflow builds + generates in a single run. + deno publish --allow-dirty From 50a398904a328533e1e5876eaf8492a985b2641b Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 22:08:14 +0100 Subject: [PATCH 27/32] Refactor JSR package structure and remove FFI bindings - Updated the JSR package to use a new directory structure for binaries, changing the path from `jsr/binaries/linux-x86_64.json` to `jsr/bin/linux-x86_64.json`. - Deleted the `ffi.ts` and `mod.ts` files, removing the TypeScript bindings for the shared library. - Updated the README.md to reflect the new usage instructions for importing the JSON and writing the shared library to disk. - Added a new `linux-x86_64.json` file to store the shared library metadata. --- .github/workflows/publish-jsr.yml | 2 +- jsr/README.md | 51 ++++++---- jsr/{binaries => bin}/linux-x86_64.json | 2 - jsr/ffi.ts | 55 ----------- jsr/jsr.json.in | 6 +- jsr/mod.ts | 122 ------------------------ jsr/scripts/embed_so.ts | 4 +- 7 files changed, 36 insertions(+), 206 deletions(-) rename jsr/{binaries => bin}/linux-x86_64.json (98%) delete mode 100644 jsr/ffi.ts delete mode 100644 jsr/mod.ts diff --git a/.github/workflows/publish-jsr.yml b/.github/workflows/publish-jsr.yml index 6702d1e..37bccac 100644 --- a/.github/workflows/publish-jsr.yml +++ b/.github/workflows/publish-jsr.yml @@ -52,7 +52,7 @@ jobs: run: | deno run --allow-read --allow-write jsr/scripts/embed_so.ts \ build/libcpp_bindings_linux.so \ - jsr/binaries/linux-x86_64.json \ + jsr/bin/linux-x86_64.json \ linux-x86_64 - name: Publish package to JSR diff --git a/jsr/README.md b/jsr/README.md index a1cbc21..92e2511 100644 --- a/jsr/README.md +++ b/jsr/README.md @@ -1,34 +1,45 @@ # @serial/cpp-bindings-linux -JSR package that ships the native Linux shared library -(`libcpp_bindings_linux.so`) as a **JSON/base64 blob** and reconstructs it -on-demand, because JSR currently doesn't handle binaries as first-class -artifacts. +JSR package that ships the native Linux shared library payload as a +**JSON/base64 blob** because JSR currently doesn't handle binaries as +first-class artifacts. ## Usage -```ts -import { createErrorCallback, loadSerialLib } from "@serial/cpp-bindings-linux"; - -const { pointer: errPtr, close: closeErr } = createErrorCallback( - (code, msg) => { - console.error("native error", code, msg); - }, -); +Import the JSON and write the `.so` to disk (consumer project example): -const lib = await loadSerialLib(); -// lib.symbols.serialOpen(...) etc. - -// cleanup -closeErr(); -lib.close(); +```ts +import blob from "@serial/cpp-bindings-linux/linux-x86_64" with { + type: "json", +}; + +// decode base64 -> bytes +const bin = atob(blob.data); +const bytes = new Uint8Array(bin.length); +for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + +// write library file +const outPath = "./libcpp_bindings_linux.so"; +await Deno.writeFile(outPath, bytes, { mode: 0o755 }); + +// optional: verify sha256 if present +if (blob.sha256) { + const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); + const hex = Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join( + "", + ); + if (hex !== blob.sha256) { + throw new Error(`sha256 mismatch: ${hex} != ${blob.sha256}`); + } +} + +// ... now your other project can dlopen(outPath) / FFI it as needed. ``` ## Permissions -- `--allow-ffi` - `--allow-read` -- `--allow-write` (only needed if you use the embedded binary extraction path) +- `--allow-write` ## License diff --git a/jsr/binaries/linux-x86_64.json b/jsr/bin/linux-x86_64.json similarity index 98% rename from jsr/binaries/linux-x86_64.json rename to jsr/bin/linux-x86_64.json index db1bbc6..4184085 100644 --- a/jsr/binaries/linux-x86_64.json +++ b/jsr/bin/linux-x86_64.json @@ -5,5 +5,3 @@ "sha256": "", "data": "" } - - diff --git a/jsr/ffi.ts b/jsr/ffi.ts deleted file mode 100644 index 34567d4..0000000 --- a/jsr/ffi.ts +++ /dev/null @@ -1,55 +0,0 @@ -export const symbols = { - serialOpen: { - parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"] as const, - result: "i64" as const, - }, - serialClose: { - parameters: ["i64", "pointer"] as const, - result: "i32" as const, - }, - serialRead: { - parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, - result: "i32" as const, - }, - serialWrite: { - parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, - result: "i32" as const, - }, -}; - -export type LoadedLibrary = Deno.DynamicLibrary; -export type SerialLib = LoadedLibrary["symbols"]; - -export type ErrorCallback = (code: number, message: string) => void; - -/** - * Creates a C-callable callback pointer compatible with `ErrorCallbackT` in the .so - * (signature: `void (*)(int code, const char* message)`). - * - * Remember to `close()` it when you're done. - */ -export function createErrorCallback(cb: ErrorCallback): { - pointer: Deno.PointerValue; - close: () => void; -} { - const callback = new Deno.UnsafeCallback( - { parameters: ["i32", "pointer"], result: "void" } as const, - (code: number, messagePtr: Deno.PointerValue) => { - let message = ""; - try { - if (messagePtr) { - message = new Deno.UnsafePointerView(messagePtr) - .getCString(); - } - } catch { - // best-effort: ignore malformed pointers - } - cb(code, message); - }, - ); - - return { - pointer: callback.pointer, - close: () => callback.close(), - }; -} diff --git a/jsr/jsr.json.in b/jsr/jsr.json.in index 154106f..cacf289 100644 --- a/jsr/jsr.json.in +++ b/jsr/jsr.json.in @@ -4,15 +4,13 @@ "license": "LGPL-3.0-only", "description": "Linux shared-library bindings for Serial-IO/cpp-core (distributed via JSON/base64 because JSR has limited binary support).", "exports": { - ".": "./mod.ts" + "./linux-x86_64": "./bin/linux-x86_64.json" }, "publish": { "include": [ "README.md", "jsr.json", - "mod.ts", - "ffi.ts", - "binaries/**" + "bin/**" ], "exclude": [ "scripts/**" diff --git a/jsr/mod.ts b/jsr/mod.ts deleted file mode 100644 index 3cda19e..0000000 --- a/jsr/mod.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { - createErrorCallback, - type LoadedLibrary, - type SerialLib, - symbols, -} from "./ffi.ts"; - -type EmbeddedBinary = { - target: string; - filename: string; - encoding: "base64"; - sha256?: string; - data: string; -}; - -let extractedLibraryPath: string | null = null; - -function base64ToBytes(base64: string): Uint8Array { - const bin = atob(base64); - const out = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); - return out; -} - -async function loadEmbeddedBinary(): Promise { - const os = Deno.build.os; - const arch = Deno.build.arch; - const target = `${os}-${arch}`; - - if (target !== "linux-x86_64") { - throw new Error( - `@serial/cpp-bindings-linux only ships an embedded binary for linux-x86_64 right now (got ${target}).`, - ); - } - - // Keep this as a dynamic import so local dev builds don't require a real binary JSON. - const mod = await import("./binaries/linux-x86_64.json", { - with: { type: "json" }, - }); - return mod.default as EmbeddedBinary; -} - -export type EnsureLibraryOptions = { - /** - * Directory to write the extracted .so into. - * Defaults to a temporary directory. - */ - dir?: string; - /** - * If true, re-write the file even if we already extracted it in this process. - */ - force?: boolean; -}; - -/** - * Extract the embedded `.so` (stored as JSON/base64) to a real file on disk and return its path. - * - * Required permissions: - * - `--allow-read` (to read the embedded JSON in the package) - * - `--allow-write` (to write the `.so` to disk) - */ -export async function ensureSharedLibraryFile( - options: EnsureLibraryOptions = {}, -): Promise { - if (extractedLibraryPath && !options.force) return extractedLibraryPath; - - const embedded = await loadEmbeddedBinary(); - if (!embedded.data) { - throw new Error( - "Embedded binary JSON is empty. If you're running from source, generate it via jsr/scripts/embed_so.ts.", - ); - } - - const dir = options.dir ?? - await Deno.makeTempDir({ prefix: "serial-cpp-bindings-linux-" }); - const path = `${dir}/${embedded.filename}`; - const bytes = base64ToBytes(embedded.data); - - await Deno.writeFile(path, bytes, { mode: 0o755 }); - extractedLibraryPath = path; - return path; -} - -export type LoadSerialLibOptions = { - /** - * If provided, skips extraction and loads the .so from this path. - */ - libraryPath?: string; - /** - * Directory to write the extracted .so into (if `libraryPath` is not provided). - */ - extractDir?: string; - /** - * Force re-extract (useful if you manage the directory yourself). - */ - forceExtract?: boolean; -}; - -/** - * Load the native library via `Deno.dlopen`. - * - * Required permissions: - * - `--allow-ffi` - * - `--allow-read` (to load the .so) - * - `--allow-write` (only if extracting the embedded binary) - */ -export async function loadSerialLib( - options: LoadSerialLibOptions = {}, -): Promise { - await Promise.resolve(); // keep async for API stability - - const path = options.libraryPath ?? - await ensureSharedLibraryFile({ - dir: options.extractDir, - force: options.forceExtract, - }); - - return Deno.dlopen(path, symbols) as LoadedLibrary; -} - -export { createErrorCallback, symbols }; -export type { LoadedLibrary, SerialLib }; diff --git a/jsr/scripts/embed_so.ts b/jsr/scripts/embed_so.ts index 131a7c8..ff3ad3a 100644 --- a/jsr/scripts/embed_so.ts +++ b/jsr/scripts/embed_so.ts @@ -1,6 +1,6 @@ // Usage: // deno run --allow-read --allow-write jsr/scripts/embed_so.ts \ -// ./build/libcpp_bindings_linux.so ./jsr/binaries/linux-x86_64.json linux-x86_64 +// ./build/libcpp_bindings_linux.so ./jsr/bin/linux-x86_64.json linux-x86_64 // // This converts the shared library into a JSON file containing base64 data for publishing to JSR. @@ -23,7 +23,7 @@ if (import.meta.main) { const [inPath, outPath, target = "linux-x86_64"] = Deno.args; if (!inPath || !outPath) { console.error( - "Expected: [target]\nExample: build/libcpp_bindings_linux.so jsr/binaries/linux-x86_64.json linux-x86_64", + "Expected: [target]\nExample: build/libcpp_bindings_linux.so jsr/bin/linux-x86_64.json linux-x86_64", ); Deno.exit(2); } From f74b2338d6c43e1f33d93e4af71cb53a7afa54ba Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 22:12:55 +0100 Subject: [PATCH 28/32] Update JSR package to use new x84_64 naming convention for binaries - Changed references from `linux-x86_64` to `x84_64` in the JSR package configuration and scripts. - Updated the JSON metadata file to reflect the new naming and added the new `x84_64.json` file. - Modified the README.md and embed_so.ts script to align with the new import path for the shared library. --- .github/workflows/publish-jsr.yml | 2 +- jsr/README.md | 2 +- jsr/bin/{linux-x86_64.json => x84_64.json} | 0 jsr/jsr.json.in | 2 +- jsr/scripts/embed_so.ts | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename jsr/bin/{linux-x86_64.json => x84_64.json} (100%) diff --git a/.github/workflows/publish-jsr.yml b/.github/workflows/publish-jsr.yml index 37bccac..63df8cc 100644 --- a/.github/workflows/publish-jsr.yml +++ b/.github/workflows/publish-jsr.yml @@ -52,7 +52,7 @@ jobs: run: | deno run --allow-read --allow-write jsr/scripts/embed_so.ts \ build/libcpp_bindings_linux.so \ - jsr/bin/linux-x86_64.json \ + jsr/bin/x84_64.json \ linux-x86_64 - name: Publish package to JSR diff --git a/jsr/README.md b/jsr/README.md index 92e2511..78e71c1 100644 --- a/jsr/README.md +++ b/jsr/README.md @@ -9,7 +9,7 @@ first-class artifacts. Import the JSON and write the `.so` to disk (consumer project example): ```ts -import blob from "@serial/cpp-bindings-linux/linux-x86_64" with { +import blob from "@serial/cpp-bindings-linux/x84_64" with { type: "json", }; diff --git a/jsr/bin/linux-x86_64.json b/jsr/bin/x84_64.json similarity index 100% rename from jsr/bin/linux-x86_64.json rename to jsr/bin/x84_64.json diff --git a/jsr/jsr.json.in b/jsr/jsr.json.in index cacf289..ccb3095 100644 --- a/jsr/jsr.json.in +++ b/jsr/jsr.json.in @@ -4,7 +4,7 @@ "license": "LGPL-3.0-only", "description": "Linux shared-library bindings for Serial-IO/cpp-core (distributed via JSON/base64 because JSR has limited binary support).", "exports": { - "./linux-x86_64": "./bin/linux-x86_64.json" + "./x84_64": "./bin/x84_64.json" }, "publish": { "include": [ diff --git a/jsr/scripts/embed_so.ts b/jsr/scripts/embed_so.ts index ff3ad3a..81cdafe 100644 --- a/jsr/scripts/embed_so.ts +++ b/jsr/scripts/embed_so.ts @@ -1,6 +1,6 @@ // Usage: // deno run --allow-read --allow-write jsr/scripts/embed_so.ts \ -// ./build/libcpp_bindings_linux.so ./jsr/bin/linux-x86_64.json linux-x86_64 +// ./build/libcpp_bindings_linux.so ./jsr/bin/x84_64.json linux-x86_64 // // This converts the shared library into a JSON file containing base64 data for publishing to JSR. @@ -23,7 +23,7 @@ if (import.meta.main) { const [inPath, outPath, target = "linux-x86_64"] = Deno.args; if (!inPath || !outPath) { console.error( - "Expected: [target]\nExample: build/libcpp_bindings_linux.so jsr/bin/linux-x86_64.json linux-x86_64", + "Expected: [target]\nExample: build/libcpp_bindings_linux.so jsr/bin/x84_64.json linux-x86_64", ); Deno.exit(2); } From 334e5d9339b449db6084837d36432d1aed3c38f2 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 22:18:55 +0100 Subject: [PATCH 29/32] Remove exclusion of scripts from JSR package configuration in jsr.json.in --- jsr/jsr.json.in | 3 --- 1 file changed, 3 deletions(-) diff --git a/jsr/jsr.json.in b/jsr/jsr.json.in index ccb3095..3e8c0b5 100644 --- a/jsr/jsr.json.in +++ b/jsr/jsr.json.in @@ -11,9 +11,6 @@ "README.md", "jsr.json", "bin/**" - ], - "exclude": [ - "scripts/**" ] } } From a8cbcd170212b8441a69e1cb21aa22fac178a586 Mon Sep 17 00:00:00 2001 From: Katze719 <38188106+Katze719@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:44:54 +0100 Subject: [PATCH 30/32] Update jsr/README.md Co-authored-by: Mqx <62719703+Mqxx@users.noreply.github.com> --- jsr/README.md | 44 +++++++------------------------------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/jsr/README.md b/jsr/README.md index 78e71c1..48b507a 100644 --- a/jsr/README.md +++ b/jsr/README.md @@ -1,47 +1,17 @@ -# @serial/cpp-bindings-linux +# C++ Bindings Linux -JSR package that ships the native Linux shared library payload as a -**JSON/base64 blob** because JSR currently doesn't handle binaries as -first-class artifacts. +This package ships the native Linux shared library payload as a **JSON/base64 blob**. ## Usage Import the JSON and write the `.so` to disk (consumer project example): ```ts -import blob from "@serial/cpp-bindings-linux/x84_64" with { - type: "json", -}; +import blob from "@serial/cpp-bindings-linux/x84_64" with {type: 'json'}; -// decode base64 -> bytes -const bin = atob(blob.data); -const bytes = new Uint8Array(bin.length); -for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); +const bytes = new TextEncoder().encode(atob(blob.data)); -// write library file -const outPath = "./libcpp_bindings_linux.so"; -await Deno.writeFile(outPath, bytes, { mode: 0o755 }); +const tempFilePath = Deno.makeTempFileSync(); +Deno.writeFileSync(tempFilePath, bytes, { mode: 0o755 }); -// optional: verify sha256 if present -if (blob.sha256) { - const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); - const hex = Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join( - "", - ); - if (hex !== blob.sha256) { - throw new Error(`sha256 mismatch: ${hex} != ${blob.sha256}`); - } -} - -// ... now your other project can dlopen(outPath) / FFI it as needed. -``` - -## Permissions - -- `--allow-read` -- `--allow-write` - -## License - -This package is licensed under **LGPL-3.0-only** (see the repository root -`LICENSE`). +// Now you can open the binary using for example `Deno.dlopen` From e1aac125ed77e17d9033888f2a43d957a52c3f35 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 22:48:55 +0100 Subject: [PATCH 31/32] Update README.md to improve formatting and add license section - Adjusted formatting of the JSON/base64 blob description for clarity. - Added a new section to specify the package license as LGPL-3.0-only. --- jsr/README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/jsr/README.md b/jsr/README.md index 48b507a..34ec660 100644 --- a/jsr/README.md +++ b/jsr/README.md @@ -1,13 +1,14 @@ # C++ Bindings Linux -This package ships the native Linux shared library payload as a **JSON/base64 blob**. +This package ships the native Linux shared library payload as a **JSON/base64 +blob**. ## Usage Import the JSON and write the `.so` to disk (consumer project example): ```ts -import blob from "@serial/cpp-bindings-linux/x84_64" with {type: 'json'}; +import blob from "@serial/cpp-bindings-linux/x84_64" with { type: "json" }; const bytes = new TextEncoder().encode(atob(blob.data)); @@ -15,3 +16,9 @@ const tempFilePath = Deno.makeTempFileSync(); Deno.writeFileSync(tempFilePath, bytes, { mode: 0o755 }); // Now you can open the binary using for example `Deno.dlopen` +``` + +## License + +This package is licensed under **LGPL-3.0-only** (see the repository root +`LICENSE`). From daf91685a74bf4dd8b8a4ca02509fb4f185556f7 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Tue, 16 Dec 2025 22:51:07 +0100 Subject: [PATCH 32/32] Update CMakeLists.txt to change cpp_core GIT_TAG from bump-version-from-git-versioning to main --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5bc2ad1..292fb97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,7 +43,7 @@ set(CMAKE_CXX_MODULE_EXTENSIONS OFF) CPMAddPackage( NAME cpp_core GITHUB_REPOSITORY Serial-IO/cpp-core - GIT_TAG bump-version-from-git-versioning + GIT_TAG main OPTIONS "CMAKE_EXPORT_COMPILE_COMMANDS OFF" )