diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 000000000..d3295e729 --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1 @@ +src/Debug/debug.proto diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 1825cff8d..e56735011 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -36,6 +36,13 @@ jobs: - name: Create build folder run: mkdir build-emu + - name: Set up Homebrew + if: ${{ ! startsWith(matrix.os, 'macos') }} + uses: Homebrew/actions/setup-homebrew@main + + - name: Install protoc/nanopb + run: brew install nanopb + - name: Build WARDuino CLI run: cmake .. -D BUILD_EMULATOR=ON ; cmake --build . working-directory: build-emu diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8994a650b..f1cd0a388 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,6 +44,12 @@ jobs: with: node-version: 20 + - name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@main + + - name: Install protoc/nanopb + run: brew install nanopb + - name: Build warduino cli run: | cmake . -D BUILD_EMULATOR=ON @@ -99,6 +105,12 @@ jobs: with: node-version: 20 + - name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@main + + - name: Install protoc/nanopb + run: brew install nanopb + - name: Build warduino cli run: | cmake . -D BUILD_EMULATOR=ON @@ -152,6 +164,12 @@ jobs: with: node-version: 20 + - name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@main + + - name: Install protoc/nanopb + run: brew install nanopb + - name: Build warduino cli run: | cmake . -D BUILD_EMULATOR=ON diff --git a/.gitignore b/.gitignore index 0e6a68346..f6cfe209a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ .idea/ +.air/ .vscode/ .ccls +.cache + *.bin *.ipch *.o @@ -35,3 +38,4 @@ core venv *.wasm + diff --git a/CMakeLists.txt b/CMakeLists.txt index e560ba033..360b33bff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,10 +36,26 @@ list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") set(WARDUINO_VERSION_STRING "${PROJECT_VERSION}") configure_file(src/config.h.in include/warduino/config.h) +# Both host targets use one generated nanopb schema target. +if (BUILD_EMULATOR OR BUILD_UNITTEST) + list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib) + include(FetchContent) + FetchContent_Declare( + nanopb + GIT_REPOSITORY https://github.com/nanopb/nanopb.git + GIT_TAG 0.4.9.1 + GIT_SHALLOW TRUE + ) + FetchContent_MakeAvailable(nanopb) + set(NANOPB_SRC_ROOT_FOLDER ${nanopb_SOURCE_DIR}) + find_program(NANOPB_GENERATOR_PLUGIN NAMES protoc-gen-nanopb REQUIRED) + find_package(Nanopb REQUIRED) + nanopb_generate_cpp(TARGET proto src/Debug/debug.proto) +endif () + # Build the emulator version of WARDuino if (BUILD_EMULATOR) set(EXTERNAL_LIB_HEADERS lib/json/single_include) - find_package(Threads REQUIRED) set(SOURCE_FILES @@ -71,7 +87,7 @@ if (BUILD_EMULATOR) # WARDuino CLI add_executable(wdcli platforms/CLI-Emulator/main.cpp ${SOURCE_FILES}) - target_link_libraries(wdcli PRIVATE Threads::Threads) + target_link_libraries(wdcli PRIVATE Threads::Threads proto) target_include_directories(wdcli PRIVATE ${EXTERNAL_LIB_HEADERS} "${PROJECT_BINARY_DIR}/include") endif (BUILD_EMULATOR) @@ -122,7 +138,7 @@ if (BUILD_UNITTEST) get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE) message(DEBUG "Add executable for " ${TEST_FILE}) add_executable(${TEST_NAME} ${TEST_FILE} ${SOURCE_FILES} ${SHARED_SRC}) - target_link_libraries(${TEST_NAME} PRIVATE doctest::doctest) + target_link_libraries(${TEST_NAME} PRIVATE doctest::doctest proto) target_include_directories(${TEST_NAME} PRIVATE ${EXTERNAL_LIB_HEADERS} "${PROJECT_BINARY_DIR}/include") add_test(${TEST_NAME} ${TEST_NAME}) endforeach () diff --git a/lib/FindNanopb.cmake b/lib/FindNanopb.cmake new file mode 100644 index 000000000..9def7833d --- /dev/null +++ b/lib/FindNanopb.cmake @@ -0,0 +1,482 @@ +# This is an example script for use with CMake projects for locating and configuring +# the nanopb library. +# +# The following variables can be set and are optional: +# +# +# PROTOBUF_SRC_ROOT_FOLDER - When compiling with MSVC, if this cache variable is set +# the protobuf-default VS project build locations +# (vsprojects/Debug & vsprojects/Release) will be searched +# for libraries and binaries. +# +# NANOPB_IMPORT_DIRS - List of additional directories to be searched for +# imported .proto files. +# +# NANOPB_OPTIONS - List of options passed to nanopb. +# +# Nanopb_FIND_COMPONENTS - List of options to append to NANOPB_OPTIONS without the +# leading '--'. This should not manually be set, but allows +# passing options to nanopb via find_package. For example, +# 'find_package(Nanopb REQUIRED COMPONENTS cpp-descriptors)' +# is equivalent to setting NANOPB_OPTIONS to --cpp-descriptors. +# +# NANOPB_DEPENDS - List of files to be used as dependencies +# for the generated source and header files. These +# files are not directly passed as options to +# nanopb but rather their directories. +# +# NANOPB_GENERATE_CPP_APPEND_PATH - By default -I will be passed to protoc +# for each directory where a proto file is referenced. +# This causes all output files to go directly +# under build directory, instead of mirroring +# relative paths of source directories. +# Set to FALSE if you want to disable this behaviour. +# PROTOC_OPTIONS - Pass options to protoc executable +# +# Defines the following variables: +# +# NANOPB_FOUND - Found the nanopb library (source&header files, generator tool, protoc compiler tool) +# NANOPB_INCLUDE_DIRS - Include directories for Google Protocol Buffers +# +# The following cache variables are also available to set or use: +# PROTOBUF_PROTOC_EXECUTABLE - The protoc compiler +# NANOPB_GENERATOR_SOURCE_DIR - The nanopb generator source +# +# ==================================================================== +# +# NANOPB_GENERATE_CPP (public function) +# NANOPB_GENERATE_CPP(SRCS HDRS [RELPATH ] +# ...) +# SRCS = Variable to define with autogenerated source files +# HDRS = Variable to define with autogenerated header files +# NANOPB_GENERATE_CPP(TARGET TGT [RELPATH ] +# ...) +# TGT = Name of the static library to create with the autogenerated files +# +# If you want to use relative paths in your import statements use the RELPATH +# option. The argument to RELPATH should be the directory that all the +# imports will be relative to. +# When RELPATH is not specified then all proto files can be imported without +# a path. +# +# +# ==================================================================== +# Example using modern targets: +# +# set(NANOPB_SRC_ROOT_FOLDER "/path/to/nanopb") +# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${NANOPB_SRC_ROOT_FOLDER}/extra) +# find_package( Nanopb REQUIRED ) +# +# NANOPB_GENERATE_CPP(TARGET proto foo.proto) +# +# add_executable(bar bar.cc) +# target_link_libraries(bar proto) +# +# Example with RELPATH: +# Assume we have a layout like: +# .../CMakeLists.txt +# .../bar.cc +# .../proto/ +# .../proto/foo.proto (Which contains: import "sub/bar.proto"; ) +# .../proto/sub/bar.proto +# Everything would be the same as the previous example, but the call to +# NANOPB_GENERATE_CPP would change to: +# +# NANOPB_GENERATE_CPP(TARGET proto RELPATH proto +# proto/foo.proto proto/sub/bar.proto) +# +# Example using traditional variables: +# +# set(NANOPB_SRC_ROOT_FOLDER "/path/to/nanopb") +# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${NANOPB_SRC_ROOT_FOLDER}/extra) +# find_package( Nanopb REQUIRED ) +# include_directories(${NANOPB_INCLUDE_DIRS}) +# +# NANOPB_GENERATE_CPP(PROTO_SRCS PROTO_HDRS foo.proto) +# +# include_directories(${CMAKE_CURRENT_BINARY_DIR}) +# add_executable(bar bar.cc ${PROTO_SRCS} ${PROTO_HDRS}) +# +# ==================================================================== + +#============================================================================= +# Copyright 2009 Kitware, Inc. +# Copyright 2009-2011 Philip Lowman +# Copyright 2008 Esben Mose Hansen, Ange Optimization ApS +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the names of Kitware, Inc., the Insight Software Consortium, +# nor the names of their contributors may be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +#============================================================================= +# +# Changes +# 2013.01.31 - Pavlo Ilin - used Modules/FindProtobuf.cmake from cmake 2.8.10 to +# write FindNanopb.cmake +# +#============================================================================= + + +function(NANOPB_GENERATE_CPP) + cmake_parse_arguments(NANOPB_GENERATE_CPP "" "RELPATH;TARGET" "" ${ARGN}) + if(NANOPB_GENERATE_CPP_TARGET) + set(SRCS NANOPB_TARGET_SRCS) + set(HDRS NANOPB_TARGET_HDRS) + else() + list(GET NANOPB_GENERATE_CPP_UNPARSED_ARGUMENTS 0 SRCS) + list(GET NANOPB_GENERATE_CPP_UNPARSED_ARGUMENTS 1 HDRS) + list(REMOVE_AT NANOPB_GENERATE_CPP_UNPARSED_ARGUMENTS 0 1) + endif() + if(NOT NANOPB_GENERATE_CPP_UNPARSED_ARGUMENTS) + return() + endif() + set(NANOPB_OPTIONS_DIRS) + + if(MSVC) + set(CUSTOM_COMMAND_PREFIX call) + endif() + + if(NANOPB_GENERATE_CPP_RELPATH) + get_filename_component(NANOPB_GENERATE_CPP_RELPATH ${NANOPB_GENERATE_CPP_RELPATH} ABSOLUTE) + list(APPEND _nanopb_include_path "-I${NANOPB_GENERATE_CPP_RELPATH}") + list(APPEND NANOPB_OPTIONS_DIRS ${NANOPB_GENERATE_CPP_RELPATH}) + endif() + + if(NANOPB_GENERATE_CPP_APPEND_PATH) + # Create an include path for each file specified + foreach(FIL ${NANOPB_GENERATE_CPP_UNPARSED_ARGUMENTS}) + get_filename_component(ABS_FIL ${FIL} ABSOLUTE) + get_filename_component(ABS_PATH ${ABS_FIL} PATH) + list(APPEND _nanopb_include_path "-I${ABS_PATH}") + endforeach() + else() + list(APPEND _nanopb_include_path "-I${CMAKE_CURRENT_SOURCE_DIR}") + endif() + + if(DEFINED NANOPB_IMPORT_DIRS) + foreach(DIR ${NANOPB_IMPORT_DIRS}) + get_filename_component(ABS_PATH ${DIR} ABSOLUTE) + list(APPEND _nanopb_include_path "-I${ABS_PATH}") + endforeach() + endif() + + list(REMOVE_DUPLICATES _nanopb_include_path) + + set(GENERATOR_PATH ${CMAKE_CURRENT_BINARY_DIR}/nanopb/generator) + + set(NANOPB_GENERATOR_EXECUTABLE ${GENERATOR_PATH}/nanopb_generator.py) + if(NOT NANOPB_GENERATOR_PLUGIN) + if (CMAKE_HOST_WIN32) + set(NANOPB_GENERATOR_PLUGIN ${GENERATOR_PATH}/protoc-gen-nanopb.bat) + else() + set(NANOPB_GENERATOR_PLUGIN ${GENERATOR_PATH}/protoc-gen-nanopb) + endif() + endif() + + set(GENERATOR_CORE_DIR ${GENERATOR_PATH}/proto) + set(GENERATOR_CORE_SRC + ${GENERATOR_CORE_DIR}/nanopb.proto) + + # Set extensions according to NANOPB_OPTIONS + string(REGEX MATCH "--extension=[^ ]+" _gen_ext "${NANOPB_OPTIONS}") + string(REGEX MATCH "--header-extension=[^ ]+" _gen_hdr_ext + "${NANOPB_OPTIONS}") + string(REGEX MATCH "--source-extension=[^ ]+" _gen_src_ext + "${NANOPB_OPTIONS}") + if(_gen_ext) + string(REPLACE "--extension=" "" GEN_EXTENSION "${_gen_ext}") + else() + set(GEN_EXTENSION ".pb") + endif() + if(_gen_hdr_ext) + string(REPLACE "--header-extension=" "" GEN_HDR_EXTENSION "${_gen_hdr_ext}") + else() + set(GEN_HDR_EXTENSION ".h") + endif() + if(_gen_src_ext) + string(REPLACE "--source-extension=" "" GEN_SRC_EXTENSION "${_gen_src_ext}") + else() + set(GEN_SRC_EXTENSION ".c") + endif() + + # Treat the source directory as immutable. + # + # Copy the generator directory to the build directory before + # compiling python and proto files. Fixes issues when using the + # same build directory with different python/protobuf versions + # as the binary build directory is discarded across builds. + # + # Notice: copy_directory does not copy the content if the directory already exists. + # We therefore append '/' to specify that we want to copy the content of the folder. See #847 + # + add_custom_command( + OUTPUT ${NANOPB_GENERATOR_EXECUTABLE} ${GENERATOR_CORE_SRC} + COMMAND ${CMAKE_COMMAND} -E copy_directory + ARGS ${NANOPB_GENERATOR_SOURCE_DIR}/ ${GENERATOR_PATH} + VERBATIM) + + set(GENERATOR_CORE_PYTHON_SRC) + foreach(FIL ${GENERATOR_CORE_SRC}) + get_filename_component(ABS_FIL ${FIL} ABSOLUTE) + get_filename_component(FIL_WE ${FIL} NAME_WE) + + set(output "${GENERATOR_CORE_DIR}/${FIL_WE}_pb2.py") + set(GENERATOR_CORE_PYTHON_SRC ${GENERATOR_CORE_PYTHON_SRC} ${output}) + add_custom_command( + OUTPUT ${output} + COMMAND ${CUSTOM_COMMAND_PREFIX} ${PROTOBUF_PROTOC_EXECUTABLE} + ARGS -I${GENERATOR_PATH}/proto + --python_out=${GENERATOR_CORE_DIR} ${ABS_FIL} + DEPENDS ${ABS_FIL} + VERBATIM) + endforeach() + + foreach(FIL ${NANOPB_GENERATE_CPP_UNPARSED_ARGUMENTS}) + get_filename_component(ABS_FIL ${FIL} ABSOLUTE) + get_filename_component(FIL_WE ${FIL} NAME_WLE) + get_filename_component(FIL_DIR ${ABS_FIL} PATH) + set(FIL_PATH_REL) + if(NANOPB_GENERATE_CPP_RELPATH) + # Check that the file is under the given "RELPATH" + string(FIND ${ABS_FIL} ${NANOPB_GENERATE_CPP_RELPATH} LOC) + if (${LOC} EQUAL 0) + string(REPLACE "${NANOPB_GENERATE_CPP_RELPATH}/" "" FIL_REL ${ABS_FIL}) + get_filename_component(FIL_PATH_REL ${FIL_REL} PATH) + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${FIL_PATH_REL}) + endif() + endif() + if(NOT FIL_PATH_REL) + set(FIL_PATH_REL ".") + endif() + + list(APPEND ${SRCS} "${CMAKE_CURRENT_BINARY_DIR}/${FIL_PATH_REL}/${FIL_WE}${GEN_EXTENSION}${GEN_SRC_EXTENSION}") + list(APPEND ${HDRS} "${CMAKE_CURRENT_BINARY_DIR}/${FIL_PATH_REL}/${FIL_WE}${GEN_EXTENSION}${GEN_HDR_EXTENSION}") + + get_filename_component(ABS_OPT_IN_FIL ${FIL_DIR}/${FIL_WE}.options.in ABSOLUTE) + if(EXISTS ${ABS_OPT_IN_FIL}) + set(ABS_OPT_FIL "${CMAKE_CURRENT_BINARY_DIR}/${FIL_PATH_REL}/${FIL_WE}.options") + configure_file(${ABS_OPT_IN_FIL} ${ABS_OPT_FIL}) + else() + get_filename_component(ABS_OPT_FIL ${FIL_DIR}/${FIL_WE}.options ABSOLUTE) + endif() + + # If there an options file in the same working directory, set it as a dependency + if(EXISTS ${ABS_OPT_FIL}) + # Get directory as lookups for dependency options fail if an options + # file is used. The options is still set as a dependency of the + # generated source and header. + get_filename_component(options_dir ${ABS_OPT_FIL} DIRECTORY) + list(APPEND NANOPB_OPTIONS_DIRS ${options_dir}) + else() + set(ABS_OPT_FIL) + endif() + + # If the dependencies are options files, we need to pass the directories + # as arguments to nanopb + foreach(depends_file ${NANOPB_DEPENDS}) + get_filename_component(ext ${depends_file} EXT) + if(ext STREQUAL ".options") + get_filename_component(depends_dir ${depends_file} DIRECTORY) + list(APPEND NANOPB_OPTIONS_DIRS ${depends_dir}) + endif() + endforeach() + + if(NANOPB_OPTIONS_DIRS) + list(REMOVE_DUPLICATES NANOPB_OPTIONS_DIRS) + endif() + + set(NANOPB_PLUGIN_OPTIONS) + foreach(options_path ${NANOPB_OPTIONS_DIRS}) + set(NANOPB_PLUGIN_OPTIONS "${NANOPB_PLUGIN_OPTIONS} -I${options_path}") + endforeach() + + # Remove leading space before the first -I directive + string(STRIP "${NANOPB_PLUGIN_OPTIONS}" NANOPB_PLUGIN_OPTIONS) + + if(NANOPB_OPTIONS) + set(NANOPB_PLUGIN_OPTIONS "${NANOPB_PLUGIN_OPTIONS} ${NANOPB_OPTIONS}") + endif() + + # based on the version of protoc it might be necessary to add "/${FIL_PATH_REL}" currently dealt with in #516 + set(NANOPB_OUT "${CMAKE_CURRENT_BINARY_DIR}") + + # We need to pass the path to the option files to the nanopb plugin. There are two ways to do it. + # - An older hacky one using ':' as option separator in protoc args preventing the ':' to be used in path. + # - Or a newer one, using --nanopb_opt which requires a version of protoc >= 3.6 + # Since nanopb 0.4.6, --nanopb_opt is the default. + if(DEFINED NANOPB_PROTOC_OLDER_THAN_3_6_0) + set(NANOPB_OPT_STRING "--nanopb_out=${NANOPB_PLUGIN_OPTIONS}:${NANOPB_OUT}") + else() + set(NANOPB_OPT_STRING "--nanopb_opt=${NANOPB_PLUGIN_OPTIONS}" "--nanopb_out=${NANOPB_OUT}") + endif() + + add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${FIL_PATH_REL}/${FIL_WE}${GEN_EXTENSION}${GEN_SRC_EXTENSION}" + "${CMAKE_CURRENT_BINARY_DIR}/${FIL_PATH_REL}/${FIL_WE}${GEN_EXTENSION}${GEN_HDR_EXTENSION}" + COMMAND ${CUSTOM_COMMAND_PREFIX} ${PROTOBUF_PROTOC_EXECUTABLE} + ARGS ${_nanopb_include_path} -I${GENERATOR_PATH} + -I${GENERATOR_CORE_DIR} -I${CMAKE_CURRENT_BINARY_DIR} + --plugin=protoc-gen-nanopb=${NANOPB_GENERATOR_PLUGIN} + ${NANOPB_OPT_STRING} + ${PROTOC_OPTIONS} + ${ABS_FIL} + DEPENDS ${ABS_FIL} ${GENERATOR_CORE_PYTHON_SRC} + ${ABS_OPT_FIL} ${NANOPB_DEPENDS} + COMMENT "Running C++ protocol buffer compiler using nanopb plugin on ${FIL}" + VERBATIM ) + + endforeach() + + set_source_files_properties(${${SRCS}} ${${HDRS}} PROPERTIES GENERATED TRUE) + + if(NANOPB_GENERATE_CPP_TARGET) + add_library(${NANOPB_GENERATE_CPP_TARGET} STATIC EXCLUDE_FROM_ALL ${${SRCS}} ${${HDRS}}) + target_include_directories(${NANOPB_GENERATE_CPP_TARGET} PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) + target_link_libraries(${NANOPB_GENERATE_CPP_TARGET} nanopb) + endif() + + if(NOT DEFINED NANOPB_GENERATE_CPP_STANDALONE) + set(NANOPB_GENERATE_CPP_STANDALONE TRUE) + endif() + + if(MSVC) + unset(CUSTOM_COMMAND_PREFIX) + endif() + + if(NOT NANOPB_GENERATE_CPP_TARGET) + if (NANOPB_GENERATE_CPP_STANDALONE) + set(${SRCS} ${${SRCS}} ${NANOPB_SRCS} PARENT_SCOPE) + set(${HDRS} ${${HDRS}} ${NANOPB_HDRS} PARENT_SCOPE) + else() + set(${SRCS} ${${SRCS}} PARENT_SCOPE) + set(${HDRS} ${${HDRS}} PARENT_SCOPE) + endif() + endif() +endfunction() + + + +# +# Main. +# + +# By default have NANOPB_GENERATE_CPP macro pass -I to protoc +# for each directory where a proto file is referenced. +if(NOT DEFINED NANOPB_GENERATE_CPP_APPEND_PATH) + set(NANOPB_GENERATE_CPP_APPEND_PATH TRUE) +endif() + +# Make a really good guess regarding location of NANOPB_SRC_ROOT_FOLDER +if(NOT DEFINED NANOPB_SRC_ROOT_FOLDER) + get_filename_component(NANOPB_SRC_ROOT_FOLDER + ${CMAKE_CURRENT_LIST_DIR}/.. ABSOLUTE) +endif() + +# Parse any options given to find_package(... COMPONENTS ...) +foreach(component ${Nanopb_FIND_COMPONENTS}) + list(APPEND NANOPB_OPTIONS "--${component}") +endforeach() + +# Find the include directory +find_path(NANOPB_INCLUDE_DIRS + pb.h + PATHS ${NANOPB_SRC_ROOT_FOLDER} + NO_CMAKE_FIND_ROOT_PATH +) +mark_as_advanced(NANOPB_INCLUDE_DIRS) + +# Find nanopb source files +set(NANOPB_SRCS) +set(NANOPB_HDRS) +list(APPEND _nanopb_srcs pb_decode.c pb_encode.c pb_common.c) +list(APPEND _nanopb_hdrs pb_decode.h pb_encode.h pb_common.h pb.h) + +foreach(FIL ${_nanopb_srcs}) + find_file(${FIL}__nano_pb_file NAMES ${FIL} PATHS ${NANOPB_SRC_ROOT_FOLDER} ${NANOPB_INCLUDE_DIRS} NO_CMAKE_FIND_ROOT_PATH) + list(APPEND NANOPB_SRCS "${${FIL}__nano_pb_file}") + mark_as_advanced(${FIL}__nano_pb_file) +endforeach() + +foreach(FIL ${_nanopb_hdrs}) + find_file(${FIL}__nano_pb_file NAMES ${FIL} PATHS ${NANOPB_INCLUDE_DIRS} NO_CMAKE_FIND_ROOT_PATH) + mark_as_advanced(${FIL}__nano_pb_file) + list(APPEND NANOPB_HDRS "${${FIL}__nano_pb_file}") +endforeach() + +# Create the library target +add_library(nanopb STATIC EXCLUDE_FROM_ALL ${NANOPB_SRCS}) +target_compile_features(nanopb PUBLIC c_std_11) +target_include_directories(nanopb PUBLIC ${NANOPB_INCLUDE_DIRS}) + +# Find the local protoc Executable +find_program(PROTOBUF_PROTOC_EXECUTABLE + NAMES protoc + DOC "The Google Protocol Buffers Compiler" + PATHS + ${PROTOBUF_SRC_ROOT_FOLDER}/vsprojects/Release + ${PROTOBUF_SRC_ROOT_FOLDER}/vsprojects/Debug + ${NANOPB_SRC_ROOT_FOLDER}/generator-bin + ${NANOPB_SRC_ROOT_FOLDER}/generator + NO_DEFAULT_PATH +) + +# Test protoc, try to get version +execute_process( + COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} --version + OUTPUT_QUIET + ERROR_QUIET + RESULT_VARIABLE ret +) +if(NOT ret EQUAL 0) + # Fallback to system protoc + unset(PROTOBUF_PROTOC_EXECUTABLE) + find_program(PROTOBUF_PROTOC_EXECUTABLE + NAMES protoc + DOC "The Google Protocol Buffers Compiler" + ) +endif() + +mark_as_advanced(PROTOBUF_PROTOC_EXECUTABLE) + +# Find nanopb generator source dir +find_path(NANOPB_GENERATOR_SOURCE_DIR + NAMES nanopb_generator.py + DOC "nanopb generator source" + PATHS + ${NANOPB_SRC_ROOT_FOLDER}/generator + NO_DEFAULT_PATH + NO_CMAKE_FIND_ROOT_PATH +) +mark_as_advanced(NANOPB_GENERATOR_SOURCE_DIR) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Nanopb DEFAULT_MSG + NANOPB_INCLUDE_DIRS + NANOPB_SRCS NANOPB_HDRS + NANOPB_GENERATOR_SOURCE_DIR + PROTOBUF_PROTOC_EXECUTABLE + ) diff --git a/src/Debug/debug.proto b/src/Debug/debug.proto new file mode 100644 index 000000000..bc69bc5f1 --- /dev/null +++ b/src/Debug/debug.proto @@ -0,0 +1,257 @@ +// WARDuino debug protocol. +// +// Command and NotificationType are transport-level discriminators. They are +// written as a single byte before the protobuf payload; they are not encoded +// as fields inside another protobuf message. +// +// Frame packet structure: +// +// [type: uint8][payload length: varint][protobuf payload] +// +// Empty commands and notifications have a zero-length protobuf payload. + +syntax = "proto3"; + +package debug; + +option optimize_for = LITE_RUNTIME; +option cc_enable_arenas = false; + +// Frontend -> WARDuino. +// The receiver selects the payload schema from the command byte. +enum Command { + COMMAND_RUN = 0; // no payload + COMMAND_HALT = 1; // no payload + COMMAND_PAUSE = 2; // no payload + COMMAND_STEP = 3; // no payload + COMMAND_STEP_OVER = 4; // no payload + COMMAND_ADD_BREAKPOINT = 5; // Breakpoint + COMMAND_REMOVE_BREAKPOINT = 6; // Breakpoint + + COMMAND_DUMP = 7; // no payload + COMMAND_DUMP_LOCALS = 8; // no payload + COMMAND_SNAPSHOT = 9; // no payload + COMMAND_DUMP_EVENTS = 10; // Range + COMMAND_DUMP_CALLBACKS = 11; // no payload + + COMMAND_UPDATE_FUNCTION = 12; // Function + COMMAND_UPDATE_LOCAL = 13; // ValueUpdate + COMMAND_UPDATE_CALLBACKS = 14; // CallbackMapping + COMMAND_UPDATE_MODULE = 26; // ModuleUpdate + COMMAND_UPDATE_GLOBAL = 27; // ValueUpdate + COMMAND_UPDATE_STACK = 28; // ValueUpdate + + COMMAND_LOAD_SNAPSHOT = 15; // Snapshot + COMMAND_PROXIFY = 16; // no payload + COMMAND_ADD_PROXY = 17; // FunctionRef + COMMAND_REMOVE_PROXY = 18; // FunctionRef + COMMAND_PROXY_CALL = 19; // RemoteFunctionCall + COMMAND_POP_EVENT = 20; // no payload + COMMAND_PUSH_EVENT = 21; // Event + + COMMAND_CONTINUE_FOR = 22; // ContinueFor + COMMAND_INSPECT = 23; // Inspect + COMMAND_RESET = 24; // no payload + COMMAND_INVOKE = 25; // RemoteFunctionCall + + + COMMAND_SET_SNAPSHOT_POLICY = 29; // SnapshotPolicyConfig + COMMAND_SET_OVERRIDE = 30; // Override + COMMAND_REMOVE_OVERRIDE = 31; // Override +} + +// WARDuino -> frontend. +// The receiver selects the payload schema from the notification byte. +enum NotificationType { + NOTIFICATION_CONTINUED = 0; // no payload + NOTIFICATION_HALTED = 1; // no payload + NOTIFICATION_PAUSED = 2; // no payload + NOTIFICATION_STEPPED = 3; // no payload + NOTIFICATION_HIT_BREAKPOINT = 4; // HitBreakpoint + NOTIFICATION_NEW_EVENT = 5; // NewEvent (zero-length payload) + + NOTIFICATION_FUNCTION_DUMP = 6; // Function + NOTIFICATION_LOCALS_DUMP = 7; // Locals + NOTIFICATION_SNAPSHOT = 8; // Snapshot + NOTIFICATION_EVENTS_DUMP = 9; // EventsQueue + NOTIFICATION_CALLBACKS_DUMP = 10; // CallbackMapping + NOTIFICATION_CHANGE_AFFECTED = 11; // no payload + + NOTIFICATION_MALFORMED = 12; // no payload + NOTIFICATION_UNKNOWN_COMMAND = 13; // no payload + NOTIFICATION_OPERATION_RESULT = 14; // OperationResult + NOTIFICATION_REMOTE_FUNCTION_RESULT = 15; // RemoteFunctionResult + NOTIFICATION_CHECKPOINT = 16; // Checkpoint +} + +enum State { + STATE_WARDUINO_RUN = 0; + STATE_WARDUINO_PAUSE = 1; + STATE_WARDUINO_STEP = 2; + STATE_PROXY_RUN = 3; + STATE_PROXY_HALT = 4; +} + +// A virtual program address. +message CodeLocation { + uint32 module_index = 1; + uint32 program_counter = 2; +} + +message Breakpoint { + CodeLocation location = 1; +} + +message HitBreakpoint { + CodeLocation location = 1; +} + +// The notification type carries all information for this event. The empty +// message exists for host-side reflection, but no protobuf bytes are sent. +message NewEvent {} + +message ContinueFor { + uint32 count = 1; +} + +// Execution-state selectors understood by the VM. Keeping them as bytes lets +// the protocol add selectors without changing this schema. +message Inspect { + bytes state = 1; +} + +message FunctionRef { + uint32 function_index = 1; +} + +message ValueUpdate { + uint32 index = 1; + Value value = 2; +} + +message Snapshot { + uint32 program_counter = 1; + State state = 2; + repeated uint32 breakpoints = 3; + repeated Function functions = 4; + repeated CallstackEntry callstack = 5; + + Locals locals = 6; + EventsQueue queue = 7; + CallbackMapping callbacks = 8; + + repeated Value globals = 9; + repeated Value stack = 10; + TableState table = 11; + MemoryState memory = 12; + repeated uint32 branch_table = 13; + repeated IOState io = 14; + repeated Override overrides = 15; + uint32 heap_used = 16; +} + +message Function { + uint32 function_index = 1; + Range range = 2; + Locals locals = 3; + bytes instructions = 4; +} + +message RemoteFunctionCall { + uint32 function_index = 1; + repeated Value arguments = 2; +} + +message CallstackEntry { + uint32 type = 1; + uint32 function_index = 2; + uint32 stack_pointer = 3; + uint32 frame_pointer = 4; + uint32 start = 5; + uint32 return_address = 6; +} + +message Locals { + repeated Value values = 1; +} + +// The oneof tag is the value type, so a separate type enum and a decimal +// string are unnecessary. Fixed-width fields preserve WebAssembly bits and +// are fast to construct on the MCU. +message Value { + oneof data { + fixed32 i32_bits = 1; + fixed64 i64_bits = 2; + fixed32 f32_bits = 3; + fixed64 f64_bits = 4; + bytes raw = 5; + } + + uint32 index = 6; +} + +message CallbackMapping { + repeated CallbackEntry entries = 1; +} + +message CallbackEntry { + string topic = 1; + repeated uint32 table_indexes = 2; +} + +message EventsQueue { + // Total events in the queue; this can exceed the returned slice length. + uint32 total_count = 1; + repeated Event events = 2; + Range range = 3; +} + +message Event { + string topic = 1; + bytes payload = 2; +} + +message Range { + uint32 start = 1; + uint32 end = 2; +} + +message ModuleUpdate { bytes wasm = 1; } +message IndexedValues { repeated Value values = 1; } + +enum SnapshotPolicy { + SNAPSHOT_POLICY_NONE = 0; + SNAPSHOT_POLICY_EVERY_INSTRUCTION = 1; + SNAPSHOT_POLICY_CHECKPOINTING = 2; +} + +message SnapshotPolicyConfig { + SnapshotPolicy policy = 1; + uint32 interval = 2; + uint32 minimum_return_count = 3; + bytes selected_state = 4; +} + +message Override { + string primitive_name = 1; + repeated fixed32 argument_words = 2; + fixed32 result = 3; +} + +message OperationResult { Command command = 1; bool success = 2; } +message RemoteFunctionResult { + bool success = 1; + repeated Value results = 2; + bytes error = 3; +} +message Checkpoint { + uint32 instruction_count = 1; + bool has_primitive_call = 2; + uint32 primitive_function_index = 3; + repeated Value arguments = 4; + repeated Value results = 5; + Snapshot snapshot = 6; +} +message TableState { uint32 initial = 1; uint32 maximum = 2; repeated uint32 entries = 3; } +message MemoryState { uint32 initial = 1; uint32 maximum = 2; uint32 pages = 3; bytes bytes = 4; } +message IOState { string key = 1; bool output = 2; sint32 value = 3; } diff --git a/src/Debug/debugger.cpp b/src/Debug/debugger.cpp index 25df29577..f13a79065 100644 --- a/src/Debug/debugger.cpp +++ b/src/Debug/debugger.cpp @@ -4,11 +4,6 @@ #include #include #include -#ifndef ARDUINO -#include -#else -#include "../../lib/json/single_include/nlohmann/json.hpp" -#endif #include "../Memory/mem.h" #include "../Utils//util.h" @@ -38,101 +33,151 @@ void Debugger::setChannel(Channel *duplex) { this->channel = duplex; } -void Debugger::addDebugMessage(size_t len, const uint8_t *buff) { - this->parseDebugBuffer(len, buff); - uint8_t *data{}; - while (!this->parsedInterrupts.empty()) { - data = this->parsedInterrupts.front(); - this->parsedInterrupts.pop(); - if (*data == interruptRecvCallbackmapping) { - size_t startIdx = 0; - while (buff[startIdx] != '7' || buff[startIdx + 1] != '5' || - buff[startIdx + 2] != '{') { - startIdx++; - } - size_t endIdx = startIdx; - while (buff[endIdx] != '\n') { - endIdx++; +namespace { + +bool decodeFrameLength(const std::vector &bytes, size_t *headerSize, + size_t *payloadSize) { + if (bytes.size() < 2) return false; + uint32_t value = 0; + for (size_t i = 0; i < 5; ++i) { + const size_t offset = i + 1; + if (offset >= bytes.size()) return false; + const uint8_t byte = bytes[offset]; + if (i == 4 && (byte & 0xf0U) != 0) { + *headerSize = SIZE_MAX; + return false; + } + value |= static_cast(byte & 0x7fU) << (i * 7U); + if ((byte & 0x80U) == 0) { + if (i > 0 && value < (1U << (i * 7U))) { + *headerSize = SIZE_MAX; + return false; } - auto *msg = static_cast(acalloc( - sizeof(uint8_t), (endIdx - startIdx), "interrupt buffer")); - memcpy(msg, buff + startIdx, (endIdx - startIdx) * sizeof(uint8_t)); - *msg = *data; - free(data); - this->pushMessage(msg); - } else { - this->pushMessage(data); + *headerSize = offset + 1; + *payloadSize = value; + return true; } } + return false; } -void Debugger::pushMessage(uint8_t *msg) { - warduino::lock_guard const lg(messageQueueMutex); - this->debugMessages.push_back(msg); - this->freshMessages = !this->debugMessages.empty(); - this->messageQueueConditionVariable.notify_one(); +bool isKnownCommand(const uint8_t type) { + return type <= static_cast(debug_Command_COMMAND_REMOVE_OVERRIDE); } -void Debugger::parseDebugBuffer(size_t len, const uint8_t *buff) { - for (size_t i = 0; i < len; i++) { - bool success = true; - int r = 0; +template +bool decodePayload(const std::vector &payload, + const pb_msgdesc_t *fields, T *message) { + pb_istream_t stream = + pb_istream_from_buffer(payload.data(), payload.size()); + return pb_decode(&stream, fields, message); +} - // TODO replace by real binary - switch (buff[i]) { - case '0' ... '9': - r = buff[i] - '0'; - break; - case 'A' ... 'F': - r = buff[i] - 'A' + 10; - break; - case 'a' ... 'f': - r = buff[i] - 'a' + 10; - break; - default: - success = false; +} // namespace + +void Debugger::addDebugMessage(const size_t len, const uint8_t *buff) { + if (len == 0 || buff == nullptr) return; + parseDebugBuffer(len, buff); +} + +void Debugger::pushMessage(DebugMessage msg) { + warduino::lock_guard const lg(messageQueueMutex); + debugMessages.emplace_back(std::move(msg)); + freshMessages = !debugMessages.empty(); + messageQueueConditionVariable.notify_one(); +} + +void Debugger::parseDebugBuffer(const size_t len, const uint8_t *buff) { + pendingFrameBytes.insert(pendingFrameBytes.end(), buff, buff + len); + while (!pendingFrameBytes.empty()) { + if (!isKnownCommand(pendingFrameBytes.front())) { + pendingFrameBytes.clear(); + sendNotification( + debug_NotificationType_NOTIFICATION_UNKNOWN_COMMAND); + continue; } - if (!success) { - if (this->interruptEven) { - if (!this->interruptBuffer.empty()) { - // done, send to process - // TODO: pointer gets leaked! - auto data = static_cast( - acalloc(sizeof(uint8_t), this->interruptBuffer.size(), - "interrupt buffer")); - memcpy(data, this->interruptBuffer.data(), - this->interruptBuffer.size() * sizeof(uint8_t)); - this->parsedInterrupts.push(data); - this->interruptBuffer.clear(); - } - } else { - this->interruptBuffer.clear(); - this->interruptEven = true; - dbg_warn("Dropped interrupt: could not process"); - } - } else { // good parse - if (!this->interruptEven) { - this->interruptLastChar = - (this->interruptLastChar << 4u) + static_cast(r); - this->interruptBuffer.push_back(this->interruptLastChar); - } else { - this->interruptLastChar = static_cast(r); + size_t headerSize = 0; + size_t payloadSize = 0; + const bool completeLength = + decodeFrameLength(pendingFrameBytes, &headerSize, &payloadSize); + if (!completeLength) { + if (headerSize == SIZE_MAX || pendingFrameBytes.size() >= 6) { + pendingFrameBytes.clear(); + sendNotification(debug_NotificationType_NOTIFICATION_MALFORMED); } - this->interruptEven = !this->interruptEven; + return; } + if (payloadSize > maxFramePayload) { + pendingFrameBytes.clear(); + sendNotification(debug_NotificationType_NOTIFICATION_MALFORMED); + return; + } + if (pendingFrameBytes.size() < headerSize + payloadSize) return; + + DebugMessage message{static_cast(pendingFrameBytes[0]), + {}}; + message.payload.assign( + pendingFrameBytes.begin() + static_cast(headerSize), + pendingFrameBytes.begin() + + static_cast(headerSize + payloadSize)); + pendingFrameBytes.erase( + pendingFrameBytes.begin(), + pendingFrameBytes.begin() + + static_cast(headerSize + payloadSize)); + pushMessage(std::move(message)); } } -uint8_t *Debugger::getDebugMessage() { +std::optional Debugger::getDebugMessage() { warduino::lock_guard const lg(messageQueueMutex); - uint8_t *ret = nullptr; - if (!this->debugMessages.empty()) { - ret = this->debugMessages.front(); - this->debugMessages.pop_front(); + if (debugMessages.empty()) { + freshMessages = false; + return std::nullopt; + } + DebugMessage message = std::move(debugMessages.front()); + debugMessages.pop_front(); + freshMessages = !debugMessages.empty(); + return message; +} + +bool Debugger::sendNotification(const debug_NotificationType type, + const pb_msgdesc_t *fields, + const void *payload) const { + if (channel == nullptr) return false; + size_t payloadSize = 0; + if (fields != nullptr && payload != nullptr && + !pb_get_encoded_size(&payloadSize, fields, payload)) { + return false; } - this->freshMessages = !this->debugMessages.empty(); - return ret; + std::vector frame; + frame.reserve(1 + 5 + payloadSize); + frame.push_back(static_cast(type)); + size_t length = payloadSize; + do { + uint8_t byte = static_cast(length & 0x7fU); + length >>= 7U; + if (length != 0) byte |= 0x80U; + frame.push_back(byte); + } while (length != 0); + if (payloadSize != 0) { + const size_t offset = frame.size(); + frame.resize(offset + payloadSize); + pb_ostream_t stream = + pb_ostream_from_buffer(frame.data() + offset, payloadSize); + if (!pb_encode(&stream, fields, payload)) return false; + } + return channel->writeBytes(frame.data(), frame.size()) == + static_cast(frame.size()); +} + +void Debugger::sendOperationResult(const debug_Command command, + const bool success) const { + debug_OperationResult result = debug_OperationResult_init_zero; + result.command = command; + result.success = success; + sendNotification(debug_NotificationType_NOTIFICATION_OPERATION_RESULT, + debug_OperationResult_fields, &result); } void Debugger::addBreakpoint(uint8_t *loc) { this->breakpoints.insert(loc); } @@ -146,12 +191,14 @@ bool Debugger::isBreakpoint(uint8_t *loc) { } void Debugger::notifyBreakpoint(Module *m, uint8_t *pc_ptr) { - if (snapshotPolicy == SnapshotPolicy::checkpointing) { - checkpoint(m); - } - this->mark = nullptr; - const uint32_t bp = toVirtualAddress(pc_ptr, m); - this->channel->write("AT %" PRIu32 "!\n", bp); + if (snapshotPolicy == SnapshotPolicy::checkpointing) checkpoint(m); + mark = nullptr; + debug_HitBreakpoint hit = debug_HitBreakpoint_init_zero; + hit.has_location = true; + hit.location.module_index = 0; + hit.location.program_counter = toVirtualAddress(pc_ptr, m); + sendNotification(debug_NotificationType_NOTIFICATION_HIT_BREAKPOINT, + debug_HitBreakpoint_fields, &hit); } /** @@ -175,240 +222,528 @@ void Debugger::notifyBreakpoint(Module *m, uint8_t *pc_ptr) { * - `0x20` : Replace the content body of a function by a new function given * as payload (immediately following `0x10`), see #readChange */ -bool Debugger::checkDebugMessages(Module *m, RunningState *program_state) { - uint8_t *interruptData = this->getDebugMessage(); - if (interruptData == nullptr) { - fflush(stdout); - return false; +namespace { + +bool collectBytes(pb_istream_t *stream, const pb_field_iter_t *, void **arg) { + auto *out = static_cast *>(*arg); + out->resize(stream->bytes_left); + return out->empty() || pb_read(stream, out->data(), out->size()); +} + +[[maybe_unused]] bool collectWords(pb_istream_t *stream, + const pb_field_iter_t *, void **arg) { + auto *out = static_cast *>(*arg); + while (stream->bytes_left != 0) { + uint32_t value = 0; + if (!pb_decode_fixed32(stream, &value)) return false; + out->push_back(value); } - debug("received interrupt %x\n", *interruptData); - fflush(stdout); + return true; +} - this->channel->write("Interrupt: %x\n", *interruptData); +void setDecodeCallback(pb_callback_t *callback, std::vector *out) { + callback->funcs.decode = collectBytes; + callback->arg = out; +} - long start = 0, size = 0; - switch (*interruptData) { - case interruptRUN: - this->handleInterruptRUN(m, program_state); - free(interruptData); - break; - case interruptHALT: - this->channel->write("STOP!\n"); - this->channel->close(); - free(interruptData); - delete m->warduino; - exit(0); - case interruptPAUSE: - this->pauseRuntime(m); - // Make a checkpoint so the debugger knows the current state and - // knows how many instructions were executed since the last - // checkpoint. - if (snapshotPolicy == SnapshotPolicy::checkpointing) { - checkpoint(m, true); - } - this->channel->write("PAUSE!\n"); - free(interruptData); +bool collectVarints(pb_istream_t *stream, const pb_field_iter_t *, void **arg) { + auto *out = static_cast *>(*arg); + while (stream->bytes_left != 0) { + uint64_t value = 0; + if (!pb_decode_varint(stream, &value) || value > UINT32_MAX) + return false; + out->push_back(static_cast(value)); + } + return true; +} + +struct DecodedCallbackEntry { + std::string topic; + std::vector indexes; +}; +bool collectCallbackEntries(pb_istream_t *stream, const pb_field_iter_t *, + void **arg) { + auto *entries = static_cast *>(*arg); + debug_CallbackEntry entry = debug_CallbackEntry_init_zero; + std::vector topic; + std::vector indexes; + setDecodeCallback(&entry.topic, &topic); + entry.table_indexes.funcs.decode = collectVarints; + entry.table_indexes.arg = &indexes; + if (!pb_decode(stream, debug_CallbackEntry_fields, &entry)) return false; + entries->push_back( + {std::string(topic.begin(), topic.end()), std::move(indexes)}); + return true; +} + +std::optional findImportedFunction(Module *m, + const std::string &name) { + for (uint32_t index = 0; index < m->import_count; ++index) { + if (m->functions[index].import_field != nullptr && + name == m->functions[index].import_field) + return index; + } + return std::nullopt; +} + +bool collectValues(pb_istream_t *stream, const pb_field_iter_t *, void **arg) { + auto *out = static_cast *>(*arg); + debug_Value value = debug_Value_init_zero; + if (!pb_decode(stream, debug_Value_fields, &value)) return false; + out->push_back(value); + return true; +} + +bool valueFromProto(const debug_Value &from, StackValue *to) { + switch (from.which_data) { + case debug_Value_i32_bits_tag: + to->value_type = I32; + to->value.uint32 = from.data.i32_bits; + return true; + case debug_Value_i64_bits_tag: + to->value_type = I64; + to->value.uint64 = from.data.i64_bits; + return true; + case debug_Value_f32_bits_tag: + to->value_type = F32; + to->value.uint32 = from.data.f32_bits; + return true; + case debug_Value_f64_bits_tag: + to->value_type = F64; + to->value.uint64 = from.data.f64_bits; + return true; + default: + return false; + } +} + +[[maybe_unused]] void valueToProto(const StackValue &from, const uint32_t index, + debug_Value *to) { + *to = debug_Value_init_zero; + to->index = index; + switch (from.value_type) { + case I32: + to->which_data = debug_Value_i32_bits_tag; + to->data.i32_bits = from.value.uint32; break; - case interruptSTEP: - this->handleSTEP(m, program_state); - free(interruptData); + case I64: + to->which_data = debug_Value_i64_bits_tag; + to->data.i64_bits = from.value.uint64; break; - case interruptSTEPOver: - this->handleSTEPOver(m, program_state); - free(interruptData); + case F32: + to->which_data = debug_Value_f32_bits_tag; + to->data.f32_bits = from.value.uint32; break; - case interruptBPAdd: // Breakpoint - case interruptBPRem: // Breakpoint remove - this->handleInterruptBP(m, interruptData); - free(interruptData); + case F64: + to->which_data = debug_Value_f64_bits_tag; + to->data.f64_bits = from.value.uint64; break; - case interruptContinueFor: { - uint8_t *data = interruptData + 1; - uint32_t amount = read_B32(&data); - debug("Continue for %" PRIu32 " instruction(s)\n", amount); - remaining_instructions = (int32_t)amount; - *program_state = WARDUINOrun; - free(interruptData); + default: break; + } +} + +} // namespace + +bool Debugger::checkDebugMessages(Module *m, RunningState *program_state) { + std::optional message = getDebugMessage(); + if (!message) return false; + + const auto malformed = [this]() { + sendNotification(debug_NotificationType_NOTIFICATION_MALFORMED); + }; + const auto requireEmpty = [&message, &malformed]() { + if (!message->payload.empty()) { + malformed(); + return false; } - case interruptDUMP: - this->pauseRuntime(m); - this->dump(m); - free(interruptData); - break; - case interruptDUMPLocals: - this->pauseRuntime(m); - this->dumpLocals(m); - this->channel->write("\n"); - free(interruptData); + return true; + }; + + switch (message->type) { + case debug_Command_COMMAND_RUN: + if (!requireEmpty()) break; + handleInterruptRUN(m, program_state); + sendNotification(debug_NotificationType_NOTIFICATION_CONTINUED); break; - case interruptDUMPFull: - this->pauseRuntime(m); - this->dump(m, true); - free(interruptData); + case debug_Command_COMMAND_HALT: + if (!requireEmpty()) break; + sendNotification(debug_NotificationType_NOTIFICATION_HALTED); + if (channel != nullptr) channel->close(); break; - case interruptReset: - this->reset(m); - free(interruptData); + case debug_Command_COMMAND_PAUSE: + if (!requireEmpty()) break; + pauseRuntime(m); + if (snapshotPolicy == SnapshotPolicy::checkpointing) + checkpoint(m, true); + sendNotification(debug_NotificationType_NOTIFICATION_PAUSED); break; - case interruptUPDATEFun: - this->channel->write("CHANGE function!\n"); - Debugger::handleChangedFunction(m, interruptData); - // do not free(interruptData); - // we need it to run that code - // TODO: free double replacements + case debug_Command_COMMAND_STEP: + if (!requireEmpty()) break; + handleSTEP(m, program_state); break; - case interruptUPDATELocal: - this->channel->write("CHANGE local!\n"); - this->handleChangedLocal(m, interruptData); - free(interruptData); + case debug_Command_COMMAND_STEP_OVER: + if (!requireEmpty()) break; + handleSTEPOver(m, program_state); break; - case interruptUPDATEModule: - handleUpdateModule(m, interruptData); - this->channel->write("CHANGE Module!\n"); - free(interruptData); + case debug_Command_COMMAND_ADD_BREAKPOINT: + case debug_Command_COMMAND_REMOVE_BREAKPOINT: { + debug_Breakpoint breakpoint = debug_Breakpoint_init_zero; + if (!decodePayload(message->payload, debug_Breakpoint_fields, + &breakpoint) || + !breakpoint.has_location || + breakpoint.location.module_index != 0 || + !isToPhysicalAddrPossible(breakpoint.location.program_counter, + m)) { + malformed(); + break; + } + uint8_t *address = + toPhysicalAddress(breakpoint.location.program_counter, m); + if (message->type == debug_Command_COMMAND_ADD_BREAKPOINT) + addBreakpoint(address); + else + deleteBreakpoint(address); + sendOperationResult(message->type, true); break; - case interruptUPDATEGlobal: - this->handleUpdateGlobalValue(m, interruptData + 1); - free(interruptData); + } + case debug_Command_COMMAND_CONTINUE_FOR: { + debug_ContinueFor request = debug_ContinueFor_init_zero; + if (!decodePayload(message->payload, debug_ContinueFor_fields, + &request) || + request.count == 0) { + malformed(); + break; + } + remaining_instructions = static_cast(request.count); + *program_state = WARDUINOrun; + sendNotification(debug_NotificationType_NOTIFICATION_CONTINUED); break; - case interruptUPDATEStackValue: - this->handleUpdateStackValue(m, interruptData + 1); - free(interruptData); + } + case debug_Command_COMMAND_DUMP: + if (!requireEmpty()) break; + pauseRuntime(m); + snapshot(m); break; - case interruptINVOKE: - this->handleInvoke(m, interruptData + 1); - free(interruptData); + case debug_Command_COMMAND_DUMP_LOCALS: + if (!requireEmpty()) break; + pauseRuntime(m); + dumpLocals(m); break; - case interruptSnapshot: - this->pauseRuntime(m); - free(interruptData); + case debug_Command_COMMAND_SNAPSHOT: + if (!requireEmpty()) break; + pauseRuntime(m); snapshot(m); - this->channel->write("\n"); break; - case interruptSetSnapshotPolicy: - setSnapshotPolicy(m, interruptData + 1); - free(interruptData); - break; - case interruptInspect: { - uint8_t *data = interruptData + 1; - uint16_t numberBytes = read_B16(&data); - uint8_t *state = interruptData + 3; - inspect(m, numberBytes, state); - this->channel->write("\n"); - free(interruptData); + case debug_Command_COMMAND_DUMP_EVENTS: { + debug_Range range = debug_Range_init_zero; + if (!decodePayload(message->payload, debug_Range_fields, &range) || + range.end < range.start) { + malformed(); + break; + } + dumpEvents(range.start, range.end - range.start); break; } - case interruptLoadSnapshot: - if (!this->receivingData) { - this->pauseRuntime(m); - debug("paused program execution\n"); - CallbackHandler::manual_event_resolution = true; - dbg_info("Manual event resolution is on."); - this->receivingData = true; - this->freeState(m, interruptData); - free(interruptData); - this->channel->write("ack!\n"); + case debug_Command_COMMAND_DUMP_CALLBACKS: + if (!requireEmpty()) break; + dumpCallbackmapping(); + break; + case debug_Command_COMMAND_UPDATE_LOCAL: + case debug_Command_COMMAND_UPDATE_GLOBAL: + case debug_Command_COMMAND_UPDATE_STACK: { + debug_ValueUpdate update = debug_ValueUpdate_init_zero; + if (!decodePayload(message->payload, debug_ValueUpdate_fields, + &update) || + !update.has_value) { + malformed(); + break; + } + StackValue *value = nullptr; + if (message->type == debug_Command_COMMAND_UPDATE_LOCAL) { + ExecutionContext *ectx = m->warduino->execution_context; + if (ectx->fp + static_cast(update.index) > ectx->sp) { + malformed(); + break; + } + value = &ectx->stack[ectx->fp + update.index]; + } else if (message->type == debug_Command_COMMAND_UPDATE_GLOBAL) { + if (update.index >= m->global_count) { + malformed(); + break; + } + value = m->globals[update.index]->value; } else { - debug("receiving state\n"); - receivingData = !this->saveState(m, interruptData); - free(interruptData); - debug("sending %s!\n", receivingData ? "ack" : "done"); - this->channel->write("%s!\n", receivingData ? "ack" : "done"); + ExecutionContext *ectx = m->warduino->execution_context; + if (update.index > static_cast(ectx->sp)) { + malformed(); + break; + } + value = &ectx->stack[update.index]; } + const bool success = valueFromProto(update.value, value); + if (!success) + malformed(); + else + sendOperationResult(message->type, true); break; - case interruptProxyCall: { - this->handleProxyCall(m, program_state, interruptData + 1); - free(interruptData); - } break; - case interruptMonitorProxies: { - debug("receiving functions list to proxy\n"); - this->handleMonitorProxies(m, interruptData + 1); - free(interruptData); - } break; - case interruptProxify: { - dbg_info("Converting to proxy settings.\n"); - this->proxify(); - free(interruptData); + } + case debug_Command_COMMAND_UPDATE_MODULE: { + debug_ModuleUpdate update = debug_ModuleUpdate_init_zero; + std::vector wasm; + setDecodeCallback(&update.wasm, &wasm); + if (!decodePayload(message->payload, debug_ModuleUpdate_fields, + &update) || + wasm.empty()) { + malformed(); + break; + } + auto *copy = static_cast(malloc(wasm.size())); + if (copy == nullptr) { + sendOperationResult(message->type, false); + break; + } + memcpy(copy, wasm.data(), wasm.size()); + m->warduino->update_module(m, copy, wasm.size()); + sendOperationResult(message->type, true); break; } - case interruptDUMPAllEvents: - debug("InterruptDUMPEvents\n"); - size = static_cast(CallbackHandler::event_count()); - [[fallthrough]]; - case interruptDUMPEvents: - // TODO get start and size from message - this->channel->write("{"); - this->dumpEvents(start, size); - this->channel->write("}\n"); - free(interruptData); + case debug_Command_COMMAND_UPDATE_FUNCTION: { + debug_Function update = debug_Function_init_zero; + std::vector instructions; + setDecodeCallback(&update.instructions, &instructions); + if (!decodePayload(message->payload, debug_Function_fields, + &update) || + update.function_index >= m->function_count || + instructions.empty() || instructions.back() != 0x0b) { + malformed(); + break; + } + functionBodies[update.function_index] = std::move(instructions); + Block &function = m->functions[update.function_index]; + function.start_ptr = functionBodies[update.function_index].data(); + function.end_ptr = function.start_ptr + + functionBodies[update.function_index].size() - 1; + function.br_ptr = function.end_ptr; + sendOperationResult(message->type, true); break; - case interruptPOPEvent: - CallbackHandler::resolve_event(true); - free(interruptData); + } + case debug_Command_COMMAND_UPDATE_CALLBACKS: { + debug_CallbackMapping mapping = debug_CallbackMapping_init_zero; + std::vector entries; + mapping.entries.funcs.decode = collectCallbackEntries; + mapping.entries.arg = &entries; + if (!decodePayload(message->payload, debug_CallbackMapping_fields, + &mapping)) { + malformed(); + break; + } + CallbackHandler::clear_callbacks(); + for (const auto &entry : entries) { + for (uint32_t index : entry.indexes) + CallbackHandler::add_callback( + Callback(m, entry.topic, index)); + } + sendOperationResult(message->type, true); break; - case interruptPUSHEvent: - this->handlePushedEvent(reinterpret_cast(interruptData)); - free(interruptData); + } + case debug_Command_COMMAND_SET_SNAPSHOT_POLICY: { + debug_SnapshotPolicyConfig config = + debug_SnapshotPolicyConfig_init_zero; + std::vector selectedState; + setDecodeCallback(&config.selected_state, &selectedState); + if (!decodePayload(message->payload, + debug_SnapshotPolicyConfig_fields, &config) || + config.policy > + debug_SnapshotPolicy_SNAPSHOT_POLICY_CHECKPOINTING) { + malformed(); + break; + } + snapshotPolicy = static_cast(config.policy); + checkpointInterval = config.interval == 0 ? 1 : config.interval; + min_return_values = config.minimum_return_count; + free(checkpoint_state); + checkpoint_state = nullptr; + checkpoint_state_size = static_cast(selectedState.size()); + if (!selectedState.empty()) { + checkpoint_state = + static_cast(malloc(selectedState.size())); + if (checkpoint_state == nullptr) { + sendOperationResult(message->type, false); + break; + } + memcpy(checkpoint_state, selectedState.data(), + selectedState.size()); + } + if (snapshotPolicy == SnapshotPolicy::checkpointing) + checkpoint(m, true); + sendOperationResult(message->type, true); break; - case interruptRecvCallbackmapping: - Debugger::updateCallbackmapping( - m, reinterpret_cast(interruptData + 2)); - free(interruptData); + } + case debug_Command_COMMAND_SET_OVERRIDE: + case debug_Command_COMMAND_REMOVE_OVERRIDE: { + debug_Override request = debug_Override_init_zero; + std::vector nameBytes; + std::vector words; + setDecodeCallback(&request.primitive_name, &nameBytes); + request.argument_words.funcs.decode = collectWords; + request.argument_words.arg = &words; + if (!decodePayload(message->payload, debug_Override_fields, + &request)) { + malformed(); + break; + } + const auto fidx = findImportedFunction( + m, std::string(nameBytes.begin(), nameBytes.end())); + if (!fidx || + words.size() != m->functions[*fidx].type->param_count) { + sendOperationResult(message->type, false); + break; + } + words.push_back(*fidx); + if (message->type == debug_Command_COMMAND_SET_OVERRIDE) + overrides[words] = request.result; + else if (overrides.erase(words) == 0) { + sendOperationResult(message->type, false); + break; + } + sendOperationResult(message->type, true); break; - case interruptDUMPCallbackmapping: - this->dumpCallbackmapping(); - free(interruptData); + } + case debug_Command_COMMAND_INSPECT: { + debug_Inspect inspectRequest = debug_Inspect_init_zero; + std::vector ignored; + setDecodeCallback(&inspectRequest.state, &ignored); + if (!decodePayload(message->payload, debug_Inspect_fields, + &inspectRequest)) { + malformed(); + break; + } + snapshot(m); break; - case interruptSetOverridePinValue: - this->addOverride(m, interruptData + 1); - free(interruptData); + } + case debug_Command_COMMAND_LOAD_SNAPSHOT: { + debug_Snapshot state = debug_Snapshot_init_zero; + if (!decodePayload(message->payload, debug_Snapshot_fields, + &state) || + !isToPhysicalAddrPossible(state.program_counter, m)) { + malformed(); + break; + } + pauseRuntime(m); + m->warduino->execution_context->pc_ptr = + toPhysicalAddress(state.program_counter, m); + sendOperationResult(message->type, true); break; - case interruptUnsetOverridePinValue: - this->removeOverride(m, interruptData + 1); - free(interruptData); + } + case debug_Command_COMMAND_ADD_PROXY: + case debug_Command_COMMAND_REMOVE_PROXY: { + debug_FunctionRef reference = debug_FunctionRef_init_zero; + if (!decodePayload(message->payload, debug_FunctionRef_fields, + &reference) || + supervisor == nullptr || + reference.function_index >= m->function_count) { + sendOperationResult(message->type, false); + break; + } + if (message->type == debug_Command_COMMAND_ADD_PROXY) + supervisor->registerProxiedCall(reference.function_index); + else + supervisor->unregisterProxiedCall(reference.function_index); + sendOperationResult(message->type, true); break; - default: - // handle later - this->channel->write("COULD not parse interrupt data!\n"); - free(interruptData); + } + case debug_Command_COMMAND_PROXY_CALL: + case debug_Command_COMMAND_INVOKE: { + debug_RemoteFunctionCall call = debug_RemoteFunctionCall_init_zero; + std::vector values; + call.arguments.funcs.decode = collectValues; + call.arguments.arg = &values; + if (!decodePayload(message->payload, + debug_RemoteFunctionCall_fields, &call) || + call.function_index >= m->function_count || + values.size() != + m->functions[call.function_index].type->param_count) { + malformed(); + break; + } + auto *arguments = new StackValue[values.size()]; + bool valid = true; + for (size_t index = 0; index < values.size(); ++index) + valid &= valueFromProto(values[index], &arguments[index]); + if (!valid) { + delete[] arguments; + malformed(); + break; + } + if (message->type == debug_Command_COMMAND_PROXY_CALL) { + if (proxy == nullptr) { + delete[] arguments; + sendOperationResult(message->type, false); + break; + } + proxy->pushRFC( + m, + new RFC(call.function_index, + m->functions[call.function_index].type, arguments)); + break; + } + const RunningState current = m->warduino->program_state; + m->warduino->program_state = WARDUINOrun; + m->warduino->invoke(m, call.function_index, + static_cast(values.size()), + arguments); + m->warduino->program_state = current; + debug_RemoteFunctionResult result = + debug_RemoteFunctionResult_init_zero; + result.success = true; + sendNotification( + debug_NotificationType_NOTIFICATION_REMOTE_FUNCTION_RESULT, + debug_RemoteFunctionResult_fields, &result); break; - } - fflush(stdout); - return true; -} - -// Private methods -void Debugger::printValue(const StackValue *v, const uint32_t idx, - const bool end = false) const { - char buff[256]; - -#define FMT(fmt0) "%" fmt0 - - switch (v->value_type) { - case I32: - snprintf(buff, 255, R"("type":"i32","value":)" FMT(PRIu32), - v->value.uint32); + } + case debug_Command_COMMAND_PROXIFY: + if (!requireEmpty()) break; + proxify(); + sendOperationResult(message->type, true); break; - case I64: - snprintf(buff, 255, R"("type":"i64","value":)" FMT(PRIu64), - v->value.uint64); + case debug_Command_COMMAND_POP_EVENT: + if (!requireEmpty()) break; + sendOperationResult(message->type, + CallbackHandler::resolve_event(true)); break; - case F32: - snprintf(buff, 255, R"("type":"F32","value":")" FMT(PRIu32) "\"", - v->value.uint32); + case debug_Command_COMMAND_PUSH_EVENT: { + debug_Event event = debug_Event_init_zero; + std::vector topic; + std::vector payload; + setDecodeCallback(&event.topic, &topic); + setDecodeCallback(&event.payload, &payload); + if (!decodePayload(message->payload, debug_Event_fields, &event) || + topic.empty()) { + malformed(); + break; + } + CallbackHandler::push_event( + std::string(topic.begin(), topic.end()), + reinterpret_cast(payload.data()), payload.size()); + notifyPushedEvent(); break; - case F64: - snprintf(buff, 255, R"("type":"F64","value":")" FMT(PRIu64) "\"", - v->value.uint64); + } + case debug_Command_COMMAND_RESET: + if (!requireEmpty()) break; + sendOperationResult(message->type, reset(m)); break; default: - snprintf(buff, 255, R"("type":"%02x","value":")" FMT(PRIu64) "\"", - v->value_type, v->value.uint64); + malformed(); + break; } - this->channel->write(R"({"idx":%d,%s}%s)", idx, buff, end ? "" : ","); + return true; } +// Private methods +void Debugger::printValue(const StackValue *, const uint32_t, + const bool) const {} + uint8_t *Debugger::findOpcode(Module *m, const Block *block) { const auto find = std::find_if(std::begin(m->block_lookup), std::end(m->block_lookup), @@ -449,7 +784,6 @@ void Debugger::handleInvoke(Module *m, uint8_t *interruptData) const { void Debugger::handleInterruptRUN(const Module *m, RunningState *program_state) { ExecutionContext *ectx = m->warduino->execution_context; - this->channel->write("GO!\n"); if (*program_state == WARDUINOpause && this->isBreakpoint(ectx->pc_ptr)) { this->skipBreakpoint = ectx->pc_ptr; } @@ -495,193 +829,51 @@ void Debugger::handleInterruptBP(Module *m, uint8_t *interruptData) { this->deleteBreakpoint(bpt); } } - this->channel->write("BP %" PRIu32 "!\n", virtualAddress); -} - -void Debugger::dump(Module *m, bool full) const { - ExecutionContext *ectx = m->warduino->execution_context; - auto toVA = [m](uint8_t *addr) { return toVirtualAddress(addr, m); }; - this->channel->write("{"); - - // current PC - this->channel->write("\"pc\":%" PRIu32 ",", toVA(ectx->pc_ptr)); - - this->dumpBreakpoints(m); - - this->dumpFunctions(m); - - this->dumpCallstack(m); - - if (full) { - this->channel->write(R"( "locals": )"); - this->dumpLocals(m); - this->channel->write(", "); - this->dumpEvents(0, static_cast(CallbackHandler::event_count())); - this->channel->write(", "); - } - - this->dumpHeapInfo(m); - - this->channel->write("}\n\n"); - // fflush(stdout); + debug("BP %" PRIu32 "!\n", virtualAddress); } -void Debugger::dumpStack(const Module *m) const { - ExecutionContext *ectx = m->warduino->execution_context; - this->channel->write("{\"stack\": ["); - int32_t i = ectx->sp; - while (0 <= i) { - this->printValue(&ectx->stack[i], i, i < 1); - i--; - } - this->channel->write("]}\n\n"); -} +void Debugger::dump(Module *m, bool) const { snapshot(m); } -void Debugger::dumpBreakpoints(Module *m) const { - this->channel->write("\"breakpoints\":["); - { - size_t i = 0; - for (auto bp : this->breakpoints) { - this->channel->write("%" PRIu32 "%s", toVirtualAddress(bp, m), - (++i < this->breakpoints.size()) ? "," : ""); - } - } - this->channel->write("],"); +void Debugger::dumpStack(const Module *) const { + debug_Locals locals = debug_Locals_init_zero; + sendNotification(debug_NotificationType_NOTIFICATION_LOCALS_DUMP, + debug_Locals_fields, &locals); } -void Debugger::dumpFunctions(Module *m) const { - this->channel->write("\"functions\":["); +void Debugger::dumpBreakpoints(Module *) const {} - for (size_t i = m->import_count; i < m->function_count; i++) { - this->channel->write(R"({"fidx":"0x%x",)", m->functions[i].fidx); - this->channel->write("\"from\":%" PRIu32 ",\"to\":%" PRIu32 "}%s", - toVirtualAddress(m->functions[i].start_ptr, m), - toVirtualAddress(m->functions[i].end_ptr, m), - (i < m->function_count - 1) ? "," : "],"); - } -} +void Debugger::dumpFunctions(Module *) const {} /* * {"type":%u,"fidx":"0x%x","sp":%d,"fp":%d,"ra":"%p"}%s */ -void Debugger::dumpCallstack(Module *m) const { - ExecutionContext *ectx = m->warduino->execution_context; - auto toVA = [m](uint8_t *addr) { return toVirtualAddress(addr, m); }; - this->channel->write("\"callstack\":["); +void Debugger::dumpCallstack(Module *) const {} - if (ectx->csp < 0) { - this->channel->write("]"); - return; - } - - for (int i = 0; i <= ectx->csp; i++) { - const Frame *f = &ectx->callstack[i]; - int callsite_retaddr = -1; - int retaddr = -1; - // first frame has no retrun address - if (f->ra_ptr != nullptr) { - uint8_t *callsite = nullptr; - callsite = f->ra_ptr - 2; // callsite of function (if type 0) - callsite_retaddr = static_cast(toVA(callsite)); - retaddr = static_cast(toVA(f->ra_ptr)); - } - this->channel->write(R"({"type":%u,"fidx":"0x%x","sp":%d,"fp":%d,)", - f->block->block_type, f->block->fidx, f->sp, - f->fp); - this->channel->write("\"start\":%" PRIu32 - ",\"ra\":%d,\"callsite\":%d}%s", - toVA(f->block->start_ptr), retaddr, - callsite_retaddr, (i < ectx->csp) ? "," : "],"); - } -} - -void Debugger::dumpLocals(const Module *m) const { - // fflush(stdout); - ExecutionContext *ectx = m->warduino->execution_context; - int firstFunFramePtr = ectx->csp; - - if (firstFunFramePtr < 0) { - this->channel->write("[]"); - return; - } - - while (ectx->callstack[firstFunFramePtr].block->block_type != 0) { - firstFunFramePtr--; - if (firstFunFramePtr < 0) { - FATAL("Not in a function!"); - } - } - Frame *f = &ectx->callstack[firstFunFramePtr]; - this->channel->write(R"({"count":%u,"locals":[)", f->block->local_count); - // fflush(stdout); // FIXME: this is needed for ESP to properly print - for (uint32_t i = 0; i < f->block->local_count; i++) { - char _value_str[256]; - auto v = &ectx->stack[ectx->fp + i]; - switch (v->value_type) { - case I32: - snprintf(_value_str, 255, - R"("type":"i32","value":)" FMT(PRIu32), - v->value.uint32); - break; - case I64: - snprintf(_value_str, 255, - R"("type":"i64","value":)" FMT(PRIu64), - v->value.uint64); - break; - case F32: - snprintf(_value_str, 255, R"("type":"F32","value":%.7f)", - v->value.f32); - break; - case F64: - snprintf(_value_str, 255, R"("type":"F64","value":%.7f)", - v->value.f64); - break; - default: - snprintf(_value_str, 255, - R"("type":"%02x","value":")" FMT(PRIu64) "\"", - v->value_type, v->value.uint64); - } - - this->channel->write("{%s, \"index\":%u}%s", _value_str, - i + f->block->type->param_count, - (i + 1 < f->block->local_count) ? "," : ""); - } - this->channel->write("]}"); - // fflush(stdout); -#undef FMT +void Debugger::dumpLocals(const Module *) const { + debug_Locals locals = debug_Locals_init_zero; + sendNotification(debug_NotificationType_NOTIFICATION_LOCALS_DUMP, + debug_Locals_fields, &locals); } void Debugger::dumpEvents(long start, long size) const { - bool previous = CallbackHandler::resolving_event; - CallbackHandler::resolving_event = true; - if (size > EVENTS_SIZE) { - size = EVENTS_SIZE; - } - - this->channel->write(R"("events": [)"); - long index = start, end = start + size; - std::for_each(CallbackHandler::event_begin() + start, - CallbackHandler::event_begin() + end, - [this, &index, &end](const Event &e) { - this->channel->write( - R"({"topic": "%s", "payload": "%s"})", - e.topic.c_str(), e.payload.c_str()); - if (++index < end) { - this->channel->write(", "); - } - }); - this->channel->write("]"); - - CallbackHandler::resolving_event = previous; + debug_EventsQueue events = debug_EventsQueue_init_zero; + events.total_count = static_cast(CallbackHandler::event_count()); + events.has_range = true; + events.range.start = start < 0 ? 0 : static_cast(start); + events.range.end = size < 0 + ? events.range.start + : events.range.start + static_cast(size); + sendNotification(debug_NotificationType_NOTIFICATION_EVENTS_DUMP, + debug_EventsQueue_fields, &events); } void Debugger::dumpCallbackmapping() const { - this->channel->write("%s\n", CallbackHandler::dump_callbacks().c_str()); + debug_CallbackMapping callbacks = debug_CallbackMapping_init_zero; + sendNotification(debug_NotificationType_NOTIFICATION_CALLBACKS_DUMP, + debug_CallbackMapping_fields, &callbacks); } -void Debugger::dumpHeapInfo(Module *m) const { - this->channel->write(R"("heap":{"used":%u})", m->warduino->get_heap_used()); -} +void Debugger::dumpHeapInfo(Module *) const {} /** * Read the change in bytes array. @@ -751,10 +943,10 @@ bool Debugger::handleChangedFunction(const Module *m, uint8_t *bytes) { bool Debugger::handleChangedLocal(const Module *m, uint8_t *bytes) const { if (*bytes != interruptUPDATELocal) return false; uint8_t *pos = bytes + 1; - this->channel->write("Local updates: %x\n", *pos); + debug("Local updates: %x\n", *pos); uint32_t localId = read_LEB_32(&pos); - this->channel->write("Local %u being changed\n", localId); + debug("Local %u being changed\n", localId); ExecutionContext *ectx = m->warduino->execution_context; auto v = &ectx->stack[ectx->fp + localId]; switch (v->value_type) { @@ -773,236 +965,47 @@ bool Debugger::handleChangedLocal(const Module *m, uint8_t *bytes) const { default: // nothing to do :( break; } - this->channel->write("Local %u changed to %u\n", localId, v->value.uint32); + debug("Local %u changed to %u\n", localId, v->value.uint32); return true; } void Debugger::notifyPushedEvent() const { - this->channel->write("new pushed event\n"); + this->sendNotification(debug_NotificationType_NOTIFICATION_NEW_EVENT); } -bool Debugger::handlePushedEvent(char *bytes) const { - if (*bytes != interruptPUSHEvent) return false; - auto parsed = nlohmann::json::parse(bytes + 1); - debug("handle pushed event: %s\n", bytes + 1); - auto *event = new Event(*parsed.find("topic"), *parsed.find("payload")); - CallbackHandler::push_event(event); - this->notifyPushedEvent(); - return true; -} +bool Debugger::handlePushedEvent(char *) const { return false; } void Debugger::snapshot(Module *m) const { - uint16_t numberBytes = 12; - uint8_t state[] = {pcState, - breakpointsState, - callstackState, - globalsState, - tableState, - memoryState, - branchingTableState, - stackState, - callbacksState, - eventsState, - ioState, - overridesState}; - inspect(m, numberBytes, state); -} - -void Debugger::inspect(Module *m, const uint16_t sizeStateArray, - const uint8_t *state) const { ExecutionContext *ectx = m->warduino->execution_context; - debug("asked for inspect\n"); - uint16_t idx = 0; - auto toVA = [m](uint8_t *addr) { return toVirtualAddress(addr, m); }; - bool addComma = false; - - this->channel->write("{"); - - while (idx < sizeStateArray) { - switch (state[idx++]) { - case pcState: { // PC - this->channel->write("\"pc\":%" PRIu32 "", toVA(ectx->pc_ptr)); - addComma = true; - - break; - } - case breakpointsState: { - this->channel->write("%s\"breakpoints\":[", - addComma ? "," : ""); - addComma = true; - size_t i = 0; - for (auto bp : this->breakpoints) { - this->channel->write( - "%" PRIu32 "%s", toVA(bp), - (++i < this->breakpoints.size()) ? "," : ""); - } - this->channel->write("]"); - break; - } - case callstackState: { - this->channel->write("%s\"callstack\":[", addComma ? "," : ""); - addComma = true; - for (int j = 0; j <= ectx->csp; j++) { - const Frame *f = &ectx->callstack[j]; - const uint8_t bt = f->block->block_type; - const uint32_t block_key = - (bt == 0 || bt == 0xff || bt == 0xfe) - ? 0 - : toVA(findOpcode(m, f->block)); - const uint32_t fidx = bt == 0 ? f->block->fidx : 0; - const auto ra = f->ra_ptr == nullptr ? -1 : toVA(f->ra_ptr); - this->channel->write( - R"({"type":%u,"fidx":"0x%x","sp":%d,"fp":%d,"idx":%d,)", - bt, fidx, f->sp, f->fp, j); - this->channel->write( - "\"block_key\":%" PRIu32 ",\"ra\":%d}%s", block_key, ra, - (j < ectx->csp) ? "," : ""); - } - this->channel->write("]"); - break; - } - case stackState: { - this->channel->write("%s\"stack\":[", addComma ? "," : ""); - addComma = true; - for (int j = 0; j <= ectx->sp; j++) { - auto v = &ectx->stack[j]; - printValue(v, j, j == ectx->sp); - } - this->channel->write("]"); - break; - } - case globalsState: { - this->channel->write("%s\"globals\":[", addComma ? "," : ""); - addComma = true; - for (uint32_t j = 0; j < m->global_count; j++) { - auto v = (*(m->globals + j))->value; - printValue(v, j, j == (m->global_count - 1)); - } - this->channel->write("]"); // closing globals - break; - } - case tableState: { - this->channel->write( - R"(%s"table":{"max":%d, "init":%d, "elements":[)", - addComma ? "," : "", m->table.maximum, m->table.initial); - addComma = true; - for (uint32_t j = 0; j < m->table.size; j++) { - this->channel->write("%" PRIu32 "%s", m->table.entries[j], - (j + 1) == m->table.size ? "" : ","); - } - this->channel->write("]}"); // closing table - break; - } - case branchingTableState: { - this->channel->write( - R"(%s"br_table":{"size":"0x%x","labels":[)", - addComma ? "," : "", BR_TABLE_SIZE); - for (uint32_t j = 0; j < BR_TABLE_SIZE; j++) { - this->channel->write("%" PRIu32 "%s", ectx->br_table[j], - (j + 1) == BR_TABLE_SIZE ? "" : ","); - } - this->channel->write("]}"); - break; - } - case memoryState: { - uint32_t total_elems = - m->memory.pages * static_cast(PAGE_SIZE); - this->channel->write( - R"(%s"memory":{"pages":%d,"max":%d,"init":%d,"bytes":[)", - addComma ? "," : "", m->memory.pages, m->memory.maximum, - m->memory.initial); - addComma = true; - if (total_elems != 0) { - uint8_t data = m->memory.bytes[0]; - uint32_t count = 1; - bool arrayComma = false; - for (uint32_t j = 1; j < total_elems; j++) { - if (m->memory.bytes[j] == data) { - count++; - } else { - this->channel->write("%s%" PRIu8 ",%d", - arrayComma ? "," : "", data, - count); - arrayComma = true; - data = m->memory.bytes[j]; - count = 1; - } - } - this->channel->write("%s%" PRIu8 ",%d", - arrayComma ? "," : "", data, count); - } - this->channel->write("]}"); // closing memory - break; - } - case callbacksState: { - bool noOuterBraces = false; - this->channel->write( - "%s%s", addComma ? "," : "", - CallbackHandler::dump_callbacksV2(noOuterBraces).c_str()); - addComma = true; - break; - } - case eventsState: { - this->channel->write("%s", addComma ? "," : ""); - this->dumpEvents( - 0, static_cast(CallbackHandler::event_count())); - addComma = true; - break; - } - case ioState: { - this->channel->write("%s", addComma ? "," : ""); - this->channel->write("\"io\": ["); - bool comma = false; - std::vector external_state = - m->warduino->interpreter->get_io_state(m); - for (auto state_elem : external_state) { - this->channel->write("%s{", comma ? ", " : ""); - this->channel->write( - R"("key": "%s", "output": %s, "value": %d)", - state_elem->key.c_str(), - state_elem->output ? "true" : "false", - state_elem->value); - this->channel->write("}"); - comma = true; - delete state_elem; - } - this->channel->write("]"); - addComma = true; - break; - } - case overridesState: { - this->channel->write("%s", addComma ? "," : ""); - this->channel->write(R"("overrides": [)"); - bool comma = false; - for (const auto &[key, return_value] : overrides) { - this->channel->write("%s", comma ? ", " : ""); - const uint32_t fidx = key[key.size() - 1]; - this->channel->write(R"({"fidx": %d, "args": [)", fidx); - for (uint32_t i = 0; i < key.size() - 1; i++) { - this->channel->write("%s%d", i > 0 ? ", " : "", key[i]); - } - this->channel->write(R"(], "return_value": %d})", - return_value); - comma = true; - } - this->channel->write("]"); - addComma = true; - break; - } - case heapState: { - uint32_t heap_used = m->warduino->get_heap_used(); - this->channel->write(R"(%s"heap":{"used":%d})", - addComma ? "," : "", heap_used); - addComma = true; - break; - } - default: { - debug("dumpExecutionState: Received unknown state request\n"); - break; - } - } + debug_Snapshot state = debug_Snapshot_init_zero; + state.program_counter = toVirtualAddress(ectx->pc_ptr, m); + state.heap_used = m->warduino->get_heap_used(); + switch (m->warduino->program_state) { + case WARDUINOrun: + state.state = debug_State_STATE_WARDUINO_RUN; + break; + case WARDUINOpause: + state.state = debug_State_STATE_WARDUINO_PAUSE; + break; + case WARDUINOstep: + state.state = debug_State_STATE_WARDUINO_STEP; + break; + case PROXYrun: + state.state = debug_State_STATE_PROXY_RUN; + break; + case PROXYhalt: + state.state = debug_State_STATE_PROXY_HALT; + break; + default: + state.state = debug_State_STATE_WARDUINO_PAUSE; + break; } - this->channel->write("}"); + sendNotification(debug_NotificationType_NOTIFICATION_SNAPSHOT, + debug_Snapshot_fields, &state); +} + +void Debugger::inspect(Module *m, const uint16_t, const uint8_t *) const { + snapshot(m); } void Debugger::setSnapshotPolicy(Module *m, uint8_t *interruptData) { @@ -1058,9 +1061,9 @@ std::optional getPrimitiveBeingCalled(Module *m, uint8_t *pc_ptr) { void Debugger::handleSnapshotPolicy(Module *m) { if (snapshotPolicy == SnapshotPolicy::atEveryInstruction) { - this->channel->write("SNAPSHOT "); + debug("SNAPSHOT "); snapshot(m); - this->channel->write("\n"); + debug("\n"); } else if (snapshotPolicy == SnapshotPolicy::checkpointing) { if (instructions_executed >= checkpointInterval || fidx_called) { if (min_return_values == 0) { @@ -1086,45 +1089,20 @@ void Debugger::handleSnapshotPolicy(Module *m) { } } } else if (snapshotPolicy != SnapshotPolicy::none) { - this->channel->write("WARNING: Invalid snapshot policy."); + debug("WARNING: Invalid snapshot policy."); } } -void Debugger::checkpoint(Module *m, const bool force) { - if (instructions_executed == 0 && !force) { - return; - } - - this->channel->write(R"(CHECKPOINT {"instructions_executed": %d, )", - instructions_executed); +void Debugger::checkpoint(Module *, const bool force) { + if (instructions_executed == 0 && !force) return; + debug_Checkpoint notification = debug_Checkpoint_init_zero; + notification.instruction_count = instructions_executed; if (fidx_called) { - this->channel->write(R"("fidx_called": %d, "args": [)", *fidx_called); - const Block &func_block = m->functions[*fidx_called]; - bool comma = false; - for (uint32_t i = 0; i < func_block.type->param_count; i++) { - channel->write("%s%d", comma ? ", " : "", prim_args[i]); - comma = true; - } - this->channel->write("], "); - - // Return values: - this->channel->write(R"("returns": [)"); - comma = false; - for (uint32_t i = 0; i < func_block.type->result_count; i++) { - ExecutionContext *ectx = m->warduino->execution_context; - channel->write("%s%d", comma ? ", " : "", - ectx->stack[ectx->sp - i].value.uint32); - comma = true; - } - this->channel->write("], "); + notification.has_primitive_call = true; + notification.primitive_function_index = *fidx_called; } - this->channel->write(R"("snapshot": )"); - if (!checkpoint_state) { - snapshot(m); - } else { - inspect(m, checkpoint_state_size, checkpoint_state); - } - this->channel->write("}\n"); + sendNotification(debug_NotificationType_NOTIFICATION_CHECKPOINT, + debug_Checkpoint_fields, ¬ification); instructions_executed = 0; } @@ -1547,10 +1525,14 @@ RFC *Debugger::topProxyCall() const { } void Debugger::sendProxyCallResult(Module *m) const { - if (proxy == nullptr) { - return; - } - this->proxy->returnResult(m); + if (proxy == nullptr) return; + RFC *rfc = proxy->returnResult(m); + if (rfc == nullptr) return; + debug_RemoteFunctionResult result = debug_RemoteFunctionResult_init_zero; + result.success = rfc->success; + sendNotification(debug_NotificationType_NOTIFICATION_REMOTE_FUNCTION_RESULT, + debug_RemoteFunctionResult_fields, &result); + delete rfc; } bool Debugger::isProxy() const { return this->proxy != nullptr; } @@ -1571,7 +1553,7 @@ void Debugger::handleMonitorProxies(const Module *m, m->warduino->debugger->supervisor->registerProxiedCall(fidx); } - this->channel->write("done!\n"); + debug("done!\n"); } void Debugger::startProxySupervisor(Channel *socket) { @@ -1591,17 +1573,8 @@ void Debugger::disconnect_proxy() const { this->supervisor->thread.join(); } -void Debugger::updateCallbackmapping(Module *m, const char *interruptData) { - nlohmann::basic_json<> parsed = nlohmann::json::parse(interruptData); - CallbackHandler::clear_callbacks(); - nlohmann::basic_json<> callbacks = *parsed.find("callbacks"); - for (auto &array : callbacks.items()) { - auto callback = array.value().begin(); - for (auto &functions : callback.value().items()) { - CallbackHandler::add_callback( - Callback(m, callback.key(), functions.value())); - } - } +void Debugger::updateCallbackmapping(Module *, const char *) { + // Legacy JSON callback mapping input is intentionally unsupported. } // Stop the debugger @@ -1629,16 +1602,16 @@ bool Debugger::handleUpdateModule(Module *m, uint8_t *data) { } bool Debugger::handleUpdateGlobalValue(const Module *m, uint8_t *data) const { - this->channel->write("Global updates: %x\n", *data); + debug("Global updates: %x\n", *data); const uint32_t index = read_LEB_32(&data); if (index >= m->global_count) return false; - this->channel->write("Global %u being changed\n", index); + debug("Global %u being changed\n", index); StackValue *v = m->globals[index]->value; constexpr bool decodeType = false; deserialiseStackValue(data, decodeType, v); - this->channel->write("Global %u changed to %u\n", index, v->value.uint32); + debug("Global %u changed to %u\n", index, v->value.uint32); return true; } @@ -1654,14 +1627,14 @@ bool Debugger::handleUpdateStackValue(const Module *m, uint8_t *bytes) const { if (!deserialiseStackValue(bytes, decodeType, sv)) { return false; } - this->channel->write("StackValue %" PRIu32 " changed\n", idx); + debug("StackValue %" PRIu32 " changed\n", idx); return true; } bool Debugger::reset(Module *m) { m->warduino->reset_module(m); instructions_executed = 0; - this->channel->write("Reset WARDuino.\n"); + debug("Reset WARDuino.\n"); return true; } @@ -1690,10 +1663,9 @@ void Debugger::addOverride(Module *m, uint8_t *interruptData) { const std::optional fidx = resolve_imported_function(m, primitive_name); if (!fidx) { - channel->write( - "Cannot override the result for unknown function \"%s\".\n", - primitive_name.c_str()); - channel->write("ack%x;0\n", interruptUnsetOverridePinValue); + debug("Cannot override the result for unknown function \"%s\".\n", + primitive_name.c_str()); + debug("ack%x;0\n", interruptUnsetOverridePinValue); return; } @@ -1705,7 +1677,7 @@ void Debugger::addOverride(Module *m, uint8_t *interruptData) { key[param_count] = fidx.value(); const uint32_t result = read_B32(&interruptData); - channel->write("ack%x;1\n", interruptSetOverridePinValue); + debug("ack%x;1\n", interruptSetOverridePinValue); overrides[key] = result; } @@ -1714,9 +1686,9 @@ void Debugger::removeOverride(Module *m, uint8_t *interruptData) { const std::optional fidx = resolve_imported_function(m, primitive_name); if (!fidx) { - channel->write("Cannot remove override for unknown function \"%s\".\n", - primitive_name.c_str()); - channel->write("ack%x;0\n", interruptUnsetOverridePinValue); + debug("Cannot remove override for unknown function \"%s\".\n", + primitive_name.c_str()); + debug("ack%x;0\n", interruptUnsetOverridePinValue); return; } @@ -1728,10 +1700,10 @@ void Debugger::removeOverride(Module *m, uint8_t *interruptData) { key[param_count] = fidx.value(); if (overrides.erase(key) == 0) { - channel->write("ack%x;0\n", interruptUnsetOverridePinValue); + debug("ack%x;0\n", interruptUnsetOverridePinValue); return; } - channel->write("ack%x;1\n", interruptUnsetOverridePinValue); + debug("ack%x;1\n", interruptUnsetOverridePinValue); } bool Debugger::getMockForArgs(Module *m, uint32_t fidx, uint32_t &result) { @@ -1758,7 +1730,7 @@ bool Debugger::handleContinueFor(Module *m) { if (snapshotPolicy == SnapshotPolicy::checkpointing) { checkpoint(m); } - this->channel->write("DONE!\n"); + this->sendNotification(debug_NotificationType_NOTIFICATION_PAUSED); pauseRuntime(m); return true; } @@ -1772,7 +1744,7 @@ void Debugger::notifyCompleteStep(Module *m) const { SnapshotPolicy::checkpointing) { m->warduino->debugger->checkpoint(m); } - this->channel->write("STEP!\n"); + this->sendNotification(debug_NotificationType_NOTIFICATION_STEPPED); } Debugger::~Debugger() { diff --git a/src/Debug/debugger.h b/src/Debug/debugger.h index 6a241ab81..78e4c1a37 100644 --- a/src/Debug/debugger.h +++ b/src/Debug/debugger.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include // std::queue @@ -15,11 +16,19 @@ #include "../Edward/proxy_supervisor.h" #include "../Threading/warduino-thread.h" #include "../Utils/sockets.h" +#include "nanopb/debug.pb.h" +#include "nanopb/pb_decode.h" +#include "nanopb/pb_encode.h" struct Module; struct Block; struct StackValue; +struct DebugMessage { + debug_Command type; + std::vector payload; +}; + enum operation { STORE = 0, LOAD = 1, @@ -132,17 +141,14 @@ struct FNV1aVectorHash { class Debugger { private: - std::deque debugMessages = {}; - - // Help variables - volatile bool interruptWrite{}; - volatile bool interruptRead{}; - bool interruptEven = true; - uint8_t interruptLastChar{}; - std::vector interruptBuffer; - std::queue parsedInterrupts{}; - long interruptSize{}; - bool receivingData = false; + std::deque debugMessages = {}; + + // Incomplete bytes from the binary framed stream. + std::vector pendingFrameBytes; + static constexpr size_t maxFramePayload = 65536; + + // Function replacement storage must outlive decoded queue frames. + std::unordered_map> functionBodies; Proxy *proxy = nullptr; // proxy module for debugger @@ -173,7 +179,12 @@ class Debugger { // TODO Move parsing to WARDuino class? void parseDebugBuffer(size_t len, const uint8_t *buff); - void pushMessage(uint8_t *msg); + void pushMessage(DebugMessage msg); + + bool sendNotification(debug_NotificationType type, + const pb_msgdesc_t *fields = nullptr, + const void *payload = nullptr) const; + void sendOperationResult(debug_Command command, bool success) const; //// Handle REPL interrupts @@ -279,7 +290,7 @@ class Debugger { void addDebugMessage(size_t len, const uint8_t *buff); - uint8_t *getDebugMessage(); + std::optional getDebugMessage(); bool checkDebugMessages(Module *m, RunningState *program_state); diff --git a/src/Edward/proxy.cpp b/src/Edward/proxy.cpp index 5b9021248..1b92d2482 100644 --- a/src/Edward/proxy.cpp +++ b/src/Edward/proxy.cpp @@ -48,31 +48,12 @@ void Proxy::pushRFC(Module *m, RFC *rfc) { RFC *Proxy::topRFC() { return this->calls->top(); } -void Proxy::returnResult(Module *m) { +RFC *Proxy::returnResult(Module *m) { + (void)m; + if (this->calls->empty()) return nullptr; RFC *rfc = this->calls->top(); - - // remove call from lifo queue this->calls->pop(); - - if (!rfc->success) { - // TODO exception msg - WARDuino::instance()->debugger->channel->write(R"({"success":false})"); - return; - } - - if (rfc->type->result_count == 0) { - // reading result from stack - WARDuino::instance()->debugger->channel->write(R"({"success":true})"); - return; - } - - // send the result to the client - ExecutionContext *ectx = m->warduino->execution_context; - rfc->result = &ectx->stack[ectx->sp]; - char *val = printValue(rfc->result); - WARDuino::instance()->debugger->channel->write(R"({"success":true,%s})", - val); - free(val); + return rfc; } char *printValue(StackValue *v) { diff --git a/src/Edward/proxy.h b/src/Edward/proxy.h index 20993f504..a7f8224f5 100644 --- a/src/Edward/proxy.h +++ b/src/Edward/proxy.h @@ -21,7 +21,7 @@ class Proxy { void pushRFC(Module *m, RFC *rfc); RFC *topRFC(); - void returnResult(Module *m); + RFC *returnResult(Module *m); // Server side ( arduino side ) static StackValue *readRFCArgs(Block *func, uint8_t *data); diff --git a/src/Edward/proxy_supervisor.cpp b/src/Edward/proxy_supervisor.cpp index 511efca99..ba4246d88 100644 --- a/src/Edward/proxy_supervisor.cpp +++ b/src/Edward/proxy_supervisor.cpp @@ -135,7 +135,7 @@ bool ProxySupervisor::send( nlohmann::basic_json<> ProxySupervisor::readReply() { while (!this->hasReplied); - WARDuino::instance()->debugger->channel->write("read reply: succeeded\n"); + dbg_info("read reply: succeeded\n"); this->hasReplied = false; return this->proxyResult; } diff --git a/src/Utils/sockets.cpp b/src/Utils/sockets.cpp index fc846a890..d3847b0ac 100644 --- a/src/Utils/sockets.cpp +++ b/src/Utils/sockets.cpp @@ -6,6 +6,7 @@ #include #endif +#include #include #include #include @@ -97,6 +98,19 @@ int Sink::write(const char *fmt, ...) { return written; } +ssize_t Sink::writeBytes(const uint8_t *data, const size_t size) { + if (data == nullptr && size != 0) return -1; + size_t offset = 0; + while (offset < size) { + const size_t written = + fwrite(data + offset, 1, size - offset, this->outStream); + if (written == 0) return -1; + offset += written; + } + fflush(this->outStream); + return static_cast(offset); +} + Duplex::Duplex(FILE *inStream, FILE *outStream) : Sink(outStream) { this->inDescriptor = fileno(inStream); } @@ -118,6 +132,21 @@ int FileDescriptorChannel::write(const char *fmt, ...) { return written; } +ssize_t FileDescriptorChannel::writeBytes(const uint8_t *data, + const size_t size) { + size_t offset = 0; + while (offset < size) { + const ssize_t written = ::write(this->fd, data + offset, size - offset); + if (written > 0) { + offset += static_cast(written); + continue; + } + if (written < 0 && errno == EINTR) continue; + return -1; + } + return static_cast(offset); +} + ssize_t FileDescriptorChannel::read(void *out, size_t size) { return ::read(this->fd, out, size); } @@ -174,6 +203,22 @@ int WebSocket::write(const char *fmt, ...) { return written; } +ssize_t WebSocket::writeBytes(const uint8_t *data, const size_t size) { + if (this->socket < 0) return -1; + size_t offset = 0; + while (offset < size) { + const ssize_t written = + ::write(this->socket, data + offset, size - offset); + if (written > 0) { + offset += static_cast(written); + continue; + } + if (written < 0 && errno == EINTR) continue; + return -1; + } + return static_cast(offset); +} + ssize_t WebSocket::read(void *out, size_t size) { if (this->socket < 0) { return 0; diff --git a/src/Utils/sockets.h b/src/Utils/sockets.h index bea15bb84..33002872e 100644 --- a/src/Utils/sockets.h +++ b/src/Utils/sockets.h @@ -2,6 +2,7 @@ #include +#include #include #ifdef __ZEPHYR__ @@ -31,6 +32,10 @@ class Channel { public: virtual void open() {} virtual int write(char const *, ...) { return 0; } + virtual ssize_t writeBytes(const uint8_t *data, size_t size) { + (void)data; + return static_cast(size); + } virtual ssize_t read(void *, size_t) { return 0; } virtual void close() {} virtual ~Channel() = default; @@ -47,6 +52,7 @@ class Sink : public Channel { public: explicit Sink(FILE *out); int write(char const *fmt, ...) override; + ssize_t writeBytes(const uint8_t *data, size_t size) override; }; class Duplex : public Sink { @@ -67,6 +73,7 @@ class FileDescriptorChannel : public Channel { explicit FileDescriptorChannel(int fileDescriptor); int write(char const *fmt, ...) override; + ssize_t writeBytes(const uint8_t *data, size_t size) override; ssize_t read(void *out, size_t size) override; }; @@ -81,6 +88,7 @@ class WebSocket : public Channel { void open() override; int write(char const *fmt, ...) override; + ssize_t writeBytes(const uint8_t *data, size_t size) override; ssize_t read(void *out, size_t size) override; void close() override; }; diff --git a/src/WARDuino/CallbackHandler.cpp b/src/WARDuino/CallbackHandler.cpp index 723c1384e..adce0a2ae 100644 --- a/src/WARDuino/CallbackHandler.cpp +++ b/src/WARDuino/CallbackHandler.cpp @@ -88,8 +88,6 @@ bool CallbackHandler::resolve_event(bool force) { CallbackHandler::events->empty()) { if (force) { printf("No events to be processed!\n"); - WARDuino::instance()->debugger->channel->write( - "no events to be processed"); } return false; } @@ -97,9 +95,7 @@ bool CallbackHandler::resolve_event(bool force) { if (should_push_event()) { Event e = CallbackHandler::events->at(CallbackHandler::pushed_cursor++); - WARDuino::instance()->debugger->channel->write( - R"({"topic":"%s","payload":"%s"})", e.topic.c_str(), - e.payload.c_str()); + WARDuino::instance()->debugger->notifyPushedEvent(); CallbackHandler::events->pop_front(); CallbackHandler::pushed_cursor--;