diff --git a/.github/build.sh b/.github/build.sh new file mode 100644 index 00000000..4d602db3 --- /dev/null +++ b/.github/build.sh @@ -0,0 +1,98 @@ +#!bin/bash + +ESP32CS_TARGET=$1 +RUN_DIR=$PWD + +export IDF_PATH=${GITHUB_WORKSPACE}/../esp-idf +TOOLCHAIN_DIR=${GITHUB_WORKSPACE}/toolchain +BUILD_DIR=${RUN_DIR}/build +BINARIES_DIR=${RUN_DIR}/binaries + +# install GCC 8.2.0 toolchain +if [ ! -f "${TOOLCHAIN_DIR}/xtensa-esp32-elf/bin/xtensa-esp32-elf-gcc" ]; then + echo "Toolchain not found in ${TOOLCHAIN_DIR}!" + exit 1 +fi + +echo "Adding ${TOOLCHAIN_DIR}/xtensa-esp32-elf/bin to the path" +# add toolchain to the path +export PATH=${TOOLCHAIN_DIR}/xtensa-esp32-elf/bin:${PATH} + +# clone ESP-IDF +if [ ! -f "${IDF_PATH}/export.sh" ]; then + echo "ESP-IDF not found under ${IDF_PATH}!" + exit 1 +fi + +python -m pip install -r "${IDF_PATH}/requirements.txt" + +if [ -d "${BUILD_DIR}" ]; then + echo "Cleaning up ${BUILD_DIR}" + rm -rf "${BUILD_DIR}" +fi + +mkdir -p "${BUILD_DIR}" "${BUILD_DIR}/config" + +# generate config.env file for confgen.py and cmake +echo "Generating ${BUILD_DIR}/config.env" +cat > "${BUILD_DIR}/config.env" < "${BINARIES_DIR}/readme.txt" << README_EOF +The binaries can be sent to the ESP32 via esptool.py similar to the following: +python esptool.py -p (PORT) -b 460800 --before default_reset --after hard_reset + write_flash 0x1000 bootloader.bin + write_flash 0x8000 partition-table.bin + write_flash 0xe000 ota_data_initial.bin + write_flash 0x10000 ESP32CommandStation.bin + +Note: The esptool.py command above should be all on one line. + +If you prefer a graphical utility for flashing you can use the Flash Download Tool +available from https://www.espressif.com/en/support/download/other-tools to write +the four binary files listed above at the listed offsets. +README_EOF + +cp "${BUILD_DIR}/partition_table/partition-table.bin" \ + "${BUILD_DIR}/ota_data_initial.bin" \ + "${BUILD_DIR}/bootloader/bootloader.bin" \ + "${BUILD_DIR}/ESP32CommandStation.bin" \ + "${BINARIES_DIR}" \ No newline at end of file diff --git a/.github/ninja-log-analyze.py b/.github/ninja-log-analyze.py new file mode 100644 index 00000000..5b00fe18 --- /dev/null +++ b/.github/ninja-log-analyze.py @@ -0,0 +1,277 @@ +#!/usr/bin/env vpython +# Copyright (c) 2018 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +"""Summarize the last ninja build, invoked with ninja's -C syntax. +This script is designed to be automatically run after each ninja build in +order to summarize the build's performance. Making build performance information +more visible should make it easier to notice anomalies and opportunities. To use +this script on Windows just set NINJA_SUMMARIZE_BUILD=1 and run autoninja.bat. +On Linux you can get autoninja to invoke this script using this syntax: +$ NINJA_SUMMARIZE_BUILD=1 autoninja -C out/Default/ chrome +You can also call this script directly using ninja's syntax to specify the +output directory of interest: +> python post_build_ninja_summary.py -C out/Default +Typical output looks like this: +>ninja -C out\debug_component base +ninja.exe -C out\debug_component base -j 960 -l 48 -d keeprsp +ninja: Entering directory `out\debug_component' +[1 processes, 1/1 @ 0.3/s : 3.092s ] Regenerating ninja files +Longest build steps: + 0.1 weighted s to build obj/base/base/trace_log.obj (6.7 s elapsed time) + 0.2 weighted s to build nasm.exe, nasm.exe.pdb (0.2 s elapsed time) + 0.3 weighted s to build obj/base/base/win_util.obj (12.4 s elapsed time) + 1.2 weighted s to build base.dll, base.dll.lib (1.2 s elapsed time) +Time by build-step type: + 0.0 s weighted time to generate 6 .lib files (0.3 s elapsed time sum) + 0.1 s weighted time to generate 25 .stamp files (1.2 s elapsed time sum) + 0.2 s weighted time to generate 20 .o files (2.8 s elapsed time sum) + 1.7 s weighted time to generate 4 PEFile (linking) files (2.0 s elapsed +time sum) + 23.9 s weighted time to generate 770 .obj files (974.8 s elapsed time sum) +26.1 s weighted time (982.9 s elapsed time sum, 37.7x parallelism) +839 build steps completed, average of 32.17/s +If no gn clean has been done then results will be for the last non-NULL +invocation of ninja. Ideas for future statistics, and implementations are +appreciated. +The "weighted" time is the elapsed time of each build step divided by the number +of tasks that were running in parallel. This makes it an excellent approximation +of how "important" a slow step was. A link that is entirely or mostly serialized +will have a weighted time that is the same or similar to its elapsed time. A +compile that runs in parallel with 999 other compiles will have a weighted time +that is tiny.""" +from __future__ import print_function +import argparse +import errno +import os +import sys +# The number of long build times to report: +long_count = 10 +# The number of long times by extension to report +long_ext_count = 5 +class Target: + """Represents a single line read for a .ninja_log file.""" + def __init__(self, start, end): + """Creates a target object by passing in the start/end times in seconds + as a float.""" + self.start = start + self.end = end + # A list of targets, appended to by the owner of this object. + self.targets = [] + self.weighted_duration = 0.0 + def Duration(self): + """Returns the task duration in seconds as a float.""" + return self.end - self.start + def SetWeightedDuration(self, weighted_duration): + """Sets the duration, in seconds, passed in as a float.""" + self.weighted_duration = weighted_duration + def WeightedDuration(self): + """Returns the task's weighted duration in seconds as a float. + Weighted_duration takes the elapsed time of the task and divides it + by how many other tasks were running at the same time. Thus, it + represents the approximate impact of this task on the total build time, + with serialized or serializing steps typically ending up with much + longer weighted durations. + weighted_duration should always be the same or shorter than duration. + """ + # Allow for modest floating-point errors + epsilon = 0.000002 + if (self.weighted_duration > self.Duration() + epsilon): + print('%s > %s?' % (self.weighted_duration, self.Duration())) + assert(self.weighted_duration <= self.Duration() + epsilon) + return self.weighted_duration + def DescribeTargets(self): + """Returns a printable string that summarizes the targets.""" + if len(self.targets) == 1: + return self.targets[0] + # Some build steps generate dozens of outputs - handle them sanely. + # It's a bit odd that if there are three targets we return all three + # but if there are more than three we just return two, but this works + # well in practice. + elif len(self.targets) > 3: + return '(%d items) ' % len(self.targets) + ( + ', '.join(self.targets[:2]) + ', ...') + else: + return ', '.join(self.targets) +# Copied with some modifications from ninjatracing +def ReadTargets(log, show_all): + """Reads all targets from .ninja_log file |log_file|, sorted by duration. + The result is a list of Target objects.""" + header = log.readline() + assert header == '# ninja log v5\n', \ + 'unrecognized ninja log version %r' % header + targets_dict = {} + last_end_seen = 0.0 + for line in log: + parts = line.strip().split('\t') + if len(parts) != 5: + # If ninja.exe is rudely halted then the .ninja_log file may be + # corrupt. Silently continue. + continue + start, end, _, name, cmdhash = parts # Ignore restat. + # Convert from integral milliseconds to float seconds. + start = int(start) / 1000.0 + end = int(end) / 1000.0 + if not show_all and end < last_end_seen: + # An earlier time stamp means that this step is the first in a new + # build, possibly an incremental build. Throw away the previous + # data so that this new build will be displayed independently. + # This has to be done by comparing end times because records are + # written to the .ninja_log file when commands complete, so end + # times are guaranteed to be in order, but start times are not. + targets_dict = {} + target = None + if cmdhash in targets_dict: + target = targets_dict[cmdhash] + if not show_all and (target.start != start or target.end != end): + # If several builds in a row just run one or two build steps then + # the end times may not go backwards so the last build may not be + # detected as such. However in many cases there will be a build step + # repeated in the two builds and the changed start/stop points for + # that command, identified by the hash, can be used to detect and + # reset the target dictionary. + targets_dict = {} + target = None + if not target: + targets_dict[cmdhash] = target = Target(start, end) + last_end_seen = end + target.targets.append(name) + return targets_dict.values() +def GetExtension(target): + """Return the file extension that best represents a target. + For targets that generate multiple outputs it is important to return a + consistent 'canonical' extension. Ultimately the goal is to group build steps + by type.""" + for output in target.targets: + # Normalize all mojo related outputs to 'mojo'. + if output.count('.mojom') > 0: + extension = 'mojo' + break + # Not a true extension, but a good grouping. + if output.endswith('type_mappings'): + extension = 'type_mappings' + break + extension = os.path.splitext(output)[1] + if len(extension) == 0: + extension = '(no extension found)' + if extension in ['.pdb', '.dll', '.exe']: + extension = 'PEFile (linking)' + # Make sure that .dll and .exe are grouped together and that the + # .dll.lib files don't cause these to be listed as libraries + break + if extension in ['.so', '.TOC']: + extension = '.so (linking)' + # Attempt to identify linking, avoid identifying as '.TOC' + break + return extension +def SummarizeEntries(entries): + """Print a summary of the passed in list of Target objects.""" + # Create a list that is in order by time stamp and has entries for the + # beginning and ending of each build step (one time stamp may have multiple + # entries due to multiple steps starting/stopping at exactly the same time). + # Iterate through this list, keeping track of which tasks are running at all + # times. At each time step calculate a running total for weighted time so + # that when each task ends its own weighted time can easily be calculated. + task_start_stop_times = [] + earliest = -1 + latest = 0 + total_cpu_time = 0 + for target in entries: + if earliest < 0 or target.start < earliest: + earliest = target.start + if target.end > latest: + latest = target.end + total_cpu_time += target.Duration() + task_start_stop_times.append((target.start, 'start', target)) + task_start_stop_times.append((target.end, 'stop', target)) + length = latest - earliest + weighted_total = 0.0 + task_start_stop_times.sort() + # Now we have all task start/stop times sorted by when they happen. If a + # task starts and stops on the same time stamp then the start will come + # first because of the alphabet, which is important for making this work + # correctly. + # Track the tasks which are currently running. + running_tasks = {} + # Record the time we have processed up to so we know how to calculate time + # deltas. + last_time = task_start_stop_times[0][0] + # Track the accumulated weighted time so that it can efficiently be added + # to individual tasks. + last_weighted_time = 0.0 + # Scan all start/stop events. + for event in task_start_stop_times: + time, action_name, target = event + # Accumulate weighted time up to now. + num_running = len(running_tasks) + if num_running > 0: + # Update the total weighted time up to this moment. + last_weighted_time += (time - last_time) / float(num_running) + if action_name == 'start': + # Record the total weighted task time when this task starts. + running_tasks[target] = last_weighted_time + if action_name == 'stop': + # Record the change in the total weighted task time while this task ran. + weighted_duration = last_weighted_time - running_tasks[target] + target.SetWeightedDuration(weighted_duration) + weighted_total += weighted_duration + del running_tasks[target] + last_time = time + assert(len(running_tasks) == 0) + # Warn if the sum of weighted times is off by more than half a second. + if abs(length - weighted_total) > 500: + print('Discrepancy!!! Length = %.3f, weighted total = %.3f' % ( + length, weighted_total)) + # Print the slowest build steps (by weighted time). + print(' Longest build steps:') + entries.sort(key=lambda x: x.WeightedDuration()) + for target in entries[-long_count:]: + print(' %8.1f weighted s to build %s (%.1f s elapsed time)' % ( + target.WeightedDuration(), + target.DescribeTargets(), target.Duration())) + # Sum up the time by file extension/type of the output file + count_by_ext = {} + time_by_ext = {} + weighted_time_by_ext = {} + # Scan through all of the targets to build up per-extension statistics. + for target in entries: + extension = GetExtension(target) + time_by_ext[extension] = time_by_ext.get(extension, 0) + target.Duration() + weighted_time_by_ext[extension] = weighted_time_by_ext.get(extension, + 0) + target.WeightedDuration() + count_by_ext[extension] = count_by_ext.get(extension, 0) + 1 + print(' Time by build-step type:') + # Copy to a list with extension name and total time swapped, to (time, ext) + weighted_time_by_ext_sorted = sorted((y, x) for (x, y) in + weighted_time_by_ext.items()) + # Print the slowest build target types (by weighted time): + for time, extension in weighted_time_by_ext_sorted[-long_ext_count:]: + print(' %8.1f s weighted time to generate %d %s files ' + '(%1.1f s elapsed time sum)' % (time, count_by_ext[extension], + extension, time_by_ext[extension])) + print(' %.1f s weighted time (%.1f s elapsed time sum, %1.1fx ' + 'parallelism)' % (length, total_cpu_time, + total_cpu_time * 1.0 / length)) + print(' %d build steps completed, average of %1.2f/s' % ( + len(entries), len(entries) / (length))) +def main(): + log_file = '.ninja_log' + parser = argparse.ArgumentParser() + parser.add_argument('-C', dest='build_directory', + help='Build directory.') + parser.add_argument('--log-file', + help="specific ninja log file to analyze.") + args, _extra_args = parser.parse_known_args() + if args.build_directory: + log_file = os.path.join(args.build_directory, log_file) + if args.log_file: + log_file = args.log_file + try: + with open(log_file, 'r') as log: + entries = ReadTargets(log, False) + SummarizeEntries(entries) + except IOError: + print('Log file %r not found, no build summary created.' % log_file) + return errno.ENOENT +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c9cb685c..c50dd509 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,30 +1,52 @@ -name: Test Build +name: Build on: [push, pull_request] jobs: build: - name: Build on PlatformIO + name: Build ${{ matrix.target }} runs-on: ubuntu-latest - + strategy: + max-parallel: 2 + matrix: + target: [ESP32CommandStation, ESP32CommandStation.pcb] steps: - - uses: actions/checkout@v1 + - name: Checkout ESP32CommandStation + uses: actions/checkout@v1 + - name: Checkout ESP-IDF v4 + uses: actions/checkout@v1 + with: + repository: espressif/esp-idf + ref: release/v4.0 + submodules: true + - name: Set up Python 3.5 + uses: actions/setup-python@v1 + with: + python-version: 3.5 - name: Install Python Wheel run: pip install wheel - - name: Install PlatformIO Core - run: pip install -U https://github.com/platformio/platformio/archive/develop.zip - - name: Install PlatformIO Espressif32 platform - run: python -m platformio platform install https://github.com/platformio/platform-espressif32.git#feature/stage - - name: Compile Base - run: python -m platformio run -# comment out development branch features -# - name: Compile PCB -# run: python -m platformio run -e pcb -# - name: Compile PCB (OLED sh1106) -# run: python -m platformio run -e pcb_oled_sh1106 -# - name: Compile PCB (OLED ssd1306) -# run: python -m platformio run -e pcb_oled_ssd1306 -# - name: Compile PCB (LCD 16x2) -# run: python -m platformio run -e pcb_lcd_16x2 -# - name: Compile PCB (LCD 20x4) -# run: python -m platformio run -e pcb_lcd_20x4 + - name: Install ESP32 toolchain + run: | + mkdir toolchain + curl -k https://dl.espressif.com/dl/xtensa-esp32-elf-gcc8_2_0-esp-2019r2-linux-amd64.tar.gz | tar zxv -C toolchain + - name: Configure CMake + uses: jwlawson/actions-setup-cmake@v1.0 + with: + cmake-version: '3.17' + - name: Configure Ninja + uses: seanmiddleditch/gha-setup-ninja@master + - name: build + run: bash .github/build.sh ${{ matrix.target }} + - name: Set up Python 2.7 + uses: actions/setup-python@v1 + with: + python-version: 2.7 + - name: Analyze Ninja Build log + run: python .github/ninja-log-analyze.py -C build + - name: Package binaries + uses: actions/upload-artifact@v1 + with: + name: ${{ matrix.target }} + path: binaries + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index d283e99e..43cb526f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ -.pioenvs -.piolibdeps +build +sdkconfig +sdkconfig.old .clang_complete .gcc-flags.json .vscode -include/index_html.h -.vscode/.browse.c_cpp.db* -.vscode/c_cpp_properties.json -.vscode/launch.json -*.code-workspace \ No newline at end of file +*.code-workspace +docs/html +docs/esp32cs.tag +docs/warnings +data/*.gz +pcb/ESP32-CS.*-bak \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..ec7492d4 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,650 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if (IDF_VERSION_MAJOR LESS 4 OR (IDF_VERSION_MAJOR EQUAL 4 AND IDF_VERSION_MINOR GREATER 0)) + message(FATAL_ERROR "ESP32CommandStation requires IDF v4.0") +endif() + +set(SUPPORTED_TARGETS esp32) +project(ESP32CommandStation) + +############################################################################### +# Switch from GNU++11 to C++14 +############################################################################### + +string(REPLACE "-std=gnu++11" "-std=c++14" CXX_OPTIONS "${CXX_COMPILE_OPTIONS}" ) +idf_build_set_property(CXX_COMPILE_OPTIONS "${CXX_OPTIONS}" REPLACE) + +############################################################################### +# Add required compilation flags for customization of OpenMRNLite +############################################################################### + +# none currently + +############################################################################### +# Enable usage of std::stoi/stol/etc +############################################################################### + +idf_build_set_property(COMPILE_DEFINITIONS "-D_GLIBCXX_USE_C99" APPEND) + +############################################################################### +# Search for GZIP application +############################################################################### + +FIND_PROGRAM(GZIP + NAMES gzip + PATHS /bin + /usr/bin + /usr/local/bin +) + +if (NOT GZIP) + message(FATAL_ERROR "Unable to find 'gzip' program") +endif() + +############################################################################### +# Generate a compressed version of web content on-demand +############################################################################### + +add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/data/index.html.gz" + COMMAND ${GZIP} -fk ${CMAKE_CURRENT_SOURCE_DIR}/data/index.html + VERBATIM) +set_property(TARGET ${CMAKE_PROJECT_NAME}.elf APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES + "${CMAKE_CURRENT_SOURCE_DIR}/data/index.html.gz") + +add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/data/jqClock-lite.min.js.gz" + COMMAND ${GZIP} -fk ${CMAKE_CURRENT_SOURCE_DIR}/data/jqClock-lite.min.js + VERBATIM) +set_property(TARGET ${CMAKE_PROJECT_NAME}.elf APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES + "${CMAKE_CURRENT_SOURCE_DIR}/data/jqClock-lite.min.js.gz") + +add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.min.js.gz" + COMMAND ${GZIP} -fk ${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.min.js + VERBATIM) +set_property(TARGET ${CMAKE_PROJECT_NAME}.elf APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES + "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.min.js.gz") + +add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.mobile-1.5.0-rc1.min.js.gz" + COMMAND ${GZIP} -fk ${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.mobile-1.5.0-rc1.min.js + VERBATIM) +set_property(TARGET ${CMAKE_PROJECT_NAME}.elf APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES + "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.mobile-1.5.0-rc1.min.js.gz") + +add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.mobile-1.5.0-rc1.min.css.gz" + COMMAND ${GZIP} -fk ${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.mobile-1.5.0-rc1.min.css + VERBATIM) +set_property(TARGET ${CMAKE_PROJECT_NAME}.elf APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES + "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.mobile-1.5.0-rc1.min.css.gz") + +add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.simple.websocket.min.js.gz" + COMMAND ${GZIP} -fk ${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.simple.websocket.min.js + VERBATIM) +set_property(TARGET ${CMAKE_PROJECT_NAME}.elf APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES + "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.simple.websocket.min.js.gz") + +############################################################################### +# Add web content to the binary +############################################################################### + +target_add_binary_data(${CMAKE_PROJECT_NAME}.elf "${CMAKE_CURRENT_SOURCE_DIR}/data/index.html.gz" BINARY) +target_add_binary_data(${CMAKE_PROJECT_NAME}.elf "${CMAKE_CURRENT_SOURCE_DIR}/data/jqClock-lite.min.js.gz" BINARY) +target_add_binary_data(${CMAKE_PROJECT_NAME}.elf "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.min.js.gz" BINARY) +target_add_binary_data(${CMAKE_PROJECT_NAME}.elf "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.mobile-1.5.0-rc1.min.js.gz" BINARY) +target_add_binary_data(${CMAKE_PROJECT_NAME}.elf "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.mobile-1.5.0-rc1.min.css.gz" BINARY) +target_add_binary_data(${CMAKE_PROJECT_NAME}.elf "${CMAKE_CURRENT_SOURCE_DIR}/data/jquery.simple.websocket.min.js.gz" BINARY) +target_add_binary_data(${CMAKE_PROJECT_NAME}.elf "${CMAKE_CURRENT_SOURCE_DIR}/data/ajax-loader.gif" BINARY) +target_add_binary_data(${CMAKE_PROJECT_NAME}.elf "${CMAKE_CURRENT_SOURCE_DIR}/data/loco-32x32.png" BINARY) + +############################################################################### +# Configuration validations +############################################################################### + +if (NOT CONFIG_FREERTOS_HZ EQUAL 1000) + message(FATAL_ERROR "FreeRTOS tick rate (hz) is required to be 1000.") +endif() + +if (NOT CONFIG_PARTITION_TABLE_FILENAME STREQUAL "ESP32CS-partitions.csv") + message(FATAL_ERROR "The custom partition table option is not enabled in menuconfig and is required for compilation.") +endif() + +if (NOT CONFIG_PARTITION_TABLE_CUSTOM_FILENAME STREQUAL "ESP32CS-partitions.csv") + message(FATAL_ERROR "The custom partition table option is not enabled in menuconfig and is required for compilation.") +endif() + +if (NOT CONFIG_PARTITION_TABLE_CUSTOM) + message(FATAL_ERROR "The custom partition table option is not enabled in menuconfig and is required for compilation.") +endif() + +if (NOT CONFIG_LWIP_SO_RCVBUF) + message(FATAL_ERROR "LwIP SO_RCVBUF is a required option in menuconfig.") +endif() + +if (CONFIG_ESP_MAIN_TASK_STACK_SIZE LESS 8192) + message(FATAL_ERROR "Main task stack size must be at least 8192 bytes.") +endif() + +############################################################################### +# Ensure SSID and PASSWORD are provided. +############################################################################### +if (CONFIG_WIFI_MODE_STATION OR CONFIG_WIFI_MODE_SOFTAP_STATION) + if (NOT CONFIG_WIFI_SSID OR NOT CONFIG_WIFI_PASSWORD) + message(FATAL_ERROR "WiFi SSID & Password are required when WiFi mode is STATION or STATION+SoftAP.") + endif() +endif() + +############################################################################### +# LCC interface configuration validations +############################################################################### +if (CONFIG_LCC_NODE_ID EQUAL "") + message(FATAL_ERROR "LCC Node ID must be defined") +endif() + +if (CONFIG_LCC_CAN_ENABLED) + if (CONFIG_LCC_CAN_RX_PIN EQUAL CONFIG_LCC_CAN_TX_PIN) + message(FATAL_ERROR "LCC CAN RX and LCC CAN TX pin must be unique.") + endif() + + if (CONFIG_LCC_CAN_RX_PIN GREATER_EQUAL 6 AND CONFIG_LCC_CAN_RX_PIN LESS_EQUAL 11) + message(FATAL_ERROR "LCC CAN RX pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (CONFIG_LCC_CAN_TX_PIN GREATER_EQUAL 6 AND CONFIG_LCC_CAN_TX_PIN LESS_EQUAL 11) + message(FATAL_ERROR "LCC CAN TX pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (CONFIG_STATUS_LED) + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_LCC_CAN_RX_PIN) + message(FATAL_ERROR "Status LED data pin and LCC CAN RX pin must be unique.") + endif() + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_LCC_CAN_TX_PIN) + message(FATAL_ERROR "Status LED data pin and LCC CAN TX pin must be unique.") + endif() + endif() + if (CONFIG_GPIO_S88) + if (CONFIG_GPIO_S88_CLOCK_PIN EQUAL CONFIG_LCC_CAN_RX_PIN) + message(FATAL_ERROR "S88 Clock pin and LCC CAN RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_LOAD_PIN EQUAL CONFIG_LCC_CAN_RX_PIN) + message(FATAL_ERROR "S88 Load pin and LCC CAN RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_RESET_PIN EQUAL CONFIG_LCC_CAN_RX_PIN) + message(FATAL_ERROR "S88 Reset pin and LCC CAN RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_CLOCK_PIN EQUAL CONFIG_LCC_CAN_TX_PIN) + message(FATAL_ERROR "S88 Clock pin and LCC CAN TX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_LOAD_PIN EQUAL CONFIG_LCC_CAN_TX_PIN) + message(FATAL_ERROR "S88 Load pin and LCC CAN TX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_RESET_PIN EQUAL CONFIG_LCC_CAN_TX_PIN) + message(FATAL_ERROR "S88 Reset pin and LCC CAN TX pin must be unique.") + endif() + endif() + if (CONFIG_NEXTION) + if (CONFIG_NEXTION_RX_PIN EQUAL CONFIG_LCC_CAN_RX_PIN) + message(FATAL_ERROR "Nextion RX pin and LCC CAN RX pin must be unique.") + endif() + if (CONFIG_NEXTION_RX_PIN EQUAL CONFIG_LCC_CAN_TX_PIN) + message(FATAL_ERROR "Nextion RX pin and LCC CAN TX pin must be unique.") + endif() + if (CONFIG_NEXTION_TX_PIN EQUAL CONFIG_LCC_CAN_RX_PIN) + message(FATAL_ERROR "Nextion TX pin and LCC CAN RX pin must be unique.") + endif() + if (CONFIG_NEXTION_TX_PIN EQUAL CONFIG_LCC_CAN_TX_PIN) + message(FATAL_ERROR "Nextion TX pin and LCC CAN TX pin must be unique.") + endif() + endif() + if (CONFIG_LOCONET) + if (CONFIG_LOCONET_RX_PIN EQUAL CONFIG_LCC_CAN_RX_PIN) + message(FATAL_ERROR "LocoNet RX pin and LCC CAN RX pin must be unique.") + endif() + if (CONFIG_LOCONET_RX_PIN EQUAL CONFIG_LCC_CAN_TX_PIN) + message(FATAL_ERROR "LocoNet RX pin and LCC CAN TX pin must be unique.") + endif() + if (CONFIG_LOCONET_TX_PIN EQUAL CONFIG_LCC_CAN_RX_PIN) + message(FATAL_ERROR "LocoNet TX pin and LCC CAN RX pin must be unique.") + endif() + if (CONFIG_LOCONET_TX_PIN EQUAL CONFIG_LCC_CAN_TX_PIN) + message(FATAL_ERROR "LocoNet TX pin and LCC CAN TX pin must be unique.") + endif() + endif() +endif() + +############################################################################### +# OPS H-Bridge validations +############################################################################### + +if (NOT CONFIG_OPS_ENABLE_PIN OR NOT CONFIG_OPS_SIGNAL_PIN OR NOT CONFIG_OPS_DCC_PREAMBLE_BITS OR CONFIG_OPS_ADC STREQUAL "") + message(FATAL_ERROR "One (or more) OPS H-Bridge required parameters is not defined.") +endif() + +if (CONFIG_OPS_ENABLE_PIN GREATER_EQUAL 6 AND CONFIG_OPS_ENABLE_PIN LESS_EQUAL 11) + message(FATAL_ERROR "OPS H-Bridge enable pin can not be set to pin 6-11 (used by onboard flash).") +endif() + +if (CONFIG_OPS_SIGNAL_PIN GREATER_EQUAL 6 AND CONFIG_OPS_SIGNAL_PIN LESS_EQUAL 11) + message(FATAL_ERROR "OPS H-Bridge signal pin can not be set to pin 6-11 (used by onboard flash).") +endif() + +if (CONFIG_OPS_DCC_PREAMBLE_BITS LESS 11 AND NOT CONFIG_OPS_RAILCOM) + message(FATAL_ERROR "OPS track preamble bits is too low, at least 11 bits are required.") +endif() + +if (CONFIG_OPS_DCC_PREAMBLE_BITS LESS 16 AND CONFIG_OPS_RAILCOM) + message(FATAL_ERROR "OPS track preamble bits is too low, at least 16 bits are required when RailCom is enabled.") +endif() + +if (CONFIG_OPS_DCC_PREAMBLE_BITS GREATER 20) + message(FATAL_ERROR "OPS track preamble bits is too high, a maximum of 20 bits is supported.") +endif() + +if (CONFIG_OPS_HBRIDGE_LMD18200) + if (CONFIG_OPS_THERMAL_PIN GREATER_EQUAL 6 AND CONFIG_OPS_THERMAL_PIN LESS_EQUAL 11) + message(FATAL_ERROR "OPS H-Bridge thermal pin can not be set to pin 6-11 (used by onboard flash).") + endif() +endif() + +if (CONFIG_OPS_RAILCOM) + if(CONFIG_OPS_ENABLE_PIN EQUAL CONFIG_OPS_RAILCOM_ENABLE_PIN) + message(FATAL_ERROR "OPS RailCom enable pin and OPS H-Bridge enable pin must be unique.") + endif() + + if (CONFIG_OPS_SIGNAL_PIN EQUAL CONFIG_OPS_RAILCOM_ENABLE_PIN) + message(FATAL_ERROR "OPS RailCom enable pin and OPS H-Bridge signal pin must be unique.") + endif() + + if (CONFIG_OPS_RAILCOM_ENABLE_PIN GREATER_EQUAL 6 AND CONFIG_OPS_RAILCOM_ENABLE_PIN LESS_EQUAL 11) + message(FATAL_ERROR "OPS RailCom enable pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (NOT CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS) + if (CONFIG_OPS_RAILCOM_ENABLE_PIN LESS 4 OR CONFIG_OPS_RAILCOM_ENABLE_PIN EQUAL 5 OR CONFIG_OPS_RAILCOM_ENABLE_PIN EQUAL 12 OR CONFIG_OPS_RAILCOM_ENABLE_PIN EQUAL 15) + message(FATAL_ERROR "OPS RailCom enable pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + endif() + + if (CONFIG_OPS_ENABLE_PIN EQUAL CONFIG_OPS_RAILCOM_BRAKE_PIN) + message(FATAL_ERROR "OPS RailCom brake pin and OPS H-Bridge enable pin must be unique.") + endif() + + if (CONFIG_OPS_RAILCOM_BRAKE_PIN GREATER_EQUAL 6 AND CONFIG_OPS_RAILCOM_BRAKE_PIN LESS_EQUAL 11) + message(FATAL_ERROR "OPS RailCom brake pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (NOT CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS) + if (CONFIG_OPS_RAILCOM_BRAKE_PIN LESS 4 OR CONFIG_OPS_RAILCOM_BRAKE_PIN EQUAL 5 OR CONFIG_OPS_RAILCOM_BRAKE_PIN EQUAL 12 OR CONFIG_OPS_RAILCOM_BRAKE_PIN EQUAL 15) + message(FATAL_ERROR "OPS RailCom brake pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + endif() + + if (CONFIG_OPS_SIGNAL_PIN EQUAL CONFIG_OPS_RAILCOM_BRAKE_PIN) + message(FATAL_ERROR "OPS RailCom brake pin and OPS H-Bridge signal pin must be unique.") + endif() + + if (CONFIG_OPS_ENABLE_PIN EQUAL CONFIG_OPS_RAILCOM_SHORT_PIN) + message(FATAL_ERROR "OPS RailCom short pin and OPS H-Bridge enable pin must be unique.") + endif() + + if (CONFIG_OPS_RAILCOM_SHORT_PIN GREATER_EQUAL 6 AND CONFIG_OPS_RAILCOM_SHORT_PIN LESS_EQUAL 11) + message(FATAL_ERROR "OPS RailCom short pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (NOT CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS) + if (CONFIG_OPS_RAILCOM_SHORT_PIN LESS 4 OR CONFIG_OPS_RAILCOM_SHORT_PIN EQUAL 5 OR CONFIG_OPS_RAILCOM_SHORT_PIN EQUAL 12 OR CONFIG_OPS_RAILCOM_SHORT_PIN EQUAL 15) + message(FATAL_ERROR "OPS RailCom short pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + endif() + + if (CONFIG_OPS_SIGNAL_PIN EQUAL CONFIG_OPS_RAILCOM_SHORT_PIN) + message(FATAL_ERROR "OPS RailCom short pin and OPS H-Bridge signal pin must be unique.") + endif() + + if (CONFIG_OPS_ENABLE_PIN EQUAL CONFIG_OPS_RAILCOM_UART_RX_PIN) + message(FATAL_ERROR "OPS RailCom UART RX pin and OPS H-Bridge enable pin must be unique.") + endif() + + if (CONFIG_OPS_RAILCOM_UART_RX_PIN GREATER_EQUAL 6 AND CONFIG_OPS_RAILCOM_UART_RX_PIN LESS_EQUAL 11) + message(FATAL_ERROR "OPS RailCom RX pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (NOT CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS) + if (CONFIG_OPS_RAILCOM_UART_RX_PIN LESS 4 OR CONFIG_OPS_RAILCOM_UART_RX_PIN EQUAL 5 OR CONFIG_OPS_RAILCOM_UART_RX_PIN EQUAL 12 OR CONFIG_OPS_RAILCOM_UART_RX_PIN EQUAL 15) + message(FATAL_ERROR "OPS RailCom RX pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + endif() + + if (CONFIG_OPS_SIGNAL_PIN EQUAL CONFIG_OPS_RAILCOM_UART_RX_PIN) + message(FATAL_ERROR "OPS RailCom UART RX pin and OPS H-Bridge signal pin must be unique.") + endif() +endif() + +############################################################################### +# PROG H-Bridge validations +############################################################################### + +if (NOT CONFIG_PROG_ENABLE_PIN OR NOT CONFIG_PROG_SIGNAL_PIN OR NOT CONFIG_PROG_DCC_PREAMBLE_BITS OR CONFIG_PROG_ADC STREQUAL "") + message(FATAL_ERROR "One (or more) PROG H-Bridge required parameters is not defined.") +endif() + +if (CONFIG_PROG_ENABLE_PIN GREATER_EQUAL 6 AND CONFIG_PROG_ENABLE_PIN LESS_EQUAL 11) + message(FATAL_ERROR "PROG H-Bridge enable pin can not be set to pin 6-11 (used by onboard flash).") +endif() + +if (CONFIG_PROG_SIGNAL_PIN GREATER_EQUAL 6 AND CONFIG_PROG_SIGNAL_PIN LESS_EQUAL 11) + message(FATAL_ERROR "PROG H-Bridge signal pin can not be set to pin 6-11 (used by onboard flash).") +endif() + +if (NOT CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS) + if (CONFIG_PROG_ENABLE_PIN LESS 4 OR CONFIG_PROG_ENABLE_PIN EQUAL 5 OR CONFIG_PROG_ENABLE_PIN EQUAL 12 OR CONFIG_PROG_ENABLE_PIN EQUAL 15) + message(FATAL_ERROR "PROG H-Bridge enable pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + + if (CONFIG_PROG_SIGNAL_PIN LESS 4 OR CONFIG_PROG_SIGNAL_PIN EQUAL 5 OR CONFIG_PROG_SIGNAL_PIN EQUAL 12 OR CONFIG_PROG_SIGNAL_PIN EQUAL 15) + message(FATAL_ERROR "PROG H-Bridge signal pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() +endif() + +if (CONFIG_PROG_DCC_PREAMBLE_BITS LESS 22) + message(FATAL_ERROR "PROG track preamble bits is too low, at least 22 bits are required.") +endif() + +if (CONFIG_PROG_DCC_PREAMBLE_BITS GREATER 75) + message(FATAL_ERROR "PROG track preamble bits is too high, a maximum of 75 bits is supported.") +endif() + +if (CONFIG_OPS_ENABLE_PIN EQUAL CONFIG_PROG_ENABLE_PIN) + message(FATAL_ERROR "OPS and PROG H-Bridge enable pin must be unique.") +endif() + +if (CONFIG_OPS_SIGNAL_PIN EQUAL CONFIG_PROG_SIGNAL_PIN) + message(FATAL_ERROR "OPS and PROG H-Bridge signal pin must be unique.") +endif() + +if (CONFIG_STATUS_LED) + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_OPS_ENABLE_PIN) + message(FATAL_ERROR "Status LED data pin and OPS H-Bridge enable pin must be unique.") + endif() + + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_PROG_ENABLE_PIN) + message(FATAL_ERROR "Status LED data pin and PROG H-Bridge enable pin must be unique.") + endif() + + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_OPS_SIGNAL_PIN) + message(FATAL_ERROR "Status LED data pin and OPS H-Bridge signal pin must be unique.") + endif() + + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_PROG_SIGNAL_PIN) + message(FATAL_ERROR "Status LED data pin and PROG H-Bridge signal pin must be unique.") + endif() + + if (CONFIG_STATUS_LED_DATA_PIN GREATER_EQUAL 6 AND CONFIG_STATUS_LED_DATA_PIN LESS_EQUAL 11) + message(FATAL_ERROR "Status LED data pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (NOT CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS) + if (CONFIG_STATUS_LED_DATA_PIN LESS 4 OR CONFIG_STATUS_LED_DATA_PIN EQUAL 5 OR CONFIG_STATUS_LED_DATA_PIN EQUAL 12 OR CONFIG_STATUS_LED_DATA_PIN EQUAL 15) + message(FATAL_ERROR "Status LED data pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + endif() +endif() + +############################################################################### +# Nextion interface validations +############################################################################### + +if (CONFIG_NEXTION) + if (CONFIG_NEXTION_RX_PIN EQUAL CONFIG_NEXTION_TX_PIN) + message(FATAL_ERROR "Nextion RX and TX pin must be unique.") + endif() + + if (CONFIG_NEXTION_RX_PIN GREATER_EQUAL 6 AND CONFIG_NEXTION_RX_PIN LESS_EQUAL 11) + message(FATAL_ERROR "Nextion RX pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (CONFIG_NEXTION_TX_PIN GREATER_EQUAL 6 AND CONFIG_NEXTION_TX_PIN LESS_EQUAL 11) + message(FATAL_ERROR "Nextion TX pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (NOT CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS) + if (CONFIG_NEXTION_RX_PIN LESS 4 OR CONFIG_NEXTION_RX_PIN EQUAL 5 OR CONFIG_NEXTION_RX_PIN EQUAL 12 OR CONFIG_NEXTION_RX_PIN EQUAL 15) + message(FATAL_ERROR "Nextion RX pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + + if (CONFIG_NEXTION_TX_PIN LESS 4 OR CONFIG_NEXTION_TX_PIN EQUAL 5 OR CONFIG_NEXTION_TX_PIN EQUAL 12 OR CONFIG_NEXTION_TX_PIN EQUAL 15) + message(FATAL_ERROR "Nextion TX pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + endif() + + if (CONFIG_STATUS_LED) + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_NEXTION_RX_PIN) + message(FATAL_ERROR "Status LED data pin and Nextion RX pin must be unique.") + endif() + + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_NEXTION_TX_PIN) + message(FATAL_ERROR "Status LED data pin and Nextion TX pin must be unique.") + endif() + endif() + if (CONFIG_GPIO_S88) + if (CONFIG_GPIO_S88_CLOCK_PIN EQUAL CONFIG_NEXTION_RX_PIN) + message(FATAL_ERROR "S88 Clock pin and Nextion RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_LOAD_PIN EQUAL CONFIG_NEXTION_RX_PIN) + message(FATAL_ERROR "S88 Load pin and Nextion RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_RESET_PIN EQUAL CONFIG_NEXTION_RX_PIN) + message(FATAL_ERROR "S88 Reset pin and Nextion RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_CLOCK_PIN EQUAL CONFIG_NEXTION_TX_PIN) + message(FATAL_ERROR "S88 Clock pin and Nextion TX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_LOAD_PIN EQUAL CONFIG_NEXTION_TX_PIN) + message(FATAL_ERROR "S88 Load pin and Nextion TX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_RESET_PIN EQUAL CONFIG_NEXTION_TX_PIN) + message(FATAL_ERROR "S88 Reset pin and Nextion TX pin must be unique.") + endif() + endif() + if (CONFIG_LOCONET) + if (CONFIG_LOCONET_RX_PIN EQUAL CONFIG_NEXTION_RX_PIN) + message(FATAL_ERROR "LocoNet RX pin and Nextion RX pin must be unique.") + endif() + if (CONFIG_LOCONET_RX_PIN EQUAL CONFIG_NEXTION_TX_PIN) + message(FATAL_ERROR "LocoNet RX pin and Nextion TX pin must be unique.") + endif() + if (CONFIG_LOCONET_TX_PIN EQUAL CONFIG_NEXTION_RX_PIN) + message(FATAL_ERROR "LocoNet TX pin and Nextion RX pin must be unique.") + endif() + if (CONFIG_LOCONET_TX_PIN EQUAL CONFIG_NEXTION_TX_PIN) + message(FATAL_ERROR "LocoNet TX pin and Nextion TX pin must be unique.") + endif() + endif() + if (CONFIG_HC12) + if (CONFIG_HC12_RX_PIN EQUAL CONFIG_NEXTION_RX_PIN) + message(FATAL_ERROR "HC12 RX pin and Nextion RX pin must be unique.") + endif() + if (CONFIG_HC12_RX_PIN EQUAL CONFIG_NEXTION_TX_PIN) + message(FATAL_ERROR "HC12 RX pin and Nextion TX pin must be unique.") + endif() + if (CONFIG_HC12_TX_PIN EQUAL CONFIG_NEXTION_RX_PIN) + message(FATAL_ERROR "HC12 TX pin and Nextion RX pin must be unique.") + endif() + if (CONFIG_HC12_TX_PIN EQUAL CONFIG_NEXTION_TX_PIN) + message(FATAL_ERROR "HC12 TX pin and Nextion TX pin must be unique.") + endif() + endif() +endif() + +############################################################################### +# HC12 radio interface validations +############################################################################### + +if (CONFIG_HC12) + if (CONFIG_HC12_RX_PIN EQUAL CONFIG_HC12_TX_PIN) + message(FATAL_ERROR "HC12 RX pin and HC12 TX pin must be unique.") + endif() + + if (CONFIG_HC12_RX_PIN GREATER_EQUAL 6 AND CONFIG_HC12_RX_PIN LESS_EQUAL 11) + message(FATAL_ERROR "HC12 RX pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (CONFIG_HC12_TX_PIN GREATER_EQUAL 6 AND CONFIG_HC12_TX_PIN LESS_EQUAL 11) + message(FATAL_ERROR "HC12 TX pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (NOT CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS) + if (CONFIG_HC12_RX_PIN LESS 4 OR CONFIG_HC12_RX_PIN EQUAL 5 OR CONFIG_HC12_RX_PIN EQUAL 12 OR CONFIG_HC12_RX_PIN EQUAL 15) + message(FATAL_ERROR "HC12 RX pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + + if (CONFIG_HC12_TX_PIN LESS 4 OR CONFIG_HC12_TX_PIN EQUAL 5 OR CONFIG_HC12_TX_PIN EQUAL 12 OR CONFIG_HC12_TX_PIN EQUAL 15) + message(FATAL_ERROR "HC12 TX pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + endif() + + if (CONFIG_STATUS_LED) + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_HC12_RX_PIN) + message(FATAL_ERROR "Status LED data pin and HC12 RX pin must be unique.") + endif() + + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_HC12_TX_PIN) + message(FATAL_ERROR "Status LED data pin and HC12 TX pin must be unique.") + endif() + endif() + if (CONFIG_GPIO_S88) + if (CONFIG_GPIO_S88_CLOCK_PIN EQUAL CONFIG_HC12_RX_PIN) + message(FATAL_ERROR "S88 Clock pin and HC12 RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_LOAD_PIN EQUAL CONFIG_HC12_RX_PIN) + message(FATAL_ERROR "S88 Load pin and HC12 RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_RESET_PIN EQUAL CONFIG_HC12_RX_PIN) + message(FATAL_ERROR "S88 Reset pin and HC12 RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_CLOCK_PIN EQUAL CONFIG_HC12_TX_PIN) + message(FATAL_ERROR "S88 Clock pin and HC12 TX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_LOAD_PIN EQUAL CONFIG_HC12_TX_PIN) + message(FATAL_ERROR "S88 Load pin and HC12 TX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_RESET_PIN EQUAL CONFIG_HC12_TX_PIN) + message(FATAL_ERROR "S88 Reset pin and HC12 TX pin must be unique.") + endif() + endif() + if (CONFIG_LOCONET) + if (CONFIG_LOCONET_RX_PIN EQUAL CONFIG_HC12_RX_PIN) + message(FATAL_ERROR "LocoNet RX pin and HC12 RX pin must be unique.") + endif() + if (CONFIG_LOCONET_RX_PIN EQUAL CONFIG_HC12_TX_PIN) + message(FATAL_ERROR "LocoNet RX pin and HC12 TX pin must be unique.") + endif() + if (CONFIG_LOCONET_TX_PIN EQUAL CONFIG_HC12_RX_PIN) + message(FATAL_ERROR "LocoNet TX pin and HC12 RX pin must be unique.") + endif() + if (CONFIG_LOCONET_TX_PIN EQUAL CONFIG_HC12_TX_PIN) + message(FATAL_ERROR "LocoNet TX pin and HC12 TX pin must be unique.") + endif() + endif() +endif() + +############################################################################### +# LocoNet interface validations +############################################################################### + +if (CONFIG_LOCONET) + if (CONFIG_LOCONET_RX_PIN GREATER_EQUAL 6 AND CONFIG_LOCONET_RX_PIN LESS_EQUAL 11) + message(FATAL_ERROR "LocoNet RX pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (CONFIG_LOCONET_TX_PIN GREATER_EQUAL 6 AND CONFIG_LOCONET_TX_PIN LESS_EQUAL 11) + message(FATAL_ERROR "LocoNet TX pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (NOT CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS) + if (CONFIG_LOCONET_RX_PIN LESS 4 OR CONFIG_LOCONET_RX_PIN EQUAL 5 OR CONFIG_LOCONET_RX_PIN EQUAL 12 OR CONFIG_LOCONET_RX_PIN EQUAL 15) + message(FATAL_ERROR "LocoNet RX pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + + if (CONFIG_LOCONET_TX_PIN LESS 4 OR CONFIG_LOCONET_TX_PIN EQUAL 5 OR CONFIG_LOCONET_TX_PIN EQUAL 12 OR CONFIG_LOCONET_TX_PIN EQUAL 15) + message(FATAL_ERROR "LocoNet TX pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + endif() + + if (CONFIG_STATUS_LED) + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_LOCONET_RX_PIN) + message(FATAL_ERROR "Status LED data pin and LocoNet RX pin must be unique.") + endif() + + if (CONFIG_STATUS_LED_DATA_PIN EQUAL CONFIG_LOCONET_TX_PIN) + message(FATAL_ERROR "Status LED data pin and LocoNet TX pin must be unique.") + endif() + endif() + if (CONFIG_GPIO_S88) + if (CONFIG_GPIO_S88_CLOCK_PIN EQUAL CONFIG_LOCONET_RX_PIN) + message(FATAL_ERROR "S88 Clock pin and LocoNet RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_LOAD_PIN EQUAL CONFIG_LOCONET_RX_PIN) + message(FATAL_ERROR "S88 Load pin and LocoNet RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_RESET_PIN EQUAL CONFIG_LOCONET_RX_PIN) + message(FATAL_ERROR "S88 Reset pin and LocoNet RX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_CLOCK_PIN EQUAL CONFIG_LOCONET_TX_PIN) + message(FATAL_ERROR "S88 Clock pin and LocoNet TX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_LOAD_PIN EQUAL CONFIG_LOCONET_TX_PIN) + message(FATAL_ERROR "S88 Load pin and LocoNet TX pin must be unique.") + endif() + if (CONFIG_GPIO_S88_RESET_PIN EQUAL CONFIG_LOCONET_TX_PIN) + message(FATAL_ERROR "S88 Reset pin and LocoNet TX pin must be unique.") + endif() + endif() +endif() + +############################################################################### +# S88 validations +############################################################################### + +if (CONFIG_GPIO_S88) + + if (CONFIG_GPIO_S88_CLOCK_PIN EQUAL CONFIG_GPIO_S88_LOAD_PIN) + message(FATAL_ERROR "S88 Clock pin and S88 Load pin must be unique.") + endif() + + if (CONFIG_GPIO_S88_CLOCK_PIN EQUAL CONFIG_GPIO_S88_RESET_PIN) + message(FATAL_ERROR "S88 Clock pin and S88 Reset pin must be unique.") + endif() + + if (CONFIG_GPIO_S88_CLOCK_PIN GREATER_EQUAL 6 AND CONFIG_GPIO_S88_CLOCK_PIN LESS_EQUAL 11) + message(FATAL_ERROR "S88 Clock pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (CONFIG_GPIO_S88_LOAD_PIN EQUAL CONFIG_GPIO_S88_RESET_PIN) + message(FATAL_ERROR "S88 Load pin and S88 Reset pin must be unique.") + endif() + + if (CONFIG_GPIO_S88_LOAD_PIN GREATER_EQUAL 6 AND CONFIG_GPIO_S88_LOAD_PIN LESS_EQUAL 11) + message(FATAL_ERROR "S88 Load pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (CONFIG_GPIO_S88_RESET_PIN GREATER_EQUAL 6 AND CONFIG_GPIO_S88_RESET_PIN LESS_EQUAL 11) + message(FATAL_ERROR "S88 Reset pin can not be set to pin 6-11 (used by onboard flash).") + endif() + + if (NOT CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS) + if (CONFIG_GPIO_S88_CLOCK_PIN LESS 4 OR CONFIG_GPIO_S88_CLOCK_PIN EQUAL 5 OR CONFIG_GPIO_S88_CLOCK_PIN EQUAL 12 OR CONFIG_GPIO_S88_CLOCK_PIN EQUAL 15) + message(FATAL_ERROR "S88 Clock pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + + if (CONFIG_GPIO_S88_LOAD_PIN LESS 4 OR CONFIG_GPIO_S88_LOAD_PIN EQUAL 5 OR CONFIG_GPIO_S88_LOAD_PIN EQUAL 12 OR CONFIG_GPIO_S88_LOAD_PIN EQUAL 15) + message(FATAL_ERROR "S88 Load pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + + if (CONFIG_GPIO_S88_RESET_PIN GREATER_EQUAL 0 AND CONFIG_GPIO_S88_RESET_PIN LESS 4 OR CONFIG_GPIO_S88_RESET_PIN EQUAL 5 OR CONFIG_GPIO_S88_RESET_PIN EQUAL 12 OR CONFIG_GPIO_S88_RESET_PIN EQUAL 15) + message(FATAL_ERROR "S88 Reset pin should not use GPIO 0-3, 5, 12, 15. These are reserved pins.") + endif() + endif() +endif() + diff --git a/README.md b/README.md index 3353a51e..13b2b9e2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,19 @@ -# What is ESP32 Command Station? -ESP32 Command Station is an open-source hardware and software Command Station for the operation of DCC equipped model railroads. +# What is ESP32 Command Station +ESP32 Command Station is an open-source hardware and software Command Station for the operation of DCC decoder equipped model railroads. -The ESP32 Command Station consists of an ESP32 micro controller connected to at least one h-bridge that can be connected directly to the tracks of a model railroad. +The ESP32 Command Station consists of an ESP32 module with up to two h-bridge devices to generate the DCC signal for the tracks. + +A more advanced ESP32 Command Station could have some/all of the following: +1. CAN transceiver (MCP2551 or SN65HVD23X) for LCC CAN connectivity. +2. OLED or LCD display for command station status. +3. Addressable RGB LEDs for visual status indicators. Full documentation can be found [here](https://atanisoft.github.io/ESP32CommandStation/). --June 23, 2019 +[![Build Status](https://github.com/atanisoft/ESP32CommandStation/workflows/Build/badge.svg)](https://github.com/atanisoft/ESP32CommandStation/actions) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/952cd95ff4564b8da8ac40e5cdd59781)](https://www.codacy.com/manual/atanisoft/ESP32CommandStation?utm_source=github.com&utm_medium=referral&utm_content=atanisoft/ESP32CommandStation&utm_campaign=Badge_Grade) +[![Contributors](https://img.shields.io/github/contributors/atanisoft/ESP32CommandStation.svg)](https://github.com/atanisoft/ESP32CommandStation/graphs/contributors) +[![Stars](https://img.shields.io/github/stars/atanisoft/ESP32CommandStation.svg)](https://github.com/atanisoft/ESP32CommandStation/stargazers) +[![License](https://img.shields.io/github/license/atanisoft/ESP32CommandStation.svg)](https://github.com/atanisoft/ESP32CommandStation/blob/master/LICENSE) + +-May 01, 2020 diff --git a/build_index_header.py b/build_index_header.py deleted file mode 100644 index a89cf558..00000000 --- a/build_index_header.py +++ /dev/null @@ -1,60 +0,0 @@ -####################################################################### -# ESP32 COMMAND STATION -# -# COPYRIGHT (c) 2017-2019 Mike Dunston -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# You should have received a copy of the GNU General Public License -# along with this program. If not, see http://www.gnu.org/licenses -####################################################################### - -Import("env") -import gzip -import os -import struct -from io import BytesIO -import shutil - -def build_index_html_h(source, target, env): - if os.path.exists('%s/include/index_html.h' % env.subst('$PROJECT_DIR')): - if os.path.getmtime('%s/data/index.html' % env.subst('$PROJECT_DIR')) < os.path.getmtime('%s/include/index_html.h' % env.subst('$PROJECT_DIR')): - return - print("Attempting to compress %s/data/index.html" % env.subst('$PROJECT_DIR')) - gzFile = BytesIO() - with open('%s/data/index.html' % env.subst('$PROJECT_DIR'), 'rb') as f, gzip.GzipFile(mode='wb', fileobj=gzFile) as gz: - shutil.copyfileobj(f, gz) - gzFile.seek(0, os.SEEK_END) - gzLen = gzFile.tell() - gzFile.seek(0, os.SEEK_SET) - print('Compressed index.html.gz file is %d bytes' % gzLen) - with open('%s/include/index_html.h' % env.subst('$PROJECT_DIR'), 'w') as f: - f.write("#pragma once\n") - f.write("const size_t indexHtmlGz_size = {};\n".format(gzLen)) - f.write("const uint8_t indexHtmlGz[] PROGMEM = {\n"); - while True: - block = gzFile.read(16) - if len(block) < 16: - if len(block): - f.write("\t") - for b in block: - # Python 2/3 compat - if type(b) is str: - b = ord(b) - f.write("0x{:02X}, ".format(b)) - f.write("\n") - break - f.write("\t0x{:02X}, 0x{:02X}, 0x{:02X}, 0x{:02X}, " - "0x{:02X}, 0x{:02X}, 0x{:02X}, 0x{:02X}, " - "0x{:02X}, 0x{:02X}, 0x{:02X}, 0x{:02X}, " - "0x{:02X}, 0x{:02X}, 0x{:02X}, 0x{:02X},\n" - .format(*struct.unpack("BBBBBBBBBBBBBBBB", block))) - f.write("};\n") - -env.AddPreAction('$BUILD_DIR/src/Interfaces/WebServer.cpp.o', build_index_html_h) diff --git a/components/Configuration/CMakeLists.txt b/components/Configuration/CMakeLists.txt new file mode 100644 index 00000000..092fee43 --- /dev/null +++ b/components/Configuration/CMakeLists.txt @@ -0,0 +1,22 @@ +set(COMPONENT_SRCS + "ConfigurationManager.cpp" + "LCCStackManager.cpp" + "LCCWiFiManager.cpp" +) + +set(COMPONENT_ADD_INCLUDEDIRS "include" ) + +set(COMPONENT_REQUIRES + "DCCSignalGenerator" + "Esp32HttpServer" + "spiffs" + "vfs" + "fatfs" + "OpenMRNLite" +) + +register_component() + +set_source_files_properties(ConfigurationManager.cpp PROPERTIES COMPILE_FLAGS "-Wno-implicit-fallthrough -Wno-ignored-qualifiers") +set_source_files_properties(LCCStackManager.cpp PROPERTIES COMPILE_FLAGS "-Wno-implicit-fallthrough -Wno-ignored-qualifiers") +set_source_files_properties(LCCWiFiManager.cpp PROPERTIES COMPILE_FLAGS "-Wno-implicit-fallthrough -Wno-ignored-qualifiers") \ No newline at end of file diff --git a/components/Configuration/ConfigurationManager.cpp b/components/Configuration/ConfigurationManager.cpp new file mode 100644 index 00000000..d2751844 --- /dev/null +++ b/components/Configuration/ConfigurationManager.cpp @@ -0,0 +1,417 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2017-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "ConfigurationManager.h" +#include "JsonConstants.h" +#include "LCCStackManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using nlohmann::json; + +static constexpr const char ESP32_CS_CONFIG_JSON[] = "esp32cs-config.json"; +static constexpr const char FACTORY_RESET_MARKER_FILE[] = "resetcfg.txt"; + +#if defined(CONFIG_ESP32CS_FORCE_FACTORY_RESET_PIN) && CONFIG_ESP32CS_FORCE_FACTORY_RESET_PIN >= 0 +#include +GPIO_PIN(FACTORY_RESET, GpioInputPU, CONFIG_ESP32CS_FORCE_FACTORY_RESET_PIN); +#else +#include +typedef DummyPinWithReadHigh FACTORY_RESET_Pin; +#endif // CONFIG_ESP32CS_FORCE_FACTORY_RESET_PIN + +// holder of the parsed configuration. +json csConfig; + +// Helper which converts a string to a uint64 value. +uint64_t string_to_uint64(string value) +{ + // remove period characters if present + value.erase(std::remove(value.begin(), value.end(), '.'), value.end()); + // convert the string to a uint64_t value + return std::stoull(value, nullptr, 16); +} + +void recursiveWalkTree(const string &path, bool remove=false) +{ + DIR *dir = opendir(path.c_str()); + if (dir) + { + dirent *ent = NULL; + while ((ent = readdir(dir)) != NULL) + { + string fullPath = path + "/" + ent->d_name; + if (ent->d_type == DT_REG) + { + struct stat statbuf; + stat(fullPath.c_str(), &statbuf); + if (remove) + { + LOG(VERBOSE, "[Config] Deleting %s (%lu bytes)", fullPath.c_str() + , statbuf.st_size); + ERRNOCHECK(fullPath.c_str(), unlink(fullPath.c_str())); + } + else + { + LOG(INFO, "[Config] %s (%lu bytes) mtime: %s", fullPath.c_str() + , statbuf.st_size, ctime(&statbuf.st_mtime)); + } + } + else if (ent->d_type == DT_DIR) + { + recursiveWalkTree(fullPath, remove); + } + } + closedir(dir); + if (remove) + { + rmdir(path.c_str()); + } + } + else + { + LOG_ERROR("[Config] Failed to open directory: %s", path.c_str()); + } +} + +ConfigurationManager::ConfigurationManager(const esp32cs::Esp32ConfigDef &cfg) + : cfg_(cfg) +{ +#if defined(CONFIG_ESP32CS_FORCE_FACTORY_RESET) + bool factory_reset_config{true}; +#else + bool factory_reset_config{false}; +#endif + + if (FACTORY_RESET_Pin::instance()->is_clr()) + { + factory_reset_config = true; + } + + sdmmc_host_t sd_host = SDSPI_HOST_DEFAULT(); + sdspi_slot_config_t sd_slot = SDSPI_SLOT_CONFIG_DEFAULT(); + esp_vfs_fat_mount_config_t sd_cfg = + { + .format_if_mount_failed = true, + .max_files = 10, + .allocation_unit_size = 0 + }; + esp_err_t err = esp_vfs_fat_sdmmc_mount(CFG_MOUNT, &sd_host, &sd_slot + , &sd_cfg, &sd_); + if (err == ESP_OK) + { + LOG(INFO, "[Config] SD card (%s %.2f MB) mounted successfully." + , sd_->cid.name + , (float)(((uint64_t)sd_->csd.capacity) * sd_->csd.sector_size) / 1048576); + FATFS *fsinfo; + DWORD clusters; + if (f_getfree("0:", &clusters, &fsinfo) == FR_OK) + { + LOG(INFO, "[Config] SD usage: %.2f/%.2f MB", + (float)(((uint64_t)fsinfo->csize * + (fsinfo->n_fatent - 2 - fsinfo->free_clst)) * + fsinfo->ssize) / 1048576L, + (float)(((uint64_t)fsinfo->csize * (fsinfo->n_fatent - 2)) * + fsinfo->ssize) / 1048576L); + } + LOG(INFO, "[Config] SD will be used for persistent storage."); + } + else + { + // unmount the SD VFS since it failed to successfully mount. We will + // remount SPIFFS in it's place instead. + esp_vfs_fat_sdmmc_unmount(); + LOG(INFO, "[Config] SD Card not present or mounting failed, using SPIFFS"); + esp_vfs_spiffs_conf_t conf = + { + .base_path = CFG_MOUNT, + .partition_label = NULL, + .max_files = 10, + .format_if_mount_failed = true + }; + // Attempt to mount the partition + ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf)); + // check that the partition mounted + size_t total = 0, used = 0; + if (esp_spiffs_info(NULL, &total, &used) == ESP_OK) + { + LOG(INFO, "[Config] SPIFFS usage: %.2f/%.2f KiB", (float)(used / 1024.0f) + , (float)(total / 1024.0f)); + } + else + { + LOG_ERROR("[Config] Unable to retrieve SPIFFS utilization statistics."); + } + LOG(INFO, "[Config] SPIFFS will be used for persistent storage."); + } + + if (exists(FACTORY_RESET_MARKER_FILE)) + { + factory_reset_config = true; + } + + if (factory_reset_config) + { + LOG(WARNING, "!!!! WARNING WARNING WARNING WARNING WARNING !!!!"); + LOG(WARNING, "!!!! WARNING WARNING WARNING WARNING WARNING !!!!"); + LOG(WARNING, "!!!! WARNING WARNING WARNING WARNING WARNING !!!!"); + LOG(WARNING, + "[Config] The factory reset flag has been set to true, all persistent " + "data will be cleared."); + uint8_t countdown = 10; + while (--countdown) + { + LOG(WARNING, "[Config] Factory reset will be initiated in %d seconds..." + , countdown); + usleep(SEC_TO_USEC(1)); + } + LOG(WARNING, "[Config] Factory reset starting!"); + } + + LOG(VERBOSE, "[Config] Persistent storage contents:"); + recursiveWalkTree(CFG_MOUNT, factory_reset_config); + // Pre-create ESP32 CS configuration directory. + mkdir(CS_CONFIG_DIR, ACCESSPERMS); + // Pre-create LCC configuration directory. + mkdir(LCC_CFG_DIR, ACCESSPERMS); +} + +void ConfigurationManager::shutdown() +{ + // Unmount the SPIFFS partition + if (esp_spiffs_mounted(NULL)) + { + LOG(INFO, "[Config] Unmounting SPIFFS..."); + ESP_ERROR_CHECK(esp_vfs_spiffs_unregister(NULL)); + } + + // Unmount the SD card if it was mounted + if (sd_) + { + LOG(INFO, "[Config] Unmounting SD..."); + ESP_ERROR_CHECK(esp_vfs_fat_sdmmc_unmount()); + } +} + +bool ConfigurationManager::exists(const string &name) +{ + struct stat statbuf; + string configFilePath = getFilePath(name); + LOG(VERBOSE, "[Config] Checking for %s", configFilePath.c_str()); + // this code is not using access(path, F_OK) as that is not available for + // SPIFFS VFS. stat(path, buf) does work though. + return !stat(configFilePath.c_str(), &statbuf); +} + +void ConfigurationManager::remove(const string &name) +{ + string configFilePath = getFilePath(name); + LOG(VERBOSE, "[Config] Removing %s", configFilePath.c_str()); + unlink(configFilePath.c_str()); +} + +string ConfigurationManager::load(const string &name) +{ + string configFilePath = getFilePath(name); + if (!exists(name)) + { + LOG_ERROR("[Config] %s does not exist, returning blank json object" + , configFilePath.c_str()); + return "{}"; + } + LOG(VERBOSE, "[Config] Loading %s", configFilePath.c_str()); + return read_file_to_string(configFilePath); +} + +void ConfigurationManager::store(const char *name, const string &content) +{ + string configFilePath = getFilePath(name); + LOG(VERBOSE, "[Config] Storing %s, %d bytes", configFilePath.c_str() + , content.length()); + write_string_to_file(configFilePath, content); +} + +string ConfigurationManager::getFilePath(const string &name) +{ + return StringPrintf("%s/%s", CS_CONFIG_DIR, name.c_str()); +} + +string ConfigurationManager::getCSConfig() +{ + nlohmann::json clone = csConfig; + openlcb::TcpClientConfig uplink = + cfg_.seg().wifi().uplink(); + openmrn_arduino::HubConfiguration hub = cfg_.seg().wifi().hub(); + esp32cs::TrackOutputConfig ops = cfg_.seg().hbridge().entry(0); + esp32cs::TrackOutputConfig prog = cfg_.seg().hbridge().entry(1); + + // insert non-persistent CDI elements that we can modify from web + clone[JSON_CDI_NODE][JSON_CDI_UPLINK_NODE] = + { + {JSON_CDI_UPLINK_RECONNECT_NODE, + CDI_READ_TRIMMED(uplink.reconnect, configFd_)}, + {JSON_CDI_UPLINK_MODE_NODE, + CDI_READ_TRIMMED(uplink.search_mode, configFd_)}, + {JSON_CDI_UPLINK_AUTO_HOST_NODE, + uplink.auto_address().host_name().read(configFd_)}, + {JSON_CDI_UPLINK_AUTO_SERVICE_NODE, + uplink.auto_address().service_name().read(configFd_)}, + {JSON_CDI_UPLINK_MANUAL_HOST_NODE, + uplink.manual_address().ip_address().read(configFd_)}, + {JSON_CDI_UPLINK_MANUAL_PORT_NODE, + CDI_READ_TRIMMED(uplink.manual_address().port, configFd_)}, + }; + clone[JSON_CDI_NODE][JSON_CDI_HUB_NODE] = + { + {JSON_CDI_HUB_ENABLE_NODE, CDI_READ_TRIMMED(hub.enable, configFd_)}, + {JSON_CDI_HUB_PORT_NODE, CDI_READ_TRIMMED(hub.port, configFd_)}, + {JSON_CDI_HUB_SERVICE_NODE, hub.service_name().read(configFd_)}, + }; + clone[JSON_CDI_NODE][JSON_HBRIDGES_NODE] = + { + {CONFIG_OPS_TRACK_NAME, + { + {JSON_DESCRIPTION_NODE, ops.description().read(configFd_)}, + {JSON_CDI_HBRIDGE_SHORT_EVENT_NODE, + uint64_to_string_hex(ops.event_short().read(configFd_))}, + {JSON_CDI_HBRIDGE_SHORT_CLEAR_EVENT_NODE, + uint64_to_string_hex(ops.event_short_cleared().read(configFd_))}, + {JSON_CDI_HBRIDGE_SHUTDOWN_EVENT_NODE, + uint64_to_string_hex(ops.event_shutdown().read(configFd_))}, + {JSON_CDI_HBRIDGE_SHUTDOWN_CLEAR_EVENT_NODE, + uint64_to_string_hex(ops.event_shutdown_cleared().read(configFd_))}, + {JSON_CDI_HBRIDGE_THERMAL_EVENT_NODE, + uint64_to_string_hex(ops.event_thermal_shutdown().read(configFd_))}, + {JSON_CDI_HBRIDGE_THERMAL_CLEAR_EVENT_NODE, + uint64_to_string_hex(ops.event_thermal_shutdown_cleared().read(configFd_))}, + } + }, + {CONFIG_PROG_TRACK_NAME, + { + {JSON_DESCRIPTION_NODE, prog.description().read(configFd_)}, + {JSON_CDI_HBRIDGE_SHORT_EVENT_NODE, + uint64_to_string_hex(prog.event_short().read(configFd_))}, + {JSON_CDI_HBRIDGE_SHORT_CLEAR_EVENT_NODE, + uint64_to_string_hex(prog.event_short_cleared().read(configFd_))}, + {JSON_CDI_HBRIDGE_SHUTDOWN_EVENT_NODE, + uint64_to_string_hex(prog.event_shutdown().read(configFd_))}, + {JSON_CDI_HBRIDGE_SHUTDOWN_CLEAR_EVENT_NODE, + uint64_to_string_hex(prog.event_shutdown_cleared().read(configFd_))}, + } + }, + }; + return clone.dump(); +} + +void ConfigurationManager::force_factory_reset() +{ + LOG(INFO, "[Config] Enabling forced factory_reset."); + string marker = "force factory reset"; + store(FACTORY_RESET_MARKER_FILE, marker); + Singleton::instance()->reboot_node(); +} + +#if defined(CONFIG_GPIO_OUTPUTS) || defined(CONFIG_GPIO_SENSORS) +bool is_restricted_pin(int8_t pin) +{ + vector restrictedPins + { +#if !defined(CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS) + 0, // Bootstrap / Firmware Flash Download + 1, // UART0 TX + 2, // Bootstrap / Firmware Flash Download + 3, // UART0 RX + 5, // Bootstrap + 6, 7, 8, 9, 10, 11, // on-chip flash pins + 12, 15, // Bootstrap / SD pins +#endif // ! CONFIG_ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS + CONFIG_OPS_ENABLE_PIN + , CONFIG_OPS_SIGNAL_PIN +#if defined(CONFIG_OPS_THERMAL_PIN) + , CONFIG_OPS_THERMAL_PIN +#endif + , CONFIG_PROG_ENABLE_PIN + , CONFIG_PROG_SIGNAL_PIN + +#if defined(CONFIG_OPS_RAILCOM) +#if defined(CONFIG_OPS_HBRIDGE_LMD18200) + , CONFIG_OPS_RAILCOM_BRAKE_PIN +#endif + , CONFIG_OPS_RAILCOM_ENABLE_PIN + , CONFIG_OPS_RAILCOM_UART_RX_PIN +#endif // CONFIG_OPS_RAILCOM + +#if defined(CONFIG_LCC_CAN_ENABLED) + , CONFIG_LCC_CAN_RX_PIN + , CONFIG_LCC_CAN_TX_PIN +#endif + +#if defined(CONFIG_HC12) + , CONFIG_HC12_RX_PIN + , CONFIG_HC12_TX_PIN +#endif + +#if defined(CONFIG_NEXTION) + , CONFIG_NEXTION_RX_PIN + , CONFIG_NEXTION_TX_PIN +#endif + +#if defined(CONFIG_DISPLAY_TYPE_OLED) || defined(CONFIG_DISPLAY_TYPE_LCD) + , CONFIG_DISPLAY_SCL + , CONFIG_DISPLAY_SDA +#if defined(CONFIG_DISPLAY_OLED_RESET_PIN) && CONFIG_DISPLAY_OLED_RESET_PIN != -1 + , CONFIG_DISPLAY_OLED_RESET_PIN +#endif +#endif + +#if defined(CONFIG_LOCONET) + , CONFIG_LOCONET_RX_PIN + , CONFIG_LOCONET_TX_PIN +#endif + +#if defined(CONFIG_GPIO_S88) + , CONFIG_GPIO_S88_CLOCK_PIN + , CONFIG_GPIO_S88_LOAD_PIN +#if defined(CONFIG_GPIO_S88_RESET_PIN) && CONFIG_GPIO_S88_RESET_PIN != -1 + , CONFIG_GPIO_S88_RESET_PIN +#endif +#endif + +#if defined(CONFIG_STATUS_LED) + , CONFIG_STATUS_LED_DATA_PIN +#endif + }; + + return std::find(restrictedPins.begin() + , restrictedPins.end() + , pin) != restrictedPins.end(); +} +#endif // CONFIG_GPIO_OUTPUTS || CONFIG_GPIO_SENSORS \ No newline at end of file diff --git a/components/Configuration/Kconfig.projbuild b/components/Configuration/Kconfig.projbuild new file mode 100644 index 00000000..30fe237b --- /dev/null +++ b/components/Configuration/Kconfig.projbuild @@ -0,0 +1,89 @@ +menu "WiFi Configuration" + config HOSTNAME_PREFIX + string "Hostname prefix" + default "esp32cs_" + help + The LCC node id will be appended to this value, ie: esp32cs_050101013F00. + + choice WIFI_MODE + bool "WiFi mode" + default WIFI_MODE_SOFTAP + config WIFI_MODE_STATION + bool "Connect to SSID" + config WIFI_MODE_SOFTAP + bool "Create SoftAP" + config WIFI_MODE_SOFTAP_STATION + bool "Connect to SSID and create SoftAP" + endchoice + + config WIFI_SOFTAP_SSID + string "SoftAP SSID" + default "esp32cs" + depends on WIFI_MODE_SOFTAP + + config WIFI_SOFTAP_PASSWORD + string "SoftAP Password" + default "esp32cs" + depends on WIFI_MODE_SOFTAP + + config WIFI_SSID + string "SSID" + depends on WIFI_MODE_STATION || WIFI_MODE_SOFTAP_STATION + + config WIFI_PASSWORD + string "Password" + depends on WIFI_MODE_STATION || WIFI_MODE_SOFTAP_STATION + + choice WIFI_IP_TYPE + bool "WiFi IP" + default WIFI_IP_DHCP + config WIFI_IP_DHCP + bool "DHCP" + config WIFI_IP_STATIC + bool "Static" + endchoice + + config WIFI_STATIC_IP_ADDRESS + string "IP address" + default "10.0.0.155" + depends on WIFI_IP_STATIC + + config WIFI_STATIC_IP_GATEWAY + string "Gateway IP address" + default "10.0.0.1" + depends on WIFI_IP_STATIC + + config WIFI_STATIC_IP_SUBNET + string "Subnet mask" + default "255.255.255.0" + depends on WIFI_IP_STATIC + + config WIFI_STATIC_IP_DNS + string "Primary DNS address" + default "8.8.8.8" + depends on WIFI_IP_STATIC + + config WIFI_SOFT_AP_CHANNEL + int + default 6 +endmenu + +menu "Advanced Configuration Settings" + + config ESP32CS_FORCE_FACTORY_RESET + bool "Perform factory reset on startup" + default n + help + Enabling this option will force clear all persistent configuration + settings upon startup, including all LCC configuration data. This + would not be recommended to be enabled for most use cases. + + config ESP32CS_FORCE_FACTORY_RESET_PIN + int "Factory Reset pin" + range -1 39 + default -1 + help + When this pin is held LOW during startup all persistent + configuration will be cleared and defaults will be restored. Note + this will also clear the LCC configuration data. +endmenu \ No newline at end of file diff --git a/components/Configuration/LCCStackManager.cpp b/components/Configuration/LCCStackManager.cpp new file mode 100644 index 00000000..93ea6972 --- /dev/null +++ b/components/Configuration/LCCStackManager.cpp @@ -0,0 +1,293 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "LCCStackManager.h" +#include "CDIHelper.h" +#include "ConfigurationManager.h" +#if defined(CONFIG_LCC_CAN_ENABLED) +#include +#endif // CONFIG_LCC_CAN_ENABLED +#include +#include + +namespace esp32cs +{ + +static constexpr const char LCC_NODE_ID_FILE[] = "lcc-node"; +static constexpr const char LCC_RESET_MARKER_FILE[] = "lcc-rst"; +static constexpr const char LCC_CAN_MARKER_FILE[] = "lcc-can"; + +#if defined(CONFIG_LCC_CAN_ENABLED) +static std::unique_ptr canBridge; +bool can_run = true; +bool can_running = false; + +static void* can_bridge_task(void *param) +{ + can_running = true; + while (can_run) + { + canBridge->run(); + vTaskDelay(pdMS_TO_TICKS(1)); + } + canBridge.reset(nullptr); + can_running = false; + return nullptr; +} +#endif // CONFIG_LCC_CAN_ENABLED + +LCCStackManager::LCCStackManager(const esp32cs::Esp32ConfigDef &cfg) : cfg_(cfg) +{ +#if defined(CONFIG_LCC_FACTORY_RESET) || defined(CONFIG_ESP32CS_FORCE_FACTORY_RESET) + bool lcc_factory_reset{true}; +#else + bool lcc_factory_reset{false}; +#endif + auto cfg_mgr = Singleton::instance(); + struct stat statbuf; + + // If we are not currently forcing a factory reset, verify if the LCC config + // file is the correct size. If it is not the expected size force a factory + // reset. + if (!lcc_factory_reset && + stat(LCC_CONFIG_FILE, &statbuf) != 0 && + statbuf.st_size != openlcb::CONFIG_FILE_SIZE) + { + LOG(WARNING + , "[LCC] Corrupt/missing configuration file detected, %s is not the " + "expected size: %lu vs %zu bytes." + , LCC_CONFIG_FILE, statbuf.st_size, openlcb::CONFIG_FILE_SIZE); + lcc_factory_reset = true; + } + else + { + LOG(VERBOSE, "[LCC] node config file(%s) is expected size %lu bytes" + , LCC_CONFIG_FILE, statbuf.st_size); + } + if (cfg_mgr->exists(LCC_RESET_MARKER_FILE) || lcc_factory_reset) + { + cfg_mgr->remove(LCC_RESET_MARKER_FILE); + if (!stat(LCC_CONFIG_FILE, &statbuf)) + { + LOG(WARNING, "[LCC] Forcing regeneration of %s", LCC_CONFIG_FILE); + ERRNOCHECK(LCC_CONFIG_FILE, unlink(LCC_CONFIG_FILE)); + } + if (!stat(LCC_CDI_XML, &statbuf)) + { + LOG(WARNING, "[LCC] Forcing regeneration of %s", LCC_CDI_XML); + ERRNOCHECK(LCC_CDI_XML, unlink(LCC_CDI_XML)); + } + cfg_mgr->remove(LCC_NODE_ID_FILE); + cfg_mgr->remove(LCC_CAN_MARKER_FILE); + } + + if (!cfg_mgr->exists(LCC_NODE_ID_FILE)) + { + LOG(INFO, "[LCC] Initializing configuration data..."); + set_node_id(uint64_to_string_hex(UINT64_C(CONFIG_LCC_NODE_ID)), false); +#if defined(CONFIG_LCC_CAN_ENABLED) + reconfigure_can(true, false); +#endif // CONFIG_LCC_CAN_ENABLED + } + LOG(INFO, "[LCC] Loading configuration"); + string node_id_str = cfg_mgr->load(LCC_NODE_ID_FILE); + nodeID_ = string_to_uint64(node_id_str); + + LOG(INFO, "[LCC] Initializing Stack (node-id: %s)" + , uint64_to_string_hex(nodeID_).c_str()); +#ifdef CONFIG_LCC_TCP_STACK + stack_ = new openlcb::SimpleTcpStack(nodeID_); +#else + stack_ = new openlcb::SimpleCanStack(nodeID_); +#if defined(CONFIG_LCC_CAN_ENABLED) + if (cfg_mgr->exists(LCC_CAN_MARKER_FILE)) + { + LOG(INFO, "[LCC] Enabling CAN interface (rx: %d, tx: %d)" + , CONFIG_LCC_CAN_RX_PIN, CONFIG_LCC_CAN_TX_PIN); + can_ = new Esp32HardwareCan("esp32can" + , (gpio_num_t)CONFIG_LCC_CAN_RX_PIN + , (gpio_num_t)CONFIG_LCC_CAN_TX_PIN + , false); + canBridge.reset( + new CanBridge((Can *)can_ + , ((openlcb::SimpleCanStack *)stack_)->can_hub())); + os_thread_create(nullptr, "CAN-BRIDGE", -1, 2048, can_bridge_task, nullptr); + } +#endif // CONFIG_LCC_CAN_ENABLED +#endif // CONFIG_LCC_TCP_STACK +} + +openlcb::SimpleStackBase *LCCStackManager::stack() +{ + return stack_; +} + +Service *LCCStackManager::service() +{ + return stack_->service(); +} + +openlcb::Node *LCCStackManager::node() +{ + return stack_->node(); +} + +openlcb::SimpleInfoFlow *LCCStackManager::info_flow() +{ + return stack_->info_flow(); +} + +openlcb::MemoryConfigHandler *LCCStackManager::memory_config_handler() +{ + return stack_->memory_config_handler(); +} + +#ifndef CONFIG_LCC_SD_FSYNC_SEC +#define CONFIG_LCC_SD_FSYNC_SEC 10 +#endif + +void LCCStackManager::start(bool is_sd) +{ + // Create the CDI.xml dynamically if it doesn't already exist. + CDIHelper::create_config_descriptor_xml(cfg_, LCC_CDI_XML, stack_); + + // Create the default internal configuration file if it doesn't already exist. + fd_ = + stack_->create_config_file_if_needed(cfg_.seg().internal_config() + , CONFIG_ESP32CS_CDI_VERSION + , openlcb::CONFIG_FILE_SIZE); + LOG(INFO, "[LCC] Config file opened using fd:%d", fd_); + + if (is_sd) + { + // ESP32 FFat library uses a 512b cache in memory by default for the SD VFS + // adding a periodic fsync call for the LCC configuration file ensures that + // config changes are saved since the LCC config file is less than 512b. + LOG(INFO, "[LCC] Creating automatic fsync(%d) calls every %d seconds." + , fd_, CONFIG_LCC_SD_FSYNC_SEC); + configAutoSync_ = + new AutoSyncFileFlow(stack_->service(), fd_ + , SEC_TO_USEC(CONFIG_LCC_SD_FSYNC_SEC)); + } +#if defined(CONFIG_LCC_PRINT_ALL_PACKETS) && !defined(CONFIG_LCC_TCP_STACK) + LOG(INFO, "[LCC] Configuring LCC packet printer"); + ((openlcb::SimpleCanStack *)stack_)->print_all_packets(); +#endif +} + +void LCCStackManager::shutdown() +{ +#if defined(CONFIG_LCC_CAN_ENABLED) + if (canBridge.get() != nullptr) + { + can_run = false; + // wait for can task shutdown + while (can_running) + { + vTaskDelay(pdMS_TO_TICKS(1)); + } + } +#endif // CONFIG_LCC_CAN_ENABLED + + // Shutdown the auto-sync handler if it is running before unmounting the FS. + if (configAutoSync_ != nullptr) + { + LOG(INFO, "[LCC] Disabling automatic fsync(%d) calls...", fd_); + SyncNotifiable n; + configAutoSync_->shutdown(&n); + LOG(INFO, "[LCC] Waiting for sync to stop"); + n.wait_for_notification(); + configAutoSync_ = nullptr; + } + + // shutdown the executor so that no more tasks will run + LOG(INFO, "[LCC] Shutting down executor"); + stack_->executor()->shutdown(); + + // close the config file if it is open + if (fd_ >= 0) + { + LOG(INFO, "[LCC] Closing config file."); + ::close(fd_); + } +} + +bool LCCStackManager::set_node_id(string node_id, bool restart) +{ + uint64_t new_node_id = string_to_uint64(node_id); + if (new_node_id != nodeID_) + { + LOG(INFO, "[LCC] Persisting updated NodeID: %s", node_id.c_str()); + auto cfg = Singleton::instance(); + string node_id_str = uint64_to_string_hex(new_node_id); + cfg->store(LCC_NODE_ID_FILE, node_id_str); + if (restart) + { + factory_reset(); + reboot_node(); + return true; + } + } + return false; +} + +bool LCCStackManager::reconfigure_can(bool enable, bool restart) +{ +#if defined(CONFIG_LCC_CAN_ENABLED) + auto cfg = Singleton::instance(); + if (cfg->exists(LCC_CAN_MARKER_FILE) && !enable) + { + LOG(INFO, "[LCC] Disabling CAN interface, reinitialization required."); + cfg->remove(LCC_CAN_MARKER_FILE); + } + else if (!cfg->exists(LCC_CAN_MARKER_FILE) && enable) + { + LOG(INFO, "[LCC] Enabling CAN interface, reinitialization required."); + string can_str = "true"; + cfg->store(LCC_CAN_MARKER_FILE, can_str); + } + if (restart) + { + reboot_node(); + return true; + } +#endif // CONFIG_LCC_CAN_ENABLED + return false; +} + +void LCCStackManager::factory_reset() +{ + LOG(INFO, "[LCC] Enabling forced factory_reset."); + auto cfg = Singleton::instance(); + string marker = uint64_to_string_hex(nodeID_); + cfg->store(LCC_RESET_MARKER_FILE, marker); +} + +std::string LCCStackManager::get_config_json() +{ + auto cfg = Singleton::instance(); + return StringPrintf("\"lcc\":{\"id\":\"%s\", \"can\":%s}" + , uint64_to_string_hex(nodeID_).c_str() + , cfg->exists(LCC_CAN_MARKER_FILE) ? "true" : "false"); +} + +void LCCStackManager::reboot_node() +{ + stack_->executor()->add(new CallbackExecutable([](){ reboot(); })); +} + +} // namespace esp32cs \ No newline at end of file diff --git a/components/Configuration/LCCWiFiManager.cpp b/components/Configuration/LCCWiFiManager.cpp new file mode 100644 index 00000000..f695c8c5 --- /dev/null +++ b/components/Configuration/LCCWiFiManager.cpp @@ -0,0 +1,333 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "LCCWiFiManager.h" +#include "JsonConstants.h" + +#include "sdkconfig.h" + +#include +#include +#include +#include +#include + +namespace esp32cs +{ + +static constexpr const char WIFI_STATION_CFG[] = "wifi-sta"; +static constexpr const char WIFI_STATION_DNS_CFG[] = "wifi-dns"; +static constexpr const char WIFI_STATION_IP_CFG[] = "wifi-ip"; +static constexpr const char WIFI_SOFTAP_CFG[] = "wifi-ap"; + +#ifndef CONFIG_WIFI_STATIC_IP_ADDRESS +#define CONFIG_WIFI_STATIC_IP_ADDRESS "" +#endif + +#ifndef CONFIG_WIFI_STATIC_IP_GATEWAY +#define CONFIG_WIFI_STATIC_IP_GATEWAY "" +#endif + +#ifndef CONFIG_WIFI_STATIC_IP_SUBNET +#define CONFIG_WIFI_STATIC_IP_SUBNET "" +#endif + +#ifndef CONFIG_WIFI_STATIC_IP_DNS +#define CONFIG_WIFI_STATIC_IP_DNS "" +#endif + +#ifndef CONFIG_WIFI_SOFTAP_SSID +#define CONFIG_WIFI_SOFTAP_SSID "esp32cs" +#endif + +#ifndef CONFIG_WIFI_SOFTAP_PASSWORD +#define CONFIG_WIFI_SOFTAP_PASSWORD "esp32cs" +#endif + +#ifndef CONFIG_WIFI_SSID +#define CONFIG_WIFI_SSID "esp32cs" +#endif + +#ifndef CONFIG_WIFI_PASSWORD +#define CONFIG_WIFI_PASSWORD "esp32cs" +#endif + +LCCWiFiManager::LCCWiFiManager(openlcb::SimpleStackBase *stack + , const esp32cs::Esp32ConfigDef &cfg) + : stack_(stack), cfg_(cfg) +{ + auto cfg_mgr = Singleton::instance(); + if (!cfg_mgr->exists(WIFI_SOFTAP_CFG) && !cfg_mgr->exists(WIFI_STATION_CFG)) + { +#if defined(CONFIG_WIFI_MODE_SOFTAP) + reconfigure_mode(JSON_VALUE_WIFI_MODE_SOFTAP_ONLY, false); +#elif defined(CONFIG_WIFI_MODE_SOFTAP_STATION) + reconfigure_mode(JSON_VALUE_WIFI_MODE_SOFTAP_STATION, false); +#else + reconfigure_mode(JSON_VALUE_WIFI_MODE_STATION_ONLY, false); +#endif +#if defined(CONFIG_WIFI_MODE_SOFTAP_STATION) || \ + defined(CONFIG_WIFI_MODE_STATION) + reconfigure_station(CONFIG_WIFI_SSID, CONFIG_WIFI_PASSWORD + , CONFIG_WIFI_STATIC_IP_ADDRESS + , CONFIG_WIFI_STATIC_IP_GATEWAY + , CONFIG_WIFI_STATIC_IP_SUBNET + , CONFIG_WIFI_STATIC_IP_DNS, false); +#endif // CONFIG_WIFI_MODE_SOFTAP_STATION || CONFIG_WIFI_MODE_STATION + } + + LOG(INFO, "[WiFi] Loading configuration"); + if (cfg_mgr->exists(WIFI_SOFTAP_CFG) && !cfg_mgr->exists(WIFI_STATION_CFG)) + { + mode_ = WIFI_MODE_AP; + string station_cfg = cfg_mgr->load(WIFI_SOFTAP_CFG); + std::pair cfg = http::break_string(station_cfg, "\n"); + ssid_ = cfg.first; + password_ = cfg.second; + LOG(INFO, "[WiFi] SoftAP only (ssid: %s)", ssid_.c_str()); + } + else if (cfg_mgr->exists(WIFI_SOFTAP_CFG) && + cfg_mgr->exists(WIFI_STATION_CFG)) + { + LOG(INFO, "[WiFi] SoftAP and Station"); + mode_ = WIFI_MODE_APSTA; + } + else if (cfg_mgr->exists(WIFI_STATION_CFG)) + { + LOG(INFO, "[WiFi] Station only"); + mode_ = WIFI_MODE_STA; + } + else + { + LOG(INFO + , "[WiFi] Unable to locate SoftAP or Station configuration, enabling " + "SoftAP: %s", CONFIG_WIFI_SOFTAP_SSID); + mode_ = WIFI_MODE_AP; + ssid_ = CONFIG_WIFI_SOFTAP_SSID; + password_ = CONFIG_WIFI_SOFTAP_PASSWORD; + } + if (mode_ != WIFI_MODE_AP) + { + string station_cfg = cfg_mgr->load(WIFI_STATION_CFG); + std::pair cfg = http::break_string(station_cfg, "\n"); + ssid_ = cfg.first; + password_ = cfg.second; + if (cfg_mgr->exists(WIFI_STATION_IP_CFG)) + { + std::vector ip_parts; + string ip_cfg = cfg_mgr->load(WIFI_STATION_IP_CFG); + http::tokenize(ip_cfg, ip_parts, "\n"); + stationIP_.reset(new tcpip_adapter_ip_info_t()); + stationIP_->ip.addr = ipaddr_addr(ip_parts[0].c_str()); + stationIP_->gw.addr = ipaddr_addr(ip_parts[1].c_str()); + stationIP_->netmask.addr = ipaddr_addr(ip_parts[2].c_str()); + LOG(INFO, "[WiFi] Static IP:" IPSTR ", gateway:" IPSTR ",netmask:" IPSTR, + IP2STR(&stationIP_->ip), IP2STR(&stationIP_->gw), IP2STR(&stationIP_->netmask)); + } + if (cfg_mgr->exists(WIFI_STATION_DNS_CFG)) + { + string dns = cfg_mgr->load(WIFI_STATION_DNS_CFG); + stationDNS_.u_addr.ip4.addr = ipaddr_addr(dns.c_str()); + LOG(INFO, "[WiFi] DNS configured: " IPSTR + , IP2STR(&stationDNS_.u_addr.ip4)); + } + } + else if (cfg_mgr->exists(WIFI_SOFTAP_CFG)) + { + string station_cfg = cfg_mgr->load(WIFI_SOFTAP_CFG); + std::pair cfg = http::break_string(station_cfg, "\n"); + ssid_ = cfg.first; + password_ = cfg.second; + } + + // TODO: Switch to SimpleStackBase * instead of casting to SimpleCanStack * + // once Esp32WiFiManager supports this. + LOG(INFO, "[WiFi] Starting WiFiManager"); + wifi_.reset( + new Esp32WiFiManager(ssid_.c_str(), password_.c_str() + , (openlcb::SimpleCanStack *)stack_ + , cfg_.seg().wifi(), CONFIG_HOSTNAME_PREFIX, mode_ + , stationIP_.get(), stationDNS_ + , CONFIG_WIFI_SOFT_AP_CHANNEL)); + + // When operating as both SoftAP and Station mode it is not necessary to wait + // for the station to be UP during CS startup. + if (mode_ == WIFI_MODE_APSTA) + { + wifi_->wait_for_ssid_connect(false); + } + +} + +void LCCWiFiManager::shutdown() +{ + wifi_.reset(nullptr); +} + +void LCCWiFiManager::reconfigure_mode(string mode, bool restart) +{ + auto cfg_mgr = Singleton::instance(); + if (!mode.compare(JSON_VALUE_WIFI_MODE_SOFTAP_ONLY)) + { + cfg_mgr->remove(WIFI_STATION_CFG); + string soft_ap_cfg = StringPrintf("%s\n%s", CONFIG_WIFI_SOFTAP_SSID + , CONFIG_WIFI_SOFTAP_PASSWORD); + cfg_mgr->store(WIFI_SOFTAP_CFG, soft_ap_cfg); + } + else if (!mode.compare(JSON_VALUE_WIFI_MODE_SOFTAP_STATION)) + { + string soft_ap_cfg = StringPrintf("%s\n%s", CONFIG_WIFI_SOFTAP_SSID + , CONFIG_WIFI_SOFTAP_PASSWORD); + cfg_mgr->store(WIFI_SOFTAP_CFG, soft_ap_cfg); + } + else + { + cfg_mgr->remove(WIFI_SOFTAP_CFG); + } + if (restart) + { + stack_->executor()->add(new CallbackExecutable([]() + { + reboot(); + })); + } +} + +void LCCWiFiManager::reconfigure_station(string ssid, string password + , string ip, string gateway + , string subnet, string dns + , bool restart) +{ + LOG(VERBOSE, "[WiFi] reconfigure_station(%s,%s,%s,%s,%s,%s,%d)", ssid.c_str() + , password.c_str(), ip.c_str(), gateway.c_str(), subnet.c_str() + , dns.c_str(), restart); + auto cfg_mgr = Singleton::instance(); + string station_cfg = StringPrintf("%s\n%s", ssid.c_str(), password.c_str()); + cfg_mgr->store(WIFI_STATION_CFG, station_cfg); + + if (!ip.empty() && !gateway.empty() && !subnet.empty()) + { + string ip_cfg = StringPrintf("%s\n%s\n%s\n", ip.c_str() + , gateway.c_str(), subnet.c_str()); + cfg_mgr->store(WIFI_STATION_IP_CFG, ip_cfg); + } + else + { + cfg_mgr->remove(WIFI_STATION_IP_CFG); + } + if (!dns.empty()) + { + cfg_mgr->store(WIFI_STATION_DNS_CFG, dns); + } + else + { + cfg_mgr->remove(WIFI_STATION_DNS_CFG); + } + + if (restart) + { + stack_->executor()->add(new CallbackExecutable([]() + { + reboot(); + })); + } +} + +string LCCWiFiManager::wifi_scan_json(bool ignore_duplicates) +{ + string result = "["; + SyncNotifiable n; + wifi_->start_ssid_scan(&n); + n.wait_for_notification(); + size_t num_found = wifi_->get_ssid_scan_result_count(); + vector seen_ssids; + for (int i = 0; i < num_found; i++) + { + auto entry = wifi_->get_ssid_scan_result(i); + if (ignore_duplicates) + { + if (std::find_if(seen_ssids.begin(), seen_ssids.end() + , [entry](string &s) + { + return s == (char *)entry.ssid; + }) != seen_ssids.end()) + { + // filter duplicate SSIDs + continue; + } + seen_ssids.push_back((char *)entry.ssid); + } + if (result.length() > 1) + { + result += ","; + } + LOG(VERBOSE, "auth:%d,rssi:%d,ssid:%s", entry.authmode, entry.rssi, entry.ssid); + result += StringPrintf("{\"auth\":%d,\"rssi\":%d,\"ssid\":\"", + entry.authmode, entry.rssi); + // TODO: remove this in favor of proper url encoding of non-ascii characters + for (uint8_t idx = 0; idx < 33; idx++) + { + if (entry.ssid[idx] >= 0x20 && entry.ssid[idx] <= 0x7F) + { + result += entry.ssid[idx]; + } + else if (entry.ssid[idx]) + { + result += StringPrintf("%%%02x", entry.ssid[idx]); + } + else + { + // end of ssid + break; + } + } + result += "\"}"; + } + result += "]"; + wifi_->clear_ssid_scan_results(); + return result; +} + +string LCCWiFiManager::get_config_json() +{ + auto cfg_mgr = Singleton::instance(); + string config = StringPrintf("\"wifi\":{" + "\"mode\":\"%s\"" + , mode_ == WIFI_MODE_AP ? "softap" + : mode_ == WIFI_MODE_APSTA ? "softap-station" : "station"); + if (mode_ != WIFI_MODE_AP) + { + config += StringPrintf(",\"station\":{\"ssid\":\"%s\",\"password\":\"%s\"" + , ssid_.c_str(), password_.c_str()); + if (cfg_mgr->exists(WIFI_STATION_DNS_CFG)) + { + config += ",\"dns\":\"" + cfg_mgr->load(WIFI_STATION_DNS_CFG) + "\""; + } + if (stationIP_.get() != nullptr) + { + config += StringPrintf(",\"ip\":\"" IPSTR "\",\"gateway\":\"" IPSTR "\"," + "\"netmask\":\"" IPSTR "\"" + , IP2STR(&stationIP_->ip), IP2STR(&stationIP_->gw) + , IP2STR(&stationIP_->netmask)); + } + config += "}"; + } + config += "}"; + return config; +} + +} // namespace esp32cs \ No newline at end of file diff --git a/components/Configuration/include/AutoPersistCallbackFlow.h b/components/Configuration/include/AutoPersistCallbackFlow.h new file mode 100644 index 00000000..c18d8a91 --- /dev/null +++ b/components/Configuration/include/AutoPersistCallbackFlow.h @@ -0,0 +1,58 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef AUTO_PERSIST_CB_FLOW_H_ +#define AUTO_PERSIST_CB_FLOW_H_ + +#include +#include + +class AutoPersistFlow : private StateFlowBase +{ +public: + AutoPersistFlow(Service *service + , uint64_t interval + , std::function callback) + : StateFlowBase(service) + , interval_(interval) + , callback_(std::move(callback)) + { + HASSERT(callback_); + start_flow(STATE(sleep_and_persist)); + } + + void stop() + { + set_terminated(); + timer_.ensure_triggered(); + } + +private: + StateFlowTimer timer_{this}; + uint64_t interval_; + std::function callback_; + StateFlowBase::Action sleep_and_persist() + { + return sleep_and_call(&timer_, interval_, STATE(persist)); + } + StateFlowBase::Action persist() + { + callback_(); + return yield_and_call(STATE(sleep_and_persist)); + } +}; +#endif // AUTO_PERSIST_CB_FLOW_H_ \ No newline at end of file diff --git a/components/Configuration/include/CDIHelper.h b/components/Configuration/include/CDIHelper.h new file mode 100644 index 00000000..a0bc8ce0 --- /dev/null +++ b/components/Configuration/include/CDIHelper.h @@ -0,0 +1,105 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef _CDIHELPER_H_ +#define _CDIHELPER_H_ + +#include +#include + +class CDIHelper +{ +public: +/// Creates the XML representation of the configuration structure and saves +/// it to a file on the filesystem. Must be called after SPIFFS.begin() but +/// before calling the {\link create_config_file_if_needed} method. The +/// config file will be re-written whenever there was a change in the +/// contents. It is also necessary to declare the static compiled-in CDI to +/// be empty: +/// ``` +/// namespace openlcb { +/// // This will stop openlcb from exporting the CDI memory space +/// // upon start. +/// extern const char CDI_DATA[] = ""; +/// } // namespace openlcb +/// ``` +/// @param cfg is the global configuration instance (usually called cfg). +/// @param filename is where the xml file can be stored on the +/// filesystem. For example "/spiffs/cdi.xml". +template +static void create_config_descriptor_xml( + const ConfigDef &config, const char *filename + , openlcb::SimpleStackBase *stack = nullptr) +{ + string cdi_string; + ConfigDef cfg(config.offset()); + cfg.config_renderer().render_cdi(&cdi_string); + + cdi_string += '\0'; + + bool need_write = false; + LOG(INFO, "[CDI] Checking %s...", filename); + FILE *ff = fopen(filename, "rb"); + if (!ff) + { + LOG(INFO, "[CDI] File %s does not exist", filename); + need_write = true; + } + else + { + fclose(ff); + string current_str = read_file_to_string(filename); + if (current_str != cdi_string) + { + LOG(INFO, "[CDI] File %s is not up-to-date", filename); + need_write = true; + } +#if LOGLEVEL == VERBOSE + else + { + LOG(INFO, "[CDI] File %s appears up-to-date (len %u vs %u)", filename + , current_str.size(), cdi_string.size()); + } +#endif + } + if (need_write) + { + LOG(INFO, "[CDI] Updating %s (len %u)", filename, + cdi_string.size()); + write_string_to_file(filename, cdi_string); + } + + if (stack) + { + LOG(INFO, "[CDI] Registering CDI with stack..."); + // Creates list of event IDs for factory reset. + auto *v = new vector(); + cfg.handle_events([v](unsigned o) { v->push_back(o); }); + v->push_back(0); + stack->set_event_offsets(v); + // We leak v because it has to stay alive for the entire lifetime of + // the stack. + + // Exports the file memory space. + openlcb::MemorySpace *space = new openlcb::ROFileMemorySpace(filename); + stack->memory_config_handler()->registry()->insert( + stack->node(), openlcb::MemoryConfigDefs::SPACE_CDI, space); + } +} +}; + +#endif // _CDIHELPER_H_ \ No newline at end of file diff --git a/include/LCCCDI.h b/components/Configuration/include/CSConfigDescriptor.h similarity index 65% rename from include/LCCCDI.h rename to components/Configuration/include/CSConfigDescriptor.h index 4d3416aa..c1e8a60d 100644 --- a/include/LCCCDI.h +++ b/components/Configuration/include/CSConfigDescriptor.h @@ -1,5 +1,5 @@ /********************************************************************** -DCC COMMAND STATION FOR ESP32 +ESP32 COMMAND STATION COPYRIGHT (c) 2019 Mike Dunston @@ -15,62 +15,57 @@ COPYRIGHT (c) 2019 Mike Dunston along with this program. If not, see http://www.gnu.org/licenses **********************************************************************/ -#pragma once +#ifndef CS_CDI_H_ +#define CS_CDI_H_ -#include "ESP32CommandStation.h" #include #include #include #include +#include -namespace openlcb { - const SimpleNodeStaticValues SNIP_STATIC_DATA = { - 4, - "github.com/atanisoft (Mike Dunston)", - "ESP32 Command Station", - "ESP32-v1", - VERSION - }; - - /// Modify this value whenever the config needs to be reinitialized on the - /// node for a firmware update. - static constexpr uint16_t CANONICAL_VERSION = 0x0130; +namespace esp32cs +{ + using TrackOutputs = openlcb::RepeatedGroup; /// Defines the main segment in the configuration CDI. This is laid out at /// origin 128 to give space for the ACDI user data at the beginning. - CDI_GROUP(CommandStationSegment, Segment(MemoryConfigDefs::SPACE_CONFIG), + CDI_GROUP(CommandStationSegment, + Segment(openlcb::MemoryConfigDefs::SPACE_CONFIG), Offset(128)); /// Each entry declares the name of the current entry, then the type and /// then optional arguments list. - CDI_GROUP_ENTRY(internal_config, InternalConfigData); - /// CV Access via MemoryConfig protocol. - //CDI_GROUP_ENTRY(cv, TractionShortCvSpace); + CDI_GROUP_ENTRY(internal_config, openlcb::InternalConfigData); + /// WiFi configuration CDI_GROUP_ENTRY(wifi, WiFiConfiguration, Name("WiFi Configuration")); + /// H-Bridge configuration + CDI_GROUP_ENTRY(hbridge, TrackOutputs, Name("H-Bridge Configuration")); CDI_GROUP_END(); /// This segment is only needed temporarily until there is program code to set /// the ACDI user data version byte. - CDI_GROUP(VersionSeg, Segment(MemoryConfigDefs::SPACE_CONFIG), + CDI_GROUP(VersionSeg, Segment(openlcb::MemoryConfigDefs::SPACE_CONFIG), Name("Version information")); - CDI_GROUP_ENTRY(acdi_user_version, Uint8ConfigEntry, + CDI_GROUP_ENTRY(acdi_user_version, openlcb::Uint8ConfigEntry, Name("ACDI User Data version"), Description("Set to 2 and do not change.")); CDI_GROUP_END(); - /// The main structure of the CDI. ConfigDef is the symbol used in - /// src/LCCInterface.cpp to refer to the configuration defined here. - CDI_GROUP(ConfigDef, MainCdi()); + /// The main structure of the ESP32 Command Station CDI. + CDI_GROUP(Esp32ConfigDef, MainCdi()); /// Adds the tag with the values from SNIP_STATIC_DATA /// above. - CDI_GROUP_ENTRY(ident, Identification); + CDI_GROUP_ENTRY(ident, openlcb::Identification); /// Adds an tag. - CDI_GROUP_ENTRY(acdi, Acdi); + CDI_GROUP_ENTRY(acdi, openlcb::Acdi); /// Adds a segment for changing the values in the ACDI user-defined /// space. UserInfoSegment is defined in the system header. - CDI_GROUP_ENTRY(userinfo, UserInfoSegment); + CDI_GROUP_ENTRY(userinfo, openlcb::UserInfoSegment); /// Adds the main configuration segment. CDI_GROUP_ENTRY(seg, CommandStationSegment); /// Adds the versioning segment. CDI_GROUP_ENTRY(version, VersionSeg); CDI_GROUP_END(); } + +#endif // CS_CDI_H_ diff --git a/components/Configuration/include/ConfigurationManager.h b/components/Configuration/include/ConfigurationManager.h new file mode 100644 index 00000000..10aba17c --- /dev/null +++ b/components/Configuration/include/ConfigurationManager.h @@ -0,0 +1,69 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2018-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef CONFIG_MGR_H_ +#define CONFIG_MGR_H_ + +#include +#include + +#include "CSConfigDescriptor.h" + +static constexpr char CFG_MOUNT[] = "/cfg"; +static constexpr char CS_CONFIG_DIR[] = "/cfg/ESP32CS"; +static constexpr char LCC_CFG_DIR[] = "/cfg/LCC"; +static constexpr char LCC_CDI_XML[] = "/cfg/LCC/cdi.xml"; +static constexpr char LCC_CONFIG_FILE[] = "/cfg/LCC/config"; + +namespace openlcb +{ + class SimpleStackBase; +} + +// Class definition for the Configuration Management system in ESP32 Command Station +class ConfigurationManager : public Singleton +{ +public: + ConfigurationManager(const esp32cs::Esp32ConfigDef &); + void shutdown(); + bool is_sd() + { + return sd_ != nullptr; + } + + bool exists(const std::string &); + void remove(const std::string &); + std::string load(const std::string &); + void store(const char *, const std::string &); + std::string getCSConfig(); + void force_factory_reset(); +private: + std::string getFilePath(const std::string &); + + const esp32cs::Esp32ConfigDef cfg_; + int configFd_{-1}; + sdmmc_card_t *sd_{nullptr}; +}; + +// Returns true if the provided pin is one of the ESP32 pins that has usage +// restrictions. This will always return false if the configuration flag +// ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS is enabled. +bool is_restricted_pin(int8_t); + +uint64_t string_to_uint64(std::string value); + +#endif // CONFIG_MGR_H_ diff --git a/include/JsonConstants.h b/components/Configuration/include/JsonConstants.h similarity index 53% rename from include/JsonConstants.h rename to components/Configuration/include/JsonConstants.h index a1a52555..144dbb79 100644 --- a/include/JsonConstants.h +++ b/components/Configuration/include/JsonConstants.h @@ -1,7 +1,7 @@ /********************************************************************** ESP32 COMMAND STATION -COPYRIGHT (c) 2019 Mike Dunston +COPYRIGHT (c) 2019-2020 Mike Dunston 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 @@ -15,13 +15,15 @@ COPYRIGHT (c) 2019 Mike Dunston along with this program. If not, see http://www.gnu.org/licenses **********************************************************************/ -#pragma once +#ifndef JSON_CONSTS_H_ +#define JSON_CONSTS_H_ constexpr const char * JSON_FILE_NODE = "file"; constexpr const char * JSON_NAME_NODE = "name"; constexpr const char * JSON_STATE_NODE = "state"; constexpr const char * JSON_USAGE_NODE = "usage"; +constexpr const char * JSON_MODE_NODE = "mode"; constexpr const char * JSON_COUNT_NODE = "count"; @@ -77,6 +79,72 @@ constexpr const char * JSON_CREATE_NODE = "create"; constexpr const char * JSON_OVERALL_STATE_NODE = "overallState"; constexpr const char * JSON_LAST_UPDATE_NODE = "lastUpdate"; +constexpr const char * JSON_LCC_NODE = "lcc"; +constexpr const char * JSON_LCC_FORCE_RESET_NODE = "reset"; +constexpr const char * JSON_LCC_NODE_ID_NODE = "id"; +constexpr const char * JSON_LCC_CAN_NODE = "can"; + +constexpr const char * JSON_WIFI_NODE = "wifi"; +constexpr const char * JSON_WIFI_MODE_NODE = "mode"; +constexpr const char * JSON_WIFI_SSID_NODE = "ssid"; +constexpr const char * JSON_WIFI_PASSWORD_NODE = "password"; +constexpr const char * JSON_WIFI_SOFTAP_NODE = "softap"; +constexpr const char * JSON_WIFI_STATION_NODE = "station"; +constexpr const char * JSON_WIFI_STATION_IP_NODE = "ip"; +constexpr const char * JSON_WIFI_STATION_GATEWAY_NODE = "gateway"; +constexpr const char * JSON_WIFI_STATION_NETMASK_NODE = "netmask"; +constexpr const char * JSON_WIFI_DNS_NODE = "dns"; + +constexpr const char * JSON_WIFI_RSSI_NODE = "rssi"; +constexpr const char * JSON_WIFI_AUTH_NODE = "auth"; + +constexpr const char * JSON_HC12_NODE = "hc12"; +constexpr const char * JSON_HC12_ENABLED_NODE = "enabled"; +constexpr const char * JSON_HC12_UART_NODE = "uart"; +constexpr const char * JSON_HC12_RX_NODE = "rx"; +constexpr const char * JSON_HC12_TX_NODE = "tx"; + +constexpr const char * JSON_HBRIDGES_NODE = "hbridges"; +constexpr const char * JSON_HBRIDGE_ENABLE_PIN_NODE = "enable"; +constexpr const char * JSON_HBRIDGE_SIGNAL_PIN_NODE = "signal"; +constexpr const char * JSON_HBRIDGE_PREAMBLE_BITS_NODE = "preamble"; +constexpr const char * JSON_HBRIDGE_THERMAL_PIN_NODE = "thermal"; +constexpr const char * JSON_HBRIDGE_SENSE_PIN_NODE = "sense"; +constexpr const char * JSON_HBRIDGE_RMT_CHANNEL_NODE = "rmt"; + +constexpr const char * JSON_RAILCOM_NODE = "railcom"; +constexpr const char * JSON_RAILCOM_ENABLE_PIN_NODE = "enable"; +constexpr const char * JSON_RAILCOM_BRAKE_PIN_NODE = "brake"; +constexpr const char * JSON_RAILCOM_SHORT_PIN_NODE = "short"; +constexpr const char * JSON_RAILCOM_UART_NODE = "uart"; +constexpr const char * JSON_RAILCOM_RX_NODE = "rx"; + +constexpr const char * JSON_CDI_NODE = "cdi"; +constexpr const char * JSON_CDI_UPLINK_NODE = "uplink"; +constexpr const char * JSON_CDI_UPLINK_RECONNECT_NODE = "reconnect"; +constexpr const char * JSON_CDI_UPLINK_MODE_NODE = "mode"; +constexpr const char * JSON_CDI_UPLINK_AUTO_HOST_NODE = "auto_host"; +constexpr const char * JSON_CDI_UPLINK_AUTO_SERVICE_NODE = "auto_service"; +constexpr const char * JSON_CDI_UPLINK_MANUAL_HOST_NODE = "manual_host"; +constexpr const char * JSON_CDI_UPLINK_MANUAL_PORT_NODE = "manual_port"; +constexpr const char * JSON_CDI_HUB_NODE = "hub"; +constexpr const char * JSON_CDI_HUB_ENABLE_NODE = "enable"; +constexpr const char * JSON_CDI_HUB_PORT_NODE = "port"; +constexpr const char * JSON_CDI_HUB_SERVICE_NODE = "service"; +constexpr const char * JSON_CDI_HBRIDGE_SHORT_EVENT_NODE = "short"; +constexpr const char * JSON_CDI_HBRIDGE_SHORT_CLEAR_EVENT_NODE = "short_clear"; +constexpr const char * JSON_CDI_HBRIDGE_SHUTDOWN_EVENT_NODE = "shutdown"; +constexpr const char * JSON_CDI_HBRIDGE_SHUTDOWN_CLEAR_EVENT_NODE = "shutdown_clear"; +constexpr const char * JSON_CDI_HBRIDGE_THERMAL_EVENT_NODE = "thermal"; +constexpr const char * JSON_CDI_HBRIDGE_THERMAL_CLEAR_EVENT_NODE = "thermal_clear"; + +constexpr const char * JSON_VALUE_STATION_IP_MODE_STATIC = "static"; +constexpr const char * JSON_VALUE_STATION_IP_MODE_DHCP = "dhcp"; + +constexpr const char * JSON_VALUE_WIFI_MODE_SOFTAP_ONLY = "softap"; +constexpr const char * JSON_VALUE_WIFI_MODE_SOFTAP_STATION = "softap-station"; +constexpr const char * JSON_VALUE_WIFI_MODE_STATION_ONLY = "station"; + constexpr const char * JSON_VALUE_FORWARD = "FWD"; constexpr const char * JSON_VALUE_REVERSE = "REV"; constexpr const char * JSON_VALUE_TRUE = "true"; @@ -85,9 +153,12 @@ constexpr const char * JSON_VALUE_NORMAL = "Normal"; constexpr const char * JSON_VALUE_OFF = "Off"; constexpr const char * JSON_VALUE_ON = "On"; constexpr const char * JSON_VALUE_FAULT = "Fault"; +constexpr const char * JSON_VALUE_ERROR = "Error"; constexpr const char * JSON_VALUE_THROWN = "Thrown"; constexpr const char * JSON_VALUE_CLOSED = "Closed"; constexpr const char * JSON_VALUE_LONG_ADDRESS = "Long Address"; constexpr const char * JSON_VALUE_SHORT_ADDRESS = "Short Address"; constexpr const char * JSON_VALUE_MOBILE_DECODER = "Mobile Decoder"; constexpr const char * JSON_VALUE_STATIONARY_DECODER = "Stationary Decoder"; + +#endif // JSON_CONSTS_H_ diff --git a/components/Configuration/include/LCCStackManager.h b/components/Configuration/include/LCCStackManager.h new file mode 100644 index 00000000..a40c5954 --- /dev/null +++ b/components/Configuration/include/LCCStackManager.h @@ -0,0 +1,66 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "CSConfigDescriptor.h" + +#include + +namespace openlcb +{ + class SimpleStackBase; + class Node; + class SimpleInfoFlow; + class MemoryConfigHandler; +} + +class AutoSyncFileFlow; +class Service; + +namespace openmrn_arduino +{ + class Esp32HardwareCan; +} + +namespace esp32cs +{ + +class LCCStackManager : public Singleton +{ +public: + LCCStackManager(const esp32cs::Esp32ConfigDef &cfg); + openlcb::SimpleStackBase *stack(); + Service *service(); + openlcb::Node *node(); + openlcb::SimpleInfoFlow *info_flow(); + openlcb::MemoryConfigHandler *memory_config_handler(); + void start(bool is_sd); + void shutdown(); + bool set_node_id(std::string, bool restart = true); + bool reconfigure_can(bool enable, bool restart = true); + void factory_reset(); + std::string get_config_json(); + void reboot_node(); +private: + const Esp32ConfigDef cfg_; + int fd_; + uint64_t nodeID_{0}; + openlcb::SimpleStackBase *stack_; + openmrn_arduino::Esp32HardwareCan *can_; + AutoSyncFileFlow *configAutoSync_; +}; + +} // namespace esp32cs \ No newline at end of file diff --git a/components/Configuration/include/LCCWiFiManager.h b/components/Configuration/include/LCCWiFiManager.h new file mode 100644 index 00000000..e4de1cf1 --- /dev/null +++ b/components/Configuration/include/LCCWiFiManager.h @@ -0,0 +1,69 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include +#include +#include +#include + +#include "CSConfigDescriptor.h" + +namespace openlcb +{ + class SimpleStackBase; +} + +namespace esp32cs +{ + +class LCCWiFiManager : public Singleton +{ +public: + LCCWiFiManager(openlcb::SimpleStackBase *stack + , const esp32cs::Esp32ConfigDef &cfg); + void shutdown(); + void reconfigure_mode(std::string mode, bool restart = true); + void reconfigure_station(std::string ssid, std::string password + , std::string ip = "", std::string gateway = "" + , std::string subnet = "", std::string dns = "" + , bool restart = true); + std::string wifi_scan_json(bool ignore_duplicates=true); + std::string get_config_json(); + bool is_softap_enabled() + { + return mode_ == WIFI_MODE_AP || mode_ == WIFI_MODE_APSTA; + } + bool is_station_enabled() + { + return mode_ != WIFI_MODE_AP; + } + std::string get_ssid() + { + return ssid_; + } +private: + openlcb::SimpleStackBase *stack_; + const esp32cs::Esp32ConfigDef cfg_; + std::unique_ptr wifi_; + std::string ssid_{""}; + std::string password_{""}; + wifi_mode_t mode_{WIFI_MODE_STA}; + std::unique_ptr stationIP_{nullptr}; + ip_addr_t stationDNS_{ip_addr_any}; +}; + +} // namespace esp32cs \ No newline at end of file diff --git a/components/DCCSignalGenerator/CMakeLists.txt b/components/DCCSignalGenerator/CMakeLists.txt new file mode 100644 index 00000000..09efe2dc --- /dev/null +++ b/components/DCCSignalGenerator/CMakeLists.txt @@ -0,0 +1,31 @@ +set(COMPONENT_ADD_INCLUDEDIRS "include" ) + +set(COMPONENT_PRIV_INCLUDEDIRS "private_include" ) + +set(COMPONENT_SRCS + "DCCSignalVFS.cpp" + "DuplexedTrackIf.cpp" + "EStopHandler.cpp" + "MonitoredHBridge.cpp" + "RMTTrackDevice.cpp" +) + +set(COMPONENT_REQUIRES + "OpenMRNLite" + "driver" + "esp_adc_cal" + "LCCTrainSearchProtocol" + "nlohmann_json" + "StatusDisplay" + "StatusLED" + "vfs" +) + +register_component() + +set_source_files_properties(DCCSignalVFS.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(DuplexedTrackIf.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(EStopHandler.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(LocalTrackIf.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(MonitoredHBridge.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(RMTTrackDevice.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) \ No newline at end of file diff --git a/components/DCCSignalGenerator/DCCSignalVFS.cpp b/components/DCCSignalGenerator/DCCSignalVFS.cpp new file mode 100644 index 00000000..2b436244 --- /dev/null +++ b/components/DCCSignalGenerator/DCCSignalVFS.cpp @@ -0,0 +1,492 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "RMTTrackDevice.h" +#include "EStopHandler.h" +#include "Esp32RailComDriver.h" +#include "HBridgeThermalMonitor.h" +#include "TrackPowerBitInterface.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace esp32cs +{ +/// RMT channel to use for the OPS track output. +static constexpr rmt_channel_t OPS_RMT_CHANNEL = RMT_CHANNEL_0; + +/// RMT channel to use for the PROG track output. +static constexpr rmt_channel_t PROG_RMT_CHANNEL = RMT_CHANNEL_3; + +/// OPS Track signal pin. +GPIO_PIN(OPS_SIGNAL, GpioOutputSafeLow, CONFIG_OPS_SIGNAL_PIN); + +/// OPS Track h-bridge enable pin. +GPIO_PIN(OPS_ENABLE, GpioOutputSafeLow, CONFIG_OPS_ENABLE_PIN); + +#ifdef CONFIG_OPS_THERMAL_PIN +/// OPS Track h-bridge thermal alert pin, active LOW. +GPIO_PIN(OPS_THERMAL, GpioInputPU, CONFIG_OPS_THERMAL_PIN); +#else +/// OPS Track h-bridge thermal alert pin, not connected to physical pin. +typedef DummyPinWithReadHigh OPS_THERMAL_Pin; +#endif // CONFIG_OPS_THERMAL_PIN + +/// RailCom driver instance for the PROG track, unused. +NoRailcomDriver progRailComDriver; + +/// PROG Track signal pin. +GPIO_PIN(PROG_SIGNAL, GpioOutputSafeLow, CONFIG_PROG_SIGNAL_PIN); + +/// PROG Track h-bridge enable pin. +GPIO_PIN(PROG_ENABLE, GpioOutputSafeLow, CONFIG_PROG_ENABLE_PIN); + +#if defined(CONFIG_OPS_RAILCOM) +/// OPS Track h-bridge brake pin, active HIGH. +GPIO_PIN(OPS_HBRIDGE_BRAKE, GpioOutputSafeHigh, CONFIG_OPS_RAILCOM_BRAKE_PIN); + +/// RailCom detector enable pin, active HIGH. +GPIO_PIN(OPS_RAILCOM_ENABLE, GpioOutputSafeLow, CONFIG_OPS_RAILCOM_ENABLE_PIN); + +/// RailCom detector data pin. +GPIO_PIN(OPS_RAILCOM_DATA, GpioInputPU, CONFIG_OPS_RAILCOM_UART_RX_PIN); + +/// RailCom hardware definition +struct RailComHW +{ +#if defined(CONFIG_OPS_RAILCOM_UART1) + static constexpr uart_port_t UART = UART_NUM_1; + static constexpr uart_dev_t *UART_BASE = &UART1; + static constexpr periph_module_t UART_PERIPH = PERIPH_UART1_MODULE; + static constexpr int UART_ISR_SOURCE = ETS_UART1_INTR_SOURCE; + static constexpr uint32_t UART_MATRIX_IDX = U1RXD_IN_IDX; +#elif defined(CONFIG_OPS_RAILCOM_UART2) + static constexpr uart_port_t UART = UART_NUM_2; + static constexpr uart_dev_t *UART_BASE = &UART2; + static constexpr periph_module_t UART_PERIPH = PERIPH_UART2_MODULE; + static constexpr int UART_ISR_SOURCE = ETS_UART2_INTR_SOURCE; + static constexpr uint32_t UART_MATRIX_IDX = U2RXD_IN_IDX; +#else + #error Unsupported UART selected for OPS RailCom! +#endif + + using DATA = OPS_RAILCOM_DATA_Pin; + using HB_BRAKE = OPS_HBRIDGE_BRAKE_Pin; + using HB_ENABLE = OPS_ENABLE_Pin; + using RC_ENABLE = OPS_RAILCOM_ENABLE_Pin; + + static void hw_init() + { + DATA::hw_init(); + HB_BRAKE::hw_init(); + RC_ENABLE::hw_init(); + + // initialize the UART + periph_module_enable(UART_PERIPH); + PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[DATA::pin()], PIN_FUNC_GPIO); + gpio_matrix_in(DATA::pin(), UART_MATRIX_IDX, 0); + } + + static constexpr timg_dev_t *TIMER_BASE = &TIMERG0; + static constexpr timer_idx_t TIMER_IDX = TIMER_0; + static constexpr timer_group_t TIMER_GRP = TIMER_GROUP_0; + static constexpr periph_module_t TIMER_PERIPH = PERIPH_TIMG0_MODULE; + static constexpr int TIMER_ISR_SOURCE = ETS_TG0_T0_LEVEL_INTR_SOURCE + TIMER_IDX; + + /// Number of microseconds to wait after the final packet bit completes + /// before disabling the ENABLE pin on the h-bridge. + static constexpr uint32_t RAILCOM_TRIGGER_DELAY_USEC = 1; + + /// Number of microseconds to wait for railcom data on channel 1. + static constexpr uint32_t RAILCOM_MAX_READ_DELAY_CH_1 = + 177 - RAILCOM_TRIGGER_DELAY_USEC; + + /// Number of microseconds to wait for railcom data on channel 2. + static constexpr uint32_t RAILCOM_MAX_READ_DELAY_CH_2 = + 454 - RAILCOM_MAX_READ_DELAY_CH_1; +}; + +/// RailCom driver instance for the OPS track. +Esp32RailComDriver opsRailComDriver; + +#else + +/// RailCom driver instance for the OPS track. +NoRailcomDriver opsRailComDriver; + +#endif // CONFIG_OPS_RAILCOM +/// Initializer for all GPIO pins. +typedef GpioInitializer< + OPS_SIGNAL_Pin, OPS_ENABLE_Pin, OPS_THERMAL_Pin +, PROG_SIGNAL_Pin, PROG_ENABLE_Pin +> DCCGpioInitializer; + +static std::unique_ptr dcc_poller; +static std::unique_ptr track[RMT_CHANNEL_MAX]; +static std::unique_ptr track_mon[RMT_CHANNEL_MAX]; +static std::unique_ptr ops_thermal_mon; +static std::unique_ptr power_event; +static std::unique_ptr estop_handler; +static std::unique_ptr prog_track_backend; +#if defined(CONFIG_OPS_RAILCOM) +static std::unique_ptr railcom_hub; +static std::unique_ptr railcom_dumper; +#endif // CONFIG_OPS_RAILCOM + +/// Updates the status display with the current state of the track outputs. +static void update_status_display() +{ + auto status = Singleton::instance(); + status->track_power("%s:%s %s:%s", CONFIG_OPS_TRACK_NAME + , OPS_ENABLE_Pin::instance()->is_set() ? "On" : "Off" + , CONFIG_PROG_TRACK_NAME + , PROG_ENABLE_Pin::instance()->is_set() ? "On" : "Off"); +} + +/// Triggers an estop event to be sent +void initiate_estop() +{ + // TODO: add event publish + estop_handler->set_state(true); +} + +/// Returns true if the OPS track output is enabled +bool is_ops_track_output_enabled() +{ + return OPS_ENABLE_Pin::instance()->is_set(); +} + +/// Enables the OPS track output +void enable_ops_track_output() +{ + if (!is_ops_track_output_enabled()) + { + LOG(INFO, "[Track] Enabling track output: %s", CONFIG_OPS_TRACK_NAME); + OPS_ENABLE_Pin::instance()->set(); +#if CONFIG_STATUS_LED + Singleton::instance()->setStatusLED( + StatusLED::LED::OPS_TRACK, StatusLED::COLOR::GREEN); +#endif // CONFIG_STATUS_LED + update_status_display(); + } +} + +/// Enables the OPS track output +void disable_ops_track_output() +{ + if (is_ops_track_output_enabled()) + { + LOG(INFO, "[Track] Disabling track output: %s", CONFIG_OPS_TRACK_NAME); + OPS_ENABLE_Pin::instance()->clr(); +#if CONFIG_STATUS_LED + Singleton::instance()->setStatusLED( + StatusLED::LED::OPS_TRACK, StatusLED::COLOR::OFF); +#endif // CONFIG_STATUS_LED + update_status_display(); + } +} + +/// Enables the PROG track output +static void enable_prog_track_output() +{ + if (PROG_ENABLE_Pin::instance()->is_clr()) + { + LOG(INFO, "[Track] Enabling track output: %s", CONFIG_PROG_TRACK_NAME); + PROG_ENABLE_Pin::instance()->set(); + track_mon[PROG_RMT_CHANNEL]->enable_prog_response(true); +#if CONFIG_STATUS_LED + Singleton::instance()->setStatusLED( + StatusLED::LED::PROG_TRACK, StatusLED::COLOR::GREEN); +#endif // CONFIG_STATUS_LED + update_status_display(); + } +} + +/// Disables the PROG track outputs +static void disable_prog_track_output() +{ + if (PROG_ENABLE_Pin::instance()->is_set()) + { + LOG(INFO, "[Track] Disabling track output: %s", CONFIG_PROG_TRACK_NAME); + PROG_ENABLE_Pin::instance()->clr(); + track_mon[PROG_RMT_CHANNEL]->enable_prog_response(false); +#if CONFIG_STATUS_LED + Singleton::instance()->setStatusLED( + StatusLED::LED::PROG_TRACK, StatusLED::COLOR::OFF); +#endif // CONFIG_STATUS_LED + update_status_display(); + } +} + +/// Disables all track outputs +void disable_track_outputs() +{ + disable_ops_track_output(); + disable_prog_track_output(); +} + +/// ESP32 VFS ::write() impl for the RMTTrackDevice. +/// @param fd is the file descriptor being written to. +/// @param data is the data to write. +/// @param size is the size of data. +/// @returns number of bytes written. +static ssize_t dcc_vfs_write(int fd, const void *data, size_t size) +{ + HASSERT(track[fd] != nullptr); + return track[fd]->write(fd, data, size); +} + +/// ESP32 VFS ::open() impl for the RMTTrackDevice +/// @param path is the file location to be opened. +/// @param flags is not used. +/// @param mode is not used. +/// @returns file descriptor for the opened file location. +static int dcc_vfs_open(const char *path, int flags, int mode) +{ + int fd; + if (!strcasecmp(path + 1, CONFIG_OPS_TRACK_NAME)) + { + fd = OPS_RMT_CHANNEL; + } + else if (!strcasecmp(path + 1, CONFIG_PROG_TRACK_NAME)) + { + fd = PROG_RMT_CHANNEL; + } + else + { + LOG_ERROR("[Track] Attempt to open unknown track interface: %s", path + 1); + errno = ENOTSUP; + return -1; + } + LOG(INFO, "[Track] Connecting track interface (track:%s, fd:%d)", path + 1, fd); + return fd; +} + +/// ESP32 VFS ::close() impl for the RMTTrackDevice. +/// @param fd is the file descriptor to close. +/// @returns the status of the close() operation, only returns zero. +static int dcc_vfs_close(int fd) +{ + HASSERT(track[fd] != nullptr); + LOG(INFO, "[Track] Disconnecting track interface (track:%s, fd:%d)" + , track[fd]->name(), fd); + return 0; +} + +/// ESP32 VFS ::ioctl() impl for the RMTTrackDevice. +/// @param fd is the file descriptor to operate on. +/// @param cmd is the ioctl command to execute. +/// @param args are the arguments to ioctl. +/// @returns the result of the ioctl command, zero on success, non-zero will +/// set errno. +static int dcc_vfs_ioctl(int fd, int cmd, va_list args) +{ + HASSERT(track[fd] != nullptr); + return track[fd]->ioctl(fd, cmd, args); +} + +/// RMT transmit complete callback. +/// +/// @param channel is the RMT channel that has completed transmission. +/// @param ctx is unused. +/// +/// This is called automatically by the RMT peripheral when it reaches the end +/// of TX data. +static void rmt_tx_callback(rmt_channel_t channel, void *ctx) +{ + if (track[channel] != nullptr) + { + track[channel]->rmt_transmit_complete(); + } +} + +/// Initializes the RMT based signal generation +/// +/// @param param unused. +/// +/// Note: this is necessary to ensure the RMT ISRs are started on the second +/// core of the ESP32. +static void init_rmt_outputs(void *param) +{ + // Connect our callback into the RMT so we can queue up the next packet for + // transmission when needed. + rmt_register_tx_end_callback(rmt_tx_callback, nullptr); + + track[OPS_RMT_CHANNEL].reset( + new RMTTrackDevice(CONFIG_OPS_TRACK_NAME, OPS_RMT_CHANNEL + , CONFIG_OPS_DCC_PREAMBLE_BITS + , CONFIG_OPS_PACKET_QUEUE_SIZE, OPS_SIGNAL_Pin::pin() + , reinterpret_cast(&opsRailComDriver))); + + track[PROG_RMT_CHANNEL].reset( + new RMTTrackDevice(CONFIG_PROG_TRACK_NAME, PROG_RMT_CHANNEL + , CONFIG_PROG_DCC_PREAMBLE_BITS + , CONFIG_PROG_PACKET_QUEUE_SIZE, PROG_SIGNAL_Pin::pin() + , &progRailComDriver)); + +#if defined(CONFIG_OPS_ENERGIZE_ON_STARTUP) + power_event->set_state(true); +#endif + + // this is a one-time task, shutdown the task before returning + vTaskDelete(nullptr); +} + +/// Initializes the ESP32 VFS adapter for the DCC track interface and the short +/// detection devices. +/// @param node is the OpenLCB node to bind to. +/// @param service is the OpenLCB @ref Service to use for recurring tasks. +/// @param ops_cfg is the CDI element for the OPS track output. +/// @param prog_cfg is the CDI element for the PROG track output. +void init_dcc_vfs(openlcb::Node *node, Service *service + , const esp32cs::TrackOutputConfig &ops_cfg + , const esp32cs::TrackOutputConfig &prog_cfg) +{ + // register the VFS handler as the LocalTrackIf uses this to route DCC + // packets to the track. + esp_vfs_t vfs; + memset(&vfs, 0, sizeof(vfs)); + vfs.flags = ESP_VFS_FLAG_DEFAULT; + vfs.ioctl = &esp32cs::dcc_vfs_ioctl; + vfs.open = &esp32cs::dcc_vfs_open; + vfs.close = &esp32cs::dcc_vfs_close; + vfs.write = &esp32cs::dcc_vfs_write; + + LOG(INFO, "[Track] Registering /dev/track VFS interface"); + ESP_ERROR_CHECK(esp_vfs_register("/dev/track", &vfs, nullptr)); + + DCCGpioInitializer::hw_init(); + OPS_ENABLE_Pin::set_pulldown_on(); + PROG_ENABLE_Pin::set_pulldown_on(); + +#if defined(CONFIG_OPS_RAILCOM) + railcom_hub.reset(new dcc::RailcomHubFlow(service)); + opsRailComDriver.hw_init(railcom_hub.get()); +#if defined(CONFIG_OPS_RAILCOM_DUMP_PACKETS) + railcom_dumper.reset(new dcc::RailcomPrintfFlow(railcom_hub.get())); +#endif +#endif // CONFIG_OPS_RAILCOM + + ops_thermal_mon.reset( + new HBridgeThermalMonitor(node, ops_cfg, OPS_THERMAL_Pin::instance())); + + track_mon[OPS_RMT_CHANNEL].reset( + new HBridgeShortDetector(node, (adc1_channel_t)CONFIG_OPS_ADC + , OPS_ENABLE_Pin::instance() + , CONFIG_OPS_HBRIDGE_LIMIT_MILLIAMPS + , CONFIG_OPS_HBRIDGE_MAX_MILLIAMPS + , CONFIG_OPS_TRACK_NAME + , CONFIG_OPS_HBRIDGE_TYPE_NAME + , ops_cfg)); + + track_mon[PROG_RMT_CHANNEL].reset( + new HBridgeShortDetector(node, (adc1_channel_t)CONFIG_PROG_ADC + , PROG_ENABLE_Pin::instance() + , CONFIG_PROG_HBRIDGE_MAX_MILLIAMPS + , CONFIG_PROG_TRACK_NAME + , CONFIG_PROG_HBRIDGE_TYPE_NAME + , prog_cfg)); + + // initialize the RMT using the second core so that the ISR is bound to that + // core instead of this core. + // TODO: should this block until the RMT is running? + xTaskCreatePinnedToCore(&init_rmt_outputs, "RMT Init", 2048, nullptr, 2 + , nullptr, APP_CPU_NUM); + + LOG(INFO + , "[Track] Registering LCC EventConsumer for Track Power (On:%s, Off:%s)" + , uint64_to_string_hex(openlcb::Defs::CLEAR_EMERGENCY_OFF_EVENT).c_str() + , uint64_to_string_hex(openlcb::Defs::EMERGENCY_OFF_EVENT).c_str()); + power_event.reset( + new openlcb::BitEventConsumer( + new TrackPowerBit(node, OPS_ENABLE_Pin::instance()))); + + // Initialize the e-stop event handler + estop_handler.reset(new EStopHandler(node)); + + // Initialize the Programming Track backend handler + prog_track_backend.reset( + new ProgrammingTrackBackend(service + , &enable_prog_track_output + , &disable_prog_track_output)); + + // Configure h-bridge polling + dcc_poller.reset(new openlcb::RefreshLoop(node, + { ops_thermal_mon->polling() + , track_mon[OPS_RMT_CHANNEL].get() + , track_mon[PROG_RMT_CHANNEL].get() + })); + + update_status_display(); +} + +void shutdown_dcc_vfs() +{ + // disconnect the RMT TX complete callback so that no more DCC packets will + // be sent to the tracks. + rmt_register_tx_end_callback(nullptr, nullptr); + + // stop any future polling of the DCC outputs + dcc_poller->stop(); + + // Note that other objects are not released at this point since they may + // still be called by other systems until the reboot occurs. +} + +/// @return string containing a two element json array of the track monitors. +std::string get_track_state_json() +{ + return StringPrintf("[%s,%s]" + , track_mon[OPS_RMT_CHANNEL]->getStateAsJson().c_str() + , track_mon[PROG_RMT_CHANNEL]->getStateAsJson().c_str()); +} + +/// @return DCC++ status data from the OPS track only. +std::string get_track_state_for_dccpp() +{ + return track_mon[OPS_RMT_CHANNEL]->get_state_for_dccpp(); +} + +/// Enables or disables track power via event state. +/// @param new_value is the requested status. +void TrackPowerBit::set_state(bool new_value) +{ + if (new_value) + { + enable_ops_track_output(); + } + else + { + disable_track_outputs(); + } +} + +} // namespace esp32cs \ No newline at end of file diff --git a/components/DCCSignalGenerator/DuplexedTrackIf.cpp b/components/DCCSignalGenerator/DuplexedTrackIf.cpp new file mode 100644 index 00000000..45f01215 --- /dev/null +++ b/components/DCCSignalGenerator/DuplexedTrackIf.cpp @@ -0,0 +1,77 @@ +/** \copyright + * Copyright (c) 2014, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file LocalTrackIf.hxx + * + * Control flow that acts as a trackInterface and sends all packets to a local + * fd that represents the DCC mainline, such as TivaDCC. + * + * NOTE: This has been customized for ESP32 Command Station to split the OPS + * and PROG ioctl handling based on the send_long_preamble header flag. This + * is not intended for merge back to OpenMRN. + * + * @author Balazs Racz + * @date 24 Aug 2014 + */ + +#include "can_ioctl.h" +#include "DuplexedTrackIf.h" + +#include +#include +#include +#include +#include +#include + +namespace esp32cs +{ + +DuplexedTrackIf::DuplexedTrackIf(Service *service, int pool_size, int ops_fd + , int prog_fd) + : StateFlow, QList<1>>(service) + , fd_ops_(ops_fd), fd_prog_(prog_fd) + , pool_(sizeof(Buffer), pool_size) +{ +} + +StateFlowBase::Action DuplexedTrackIf::entry() +{ + auto *p = message()->data(); + auto fd = p->packet_header.send_long_preamble ? + fd_prog_ : fd_ops_; + HASSERT(fd >= 0); + int ret = ::write(fd, p, sizeof(*p)); + if (ret < 0) + { + HASSERT(errno == ENOSPC); + ::ioctl(fd, CAN_IOC_WRITE_ACTIVE, this); + return wait(); + } + return finish(); +} + +} // namespace esp32cs \ No newline at end of file diff --git a/components/DCCSignalGenerator/EStopHandler.cpp b/components/DCCSignalGenerator/EStopHandler.cpp new file mode 100644 index 00000000..1b1d5237 --- /dev/null +++ b/components/DCCSignalGenerator/EStopHandler.cpp @@ -0,0 +1,73 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2019 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "EStopHandler.h" + +#include + +namespace esp32cs +{ + +void EStopHandler::set_state(bool new_value) +{ + if (new_value) + { + LOG(INFO, "[eStop] Received eStop request, sending eStop to all trains."); + // TODO: add helper method on AllTrainNodes for this. + auto trains = Singleton::instance(); + for (size_t id = 0; id < trains->size(); id++) + { + auto node = trains->get_train_node_id(id); + if (node) + { + trains->get_train_impl(node)->set_emergencystop(); + } + } + { + AtomicHolder l(this); + remaining_ = CONFIG_DCC_ESTOP_PACKET_COUNT; + packet_processor_add_refresh_source(this + , dcc::UpdateLoopBase::ESTOP_PRIORITY); + } + } + else + { + AtomicHolder l(this); + if (remaining_) + { + LOG(INFO, "[eStop] Received eStop clear request."); + remaining_ = 0; + packet_processor_remove_refresh_source(this); + } + } +} + +void EStopHandler::get_next_packet(unsigned code, dcc::Packet* packet) +{ + packet->set_dcc_speed14(dcc::DccShortAddress(0), true, false + , dcc::Packet::EMERGENCY_STOP); + { + AtomicHolder l(this); + remaining_--; + if (remaining_ <= 0) + { + packet_processor_remove_refresh_source(this); + } + } +} + +} diff --git a/components/DCCSignalGenerator/Kconfig.projbuild b/components/DCCSignalGenerator/Kconfig.projbuild new file mode 100644 index 00000000..8f5c505a --- /dev/null +++ b/components/DCCSignalGenerator/Kconfig.projbuild @@ -0,0 +1,475 @@ +menu "DCC Signal" + menu "OPS" + config OPS_TRACK_NAME + string "Name" + default "OPS" + + config OPS_ENERGIZE_ON_STARTUP + bool "Energize track upon startup" + default n + help + Enabling this option will energize the OPS track + automatically upon startup of the command station. + + choice OPS_HBRIDGE_TYPE + bool "H-Bridge type" + default OPS_HBRIDGE_L298 + help + The following motor driver modules are supported: + L298 : Arduino Motor shield Rev3 based on the L298 chip. Max Output 2A per channel https://store.arduino.cc/usa/arduino-motor-shield-rev3 + LMD18200 : Texas Instruments LMD18200 55V 3A h-bridge. http://www.ti.com/lit/ds/symlink/lmd18200.pdf + POLOLU : Pololu MC33926 Motor Driver (shield or carrier). Max Output 2.5A per channel https://www.pololu.com/product/1213 / https://www.pololu.com/product/2503 + BTS7960B_5A : Infineon Technologies BTS 7960 Motor Driver Module. Max Output 5A (43A actual max) https://www.infineon.com/dgdl/bts7960b-pb-final.pdf + BTS7960B_10A : Infineon Technologies BTS 7960 Motor Driver Module. Max Output 10A (43A actual max) https://www.infineon.com/dgdl/bts7960b-pb-final.pdf + config OPS_HBRIDGE_L298 + bool "L298" + config OPS_HBRIDGE_LMD18200 + bool "LMD18200" + config OPS_HBRIDGE_POLOLU + bool "Pololu MC33926" + config OPS_HBRIDGE_BTS7960B_5A + bool "BTS7960B (5A limit)" + config OPS_HBRIDGE_BTS7960B_10A + bool "BTS7960B (10A limit)" + endchoice + + config OPS_HBRIDGE_TYPE_NAME + string + default "L298" if OPS_HBRIDGE_L298 + default "LMD18200" if OPS_HBRIDGE_LMD18200 + default "MC33926" if OPS_HBRIDGE_POLOLU + default "BTS7960B" if OPS_HBRIDGE_BTS7960B_5A + default "BTS7960B" if OPS_HBRIDGE_BTS7960B_10A + + config OPS_HBRIDGE_MAX_MILLIAMPS + int + default 2000 if OPS_HBRIDGE_L298 + default 3000 if OPS_HBRIDGE_LMD18200 + default 2500 if OPS_HBRIDGE_POLOLU + default 43000 if OPS_HBRIDGE_BTS7960B_5A + default 43000 if OPS_HBRIDGE_BTS7960B_10A + + config OPS_HBRIDGE_LIMIT_MILLIAMPS + int + default 2000 if OPS_HBRIDGE_L298 + default 3000 if OPS_HBRIDGE_LMD18200 + default 2500 if OPS_HBRIDGE_POLOLU + default 5000 if OPS_HBRIDGE_BTS7960B_5A + default 10000 if OPS_HBRIDGE_BTS7960B_10A + + config OPS_ENABLE_PIN + int "H-Bridge enable/pwm pin" + default 25 + range 0 32 + help + This pin will be HIGH when the H-Bridge output should be + enabled and will be LOW when it should be disabled. This + pin should typically be connected to the PWM input of the + H-Bridge IC. + + config OPS_SIGNAL_PIN + int "H-Bridge signal/direction pin" + default 19 + range 0 32 + help + This pin will transition HIGH/LOW based on the DCC signal + data being generated by the command station. This should + typically be connected to the direction pin on the H-Bridge + IC. + + choice OPS_CURRENT_SENSE_PIN + bool "H-Bridge current sense pin" + help + This is used for short circuit detection. + default OPS_ADC_CHANNEL_0 + config OPS_ADC_CHANNEL_0 + bool "ADC1 Channel 0 (GPIO 36)" + config OPS_ADC_CHANNEL_1 + bool "ADC1 Channel 1 (GPIO 37)" + help + Note this pin may not be exposed on all ESP32 modules + config OPS_ADC_CHANNEL_2 + bool "ADC1 Channel 2 (GPIO 38)" + help + Note this pin may not be exposed on all ESP32 modules + config OPS_ADC_CHANNEL_3 + bool "ADC1 Channel 3 (GPIO 39)" + config OPS_ADC_CHANNEL_4 + bool "ADC1 Channel 4 (GPIO 32)" + config OPS_ADC_CHANNEL_5 + bool "ADC1 Channel 5 (GPIO 33)" + config OPS_ADC_CHANNEL_6 + bool "ADC1 Channel 6 (GPIO 34)" + config OPS_ADC_CHANNEL_7 + bool "ADC1 Channel 7 (GPIO 35)" + endchoice + + config OPS_ADC + int + default 0 if OPS_ADC_CHANNEL_0 + default 1 if OPS_ADC_CHANNEL_1 + default 2 if OPS_ADC_CHANNEL_2 + default 3 if OPS_ADC_CHANNEL_3 + default 4 if OPS_ADC_CHANNEL_4 + default 5 if OPS_ADC_CHANNEL_5 + default 6 if OPS_ADC_CHANNEL_6 + default 7 if OPS_ADC_CHANNEL_7 + + config OPS_THERMAL_PIN + int "LMD18200 Thermal Alert pin" + range 0 39 + depends on OPS_HBRIDGE_LMD18200 + help + The LMD18200 has a thermal alert pin that will be HIGH if + the IC is too HOT for normal operation. When the command + station detects this condition it will shutdown the track + output and re-enable it once the thermal alert has been + cleared. + + config OPS_RAILCOM + bool "Enable RailCom detector" + default n + help + Enabling this functionality will cause the command station + to interrupt the DCC signal between packets to + receive/decode any RailCom data that decoders may be + writing to the rails. + + config OPS_RAILCOM_ENABLE_PIN + int "RailCom detector enable pin" + depends on OPS_RAILCOM + help + This pin will go HIGH when the RailCom detector circuitry + should be active and will go LOW when it should be + disabled. + + config OPS_RAILCOM_BRAKE_PIN + int "H-Bridge brake pin" + depends on OPS_RAILCOM && OPS_HBRIDGE_LMD18200 + help + This pin should be connected to the H-Bridge BRAKE pin + input and is used to put the H-Bridge into a "coast" mode. + + choice OPS_RAILCOM_UART + bool "RailCom UART" + default OPS_RAILCOM_UART1 + depends on OPS_RAILCOM + config OPS_RAILCOM_UART1 + bool "UART1" + config OPS_RAILCOM_UART2 + bool "UART2" + endchoice + + config OPS_RAILCOM_UART + int + default 1 if OPS_RAILCOM_UART1 + default 2 if OPS_RAILCOM_UART2 + depends on OPS_RAILCOM + + config OPS_RAILCOM_UART_RX_PIN + int "RailCom UART RX pin" + range 0 39 + depends on OPS_RAILCOM + help + This pin should be connected to the RailCom detector data + output. + + config OPS_RAILCOM_DUMP_PACKETS + bool "Display all RailCom packets as they are received" + default n + depends on OPS_RAILCOM + + config OPS_DCC_PREAMBLE_BITS + int "DCC packet preamble bits" + range 11 20 + default 11 if !OPS_RAILCOM + default 16 if OPS_RAILCOM + help + This controls the number of "1" bits to be transmitted + before the payload of the DCC packet. If RailCom is enabled + this must be at least 16, when RailCom is not enabled this + can be as few as 11. + + config OPS_PACKET_QUEUE_SIZE + int "Number of packets to queue for the OPS track" + default 5 + help + This is the number of raw packets to allow for outbound + transmission to the track. Generally this does not need to + be very large and should be around the same size as + DCC_PACKET_POOL_SIZE. + endmenu + + menu "PROG" + config PROG_TRACK_NAME + string "Name" + default "PROG" + + choice PROG_HBRIDGE_TYPE + bool "H-Bridge type" + default PROG_HBRIDGE_L298 + help + The following motor driver modules are supported: + L298 : Arduino Motor shield Rev3 based on the L298 chip. Max Output 2A per channel https://store.arduino.cc/usa/arduino-motor-shield-rev3 + LMD18200 : Texas Instruments LMD18200 55V 3A h-bridge. http://www.ti.com/lit/ds/symlink/lmd18200.pdf + POLOLU : Pololu MC33926 Motor Driver (shield or carrier). Max Output 2.5A per channel https://www.pololu.com/product/1213 / https://www.pololu.com/product/2503 + BTS7960B_5A : Infineon Technologies BTS 7960 Motor Driver Module. Max Output 5A (43A actual max) https://www.infineon.com/dgdl/bts7960b-pb-final.pdf + BTS7960B_10A : Infineon Technologies BTS 7960 Motor Driver Module. Max Output 10A (43A actual max) https://www.infineon.com/dgdl/bts7960b-pb-final.pdf + config PROG_HBRIDGE_L298 + bool "L298" + config PROG_HBRIDGE_LMD18200 + bool "LMD18200" + config PROG_HBRIDGE_POLOLU + bool "Pololu MC33926" + config PROG_HBRIDGE_BTS7960B_5A + bool "BTS7960B (5A limit)" + config PROG_HBRIDGE_BTS7960B_10A + bool "BTS7960B (10A limit)" + endchoice + + config PROG_HBRIDGE_TYPE_NAME + string + default "L298" if PROG_HBRIDGE_L298 + default "LMD18200" if PROG_HBRIDGE_LMD18200 + default "MC33926" if PROG_HBRIDGE_POLOLU + default "BTS7960B" if PROG_HBRIDGE_BTS7960B_5A + default "BTS7960B" if PROG_HBRIDGE_BTS7960B_10A + + config PROG_HBRIDGE_MAX_MILLIAMPS + int + default 2000 if PROG_HBRIDGE_L298 + default 3000 if PROG_HBRIDGE_LMD18200 + default 2500 if PROG_HBRIDGE_POLOLU + default 43000 if PROG_HBRIDGE_BTS7960B_5A + default 43000 if PROG_HBRIDGE_BTS7960B_10A + + config PROG_HBRIDGE_LIMIT_MILLIAMPS + int + default 2000 if PROG_HBRIDGE_L298 + default 3000 if PROG_HBRIDGE_LMD18200 + default 2500 if PROG_HBRIDGE_POLOLU + default 5000 if PROG_HBRIDGE_BTS7960B_5A + default 10000 if PROG_HBRIDGE_BTS7960B_10A + + config PROG_ENABLE_PIN + int "H-Bridge enable pin" + default 23 + range 0 32 + help + This pin will be HIGH when the H-Bridge output should be + enabled and will be LOW when it should be disabled. This + pin should typically be connected to the PWM input of the + H-Bridge IC. + + config PROG_SIGNAL_PIN + int "H-Bridge signal pin" + default 18 + range 0 32 + help + This pin will transition HIGH/LOW based on the DCC signal + data being generated by the command station. This should + typically be connected to the direction pin on the H-Bridge + IC. + + choice PROG_CURRENT_SENSE_PIN + bool "H-Bridge current sense pin" + default PROG_ADC_CHANNEL_3 + help + This pin must be connected to the H-Bridge current sense + output pin, failure to do so will result in no ability to + read/write/validate CVs successfully. + config PROG_ADC_CHANNEL_0 + bool "ADC1 Channel 0 (GPIO 36)" + depends on !OPS_ADC_CHANNEL_0 + config PROG_ADC_CHANNEL_1 + bool "ADC1 Channel 1 (GPIO 37)" + help + Note this pin may not be exposed on all ESP32 modules + depends on !OPS_ADC_CHANNEL_1 + config PROG_ADC_CHANNEL_2 + bool "ADC1 Channel 2 (GPIO 38)" + help + Note this pin may not be exposed on all ESP32 modules + depends on !OPS_ADC_CHANNEL_2 + config PROG_ADC_CHANNEL_3 + bool "ADC1 Channel 3 (GPIO 39)" + depends on !OPS_ADC_CHANNEL_3 + config PROG_ADC_CHANNEL_4 + bool "ADC1 Channel 4 (GPIO 32)" + depends on !OPS_ADC_CHANNEL_4 + config PROG_ADC_CHANNEL_5 + bool "ADC1 Channel 5 (GPIO 33)" + depends on !OPS_ADC_CHANNEL_5 + config PROG_ADC_CHANNEL_6 + bool "ADC1 Channel 6 (GPIO 34)" + depends on !OPS_ADC_CHANNEL_6 + config PROG_ADC_CHANNEL_7 + bool "ADC1 Channel 7 (GPIO 35)" + depends on !OPS_ADC_CHANNEL_7 + endchoice + + config PROG_ADC + int + default 0 if PROG_ADC_CHANNEL_0 + default 1 if PROG_ADC_CHANNEL_1 + default 2 if PROG_ADC_CHANNEL_2 + default 3 if PROG_ADC_CHANNEL_3 + default 4 if PROG_ADC_CHANNEL_4 + default 5 if PROG_ADC_CHANNEL_5 + default 6 if PROG_ADC_CHANNEL_6 + default 7 if PROG_ADC_CHANNEL_7 + + config PROG_DCC_PREAMBLE_BITS + int "DCC packet preamble bits" + range 22 75 + default 22 + help + This controls the number of "1" bits to be transmitted + before the payload of the DCC packet. Some decoders may + require more "1" bits for proper operation, this is usually + only a problem with some brands of sound decoders. + + config PROG_PACKET_QUEUE_SIZE + int "Number of packets to queue for the PROG track" + default 5 + help + This is the number of raw packets to allow for outbound + transmission to the track. Generally this does not need to + be very large and should be around the same size as + DCC_PACKET_POOL_SIZE. + endmenu + + choice ADC_ATTENUATION + bool "ADC attenuation" + default ADC_ATTEN_DB_11 + help + This setting controls the expected voltage on the current + sense input pins. + 0 dB attenuation gives full-scale voltage 1.1V + 2.5 dB attenuation gives full-scale voltage 1.5V + 6 dB attenuation gives full-scale voltage 2.2V + 11 dB attenuation gives full-scale voltage 3.9V + config ADC_ATTEN_DB_0 + bool "0 dB (1.1V max)" + config ADC_ATTEN_DB_2_5 + bool "2.5 dB (1.5V max)" + config ADC_ATTEN_DB_6 + bool "6 dB (2.2V max)" + config ADC_ATTEN_DB_11 + bool "11 dB (3.9V max)" + endchoice + + config ADC_ATTENUATION + int + default 0 if ADC_ATTEN_DB_0 + default 1 if ADC_ATTEN_DB_2_5 + default 2 if ADC_ATTEN_DB_6 + default 3 if ADC_ATTEN_DB_11 + + config ADC_AVERAGE_READING_COUNT + int + default 32 + + config DCC_PACKET_POOL_SIZE + int "Maximum number of DCC packets to queue" + default 5 + range 2 20 + help + Declares the maximum number of DCC packets to allow for the + track, generally this does not need to be very large and the + default value should be sufficient. + + config DCC_ESTOP_PACKET_COUNT + int "Number of eStop packets to send before powering off track" + default 200 + +############################################################################### +# +# Log level constants from from components/OpenMRNLite/src/utils/logging.h +# +# ALWAYS : -1 +# FATAL : 0 +# LEVEL_ERROR : 1 +# WARNING : 2 +# INFO : 3 +# VERBOSE : 4 +# +# Note that FATAL will cause the MCU to reboot! +# +############################################################################### + choice DCC_RMT_LOGGING + bool "DCC RMT logging" + default DCC_RMT_LOGGING_MINIMAL + config DCC_RMT_LOGGING_VERBOSE + bool "Verbose" + config DCC_RMT_LOGGING_MINIMAL + bool "Minimal" + endchoice + config DCC_RMT_LOG_LEVEL + int + default 4 if DCC_RMT_LOGGING_MINIMAL + default 3 if DCC_RMT_LOGGING_VERBOSE + default 5 + + config DCC_HBRIDGE_USAGE_REPORT_INTERVAL + int "Usage report interval (seconds)" + default 30 + range 15 300 + + config DCC_HBRIDGE_OVERCURRENT_BEFORE_SHUTDOWN + int "Number of consecutive over-current reads before shutdown" + default 3 + +############################################################################### +# +# These options should be used with extreme care as they will alter how the DCC +# signal is generated. This is most useful when debugging the DCC signal using +# LEDs to visually inspect the signal without a logic analyzer or oscilloscope. +# +# When using LEDs to debug the DCC signal it is necessary to slow the signal. +# Enabling DCC_RMT_USE_REF_CLOCK and setting DCC_RMT_DIVIDER to at least 160 or +# even 240 should be sufficient to slow the DCC signal for visible changes in +# the LEDs. If this is not slow enough the pulse lengths can be increased. +# +############################################################################### + menu "Advanced options" + config DCC_RMT_HIGH_FIRST + bool "Generate HIGH,LOW signal" + default y + help + When enabled this will cause the RMT to generate a HIGH then + LOW signal on the output PIN. When disabled it will be LOW then + HIGH. + + config DCC_RMT_USE_REF_CLOCK + bool "Use REF_TICK clock source (1Mhz)" + default n + help + When enabled this will cause the RMT to use the REF_TICK clock + as the clock source, this is a 1Mhz clock. By default the RMT + will use the APB clock which is an 80Mhz clock. + + config DCC_RMT_CLOCK_DIVIDER + int "RMT clock divider" + range 1 255 + default 1 if DCC_RMT_USE_REF_CLOCK + default 80 + help + When using the APB clock (80Mhz) this will provide around a + 1usec time factor for the RMT pulses. + + config DCC_RMT_TICKS_ZERO_PULSE + int "RMT ticks for DCC zero" + default 96 + help + This is how many ticks the RMT should use for each half of a + DCC ZERO bit. + + config DCC_RMT_TICKS_ONE_PULSE + int "RMT ticks for DCC one" + default 58 + help + This is how many ticks the RMT should use for each half of a + DCC ONE bit. + endmenu +endmenu \ No newline at end of file diff --git a/components/DCCSignalGenerator/MonitoredHBridge.cpp b/components/DCCSignalGenerator/MonitoredHBridge.cpp new file mode 100644 index 00000000..6cfc1e02 --- /dev/null +++ b/components/DCCSignalGenerator/MonitoredHBridge.cpp @@ -0,0 +1,290 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "MonitoredHBridge.h" +#include +#include +#include +#include + +namespace esp32cs +{ + +HBridgeShortDetector::HBridgeShortDetector(openlcb::Node *node + , const adc1_channel_t senseChannel + , const Gpio *enablePin + , const uint32_t limitMilliAmps + , const uint32_t maxMilliAmps + , const string &name + , const string &bridgeType + , const esp32cs::TrackOutputConfig &cfg) + : DefaultConfigUpdateListener() + , channel_(senseChannel) + , enablePin_(enablePin) + , maxMilliAmps_(maxMilliAmps) + , name_(name) + , bridgeType_(bridgeType) + , overCurrentLimit_(((((limitMilliAmps << 3) + limitMilliAmps) / 10) << 12) / maxMilliAmps_) // ~90% max value + , shutdownLimit_(4080) + , isProgTrack_(false) + , progAckLimit_(0) + , cfg_(cfg) + , targetLED_(StatusLED::LED::OPS_TRACK) + , shortBit_(node, 0, 0, &state_, STATE_OVERCURRENT) + , shutdownBit_(node, 0, 0, &state_, STATE_SHUTDOWN) + , shortProducer_(&shortBit_) + , shutdownProducer_(&shortBit_) +{ + // set warning limit to ~75% of overcurrent limit + warnLimit_ = ((overCurrentLimit_ << 1) + overCurrentLimit_) >> 2; + configure(); +} + +HBridgeShortDetector::HBridgeShortDetector(openlcb::Node *node + , const adc1_channel_t senseChannel + , const Gpio *enablePin + , const uint32_t maxMilliAmps + , const string &name + , const string &bridgeType + , const esp32cs::TrackOutputConfig &cfg) + : DefaultConfigUpdateListener() + , channel_(senseChannel) + , enablePin_(enablePin) + , maxMilliAmps_(maxMilliAmps) + , name_(name) + , bridgeType_(bridgeType) + , overCurrentLimit_((250 << 12) / maxMilliAmps_) // ~250mA + , shutdownLimit_((500 << 12) / maxMilliAmps_) + , isProgTrack_(true) + , progAckLimit_((60 << 12) / maxMilliAmps_) // ~60mA + , cfg_(cfg) + , targetLED_(StatusLED::LED::PROG_TRACK) + , shortBit_(node, 0, 0, &state_, STATE_OVERCURRENT) + , shutdownBit_(node, 0, 0, &state_, STATE_SHUTDOWN) + , shortProducer_(&shortBit_) + , shutdownProducer_(&shortBit_) +{ + // set warning limit to ~75% of overcurrent limit + warnLimit_ = ((overCurrentLimit_ << 1) + overCurrentLimit_) >> 2; + configure(); +} + +string HBridgeShortDetector::getState() +{ + switch (state_) + { + case STATE_ON: + return "Normal"; + case STATE_OVERCURRENT: + return "Fault"; + case STATE_SHUTDOWN: + return "Shutdown"; + case STATE_OFF: + default: + return "Off"; + } + return "Error"; +} + +string HBridgeShortDetector::getStateAsJson() +{ + return StringPrintf("{" + "\"name\":\"%s\"," + "\"state\":\"%s\"," + "\"usage\":%.2f," + "\"prog\":\"%s\"" + "}" + , name_.c_str() + , getState().c_str() + , getUsage() / 1000.0f + , isProgrammingTrack() ? "true" : "false" + ); +} + +string HBridgeShortDetector::getStatusData() +{ + if (state_ == STATE_ON) + { + return StringPrintf("%s:On (%2.2f A)", name_.c_str(), getUsage() / 1000.0f); + } + else if (state_ == STATE_OVERCURRENT) + { + return StringPrintf("%s:F (%2.2f A)", name_.c_str(), getUsage() / 1000.0f); + } + return StringPrintf("%s:Off", name_.c_str()); +} + +string HBridgeShortDetector::get_state_for_dccpp() +{ + if (state_ == STATE_ON) + { + return StringPrintf("", name_.c_str(), name_.c_str(), getLastReading()); + } + else if (state_ == STATE_OVERCURRENT) + { + return StringPrintf("", name_.c_str()); + } + return StringPrintf("", name_.c_str()); +} + +void HBridgeShortDetector::configure() +{ + adc1_config_channel_atten(channel_, (adc_atten_t)CONFIG_ADC_ATTENUATION); + LOG(INFO, "[%s] Configuring H-Bridge (%s %u mA max) using ADC 1:%d" + , name_.c_str(), bridgeType_.c_str(), maxMilliAmps_, channel_); + LOG(INFO, "[%s] Short limit %u/4096 (%.2f mA), events (on: %s, off: %s)" + , name_.c_str(), overCurrentLimit_ + , ((overCurrentLimit_ * maxMilliAmps_) / 4096.0f) + , uint64_to_string_hex(shortBit_.event_on()).c_str() + , uint64_to_string_hex(shortBit_.event_off()).c_str()); + LOG(INFO, "[%s] Shutdown limit %u/4096 (%.2f mA), events (on: %s, off: %s)" + , name_.c_str(), shutdownLimit_ + , ((shutdownLimit_ * maxMilliAmps_) / 4096.0f) + , uint64_to_string_hex(shutdownBit_.event_on()).c_str() + , uint64_to_string_hex(shutdownBit_.event_off()).c_str()); + if (isProgTrack_) + { + LOG(INFO, "[%s] Prog ACK: %u/4096 (%.2f mA)", name_.c_str(), progAckLimit_ + , ((progAckLimit_ * maxMilliAmps_) / 4096.0f)); + } +} + +void HBridgeShortDetector::poll_33hz(openlcb::WriteHelper *helper, Notifiable *done) +{ + vector samples; + + // collect samples from ADC + while(samples.size() < adcSampleCount_) + { + samples.push_back(adc1_get_raw(channel_)); + ets_delay_us(1); + } + // average the collected samples + lastReading_ = (std::accumulate(samples.begin(), samples.end(), 0) / samples.size()); + + // if this is the PROG track check up front if we have a short or ACK. + if (isProgTrack_ && progEnable_) + { + LOG(VERBOSE, "[%s] reading: %d", name_.c_str(), lastReading_); + auto backend = Singleton::instance(); + if (lastReading_ >= overCurrentLimit_) + { + // note that only over current is checked here since this should be + // triggered before the shutdown current has been reached. + backend->notify_service_mode_short(); + } + else if (lastReading_ >= progAckLimit_) + { + // send the ack over to the backend since it is over the limit. + backend->notify_service_mode_ack(); + } + } + + uint8_t previous_state = state_; + + if (lastReading_ >= shutdownLimit_) + { + // If the average sample exceeds the shutdown limit (~90% typically) + // trigger an immediate shutdown. + LOG_ERROR("[%s] Shutdown threshold breached %6.2f mA (raw: %d / %d)" + , name_.c_str() + , getUsage() / 1000.0f + , lastReading_ + , shutdownLimit_); + enablePin_->clr(); + state_ = STATE_SHUTDOWN; +#if CONFIG_STATUS_LED + Singleton::instance()->setStatusLED((StatusLED::LED)targetLED_ + , StatusLED::COLOR::RED_BLINK); +#endif // CONFIG_STATUS_LED + } + else if (lastReading_ >= overCurrentLimit_) + { + // If we have at least a couple averages that are over the soft limit + // trigger an immediate shutdown as a short is likely to have occurred. + if(overCurrentCheckCount_++ >= overCurrentRetryCount_) + { + // disable the h-bridge output + enablePin_->clr(); + LOG_ERROR("[%s] Overcurrent detected %6.2f mA (raw: %d / %d)" + , name_.c_str() + , getUsage() / 1000.0f + , lastReading_ + , overCurrentLimit_); + state_ = STATE_OVERCURRENT; +#if CONFIG_STATUS_LED + Singleton::instance()->setStatusLED((StatusLED::LED)targetLED_ + , StatusLED::COLOR::RED); +#endif // CONFIG_STATUS_LED + } + } + else + { + if (enablePin_->is_set()) + { + overCurrentCheckCount_ = 0; + state_ = STATE_ON; +#if CONFIG_STATUS_LED + // check if we are over the warning limit and update the LED accordingly. + if (lastReading_ >= warnLimit_) + { + Singleton::instance()->setStatusLED((StatusLED::LED)targetLED_ + , StatusLED::COLOR::YELLOW); + } + else + { + Singleton::instance()->setStatusLED((StatusLED::LED)targetLED_ + , StatusLED::COLOR::GREEN); + } +#endif // CONFIG_STATUS_LED + } + else + { + state_ = STATE_OFF; + } + } + if (state_ == STATE_ON && + (esp_timer_get_time() - lastReport_) >= currentReportInterval_) + { + lastReport_ = esp_timer_get_time(); + LOG(INFO, "[%s] %6.0f mA / %d mA", name_.c_str(), getUsage() / 1000.0f + , maxMilliAmps_); + } + + // if our state has changed send out applicable events + bool async_event_req = false; + if (previous_state != state_) + { + if (previous_state == STATE_SHUTDOWN || state_ == STATE_SHUTDOWN) + { + shutdownProducer_.SendEventReport(helper, done); + async_event_req = true; + } + else if (previous_state == STATE_OVERCURRENT || state_ == STATE_OVERCURRENT) + { + shortProducer_.SendEventReport(helper, done); + async_event_req = true; + } + } + + if (!async_event_req) + { + done->notify(); + } +} + +} // namespace esp32cs diff --git a/components/DCCSignalGenerator/RMTTrackDevice.cpp b/components/DCCSignalGenerator/RMTTrackDevice.cpp new file mode 100644 index 00000000..1fdfb5a0 --- /dev/null +++ b/components/DCCSignalGenerator/RMTTrackDevice.cpp @@ -0,0 +1,448 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "RMTTrackDevice.h" +#include "sdkconfig.h" + +#include +#include + + +namespace esp32cs +{ +/////////////////////////////////////////////////////////////////////////////// +// The NMRA DCC Signal is sent as a square wave with each half having +// identical timing (or nearly identical). Packet Bytes have a minimum of 11 +// preamble ONE bits in order to be considered valid by the decoder. For +// RailCom support it is recommended to have at least 16 preamble bits. For the +// Programming Track it is required to have a longer preamble of at least 22 +// bits. Packet data follows immediately after the preamble bits, between the +// packet bytes a DCC ZERO is sent. After the last byte of packet data a DCC +// ONE is sent. +// +// DCC ZERO: +// ---------------- +// | 96 | +// ---| usec | 96 --- +// | usec | +// ---------------- +// DCC ONE: +// -------- +// | 58 | +// ---| usec | 58 --- +// | usec | +// -------- +// +// The timing can be adjusted via menuconfig with the above being the default +// values when using the APB clock. +// +/////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////// +// DCC ZERO bit pre-encoded in RMT format. +/////////////////////////////////////////////////////////////////////////////// +#if CONFIG_DCC_RMT_HIGH_FIRST +static constexpr rmt_item32_t DCC_RMT_ZERO_BIT = +{{{ + CONFIG_DCC_RMT_TICKS_ZERO_PULSE // number of microseconds for TOP half + , 1 // of the square wave. + , CONFIG_DCC_RMT_TICKS_ZERO_PULSE // number of microseconds for BOTTOM half + , 0 // of the square wave. +}}}; +#else +{{{ + CONFIG_DCC_RMT_TICKS_ZERO_PULSE // number of microseconds for TOP half + , 0 // of the square wave. + , CONFIG_DCC_RMT_TICKS_ZERO_PULSE // number of microseconds for BOTTOM half + , 1 // of the square wave. +}}}; +#endif // CONFIG_DCC_RMT_HIGH_FIRST + +/////////////////////////////////////////////////////////////////////////////// +// DCC ONE bit pre-encoded in RMT format. +/////////////////////////////////////////////////////////////////////////////// +#if CONFIG_DCC_RMT_HIGH_FIRST +static constexpr rmt_item32_t DCC_RMT_ONE_BIT = +{{{ + CONFIG_DCC_RMT_TICKS_ONE_PULSE // number of microseconds for TOP half + , 1 // of the square wave. + , CONFIG_DCC_RMT_TICKS_ONE_PULSE // number of microseconds for BOTTOM half + , 0 // of the square wave. +}}}; +#else +static constexpr rmt_item32_t DCC_RMT_ONE_BIT = +{{{ + CONFIG_DCC_RMT_TICKS_ONE_PULSE // number of microseconds for TOP half + , 0 // of the square wave. + , CONFIG_DCC_RMT_TICKS_ONE_PULSE // number of microseconds for BOTTOM half + , 1 // of the square wave. +}}}; +#endif // CONFIG_DCC_RMT_HIGH_FIRST + +/////////////////////////////////////////////////////////////////////////////// +// Marklin Motorola bit timing (WIP) +// https://people.zeelandnet.nl/zondervan/digispan.html +// http://www.drkoenig.de/digital/motorola.htm +/////////////////////////////////////////////////////////////////////////////// +static constexpr uint32_t MARKLIN_ZERO_BIT_PULSE_HIGH_USEC = 182; +static constexpr uint32_t MARKLIN_ZERO_BIT_PULSE_LOW_USEC = 26; +static constexpr uint32_t MARKLIN_ONE_BIT_PULSE_HIGH_USEC = 26; +static constexpr uint32_t MARKLIN_ONE_BIT_PULSE_LOW_USEC = 182; +static constexpr uint32_t MARKLIN_PREAMBLE_BIT_PULSE_HIGH_USEC = 104; +static constexpr uint32_t MARKLIN_PREAMBLE_BIT_PULSE_LOW_USEC = 104; + +/////////////////////////////////////////////////////////////////////////////// +// Marklin Motorola ZERO bit pre-encoded in RMT format, sent as HIGH then LOW. +/////////////////////////////////////////////////////////////////////////////// +static constexpr rmt_item32_t MARKLIN_RMT_ZERO_BIT = +{{{ + MARKLIN_ZERO_BIT_PULSE_HIGH_USEC // number of microseconds for TOP half + , 1 // of the square wave. + , MARKLIN_ZERO_BIT_PULSE_LOW_USEC // number of microseconds for BOTTOM half + , 0 // of the square wave. +}}}; + +/////////////////////////////////////////////////////////////////////////////// +// Marklin Motorola ONE bit pre-encoded in RMT format, sent as HIGH then LOW. +/////////////////////////////////////////////////////////////////////////////// +static constexpr rmt_item32_t MARKLIN_RMT_ONE_BIT = +{{{ + MARKLIN_ZERO_BIT_PULSE_HIGH_USEC // number of microseconds for TOP half + , 1 // of the square wave. + , MARKLIN_ZERO_BIT_PULSE_LOW_USEC // number of microseconds for BOTTOM half + , 0 // of the square wave. +}}}; + +/////////////////////////////////////////////////////////////////////////////// +// Marklin Motorola preamble bit pre-encoded in RMT format, both top and bottom +// half of the wave are LOW. +/////////////////////////////////////////////////////////////////////////////// +static constexpr rmt_item32_t MARKLIN_RMT_PREAMBLE_BIT = +{{{ + MARKLIN_PREAMBLE_BIT_PULSE_HIGH_USEC // number of microseconds for TOP half + , 0 // of the square wave. + , MARKLIN_PREAMBLE_BIT_PULSE_LOW_USEC // number of microseconds for BOTTOM + , 0 // half of the square wave. +}}}; + +/////////////////////////////////////////////////////////////////////////////// +// Declare ISR flags for the RMT driver ISR. +// +// NOTE: ESP_INTR_FLAG_IRAM is *NOT* included in this bitmask so that we do not +// have a dependency on execution from IRAM and the related software +// limitations of execution from there. +/////////////////////////////////////////////////////////////////////////////// +static constexpr uint32_t RMT_ISR_FLAGS = +( + ESP_INTR_FLAG_LOWMED // ISR is implemented in C code + | ESP_INTR_FLAG_SHARED // ISR is shared across multiple handlers +); + +/////////////////////////////////////////////////////////////////////////////// +// Bit mask constants used as part of the packet translation layer. +/////////////////////////////////////////////////////////////////////////////// +static constexpr DRAM_ATTR uint8_t PACKET_BIT_MASK[] = +{ + 0x80, 0x40, 0x20, 0x10, // + 0x08, 0x04, 0x02, 0x01 // +}; + +/////////////////////////////////////////////////////////////////////////////// +// RMTTrackDevice constructor. +// +// This creates a VFS interface for the packet queue which can be used by +// the LocalTrackIf implementation. +// +// The VFS mount point is /dev/track. This must be opened by the caller before +// the LocalTrackIf is able to route dcc::Packets to the track. +// +// This also allocates two h-bridge monitoring state flows, these will check +// for short circuits and disable the track output from the h-bridge +// independently from the RMT signal being generated. +/////////////////////////////////////////////////////////////////////////////// +RMTTrackDevice::RMTTrackDevice(const char *name + , const rmt_channel_t channel + , const uint8_t dccPreambleBitCount + , size_t packet_queue_len + , gpio_num_t pin + , RailcomDriver *railcomDriver) + : name_(name) + , channel_(channel) + , dccPreambleBitCount_(dccPreambleBitCount) + , railcomDriver_(railcomDriver) + , packetQueue_(DeviceBuffer::create(packet_queue_len)) +{ + uint16_t maxBitCount = dccPreambleBitCount_ // preamble bits + + 1 // packet start bit + + (dcc::Packet::MAX_PAYLOAD * 8) // payload bits + + dcc::Packet::MAX_PAYLOAD // end of byte bits + + 1 // end of packet bit + + 1; // RMT extra bit + HASSERT(maxBitCount <= MAX_RMT_BITS); + + uint8_t memoryBlocks = (maxBitCount / RMT_MEM_ITEM_NUM) + 1; + HASSERT(memoryBlocks <= MAX_RMT_MEMORY_BLOCKS); + + LOG(INFO + , "[%s] DCC config: zero: %duS, one: %duS, preamble-bits: %d, wave: %s" + , name_, CONFIG_DCC_RMT_TICKS_ZERO_PULSE, CONFIG_DCC_RMT_TICKS_ONE_PULSE + , dccPreambleBitCount_ +#if CONFIG_DCC_RMT_HIGH_FIRST + , "high, low" +#else + , "low, high" +#endif + ); + /*LOG(INFO + , "[%s] Marklin-Motorola config: zero: %duS (high), zero: %duS (low), " + "one: %duS (high), one: %duS (low), preamble: %duS (low)", + , name_, MARKLIN_ZERO_BIT_PULSE_HIGH_USEC, MARKLIN_ZERO_BIT_PULSE_LOW_USEC + , MARKLIN_ONE_BIT_PULSE_HIGH_USEC, MARKLIN_ONE_BIT_PULSE_LOW_USEC + , MARKLIN_PREAMBLE_BIT_PULSE_HIGH_USEC + MARKLIN_PREAMBLE_BIT_PULSE_LOW_USEC);*/ + LOG(INFO + , "[%s] signal pin: %d, RMT(ch:%d,mem:%d[%d],clk-div:%d,clk-src:%s)" + , name_, pin, channel_, maxBitCount, memoryBlocks + , CONFIG_DCC_RMT_CLOCK_DIVIDER +#if defined(CONFIG_DCC_RMT_USE_REF_CLOCK) + , "REF" +#else + , "APB" +#endif // CONFIG_DCC_RMT_USE_REF_CLOCK + ); + rmt_config_t config = + { + .rmt_mode = RMT_MODE_TX, + .channel = channel_, + .clk_div = CONFIG_DCC_RMT_CLOCK_DIVIDER, + .gpio_num = pin, + .mem_block_num = memoryBlocks, + { + .tx_config = + { + .loop_en = false, + .carrier_freq_hz = 0, + .carrier_duty_percent = 0, + .carrier_level = rmt_carrier_level_t::RMT_CARRIER_LEVEL_LOW, + .carrier_en = false, + .idle_level = rmt_idle_level_t::RMT_IDLE_LEVEL_LOW, + .idle_output_en = false + } + } + }; + ESP_ERROR_CHECK(rmt_config(&config)); + ESP_ERROR_CHECK(rmt_driver_install(channel_, 0, RMT_ISR_FLAGS)); + +#if defined(CONFIG_DCC_RMT_USE_REF_CLOCK) + LOG(INFO, "[%s] Configuring RMT to use REF_CLK", name_); + ESP_ERROR_CHECK(rmt_set_source_clk(channel_, RMT_BASECLK_REF)); +#endif // CONFIG_DCC_RMT_USE_APB_CLOCK + + LOG(INFO, "[%s] Starting signal generator", name_); + // send one bit to kickstart the signal, remaining data will come from the + // packet queue. We intentionally do not wait for the RMT TX complete here. + rmt_write_items(channel_, &DCC_RMT_ONE_BIT, 1, false); +} + +/////////////////////////////////////////////////////////////////////////////// +// ESP VFS callback for ::write() +// +// This will write *ONE* dcc::Packet to either the OPS or PROG packet queue. If +// there is no space in the packet queue the packet will be rejected and errno +// set to ENOSPC. +// +// NOTE: At this time Marklin packets will be actively rejected. +/////////////////////////////////////////////////////////////////////////////// +ssize_t RMTTrackDevice::write(int fd, const void * data, size_t size) +{ + if (size != sizeof(dcc::Packet)) + { + errno = EINVAL; + return -1; + } + const dcc::Packet *sourcePacket{(dcc::Packet *)data}; + + if (sourcePacket->packet_header.is_marklin) + { + // drop marklin packets as unsupported for now. + errno = ENOTSUP; + return -1; + } + + { + AtomicHolder l(&packetQueueLock_); + dcc::Packet* writePacket; + if (packetQueue_->space() && + packetQueue_->data_write_pointer(&writePacket)) + { + memcpy(writePacket, data, size); + packetQueue_->advance(1); + return 1; + } + } + // packet queue is full! + errno = ENOSPC; + return -1; +} + +/////////////////////////////////////////////////////////////////////////////// +// ESP VFS callback for ::ioctl() +// +// When the cmd is CAN_IOC_WRITE_ACTIVE the packet queue will be checked. When +// there is no space in the queue the Notifiable will be stored to be called +// after the next DCC packet has been transmitted. Any existing Notifiable will +// be called to requeue themselves if necessary. +/////////////////////////////////////////////////////////////////////////////// +int RMTTrackDevice::ioctl(int fd, int cmd, va_list args) +{ + // Attempt to write a Packet to the queue + if (IOC_TYPE(cmd) == CAN_IOC_MAGIC && IOC_SIZE(cmd) == NOTIFIABLE_TYPE && + cmd == CAN_IOC_WRITE_ACTIVE) + { + Notifiable* n = reinterpret_cast(va_arg(args, uintptr_t)); + HASSERT(n); + { + AtomicHolder l(&packetQueueLock_); + if (!packetQueue_->space()) + { + // stash the notifiable so we can call it later when there is space + std::swap(n, notifiable_); + } + } + if (n) + { + n->notify(); + } + return 0; + } + + // Unknown ioctl operation + errno = EINVAL; + return -1; +} + +/////////////////////////////////////////////////////////////////////////////// +// RMT transmit complete callback. +// +// When RailCom is enabled this will poll for RailCom data before transmission +// of the next dcc::Packet from the queue. +// +// Note: This does not use ESP-IDF provided rmt_write_items to increase +// performance by avoiding various FreeRTOS functions used by the API. +/////////////////////////////////////////////////////////////////////////////// +void RMTTrackDevice::rmt_transmit_complete() +{ + encode_next_packet(); + railcomDriver_->start_cutout(); + + // send the packet to the RMT, note not using memcpy for the packet as this + // directly accesses hardware registers. + RMT.apb_conf.fifo_mask = RMT_DATA_MODE_MEM; + for(uint32_t index = 0; index < pktLength_; index++) + { + RMTMEM.chan[channel_].data32[index].val = packet_[index].val; + } + // RMT marker for "end of data" + RMTMEM.chan[channel_].data32[pktLength_].val = 0; + // start transmit + RMT.conf_ch[channel_].conf1.mem_rd_rst = 1; + RMT.conf_ch[channel_].conf1.mem_owner = RMT_MEM_OWNER_TX; + RMT.conf_ch[channel_].conf1.tx_start = 1; +} + +/////////////////////////////////////////////////////////////////////////////// +// Transfers a dcc::Packet to the OPS packet queue. +// +// NOTE: At this time Marklin packets will be actively discarded. +/////////////////////////////////////////////////////////////////////////////// +void RMTTrackDevice::send(Buffer *b, unsigned prio) +{ + // if it is not a Marklin Motorola packet put it in the queue, otherwise + // discard. + if (!b->data()->packet_header.is_marklin) + { + AtomicHolder l(&packetQueueLock_); + dcc::Packet* writePacket; + if (packetQueue_->space() && + packetQueue_->data_write_pointer(&writePacket)) + { + memcpy(writePacket, b->data(), b->size()); + packetQueue_->advance(1); + } + } + b->unref(); +} + +/////////////////////////////////////////////////////////////////////////////// +// Encode the next packet or reuse the existing packet. +/////////////////////////////////////////////////////////////////////////////// +void RMTTrackDevice::encode_next_packet() +{ + // Check if we need to encode the next packet or if we still have at least + // one repeat left of the current packet. + if (--pktRepeatCount_ >= 0) + { + return; + } + // attempt to fetch a packet from the queue or use an idle packet + Notifiable* n = nullptr; + dcc::Packet packet{dcc::Packet::DCC_IDLE()}; + { + AtomicHolder l(&packetQueueLock_); + if (packetQueue_->get(&packet, 1)) + { + // since we removed a packet from the queue, check if we have a pending + // notifiable to wake up. + std::swap(n, notifiable_); + } + } + if (n) + { + n->notify_from_isr(); + } + // TODO: add encoding for Marklin-Motorola + + // encode the preamble bits + for (pktLength_ = 0; pktLength_ < dccPreambleBitCount_; pktLength_++) + { + packet_[pktLength_].val = DCC_RMT_ONE_BIT.val; + } + // start of payload marker + packet_[pktLength_++].val = DCC_RMT_ZERO_BIT.val; + // encode the packet bits + for (uint8_t dlc = 0; dlc < packet.dlc; dlc++) + { + for(uint8_t bit = 0; bit < 8; bit++) + { + packet_[pktLength_++].val = + packet.payload[dlc] & PACKET_BIT_MASK[bit] ? + DCC_RMT_ONE_BIT.val : DCC_RMT_ZERO_BIT.val; + } + // end of byte marker + packet_[pktLength_++].val = DCC_RMT_ZERO_BIT.val; + } + // set the last bit of the encoded payload to be an end of packet marker + packet_[pktLength_ - 1].val = DCC_RMT_ONE_BIT.val; + // add an extra ONE bit to the end to prevent mangling of the last bit by + // the RMT + packet_[pktLength_++].val = DCC_RMT_ONE_BIT.val; + // record the repeat count + pktRepeatCount_ = packet.packet_header.rept_count; + + railcomDriver_->set_feedback_key(packet.feedback_key); +} + +} // namespace esp32cs diff --git a/include/ConfigurationManager.h b/components/DCCSignalGenerator/include/DCCSignalVFS.h similarity index 55% rename from include/ConfigurationManager.h rename to components/DCCSignalGenerator/include/DCCSignalVFS.h index 294fc981..50174c1b 100644 --- a/include/ConfigurationManager.h +++ b/components/DCCSignalGenerator/include/DCCSignalVFS.h @@ -1,7 +1,7 @@ /********************************************************************** ESP32 COMMAND STATION -COPYRIGHT (c) 2018-2019 Mike Dunston +COPYRIGHT (c) 2020 Mike Dunston 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 @@ -15,24 +15,31 @@ COPYRIGHT (c) 2018-2019 Mike Dunston along with this program. If not, see http://www.gnu.org/licenses **********************************************************************/ -#pragma once +#include "TrackOutputDescriptor.h" -#include +#include +#include -// Class definition for the Configuration Management system in ESP32 Command Station -class ConfigurationManager { -public: - ConfigurationManager(); - virtual ~ConfigurationManager(); - void init(); - void clear(); +namespace esp32cs +{ - bool exists(const char *); - void remove(const char *); - JsonObject &load(const char *); - JsonObject &load(const char *, DynamicJsonBuffer &); - void store(const char *, const JsonObject &); - JsonObject &createRootNode(bool=true); -}; +void init_dcc_vfs(openlcb::Node *node, Service *service + , const esp32cs::TrackOutputConfig &ops_cfg + , const esp32cs::TrackOutputConfig &prog_cfg); -extern ConfigurationManager configStore; +void shutdown_dcc_vfs(); + +void initiate_estop(); + +bool is_ops_track_output_enabled(); + +void enable_ops_track_output(); + +void disable_track_outputs(); + +std::string get_track_state_json(); + +// retrive status of the track signal and current usage. +std::string get_track_state_for_dccpp(); + +} // namespace esp32cs \ No newline at end of file diff --git a/components/DCCSignalGenerator/include/DuplexedTrackIf.h b/components/DCCSignalGenerator/include/DuplexedTrackIf.h new file mode 100644 index 00000000..089dba56 --- /dev/null +++ b/components/DCCSignalGenerator/include/DuplexedTrackIf.h @@ -0,0 +1,88 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef DUPLEXED_TRACK_IF_H_ +#define DUPLEXED_TRACK_IF_H_ + +#include +#include +#include +#include + +namespace esp32cs +{ + +/// StateFlow that accepts dcc::Packet structures and sends them to a local +/// device driver for producing the track signal. +/// +/// The device driver must support the notifiable-based asynchronous write +/// model. +class DuplexedTrackIf : public StateFlow, QList<1>> + , public Singleton +{ +public: + /// Creates a TrackInterface from an fd to the mainline and an fd for prog. + /// + /// This class currently does synchronous writes to the device. In order not + /// to block the executor, you have to create a new threadexecutor. + /// + /// @param service THE EXECUTOR OF THIS SERVICE WILL BE BLOCKED. + /// @param pool_size will determine how many packets the current flow's + /// alloc() will have. + /// @param ops_fd is the file descriptor for the OPS track. + /// @param prog_fd is the file descriptor for the PROG track. + DuplexedTrackIf(Service *service, int pool_size, int ops_fd, int prog_fd); + + /// @return the @ref FixedPool for dcc::Packet objects to send to the track. + FixedPool *pool() override + { + return &pool_; + } + +protected: + /// Sends a queued packet to either the OPS or PROG track. + /// + /// Track selection is made based on the DCC header flag for a longer + /// preamble which is only used for PROG track packets. + /// + /// If the packet can not be written to the file descriptor it will be held + /// until the device driver alerts that it is ready for another packet. + /// + /// Note: Both OPS and PROG packets will be processed by this method and if + /// either device driver prevents the write operation both tracks will be + /// blocked. + Action entry() override; + + /// @return next action. + Action finish() + { + return release_and_exit(); + } + + /// File descriptor for the OPS track output. + const int fd_ops_; + + /// File descriptor for the PROG track output. + const int fd_prog_; + + /// Packet pool from which to allocate packets. + FixedPool pool_; +}; + +} // namespace esp32cs + +#endif // DUPLEXED_TRACK_IF_H_ \ No newline at end of file diff --git a/components/DCCSignalGenerator/include/TrackOutputDescriptor.h b/components/DCCSignalGenerator/include/TrackOutputDescriptor.h new file mode 100644 index 00000000..326f9d31 --- /dev/null +++ b/components/DCCSignalGenerator/include/TrackOutputDescriptor.h @@ -0,0 +1,77 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef TRACK_OUTPUT_DESCRIPTOR_H_ +#define TRACK_OUTPUT_DESCRIPTOR_H_ + +#include + +namespace esp32cs +{ + /// Track output configuration + CDI_GROUP(TrackOutputConfig); + CDI_GROUP_ENTRY(description, + openlcb::StringConfigEntry<15>, + Name("Description"), + Description("Track output description.")); + CDI_GROUP_ENTRY(event_short, + openlcb::EventConfigEntry, + Name("Short Detected"), + Description("This event will be produced when a short has " + "been detected on the track output.")); + CDI_GROUP_ENTRY(event_short_cleared, + openlcb::EventConfigEntry, + Name("Short Cleared"), + Description("This event will be produced when a short has " + "been cleared on the track output.")); + CDI_GROUP_ENTRY(event_shutdown, + openlcb::EventConfigEntry, + Name("H-Bridge Shutdown"), + Description("This event will be produced when the track " + "output power has exceeded the safety threshold " + "of the H-Bridge.")); + CDI_GROUP_ENTRY(event_shutdown_cleared, + openlcb::EventConfigEntry, + Name("H-Bridge Shutdown Cleared"), + Description("This event will be produced when the track " + "output power has returned to safe levels.")); + CDI_GROUP_ENTRY(event_thermal_shutdown, + openlcb::EventConfigEntry, + Name("H-Bridge Thermal Shutdown"), + Description("This event will be produced when the H-Bridge " + "raises a thermal warning alert.")); + CDI_GROUP_ENTRY(event_thermal_shutdown_cleared, + openlcb::EventConfigEntry, + Name("H-Bridge Thermal Shutdown Cleared"), + Description("This event will be produced when the H-Bridge " + "clears the thermal warning alert.")); + CDI_GROUP_ENTRY(thermal_debounce, + openlcb::Uint8ConfigEntry, + Name("H-Bridge Thermal Shutdown Debounce"), + Default(2), + Description("Amount of time to allow for stabilization of " + "the H-Bridge Thermal pin before production of " + "an event. Each period is approximately 30 msec") + ); + CDI_GROUP_END(); + + static constexpr uint8_t OPS_CDI_TRACK_OUTPUT_IDX = 0; + static constexpr uint8_t PROG_CDI_TRACK_OUTPUT_IDX = 1; + +} // namespace esp32cs + +#endif // TRACK_OUTPUT_DESCRIPTOR_H_ \ No newline at end of file diff --git a/components/DCCSignalGenerator/private_include/EStopHandler.h b/components/DCCSignalGenerator/private_include/EStopHandler.h new file mode 100644 index 00000000..1e133bbd --- /dev/null +++ b/components/DCCSignalGenerator/private_include/EStopHandler.h @@ -0,0 +1,72 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef ESTOP_HANDLER_H_ +#define ESTOP_HANDLER_H_ + +#include +#include +#include +#include +#include +#include + +namespace esp32cs +{ + +// Event handler for the E-Stop well known events. This will generate a +// continuous stream of e-stop DCC packets until the E-Stop event has been +// received or the state has been reset via API. +class EStopHandler : public openlcb::BitEventInterface + , public dcc::NonTrainPacketSource + , private Atomic +{ +public: + EStopHandler(openlcb::Node *node) + : BitEventInterface(openlcb::Defs::EMERGENCY_STOP_EVENT + , openlcb::Defs::CLEAR_EMERGENCY_STOP_EVENT) + , node_(node) + , remaining_(0) + { + LOG(INFO, "[eStop] Registering emergency stop handler (On: %s, Off:%s)" + , uint64_to_string_hex(openlcb::Defs::EMERGENCY_STOP_EVENT).c_str() + , uint64_to_string_hex(openlcb::Defs::CLEAR_EMERGENCY_STOP_EVENT).c_str()); + } + + openlcb::EventState get_current_state() override + { + return openlcb::EventState::INVALID; + } + + void set_state(bool new_value) override; + + void get_next_packet(unsigned code, dcc::Packet* packet); + + openlcb::Node *node() override + { + return node_; + } + +private: + openlcb::BitEventPC pc_{this}; + openlcb::Node *node_; + int16_t remaining_; +}; + +} // namespace esp32cs + +#endif // ESTOP_HANDLER_H_ diff --git a/components/DCCSignalGenerator/private_include/Esp32RailComDriver.h b/components/DCCSignalGenerator/private_include/Esp32RailComDriver.h new file mode 100644 index 00000000..edfd5f8c --- /dev/null +++ b/components/DCCSignalGenerator/private_include/Esp32RailComDriver.h @@ -0,0 +1,311 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef ESP32_RAILCOM_DRIVER_H_ +#define ESP32_RAILCOM_DRIVER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace esp32cs +{ + +template +static void esp32_railcom_timer_tick(void *param); + +template +static void esp32_railcom_uart_isr(void *arg); + +template +class Esp32RailComDriver : public RailcomDriver +{ +public: + Esp32RailComDriver() + { + } + + void hw_init(dcc::RailcomHubFlow *hubFlow) + { + railComHubFlow_ = hubFlow; + + HW::hw_init(); + LOG(INFO, "[RailCom] Initializing detector using UART %d", HW::UART); + uint32_t baud_clock = (((esp_clk_apb_freq()) << 4) / 250000L); + HW::UART_BASE->conf1.rx_flow_en = 0; + HW::UART_BASE->conf0.tx_flow_en = 0; + HW::UART_BASE->conf0.val = 0; + HW::UART_BASE->conf0.parity = 0; + HW::UART_BASE->conf0.bit_num = 3; + HW::UART_BASE->conf0.stop_bit_num = 1; + HW::UART_BASE->clk_div.div_int = (baud_clock >> 4); + HW::UART_BASE->clk_div.div_frag = (baud_clock & 0xf); + HW::UART_BASE->idle_conf.tx_idle_num = 0; + HW::UART_BASE->rs485_conf.dl1_en = 0; + + ESP_ERROR_CHECK( + esp_intr_alloc(HW::UART_ISR_SOURCE, ESP_INTR_FLAG_LOWMED + , esp32_railcom_uart_isr, this, nullptr)); + + LOG(INFO, "[RailCom] Configuring hardware timer (%d:%d)...", HW::TIMER_GRP + , HW::TIMER_IDX); + periph_module_enable(HW::TIMER_PERIPH); + configure_timer(false, 80, false, true, HW::RAILCOM_TRIGGER_DELAY_USEC, true); + ESP_ERROR_CHECK( + esp_intr_alloc_intrstatus(HW::TIMER_ISR_SOURCE, ESP_INTR_FLAG_LOWMED + , TIMG_INT_ST_TIMERS_REG(HW::TIMER_GRP) + , BIT(HW::TIMER_IDX) + , esp32_railcom_timer_tick, this, nullptr)); + } + + void feedback_sample() override + { + // NOOP + } + + void disable_output() + { + // Enable the BRAKE pin on the h-bridge to force it into coast mode + HW::HB_BRAKE::set(true); + + // cache the current state of the pin so we can restore it after the + // cutout. + enabled_= HW::HB_ENABLE::get(); + HW::HB_ENABLE::set(false); + + //ets_delay_us(0); + } + + void enable_output() + { + HW::HB_ENABLE::set(enabled_); + HW::HB_BRAKE::set(false); + } + + void start_cutout() override + { + disable_output(); + + ets_delay_us(HW::RAILCOM_TRIGGER_DELAY_USEC); + + // flush the uart queue of any pending data + reset_uart_fifo(); + + // enable the UART RX interrupts + SET_PERI_REG_MASK( + UART_INT_CLR_REG(HW::UART) + , (UART_RXFIFO_FULL_INT_CLR | UART_RXFIFO_TOUT_INT_CLR)); + SET_PERI_REG_MASK( + UART_INT_ENA_REG(HW::UART) + , (UART_RXFIFO_FULL_INT_ENA | UART_RXFIFO_TOUT_INT_ENA)); + + // enable the RailCom detector + HW::RC_ENABLE::set(true); + + // set our phase and start the timer + railcomPhase_ = RailComPhase::CUTOUT_PHASE1; + start_timer(HW::RAILCOM_MAX_READ_DELAY_CH_1); + } + + void middle_cutout() override + { + // NO OP, handled in ISR + } + + void end_cutout() override + { + // disable the UART RX interrupts + CLEAR_PERI_REG_MASK( + UART_INT_ENA_REG(HW::UART) + , (UART_RXFIFO_FULL_INT_ENA | UART_RXFIFO_TOUT_INT_ENA)); + + // disable the RailCom detector + HW::RC_ENABLE::set(false); + + // ets_delay_us(0); + } + + void set_feedback_key(uint32_t key) override + { + if (railComFeedback_) + { + // send the feedback to the hub + railComHubFlow_->send(railComFeedback_); + } + // allocate a feedback packet + railComFeedback_ = railComHubFlow_->alloc(); + if (railComFeedback_) + { + railComFeedback_->data()->reset(key); + railcomFeedbackKey_ = key; + } + } + + void timer_tick() + { + // clear the interrupt status register for our timer + HW::TIMER_BASE->int_clr_timers.val = BIT(HW::TIMER_IDX); + + if (railcomPhase_ == RailComPhase::CUTOUT_PHASE1) + { + middle_cutout(); + + railcomPhase_ = RailComPhase::CUTOUT_PHASE2; + start_timer(HW::RAILCOM_MAX_READ_DELAY_CH_2); + } + else if (railcomPhase_ == RailComPhase::CUTOUT_PHASE2) + { + end_cutout(); + railcomPhase_ = RailComPhase::PRE_CUTOUT; + enable_output(); + } + } + + typedef enum : uint8_t + { + PRE_CUTOUT, + CUTOUT_PHASE1, + CUTOUT_PHASE2 + } RailComPhase; + + RailComPhase railcom_phase() + { + return railcomPhase_; + } + + Buffer *buf() + { + return railComFeedback_; + } + +private: + + void configure_timer(bool reload, uint16_t divider, bool enable, bool count_up, uint64_t alarm, bool alarm_en) + { + // make sure the ISR is disabled and that the status is cleared before + // reconfiguring the timer. + HW::TIMER_BASE->int_ena.val &= (~BIT(HW::TIMER_IDX)); + HW::TIMER_BASE->int_clr_timers.val = BIT(HW::TIMER_IDX); + + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].config.autoreload = reload; + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].config.divider = divider; + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].config.enable = enable; + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].config.increase = count_up; + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].config.level_int_en = 1; + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].config.edge_int_en = 0; + start_timer(alarm, true, false); + + // enable the ISR now that the timer has been configured + HW::TIMER_BASE->int_ena.val |= BIT(HW::TIMER_IDX); + } + + void start_timer(uint32_t usec, bool enable_alarm = true, bool enable_timer = true) + { + // disable the timer since we will reconfigure it + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].config.enable = 0; + + // reload the timer with a default count of zero + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].load_high = 0UL; + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].load_low = 0UL; + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].reload = 1; + + // set the next alarm period + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].alarm_high = 0; + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].alarm_low = usec; + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].config.alarm_en = enable_alarm; + + // start the timer + HW::TIMER_BASE->hw_timer[HW::TIMER_IDX].config.enable = enable_timer; + } + + void reset_uart_fifo() + { + while(HW::UART_BASE->status.rxfifo_cnt != 0 + || (HW::UART_BASE->mem_rx_status.wr_addr != + HW::UART_BASE->mem_rx_status.rd_addr)) + { + (void)HW::UART_BASE->fifo.rw_byte; + } + } + + uintptr_t railcomFeedbackKey_{0}; + dcc::RailcomHubFlow *railComHubFlow_; + Buffer *railComFeedback_{nullptr}; + RailComPhase railcomPhase_{RailComPhase::PRE_CUTOUT}; + bool enabled_{false}; +}; + +template +static void esp32_railcom_timer_tick(void *param) +{ + Esp32RailComDriver *driver = + reinterpret_cast *>(param); + driver->timer_tick(); +} + +template +static void esp32_railcom_uart_isr(void *param) +{ + Esp32RailComDriver *driver = + reinterpret_cast *>(param); + Buffer *fb = driver->buf(); + + if (HW::UART_BASE->int_st.rxfifo_full // RX fifo is full + || HW::UART_BASE->int_st.rxfifo_tout) // RX data available + { + uint8_t rx_fifo_len = HW::UART_BASE->status.rxfifo_cnt; + if (driver->railcom_phase() == + Esp32RailComDriver::RailComPhase::CUTOUT_PHASE1) + { + // this will flush the uart and process only the first two bytes + for (uint8_t idx = 0; idx < rx_fifo_len; idx++) + { + fb->data()->add_ch1_data(HW::UART_BASE->fifo.rw_byte); + } + } + else if (driver->railcom_phase() == + Esp32RailComDriver::RailComPhase::CUTOUT_PHASE2) + { + // this will flush the uart and process only the first six bytes + for (uint8_t idx = 0; idx < rx_fifo_len; idx++) + { + fb->data()->add_ch2_data(HW::UART_BASE->fifo.rw_byte); + } + } + + // clear interrupt status + HW::UART_BASE->int_clr.val = (UART_RXFIFO_FULL_INT_CLR + | UART_RXFIFO_TOUT_INT_CLR); + } + else + { + ets_printf("unexpected UART status %04x\n", HW::UART_BASE->int_st.val); + } +} + +} // namespace esp32cs + +#endif // ESP32_RAILCOM_DRIVER_H_ \ No newline at end of file diff --git a/components/DCCSignalGenerator/private_include/HBridgeThermalMonitor.h b/components/DCCSignalGenerator/private_include/HBridgeThermalMonitor.h new file mode 100644 index 00000000..7785a925 --- /dev/null +++ b/components/DCCSignalGenerator/private_include/HBridgeThermalMonitor.h @@ -0,0 +1,86 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef H_BRIDGE_THERMAL_MONITOR_H_ +#define H_BRIDGE_THERMAL_MONITOR_H_ + +#include "TrackOutputDescriptor.h" + +#include +#include +#include +#include +#include +#include + +namespace esp32cs +{ + +class HBridgeThermalMonitor : public DefaultConfigUpdateListener +{ +public: + using ProducerClass = + openlcb::PolledProducer; + + HBridgeThermalMonitor(openlcb::Node *node + , const TrackOutputConfig &cfg + , const Gpio *gpio) + : producer_(QuiesceDebouncer::Options(2) + , node, 0, 0, gpio) + , cfg_(cfg) + { + } + + UpdateAction apply_configuration(int fd, bool initial_load, + BarrierNotifiable *done) override + { + AutoNotify n(done); + UpdateAction res = UPDATED; + uint8_t thermal_debounce = cfg_.thermal_debounce().read(fd); + openlcb::EventId thermal_shutdown = cfg_.event_thermal_shutdown().read(fd); + openlcb::EventId thermal_shutdown_cleared = cfg_.event_thermal_shutdown_cleared().read(fd); + if (thermal_shutdown_cleared != producer_.event_off() || thermal_shutdown != producer_.event_on()) + { + auto saved_gpio = producer_.gpio_; + auto saved_node = producer_.node(); + producer_.ProducerClass::~ProducerClass(); + new (&producer_) ProducerClass( + QuiesceDebouncer::Options(thermal_debounce), saved_node, + thermal_shutdown, thermal_shutdown_cleared, saved_gpio); + res = REINIT_NEEDED; + } + return res; + } + + void factory_reset(int fd) override + { + CDI_FACTORY_RESET(cfg_.thermal_debounce); + } + + openlcb::Polling *polling() + { + return &producer_; + } + +private: + ProducerClass producer_; + const TrackOutputConfig cfg_; +}; + +} // namespace esp32cs + +#endif // H_BRIDGE_THERMAL_MONITOR_H_ \ No newline at end of file diff --git a/components/DCCSignalGenerator/private_include/MonitoredHBridge.h b/components/DCCSignalGenerator/private_include/MonitoredHBridge.h new file mode 100644 index 00000000..add47900 --- /dev/null +++ b/components/DCCSignalGenerator/private_include/MonitoredHBridge.h @@ -0,0 +1,191 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2018-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef MONITORED_H_BRIDGE_ +#define MONITORED_H_BRIDGE_ + +#include "TrackOutputDescriptor.h" +#include "sdkconfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace esp32cs +{ + +class HBridgeShortDetector : public DefaultConfigUpdateListener + , public openlcb::Polling +{ +public: + HBridgeShortDetector(openlcb::Node *node + , const adc1_channel_t senseChannel + , const Gpio *enablePin + , const uint32_t limitMilliAmps + , const uint32_t maxMilliAmps + , const string &name + , const string &bridgeType + , const esp32cs::TrackOutputConfig &cfg); + + HBridgeShortDetector(openlcb::Node *node + , const adc1_channel_t senseChannel + , const Gpio *enablePin + , const uint32_t + , const std::string &name + , const std::string &bridgeType + , const esp32cs::TrackOutputConfig &cfg); + + enum STATE : uint8_t + { + STATE_OVERCURRENT = BIT(0) + , STATE_SHUTDOWN = BIT(1) + , STATE_ON = BIT(2) + , STATE_OFF = BIT(3) + }; + + std::string getName() + { + return name_; + } + + uint32_t getMaxMilliAmps() + { + return maxMilliAmps_; + } + + uint32_t getLastReading() + { + return lastReading_; + } + + bool isProgrammingTrack() + { + return isProgTrack_; + } + + bool isEnabled() + { + return state_ != STATE_OFF; + } + + float getUsage() + { + if (state_ != STATE_OFF) + { + return ((lastReading_ * maxMilliAmps_) / 4096.0f); + } + return 0.0f; + } + + std::string getState(); + + std::string getStateAsJson(); + + std::string getStatusData(); + + std::string get_state_for_dccpp(); + + UpdateAction apply_configuration(int fd, bool initial_load, BarrierNotifiable *done) override + { + AutoNotify n(done); + UpdateAction res = initial_load ? REINIT_NEEDED : UPDATED; + openlcb::EventId short_detected = cfg_.event_short().read(fd); + openlcb::EventId short_cleared = cfg_.event_short_cleared().read(fd); + openlcb::EventId shutdown = cfg_.event_shutdown().read(fd); + openlcb::EventId shutdown_cleared = cfg_.event_shutdown_cleared().read(fd); + + auto saved_node = shortBit_.node(); + if (short_detected != shortBit_.event_on() || + short_cleared != shortBit_.event_off()) + { + shortBit_.openlcb::MemoryBit::~MemoryBit(); + new (&shortBit_)openlcb::MemoryBit(saved_node, short_detected, short_cleared, &state_, STATE_OVERCURRENT); + shortProducer_.openlcb::BitEventProducer::~BitEventProducer(); + new (&shortProducer_)openlcb::BitEventProducer(&shortBit_); + res = REINIT_NEEDED; + } + + if (shutdown != shortBit_.event_on() || + shutdown_cleared != shortBit_.event_off()) + { + saved_node = shutdownBit_.node(); + shutdownBit_.openlcb::MemoryBit::~MemoryBit(); + new (&shutdownBit_)openlcb::MemoryBit(saved_node, shutdown, shutdown_cleared, &state_, STATE_SHUTDOWN); + shutdownProducer_.openlcb::BitEventProducer::~BitEventProducer(); + new (&shutdownProducer_)openlcb::BitEventProducer(&shutdownBit_); + res = REINIT_NEEDED; + } + return res; + } + + void factory_reset(int fd) override + { + LOG(INFO + , "[LCC] MonitoredHBridge(%s) factory_reset(%d) invoked, defaulting " + "configuration", name_.c_str(), fd); + cfg_.description().write(fd, StringPrintf("%s Track", name_.c_str())); + } + + void poll_33hz(openlcb::WriteHelper *helper, Notifiable *done) override; + + void enable_prog_response(bool enable) + { + progEnable_ = enable; + } + +private: + const adc1_channel_t channel_; + const Gpio *enablePin_; + const Gpio *thermalWarningPin_; + const uint32_t maxMilliAmps_; + const std::string name_; + const std::string bridgeType_; + const uint32_t overCurrentLimit_; + const uint32_t shutdownLimit_; + const bool isProgTrack_; + const uint32_t progAckLimit_; + const esp32cs::TrackOutputConfig cfg_; + const uint8_t targetLED_; + const uint8_t adcSampleCount_{CONFIG_ADC_AVERAGE_READING_COUNT}; + const uint8_t overCurrentRetryCount_{CONFIG_DCC_HBRIDGE_OVERCURRENT_BEFORE_SHUTDOWN}; + const uint64_t currentReportInterval_{SEC_TO_USEC(CONFIG_DCC_HBRIDGE_USAGE_REPORT_INTERVAL)}; + uint32_t warnLimit_{0}; + openlcb::MemoryBit shortBit_; + openlcb::MemoryBit shutdownBit_; + openlcb::BitEventProducer shortProducer_; + openlcb::BitEventProducer shutdownProducer_; + uint64_t lastReport_{0}; + uint32_t lastReading_{0}; + uint8_t state_{STATE_OFF}; + uint8_t overCurrentCheckCount_{0}; + bool progEnable_{false}; + + void configure(); +}; + +} // namespace esp32cs +#endif // MONITORED_H_BRIDGE_ \ No newline at end of file diff --git a/components/DCCSignalGenerator/private_include/RMTTrackDevice.h b/components/DCCSignalGenerator/private_include/RMTTrackDevice.h new file mode 100644 index 00000000..129b7fa3 --- /dev/null +++ b/components/DCCSignalGenerator/private_include/RMTTrackDevice.h @@ -0,0 +1,98 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef _RMT_TRACK_DEVICE_H_ +#define _RMT_TRACK_DEVICE_H_ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "can_ioctl.h" +#include "MonitoredHBridge.h" +#include "sdkconfig.h" + +namespace esp32cs +{ + +class RMTTrackDevice : public dcc::PacketFlowInterface +{ +public: + RMTTrackDevice(const char *name, const rmt_channel_t channel + , const uint8_t dccPreambleBitCount, size_t packet_queue_len + , gpio_num_t pin, RailcomDriver *railcomDriver); + + // VFS interface helper + ssize_t write(int, const void *, size_t); + + // VFS interface helper + int ioctl(int, int, va_list); + + // RMT callback for transmit completion. This will be called via the ISR + // context but not from an IRAM restricted context. + void rmt_transmit_complete(); + + // Used only for DCCProgrammer OPS track requests, TBD if this can be removed. + void send(Buffer *, unsigned); + + const char *name() const + { + return name_; + } + +private: + // maximum number of RMT memory blocks (256 bytes each, 4 bytes per data bit) + // this will result in a max payload of 192 bits which is larger than any + // known DCC packet with the addition of up to 50 preamble bits. + static constexpr uint8_t MAX_RMT_MEMORY_BLOCKS = 3; + + // maximum number of bits that can be transmitted as one packet. + static constexpr uint8_t MAX_RMT_BITS = (RMT_MEM_ITEM_NUM * MAX_RMT_MEMORY_BLOCKS); + + const char *name_; + const rmt_channel_t channel_; + const uint8_t dccPreambleBitCount_; + RailcomDriver *railcomDriver_; + Atomic packetQueueLock_; + DeviceBuffer *packetQueue_; + Notifiable* notifiable_{nullptr}; + int8_t pktRepeatCount_{0}; + uint32_t pktLength_{0}; + rmt_item32_t packet_[MAX_RMT_BITS]; + + void encode_next_packet(); + + DISALLOW_COPY_AND_ASSIGN(RMTTrackDevice); +}; + +} // namespace esp32cs + +#endif // _RMT_TRACK_DEVICE_H_ diff --git a/components/DCCSignalGenerator/private_include/TrackPowerBitInterface.h b/components/DCCSignalGenerator/private_include/TrackPowerBitInterface.h new file mode 100644 index 00000000..2c3df94b --- /dev/null +++ b/components/DCCSignalGenerator/private_include/TrackPowerBitInterface.h @@ -0,0 +1,62 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef TRACK_POWER_BIT_INTERFACE_H_ +#define TRACK_POWER_BIT_INTERFACE_H_ + +#include +#include +#include + +namespace esp32cs +{ + +class TrackPowerBit : public openlcb::BitEventInterface +{ +public: + TrackPowerBit(openlcb::Node *node, const Gpio *gpio) + : openlcb::BitEventInterface(openlcb::Defs::CLEAR_EMERGENCY_OFF_EVENT + , openlcb::Defs::EMERGENCY_OFF_EVENT) + , node_(node) + , gpio_(gpio) + { + } + + openlcb::EventState get_current_state() override + { + if (gpio_->is_set()) + { + return openlcb::EventState::VALID; + } + return openlcb::EventState::INVALID; + } + + void set_state(bool new_value) override; + + openlcb::Node *node() + { + return node_; + } + +private: + openlcb::Node *node_; + const Gpio *gpio_; +}; + +} // namespace esp32cs + +#endif // TRACK_POWER_BIT_INTERFACE_H_ \ No newline at end of file diff --git a/components/DCCSignalGenerator/private_include/can_ioctl.h b/components/DCCSignalGenerator/private_include/can_ioctl.h new file mode 100644 index 00000000..9082c6dd --- /dev/null +++ b/components/DCCSignalGenerator/private_include/can_ioctl.h @@ -0,0 +1,87 @@ +/** \copyright + * Copyright (c) 2013, Stuart W Baker + * All rights reserved. + * + * 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. + * + * 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. + * + * \file can_ioctl.h + * This file implements can specific ioctl() keys. + * + * @author Stuart W. Baker + * @date 2 November 2013 + */ + +#ifndef _FREERTOS_CAN_IOCTL_H_ +#define _FREERTOS_CAN_IOCTL_H_ + +#include +#include "stropts.h" + +#if defined (__cplusplus) +extern "C" { +#endif + +/** Magic number for this driver's ioctl calls */ +#define CAN_IOC_MAGIC ('c') + +/// ioctl minor type used for the read/write active notifiable integration. +#define NOTIFIABLE_TYPE 13 + +/** read active ioctl. Argument is a literal pointer to a Notifiable. */ +#define CAN_IOC_READ_ACTIVE IOW(CAN_IOC_MAGIC, 1, NOTIFIABLE_TYPE) + +/** write active ioctl. Argument is a literal pointer to a Notifiable. */ +#define CAN_IOC_WRITE_ACTIVE IOW(CAN_IOC_MAGIC, 2, NOTIFIABLE_TYPE) + +/** CAN state type */ +typedef uint32_t can_state_t; + +/** Read the CAN state */ +#define SIOCGCANSTATE IOR(CAN_IOC_MAGIC, 3, sizeof(can_state_t)) + +/** CAN bus active */ +#define CAN_STATE_ACTIVE 0 + +/** CAN bus error warning */ +#define CAN_STATE_BUS_WARNING 1 + +/** CAN bus error passive */ +#define CAN_STATE_BUS_PASSIVE 2 + +/** CAN bus off */ +#define CAN_STATE_BUS_OFF 3 + +/** CAN bus scanning baud rate (CANFD) */ +#define CAN_STATE_SCANNING_BAUDRATE 4 + +/** CAN bus stopped */ +#define CAN_STATE_STOPPED 5 + +/** CAN bus sleeping */ +#define CAN_STATE_SLEEPING 6 + +#if defined (__cplusplus) +} +#endif + +#endif /* _FREERTOS_CAN_IOCTL_H_ */ diff --git a/components/DCCSignalGenerator/private_include/stropts.h b/components/DCCSignalGenerator/private_include/stropts.h new file mode 100644 index 00000000..b0df5ab7 --- /dev/null +++ b/components/DCCSignalGenerator/private_include/stropts.h @@ -0,0 +1,128 @@ +/** \copyright + * Copyright (c) 2013, Stuart W Baker + * All rights reserved. + * + * 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. + * + * 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. + * + * \file stropts.h + * This file implements ioctl() prototypes and defines. + * + * @author Stuart W. Baker + * @date 2 November 2013 + */ + +#ifndef _stropts_h_ +#define _stropts_h_ + +#if defined (__cplusplus) +extern "C" { +#endif + +#if defined(ESP32) +#include +#else +/** Request and ioctl transaction + * @param fd file descriptor + * @param key ioctl key + * @param ... key data (as a pointer or unsigned long type) + */ +int ioctl(int fd, unsigned long int key, ...); +#endif // ESP32 + +/** ioctl key value for operation (not read or write) */ +#define IOC_NONE 0U + +/** ioctl key write bit */ +#define IOC_WRITE 1U + +/** ioctl key read bit */ +#define IOC_READ 2U + +/** create an ioctl. + * @param _dir direction + * @param _type device driver unique number + * @param _num ioctl index number + * @param _size size of ioctl data in bytes + */ +#define IOC(_dir, _type, _num, _size) \ + (((_dir) << 30) | ((_type) << 8) | ((_num)) | ((_size) << 16)) + +/** create an operation ioctl + * @param _type device driver unique number + * @param _num ioctl index number + */ +#define IO(_type, _num) IOC(IOC_NONE, (_type), (_num), 0) + +/** create an operation ioctl + * @param _type device driver unique number + * @param _num ioctl index number + * @param _size size of ioctl data in bytes + */ +#define IOR(_type, _num, _size) IOC(IOC_READ, (_type), (_num), (_size)) + +/** create an operation ioctl + * @param _type device driver unique number + * @param _num ioctl index number + * @param _size size of ioctl data in bytes + */ +#define IOW(_type, _num, _size) IOC(IOC_WRITE, (_type), (_num), (_size)) + +/** create an operation ioctl + * @param _type device driver unique number + * @param _num ioctl index number + * @param _size size of ioctl data in bytes + */ +#define IOWR(_type, _num, _size) IOC(IOC_WRITE | IOC_READ, (_type), (_num), (_size)) + +/** Decode ioctl number direction. + * @param _num encoded ioctl value + * @return IOC_NONE, IOC_WRITE, IOC_READ, or IOC_WRITE | IOC_READ + */ +#define IOC_DIR(_num) (((_num) >> 30) & 0x00000003) + +/** Decode ioctl type. + * @param _num encoded ioctl value + * @return device driver unique number + */ +#define IOC_TYPE(_num) (((_num) >> 8) & 0x000000FF) + +/** Decode ioctl number. + * @param _num encoded ioctl value + * @return ioctl index number + */ +#define IOC_NR(_num) (((_num) >> 0) & 0x000000FF) + +/** Decode ioctl size. + * @param _num encoded ioctl value + * @return size of ioctl data in bytes + */ +#define IOC_SIZE(_num) (((_num) >> 16) & 0x00003FFF) + +/** Number of bits that make up the size field */ +#define IOC_SIZEBITS 14 + +#if defined (__cplusplus) +} +#endif + +#endif /* _stropts_h_ */ diff --git a/components/DCCTurnoutManager/CMakeLists.txt b/components/DCCTurnoutManager/CMakeLists.txt new file mode 100644 index 00000000..c9db751c --- /dev/null +++ b/components/DCCTurnoutManager/CMakeLists.txt @@ -0,0 +1,16 @@ +set(COMPONENT_SRCS + "Turnouts.cpp" +) + +set(COMPONENT_ADD_INCLUDEDIRS "include" ) + +set(COMPONENT_REQUIRES + "OpenMRNLite" + "DCCppProtocol" + "nlohmann_json" + "Configuration" +) + +register_component() + +set_source_files_properties(Turnouts.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) \ No newline at end of file diff --git a/components/DCCTurnoutManager/Kconfig.projbuild b/components/DCCTurnoutManager/Kconfig.projbuild new file mode 100644 index 00000000..cb569859 --- /dev/null +++ b/components/DCCTurnoutManager/Kconfig.projbuild @@ -0,0 +1,32 @@ + +# Log level constants from from components/OpenMRNLite/src/utils/logging.h +# +# ALWAYS : -1 +# FATAL : 0 +# LEVEL_ERROR : 1 +# WARNING : 2 +# INFO : 3 +# VERBOSE : 4 +# +# Note that FATAL will cause the MCU to reboot! + +menu "DCC Turnout" + + config TURNOUT_PERSISTENCE_INTERVAL_SEC + int "Number of seconds between automatic persistence of turnout list" + default 30 + + choice TURNOUT_LOGGING + bool "Turnout Manager logging" + default TURNOUT_LOGGING_MINIMAL + config TURNOUT_LOGGING_VERBOSE + bool "Verbose" + config TURNOUT_LOGGING_MINIMAL + bool "Minimal" + endchoice + config TURNOUT_LOG_LEVEL + int + default 4 if TURNOUT_LOGGING_MINIMAL + default 3 if TURNOUT_LOGGING_VERBOSE + default 5 +endmenu \ No newline at end of file diff --git a/components/DCCTurnoutManager/Turnouts.cpp b/components/DCCTurnoutManager/Turnouts.cpp new file mode 100644 index 00000000..cf95f39d --- /dev/null +++ b/components/DCCTurnoutManager/Turnouts.cpp @@ -0,0 +1,349 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2017-2019 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "Turnouts.h" + +#include +#include +#include +#include +#include + +using nlohmann::json; + +std::unique_ptr turnoutManager; + +static constexpr const char * TURNOUTS_JSON_FILE = "turnouts.json"; + +static constexpr const char *TURNOUT_TYPE_STRINGS[] = +{ + "LEFT", + "RIGHT", + "WYE", + "MULTI" +}; + +TurnoutManager::TurnoutManager(openlcb::Node *node, Service *service) + : turnoutEventConsumer_(node, this) + , persistFlow_(service, SEC_TO_NSEC(CONFIG_TURNOUT_PERSISTENCE_INTERVAL_SEC) + , std::bind(&TurnoutManager::persist, this)) + , dirty_(false) +{ + OSMutexLock h(&mux_); + LOG(INFO, "[Turnout] Initializing DCC Turnout database"); + json root = json::parse( + Singleton::instance()->load(TURNOUTS_JSON_FILE)); + for (auto turnout : root) + { + turnouts_.push_back( + std::make_unique(turnout[JSON_ADDRESS_NODE].get() + , turnout[JSON_STATE_NODE].get() + , (TurnoutType)turnout[JSON_TYPE_NODE].get())); + } + LOG(INFO, "[Turnout] Loaded %d DCC turnout(s)", turnouts_.size()); +} + +void TurnoutManager::clear() +{ + OSMutexLock h(&mux_); + for (auto & turnout : turnouts_) + { + turnout.reset(nullptr); + } + turnouts_.clear(); + dirty_ = true; +} + +#define FIND_TURNOUT(address) \ + std::find_if(turnouts_.begin(), turnouts_.end(), \ + [address](std::unique_ptr & turnout) -> bool \ + { \ + return (turnout->getAddress() == address); \ + } \ + ) + +string TurnoutManager::set(uint16_t address, bool thrown, bool sendDCC) +{ + OSMutexLock h(&mux_); + auto const &elem = FIND_TURNOUT(address); + if (elem != turnouts_.end()) + { + elem->get()->set(thrown, sendDCC); + return StringPrintf("" + , std::distance(turnouts_.begin(), elem) + 1 + , elem->get()->isThrown()); + } + + // we didn't find it, create it and set it + turnouts_.push_back(std::make_unique(turnouts_.size() + 1 + , address)); + dirty_ = true; + return set(address, thrown, sendDCC); +} + +string TurnoutManager::toggle(uint16_t address) +{ + OSMutexLock h(&mux_); + auto const &elem = FIND_TURNOUT(address); + if (elem != turnouts_.end()) + { + elem->get()->toggle(); + return StringPrintf("" + , std::distance(turnouts_.begin(), elem) + , elem->get()->isThrown()); + } + + // we didn't find it, create it and throw it + turnouts_.push_back(std::make_unique(address, -1)); + dirty_ = true; + return toggle(address); +} + +string TurnoutManager::getStateAsJson(bool readable) +{ + OSMutexLock h(&mux_); + return get_state_as_json(readable); +} + +string TurnoutManager::get_state_for_dccpp() +{ + OSMutexLock h(&mux_); + if (turnouts_.empty()) + { + return COMMAND_FAILED_RESPONSE; + } + string status; + uint16_t index = 0; + for (auto& turnout : turnouts_) + { + uint16_t board; + int8_t port; + encodeDCCAccessoryAddress(&board, &port, turnout->getAddress()); + status += StringPrintf("", index++, board, port + , turnout->isThrown()); + } + return status; +} + +Turnout *TurnoutManager::createOrUpdate(const uint16_t address + , const TurnoutType type) +{ + OSMutexLock h(&mux_); + auto const &elem = FIND_TURNOUT(address); + if (elem != turnouts_.end()) + { + elem->get()->update(address, type); + return elem->get(); + } + // we didn't find it, create it! + turnouts_.push_back(std::make_unique(address, false, type)); + dirty_ = true; + return turnouts_.back().get(); +} + +bool TurnoutManager::remove(const uint16_t address) +{ + OSMutexLock h(&mux_); + auto const &elem = FIND_TURNOUT(address); + if (elem != turnouts_.end()) + { + LOG(CONFIG_TURNOUT_LOG_LEVEL, "[Turnout %d] Deleted", address); + turnouts_.erase(elem); + dirty_ = true; + return true; + } + LOG(WARNING, "[Turnout %d] not found", address); + return false; +} + +Turnout *TurnoutManager::getByIndex(const uint16_t index) +{ + OSMutexLock h(&mux_); + if (index < turnouts_.size()) + { + return turnouts_[index].get(); + } + LOG(WARNING, "[Turnout] index %d not found (max: %d)", index + , turnouts_.size()); + return nullptr; +} + +Turnout *TurnoutManager::get(const uint16_t address) +{ + OSMutexLock h(&mux_); + auto const &elem = FIND_TURNOUT(address); + if (elem != turnouts_.end()) + { + return elem->get(); + } + LOG(WARNING, "[Turnout %d] not found", address); + return nullptr; +} + +uint16_t TurnoutManager::count() +{ + OSMutexLock h(&mux_); + return turnouts_.size(); +} + +// TODO: shift this to consume the LCC event directly +void TurnoutManager::send(Buffer *b, unsigned prio) +{ + // add ref count so send doesn't delete it + dcc::Packet *pkt = b->data(); + // Verify that the packet looks like a DCC Accessory decoder packet + if(!pkt->packet_header.is_marklin && + pkt->dlc == 2 && + pkt->payload[0] & 0x80 && + pkt->payload[1] & 0x80) + { + // packet data format: + // payload[0] payload[1] + // 10aaaaaa 1AAACDDD + // ^ ^^^^^^ ^^^^^^^^ + // | | || || | + // | | || || \-state bit + // | | || |\-output index + // | | || \-activate/deactivate output flag (ignored) + // | | |\-board address most significant three bits + // | | | stored in 1s complement (1=0, 0=1) + // | | \-accessory packet flag + // | \-board address (least significant six bits) + // \-accessory packet flag + // converting back to a single address using the following: AAAaaaaaaDDD + // note that only the output index is used in calculation of the final + // address since only the base address is stored in the CS. + uint16_t boardAddress = ((~pkt->payload[1] & 0b01110000) << 2) | + (pkt->payload[0] & 0b00111111); + uint8_t boardIndex = (pkt->payload[1] & 0b00000110) >> 1; + // least significant bit of the second byte is thrown/closed indicator. + bool state = pkt->payload[1] & 0b00000001; + // Set the turnout to the requested state, don't send a DCC packet. + set(decodeDCCAccessoryAddress(boardAddress, boardIndex), state); + } + b->unref(); +} + +string TurnoutManager::get_state_as_json(bool readableStrings) +{ + string content = "["; + for (const auto& turnout : turnouts_) + { + // only add the seperator if we have already serialized at least one + // turnout. + if (content.length() > 1) + { + content += ","; + } + content += turnout->toJson(readableStrings); + } + content += "]"; + return content; +} + +void TurnoutManager::persist() +{ + // Check if we have any changes to persist, if not exit early to reduce + // unnecessary wear on the flash when running on SPIFFS. + OSMutexLock h(&mux_); + if (!dirty_) + { + return; + } + Singleton::instance()->store(TURNOUTS_JSON_FILE + , get_state_as_json(false)); + dirty_ = false; +} + +void encodeDCCAccessoryAddress(uint16_t *board, int8_t *port + , uint16_t address) +{ + // DCC address starts at 1, board address is 0-511 and index is 0-3. + *board = ((address - 1) / 4) + 1; + *port = (address - 1) % 4; +} + +uint16_t decodeDCCAccessoryAddress(uint16_t board, int8_t port) +{ + if (board == 0) + { + return 0; + } + uint32_t addr = ((board - 1) << 2); + addr += port + 1; + return (uint16_t)(addr & 0xFFFF); +} + +Turnout::Turnout(uint16_t address, bool thrown, TurnoutType type) + : _address(address), _thrown(thrown), _type(type) +{ + LOG(INFO, "[Turnout %d] Registered as type %s and initial state of %s" + , _address, TURNOUT_TYPE_STRINGS[_type] + , _thrown ? JSON_VALUE_THROWN : JSON_VALUE_CLOSED); +} + +void Turnout::update(uint16_t address, TurnoutType type) +{ + _address = address; + _type = type; + LOG(CONFIG_TURNOUT_LOG_LEVEL, "[Turnout %d] Updated type %s", _address + , TURNOUT_TYPE_STRINGS[_type]); +} + +string Turnout::toJson(bool readableStrings) +{ + string serialized = StringPrintf("{\"%s\":%d,\"%s\":%d,\"%s\":" + , JSON_ADDRESS_NODE, _address, JSON_TYPE_NODE, _type, JSON_STATE_NODE); + if (readableStrings) + { + serialized += StringPrintf("\"%s\"", _thrown ? JSON_VALUE_THROWN + : JSON_VALUE_CLOSED); + } + else + { + serialized += integer_to_string(_thrown); + } + serialized += "}"; + return serialized; +} + +void Turnout::set(bool thrown, bool sendDCCPacket) +{ + _thrown = thrown; + if (sendDCCPacket) + { + packet_processor_add_refresh_source(this); + } + LOG(CONFIG_TURNOUT_LOG_LEVEL, "[Turnout %d] Set to %s", _address + , _thrown ? JSON_VALUE_THROWN : JSON_VALUE_CLOSED); +} + +void Turnout::get_next_packet(unsigned code, dcc::Packet* packet) +{ + // shift address by one to account for the output pair state bit (thrown). + uint16_t addr = ((_address << 1) | _thrown); + // always send activate as true (sets C to 1) + packet->add_dcc_basic_accessory(addr, true); + +//#ifdef CONFIG_TURNOUT_LOGGING_VERBOSE + LOG(INFO, "[Turnout %d] Packet: %s", _address + , packet_to_string(*packet, true).c_str()); +//#endif + + // remove ourselves as turnouts are single fire sources + packet_processor_remove_refresh_source(this); +} \ No newline at end of file diff --git a/components/DCCTurnoutManager/include/Turnouts.h b/components/DCCTurnoutManager/include/Turnouts.h new file mode 100644 index 00000000..8e5394ef --- /dev/null +++ b/components/DCCTurnoutManager/include/Turnouts.h @@ -0,0 +1,105 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2017-2019 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef TURNOUTS_H_ +#define TURNOUTS_H_ + +#include +#include +#include +#include +#include +#include + +enum TurnoutType +{ + LEFT=0, + RIGHT, + WYE, + MULTI, + MAX_TURNOUT_TYPES // NOTE: this must be the last entry in the enum. +}; + +void encodeDCCAccessoryAddress(uint16_t *board, int8_t *port, uint16_t address); +uint16_t decodeDCCAccessoryAddress(uint16_t board, int8_t port); + +class Turnout : public dcc::NonTrainPacketSource +{ +public: + Turnout(uint16_t, bool=false, TurnoutType=TurnoutType::LEFT); + virtual ~Turnout() {} + void update(uint16_t, TurnoutType); + void set(bool=false, bool=true); + std::string toJson(bool=false); + uint16_t getAddress() + { + return _address; + } + bool isThrown() + { + return _thrown; + } + void toggle() + { + set(!_thrown); + } + TurnoutType getType() + { + return _type; + } + void setType(const TurnoutType type) + { + _type = type; + } + void get_next_packet(unsigned code, dcc::Packet* packet) override; +private: + uint16_t _address; + bool _thrown; + TurnoutType _type; +}; + +class TurnoutManager : public dcc::PacketFlowInterface + , public Singleton +{ +public: + TurnoutManager(openlcb::Node *, Service *); + void stop() + { + persistFlow_.stop(); + } + void clear(); + std::string set(uint16_t, bool=false, bool=true); + std::string toggle(uint16_t); + std::string getStateAsJson(bool=true); + std::string get_state_for_dccpp(); + Turnout *createOrUpdate(const uint16_t, const TurnoutType=TurnoutType::LEFT); + bool remove(const uint16_t); + Turnout *getByIndex(const uint16_t); + Turnout *get(const uint16_t); + uint16_t count(); + void send(Buffer *, unsigned); +private: + std::string get_state_as_json(bool); + void persist(); + std::vector> turnouts_; + openlcb::DccAccyConsumer turnoutEventConsumer_; + AutoPersistFlow persistFlow_; + bool dirty_; + OSMutex mux_; +}; + +#endif // TURNOUTS_H_ \ No newline at end of file diff --git a/components/DCCppProtocol/CMakeLists.txt b/components/DCCppProtocol/CMakeLists.txt new file mode 100644 index 00000000..1e6ce106 --- /dev/null +++ b/components/DCCppProtocol/CMakeLists.txt @@ -0,0 +1,20 @@ + +set(COMPONENT_SRCS + "DCCppProtocol.cpp" + "DCCProgrammer.cpp" +) + +set(COMPONENT_ADD_INCLUDEDIRS "include" ) + +set(COMPONENT_REQUIRES + "Configuration" + "DCCSignalGenerator" + "DCCTurnoutManager" + "Esp32HttpServer" + "GPIO" +) + +register_component() + +set_source_files_properties(DCCppProtocol.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(DCCProgrammer.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) \ No newline at end of file diff --git a/components/DCCppProtocol/DCCProgrammer.cpp b/components/DCCppProtocol/DCCProgrammer.cpp new file mode 100644 index 00000000..e28754da --- /dev/null +++ b/components/DCCppProtocol/DCCProgrammer.cpp @@ -0,0 +1,256 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2019 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "DCCProgrammer.h" + +#include +#include +#include +#include + +// number of attempts the programming track will make to read/write a CV +static constexpr uint8_t PROG_TRACK_CV_ATTEMPTS = 3; + +static bool enterServiceMode() +{ + BufferPtr req = + invoke_flow(Singleton::instance() + , ProgrammingTrackRequest::ENTER_SERVICE_MODE); + return req->data()->resultCode == 0; +} + +static void leaveServiceMode() +{ + invoke_flow(Singleton::instance() + , ProgrammingTrackRequest::EXIT_SERVICE_MODE); +} + +static bool sendServiceModeDecoderReset() +{ + BufferPtr req = + invoke_flow(Singleton::instance() + , ProgrammingTrackRequest::SEND_RESET, 15); + return (req->data()->resultCode == 0); +} + +static bool sendServiceModePacketWithAck(dcc::Packet pkt) +{ + BufferPtr req = + invoke_flow(Singleton::instance() + , ProgrammingTrackRequest::SEND_PROGRAMMING_PACKET, pkt + , 15); + return req->data()->hasAck_; +} + +static bool executeProgTrackWriteRequest(dcc::Packet pkt) +{ + if (enterServiceMode()) + { + LOG(VERBOSE, "[PROG] Resetting DCC Decoder"); + if (!sendServiceModeDecoderReset()) + { + leaveServiceMode(); + return false; + } + LOG(VERBOSE, "[PROG] Sending DCC packet: %s", dcc::packet_to_string(pkt).c_str()); + if (!sendServiceModePacketWithAck(pkt)) + { + leaveServiceMode(); + return false; + } + LOG(VERBOSE, "[PROG] Resetting DCC Decoder (after PROG)"); + if (!sendServiceModeDecoderReset()) + { + leaveServiceMode(); + return false; + } + leaveServiceMode(); + return true; + } + return false; +} + +int16_t readCV(const uint16_t cv) +{ + int16_t value = -1; + if (enterServiceMode()) + { + for(int attempt = 0; attempt < PROG_TRACK_CV_ATTEMPTS && value == -1; attempt++) { + LOG(INFO, "[PROG %d/%d] Attempting to read CV %d", attempt+1, PROG_TRACK_CV_ATTEMPTS, cv); + // reset cvValue to all bits OFF + value = 0; + for(uint8_t bit = 0; bit < 8; bit++) { + LOG(VERBOSE, "[PROG %d/%d] CV %d, bit [%d/7]", attempt+1, PROG_TRACK_CV_ATTEMPTS, cv, bit); + dcc::Packet pkt; + pkt.start_dcc_svc_packet(); + pkt.add_dcc_prog_command(0x78, cv, 0xE8 + bit); + if (sendServiceModePacketWithAck(pkt)) + { + LOG(VERBOSE, "[PROG %d/%d] CV %d, bit [%d/7] ON", attempt+1, PROG_TRACK_CV_ATTEMPTS, cv, bit); + value &= (1 << bit); + } else { + LOG(VERBOSE, "[PROG %d/%d] CV %d, bit [%d/7] OFF", attempt+1, PROG_TRACK_CV_ATTEMPTS, cv, bit); + } + } + dcc::Packet pkt; + pkt.set_dcc_svc_verify_byte(cv, value); + if (sendServiceModePacketWithAck(pkt)) + { + LOG(INFO, "[PROG %d/%d] CV %d, verified as %d", attempt+1, PROG_TRACK_CV_ATTEMPTS, cv, value); + } + else + { + LOG(WARNING, "[PROG %d/%d] CV %d, could not be verified", attempt+1, PROG_TRACK_CV_ATTEMPTS, cv); + value = -1; + } + } + LOG(INFO, "[PROG] CV %d value is %d", cv, value); + leaveServiceMode(); + } + else + { + LOG_ERROR("[PROG] Failed to enter programming mode!"); + } + return value; +} + +bool writeProgCVByte(const uint16_t cv, const uint8_t value) +{ + bool writeVerified = false; + dcc::Packet pkt, verifyPkt; + pkt.set_dcc_svc_write_byte(cv, value); + verifyPkt.set_dcc_svc_verify_byte(cv, value); + + for(uint8_t attempt = 1; + attempt <= PROG_TRACK_CV_ATTEMPTS && !writeVerified; + attempt++) + { + LOG(INFO, "[PROG %d/%d] Attempting to write CV %d as %d", attempt + , PROG_TRACK_CV_ATTEMPTS, cv, value); + + if (executeProgTrackWriteRequest(pkt) && + executeProgTrackWriteRequest(verifyPkt)) + { + // write byte and verify byte were successful + writeVerified = true; + } + + if (!writeVerified) + { + LOG(WARNING, "[PROG %d/%d] CV %d write value %d could not be verified." + , attempt, PROG_TRACK_CV_ATTEMPTS, cv, value); + } + else + { + LOG(INFO, "[PROG %d/%d] CV %d write value %d verified.", attempt + , PROG_TRACK_CV_ATTEMPTS, cv, value); + } + } + return writeVerified; +} + +bool writeProgCVBit(const uint16_t cv, const uint8_t bit, const bool value) +{ + bool writeVerified = false; + dcc::Packet pkt, verifyPkt; + pkt.set_dcc_svc_write_bit(cv, bit, value); + verifyPkt.set_dcc_svc_verify_bit(cv, bit, value); + + for(uint8_t attempt = 1; + attempt <= PROG_TRACK_CV_ATTEMPTS && !writeVerified; + attempt++) { + LOG(INFO, "[PROG %d/%d] Attempting to write CV %d bit %d as %d", attempt + , PROG_TRACK_CV_ATTEMPTS, cv, bit, value); + if (executeProgTrackWriteRequest(pkt) && + executeProgTrackWriteRequest(verifyPkt)) + { + // write byte and verify byte were successful + writeVerified = true; + } + + if (!writeVerified) + { + LOG(WARNING, "[PROG %d/%d] CV %d write bit %d could not be verified." + , attempt, PROG_TRACK_CV_ATTEMPTS, cv, bit); + } + else + { + LOG(INFO, "[PROG %d/%d] CV %d write bit %d verified.", attempt + , PROG_TRACK_CV_ATTEMPTS, cv, bit); + } + } + return writeVerified; +} + +void writeOpsCVByte(const uint16_t locoAddress, const uint16_t cv + , const uint8_t cvValue) +{ + auto track = Singleton::instance(); + auto b = get_buffer_deleter(track->alloc()); + if (b) + { + LOG(INFO, "[OPS] Updating CV %d to %d for loco %d", cv, cvValue + , locoAddress); + + b->data()->start_dcc_packet(); + if(locoAddress > 127) + { + b->data()->add_dcc_address(dcc::DccLongAddress(locoAddress)); + } + else + { + b->data()->add_dcc_address(dcc::DccShortAddress(locoAddress)); + } + b->data()->add_dcc_pom_write1(cv, cvValue); + b->data()->packet_header.rept_count = 3; + track->send(b.get()); + } + else + { + LOG_ERROR("[OPS] Failed to retrieve DCC Packet for programming request"); + } +} + +void writeOpsCVBit(const uint16_t locoAddress, const uint16_t cv + , const uint8_t bit, const bool value) +{ + auto track = Singleton::instance(); + auto b = get_buffer_deleter(track->alloc()); + if (b) + { + LOG(INFO, "[OPS] Updating CV %d bit %d to %d for loco %d", cv, bit, value + , locoAddress); + b->data()->start_dcc_packet(); + if(locoAddress > 127) + { + b->data()->add_dcc_address(dcc::DccLongAddress(locoAddress)); + } + else + { + b->data()->add_dcc_address(dcc::DccShortAddress(locoAddress)); + } + // TODO add_dcc_pom_write_bit(cv, bit, value) + b->data()->add_dcc_prog_command(0xe8, cv - 1 + , (uint8_t)(0xF0 + bit + value * 8)); + b->data()->packet_header.rept_count = 3; + track->send(b.get()); + } + else + { + LOG_ERROR("[OPS] Failed to retrieve DCC Packet for programming request"); + } +} diff --git a/components/DCCppProtocol/DCCppProtocol.cpp b/components/DCCppProtocol/DCCppProtocol.cpp new file mode 100644 index 00000000..a940efea --- /dev/null +++ b/components/DCCppProtocol/DCCppProtocol.cpp @@ -0,0 +1,806 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2017-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses + +The DCC++ protocol specification is +COPYRIGHT (c) 2013-2016 Gregg E. Berman +and has been adapter for use in ESP32 COMMAND STATION. + +**********************************************************************/ + +#include "DCCppProtocol.h" +#include "DCCProgrammer.h" + +#include "sdkconfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if CONFIG_GPIO_OUTPUTS +#include +#endif // CONFIG_GPIO_OUTPUTS +#if CONFIG_GPIO_SENSORS +#include +#include +#if CONFIG_GPIO_S88 +#include +#endif // CONFIG_GPIO_S88 +#endif // CONFIG_GPIO_SENSORS +#include + +std::vector> registeredCommands; + +// command handler, this command attempts +// to read a CV value from the PROGRAMMING track. The returned value will be +// the actual CV value or -1 when there is a failure reading or verifying the +// CV. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(ReadCVCommand, "R", 3) +DCC_PROTOCOL_COMMAND_HANDLER(ReadCVCommand, +[](const vector arguments) +{ + uint16_t cv = std::stoi(arguments[0]); + uint16_t callback = std::stoi(arguments[1]); + uint16_t callbackSub = std::stoi(arguments[2]); + int16_t value = readCV(cv); + return StringPrintf("", + callback, + callbackSub, + cv, + value); +}) + +// command handler, this command +// attempts to write a CV value on the PROGRAMMING track. The returned value +// is either the actual CV value written or -1 if there is a failure writing or +// verifying the CV value. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(WriteCVByteProgCommand, "W", 4) +DCC_PROTOCOL_COMMAND_HANDLER(WriteCVByteProgCommand, +[](const vector arguments) +{ + uint16_t cv = std::stoi(arguments[0]); + int16_t value = std::stoi(arguments[1]); + uint16_t callback = std::stoi(arguments[2]); + uint16_t callbackSub = std::stoi(arguments[3]); + if (!writeProgCVByte(cv, value)) + { + LOG_ERROR("[PROG] Failed to write CV %d as %d", cv, value); + value = -1; + } + return StringPrintf("", callback, callbackSub, cv, value); +}) + +// command handler, this +// command attempts to write a single bit value for a CV on the PROGRAMMING +// track. The returned value is either the actual bit value of the CV or -1 if +// there is a failure writing or verifying the CV value. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(WriteCVBitProgCommand, "B", 5) +DCC_PROTOCOL_COMMAND_HANDLER(WriteCVBitProgCommand, +[](const vector arguments) +{ + int cv = std::stoi(arguments[0]); + uint8_t bit = std::stoi(arguments[1]); + int8_t value = std::stoi(arguments[2]); + uint16_t callback = std::stoi(arguments[3]); + uint16_t callbackSub = std::stoi(arguments[4]); + if (!writeProgCVBit(cv, bit, value)) + { + LOG_ERROR("[PROG] Failed to write CV %d BIT %d as %d", cv, bit, value); + value = -1; + } + return StringPrintf("", callback, callbackSub, cv, bit + , value); +}) + +// command handler, this command sends a CV write packet +// on the MAIN OPERATIONS track for a given LOCO. No verification is attempted. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(WriteCVByteOpsCommand, "w", 3) +DCC_PROTOCOL_COMMAND_HANDLER(WriteCVByteOpsCommand, +[](const vector arguments) +{ + writeOpsCVByte(std::stoi(arguments[0]), std::stoi(arguments[1]) + , std::stoi(arguments[2])); + return COMMAND_SUCCESSFUL_RESPONSE; +}) + +// command handler, this command sends a CV bit +// write packet on the MAIN OPERATIONS track for a given LOCO. No verification +// is attempted. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(WriteCVBitOpsCommand, "b", 4) +DCC_PROTOCOL_COMMAND_HANDLER(WriteCVBitOpsCommand, +[](const vector arguments) +{ + writeOpsCVBit(std::stoi(arguments[0]), std::stoi(arguments[1]) + , std::stoi(arguments[2]), arguments[3][0] == '1'); + return COMMAND_SUCCESSFUL_RESPONSE; +}) + +string convert_loco_to_dccpp_state(openlcb::TrainImpl *impl, size_t id) +{ + dcc::SpeedType speed(impl->get_speed()); + if (speed.mph()) + { + return StringPrintf("", id, (int)speed.mph() + 1 + , speed.direction() == dcc::SpeedType::FORWARD); + } + return StringPrintf("", id + , speed.direction() == dcc::SpeedType::FORWARD); +} + +// command handler, this command sends the current free heap space as response. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(FreeHeapCommand, "F", 0) +DCC_PROTOCOL_COMMAND_HANDLER(FreeHeapCommand, +[](const vector arguments) +{ + return StringPrintf("", os_get_free_heap()); +}) + +// command handler, this command sends an estop packet to all active +// locomotives. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(EStopCommand, "estop", 0) +DCC_PROTOCOL_COMMAND_HANDLER(EStopCommand, +[](const vector arguments) +{ + esp32cs::initiate_estop(); + return COMMAND_SUCCESSFUL_RESPONSE; +}) + +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(CurrentDrawCommand, "c", 0) +DCC_PROTOCOL_COMMAND_HANDLER(CurrentDrawCommand, +[](const vector arguments) +{ + return esp32cs::get_track_state_for_dccpp(); +}) + +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(PowerOnCommand, "1", 0) +DCC_PROTOCOL_COMMAND_HANDLER(PowerOnCommand, +[](const vector arguments) +{ + esp32cs::enable_ops_track_output(); + // hardcoded response since enable/disable is deferred until the next + // check interval. + return StringPrintf("", CONFIG_OPS_TRACK_NAME); +}) + +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(PowerOffCommand, "0", 0) +DCC_PROTOCOL_COMMAND_HANDLER(PowerOffCommand, +[](const vector arguments) +{ + esp32cs::disable_track_outputs(); + // hardcoded response since enable/disable is deferred until the next + // check interval. + return StringPrintf("", CONFIG_OPS_TRACK_NAME); +}) + +#define GET_LOCO_VIA_EXECUTOR(NAME, address) \ + openlcb::TrainImpl *NAME = nullptr; \ + { \ + SyncNotifiable n; \ + Singleton::instance()->stack()->executor()->add( \ + new CallbackExecutable([&]() \ + { \ + NAME = Singleton::instance()->get_train_impl( \ + commandstation::DccMode::DCC_128, address); \ + n.notify(); \ + })); \ + n.wait_for_notification(); \ + } + +// command handler, this command +// converts the provided locomotive control command into a compatible DCC +// locomotive control packet. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(ThrottleCommandAdapter, "t", 4) +DCC_PROTOCOL_COMMAND_HANDLER(ThrottleCommandAdapter, +[](const vector arguments) +{ + int reg_num = std::stoi(arguments[0]); + uint16_t loco_addr = std::stoi(arguments[1]); + uint8_t req_speed = std::stoi(arguments[2]); + uint8_t req_dir = std::stoi(arguments[3]); + + GET_LOCO_VIA_EXECUTOR(impl, loco_addr); + LOG(INFO, "[DCC++ loco %d] Set speed to %d", loco_addr, req_speed); + LOG(INFO, "[DCC++ loco %d] Set direction to %s", loco_addr + , req_dir ? "FWD" : "REV"); + auto speed = dcc::SpeedType::from_mph(req_speed); + if (!req_dir) + { + speed.set_direction(dcc::SpeedType::REVERSE); + } + impl->set_speed(speed); + return convert_loco_to_dccpp_state(impl, reg_num); +}); + +// command handler, this command +// converts the provided locomotive control command into a compatible DCC +// locomotive control packet. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(ThrottleExCommandAdapter, "tex", 3) +DCC_PROTOCOL_COMMAND_HANDLER(ThrottleExCommandAdapter, +[](const vector arguments) +{ + uint16_t loco_addr = std::stoi(arguments[0]); + int8_t req_speed = std::stoi(arguments[1]); + int8_t req_dir = std::stoi(arguments[2]); + + GET_LOCO_VIA_EXECUTOR(impl, loco_addr); + + if (req_speed >= 0) + { + auto speed = dcc::SpeedType::from_mph(req_speed); + if (req_dir == 0) + { + speed.set_direction(dcc::SpeedType::REVERSE); + } + LOG(INFO, "[DCC++ loco %d] Set speed to %d (%s)", loco_addr, abs(req_speed) + , req_speed > 0 ? "FWD" : "REV"); + impl->set_speed(speed); + } + else if (req_dir >= 0) + { + dcc::SpeedType speed(impl->get_speed()); + LOG(INFO, "[DCC++ loco %d] Set direction to %s", loco_addr + , req_dir ? "FWD" : "REV"); + speed.set_direction(req_dir ? dcc::SpeedType::FORWARD : dcc::SpeedType::REVERSE); + impl->set_speed(speed); + } + return convert_loco_to_dccpp_state(impl, 0); +}) + +// command handler, this command converts a +// locomotive function update into a compatible DCC function control packet. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(FunctionCommandAdapter, "f", 2) +DCC_PROTOCOL_COMMAND_HANDLER(FunctionCommandAdapter, +[](const vector arguments) +{ + uint16_t loco_addr = std::stoi(arguments[0]); + uint8_t func_byte = std::stoi(arguments[1]); + uint8_t first{1}; + uint8_t last{4}; + uint8_t bits{func_byte}; + + GET_LOCO_VIA_EXECUTOR(impl, loco_addr); + + // check this is a request for functions F13-F28 + if(arguments.size() > 2) + { + bits = std::stoi(arguments[2]); + if((func_byte & 0xDE) == 0xDE) + { + first = 13; + last = 20; + } + else + { + first = 21; + last = 28; + } + } + else + { + // this is a request for functions FL,F1-F12 + // for safety this guarantees that first nibble of function byte will always + // be of binary form 10XX which should always be the case for FL,F1-F12 + if((func_byte & 0xB0) == 0xB0) + { + first = 5; + last = 8; + } + else if((func_byte & 0xA0) == 0xA0) + { + first = 9; + last = 12; + } + else + { + impl->set_fn(0, func_byte & BIT(4)); + } + } + for(uint8_t id = first; id <= last; id++) + { + LOG(INFO, "[DCC++ loco %d] Set function %d to %d", loco_addr, id + , (int)(bits & BIT(id - first))); + impl->set_fn(id, bits & BIT(id - first)); + } + return COMMAND_NO_RESPONSE; +}); + +// command handler, this command converts a +// locomotive function update into a compatible DCC function control packet. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(FunctionExCommandAdapter, "fex", 3) +DCC_PROTOCOL_COMMAND_HANDLER(FunctionExCommandAdapter, +[](const vector arguments) +{ + int loco_addr = std::stoi(arguments[0]); + int function = std::stoi(arguments[1]); + int state = std::stoi(arguments[2]); + + GET_LOCO_VIA_EXECUTOR(impl, loco_addr); + LOG(INFO, "[DCC++ loco %d] Set function %d to %d", loco_addr, function + , state); + impl->set_fn(function, state); + return COMMAND_NO_RESPONSE; +}); + +// wrapper to handle the following command structures: +// CREATE: +// DELETE: +// DELETE: +// QUERY : +// SHOW : +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(ConsistCommandAdapter, "C", 0) +DCC_PROTOCOL_COMMAND_HANDLER(ConsistCommandAdapter, +[](const vector arguments) +{ + // TODO: reimplement + /* + if (arguments.empty()) + { + return locoManager->getConsistStateAsDCCpp(); + } + else if (arguments.size() == 1 && + locoManager->removeLocomotiveConsist(std::stoi(arguments[1]))) + { + return COMMAND_SUCCESSFUL_RESPONSE; + } + else if (arguments.size() == 2) + { + int8_t consistAddress = std::stoi(arguments[0]); + uint16_t locomotiveAddress = std::stoi(arguments[1]); + if (consistAddress == 0) + { + // query which consist loco is in + auto consist = locoManager->getConsistForLoco(locomotiveAddress); + if (consist) + { + return StringPrintf("", + consist->legacy_address() * consist->isDecoderAssistedConsist() ? -1 : 1, + locomotiveAddress); + } + } + else + { + // remove loco from consist + auto consist = locoManager->getConsistByID(consistAddress); + if (consist->isAddressInConsist(locomotiveAddress)) + { + consist->removeLocomotive(locomotiveAddress); + return COMMAND_SUCCESSFUL_RESPONSE; + } + } + // if we get here either the query or remove failed + return COMMAND_FAILED_RESPONSE; + } + else if (arguments.size() >= 3) + { + // create or update consist + uint16_t consistAddress = std::stoi(arguments[0]); + auto consist = locoManager->getConsistByID(consistAddress); + if (consist) + { + // existing consist, need to update + consist->releaseLocomotives(); + } + else + { + // verify if all provided locos are not already in a consist + for(int index = 1; index < arguments.size(); index++) + { + int32_t locomotiveAddress = std::stoi(arguments[index]); + if(locoManager->isAddressInConsist(abs(locomotiveAddress))) + { + LOG_ERROR("[Consist] Locomotive %d is already in a consist.", abs(locomotiveAddress)); + return COMMAND_FAILED_RESPONSE; + } + } + consist = locoManager->createLocomotiveConsist(consistAddress); + if(!consist) + { + LOG_ERROR("[Consist] Unable to create new Consist"); + return COMMAND_FAILED_RESPONSE; + } + } + // add locomotives to consist + for(int index = 1; index < arguments.size(); index++) + { + int32_t locomotiveAddress = std::stoi(arguments[index]); + consist->addLocomotive(abs(locomotiveAddress), locomotiveAddress > 0, + index - 1); + } + return COMMAND_SUCCESSFUL_RESPONSE; + } + */ + return COMMAND_FAILED_RESPONSE; +}) + +/* + : creates a new turnout ID, with specified BOARD + and INDEX. If turnout ID already exists, it is + updated with specificed BOARD and INDEX + returns: if successful and if unsuccessful (ie: out of memory) + + : deletes definition of turnout ID + returns: if successful and if unsuccessful (ie: ID does not exist) + + : lists all defined turnouts + returns: for each defined turnout or + if no turnouts defined + : sets turnout ID to either the "thrown" or + "unthrown" position + returns: , or if turnout ID does not exist +where + ID: the numeric ID (0-32767) of the turnout to control + BOARD: the primary address of the decoder controlling this turnout + (0-511) + INDEX: the subaddress of the decoder controlling this turnout (0-3) + THROW: 0 (unthrown) or 1 (thrown) +*/ +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(TurnoutCommandAdapter, "T", 0) +DCC_PROTOCOL_COMMAND_HANDLER(TurnoutCommandAdapter, +[](const vector arguments) +{ + auto turnoutManager = Singleton::instance(); + if (arguments.empty()) + { + // list all turnouts + return turnoutManager->get_state_for_dccpp(); + } + else + { + // index starts at one, reduce the index parameter by one. + uint16_t index = std::stoi(arguments[0]) - 1; + if (arguments.size() == 1) + { + auto turnout = turnoutManager->getByIndex(index); + if (turnout && turnoutManager->remove(turnout->getAddress())) + { + // delete turnout + return COMMAND_SUCCESSFUL_RESPONSE; + } + } + else if (arguments.size() == 2) + { + // throw turnout + auto turnout = turnoutManager->getByIndex(index); + if (turnout) + { + turnout->set(std::stoi(arguments[1])); + return COMMAND_SUCCESSFUL_RESPONSE; + } + } + else if (arguments.size() == 3) + { + // board can be 0-511 and port 0-3, however board 0 is problematic + int16_t board = std::stoi(arguments[1]); + int8_t port = std::stoi(arguments[2]); + // validate input parameters and reject values that are out of range + if (board <= 0 || board > 511 || port < 0 || port > 3) + { + LOG_ERROR("[DCC++ T] Rejecting invalid board(%d), port(%d)", board + , port); + return COMMAND_FAILED_RESPONSE; + } + // create/update turnout + uint16_t addr = decodeDCCAccessoryAddress(board, port); + if (addr == 0 || addr > 2044) + { + LOG_ERROR("[DCC++ T] Address %d is out of range, rejecting", addr); + return COMMAND_FAILED_RESPONSE; + } + LOG(VERBOSE, "[DCC++ T] decoded %d:%d to %d", board, port, addr); + turnoutManager->createOrUpdate(addr); + return COMMAND_SUCCESSFUL_RESPONSE; + } + } + return COMMAND_FAILED_RESPONSE; +}) + +/* + : Toggle turnout. + : Create/Update a Turnout + all will return : if successful and if unsuccessful. + +where + ADDRESS: the DCC decoder address for the turnout + TYPE: turnout type: + 0 : LEFT + 1 : RIGHT + 2 : WYE + 3 : MULTI +*/ +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(TurnoutExCommandAdapter, "Tex", 1) +DCC_PROTOCOL_COMMAND_HANDLER(TurnoutExCommandAdapter, +[](const vector arguments) +{ + if (!arguments.empty()) + { + uint16_t addr = std::stoi(arguments[0]); + if (addr == 0 || addr > 2044) + { + LOG_ERROR("[DCC++ Turnout] Address %d is out of range, rejecting", addr); + return COMMAND_FAILED_RESPONSE; + } + if (arguments.size() == 1) + { + return Singleton::instance()->toggle(addr); + } + TurnoutType type = (TurnoutType)std::stoi(arguments[1]); + if (Singleton::instance()->createOrUpdate(addr, type)) + { + return COMMAND_SUCCESSFUL_RESPONSE; + } + } + return COMMAND_FAILED_RESPONSE; +}) + +/* + : Throws a turnout (accessory decoder) + returns: + +Note: When this is received a Turnout will be created based on the decoded +DCC address for the accessory decoder. +where + BOARD: the primary address of the decoder controlling this turnout + (0-511) + INDEX: the subaddress of the decoder controlling this turnout (0-3) + THROW: 0 (unthrown) or 1 (thrown) +*/ +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(AccessoryCommand, "a", 3) +DCC_PROTOCOL_COMMAND_HANDLER(AccessoryCommand, +[](const vector arguments) +{ + return Singleton::instance()->set( + decodeDCCAccessoryAddress(std::stoi(arguments[0]) + , std::stoi(arguments[1])) + , std::stoi(arguments[2]) + ); +}) + + +// command handler, this command will clear all stored Turnouts, Outputs, +// Sensors and S88 Sensors (if enabled) after sending this command. Note, when +// running with the PCB configuration only turnouts will be cleared. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(ConfigErase, "e", 0) +DCC_PROTOCOL_COMMAND_HANDLER(ConfigErase, +[](const vector arguments) +{ + Singleton::instance()->clear(); +#if CONFIG_GPIO_SENSORS + SensorManager::clear(); + SensorManager::store(); +#if CONFIG_GPIO_S88 + S88BusManager::instance()->clear(); + S88BusManager::instance()->store(); +#endif // CONFIG_GPIO_S88 +#endif // CONFIG_GPIO_SENSORS +#if CONFIG_GPIO_OUTPUTS + OutputManager::clear(); + OutputManager::store(); +#endif // CONFIG_GPIO_OUTPUTS + return COMMAND_SUCCESSFUL_RESPONSE; +}) + +// command handler, this command stores all currently defined Turnouts, +// Sensors, S88 Sensors (if enabled) and Outputs to persistent storage for use +// by the Command Station in subsequent startups. Note, when running with the +// PCB configuration only turnouts will be stored. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(ConfigStore, "E", 0) +DCC_PROTOCOL_COMMAND_HANDLER(ConfigStore, +[](const vector arguments) +{ + return StringPrintf("" + , Singleton::instance()->count() +#if CONFIG_GPIO_SENSORS + , SensorManager::store() +#if CONFIG_GPIO_S88 + + S88BusManager::instance()->store() +#endif // CONFIG_GPIO_S88 +#else + , 0 +#endif // CONFIG_GPIO_SENSORS +#if CONFIG_GPIO_OUTPUTS + , OutputManager::store() +#else + , 0 +#endif // CONFIG_GPIO_OUTPUTS + ); +}) + +// command handler, this command sends the current status for all parts of +// the ESP32 Command Station. JMRI uses this command as a keep-alive heartbeat +// command. +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(StatusCommand, "s", 0) +DCC_PROTOCOL_COMMAND_HANDLER(StatusCommand, +[](const vector arguments) +{ + wifi_mode_t mode; + const esp_app_desc_t *app_data = esp_ota_get_app_description(); + string status = StringPrintf("" + , app_data->version, app_data->date, app_data->time); + status += esp32cs::get_track_state_for_dccpp(); + auto trains = Singleton::instance(); + for (size_t id = 0; id < trains->size(); id++) + { + auto nodeid = trains->get_train_node_id(id); + if (nodeid) + { + auto impl = trains->get_train_impl(nodeid); + status += convert_loco_to_dccpp_state(impl, id); + } + } + status += Singleton::instance()->get_state_for_dccpp(); +#if CONFIG_GPIO_OUTPUTS + status += OutputManager::get_state_for_dccpp(); +#endif // CONFIG_GPIO_OUTPUTS + if (esp_wifi_get_mode(&mode) == ESP_OK) + { + tcpip_adapter_ip_info_t ip_info; + if (mode == WIFI_MODE_STA || mode == WIFI_MODE_APSTA) + { + if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info) == ESP_OK) + { + status += StringPrintf("", IP2STR(&ip_info.ip)); + } + if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip_info) == ESP_OK) + { + status += StringPrintf("", IP2STR(&ip_info.ip)); + } + } + else if (mode != WIFI_MODE_NULL && + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip_info) == ESP_OK) + { + status += StringPrintf("", IP2STR(&ip_info.ip)); + } + } + return status; +}) + +void DCCPPProtocolHandler::init() +{ + registerCommand(new ThrottleCommandAdapter()); + registerCommand(new ThrottleExCommandAdapter()); + registerCommand(new FunctionCommandAdapter()); + registerCommand(new FunctionExCommandAdapter()); + registerCommand(new ConsistCommandAdapter()); + registerCommand(new AccessoryCommand()); + registerCommand(new PowerOnCommand()); + registerCommand(new PowerOffCommand()); + registerCommand(new CurrentDrawCommand()); + registerCommand(new StatusCommand()); + registerCommand(new ReadCVCommand()); + registerCommand(new WriteCVByteProgCommand()); + registerCommand(new WriteCVBitProgCommand()); + registerCommand(new WriteCVByteOpsCommand()); + registerCommand(new WriteCVBitOpsCommand()); + registerCommand(new ConfigErase()); + registerCommand(new ConfigStore()); +#if defined(CONFIG_GPIO_OUTPUTS) + registerCommand(new OutputCommandAdapter()); + registerCommand(new OutputExCommandAdapter()); +#endif + registerCommand(new TurnoutCommandAdapter()); + registerCommand(new TurnoutExCommandAdapter()); +#if defined(CONFIG_GPIO_SENSORS) + registerCommand(new SensorCommandAdapter()); +#if defined(CONFIG_GPIO_S88) + registerCommand(new S88BusCommandAdapter()); +#endif + registerCommand(new RemoteSensorsCommandAdapter()); +#endif + registerCommand(new FreeHeapCommand()); + registerCommand(new EStopCommand()); +} + +string DCCPPProtocolHandler::process(const string &commandString) +{ + vector parts; + http::tokenize(commandString, parts); + string commandID = parts.front(); + parts.erase(parts.begin()); + LOG(VERBOSE, "Command: %s, argument count: %d", commandID.c_str() + , parts.size()); + auto handler = getCommandHandler(commandID); + if (handler) + { + if (parts.size() >= handler->getMinArgCount()) + { + return handler->process(parts); + } + else + { + LOG_ERROR("%s requires (at least) %zu args but %zu args were provided, " + "reporting failure", commandID.c_str() + , handler->getMinArgCount(), parts.size()); + return COMMAND_FAILED_RESPONSE; + } + } + LOG_ERROR("No command handler for [%s]", commandID.c_str()); + return COMMAND_FAILED_RESPONSE; +} + +void DCCPPProtocolHandler::registerCommand(DCCPPProtocolCommand *cmd) +{ + for (const auto& command : registeredCommands) + { + if(!command->getID().compare(cmd->getID())) + { + LOG_ERROR("Ignoring attempt to register second command with ID: %s", + cmd->getID().c_str()); + return; + } + } + LOG(VERBOSE, "Registering interface command %s", cmd->getID().c_str()); + registeredCommands.emplace_back(cmd); +} + +DCCPPProtocolCommand *DCCPPProtocolHandler::getCommandHandler(const string &id) +{ + auto command = std::find_if(registeredCommands.begin() + , registeredCommands.end() + , [id](const std::unique_ptr &cmd) + { + return cmd->getID() == id; + }); + if (command != registeredCommands.end()) + { + return command->get(); + } + return nullptr; +} + +DCCPPProtocolConsumer::DCCPPProtocolConsumer() +{ + _buffer.resize(256); +} + +std::string DCCPPProtocolConsumer::feed(uint8_t *data, size_t len) +{ + for(size_t i = 0; i < len; i++) + { + _buffer.emplace_back(data[i]); + } + return processData(); +} + +string DCCPPProtocolConsumer::processData() +{ + auto s = _buffer.begin(); + auto consumed = _buffer.begin(); + string response; + for(; s != _buffer.end();) + { + s = std::find(s, _buffer.end(), '<'); + auto e = std::find(s, _buffer.end(), '>'); + if(s != _buffer.end() && e != _buffer.end()) + { + // discard the < + s++; + // discard the > + *e = 0; + std::string str(reinterpret_cast(&*s)); + response += DCCPPProtocolHandler::process(std::move(str)); + consumed = e; + } + s = e; + } + // drop everything we used from the buffer. + _buffer.erase(_buffer.begin(), consumed); + return response; +} diff --git a/include/DCCProgrammer.h b/components/DCCppProtocol/include/DCCProgrammer.h similarity index 54% rename from include/DCCProgrammer.h rename to components/DCCppProtocol/include/DCCProgrammer.h index d8339219..9663d9ee 100644 --- a/include/DCCProgrammer.h +++ b/components/DCCppProtocol/include/DCCProgrammer.h @@ -14,63 +14,66 @@ COPYRIGHT (c) 2019 Mike Dunston You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses **********************************************************************/ -#pragma once + +#ifndef DCC_PROG_H_ +#define DCC_PROG_H_ #include -enum CV_NAMES { - SHORT_ADDRESS=1, - DECODER_VERSION=7, - DECODER_MANUFACTURER=8, - ACCESSORY_DECODER_MSB_ADDRESS=9, - LONG_ADDRESS_MSB_ADDRESS=17, - LONG_ADDRESS_LSB_ADDRESS=18, - CONSIST_ADDRESS=19, - CONSIST_FUNCTION_CONTROL_F1_F8=21, - CONSIST_FUNCTION_CONTROL_FL_F9_F12=22, - DECODER_CONFIG=29 +enum CV_NAMES +{ + SHORT_ADDRESS = 1 +, DECODER_VERSION = 7 +, DECODER_MANUFACTURER = 8 +, ACCESSORY_DECODER_MSB_ADDRESS = 9 +, LONG_ADDRESS_MSB_ADDRESS = 17 +, LONG_ADDRESS_LSB_ADDRESS = 18 +, CONSIST_ADDRESS = 19 +, CONSIST_FUNCTION_CONTROL_F1_F8 = 21 +, CONSIST_FUNCTION_CONTROL_FL_F9_F12 = 22 +, DECODER_CONFIG = 29 }; static constexpr uint8_t CONSIST_ADDRESS_REVERSED_ORIENTATION = 0x80; static constexpr uint8_t CONSIST_ADDRESS_NO_ADDRESS = 0x00; -enum DECODER_CONFIG_BITS { - LOCOMOTIVE_DIRECTION=0, - FL_CONTROLLED_BY_SPEED=1, - POWER_CONVERSION=2, - BIDIRECTIONAL_COMMUNICATION=3, - SPEED_TABLE=4, - SHORT_OR_LONG_ADDRESS=5, - ACCESSORY_ADDRESS_MODE=6, - DECODER_TYPE=7 +enum DECODER_CONFIG_BITS +{ + LOCOMOTIVE_DIRECTION = 0 +, FL_CONTROLLED_BY_SPEED = 1 +, POWER_CONVERSION = 2 +, BIDIRECTIONAL_COMMUNICATION = 3 +, SPEED_TABLE = 4 +, SHORT_OR_LONG_ADDRESS = 5 +, ACCESSORY_ADDRESS_MODE = 6 +, DECODER_TYPE = 7 }; -enum CONSIST_FUNCTION_CONTROL_F1_F8_BITS { - F1_BIT=0, - F2_BIT=1, - F3_BIT=2, - F4_BIT=3, - F5_BIT=4, - F6_BIT=5, - F7_BIT=6, - F8_BIT=7 +enum CONSIST_FUNCTION_CONTROL_F1_F8_BITS +{ + F1_BIT = 0 +, F2_BIT = 1 +, F3_BIT = 2 +, F4_BIT = 3 +, F5_BIT = 4 +, F6_BIT = 5 +, F7_BIT = 6 +, F8_BIT = 7 }; -enum CONSIST_FUNCTION_CONTROL_FL_F9_F12_BITS { - FL_BIT=0, - F9_BIT=1, - F10_BIT=2, - F11_BIT=3, - F12_BIT=4 +enum CONSIST_FUNCTION_CONTROL_FL_F9_F12_BITS +{ + FL_BIT = 0 +, F9_BIT = 1 +, F10_BIT = 2 +, F11_BIT = 3 +, F12_BIT = 4 }; - -extern bool progTrackBusy; - -bool enterProgrammingMode(); -void leaveProgrammingMode(); int16_t readCV(const uint16_t); bool writeProgCVByte(const uint16_t, const uint8_t); bool writeProgCVBit(const uint16_t, const uint8_t, const bool); void writeOpsCVByte(const uint16_t, const uint16_t, const uint8_t); void writeOpsCVBit(const uint16_t, const uint16_t, const uint8_t, const bool); + +#endif // DCC_PROG_H_ \ No newline at end of file diff --git a/components/DCCppProtocol/include/DCCppProtocol.h b/components/DCCppProtocol/include/DCCppProtocol.h new file mode 100644 index 00000000..655ad832 --- /dev/null +++ b/components/DCCppProtocol/include/DCCppProtocol.h @@ -0,0 +1,84 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2017-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef DCC_PROTOCOL_H_ +#define DCC_PROTOCOL_H_ + +#include +#include +#include + +#include "sdkconfig.h" + +// Class definition for a single protocol command +class DCCPPProtocolCommand +{ +public: + virtual ~DCCPPProtocolCommand() {} + virtual std::string process(const std::vector) = 0; + virtual std::string getID() = 0; + virtual size_t getMinArgCount() = 0; +}; + +#define DECLARE_DCC_PROTOCOL_COMMAND_CLASS(name, id, min_args) \ +class name : public DCCPPProtocolCommand \ +{ \ +public: \ + std::string process(const std::vector) override; \ + std::string getID() override \ + { \ + return id; \ + } \ + size_t getMinArgCount() override \ + { \ + return min_args; \ + } \ +}; + +#define DCC_PROTOCOL_COMMAND_HANDLER(name, func) \ +std::string name::process(const std::vector args) \ +{ \ + return func(args); \ +} + +// Class definition for the Protocol Interpreter +class DCCPPProtocolHandler +{ +public: + static void init(); + static std::string process(const std::string &); + static void registerCommand(DCCPPProtocolCommand *); + static DCCPPProtocolCommand *getCommandHandler(const std::string &); +}; + +class DCCPPProtocolConsumer +{ +public: + DCCPPProtocolConsumer(); + std::string feed(uint8_t *, size_t); +private: + std::string processData(); + std::vector _buffer; +}; + +const std::string COMMAND_FAILED_RESPONSE = ""; +const std::string COMMAND_SUCCESSFUL_RESPONSE = ""; +const std::string COMMAND_NO_RESPONSE = ""; + +std::string convert_loco_to_dccpp_state(openlcb::TrainImpl *impl, size_t id); + +#endif // DCC_PROTOCOL_H_ \ No newline at end of file diff --git a/components/Esp32HttpServer/CMakeLists.txt b/components/Esp32HttpServer/CMakeLists.txt new file mode 100644 index 00000000..dbeeaaf7 --- /dev/null +++ b/components/Esp32HttpServer/CMakeLists.txt @@ -0,0 +1,20 @@ +set(COMPONENT_SRCS + "Dnsd.cpp" + "HttpdConstants.cpp" + "HttpRequest.cpp" + "HttpRequestFlow.cpp" + "HttpRequestWebSocket.cpp" + "HttpResponse.cpp" + "HttpServer.cpp" +) + +set(COMPONENT_ADD_INCLUDEDIRS "include" ) + +set(COMPONENT_REQUIRES "OpenMRNLite" "lwip" "mbedtls") + +register_component() +set_source_files_properties(HttpRequest.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(HttpRequestWebSocket.cpp PROPERTIES COMPILE_FLAGS "-Wno-implicit-fallthrough -Wno-ignored-qualifiers") +set_source_files_properties(HttpRequestFlow.cpp PROPERTIES COMPILE_FLAGS "-Wno-implicit-fallthrough -Wno-ignored-qualifiers") +set_source_files_properties(HttpResponse.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(HttpServer.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) diff --git a/components/Esp32HttpServer/Dnsd.cpp b/components/Esp32HttpServer/Dnsd.cpp new file mode 100644 index 00000000..0ad25346 --- /dev/null +++ b/components/Esp32HttpServer/Dnsd.cpp @@ -0,0 +1,167 @@ +/********************************************************************** +ESP32 HTTP Server + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "Dnsd.h" +#include +#include +#include +#include + +namespace http +{ + +static void* dns_thread_start(void* arg) +{ + Singleton::instance()->dns_process_thread(); + return nullptr; +} + +Dnsd::Dnsd(uint32_t ip, string name, uint16_t port) + : local_ip_(ip), name_(name), port_(port) + , dns_thread_(name_.c_str(), 1, config_dnsd_stack_size(), dns_thread_start + , nullptr) +{ +} + +Dnsd::~Dnsd() +{ + shutdown_ = true; + while(!shutdownComplete_) + { + vTaskDelay(pdMS_TO_TICKS(1)); + } +} + +void Dnsd::dns_process_thread() +{ + struct sockaddr_in addr; + int fd; + + ERRNOCHECK("socket", fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP)); + + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(port_); + int val = 1; + ERRNOCHECK("setsockopt_reuseaddr", + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val))); + ERRNOCHECK("bind", + ::bind(fd, (struct sockaddr *) &addr, sizeof(addr))); + + LOG(INFO, "[%s] Listening on port %d, fd %d, using %s for local IP" + , name_.c_str(), port_, fd, ipv4_to_string(local_ip_).c_str()); + vector receiveBuffer(config_dnsd_buffer_size(), 0); + vector responseBuffer; + + while (!shutdown_) + { + receiveBuffer.clear(); + sockaddr_in source; + socklen_t source_len = sizeof(sockaddr_in); + // This will block the thread until it receives a message on the socket. + int len = ::recvfrom(fd, receiveBuffer.data(), config_dnsd_buffer_size(), 0 + , (sockaddr *)&source, &source_len); + if(len >= sizeof(DNSHeader)) + { + DNSHeader *header = (DNSHeader *)receiveBuffer.data(); + string parsed_domain_name; + + // parse the request to extract the domain name being requested + if (ntohs(header->questions) == 1 && header->answers == 0 && + header->authorties == 0 && header->resources == 0) + { + // extract the requested domain name, it is a broken into segments + // with the period replaced with a zero byte between segments instead + // of the period. Max length of the domain name segments is 255 bytes + // including the null markers. + uint16_t offset = sizeof(DNSHeader); + while (offset < 255 && offset < len) + { + uint8_t segment_len = receiveBuffer.data()[offset++]; + if (segment_len) + { + if (parsed_domain_name.length()) + { + parsed_domain_name += '.'; + } + parsed_domain_name.append( + (const char *)(receiveBuffer.data() + offset), segment_len); + offset += segment_len; + } + else + { + break; + } + } + } + if (parsed_domain_name.empty()) + { + // no domain name to look up, discard the request and get another. + continue; + } + LOG(CONFIG_HTTP_DNS_LOG_LEVEL + , "[%s <- %s] id: %d, rd:%d, tc:%d, aa:%d, opc:%d, qr:%d, rc:%d, z:%d " + "ra:%d, q:%d, a:%d, au:%d, res:%d, len:%d, domain:%s" + , name_.c_str(), inet_ntoa(source.sin_addr), header->id, header->rd + , header->tc, header->aa, header->opc, header->qr, header->rc + , header->z, header->ra, header->questions, header->answers + , header->authorties, header->resources, len + , parsed_domain_name.c_str()); + // check if it is a request or response, qr = 0 and opc = 0 is request + if (!header->qr && !header->opc) + { + // convert the request to a response by modifying the request header + // in place (since it gets copied to the response as-is). + header->qr = 1; + header->ra = 1; + header->answers = 1; + // response payload + DNSResponse response = + { + .id = htons(0xC00C) + , .answer = htons(1) + , .classes = htons(1) + , .ttl = htonl(60) + , .length = htons(sizeof(uint32_t)) + , .address = htonl(local_ip_) + }; + responseBuffer.resize(len + sizeof(DNSResponse)); + memcpy(responseBuffer.data(), receiveBuffer.data(), len); + memcpy(responseBuffer.data() + len, &response, sizeof(DNSResponse)); + LOG(CONFIG_HTTP_DNS_LOG_LEVEL, "[%s -> %s] %s -> %s", name_.c_str() + , inet_ntoa(source.sin_addr), parsed_domain_name.c_str() + , ipv4_to_string(local_ip_).c_str()); + ERRNOCHECK("sendto", sendto(fd, responseBuffer.data() + , responseBuffer.size(), 0 + , (const sockaddr*)&source + , sizeof(sockaddr_in))); + } + } + else + { + if (errno == EAGAIN || errno == ECONNRESET || errno == ENOTCONN || + errno == ETIMEDOUT) + { + continue; + } + print_errno_and_exit("recvfrom"); + } + } + shutdownComplete_ = true; +} + +} // namespace http diff --git a/components/Esp32HttpServer/HttpRequest.cpp b/components/Esp32HttpServer/HttpRequest.cpp new file mode 100644 index 00000000..fa3b2120 --- /dev/null +++ b/components/Esp32HttpServer/HttpRequest.cpp @@ -0,0 +1,287 @@ +/********************************************************************** +ESP32 HTTP Server + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "Httpd.h" + +namespace http +{ + +static constexpr const char * HTTP_METHOD_DELETE = "DELETE"; +static constexpr const char * HTTP_METHOD_GET = "GET"; +static constexpr const char * HTTP_METHOD_HEAD = "HEAD"; +static constexpr const char * HTTP_METHOD_POST = "POST"; +static constexpr const char * HTTP_METHOD_PATCH = "PATCH"; +static constexpr const char * HTTP_METHOD_PUT = "PUT"; + +std::map well_known_http_headers = +{ + { ACCEPT, "Accept" } +, { CACHE_CONTROL, "Cache-Control" } +, { CONNECTION, "Connection" } +, { CONTENT_ENCODING, "Content-Encoding" } +, { CONTENT_TYPE, "Content-Type" } +, { CONTENT_LENGTH, "Content-Length" } +, { CONTENT_DISPOSITION, "Content-Disposition" } +, { EXPECT, "Expect" } +, { HOST, "Host" } +, { IF_MODIFIED_SINCE, "If-Modified-Since" } +, { LAST_MODIFIED, "Last-Modified" } +, { LOCATION, "Location" } +, { ORIGIN, "Origin" } +, { UPGRADE, "Upgrade" } +, { WS_VERSION, "Sec-WebSocket-Version" } +, { WS_KEY, "Sec-WebSocket-Key" } +, { WS_ACCEPT, "Sec-WebSocket-Accept"} +}; + +void HttpRequest::method(const string &value) +{ + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[HttpReq %p] Setting Method: %s", this, value.c_str()); + raw_method_.assign(std::move(value)); + if (!raw_method_.compare(HTTP_METHOD_DELETE)) + { + method_ = HttpMethod::DELETE; + } + else if (!raw_method_.compare(HTTP_METHOD_GET)) + { + method_ = HttpMethod::GET; + } + else if (!raw_method_.compare(HTTP_METHOD_HEAD)) + { + method_ = HttpMethod::HEAD; + } + else if (!raw_method_.compare(HTTP_METHOD_POST)) + { + method_ = HttpMethod::POST; + } + else if (!raw_method_.compare(HTTP_METHOD_PATCH)) + { + method_ = HttpMethod::PATCH; + } + else if (!raw_method_.compare(HTTP_METHOD_PUT)) + { + method_ = HttpMethod::PUT; + } +} + +HttpMethod HttpRequest::method() +{ + return method_; +} + +const string &HttpRequest::raw_method() +{ + return raw_method_; +} + +const string &HttpRequest::uri() +{ + return uri_; +} + +void HttpRequest::uri(const string &value) +{ + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[HttpReq %p] Setting URI: %s", this, value.c_str()); + uri_.assign(std::move(value)); +} + +void HttpRequest::param(const std::pair &value) +{ + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[HttpReq %p] Adding param: %s: %s", this, value.first.c_str() + , value.second.c_str()); + params_.insert(std::move(value)); +} + +void HttpRequest::header(const std::pair &value) +{ + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[HttpReq %p] Adding header: %s: %s", this, value.first.c_str() + , value.second.c_str()); + headers_.insert(value); +} + +void HttpRequest::header(HttpHeader header, std::string value) +{ + if (has_header(header)) + { + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[HttpReq %p] Replacing header: %s: %s (old: %s)", this + , well_known_http_headers[header].c_str(), value.c_str() + , headers_[well_known_http_headers[header]].c_str()); + } + else + { + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[HttpReq %p] Adding header: %s: %s", this + , well_known_http_headers[header].c_str(), value.c_str()); + } + headers_[well_known_http_headers[header]] = value; +} + +bool HttpRequest::has_header(const string &name) +{ + return headers_.find(name) != headers_.end(); +} + +bool HttpRequest::has_header(const HttpHeader name) +{ + return has_header(well_known_http_headers[name]); +} + +const string &HttpRequest::header(const string name) +{ + if (!has_header(name)) + { + return no_value_; + } + return headers_[name]; +} + +const string &HttpRequest::header(const HttpHeader name) +{ + return header(well_known_http_headers[name]); +} + +void HttpRequest::reset() +{ + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[HttpReq %p] Resetting to blank request", this); + headers_.clear(); + params_.clear(); + raw_method_.clear(); + method_ = HttpMethod::UNKNOWN_METHOD; + uri_.clear(); + error_ = false; +} + +bool HttpRequest::keep_alive() +{ + if (!has_header(HttpHeader::CONNECTION)) + { + return false; + } + return header(HttpHeader::CONNECTION).compare(HTTP_CONNECTION_CLOSE); +} + +void HttpRequest::error(bool value) +{ + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[HttpReq %p] Setting error flag to %d", this, value); + error_ = value; +} + +bool HttpRequest::error() +{ + return error_; +} + +ContentType HttpRequest::content_type() +{ + static const string MULTIPART_FORM = "multipart/form-data"; + static const string FORM_URLENCODED = "application/x-www-form-urlencoded"; + // For a multipart/form-data the Content-Type value will look like: + // multipart/form-data; boundary=----WebKitFormBoundary4Aq7x8166jGWkA0q + if (!header(HttpHeader::CONTENT_TYPE).compare(0, MULTIPART_FORM.size() + , MULTIPART_FORM)) + { + return ContentType::MULTIPART_FORMDATA; + } + if (!header(HttpHeader::CONTENT_TYPE).compare(0, FORM_URLENCODED.size() + , FORM_URLENCODED)) + { + return ContentType::FORM_URLENCODED; + } + return ContentType::UNKNOWN_TYPE; +} + +void HttpRequest::set_status(HttpStatusCode code) +{ + status_ = code; +} + +size_t HttpRequest::params() +{ + return params_.size(); +} + +string HttpRequest::param(string name) +{ + if (params_.count(name)) + { + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[Req %p] Param %s -> %s", this, name.c_str(), params_[name].c_str()); + return params_[name]; + } + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[Req %p] Param %s doesn't exist", this, name.c_str()); + return no_value_; +} + +bool HttpRequest::param(string name, bool def) +{ + if (params_.count(name)) + { + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[Req %p] Param %s -> %s", this, name.c_str(), params_[name].c_str()); + auto value = params_[name]; + std::transform(value.begin(), value.end(), value.begin(), ::tolower); + return value.compare("false"); + } + return def; +} + +int HttpRequest::param(string name, int def) +{ + if (params_.count(name)) + { + LOG(CONFIG_HTTP_REQ_LOG_LEVEL + , "[Req %p] Param %s -> %s", this, name.c_str(), params_[name].c_str()); + return std::stoi(params_[name]); + } + return def; +} + +bool HttpRequest::has_param(string name) +{ + return params_.count(name); +} + +string HttpRequest::to_string() +{ + string res = StringPrintf("[HttpReq %p] method:%s uri:%s,error:%d," + "header-count:%zu,param-count:%zu" + , this, raw_method_.c_str(), uri_.c_str(), error_ + , headers_.size(), params_.size()); + for (auto &ent : headers_) + { + res.append( + StringPrintf("\nheader: %s: %s%s", ent.first.c_str(), ent.second.c_str() + , HTML_EOL)); + } + for (auto &ent : params_) + { + res.append( + StringPrintf("\nparam: %s: %s%s", ent.first.c_str(), ent.second.c_str() + , HTML_EOL)); + } + return res; +} + +} // namespace http diff --git a/components/Esp32HttpServer/HttpRequestFlow.cpp b/components/Esp32HttpServer/HttpRequestFlow.cpp new file mode 100644 index 00000000..197e0852 --- /dev/null +++ b/components/Esp32HttpServer/HttpRequestFlow.cpp @@ -0,0 +1,981 @@ +/********************************************************************** +ESP32 HTTP Server + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "Httpd.h" +#include "HttpStringUtils.h" + +namespace http +{ + +static vector captive_portal_uris = +{ + "/generate_204", // Android + "/gen_204", // Android 9.0 + "/mobile/status.php", // Android 8.0 (Samsung s9+) + "/ncsi.txt", // Windows + "/success.txt", // OSX / FireFox + "/hotspot-detect.html", // iOS 8/9 + "/hotspotdetect.html", // iOS 8/9 + "/library/test/success.html" // iOS 8/9 + "/kindle-wifi/wifiredirect.html" // Kindle + "/kindle-wifi/wifistub.html" // Kindle +}; + +HttpRequestFlow::HttpRequestFlow(Httpd *server, int fd + , uint32_t remote_ip) + : StateFlowBase(server) + , server_(server) + , fd_(fd) + , remote_ip_(remote_ip) +{ + start_flow(STATE(start_request)); + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL, "[Httpd fd:%d] Connected.", fd_); +} + +HttpRequestFlow::~HttpRequestFlow() +{ + if (close_) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL, "[Httpd fd:%d] Closed", fd_); + ::close(fd_); + } +} + +StateFlowBase::Action HttpRequestFlow::start_request() +{ + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL, "[Httpd fd:%d] reading header", fd_); + req_.reset(); + part_boundary_.assign(""); + part_filename_.assign(""); + part_type_.assign(""); + raw_header_.assign(""); + start_time_ = esp_timer_get_time(); + buf_.resize(header_read_size_); + return read_repeated_with_timeout(&helper_, timeout_, fd_, buf_.data() + , header_read_size_ + , STATE(parse_header_data)); +} + +StateFlowBase::Action HttpRequestFlow::parse_header_data() +{ + if (helper_.hasError_) + { + return call_immediately(STATE(abort_request)); + } + else if (helper_.remaining_ == header_read_size_ || buf_.empty()) + { + return yield_and_call(STATE(read_more_data)); + } + + raw_header_.append((char *)buf_.data() + , header_read_size_ - helper_.remaining_); + + // if we don't have an EOL string in the data yet, get more data + if (raw_header_.find(HTML_EOL) == string::npos) + { + return yield_and_call(STATE(read_more_data)); + } + + // parse the data we have into delimited lines of header data, this will + // leave some data in the raw_header_ which will need to be pushed back + // into the buf_ after we reach the body segment. + vector lines; + size_t parsed = tokenize(raw_header_, lines, HTML_EOL, false); + + // drop whatever has been tokenized so we don't process it again + raw_header_.erase(0, parsed); + + // the first line is always the request line + if (req_.raw_method().empty()) + { + vector request; + tokenize(lines[0], request, " "); + if (request.size() != 3) + { + LOG_ERROR("[Httpd fd:%d] Malformed request: %s.", fd_, lines[0].c_str()); + req_.set_status(HttpStatusCode::STATUS_BAD_REQUEST); + return call_immediately(STATE(abort_request_with_response)); + } + req_.method(request[0]); + vector uri_parts; + tokenize(request[1], uri_parts, "?"); + req_.uri(uri_parts[0]); + uri_parts.erase(uri_parts.begin()); + for (string &part : uri_parts) + { + vector params; + tokenize(part, params, "&"); + for (auto param : params) + { + req_.param(break_string(param, "=")); + } + } + // remove the first line since we processed it + lines.erase(lines.begin()); + } + + // process any remaining lines as headers until we reach a blank line + size_t processed_lines = 0; + for (auto &line : lines) + { + processed_lines++; + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] line(%zu/%zu): ||%s||", fd_, req_.uri().c_str() + , processed_lines, lines.size(), line.c_str()); + // check if we have reached a blank line, this is immediately after the + // last header in the request. + if (line.empty()) + { + // Now that we have the request headers parsed we can check if the + // request exceeds the size limits of the server. + if (server_->is_request_too_large(&req_)) + { + // If the request has the EXPECT header we can reject the request with + // the EXPECTATION_FAILED (417) status, otherwise reject it with + // BAD_REQUEST (400). + req_.set_status(HttpStatusCode::STATUS_BAD_REQUEST); + if (req_.has_header(HttpHeader::EXPECT)) + { + req_.set_status(HttpStatusCode::STATUS_EXPECATION_FAILED); + } + LOG_ERROR("[Httpd fd:%d,uri:%s] Request body is too large, " + "aborting with status %d" + , fd_, req_.uri().c_str(), res_->code_); + return call_immediately(STATE(abort_request_with_response)); + } + + if (!server_->is_servicable_uri(&req_)) + { + // check if it is a captive portal request + if (server_->captive_active_ && + std::find_if(captive_portal_uris.begin(), captive_portal_uris.end() + , [&](const string &ent) {return !ent.compare(req_.uri());}) + != captive_portal_uris.end() && remote_ip_) + { + if (!server_->captive_auth_.count(remote_ip_) || + (server_->captive_auth_[remote_ip_] > server_->captive_timeout_ && + server_->captive_timeout_ != UINT32_MAX)) + { + // new client or authentication expired, send the canned response + res_.reset(new StringResponse(server_->captive_response_ + , MIME_TYPE_TEXT_HTML)); + } + else if (req_.uri().find("_204") > 0 || + req_.uri().find("status.php") > 0) + { + // These URIs require a generic response with code 204 + res_.reset( + new AbstractHttpResponse(HttpStatusCode::STATUS_NO_CONTENT)); + } + else if (req_.uri().find("ncsi.txt") > 0) + { + // Windows success page content + res_.reset( + new StringResponse("Microsoft NCSI", MIME_TYPE_TEXT_PLAIN)); + } + else if (req_.uri().find("success.txt") > 0) + { + // Generic success.txt page content + res_.reset( + new StringResponse("success", MIME_TYPE_TEXT_PLAIN)); + } + else + { + // iOS success page content + res_.reset( + new StringResponse("Success" + "Success" + , MIME_TYPE_TEXT_HTML)); + } + } + else if (server_->captive_active_ && + !server_->captive_auth_uri_.compare(req_.uri())) + { + server_->captive_auth_[remote_ip_] = + esp_timer_get_time() + server_->captive_timeout_; + res_.reset(new AbstractHttpResponse(HttpStatusCode::STATUS_OK)); + } + else + { + LOG_ERROR("[Httpd fd:%d,uri:%s] Unknown URI, sending 404", fd_ + , req_.uri().c_str()); + res_.reset(new UriNotFoundResponse(req_.uri())); + } + req_.error(true); + return call_immediately(STATE(send_response_headers)); + } + + // Check if we have processed all parsed lines, if not add them back to + // the buffer for deferred processing. + if (processed_lines != lines.size()) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Processed %zu/%zu lines, adding remaining " + "lines to body payload" + , fd_, req_.uri().c_str(), processed_lines, lines.size()); + string unprocessed = string_join(lines.begin() + processed_lines + , lines.end(), HTML_EOL) + raw_header_; + raw_header_.assign(unprocessed); + } + + return yield_and_call(STATE(process_request)); + } + else + { + // it appears to be a header entry + + // split header into name/value pair + auto h = break_string(line, ": "); + + // URL decode the header name and value + h.first = url_decode(h.first); + h.second = url_decode(h.second); + + // stash the header for later retrieval + req_.header(h); + } + } + + return yield_and_call(STATE(read_more_data)); +} + +StateFlowBase::Action HttpRequestFlow::process_request() +{ + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL, "[Httpd fd:%d] %s", fd_ + , req_.to_string().c_str()); + + if (!req_.header(HttpHeader::UPGRADE).compare(HTTP_UPGRADE_HEADER_WEBSOCKET)) + { + // upgrade to websocket! + if (!req_.has_header(HttpHeader::WS_VERSION) || + !req_.has_header(HttpHeader::WS_KEY)) + { + LOG_ERROR("[Httpd fd:%d,uri:%s] Missing required websocket header(s)\n%s" + , fd_, req_.uri().c_str(), req_.to_string().c_str()); + req_.set_status(HttpStatusCode::STATUS_BAD_REQUEST); + return call_immediately(STATE(abort_request_with_response)); + } + return call_immediately(STATE(upgrade_to_websocket)); + } + + // if it is a PUT or POST and there is a content-length header we need to + // read the body of the message in chunks and pass it off to the request + // handler. + if (req_.method() == HttpMethod::POST || req_.method() == HttpMethod::PUT) + { + // move the raw unparsed header bits back to the buffer + if (!raw_header_.empty()) + { + // resize for the reading of the body payload + buf_.clear(); + buf_.reserve(body_read_size_); + std::move(raw_header_.begin(), raw_header_.end(), std::back_inserter(buf_)); + raw_header_.assign(""); + } + + // If we do not have a Content-Length header outright reject the request + // as there is no telling how big the payload is without reading it in + // full. + if (!req_.has_header(HttpHeader::CONTENT_LENGTH)) + { + LOG_ERROR("[Httpd fd:%d] Request does not have the Content-Type " + "header, aborting!\n%s", fd_, req_.to_string().c_str()); + req_.set_status(HttpStatusCode::STATUS_BAD_REQUEST); + return call_immediately(STATE(abort_request_with_response)); + } + + // extract the body length from the content length header + body_len_ = std::stoul(req_.header(HttpHeader::CONTENT_LENGTH)); + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] body (header): %zu", fd_, req_.uri().c_str() + , body_len_); + + if (req_.content_type() == ContentType::MULTIPART_FORMDATA) + { + // If we do not have a streaming handler for the URI abort the request. + if (!server_->stream_handler(req_.uri())) + { + LOG_ERROR("[Httpd fd:%d] No streaming handler to receive payload, " + "aborting!", fd_); + req_.set_status(HttpStatusCode::STATUS_SERVER_ERROR); + return call_immediately(STATE(abort_request_with_response)); + } + // cache the stream handler for this URI + part_stream_ = server_->stream_handler(req_.uri()); + + // force request to be concluded at end of processing + req_.header(HttpHeader::CONNECTION, HTTP_CONNECTION_CLOSE); + + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "Converting to multipart/form-data req"); + return call_immediately(STATE(start_multipart_processing)); + } + else if (req_.content_type() == ContentType::FORM_URLENCODED) + { + // convert the request body from form url-encoded to parameters and send + // it for processing + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "Converting to application/x-www-form-urlencoded req"); + return call_immediately(STATE(read_form_data)); + } + else if (server_->stream_handler(req_.uri())) + { + // cache the stream handler for this URI + part_stream_ = server_->stream_handler(req_.uri()); + + // we have some of the body already read in, process it before requesting + // more data + if (!buf_.empty()) + { + return yield_and_call(STATE(stream_body)); + } + else + { + // read the payload and process it in chunks + return read_repeated_with_timeout(&helper_, timeout_, fd_ + , buf_.data() + , std::min(body_len_, body_read_size_) + , STATE(stream_body)); + } + } + else if (req_.has_header(HttpHeader::CONTENT_LENGTH) && + std::stol(req_.header(HttpHeader::CONTENT_LENGTH)) > 0) + { + // the POST/PUT request has a payload but unrecognized Content-Type, + // abort the request as we can't process it. + LOG_ERROR("[Httpd fd:%d] Unrecognized Content-Type, aborting!\n%s", fd_ + , req_.to_string().c_str()); + req_.set_status(HttpStatusCode::STATUS_SERVER_ERROR); + return call_immediately(STATE(abort_request_with_response)); + } + } + + return yield_and_call(STATE(process_request_handler)); +} + +StateFlowBase::Action HttpRequestFlow::process_request_handler() +{ + if (server_->have_known_response(req_.uri())) + { + res_ = server_->response(&req_); + } + else + { + auto handler = server_->handler(req_.method(), req_.uri()); + auto res = handler(&req_); + if (res && !res_) + { + res_.reset(res); + } + } + + return yield_and_call(STATE(send_response_headers)); +} + +StateFlowBase::Action HttpRequestFlow::read_more_data() +{ + // we need more data to parse the request + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d] Requesting more data to process request", fd_); + return read_repeated_with_timeout(&helper_, timeout_, fd_, buf_.data() + , header_read_size_, STATE(parse_header_data)); +} + +StateFlowBase::Action HttpRequestFlow::stream_body() +{ + if (helper_.hasError_) + { + return call_immediately(STATE(abort_request)); + } + HASSERT(part_stream_); + size_t data_len = body_read_size_ - helper_.remaining_; + // if we received some data pass it on to the handler + if (data_len) + { + bool abort_req = false; + auto res = part_stream_(&req_, "", body_len_, buf_.data(), data_len + , body_offs_, (body_offs_ + data_len) >= body_len_ + , &abort_req); + body_offs_ += data_len; + if (res && !res_) + { + res_.reset(res); + } + if (abort_req) + { + return call_immediately(STATE(send_response_headers)); + } + } + if (body_offs_ < body_len_) + { + size_t data_req = std::min(body_len_ - body_offs_, body_read_size_); + return read_repeated_with_timeout(&helper_, timeout_, fd_, buf_.data() + , data_req, STATE(stream_body)); + } + return yield_and_call(STATE(send_response_headers)); +} + +StateFlowBase::Action HttpRequestFlow::start_multipart_processing() +{ + // check if the request has the "Expect: 100-continue" header. If it does + // send the 100/continue line so the client starts streaming data. + if (!req_.header(HttpHeader::EXPECT).compare("100-continue")) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL, "[Httpd fd:%d,uri:%s] Sending:%s", fd_ + , req_.uri().c_str(), multipart_res_.c_str()); + return write_repeated(&helper_, fd_, multipart_res_.c_str() + , multipart_res_.length() + , STATE(read_multipart_headers)); + } + return call_immediately(STATE(read_multipart_headers)); +} + +extern std::map well_known_http_headers; +StateFlowBase::Action HttpRequestFlow::parse_multipart_headers() +{ + // https://tools.ietf.org/html/rfc7578 + // https://tools.ietf.org/html/rfc2388 (obsoleted by above) + if (helper_.hasError_) + { + return call_immediately(STATE(abort_request)); + } + else if (helper_.remaining_ == header_read_size_ && buf_.empty()) + { + return yield_and_call(STATE(read_multipart_headers)); + } + + // parse the boundary marker on our first pass through the headers + if (part_boundary_.empty()) + { + string type = req_.header(HttpHeader::CONTENT_TYPE); + if (type.find('=') == string::npos) + { + LOG_ERROR("[Httpd fd:%d] Unable to find multipart/form-data bounary " + "marker, giving up:\n%s", fd_, req_.to_string().c_str()); + req_.set_status(HttpStatusCode::STATUS_BAD_REQUEST); + return call_immediately(STATE(abort_request_with_response)); + } + part_boundary_.assign(type.substr(type.find_last_of('=') + 1)); + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d] multipart/form-data boundary: %s (%zu)" + , fd_, part_boundary_.c_str(), part_boundary_.length()); + part_count_ = 0; + } + + // add any data that has been received to the parse buffer + size_t bytes_received = header_read_size_ - helper_.remaining_; + raw_header_.append((char *)buf_.data(), bytes_received); + buf_.clear(); + + // if we don't have an EOL string in the data yet, get more data + if (raw_header_.find(HTML_EOL) == string::npos) + { + if (raw_header_.length() < config_httpd_max_header_size()) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d] no EOL character yet, reading more data: %zu\n%s" + , fd_, raw_header_.size(), raw_header_.c_str()); + return yield_and_call(STATE(read_multipart_headers)); + } + LOG_ERROR("[Httpd fd:%d] Received %zu bytes without being able to parse " + "headers, aborting.", fd_, raw_header_.length()); + LOG_ERROR("request:%s", req_.to_string().c_str()); + LOG_ERROR(raw_header_.c_str()); + req_.set_status(HttpStatusCode::STATUS_BAD_REQUEST); + return call_immediately(STATE(abort_request_with_response)); + } + + // parse the data we have into delimited lines of header data, this will + // leave some data in the raw_header_ which will need to be pushed back + // into the buf_ after we reach the body segment. + vector lines; + size_t parsed = tokenize(raw_header_, lines, HTML_EOL, false); + // drop whatever has been tokenized so we don't process it again + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] body: %zu, parsed: %zu, header: %zu" + , fd_, req_.uri().c_str(), body_len_, parsed, raw_header_.length()); + raw_header_.erase(0, parsed); + + // reduce the body size by the amount of data we have successfully parsed. + body_len_ -= parsed; + + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] parsed: %zu, body: %zu", fd_, req_.uri().c_str() + , parsed, body_len_); + + // process any remaining lines as headers until we reach a blank line + int processed_lines = 0; + for (auto &line : lines) + { + processed_lines++; + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] line(%zu/%zu): ||%s||" + , fd_, req_.uri().c_str(), processed_lines, lines.size(), line.c_str()); + if (line.empty()) + { + if (found_part_boundary_) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] found blank line, starting body stream" + , fd_, req_.uri().c_str()); + process_part_body_ = true; + // Check if we have processed all parsed lines, if not add them back to + // the buffer for deferred processing. + if (processed_lines != lines.size()) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Processed %zu/%zu lines, adding remaining " + "lines to body payload" + , fd_, req_.uri().c_str(), processed_lines, lines.size()); + // add the unprocessed lines back to the buffer + raw_header_.insert(0, string_join(lines.begin() + processed_lines + , lines.end(), HTML_EOL)); + } + break; + } + else + { + LOG_ERROR("[Httpd fd:%d,uri:%s] Missing part boundary marker, aborting " + "request", fd_, req_.uri().c_str()); + req_.set_status(HttpStatusCode::STATUS_BAD_REQUEST); + return call_immediately(STATE(abort_request_with_response)); + } + } + else if (line.find(part_boundary_) != string::npos) + { + // skip the first two bytes as the line will be: -- + if (found_part_boundary_) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] End of multipart/form-data segment(%d)" + , fd_, req_.uri().c_str(), part_count_); + found_part_boundary_ = false; + if (line.find_last_of("--") == line.length() - 2) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] End of last segment reached" + , fd_, req_.uri().c_str()); + return yield_and_call(STATE(send_response_headers)); + } + } + else + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Start of multipart/form-data segment(%d)" + , fd_, req_.uri().c_str(), part_count_); + found_part_boundary_ = true; + // reset part specific data + part_len_ = 0; + part_offs_ = 0; + part_count_++; + part_filename_.assign(""); + part_type_.assign("text/plain"); + } + } + else if (found_part_boundary_) + { + std::pair parts = break_string(line, ": "); + if (!parts.first.compare( + well_known_http_headers[HttpHeader::CONTENT_TYPE])) + { + part_type_.assign(parts.second); + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] multipart/form-data segment(%d) uses %s:%s" + , fd_, req_.uri().c_str(), part_count_ + , well_known_http_headers[HttpHeader::CONTENT_TYPE].c_str() + , part_type_.c_str()); + } + else if (!parts.first.compare( + well_known_http_headers[HttpHeader::CONTENT_DISPOSITION])) + { + // extract filename if present + vector disposition; + tokenize(parts.second, disposition, "; "); + for (auto &part : disposition) + { + if (part.find("filename") != string::npos) + { + vector file_parts; + tokenize(part, file_parts, "\""); + part_filename_.assign(file_parts[1]); + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] multipart/form-data segment(%d) " + "filename detected as '%s'" + , fd_, req_.uri().c_str(), part_count_, part_filename_.c_str()); + } + else if (part.find("form-data") != string::npos) + { + // ignored field + } + else if (part.find("name") != string::npos) + { + // ignored field (for now) + } + else + { + LOG_ERROR( + "[Httpd fd:%d,uri:%s] Unrecognized field in segment(%d): %s" + , fd_, req_.uri().c_str(), part_count_, part.c_str()); + } + } + } + else + { + LOG_ERROR("[Httpd fd:%d,uri:%s] Received unexpected header in segment, " + "aborting\n%s", fd_, req_.uri().c_str(), line.c_str()); + req_.set_status(HttpStatusCode::STATUS_BAD_REQUEST); + return call_immediately(STATE(abort_request_with_response)); + } + } + } + if (process_part_body_) + { + // if there was some data leftover from the parsing of the headers, + // transfer it back to the pending buffer and start streaming it. + if (!raw_header_.empty()) + { + buf_.clear(); + std::move(raw_header_.begin(), raw_header_.end(), std::back_inserter(buf_)); + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] segment(%d) size %zu/%zu" + , fd_, req_.uri().c_str(), part_count_, buf_.size(), body_len_); + body_len_ += buf_.size(); + raw_header_.assign(""); + } + + // size is expanded to account for "--" and "\r\n", default to expecting a + // single part. If there is leftover body_len_ after we process this chunk + // we will return to this state to get the next part. + part_len_ = body_len_ - (part_boundary_.size() + 4); + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] segment(%d) size %zu/%zu" + , fd_, req_.uri().c_str(), part_count_, part_len_, body_len_); + size_t data_req = std::min(part_len_, body_read_size_ - buf_.size()); + + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Requesting %zu/%zu bytes for segment(%d)" + , fd_, req_.uri().c_str(), data_req, part_len_, part_count_); + // read the payload and process it in chunks + return read_repeated_with_timeout(&helper_, timeout_, fd_ + , buf_.data() + buf_.size(), data_req + , STATE(stream_multipart_body)); + } + return yield_and_call(STATE(read_multipart_headers)); +} + +StateFlowBase::Action HttpRequestFlow::read_multipart_headers() +{ + // If we have data in the buffer already and it is more than the size of + // what we are trying to read, call directly into the parser to clear the + // data. + if (!buf_.empty() && buf_.size() >= header_read_size_) + { + return call_immediately(STATE(parse_multipart_headers)); + } + + // We either have no data or less bytes of data than header_read_size_. + // Request enough data for at least header_read_size_ number of bytes. + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] requesting %zu bytes for multipart/form-data " + "processing", fd_, req_.uri().c_str(), header_read_size_ - buf_.size()); + return read_repeated_with_timeout(&helper_, timeout_, fd_ + , buf_.data() + buf_.size() + , header_read_size_ - buf_.size() + , STATE(parse_multipart_headers)); +} + +StateFlowBase::Action HttpRequestFlow::stream_multipart_body() +{ + if (helper_.hasError_) + { + return call_immediately(STATE(abort_request)); + } + HASSERT(part_stream_); + size_t data_len = body_read_size_ - helper_.remaining_; + if (data_len) + { + if (body_len_ > data_len) + { + body_len_ -= data_len; + } + else + { + body_len_ = 0; + } + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Received %zu/%zu bytes", fd_, req_.uri().c_str() + , part_offs_, part_len_); + bool abort_req = false; + auto res = part_stream_(&req_, part_filename_, part_len_, buf_.data() + , data_len, part_offs_ + , (part_offs_ + data_len) >= part_len_, &abort_req); + part_offs_ += data_len; + if (res && !res_) + { + res_.reset(res); + } + if (abort_req) + { + return yield_and_call(STATE(send_response_headers)); + } + } + // if we didn't receive any data or we need to read more data to reach the + // end of the part, try to retrieve more data. + if (part_offs_ < part_len_) + { + size_t data_req = std::min(part_len_ - part_offs_, body_read_size_); + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Requesting %zu bytes", fd_, req_.uri().c_str() + , data_req); + return read_repeated_with_timeout(&helper_, timeout_, fd_, buf_.data() + , data_req, STATE(stream_multipart_body)); + } + if (body_len_) + { + return yield_and_call(STATE(read_multipart_headers)); + } + + return yield_and_call(STATE(send_response_headers)); +} + +StateFlowBase::Action HttpRequestFlow::read_form_data() +{ + // Request enough data for at least header_read_size_ number of bytes. + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] requesting %zu bytes for form-data processing", fd_ + , req_.uri().c_str(), header_read_size_ - buf_.size()); + return read_repeated_with_timeout(&helper_, timeout_, fd_ + , buf_.data() + buf_.size() + , header_read_size_ - buf_.size() + , STATE(parse_form_data)); +} + +StateFlowBase::Action HttpRequestFlow::parse_form_data() +{ + if (helper_.hasError_) + { + return call_immediately(STATE(abort_request)); + } + else if (helper_.remaining_ == header_read_size_ || buf_.empty()) + { + return yield_and_call(STATE(read_form_data)); + } + + raw_header_.append((char *)buf_.data() + , header_read_size_ - helper_.remaining_); + buf_.clear(); + + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL, "body: %zu, received: %zu", body_len_ + , (header_read_size_ - helper_.remaining_)); + + // track how much we have read in of the body payload + if (body_len_ > (header_read_size_ - helper_.remaining_)) + { + body_len_ -= (header_read_size_ - helper_.remaining_); + } + else + { + body_len_ = 0; + } + + vector params; + size_t parsed = tokenize(raw_header_, params, "&", body_len_ == 0); + + // drop whatever has been tokenized so we don't process it again + raw_header_.erase(0, parsed); + + for (auto param : params) + { + auto p = break_string(param, "="); + + // URL decode the parameter name and value + p.first = url_decode(p.first); + p.second = url_decode(p.second); + + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "param: %s, value: %s", p.first.c_str(), p.second.c_str()); + // add the parameter to the request + req_.param(p); + } + + // If there is more body payload to read request more data before processing + // the request fully. + if (body_len_) + { + size_t data_req = std::min(body_len_, header_read_size_); + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Requesting %zu bytes", fd_, req_.uri().c_str() + , data_req); + return read_repeated_with_timeout(&helper_, timeout_, fd_, buf_.data() + , data_req, STATE(parse_form_data)); + } + + return yield_and_call(STATE(process_request_handler)); +} + +StateFlowBase::Action HttpRequestFlow::send_response_headers() +{ + // when there is no response object created, default to the status code from + // the request. + if (!res_) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] no response body, creating one with status:%d" + , fd_, req_.uri().c_str(), req_.status_); + res_.reset(new AbstractHttpResponse(req_.status_)); + } + size_t len = 0; + bool keep_alive = req_.keep_alive() && + req_count_ < config_httpd_max_req_per_connection(); + uint8_t *payload = res_->get_headers(&len, keep_alive); + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Sending headers using %zu bytes (%d)." + , fd_, req_.uri().c_str(), len, res_->code_); + return write_repeated(&helper_, fd_, payload, len, STATE(send_response_body)); +} + +StateFlowBase::Action HttpRequestFlow::send_response_body() +{ + if (req_.method() == HttpMethod::HEAD) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] HEAD request, no body required.", fd_ + , req_.uri().c_str()); + } + else if (res_->get_body_length()) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Sending body of %d bytes.", fd_ + , req_.uri().c_str(), res_->get_body_length()); + // check if we can send the entire response in one call or not. + if (res_->get_body_length() > config_httpd_response_chunk_size()) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Converting to streamed response.", fd_ + , req_.uri().c_str()); + response_body_offs_ = 0; + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Sending [%d-%d/%d]", fd_ + , req_.uri().c_str(), response_body_offs_ + , config_httpd_response_chunk_size(), res_->get_body_length()); + return write_repeated(&helper_, fd_, res_->get_body() + , config_httpd_response_chunk_size() + , STATE(send_response_body_split)); + } + return write_repeated(&helper_, fd_, res_->get_body() + , res_->get_body_length(), STATE(request_complete)); + } + return yield_and_call(STATE(request_complete)); +} + +StateFlowBase::Action HttpRequestFlow::send_response_body_split() +{ + // check if there has been an error and abort if needed + if (helper_.hasError_) + { + return yield_and_call(STATE(abort_request)); + } + response_body_offs_ += (config_httpd_response_chunk_size() - + helper_.remaining_); + if (response_body_offs_ >= res_->get_body_length()) + { + // the body has been sent fully + return yield_and_call(STATE(request_complete)); + } + size_t remaining = res_->get_body_length() - response_body_offs_; + if (remaining > config_httpd_response_chunk_size()) + { + remaining = config_httpd_response_chunk_size(); + } + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Sending [%d-%d/%d]", fd_, req_.uri().c_str() + , response_body_offs_, response_body_offs_ + remaining + , res_->get_body_length()); + return write_repeated(&helper_, fd_, res_->get_body() + response_body_offs_ + , remaining, STATE(send_response_body_split)); +} + +StateFlowBase::Action HttpRequestFlow::request_complete() +{ +#if CONFIG_HTTP_REQ_FLOW_LOG_LEVEL == VERBOSE + if (!req_.uri().empty()) + { + uint32_t proc_time = USEC_TO_MSEC(esp_timer_get_time() - start_time_); + if (res_->get_body_length()) + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Processed in %d ms (%d, %zu bytes).", fd_ + , req_.uri().c_str(), proc_time, res_->code_, res_->get_body_length()); + } + else + { + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Processed in %d ms (%d).", fd_ + , req_.uri().c_str(), proc_time, res_->code_); + } + } +#endif // CONFIG_HTTP_REQ_FLOW_LOG_LEVEL == VERBOSE + req_count_++; + // If the connection setting is not keep-alive, or there was an error during + // processing, or we have processed more than the configured number of + // requests, or the URI was empty (parse failure?), or the result code is a + // redirect shutdown the socket immediately. FireFox will not follow the + // redirect request if the connection is kept open. + if (!req_.keep_alive() || req_.error() || + req_count_ >= config_httpd_max_req_per_connection() || + req_.uri().empty() || + res_->code_ == HttpStatusCode::STATUS_FOUND || + res_->code_ == HttpStatusCode::STATUS_MOVED_PERMANENTLY) + { + req_.reset(); + HttpRequestFlow *flow = this; + server_->executor()->add(new CallbackExecutable([flow](){delete flow;})); + return exit(); + } + + return call_immediately(STATE(start_request)); +} + +StateFlowBase::Action HttpRequestFlow::upgrade_to_websocket() +{ + // keep the socket open since we will reuse it as the websocket + close_ = false; + LOG(CONFIG_HTTP_REQ_FLOW_LOG_LEVEL + , "[Httpd fd:%d,uri:%s] Upgrading to WebSocket", fd_, req_.uri().c_str()); + new WebSocketFlow(server_, fd_, remote_ip_, req_.header(HttpHeader::WS_KEY) + , req_.header(HttpHeader::WS_VERSION) + , server_->ws_handler(req_.uri())); + req_.reset(); + HttpRequestFlow *flow = this; + server_->executor()->add(new CallbackExecutable([flow](){delete flow;})); + return exit(); +} + +StateFlowBase::Action HttpRequestFlow::abort_request_with_response() +{ + req_.error(true); + return call_immediately(STATE(send_response_headers)); +} + +StateFlowBase::Action HttpRequestFlow::abort_request() +{ + req_.error(true); + return call_immediately(STATE(request_complete)); +} + +} // namespace http + diff --git a/components/Esp32HttpServer/HttpRequestWebSocket.cpp b/components/Esp32HttpServer/HttpRequestWebSocket.cpp new file mode 100644 index 00000000..ec89c161 --- /dev/null +++ b/components/Esp32HttpServer/HttpRequestWebSocket.cpp @@ -0,0 +1,417 @@ +/********************************************************************** +ESP32 HTTP Server + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "Httpd.h" + +#if defined(ESP32) || defined(ESP_IDF_VERSION_MAJOR) +#include +#else +#error unknown platform for mbedTLS +#endif // ESP32 || ESP_IDF_VERSION_MAJOR + +namespace http +{ + +typedef enum +{ + OP_CONTINUATION = 0x0, // Continuation Frame + OP_TEXT = 0x1, // Text Frame + OP_BINARY = 0x2, // Binary Frame + OP_CLOSE = 0x8, // Connection Close Frame + OP_PING = 0x9, // Ping Frame + OP_PONG = 0xA, // Pong Frame +} WebSocketOpcode; + +static constexpr uint8_t WEBSOCKET_FINAL_FRAME = 0x80; +static constexpr uint8_t WEBSOCKET_FRAME_IS_MASKED = 0x80; +static constexpr uint8_t WEBSOCKET_FRAME_LEN_SINGLE = 126; +static constexpr uint8_t WEBSOCKET_FRAME_LEN_UINT16 = 126; +static constexpr uint8_t WEBSOCKET_FRAME_LEN_UINT64 = 127; + +// This is the WebSocket UUID it is used as part of the handshake process. +static constexpr const char * WEBSOCKET_UUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +WebSocketFlow::WebSocketFlow(Httpd *server, int fd, uint32_t remote_ip + , const string &ws_key, const string &ws_version + , WebSocketHandler handler) + : StateFlowBase(server) + , server_(server) + , fd_(fd) + , remote_ip_(remote_ip) + , timeout_(MSEC_TO_NSEC(config_httpd_websocket_timeout_ms())) + , max_frame_size_(config_httpd_websocket_max_frame_size()) + , handler_(handler) +{ + server_->add_websocket(fd, this); + string key_data = ws_key + WEBSOCKET_UUID; + unsigned char key_sha1[20]; + + LOG(CONFIG_HTTP_WS_LOG_LEVEL + , "[WebSocket fd:%d] Connected, starting handshake", fd_); + // SHA1 encode the ws_key plus the websocket UUID, if this fails close the + // socket immediately. + if (!mbedtls_sha1_ret((unsigned char *)key_data.c_str(), key_data.length() + , key_sha1)) + { + AbstractHttpResponse resp(STATUS_SWITCH_PROTOCOL); + resp.header(HttpHeader::CONNECTION, HTTP_CONNECTION_UPGRADE); + resp.header(HttpHeader::UPGRADE, HTTP_UPGRADE_HEADER_WEBSOCKET); + resp.header(HttpHeader::WS_VERSION, ws_version); + resp.header(HttpHeader::WS_ACCEPT + , base64_encode(string((char *)key_sha1, 20))); + handshake_.assign(std::move(resp.to_string())); + + // Allocate buffer for frame data. + data_ = (uint8_t *)malloc(max_frame_size_); + if (data_) + { + start_flow(STATE(send_handshake)); + return; + } + } + LOG_ERROR("[WebSocket fd:%d] Error estabilishing connection, aborting", fd_); + WebSocketFlow *flow = this; + server_->executor()->add(new CallbackExecutable([flow](){delete flow;})); +} + +WebSocketFlow::~WebSocketFlow() +{ + // remove ourselves from the server so we don't get called again + server_->remove_websocket(fd_); + + LOG(CONFIG_HTTP_WS_LOG_LEVEL, "[WebSocket fd:%d] Disconnected", fd_); + ::close(fd_); + + if (data_) + { + free(data_); + } + textToSend_.clear(); +} + +void WebSocketFlow::send_text(string &text) +{ + OSMutexLock l(&textLock_); + textToSend_.append(text); +} + +int WebSocketFlow::id() +{ + return fd_; +} + +uint32_t WebSocketFlow::ip() +{ + return remote_ip_; +} + +void WebSocketFlow::request_close() +{ + close_requested_ = true; +} + +StateFlowBase::Action WebSocketFlow::read_fully_with_timeout(void *buf + , size_t size + , size_t attempts + , StateFlowBase::Callback success + , StateFlowBase::Callback timeout) +{ + LOG(CONFIG_HTTP_WS_LOG_LEVEL + , "[WebSocket fd:%d] requesting %zu bytes", fd_, size); + buf_ = (uint8_t *)buf; + buf_offs_ = 0; + buf_remain_ = size; + buf_next_ = success; + buf_next_timeout_ = timeout; + buf_attempts_ = attempts; + return read_repeated_with_timeout(&helper_, timeout_, fd_, buf_, size + , STATE(data_received)); +} + +StateFlowBase::Action WebSocketFlow::data_received() +{ + HASSERT(buf_next_); + size_t received = (buf_remain_ - helper_.remaining_); + LOG(CONFIG_HTTP_WS_LOG_LEVEL + , "[WebSocket fd:%d] hasError:%d, readFully:%d, readNonblocking:%d, " + "readWithTimeout: %d, remaining:%d, received:%zu" + , fd_, helper_.hasError_, helper_.readFully_, helper_.readNonblocking_ + , helper_.readWithTimeout_, helper_.remaining_, received); + if (helper_.hasError_) + { + LOG(INFO, "[WebSocket fd:%d] read-error, disconnecting", fd_); + return yield_and_call(STATE(shutdown_connection)); + } + buf_remain_ -= received; + buf_offs_ += received; + LOG(CONFIG_HTTP_WS_LOG_LEVEL + , "[WebSocket fd:%d] Received %zu bytes, %zu bytes remain", fd_ + , received, buf_remain_); + if (buf_remain_ && buf_attempts_ > 0) + { + buf_attempts_--; + return read_repeated_with_timeout(&helper_, timeout_, fd_, buf_ + buf_offs_ + , buf_remain_, STATE(data_received)); + } + else if (buf_remain_) + { + return yield_and_call(buf_next_timeout_); + } + return yield_and_call(buf_next_); +} + +StateFlowBase::Action WebSocketFlow::send_handshake() +{ + LOG(CONFIG_HTTP_WS_LOG_LEVEL + , "[WebSocket fd:%d] Sending handshake:\n%s", fd_, handshake_.c_str()); + return write_repeated(&helper_, fd_, handshake_.c_str(), handshake_.length() + , STATE(handshake_sent)); +} + +StateFlowBase::Action WebSocketFlow::handshake_sent() +{ + handshake_.clear(); + if (helper_.hasError_) + { + LOG(INFO, "[WebSocket fd:%d] read-error, disconnecting", fd_); + return yield_and_call(STATE(shutdown_connection)); + } + handler_(this, WebSocketEvent::WS_EVENT_CONNECT, nullptr, 0); + return yield_and_call(STATE(read_frame_header)); +} + +StateFlowBase::Action WebSocketFlow::read_frame_header() +{ + // Check if there has been a request to shutdown the connection + if (close_requested_) + { + return yield_and_call(STATE(shutdown_connection)); + } + // reset frame state to defaults + header_ = 0; + opcode_ = 0; + frameLenType_ = 0; + frameLength_ = 0; + maskingKey_ = 0; + bzero(data_, max_frame_size_); + LOG(CONFIG_HTTP_WS_LOG_LEVEL, "[WebSocket fd:%d] Reading WS packet", fd_); + return read_fully_with_timeout(&header_, sizeof(uint16_t) + , config_httpd_websocket_max_read_attempts() + , STATE(frame_header_received) + , STATE(send_frame_header)); +} + +StateFlowBase::Action WebSocketFlow::frame_header_received() +{ + opcode_ = static_cast(header_ & 0x0F); + masked_ = ((header_ >> 8) & WEBSOCKET_FRAME_IS_MASKED); + uint8_t len = ((header_ >> 8) & 0x7F); + LOG(CONFIG_HTTP_WS_LOG_LEVEL + , "[WebSocket fd:%d] opc: %d, masked: %d, len: %d", fd_, opcode_, masked_ + , len); + if (len < WEBSOCKET_FRAME_LEN_SINGLE) + { + frameLenType_ = 0; + frameLength_ = len; + return call_immediately(STATE(frame_data_len_received)); + } + else if (len == WEBSOCKET_FRAME_LEN_UINT16) + { + frameLenType_ = 1; + frameLength_ = 0; + // retrieve the payload length as a 16 bit number + return read_fully_with_timeout(&frameLength16_, sizeof(uint16_t) + , config_httpd_websocket_max_read_attempts() + , STATE(frame_data_len_received) + , STATE(shutdown_connection)); + } + else if (len == WEBSOCKET_FRAME_LEN_UINT64) + { + frameLenType_ = 2; + // retrieve the payload length as a 64 bit number + return read_fully_with_timeout(&frameLength_, sizeof(uint64_t) + , config_httpd_websocket_max_read_attempts() + , STATE(frame_data_len_received) + , STATE(shutdown_connection)); + } + + // if we get here the frame is malformed, shutdown the connection + return yield_and_call(STATE(shutdown_connection)); +} + +StateFlowBase::Action WebSocketFlow::frame_data_len_received() +{ + if (frameLenType_ == 1) + { + // byte swap frameLength16_ into frameLength_ + frameLength_ = (frameLength16_ << 8) | (frameLength16_ >> 8); + } + else if (frameLenType_ == 2) + { + // byte swap frameLength_ (64 bit) + uint8_t *p = (uint8_t *)frameLength_; + uint64_t temp = p[7] | (uint16_t)(p[6]) << 8 + | (uint32_t)(p[5]) << 16 | (uint32_t)(p[4]) << 24 + | (uint64_t)(p[3]) << 32 | (uint64_t)(p[2]) << 40 + | (uint64_t)(p[1]) << 48 | (uint64_t)(p[0]) << 56; + frameLength_ = temp; + } + + if (masked_) + { + // frame uses data masking, read the mask and then start reading the + // frame payload + return read_fully_with_timeout(&maskingKey_, sizeof(uint32_t) + , config_httpd_websocket_max_read_attempts() + , STATE(start_recv_frame_data) + , STATE(shutdown_connection)); + } + // no masking, start reading the frame data + return yield_and_call(STATE(start_recv_frame_data)); +} + +StateFlowBase::Action WebSocketFlow::start_recv_frame_data() +{ + LOG(CONFIG_HTTP_WS_LOG_LEVEL, "[WebSocket fd:%d] Reading WS packet (%d len)" + , fd_, (int)frameLength_); + // restrict the size of the fame buffer so we don't use all of the ram for + // one frame. + data_size_ = std::min(frameLength_, max_frame_size_); + return read_fully_with_timeout(data_, data_size_ + , config_httpd_websocket_max_read_attempts() + , STATE(recv_frame_data) + , STATE(send_frame_header)); +} + +StateFlowBase::Action WebSocketFlow::recv_frame_data() +{ + size_t received_len = data_size_ - helper_.remaining_; + if (received_len) + { + LOG(CONFIG_HTTP_WS_LOG_LEVEL + , "[WebSocket fd:%d] Received %zu bytes", fd_, received_len); + if (masked_) + { + uint8_t *mask = reinterpret_cast(&maskingKey_); + char buf[10]; + LOG(CONFIG_HTTP_WS_LOG_LEVEL + , "[WebSocket fd:%d] Demasking %zu bytes (mask: %s)", fd_, received_len + , unsigned_integer_to_buffer_hex(maskingKey_, buf)); + for (size_t idx = 0; idx < received_len; idx++) + { + data_[idx] ^= mask[idx % 4]; + } + } + if (opcode_ == OP_PING) + { + // send PONG + } + else if (opcode_ == OP_TEXT) + { + handler_(this, WebSocketEvent::WS_EVENT_TEXT, data_, received_len); + } + else if (opcode_ == OP_BINARY) + { + handler_(this, WebSocketEvent::WS_EVENT_BINARY, data_, received_len); + } + if (opcode_ == OP_CLOSE || close_requested_) + { + return yield_and_call(STATE(shutdown_connection)); + } + } + frameLength_ -= received_len; + if (frameLength_) + { + data_size_ = std::min(frameLength_, max_frame_size_); + return read_fully_with_timeout(data_, data_size_ + , config_httpd_websocket_max_read_attempts() + , STATE(recv_frame_data) + , STATE(shutdown_connection)); + } + return yield_and_call(STATE(read_frame_header)); +} + +StateFlowBase::Action WebSocketFlow::shutdown_connection() +{ + handler_(this, WebSocketEvent::WS_EVENT_DISCONNECT, nullptr, 0); + WebSocketFlow *flow = this; + server_->executor()->add(new CallbackExecutable([flow](){delete flow;})); + return exit(); +} + +StateFlowBase::Action WebSocketFlow::send_frame_header() +{ + // TODO: add binary message sending support + OSMutexLock l(&textLock_); + if (textToSend_.empty()) + { + return yield_and_call(STATE(read_frame_header)); + } + bzero(data_, max_frame_size_); + size_t send_size = 0; + if (textToSend_.length() < WEBSOCKET_FRAME_LEN_SINGLE) + { + data_[0] = WEBSOCKET_FINAL_FRAME | OP_TEXT; + data_[1] = textToSend_.length(); + memcpy(data_ + 2, textToSend_.data(), textToSend_.length()); + data_size_ = textToSend_.length(); + send_size = data_size_ + 2; + } + else if (textToSend_.length() < max_frame_size_ - 4) + { + data_size_ = textToSend_.length(); + data_[0] = WEBSOCKET_FINAL_FRAME | OP_TEXT; + data_[1] = WEBSOCKET_FRAME_LEN_UINT16; + data_[2] = data_size_ & 0xFF; + data_[3] = (data_size_ >> 8) & 0xFF; + memcpy(data_+ 4, textToSend_.data(), textToSend_.length()); + send_size = data_size_ + 4; + } + else + { + // size is greater than our max frame, send it as fragments + data_size_ = max_frame_size_ - 4; + data_[0] = OP_CONTINUATION; + data_[1] = WEBSOCKET_FRAME_LEN_UINT16; + data_[2] = data_size_ & 0xFF; + data_[3] = (data_size_ >> 8) & 0xFF; + memcpy(data_+ 4, textToSend_.data(), data_size_); + send_size = data_size_ + 4; + } + LOG(CONFIG_HTTP_WS_LOG_LEVEL + , "[WebSocket fd:%d] send:%zu, text:%zu", fd_, send_size + , textToSend_.length()); + return write_repeated(&helper_, fd_, data_, send_size, STATE(frame_sent)); +} + +StateFlowBase::Action WebSocketFlow::frame_sent() +{ + if (helper_.hasError_) + { + LOG(WARNING, "[WebSocket fd:%d] write error, disconnecting", fd_); + return yield_and_call(STATE(shutdown_connection)); + } + OSMutexLock l(&textLock_); + textToSend_.erase(0, data_size_); + if (textToSend_.empty()) + { + return yield_and_call(STATE(read_frame_header)); + } + return yield_and_call(STATE(send_frame_header)); +} + +} // namespace http diff --git a/components/Esp32HttpServer/HttpResponse.cpp b/components/Esp32HttpServer/HttpResponse.cpp new file mode 100644 index 00000000..fb0823b5 --- /dev/null +++ b/components/Esp32HttpServer/HttpResponse.cpp @@ -0,0 +1,192 @@ +/********************************************************************** +ESP32 HTTP Server + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "Httpd.h" + +namespace http +{ + +extern string HTTP_BUILD_TIME; + +extern std::map well_known_http_headers; + +static std::map http_code_strings = +{ + {STATUS_CONTINUE, "Continue"}, + {STATUS_SWITCH_PROTOCOL, "Switching Protocols"}, + + {STATUS_OK, "OK"}, + {STATUS_CREATED, "Created"}, + {STATUS_ACCEPTED, "Accepted"}, + {STATUS_NON_AUTH_INFO, "Non-Authoritative Information"}, + {STATUS_NO_CONTENT, "No Content"}, + {STATUS_RESET_CONTENT, "Reset Content"}, + {STATUS_PARTIAL_CONTENT, "Partial Content"}, + + {STATUS_MULTIPLE_CHOICES, "Multiple Choices"}, + {STATUS_MOVED_PERMANENTLY, "Moved Permanently"}, + {STATUS_FOUND, "Found"}, + {STATUS_SEE_OTHER, "See Other"}, + {STATUS_NOT_MODIFIED, "Not Modified"}, + {STATUS_USE_PROXY, "Use Proxy"}, + {STATUS_TEMP_REDIRECT, "Temporary Redirect"}, + + {STATUS_BAD_REQUEST, "Bad Request"}, + {STATUS_NOT_AUTHORIZED, "Unauthorized"}, + {STATUS_PAYMENT_REQUIRED, "Payment Required"}, + {STATUS_FORBIDDEN, "Forbidden"}, + {STATUS_NOT_FOUND, "Not Found"}, + {STATUS_NOT_ALLOWED, "Method Not Allowed"}, + {STATUS_NOT_ACCEPTABLE, "Not Acceptable"}, + {STATUS_PROXY_AUTH_REQ, "Proxy Authentication Required"}, + {STATUS_TIMEOUT, "Request Time-out"}, + {STATUS_CONFLICT, "Conflict"}, + {STATUS_GONE, "Gone"}, + {STATUS_LENGTH_REQ, "Length Required"}, + {STATUS_PRECOND_FAIL, "Precondition Failed"}, + {STATUS_ENTITY_TOO_LARGE, "Request Entity Too Large"}, + {STATUS_URI_TOO_LARGE, "Request-URI Too Large"}, + {STATUS_UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type"}, + {STATUS_RANGE_NOT_SATISFIABLE, "Requested range not satisfiable"}, + {STATUS_EXPECATION_FAILED, "Expectation Failed"}, + + {STATUS_SERVER_ERROR, "Internal Server Error"}, + {STATUS_NOT_IMPLEMENTED, "Not Implemented"}, + {STATUS_BAD_GATEWAY, "Bad Gateway"}, + {STATUS_SERVICE_UNAVAILABLE, "Service Unavailable"}, + {STATUS_GATEWAY_TIMEOUT, "Gateway Time-out"}, + {STATUS_HTTP_VERSION_UNSUPPORTED, "HTTP Version not supported"} +}; + +AbstractHttpResponse::AbstractHttpResponse(HttpStatusCode code + , const string &mime_type) + : code_(code), mime_type_(mime_type) + , encoded_headers_("") +{ + // seed default headers + header(HttpHeader::CACHE_CONTROL, HTTP_CACHE_CONTROL_NO_CACHE); +} + +AbstractHttpResponse::~AbstractHttpResponse() +{ + encoded_headers_.clear(); + headers_.clear(); +} + +uint8_t *AbstractHttpResponse::get_headers(size_t *len, bool keep_alive + , bool add_keep_alive) +{ + to_string(false, keep_alive, add_keep_alive); + *len = encoded_headers_.size(); + return (uint8_t *)encoded_headers_.c_str(); +} + +string AbstractHttpResponse::to_string(bool include_body, bool keep_alive + , bool add_keep_alive) +{ + encoded_headers_.assign(StringPrintf("HTTP/1.1 %d %s%s", code_ + , http_code_strings[code_].c_str() + , HTML_EOL)); + for (auto &ent : headers_) + { + LOG(CONFIG_HTTP_RESP_LOG_LEVEL, "[resp-header] %s -> %s", ent.first.c_str() + , ent.second.c_str()); + encoded_headers_.append( + StringPrintf("%s: %s%s", ent.first.c_str(), ent.second.c_str() + , HTML_EOL)); + } + + if (get_body_length()) + { + LOG(CONFIG_HTTP_RESP_LOG_LEVEL, "[resp-header] %s -> %zu" + , well_known_http_headers[HttpHeader::CONTENT_LENGTH].c_str() + , get_body_length()); + encoded_headers_.append( + StringPrintf("%s: %zu%s" + , well_known_http_headers[HttpHeader::CONTENT_LENGTH].c_str() + , get_body_length(), HTML_EOL)); + LOG(CONFIG_HTTP_RESP_LOG_LEVEL, "[resp-header] %s -> %s" + , well_known_http_headers[HttpHeader::CONTENT_TYPE].c_str() + , get_body_mime_type().c_str()); + encoded_headers_.append( + StringPrintf("%s: %s%s" + , well_known_http_headers[HttpHeader::CONTENT_TYPE].c_str() + , get_body_mime_type().c_str(), HTML_EOL)); + } + + if (add_keep_alive) + { + string connection = keep_alive ? HTTP_CONNECTION_CLOSE + : HTTP_CONNECTION_KEEP_ALIVE; + LOG(CONFIG_HTTP_RESP_LOG_LEVEL, "[resp-header] %s -> %s" + , well_known_http_headers[HttpHeader::CONNECTION].c_str() + , connection.c_str()); + encoded_headers_.append( + StringPrintf("%s: %s%s" + , well_known_http_headers[HttpHeader::CONNECTION].c_str() + , connection.c_str(), HTML_EOL)); + } + + // leave blank line after headers before the body + encoded_headers_.append(HTML_EOL); + + return encoded_headers_; +} + +void AbstractHttpResponse::header(const string &header, const string &value) +{ + headers_[header] = std::move(value); +} + +void AbstractHttpResponse::header(const HttpHeader header, const string &value) +{ + headers_[well_known_http_headers[header]] = std::move(value); +} + +RedirectResponse::RedirectResponse(const string &target_uri) + : AbstractHttpResponse(HttpStatusCode::STATUS_FOUND) +{ + header(HttpHeader::LOCATION, target_uri); +} + +StringResponse::StringResponse(const string &response, const string &mime_type) + : AbstractHttpResponse(HttpStatusCode::STATUS_OK, mime_type) + , response_(std::move(response)) +{ +} + +StaticResponse::StaticResponse(const uint8_t *payload, const size_t length + , const std::string mime_type + , const std::string encoding) + : AbstractHttpResponse(STATUS_OK, mime_type) + , payload_(payload), length_(length) +{ + if (!encoding.empty()) + { + header(HttpHeader::CONTENT_ENCODING, encoding); + } + header(HttpHeader::LAST_MODIFIED, HTTP_BUILD_TIME); + // update the default cache strategy to set the must-revalidate and max-age + header(HttpHeader::CACHE_CONTROL + , StringPrintf("%s, %s, %s=%d" + , HTTP_CACHE_CONTROL_NO_CACHE + , HTTP_CACHE_CONTROL_MUST_REVALIDATE + , HTTP_CACHE_CONTROL_MAX_AGE + , config_httpd_cache_max_age_sec())); +} + +} // namespace http diff --git a/components/Esp32HttpServer/HttpServer.cpp b/components/Esp32HttpServer/HttpServer.cpp new file mode 100644 index 00000000..78dccc7f --- /dev/null +++ b/components/Esp32HttpServer/HttpServer.cpp @@ -0,0 +1,401 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2019 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "Httpd.h" + +#if defined(ESP32) || defined(ESP_IDF_VERSION_MAJOR) + +#include +#include + +// this method is not exposed via the MDNS class today, declare it here so we +// can call it if needed. This is implemented inside Esp32WiFiManager.cxx. +void mdns_unpublish(const char *service); + +#endif // ESP32 || ESP_IDF_VERSION_MAJOR + +namespace http +{ + +string HTTP_BUILD_TIME = __DATE__ " " __TIME__; + +/// Callback for a newly accepted socket connection. +/// +/// @param fd is the socket handle. +void incoming_http_connection(int fd) +{ + Singleton::instance()->new_connection(fd); +} + +Httpd::Httpd(MDNS *mdns, uint16_t port, const string &name, const string service_name) + : Service(&executor_) + , name_(name) + , mdns_(mdns) + , mdns_service_(service_name) + , executor_(name.c_str(), config_httpd_server_priority() + , config_httpd_server_stack_size()) + , port_(port) +{ + // pre-initialize the timeout parameters for all sockets that are accepted + socket_timeout_.tv_sec = 0; + socket_timeout_.tv_usec = MSEC_TO_USEC(config_httpd_socket_timeout_ms()); + +#if defined(ESP32) || defined(ESP_IDF_VERSION_MAJOR) + // Hook into the Esp32WiFiManager to start/stop the listener automatically + // based on the AP/Station interface status. + Singleton::instance()->add_event_callback( + [&](system_event_t *event) + { + if (event->event_id == SYSTEM_EVENT_STA_GOT_IP || + event->event_id == SYSTEM_EVENT_AP_START) + { + // If it is the SoftAP interface, start the dns server + if (event->event_id == SYSTEM_EVENT_AP_START) + { + tcpip_adapter_ip_info_t ip_info; + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip_info); + start_dns_listener(ntohl(ip_info.ip.addr)); + } + start_http_listener(); + } + else if (event->event_id == SYSTEM_EVENT_STA_LOST_IP || + event->event_id == SYSTEM_EVENT_AP_STOP) + { + stop_http_listener(); + stop_dns_listener(); + } + }); +#endif // ESP32 || ESP_IDF_VERSION_MAJOR +} + +Httpd::~Httpd() +{ + stop_http_listener(); + stop_dns_listener(); + executor_.shutdown(); + handlers_.clear(); + static_uris_.clear(); + redirect_uris_.clear(); + websocket_uris_.clear(); +} + +void Httpd::uri(const std::string &uri, const size_t method_mask + , RequestProcessor handler, StreamProcessor stream_handler) +{ + handlers_.insert( + std::make_pair(uri, std::make_pair(method_mask, std::move(handler)))); + if (stream_handler) + { + stream_handlers_.insert(std::make_pair(uri, std::move(stream_handler))); + } +} + +void Httpd::uri(const std::string &uri, RequestProcessor handler) +{ + this->uri(uri, 0xFFFF, handler, nullptr); +} + +void Httpd::redirect_uri(const string &source, const string &target) +{ + redirect_uris_.insert( + std::make_pair(std::move(source) + , std::make_shared(target))); +} + +void Httpd::static_uri(const string &uri, const uint8_t *payload + , const size_t length, const string &mime_type + , const string &encoding) +{ + static_uris_.insert( + std::make_pair(uri + , std::make_shared(payload, length, mime_type + , encoding))); + static_cached_.insert( + std::make_pair(uri + , std::make_shared( + HttpStatusCode::STATUS_NOT_MODIFIED))); +} + +void Httpd::websocket_uri(const string &uri, WebSocketHandler handler) +{ + websocket_uris_.insert(std::make_pair(std::move(uri), std::move(handler))); +} + +void Httpd::send_websocket_binary(int id, uint8_t *data, size_t len) +{ + OSMutexLock l(&websocketsLock_); + if (!websockets_.count(id)) + { + LOG_ERROR("[Httpd] Attempt to send data to unknown websocket:%d, " + "discarding.", id); + return; + } + //websockets_[id]->send_binary(data, len); +} + +void Httpd::send_websocket_text(int id, std::string &text) +{ + OSMutexLock l(&websocketsLock_); + if (!websockets_.count(id)) + { + LOG_ERROR("[Httpd] Attempt to send text to unknown websocket:%d, " + "discarding text.", id); + return; + } + websockets_[id]->send_text(text); +} + +void Httpd::broadcast_websocket_text(std::string &text) +{ + OSMutexLock l(&websocketsLock_); + for (auto &client : websockets_) + { + client.second->send_text(text); + } +} + +void Httpd::new_connection(int fd) +{ + sockaddr_in source; + socklen_t source_len = sizeof(sockaddr_in); + if (getpeername(fd, (sockaddr *)&source, &source_len)) + { + source.sin_addr.s_addr = 0; + } + LOG(CONFIG_HTTP_SERVER_LOG_LEVEL, "[%s fd:%d/%s] Connected", name_.c_str() + , fd, ipv4_to_string(ntohl(source.sin_addr.s_addr)).c_str()); + // Set socket receive timeout + ERRNOCHECK("setsockopt_recv_timeout", + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &socket_timeout_ + , sizeof(struct timeval))); + + // Set socket send timeout + ERRNOCHECK("setsockopt_send_timeout", + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &socket_timeout_ + , sizeof(struct timeval))); + + // Reconfigure the socket for non-blocking operations + ::fcntl(fd, F_SETFL, O_RDWR | O_NONBLOCK); + + // Start the HTTP processing on this new socket + new HttpRequestFlow(this, fd, ntohl(source.sin_addr.s_addr)); +} + +void Httpd::captive_portal(string first_access_response + , string auth_uri, uint64_t auth_timeout) +{ + captive_response_.assign(std::move(first_access_response)); + captive_auth_uri_.assign(std::move(auth_uri)); + captive_timeout_ = auth_timeout; + captive_active_ = true; +} + +void Httpd::start_http_listener() +{ + if (http_active_) + { + return; + } + LOG(INFO, "[%s] Starting HTTP listener on port %d", name_.c_str(), port_); + listener_.emplace(port_, incoming_http_connection, "HttpSocket"); + http_active_ = true; + if (mdns_) + { + mdns_->publish(name_.c_str(), mdns_service_.c_str(), port_); + } +} + +void Httpd::start_dns_listener(uint32_t ip) +{ + if (dns_active_) + { + return; + } + LOG(INFO, "[%s] Starting DNS listener", name_.c_str()); + dns_.emplace(ip); + dns_active_ = true; +} + +void Httpd::stop_http_listener() +{ + if (http_active_) + { + LOG(INFO, "[%s] Shutting down HTTP listener", name_.c_str()); + listener_.reset(); + http_active_ = false; +#ifdef ESP32 + if (mdns_) + { + mdns_unpublish(mdns_service_.c_str()); + } +#endif + OSMutexLock l(&websocketsLock_); + for (auto &client : websockets_) + { + client.second->request_close(); + } + } +} + +void Httpd::stop_dns_listener() +{ + if (dns_active_) + { + LOG(INFO, "[%s] Shutting down HTTP listener", name_.c_str()); + dns_.reset(); + dns_active_ = false; + } +} + +void Httpd::add_websocket(int id, WebSocketFlow *ws) +{ + OSMutexLock l(&websocketsLock_); + websockets_[id] = ws; +} + +void Httpd::remove_websocket(int id) +{ + OSMutexLock l(&websocketsLock_); + websockets_.erase(id); +} + +bool Httpd::have_known_response(const string &uri) +{ + return static_uris_.count(uri) || redirect_uris_.count(uri); +} + +std::shared_ptr Httpd::response(HttpRequest *request) +{ + if (static_uris_.count(request->uri())) + { + if (request->has_header(HttpHeader::IF_MODIFIED_SINCE) && + !request->header(HttpHeader::IF_MODIFIED_SINCE).compare(HTTP_BUILD_TIME)) + { + return static_cached_[request->uri()]; + } + return static_uris_[request->uri()]; + } + else if (redirect_uris_.count(request->uri())) + { + return redirect_uris_[request->uri()]; + } + return nullptr; +} + +bool Httpd::is_request_too_large(HttpRequest *req) +{ + HASSERT(req); + + // if the request doesn't have the content-length header we can try to + // process it but it likely will fail. + if (req->has_header(HttpHeader::CONTENT_LENGTH)) + { + uint32_t len = std::stoul(req->header(HttpHeader::CONTENT_LENGTH)); + if (len > config_httpd_max_req_size()) + { + LOG_ERROR("[Httpd uri:%s] Request body too large %d > %d!" + , req->uri().c_str(), len, config_httpd_max_req_size()); + // request size too big + return true; + } + } +#if CONFIG_HTTP_SERVER_LOG_LEVEL == VERBOSE + else + { + LOG(CONFIG_HTTP_SERVER_LOG_LEVEL + , "[Httpd] Request does not have Content-Length header:\n%s" + , req->to_string().c_str()); + } +#endif + + return false; +} + +bool Httpd::is_servicable_uri(HttpRequest *req) +{ + HASSERT(req); + + // check if it is a GET of a known URI or if it is a Websocket URI + if ((req->method() == HttpMethod::GET && have_known_response(req->uri())) || + websocket_uris_.count(req->uri())) + { + LOG(CONFIG_HTTP_SERVER_LOG_LEVEL, "[Httpd uri:%s] Known GET URI" + , req->uri().c_str()); + return true; + } + + // check if it is a POST/PUT and there is a body payload + if (req->has_header(HttpHeader::CONTENT_LENGTH) && + std::stoi(req->header(HttpHeader::CONTENT_LENGTH)) && + (req->method() == HttpMethod::POST || + req->method() == HttpMethod::PUT) && + req->content_type() == ContentType::MULTIPART_FORMDATA) + { + auto processor = stream_handler(req->uri()); + LOG(CONFIG_HTTP_SERVER_LOG_LEVEL + , "[Httpd uri:%s] POST/PUT request, streamproc found: %d" + , req->uri().c_str(), processor != nullptr); + return processor != nullptr; + } + + // Check if we have a handler for the provided URI + auto processor = handler(req->method(), req->uri()); + LOG(CONFIG_HTTP_SERVER_LOG_LEVEL, "[Httpd uri:%s] method: %s, proc: %d" + , req->raw_method().c_str(), req->uri().c_str(), processor != nullptr); + return processor != nullptr; +} + +RequestProcessor Httpd::handler(HttpMethod method, const std::string &uri) +{ + LOG(CONFIG_HTTP_SERVER_LOG_LEVEL, "[Httpd uri:%s] Searching for URI handler" + , uri.c_str()); + if (handlers_.count(uri)) + { + auto handler = handlers_[uri]; + if (handler.first & method) + { + return handler.second; + } + } + LOG(CONFIG_HTTP_SERVER_LOG_LEVEL, "[Httpd uri:%s] No suitable handler found" + , uri.c_str()); + return nullptr; +} + +StreamProcessor Httpd::stream_handler(const std::string &uri) +{ + LOG(CONFIG_HTTP_SERVER_LOG_LEVEL + , "[Httpd uri:%s] Searching for URI stream handler", uri.c_str()); + if (stream_handlers_.count(uri)) + { + return stream_handlers_[uri]; + } + LOG(CONFIG_HTTP_SERVER_LOG_LEVEL + , "[Httpd uri:%s] No suitable stream handler found", uri.c_str()); + return nullptr; +} + +WebSocketHandler Httpd::ws_handler(const string &uri) +{ + if (websocket_uris_.count(uri)) + { + return websocket_uris_[uri]; + } + return nullptr; +} + +} // namespace http diff --git a/components/Esp32HttpServer/HttpdConstants.cpp b/components/Esp32HttpServer/HttpdConstants.cpp new file mode 100644 index 00000000..c898f49b --- /dev/null +++ b/components/Esp32HttpServer/HttpdConstants.cpp @@ -0,0 +1,49 @@ +/********************************************************************** +ESP32 HTTP Server + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include + +namespace http +{ + +/////////////////////////////////////////////////////////////////////////////// +// Httpd constants +/////////////////////////////////////////////////////////////////////////////// + +DEFAULT_CONST(httpd_server_stack_size, 6144); +DEFAULT_CONST(httpd_server_priority, 0); +DEFAULT_CONST(httpd_header_chunk_size, 512); +DEFAULT_CONST(httpd_body_chunk_size, 3072); +DEFAULT_CONST(httpd_response_chunk_size, 2048); +DEFAULT_CONST(httpd_max_header_size, 1024); +DEFAULT_CONST(httpd_max_req_size, 4194304); +DEFAULT_CONST(httpd_max_req_per_connection, 5); +DEFAULT_CONST(httpd_req_timeout_ms, 5); +DEFAULT_CONST(httpd_socket_timeout_ms, 50); +DEFAULT_CONST(httpd_websocket_timeout_ms, 200); +DEFAULT_CONST(httpd_websocket_max_frame_size, 256); +DEFAULT_CONST(httpd_websocket_max_read_attempts, 2); +DEFAULT_CONST(httpd_cache_max_age_sec, 300); + +/////////////////////////////////////////////////////////////////////////////// +// Dnsd constants +/////////////////////////////////////////////////////////////////////////////// + +DEFAULT_CONST(dnsd_stack_size, 3072); +DEFAULT_CONST(dnsd_buffer_size, 512); + +} // namespace http \ No newline at end of file diff --git a/components/Esp32HttpServer/Kconfig.projbuild b/components/Esp32HttpServer/Kconfig.projbuild new file mode 100644 index 00000000..41df2005 --- /dev/null +++ b/components/Esp32HttpServer/Kconfig.projbuild @@ -0,0 +1,98 @@ +# +# Log level constants from from components/OpenMRNLite/src/utils/logging.h +# +# ALWAYS : -1 +# FATAL : 0 +# LEVEL_ERROR : 1 +# WARNING : 2 +# INFO : 3 +# VERBOSE : 4 +# +# Note that FATAL will cause the MCU to reboot! + +menu "HTTP/DNS Server" + choice HTTP_DNS_LOGGING + bool "DNS logging" + default HTTP_DNS_LOGGING_MINIMAL + config HTTP_DNS_LOGGING_VERBOSE + bool "Verbose" + config HTTP_DNS_LOGGING_MINIMAL + bool "Minimal" + endchoice + config HTTP_DNS_LOG_LEVEL + int + default 4 if HTTP_DNS_LOGGING_MINIMAL + default 3 if HTTP_DNS_LOGGING_VERBOSE + default 5 + + choice HTTP_SERVER_LOGGING + bool "HttpRequest logging" + default HTTP_SERVER_LOGGING_MINIMAL + config HTTP_SERVER_LOGGING_VERBOSE + bool "Verbose" + config HTTP_SERVER_LOGGING_MINIMAL + bool "Minimal" + endchoice + config HTTP_SERVER_LOG_LEVEL + int + default 4 if HTTP_SERVER_LOGGING_MINIMAL + default 3 if HTTP_SERVER_LOGGING_VERBOSE + default 5 + + choice HTTP_REQ_LOGGING + bool "HttpRequest logging" + default HTTP_REQ_LOGGING_MINIMAL + config HTTP_REQ_LOGGING_VERBOSE + bool "Verbose" + config HTTP_REQ_LOGGING_MINIMAL + bool "Minimal" + endchoice + config HTTP_REQ_LOG_LEVEL + int + default 4 if HTTP_REQ_LOGGING_MINIMAL + default 3 if HTTP_REQ_LOGGING_VERBOSE + default 5 + + choice HTTP_RESP_LOGGING + bool "HttpRequest logging" + default HTTP_RESP_LOGGING_MINIMAL + config HTTP_RESP_LOGGING_VERBOSE + bool "Verbose" + config HTTP_RESP_LOGGING_MINIMAL + bool "Minimal" + endchoice + config HTTP_RESP_LOG_LEVEL + int + default 4 if HTTP_RESP_LOGGING_MINIMAL + default 3 if HTTP_RESP_LOGGING_VERBOSE + default 5 + + choice HTTP_REQ_FLOW_LOGGING + bool "HttpRequestFlow logging" + default HTTP_REQ_FLOW_LOGGING_MINIMAL + config HTTP_REQ_FLOW_LOGGING_VERBOSE + bool "Verbose" + config HTTP_REQ_FLOW_LOGGING_MINIMAL + bool "Minimal" + endchoice + config HTTP_REQ_FLOW_LOG_LEVEL + int + default 4 if HTTP_REQ_FLOW_LOGGING_MINIMAL + default 3 if HTTP_REQ_FLOW_LOGGING_VERBOSE + default 5 + + choice HTTP_WS_LOGGING + bool "WebSocket logging" + default HTTP_WS_LOGGING_MINIMAL + config HTTP_WS_LOGGING_VERBOSE + bool "Verbose" + config HTTP_WS_LOGGING_MINIMAL + bool "Minimal" + endchoice + config HTTP_WS_LOG_LEVEL + int + default 4 if HTTP_WS_LOGGING_MINIMAL + default 3 if HTTP_WS_LOGGING_VERBOSE + default 5 + +endmenu \ No newline at end of file diff --git a/components/Esp32HttpServer/include/Dnsd.h b/components/Esp32HttpServer/include/Dnsd.h new file mode 100644 index 00000000..3943c6ba --- /dev/null +++ b/components/Esp32HttpServer/include/Dnsd.h @@ -0,0 +1,84 @@ +/********************************************************************** +ESP32 HTTP Server + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef DNSD_H_ +#define DNSD_H_ + +#include +#include +#include +#include + +namespace http +{ + +DECLARE_CONST(dnsd_stack_size); +DECLARE_CONST(dnsd_buffer_size); + +static constexpr uint16_t DEFAULT_DNS_PORT = 53; + +class Dnsd : public Singleton +{ +public: + Dnsd(uint32_t local_ip, std::string name = "dnsd" + , uint16_t port = DEFAULT_DNS_PORT); + ~Dnsd(); + void dns_process_thread(); +private: + uint32_t local_ip_; + std::string name_; + uint16_t port_; + OSThread dns_thread_; + bool shutdown_{false}; + bool shutdownComplete_{false}; + + struct DNSHeader + { + uint16_t id; // identification number + union { + struct { + uint16_t rd : 1; // recursion desired + uint16_t tc : 1; // truncated message + uint16_t aa : 1; // authoritive answer + uint16_t opc : 4; // message_type + uint16_t qr : 1; // query/response flag + uint16_t rc : 4; // response code + uint16_t z : 3; // its z! reserved + uint16_t ra : 1; // recursion available + }; + uint16_t flags; + }; + uint16_t questions; // number of question entries + uint16_t answers; // number of answer entries + uint16_t authorties; // number of authority entries + uint16_t resources; // number of resource entries + } __attribute__((packed)); + + struct DNSResponse + { + uint16_t id; + uint16_t answer; + uint16_t classes; + uint32_t ttl; + uint16_t length; + uint32_t address; + } __attribute__((packed)); +}; + +} // namespace http + +#endif // DNSD_H_ diff --git a/components/Esp32HttpServer/include/HttpStringUtils.h b/components/Esp32HttpServer/include/HttpStringUtils.h new file mode 100644 index 00000000..fea09c39 --- /dev/null +++ b/components/Esp32HttpServer/include/HttpStringUtils.h @@ -0,0 +1,217 @@ +/********************************************************************** +ESP32 HTTP Server + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +/// @file std::string utility methods. + +#ifndef STRINGUTILS_H_ +#define STRINGUTILS_H_ + +#include +#include + +namespace http +{ + +using std::string; +using std::vector; +using std::pair; +using std::make_pair; + +/// Helper method to break a string into a pair based on a delimeter. +/// +/// @param str is the string to break. +/// @param delim is the delimeter to break the string on. +/// +/// @return the pair of string objects. +static inline pair break_string(string &str + , const string& delim) +{ + size_t pos = str.find_first_of(delim); + if (pos == string::npos) + { + return make_pair(str, ""); + } + size_t end_pos = pos + delim.length(); + if (end_pos > str.length()) + { + end_pos = pos; + } + return make_pair(str.substr(0, pos), str.substr(end_pos)); +} + +/// Helper which will break a string into multiple pieces based on a provided +/// delimeter. +/// +/// @param str is the string to tokenize. +/// @param tokens is the container which will receive the tokenized strings. +/// @param delimeter to tokenize the string on. +/// @param keep_incomplete will take the last token of the input string and +/// insert it to the container as the last element when this is true. When +/// this is false the last token will not be inserted to the container. +/// @param discard_empty will discard empty tokens when set to true. +template +string::size_type tokenize(const string& str, ContainerT& tokens + , const string& delimeter = " " + , bool keep_incomplete = true + , bool discard_empty = false) +{ + string::size_type pos, lastPos = 0; + + using value_type = typename ContainerT::value_type; + using size_type = typename ContainerT::size_type; + + while(lastPos < str.length()) + { + pos = str.find_first_of(delimeter, lastPos); + if (pos == std::string::npos) + { + if (!keep_incomplete) + { + return lastPos; + } + pos = str.length(); + } + + if (pos != lastPos || !discard_empty) + { + tokens.emplace_back(value_type(str.data() + lastPos + , (size_type)pos - lastPos)); + } + lastPos = pos + delimeter.length(); + } + return lastPos; +} + +/// Helper which joins a vector with a delimeter. +/// +/// @param strings is the vector to join +/// @param delimeter is the string to join the segments with. +/// @return the joined string. +static inline string string_join(const vector& strings + , const string& delimeter = "") +{ + string result; + for (auto piece : strings) + { + if (!result.empty()) + { + result += delimeter; + } + result += piece; + } + return result; +} + +/// Helper which joins a vector using a first and last iterator. +/// +/// @param first is the starting iterator position. +/// @param last is the starting iterator position. +/// @param delimeter is the string to join the segments with. +/// @return the joined string. +static inline string string_join(const vector::iterator first + , const vector::iterator last + , const string& delimeter = "") +{ + vector vec(first, last); + return string_join(vec, delimeter); +} + +/// Helper which URL decodes a string as described in RFC-1738 sec. 2.2. +/// +/// @param source is the string to be decoded. +/// @return the decoded string. +/// RFC: https://www.ietf.org/rfc/rfc1738.txt +static inline string url_decode(const string source) +{ + string decoded = source; + + // replace + with space + std::replace(decoded.begin(), decoded.end(), '+', ' '); + + // search and replace %{hex}{hex} with hex decoded character + while (decoded.find("%") != string::npos) + { + // find the % character + auto pos = decoded.find("%"); + if (pos + 2 < decoded.size()) + { + // decode the character + auto sub = decoded.substr(pos + 1, 2); + char ch = std::stoi(sub, nullptr, 16); + // insert the replacement + decoded.insert(pos, 1, ch); + // remove the decoded piece + decoded.erase(pos + 1, 3); + } + else + { + // the % is not followed by at least two characters. + break; + } + } + return decoded; +} + +/// Helper which URL encodes a string as described in RFC-1738 sec. 2.2. +/// +/// @param source is the string to be encoded. +/// @return the encoded string. +/// +/// NOTE: this method does not take into account the encoding of a URI with +/// query parameters after the "?". This should be handled by the caller by +/// passing the path and query portions seperately at this time. +/// RFC: https://www.ietf.org/rfc/rfc1738.txt +static inline string url_encode(const string source) +{ + const string reserved_characters = "?#/:;+@&="; + const string illegal_characters = "%<>{}|\\\"^`!*'()$,[]"; + string encoded = ""; + + // reserve the size of the source string plus 25%, this space will be + // reclaimed if the final string length is shorter. + encoded.reserve(source.length() + (source.length() / 4)); + + // process the source string character by character checking for any that + // are outside the ASCII printable character range, in the reserve character + // list or illegal character list. For accepted characters it will be added + // to the encoded string directly, any that require encoding will be hex + // encoded before being added to the encoded string. + for (auto ch : source) + { + if (ch <= 0x20 || ch >= 0x7F || + reserved_characters.find(ch) != string::npos || + illegal_characters.find(ch) != string::npos) + { + // if it is outside the printable ASCII character *OR* is in either the + // reserved or illegal characters we need to encode it as %HH. + // NOTE: space will be encoded as "%20" and not as "+", either is an + // acceptable option per the RFC. + encoded += StringPrintf("%%%02x", ch); + } + else + { + encoded += ch; + } + } + // shrink the buffer to the actual length + encoded.shrink_to_fit(); + return encoded; +} + +} // namespace http + +#endif // STRINGUTILS_H_ \ No newline at end of file diff --git a/components/Esp32HttpServer/include/Httpd.h b/components/Esp32HttpServer/include/Httpd.h new file mode 100644 index 00000000..8f74f0f5 --- /dev/null +++ b/components/Esp32HttpServer/include/Httpd.h @@ -0,0 +1,1277 @@ +/********************************************************************** +ESP32 HTTP Server + +COPYRIGHT (c) 2019-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +/// @file HTTP server with Captive Portal support. + +#ifndef HTTPD_H_ +#define HTTPD_H_ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Dnsd.h" + +/// Namespace for HTTP related functionality. +namespace http +{ + +/// FreeRTOS task stack size for the httpd Executor. +DECLARE_CONST(httpd_server_stack_size); + +/// FreeRTOS task priority for the httpd Executor. +DECLARE_CONST(httpd_server_priority); + +/// Max number of bytes to read in a single chunk when reading the HTTP request +/// headers. +DECLARE_CONST(httpd_header_chunk_size); + +/// Max number of bytes to read in a single chunk when reading the HTTP request +/// body payload. +DECLARE_CONST(httpd_body_chunk_size); + +/// Max number of bytes to write in a single chunk when sending the HTTP +/// response to the client. +DECLARE_CONST(httpd_response_chunk_size); + +/// Maximum size of the HTTP headers before which the request will be aborted +/// with a BAD_REQUEST (400) error code. +DECLARE_CONST(httpd_max_header_size); + +/// Maximum size of the HTTP request body payload. Any request which exceeds +/// this limit will be forcibly aborted. +DECLARE_CONST(httpd_max_req_size); + +/// This is the maximum number of HTTP requests which should be processed for a +/// single connection with keep-alive active before the connection will be +/// closed with the "Connection: close" header. +DECLARE_CONST(httpd_max_req_per_connection); + +/// This is the maximum wait time for receiving a single chunk of data from the +/// HTTP request. +DECLARE_CONST(httpd_req_timeout_ms); + +/// This is the number of milliseconds to use for the socket send and receive +/// timeouts. +DECLARE_CONST(httpd_socket_timeout_ms); + +/// This is the number of milliseconds to use as the read timeout for all +/// websocket connections. When this limit and the max_XX_attempts limit have +/// been exceeded the send/receive attempt is aborted and the inverse operation +/// will be attempted. +DECLARE_CONST(httpd_websocket_timeout_ms); + +/// This is the maximum data size to send/receive in a single operation. If a +/// websocket frame is received exceeding this limit it will be processed in +/// chunks of this size. +DECLARE_CONST(httpd_websocket_max_frame_size); + +/// This controls how many attempts will be allowed to receive a websocket +/// frame before attempting to send out a websocket frame. +DECLARE_CONST(httpd_websocket_max_read_attempts); + +/// This controls the Cache-Control: max-age=XXX value in the response headers +/// for static content. +DECLARE_CONST(httpd_cache_max_age_sec); + +/// Commonly used HTTP status codes. +/// @enum HttpStatusCode +enum HttpStatusCode +{ + STATUS_CONTINUE=100, + STATUS_SWITCH_PROTOCOL=101, + + STATUS_OK=200, + STATUS_CREATED=201, + STATUS_ACCEPTED=202, + STATUS_NON_AUTH_INFO=203, + STATUS_NO_CONTENT=204, + STATUS_RESET_CONTENT=205, + STATUS_PARTIAL_CONTENT=206, + + STATUS_MULTIPLE_CHOICES=300, + STATUS_MOVED_PERMANENTLY=301, + STATUS_FOUND=302, + STATUS_SEE_OTHER=303, + STATUS_NOT_MODIFIED=304, + STATUS_USE_PROXY=305, + STATUS_TEMP_REDIRECT=307, + + STATUS_BAD_REQUEST=400, + STATUS_NOT_AUTHORIZED=401, + STATUS_PAYMENT_REQUIRED=402, + STATUS_FORBIDDEN=403, + STATUS_NOT_FOUND=404, + STATUS_NOT_ALLOWED=405, + STATUS_NOT_ACCEPTABLE=406, + STATUS_PROXY_AUTH_REQ=407, + STATUS_TIMEOUT=408, + STATUS_CONFLICT=409, + STATUS_GONE=410, + STATUS_LENGTH_REQ=411, + STATUS_PRECOND_FAIL=412, + STATUS_ENTITY_TOO_LARGE=413, + STATUS_URI_TOO_LARGE=414, + STATUS_UNSUPPORTED_MEDIA_TYPE=415, + STATUS_RANGE_NOT_SATISFIABLE=416, + STATUS_EXPECATION_FAILED=417, + + STATUS_SERVER_ERROR=500, + STATUS_NOT_IMPLEMENTED=501, + STATUS_BAD_GATEWAY=502, + STATUS_SERVICE_UNAVAILABLE=503, + STATUS_GATEWAY_TIMEOUT=504, + STATUS_HTTP_VERSION_UNSUPPORTED=505 +}; + +/// Default HTTP listener port +static constexpr uint16_t DEFAULT_HTTP_PORT = 80; + +/// Commonly used and well-known HTTP methods. +enum HttpMethod +{ + /// Request is for deleting a resource. + DELETE = BIT(1), + /// Request is for retrieving a resource. + GET = BIT(2), + /// Request is for retrieving the headers for a resource. + HEAD = BIT(3), + /// Request is for creating a resource. + POST = BIT(4), + /// Request is for patching an existing resource. + PATCH = BIT(5), + /// Request is for applying an update to an existing resource. + PUT = BIT(6), + /// Request type was not understood by the server. + UNKNOWN_METHOD = BIT(7), +}; + +/// Commonly used and well-known HTTP headers +enum HttpHeader +{ + ACCEPT, + CACHE_CONTROL, + CONNECTION, + CONTENT_ENCODING, + CONTENT_TYPE, + CONTENT_LENGTH, + CONTENT_DISPOSITION, + EXPECT, + HOST, + IF_MODIFIED_SINCE, + LAST_MODIFIED, + LOCATION, + ORIGIN, + UPGRADE, + WS_VERSION, + WS_KEY, + WS_ACCEPT, +}; + +/// Commonly used and well-known values for the Content-Type HTTP header. +/// +/// These are currently only used for POST and PUT HTTP requests. All other +/// types will receive a stream of the body of the HTTP request. +enum ContentType +{ + MULTIPART_FORMDATA, + FORM_URLENCODED, + UNKNOWN_TYPE +}; + +/// WebSocket events for the @ref WebSocketHandler callback. +typedef enum +{ + /// A new Websocket connection has been established. + WS_EVENT_CONNECT, + + /// A WebSocket connection has been closed. + WS_EVENT_DISCONNECT, + + /// A TEXT message has been received from a WebSocket. Note that it may be + /// sent to the handler in pieces. + WS_EVENT_TEXT, + + /// A BINARY message has been received from a WebSocket. Note that it may be + /// sent to the handler in pieces. + WS_EVENT_BINARY +} WebSocketEvent; + +// Values for Cache-Control +// TODO: introduce enum constants for these +static constexpr const char * HTTP_CACHE_CONTROL_NO_CACHE = "no-cache"; +static constexpr const char * HTTP_CACHE_CONTROL_NO_STORE = "no-store"; +static constexpr const char * HTTP_CACHE_CONTROL_NO_TRANSFORM = "no-transform"; +static constexpr const char * HTTP_CACHE_CONTROL_MAX_AGE = "max-age"; +static constexpr const char * HTTP_CACHE_CONTROL_PUBLIC = "public"; +static constexpr const char * HTTP_CACHE_CONTROL_PRIVATE = "private"; +static constexpr const char * HTTP_CACHE_CONTROL_MUST_REVALIDATE = "must-revalidate"; + +// Values for Connection header +// TODO: introduce enum constants for these +static constexpr const char * HTTP_CONNECTION_CLOSE = "close"; +static constexpr const char * HTTP_CONNECTION_KEEP_ALIVE = "keep-alive"; +static constexpr const char * HTTP_CONNECTION_UPGRADE = "Upgrade"; + +// TODO: introduce enum constant for this value +static constexpr const char * HTTP_UPGRADE_HEADER_WEBSOCKET = "websocket"; + +// HTTP end of line characters +static constexpr const char * HTML_EOL = "\r\n"; + +// Common mime-types +// TODO: introduce enum constants for these +static constexpr const char * MIME_TYPE_NONE = ""; +static constexpr const char * MIME_TYPE_TEXT_CSS = "text/css"; +static constexpr const char * MIME_TYPE_TEXT_HTML = "text/html"; +static constexpr const char * MIME_TYPE_TEXT_JAVASCRIPT = "text/javascript"; +static constexpr const char * MIME_TYPE_TEXT_PLAIN = "text/plain"; +static constexpr const char * MIME_TYPE_TEXT_XML = "text/XML"; +static constexpr const char * MIME_TYPE_IMAGE_GIF = "image/gif"; +static constexpr const char * MIME_TYPE_IMAGE_PNG = "image/png"; +static constexpr const char * MIME_TYPE_APPLICATION_JSON = "application/json"; +static constexpr const char * MIME_TYPE_OCTET_STREAM = "application/octet-stream"; + +// TODO: introduce enum constants for these +static constexpr const char * HTTP_ENCODING_NONE = ""; +static constexpr const char * HTTP_ENCODING_GZIP = "gzip"; + +/// Forward declaration of the WebSocketFlow so it can access internal methods +/// of various classes. +class WebSocketFlow; + +/// Forward declaration of the HttpdRequestFlow so it can access internal +/// methods of various classes. +class HttpRequestFlow; + +/// Forward declaration of the Httpd so it can access internal methods of +/// various classes. +class Httpd; + +/// This is the base class for an HTTP response. +class AbstractHttpResponse +{ +public: + /// Constructor. + /// + /// @param code is the @ref HttpStatusCode to use for the + /// response. + /// @param mime_type is the mime type to send as part of the Content-Type + /// header. + AbstractHttpResponse(HttpStatusCode code=STATUS_NOT_FOUND + , const std::string &mime_type=MIME_TYPE_TEXT_PLAIN); + + /// Destructor. + ~AbstractHttpResponse(); + + /// Encodes the HTTP response headers for transmission to the client. + /// + /// @param len is the length of the headers after encoding. This is an + /// OUTPUT parameter. + /// @param keep_alive is used to control the "Connection: [keep-alive|close]" + /// header. + /// @param add_keep_alive is used to control if the Connection header will be + /// included in the response. + /// + /// @return a buffer containing the HTTP response. This buffer is owned by + /// the @ref AbstractHttpResponse and does not need to be + /// freed by the caller. + uint8_t *get_headers(size_t *len, bool keep_alive=false + , bool add_keep_alive=true); + + /// Encodes the HTTP response into a string which can be printed. This is + /// used by @ref get_headers internally with include_body set to false. + /// + /// @param include_body will include the body of the response as well as any + /// headers. + /// @param keep_alive is used to control the "Connection: [keep-alive|close]" + /// header. + /// @param add_keep_alive is used to control if the Connection header will be + /// included in the response. + /// + /// @return the request as a printable string. + std::string to_string(bool include_body=false, bool keep_alive=false + , bool add_keep_alive=true); + + /// @return the response body as a buffer which can be streamed. This buffer + /// is owned by the @ref AbstractHttpResponse and does not + /// need to be freed by the caller. + /// + /// Note: this method should be overriden by sub-classes to supply the + /// response body. + virtual const uint8_t *get_body() + { + return nullptr; + } + + /// @return the size of the body payload. + /// + /// Note: this method should be overriden by sub-classes to supply the + /// response body. + virtual size_t get_body_length() + { + return 0; + } + + /// @return the mime type to include in the HTTP response header. + /// + /// Note: this method should be overriden by sub-classes to supply the + /// response body. + std::string get_body_mime_type() + { + return mime_type_; + } + +protected: + /// Adds an arbitrary HTTP header to the response object. + /// + /// @param header is the HTTP header name to add. + /// @param value is the HTTP header value to add. + void header(const std::string &header, const std::string &value); + + /// Adds a well-known HTTP header to the response object. + /// + /// @param header is the HTTP header name to add. + /// @param value is the HTTP header value to add. + void header(const HttpHeader header, const std::string &value); + +private: + /// Collection of headers and values for this HTTP response. + std::map headers_; + + /// @ref HttpStatusCode for this HTTP response. + HttpStatusCode code_; + + /// Content-Type header value. + std::string mime_type_; + + /// Temporary storage for the HTTP response headers in an encoded format + /// that is ready for transmission to the client. + std::string encoded_headers_; + + /// Gives @ref WebSocketFlow access to protected/private + /// members. + friend class WebSocketFlow; + + /// Gives @ref HttpRequestFlow access to protected/private + /// members. + friend class HttpRequestFlow; +}; + +/// HTTP Response object for URI not found. +class UriNotFoundResponse : public AbstractHttpResponse +{ +public: + /// Constructor. + /// + /// @param uri is the URI that was requested that is not known by the server. + UriNotFoundResponse(const std::string &uri) + : body_(StringPrintf("URI %s was not found.", uri.c_str())) + { + } + + /// @return the pre-formatted body of this response. + const uint8_t *get_body() override + { + return (const uint8_t *)(body_.c_str()); + } + + /// @return the size of the pre-formatted body of this response. + size_t get_body_length() override + { + return body_.length(); + } + +private: + /// Temporary storage for the response body. + const std::string body_; +}; + +/// HTTP Response object used to redirect the client to a different location +/// via the HTTP Header: "Location: uri". +class RedirectResponse : public AbstractHttpResponse +{ +public: + /// Constructor. + /// + /// @param target_url is the target which the client should be redirected to. + RedirectResponse(const std::string &target_url); +}; + +/// HTTP Response object which is used to return a static payload to a client +/// when the URI is accessed. +/// +/// Note: The payload must remain alive as long as the URI is referenced by the +/// @ref Httpd server. +class StaticResponse : public AbstractHttpResponse +{ +public: + /// Constructor. + /// + /// @param payload is the body of the response to send. + /// @param length is the length of the body payload. + /// @param mime_type is the value to send in the Content-Type HTTP header. + /// @param encoding is the optional encoding to send in the Content-Encoding + /// HTTP Header. + StaticResponse(const uint8_t *payload, const size_t length + , const std::string mime_type + , const std::string encoding = HTTP_ENCODING_NONE); + + /// @return the pre-formatted body of this response. + const uint8_t *get_body() override + { + return payload_; + } + + /// @return the size of the pre-formatted body of this response. + size_t get_body_length() override + { + return length_; + } + +private: + /// Pointer to the payload to return for this URI. + const uint8_t *payload_; + + /// Length of the payload to return for this URI. + const size_t length_; +}; + +/// HTTP Response object which can be used to return a string based response to +/// a given URI. +class StringResponse : public AbstractHttpResponse +{ +public: + /// Constructor. + /// + /// @param response is the response string to send as the HTTP response body. + /// @param mime_type is the value to use for the Content-Type HTTP header. + /// + /// Note: The ownership of the response object passed into this method will + /// be transfered to this class instance and will be cleaned up after it has + /// been sent to the client. + StringResponse(const std::string &response, const std::string &mime_type); + + /// @return the pre-formatted body of this response. + const uint8_t *get_body() override + { + return (uint8_t *)response_.c_str(); + } + + /// @return the size of the pre-formatted body of this response. + size_t get_body_length() override + { + return response_.length(); + } + +private: + /// Temporary storage of the response payload. + std::string response_; +}; + +/// Specialized @ref StringResponse type for json payloads. +class JsonResponse : public StringResponse +{ +public: + /// Constructor. + /// + /// @param response is the response string to send as the HTTP response body. + /// + /// This calls into @ref StringResponse passing in + /// @ref MIME_TYPE_APPLICATION_JSON as mime_type. + /// + /// Note: The ownership of the response object passed into this method will + /// be transfered to this class instance and will be cleaned up after it has + /// been sent to the client. + JsonResponse(const std::string &response) + : StringResponse(response, MIME_TYPE_APPLICATION_JSON) + { + } +}; + +/// Runtime state of an HTTP Request. +class HttpRequest +{ +public: + /// @return the parsed well-known @ref HttpMethod. + HttpMethod method(); + + /// @return the unparsed HTTP method. + const std::string &raw_method(); + + /// @return the URI that this request is for. + const std::string &uri(); + + /// @return true if the named HTTP Header exists. + /// @param name of the parameter to check for. + bool has_header(const std::string &name); + + /// @return true if the well-known @ref HttpHeader exists. + bool has_header(const HttpHeader name); + + /// @return the value of the named HTTP Header or a blank string if it does + /// not exist. + const std::string &header(const std::string name); + + /// @return the value of the well-known @ref HttpHeader or a blank string if + /// it does not exist. + const std::string &header(const HttpHeader name); + + /// @return true if the well-known @ref HttpHeader::CONNECTION header exists + /// with a value of "keep-alive". + bool keep_alive(); + + /// @return true if the request could not be parsed successfully. + bool error(); + + /// @return the parsed @ref ContentType for this request. + ContentType content_type(); + + /// Sets the request response status code. + /// + /// @param code is the @ref HttpStatusCode to respond with upon conclusion of + /// this @ref HttpRequest. + /// + /// This can be used by the @ref RequestProcessor to set the response code + /// when no response body is required. + void set_status(HttpStatusCode code); + + /// @return number of parameters to this @ref HttpRequest. + size_t params(); + + /// @return parameter value or a blank string if parameter is not present. + /// @param name is the name of the parameter to return. + std::string param(std::string name); + + /// @return parameter value or default value is parameter is not present. + /// @param name is the name of the parameter to return as a boolean value. + /// @param def is the default value to return if the parameter does not exist + /// or is otherwise non-convertable to a boolean. + bool param(std::string name, bool def); + + /// @return parameter value or default value is parameter is not present. + /// @param name is the name of the parameter to return as an integer value. + /// @param def is the default value to return if the parameter does not exist + /// or is otherwise non-convertable to an integer. + int param(std::string name, int def); + + /// @return true if the parameter is present. + /// @param name is the name of the parameter to return. + bool has_param(std::string name); + + /// @return string form of the request, this is headers only. + std::string to_string(); + +private: + /// Gives @ref HttpRequestFlow access to protected/private members. + friend class HttpRequestFlow; + + /// Sets the @ref HttpMethod if it is well-known, otherwise only the unparsed + /// value will be available. + /// + /// @param value is the raw value parsed from the first line of the HTTP + /// request stream. + void method(const std::string &value); + + /// Sets the URI of the HttpRequest. + /// + /// @param value is the value of the URI. + void uri(const std::string &value); + + /// Adds a URI parameter to the request. + /// + /// @param value is a pair of the key:value pair. + void param(const std::pair &value); + + /// Adds an HTTP Header to the request. + /// + /// @param value is a pair of the key:value pair. + void header(const std::pair &value); + + /// Adds/replaces a HTTP Header to the request. + /// + /// @param header is the @ref HttpHeader to add/replace. + /// @param value is the value for the header. + void header(HttpHeader header, std::string value); + + /// Resets the internal state of the @ref HttpRequest to defaults so it can + /// be reused for subsequent requests. + void reset(); + + /// Sets/Resets the parse error flag. + /// + /// @param value should be true if there is a parse failure, false otherwise. + void error(bool value); + + /// default return value when a requested header or parameter is not known. + const std::string no_value_{""}; + + /// Collection of HTTP Headers that have been parsed from the HTTP request + /// stream. + std::map headers_; + + /// Collection of parameters supplied with the HTTP Request after the URI. + std::map params_; + + /// Parsed @ref HttpMethod for this @ref HttpRequest. + HttpMethod method_; + + /// Raw unparsed HTTP method for this @ref HttpRequest. + std::string raw_method_; + + /// Parsed URI of this @ref HttpRequest. + std::string uri_; + + /// Parse error flag. + bool error_; + + /// @ref HttpStatusCode to return by default when this @ref HttpRequest has + /// completed and there is no @ref AbstractHttpResponse to return. + HttpStatusCode status_{HttpStatusCode::STATUS_SERVER_ERROR}; +}; + +/// URI processing handler that will be invoked for requests which have no body +/// payload to stream. Currently the only requests which have a body payload +/// +/// When this function is invoked it has the option to return a pointer to a +/// @ref AbstractHttpResponse which will be sent to the client or it can call +/// @ref HttpRequest::set_status if no response body is required. +typedef std::function< + AbstractHttpResponse *(HttpRequest * /** request*/)> RequestProcessor; + + +#define HTTP_HANDLER(name) \ +AbstractHttpResponse * name (HttpRequest *); + +#define HTTP_HANDLER_IMPL(name, request) \ +AbstractHttpResponse * name (HttpRequest * request) + +/// URI processing handler which will be invoked for POST/PUT requests that +/// have a body payload. +/// +/// When this function is invoked the "abort" parameter can be set to true and +/// the request will be aborted immediately and an error returned to the +/// client. The function has the same option of calling +/// @ref HttpRequest::set_status or returning a pointer to a +/// @ref AbstractHttpResponse. +typedef std::function StreamProcessor; + +#define HTTP_STREAM_HANDLER(name) \ +AbstractHttpResponse * name (HttpRequest *request \ + , const std::string &filename, size_t size \ + , const uint8_t *data, size_t length \ + , size_t offset, bool final, bool *abort) + +#define HTTP_STREAM_HANDLER_IMPL(name, request, filename, size, data, length \ + , offset, final, abort) \ +AbstractHttpResponse * name (HttpRequest * request \ + , const std::string & filename, size_t size \ + , const uint8_t * data, size_t length \ + , size_t offset, bool final, bool * abort) + + +/// WebSocket processing Handler. +/// +/// This method will be invoked when there is an event to be processed. +/// +/// When @ref WebSocketEvent is @ref WebSocketEvent::WS_EVENT_CONNECT or +/// @ref WebSocketEvent::WS_EVENT_DISCONNECT data will be +/// nullptr and data length will be zero. +/// +/// When @ref WebSocketEvent is @ref WebSocketEvent::WS_EVENT_TEXT or +/// @ref WebSocketEvent::WS_EVENT_BINARY the data parameter +/// will be a buffer of data length bytes of text or binary data to be +/// processed by the handler. +/// +/// The handler can invoke the @ref WebSocketFlow parameter to retrieve +/// additional details about the WebSocket client, queue response text or +/// binary data for delivery at next available opportunity, or request the +/// WebSocket connection to be closed. +typedef std::function WebSocketHandler; + +#define WEBSOCKET_STREAM_HANDLER(name) \ +void name (WebSocketFlow *, WebSocketEvent, const uint8_t *, size_t); + +#define WEBSOCKET_STREAM_HANDLER_IMPL(name, websocket, event, data, length) \ +void name (WebSocketFlow * websocket, WebSocketEvent event \ + , const uint8_t * data, size_t length) + +/// HTTP Server implementation +class Httpd : public Service, public Singleton +{ +public: + /// Constructor. + /// + /// @param mdns is the @ref MDNS instance to use for publishing mDNS records + /// when the server is active. This is disabled by default. + /// @param port is the port to listen for HTTP requests on, default is 80. + /// @param name is the name to use for the executor, default is "httpd". + /// @param service_name is the mDNS service name to advertise when the server + /// is active, default is _http._tcp. + Httpd(MDNS *mdns = nullptr, uint16_t port = DEFAULT_HTTP_PORT + , const std::string &name = "httpd" + , const std::string service_name = "_http._tcp"); + + /// Destructor. + ~Httpd(); + + /// Registers a URI with the provided handler. + /// + /// @param uri is the URI to call the provided handler for. + /// @param method_mask is the @ref HttpMethod for this URI, when multiple + /// @ref HttpMethod values are required they must be ORed together. + /// @param handler is the @ref RequestProcessor to invoke when this URI is + /// requested. When there is a request body and the method is POST/PUT this + /// function will not be invoked. + /// @param stream_handler is the @ref StreamProcessor to invoke when this URI + /// is requested as POST/PUT and the request has a body payload. + void uri(const std::string &uri, const size_t method_mask + , RequestProcessor handler + , StreamProcessor stream_handler = nullptr); + + /// Registers a URI with the provided handler that can process all + /// @ref HttpMethod values. Note that any request with a body payload will + /// result in an error being returned to the client for this URI since there + /// is no stream handler defined by this method. + /// + /// @param uri is the URI to call the provided handler for. + /// @param handler is the @ref RequestProcessor to invoke when this URI is + /// requested. When there is a request body and the method is POST/PUT this + /// function will not be invoked. + void uri(const std::string &uri, RequestProcessor handler); + + /// Registers a URI to redirect to another location. + /// + /// @param source is the URI which should trigger the redirect. + /// @param target is where the request should be routed instead. + /// + /// Note: This will result in the client receiving an HTTP 302 response + /// with an updated Location value provided. + void redirect_uri(const std::string &source, const std::string &target); + + /// Registers a static response URI. + /// + /// @param uri is the URI to serve the static content for. + /// @param content is the content to send back to the client when this uri is + /// requested. Note that large content blocks will be broken into smaller + /// pieces for transmission to the client and thus must remain in memory. + /// @param length is the length of the content to send to the client. + /// @param mime_type is the Content-Type parameter to return to the client. + /// @param encoding is the encoding for the content, if not specified the + /// Content-Encoding header will not be transmitted. + void static_uri(const std::string &uri, const uint8_t *content + , const size_t length, const std::string &mime_type + , const std::string &encoding = HTTP_ENCODING_NONE); + + /// Registers a WebSocket handler for a given URI. /// + /// @param uri is the URI to process as a WebSocket endpoint. + /// @param handler is the @ref WebSocketHandler to invoke when this URI is + /// requested. + void websocket_uri(const std::string &uri, WebSocketHandler handler); + + /// Sends a binary message to a single WebSocket. + /// + /// @param id is the ID of the WebSocket to send the data to. + /// @param data is the binary data to send to the websocket client. + /// @param length is the length of the binary data to send to the websocket + /// client. + /// + /// Note: this is currently unimplemented. + void send_websocket_binary(int id, uint8_t *data, size_t length); + + /// Sends a text message to a single WebSocket. + /// + /// @param id is the ID of the WebSocket to send the text to. + /// @param text is the text to send to the WebSocket client. + void send_websocket_text(int id, std::string &text); + + /// Broadcasts a text message to all connected WebSocket clients. + /// + /// @param text is the text to send to all WebSocket clients. + void broadcast_websocket_text(std::string &text); + + /// Creates a new @ref HttpRequestFlow for the provided socket handle. + /// + /// @param fd is the socket handle. + void new_connection(int fd); + + /// Enables processing of known captive portal endpoints. + /// + /// @param first_access_response is the HTML response to send upon first + /// access from a client. + /// @param auth_uri is the callback URI that the client should access to + /// bypass the captive portal redirection. Note this URI will be processed + /// internally by @ref Httpd before invoking the @ref RequestProcessor. If + /// there is no @ref RequestProcessor for this URI a redirect to / will be + /// sent instead. + /// @param auth_timeout is the number of seconds to cache the source IP + /// address for a client before forcing a re-authentication to occur. A value + /// of UINT32_MAX will disable the timeout. + /// + /// The following URIs will be processed as captive portal endpoints and + /// force redirect to the captive portal when accessed and the source IP + /// has not previously accessed the auth_uri: + /// | URI | Environment | + /// | --- | ----------- | + /// | /generate_204 | Android | + /// | /gen_204 | Android 9.0 | + /// | /mobile/status.php | Android 8.0 (Samsung s9+) | + /// | /ncsi.txt | Windows | + /// | /success.txt | OSX / FireFox | + /// | /hotspot-detect.html | iOS 8/9 | + /// | /hotspotdetect.html | iOS 8/9 | + /// | /library/test/success.html | iOS 8/9 | + /// | /kindle-wifi/wifiredirect.html | Kindle | + /// | /kindle-wifi/wifistub.html | Kindle | + /// + void captive_portal(std::string first_access_response + , std::string auth_uri = "/captiveauth" + , uint64_t auth_timeout = UINT32_MAX); + +private: + /// Gives @ref WebSocketFlow access to protected/private members. + friend class WebSocketFlow; + + /// Gives @ref HttpRequestFlow access to protected/private members. + friend class HttpRequestFlow; + + /// Starts the HTTP socket listener. + void start_http_listener(); + + /// Starts the DNS socket listener. + /// + /// @param ip is the IP address to redirect all DNS requests to. This should + /// be in HOST native order, the DNS server will convert to network order. + void start_dns_listener(uint32_t ip); + + /// Stops the HTTP socket listener (if active). + void stop_http_listener(); + + /// Stops the DNS socket listener (if active). + void stop_dns_listener(); + + /// Registers a new @ref WebSocketFlow with the server to allow sending + /// text or binary messages based on the WebSocket ID. + /// + /// @param id is the ID of the WebSocket client. + /// @param ws is the @ref WebSocketFlow managing the WebSocket client. + void add_websocket(int id, WebSocketFlow *ws); + + /// Removes a previously registered WebSocket client. + /// + /// @param id of the WebSocket client to remove. + void remove_websocket(int id); + + /// @return the @ref RequestProcessor for a URI. + /// @param uri is the URI to retrieve the @ref RequestProcessor for. + RequestProcessor handler(HttpMethod method, const std::string &uri); + + /// @return the @ref StreamProcessor for a URI. + /// @param uri is the URI to retrieve the @ref StreamProcessor for. + StreamProcessor stream_handler(const std::string &uri); + + /// @return the @ref WebSocketHandler for the provided URI. + /// @param uri is the URI to retrieve the @ref WebSocketHandler for. + WebSocketHandler ws_handler(const std::string &uri); + + /// @return true if there is a @ref AbstractHttpResponse for the URI. + /// @param uri is the URI to check. + bool have_known_response(const std::string &uri); + + /// @return the @ref AbstractHttpResponse for the @param request. This will + /// evaluate the request for static_uri and redirect registered endpoints. + /// + /// For a static_uri endpoint the request headers will be evaluated for the + /// presence of @ref HttpHeader::IF_MODIFIED_SINCE and will return either the + /// requested resource or a @ref AbstractHttpResponse with + /// @ref HttpStatusCode::STATUS_NOT_MODIFIED as the code if the resource has + /// not been modified. + std::shared_ptr response(HttpRequest *request); + + /// @return true if the @param request is too large to be processed. Size + /// is configured via httpd_max_req_size. + bool is_request_too_large(HttpRequest *request); + + /// @return true if the @param request can be serviced by this @ref Httpd. + bool is_servicable_uri(HttpRequest *request); + + /// Name to use for the @ref Httpd server. + const std::string name_; + + /// @ref MDNS instance to use for publishing mDNS records when the @ref Httpd + /// server is active; + MDNS *mdns_; + + /// mDNS service name to advertise when the @ref Httpd Server is running. + const std::string mdns_service_; + + /// @ref Executor that manages all @ref StateFlow for the @ref Httpd server. + Executor<1> executor_; + + /// TCP/IP port to listen for HTTP requests on. + uint16_t port_; + + /// @ref SocketListener that will accept() the socket connections and call + /// the @ref Httpd when a new client is available. + uninitialized listener_; + + /// @ref Dnsd that will serve DNS responses when enabled. + uninitialized dns_; + + /// Internal state flag for the listener_ being active. + bool http_active_{false}; + + /// Internal state flag for the dns_ being active. + bool dns_active_{false}; + + /// Internal map of all registered @ref RequestProcessor handlers. + std::map> handlers_; + + /// Internal map of all registered @ref RequestProcessor handlers. + std::map stream_handlers_; + + /// Internal map of all registered static URIs to use when the client does + /// not specify the @ref HttpHeader::IF_MODIFIED_SINCE or the value is not + /// the current version. + std::map> static_uris_; + + /// Internal map of all registeres static URIs to use when resource has not + /// been modified since the client last retrieved it. + std::map> static_cached_; + + /// Internal map of all redirected URIs. + std::map> redirect_uris_; + + /// Internal map of all registered @ref WebSocketHandler URIs. + std::map websocket_uris_; + + /// Internal map of active @ref WebSocketFlow instances. + std::map websockets_; + + /// Lock object for websockets_. + OSMutex websocketsLock_; + + /// Internal holder for captive portal response. + std::string captive_response_; + + /// Internal holder for captive portal response. + std::string captive_auth_uri_; + + /// Timeout for captive portal authenticated clients. + uint32_t captive_timeout_{UINT32_MAX}; + + /// Internal state flag for the captive portal. + bool captive_active_{false}; + + /// Tracking entries for authenticated captive portal clients. + std::map captive_auth_; + + /// timeval to use for newly connected sockets for SO_RCVTIMEO and + /// SO_SNDTIMEO. + struct timeval socket_timeout_; + + DISALLOW_COPY_AND_ASSIGN(Httpd); +}; + +/// HTTP Request parser that implements the @ref StateFlowBase interface. +class HttpRequestFlow : private StateFlowBase +{ +public: + /// Constructor. + /// + /// @param server is the @ref Httpd server owning this request. + /// @param fd is the socket handle. + /// @param remote_ip is the remote IP address of the client. + HttpRequestFlow(Httpd *server, int fd, uint32_t remote_ip); + + /// Destructor. + ~HttpRequestFlow(); + +private: + /// @ref StateFlowTimedSelectHelper which assists in reading/writing of the + /// request data stream. + StateFlowTimedSelectHelper helper_{this}; + + /// Timeout value to use for reading data while processing the request. + const long long timeout_{MSEC_TO_NSEC(config_httpd_req_timeout_ms())}; + + /// Maximum read size for a single read() call when processing the headers. + const size_t header_read_size_{(size_t)config_httpd_header_chunk_size()}; + + /// Maximum read size for a single read() call when processing the request + /// body. + const size_t body_read_size_{(size_t)config_httpd_body_chunk_size()}; + + /// @ref Httpd instance that owns this request. + Httpd *server_; + + /// Underlying socket handle for this request. + int fd_; + + /// Remote client IP (if known). + uint32_t remote_ip_; + + /// @ref HttpRequest data holder. + HttpRequest req_; + + /// Flag to indicate that the underlying socket handle should be closed when + /// this @ref HttpRequestFlow is deleted. In the case of a WebSocket the + /// socket needs to be preserved. + bool close_{true}; + + /// Temporary buffer used for reading the HTTP request. + std::vector buf_; + + /// Index into @ref buf_ for partial reads. + size_t body_offs_; + + /// Total size of the request body. + size_t body_len_; + + /// Temporary accumulator for the HTTP header data as it is being parsed. + std::string raw_header_; + + /// @ref AbstractHttpResponse that represents the response to this request. + std::shared_ptr res_; + + /// Index into the response body payload. + size_t response_body_offs_{0}; + + /// Request start time. + uint64_t start_time_; + + /// Current request number for this client connection. + uint8_t req_count_{0}; + + /// Count of multipart/form-data segments that have been encountered and + /// processing started. + size_t part_count_{0}; + + /// Temporary storage of multipart encoded form data boundary. + std::string part_boundary_; + + /// Internal flag to indicate we have found the boundary marker for a + /// multipart/form-data encoded POST/PUT body payload. + bool found_part_boundary_{false}; + + /// Internal flag to indicate we should process the body segment of a + /// multipart/form-data encoded POST/PUT body payload. + bool process_part_body_{false}; + + /// Temporary storage of the filename for the multipart/form-data part. + std::string part_filename_; + + /// Temporary storage of the content-type for a multipart/form-data part. + std::string part_type_; + + /// Temporary storage of the length for a multipart/form-data part. + size_t part_len_{0}; + + /// Index into @ref buf_ for partial reads for a multipart/form-data part. + size_t part_offs_{0}; + + /// Temporary holder of the @ref StreamProcessor to avoid subsequent lookups + /// during streaming of the parts. + StreamProcessor part_stream_{nullptr}; + + /// Response to send when a PUT/POST of multipart/form-data is received this + /// needs to be sent before the client will send the content to be processed. + std::string multipart_res_{"HTTP/1.1 100 Continue\r\n\r\n"}; + + STATE_FLOW_STATE(start_request); + STATE_FLOW_STATE(read_more_data); + STATE_FLOW_STATE(parse_header_data); + STATE_FLOW_STATE(process_request); + STATE_FLOW_STATE(process_request_handler); + STATE_FLOW_STATE(stream_body); + STATE_FLOW_STATE(start_multipart_processing); + STATE_FLOW_STATE(parse_multipart_headers); + STATE_FLOW_STATE(read_multipart_headers); + STATE_FLOW_STATE(stream_multipart_body); + STATE_FLOW_STATE(read_form_data); + STATE_FLOW_STATE(parse_form_data); + STATE_FLOW_STATE(send_response); + STATE_FLOW_STATE(send_response_headers); + STATE_FLOW_STATE(send_response_body); + STATE_FLOW_STATE(send_response_body_split); + STATE_FLOW_STATE(request_complete); + STATE_FLOW_STATE(upgrade_to_websocket); + STATE_FLOW_STATE(abort_request_with_response); + STATE_FLOW_STATE(abort_request); +}; + +/// WebSocket processor implementing the @ref StateFlowBase interface. +class WebSocketFlow : private StateFlowBase +{ +public: + /// Constructor. + /// + /// @param server is the @ref Httpd server owning this WebSocket. + /// @param fd is the socket handle. + /// @param remote_ip is the remote IP address (if known). + /// @param ws_key is the "Sec-WebSocket-Key" HTTP Header from the initial + /// request. + /// @param ws_version is the "Sec-WebSocket-Version" HTTP header from the + /// initial request. + /// @param handler is the @ref WebSocketHandler that will process the events + /// as they are raised. + WebSocketFlow(Httpd *server, int fd, uint32_t remote_ip + , const std::string &ws_key, const std::string &ws_version + , WebSocketHandler handler); + + /// Destructor. + ~WebSocketFlow(); + + /// Sends text to this WebSocket at the next possible interval. + /// + /// @param text is the text to send. + void send_text(std::string &text); + + /// @return the ID of the WebSocket. + int id(); + + /// @return the IP address of the remote side of the WebSocket. + /// + /// Note: This may return zero in which case the remote IP address is not + /// known. + uint32_t ip(); + + /// This will trigger an orderly shutdown of the WebSocket at the next + /// opportunity. This will trigger the @ref WebSocketHandler with the + /// @ref WebSocketEvent set to @ref WebSocketEvent::WS_EVENT_DISCONNECT. + void request_close(); + +private: + /// @ref StateFlowTimedSelectHelper which assists in reading/writing of the + /// request data stream. + StateFlowTimedSelectHelper helper_{this}; + + /// @ref Httpd instance that owns this request. + Httpd *server_; + + /// Underlying socket handle for this request. + int fd_; + + /// Remote client IP (if known). + uint32_t remote_ip_; + + /// WebSocket read/write timeout for a data frame. + const uint64_t timeout_; + + /// Maximum size to read/write of a frame in one call. + const uint64_t max_frame_size_; + + /// Temporary buffer used for reading/writing WebSocket frame data. + uint8_t *data_; + + /// Size of the used data in the temporary buffer. + size_t data_size_; + + /// Temporary holder for the WebSocket handshake response. + std::string handshake_; + + /// @ref WebSocketHandler to be invoked when a @ref WebSocketEvent needs to be + /// processed. + WebSocketHandler handler_; + + /// internal frame header data + uint16_t header_; + + /// Parsed op code from the header data. + uint8_t opcode_; + + /// When true the frame data is XOR masked with a 32bit XOR mask. + bool masked_; + + /// internal flag indicating the frame length type. + uint8_t frameLenType_; + + /// Temporary holder for 16bit frame length data. + uint16_t frameLength16_; + + /// Length of the WebSocket Frame data. + uint64_t frameLength_; + + /// 32bit XOR mask to apply to the data when @ref masked_ is true. + uint32_t maskingKey_; + + /// Lock for the @ref textToSend_ buffer. + OSMutex textLock_; + + /// Buffer of raw text message(s) to send to the client. Multiple messages + /// can be sent as one frame if they are sent to this client rapidly. + std::string textToSend_; + + /// When set to true the @ref WebSocketFlow will attempt to shutdown the + /// WebSocket connection at it's next opportunity. + bool close_requested_{false}; + + // helper for fully reading a data block with a timeout so we can close the + // socket if there are too many timeout errors + uint8_t *buf_{nullptr}; + size_t buf_size_; + size_t buf_offs_; + size_t buf_remain_; + uint8_t buf_attempts_; + Callback buf_next_; + Callback buf_next_timeout_; + Action read_fully_with_timeout(void *buf, size_t size, size_t attempts + , Callback success, Callback timeout); + STATE_FLOW_STATE(data_received); + + /// Internal state flow for reading a websocket frame of data and sending + /// back a response, possibly broken into chunks. + STATE_FLOW_STATE(send_handshake); + STATE_FLOW_STATE(handshake_sent); + STATE_FLOW_STATE(read_frame_header); + STATE_FLOW_STATE(frame_header_received); + STATE_FLOW_STATE(frame_data_len_received); + STATE_FLOW_STATE(start_recv_frame_data); + STATE_FLOW_STATE(recv_frame_data); + STATE_FLOW_STATE(shutdown_connection); + STATE_FLOW_STATE(send_frame_header); + STATE_FLOW_STATE(frame_sent); +}; + +} // namespace http + +#endif // HTTPD_H_ diff --git a/components/GPIO/CMakeLists.txt b/components/GPIO/CMakeLists.txt new file mode 100644 index 00000000..e525b7b8 --- /dev/null +++ b/components/GPIO/CMakeLists.txt @@ -0,0 +1,24 @@ +set(COMPONENT_SRCS + "Outputs.cpp" + "Sensors.cpp" + "RemoteSensors.cpp" + "S88Sensors.cpp" +) + +set(COMPONENT_ADD_INCLUDEDIRS "include" ) + +set(COMPONENT_REQUIRES + "Configuration" + "DCCppProtocol" + "nlohmann_json" + "OpenMRNLite" + "StatusDisplay" + "driver" +) + +register_component() + +set_source_files_properties(Outputs.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(RemoteSensors.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(Sensors.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(S88Sensors.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) \ No newline at end of file diff --git a/components/GPIO/Kconfig.projbuild b/components/GPIO/Kconfig.projbuild new file mode 100644 index 00000000..4450bfd3 --- /dev/null +++ b/components/GPIO/Kconfig.projbuild @@ -0,0 +1,165 @@ +config GPIO_OUTPUTS + bool "Enable GPIO pins to be used as outputs" + default y + help + Enabling this option will allow usage of any free GPIO pin to be + used as a controlled output. + +config GPIO_SENSORS + bool "Enable GPIO pins to be used as sensors" + default y + help + Enabling this option will allow usage of any free GPIO pin to be + used as a polled input. + +config GPIO_S88 + bool "Enable S88 Sensor functionality" + default n + depends on GPIO_SENSORS + help + S88 (S88-n) is a feedback bus used primarily for occupancy + detection but can be used for any digital input purposes. This + module follows the S88-n timing as documented on http://www.s88-n.eu/ + +menu "GPIO" + config ALLOW_USAGE_OF_RESTRICTED_GPIO_PINS + bool "Allow usage of restricted GPIO pins" + default n + depends on GPIO_OUTPUTS || GPIO_SENSORS || GPIO_S88 + help + Enables usage of the following pins for sensors and outputs + 0 - Bootstrap / Firmware Flash Download + 1 - UART0 TX + 2 - Bootstrap / Firmware Flash Download + 3 - UART0 RX + 5 - Bootstrap + 6, 7, 8, 9, 10, 11 - flash pins + 12, 15 - Bootstrap / SD pins + +# Log level constants from from components/OpenMRNLite/src/utils/logging.h +# +# ALWAYS : -1 +# FATAL : 0 +# LEVEL_ERROR : 1 +# WARNING : 2 +# INFO : 3 +# VERBOSE : 4 +# +# Note that FATAL will cause the MCU to reboot! + + choice GPIO_OUTPUT_LOGGING + bool "GPIO Outputs logging" + default GPIO_OUTPUT_LOGGING_MINIMAL + depends on GPIO_OUTPUTS + config GPIO_OUTPUT_LOGGING_VERBOSE + bool "Verbose" + config GPIO_OUTPUT_LOGGING_MINIMAL + bool "Minimal" + endchoice + config GPIO_OUTPUT_LOG_LEVEL + int + depends on GPIO_OUTPUTS + default 4 if GPIO_OUTPUT_LOGGING_MINIMAL + default 3 if GPIO_OUTPUT_LOGGING_VERBOSE + default 5 + + choice GPIO_SENSOR_LOGGING + bool "GPIO Sensors logging" + default GPIO_SENSOR_LOGGING_MINIMAL + depends on GPIO_SENSORS + config GPIO_SENSOR_LOGGING_VERBOSE + bool "Verbose" + config GPIO_SENSOR_LOGGING_MINIMAL + bool "Minimal" + endchoice + config GPIO_SENSOR_LOG_LEVEL + int + depends on GPIO_SENSORS + default 4 if GPIO_SENSOR_LOGGING_MINIMAL + default 3 if GPIO_SENSOR_LOGGING_VERBOSE + default 5 +endmenu + +menu "Remote Sensors" + depends on GPIO_SENSORS + + config REMOTE_SENSORS_DECAY + int "Number of milliseconds until a remote sensor will automatically clear" + default 60000 + help + If a remote sensor does not report it's state within this + number of milliseconds it will automatically be deactivated. + + config REMOTE_SENSORS_FIRST_SENSOR + int "First ID to assign remote sensors" + default 100 + help + All sensors must have a unique ID number assigned to them, this + value allows configuring what the first sensor ID will be for + any remote sensors that report to the command station. +endmenu + +menu "S88 Sensors" + depends on GPIO_S88 + + config GPIO_S88_CLOCK_PIN + int "S88 Clock pin" + range 0 32 + default 17 + help + This pin is used to advance the sensor input data to the next + sensor. + + config GPIO_S88_RESET_PIN + int "S88 Reset pin" + range -1 32 + default 16 + help + This pin will be sent a pulse when initializing the read cycle + for the sensors on the bus. This behavior can be disabled by + setting this to -1. + + config GPIO_S88_LOAD_PIN + int "S88 Load pin" + range 0 32 + default 27 + help + This pin is used to tell the sensors on the bus to load their + current state for reading. + + config GPIO_S88_FIRST_SENSOR + int "First S88 sensor ID" + range 0 512 + default 512 + help + This will be used for the first sensor bus and subsequent buses + will start at the next sequential block of IDs. The block size + is defined by the sensors per bus count below. + + config GPIO_S88_SENSORS_PER_BUS + int "S88 sensors per bus" + range 8 512 + default 512 + help + This is the maximum number of inputs that are supported per + bus. In most cases the default of 512 is sufficient but if + you have smaller buses and want to have sensor IDs more + closely indexed this value can be adjusted to a lower value. + + choice GPIO_S88_SENSOR_LOGGING + bool "S88 Sensors logging" + default GPIO_S88_SENSOR_LOGGING_MINIMAL + depends on GPIO_S88 + config GPIO_S88_SENSOR_LOGGING_VERBOSE + bool "Verbose" + config GPIO_S88_SENSOR_LOGGING_MINIMAL + bool "Minimal" + endchoice + config GPIO_S88_SENSOR_LOG_LEVEL + int + depends on GPIO_S88 + default 4 if GPIO_S88_SENSOR_LOGGING_MINIMAL + default 3 if GPIO_S88_SENSOR_LOGGING_VERBOSE + default 5 + +endmenu \ No newline at end of file diff --git a/components/GPIO/Outputs.cpp b/components/GPIO/Outputs.cpp new file mode 100644 index 00000000..cb94c30a --- /dev/null +++ b/components/GPIO/Outputs.cpp @@ -0,0 +1,366 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2017-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "sdkconfig.h" + +#if defined(CONFIG_GPIO_OUTPUTS) + +#include +#include +#include +#include +#include + +#include "Outputs.h" + +std::vector> outputs; +#include + +static constexpr const char * OUTPUTS_JSON_FILE = "outputs.json"; + +void OutputManager::init() +{ + LOG(INFO, "[Output] Initializing outputs"); + nlohmann::json root = nlohmann::json::parse(Singleton::instance()->load(OUTPUTS_JSON_FILE)); + if(root.contains(JSON_COUNT_NODE)) + { + uint16_t outputCount = root[JSON_COUNT_NODE].get(); + Singleton::instance()->status("Found %02d Outputs" + , outputCount); + for(auto output : root[JSON_OUTPUTS_NODE]) + { + string data = output.dump(); + outputs.push_back(std::make_unique(data)); + } + } + LOG(INFO, "[Output] Loaded %d outputs", outputs.size()); +} + +void OutputManager::clear() +{ + outputs.clear(); +} + +uint16_t OutputManager::store() +{ + nlohmann::json root; + uint16_t outputStoredCount = 0; + for (const auto& output : outputs) + { + root[JSON_OUTPUTS_NODE].push_back(output->toJson()); + outputStoredCount++; + } + root[JSON_COUNT_NODE] = outputStoredCount; + Singleton::instance()->store(OUTPUTS_JSON_FILE, root.dump()); + return outputStoredCount; +} + +string OutputManager::set(uint16_t id, bool active) +{ + for (const auto& output : outputs) + { + if(output->getID() == id) + { + return output->set(active); + } + } + return COMMAND_FAILED_RESPONSE; +} + +Output *OutputManager::getOutput(uint16_t id) +{ + auto const &ent = std::find_if(outputs.begin(), outputs.end(), + [id](std::unique_ptr & output) -> bool + { + return output->getID() == id; + }); + if (ent != outputs.end()) + { + return ent->get(); + } + return nullptr; +} + +bool OutputManager::toggle(uint16_t id) +{ + auto output = getOutput(id); + if (output) + { + output->set(!output->isActive()); + return true; + } + return false; +} + +std::string OutputManager::getStateAsJson() +{ + string state = "["; + for (const auto& output : outputs) + { + if (state.length() > 1) + { + state += ","; + } + state += output->toJson(true); + } + state += "]"; + return state; +} + +string OutputManager::get_state_for_dccpp() +{ + string status; + for (const auto& output : outputs) + { + status += output->get_state_for_dccpp(); + } + return status; +} + +bool OutputManager::createOrUpdate(const uint16_t id, const gpio_num_t pin, const uint8_t flags) +{ + for (const auto& output : outputs) + { + if(output->getID() == id) + { + output->update(pin, flags); + return true; + } + } + if(is_restricted_pin(pin)) + { + return false; + } + outputs.push_back(std::make_unique(id, pin, flags)); + return true; +} + +bool OutputManager::remove(const uint16_t id) +{ + const auto & ent = std::find_if(outputs.begin(), outputs.end(), + [id](std::unique_ptr & output) -> bool + { + return output->getID() == id; + }); + if (ent != outputs.end()) + { + LOG(INFO, "[Output] Removing Output(%d)", (*ent)->getID()); + outputs.erase(ent); + return true; + } + return false; +} + +Output::Output(uint16_t id, gpio_num_t pin, uint8_t flags) : _id(id), _pin(pin), _flags(flags), _active(false) +{ + gpio_pad_select_gpio(_pin); + ESP_ERROR_CHECK(gpio_set_direction(_pin, GPIO_MODE_OUTPUT)); + + if((_flags & OUTPUT_IFLAG_RESTORE_STATE) == OUTPUT_IFLAG_RESTORE_STATE) + { + if((_flags & OUTPUT_IFLAG_FORCE_STATE) == OUTPUT_IFLAG_FORCE_STATE) + { + set(true, false); + } + else + { + set(false, false); + } + } + else + { + set(false, false); + } + LOG(CONFIG_GPIO_OUTPUT_LOG_LEVEL + , "[Output] Output(%d) on pin %d created, flags: %s" + , _id, _pin, getFlagsAsString().c_str()); +} + +Output::Output(string &data) +{ + nlohmann::json object = nlohmann::json::parse(data); + _id = object[JSON_ID_NODE].get(); + _pin = (gpio_num_t)object[JSON_PIN_NODE].get(); + _flags = object[JSON_FLAGS_NODE].get(); + gpio_pad_select_gpio((gpio_num_t)_pin); + ESP_ERROR_CHECK(gpio_set_direction((gpio_num_t)_pin, GPIO_MODE_OUTPUT)); + if((_flags & OUTPUT_IFLAG_RESTORE_STATE) == OUTPUT_IFLAG_RESTORE_STATE) + { + set((_flags & OUTPUT_IFLAG_FORCE_STATE) == OUTPUT_IFLAG_FORCE_STATE, false); + } + else + { + set(object[JSON_STATE_NODE].get(), false); + } + LOG(CONFIG_GPIO_OUTPUT_LOG_LEVEL + , "[Output] Output(%d) on pin %d loaded, flags: %s" + , _id, _pin, getFlagsAsString().c_str()); +} + +string Output::set(bool active, bool announce) +{ + _active = active; + ESP_ERROR_CHECK(gpio_set_level((gpio_num_t)_pin, _active)); + LOG(INFO, "[Output] Output(%d) set to %s", _id + , _active ? JSON_VALUE_ON : JSON_VALUE_OFF); + if(announce) + { + return StringPrintf("", _id, !_active); + } + return COMMAND_NO_RESPONSE; +} + +void Output::update(gpio_num_t pin, uint8_t flags) +{ + // reset the current pin + ESP_ERROR_CHECK(gpio_reset_pin(_pin)); + _pin = pin; + _flags = flags; + // setup the new pin + gpio_pad_select_gpio(_pin); + ESP_ERROR_CHECK(gpio_set_direction(_pin, GPIO_MODE_OUTPUT)); + if((_flags & OUTPUT_IFLAG_RESTORE_STATE) != OUTPUT_IFLAG_RESTORE_STATE) + { + set(false, false); + } + else + { + set((_flags & OUTPUT_IFLAG_FORCE_STATE) == OUTPUT_IFLAG_FORCE_STATE, false); + } + LOG(CONFIG_GPIO_OUTPUT_LOG_LEVEL + , "[Output] Output(%d) on pin %d updated, flags: %s", _id, _pin + , getFlagsAsString().c_str()); +} + +string Output::toJson(bool readableStrings) +{ + nlohmann::json object = + { + { JSON_ID_NODE, _id }, + { JSON_PIN_NODE, (uint8_t)_pin }, + }; + if(readableStrings) + { + object[JSON_FLAGS_NODE] = getFlagsAsString(); + object[JSON_STATE_NODE] = isActive() ? JSON_VALUE_ON : JSON_VALUE_OFF; + } + else + { + object[JSON_FLAGS_NODE] = _flags; + object[JSON_STATE_NODE] = _active; + } + return object.dump(); +} + +string Output::getStateAsJson() +{ + return StringPrintf("", _id, _pin, _flags, !_active); +} + +/********************************************************************** + : creates a new output ID, with specified PIN and IFLAG values. + if output ID already exists, it is updated with specificed + PIN and IFLAG. + Note: output state will be immediately set to ACTIVE/INACTIVE + and pin will be set to HIGH/LOW according to IFLAG value + specifcied (see below). + returns: if successful and if unsuccessful (e.g. out of memory) + + : deletes definition of output ID + returns: if successful and if unsuccessful (e.g. ID does not exist) + + : lists all defined output pins + returns: for each defined output pin or if no + output pins defined + +where + + ID: the numeric ID (0-32767) of the output + PIN: the pin number to use for the output + STATE: the state of the output (0=INACTIVE / 1=ACTIVE) + IFLAG: defines the operational behavior of the output based on bits 0, 1, and + 2 as follows: + + IFLAG, bit 0: 0 = forward operation (ACTIVE=HIGH / INACTIVE=LOW) + 1 = inverted operation (ACTIVE=LOW / INACTIVE=HIGH) + + IFLAG, bit 1: 0 = state of pin restored on power-up to either ACTIVE or + INACTIVE depending on state before power-down; state of + pin set to INACTIVE when first created. + 1 = state of pin set on power-up, or when first created, to + either ACTIVE of INACTIVE depending on IFLAG, bit 2. + + IFLAG, bit 2: 0 = state of pin set to INACTIVE upon power-up or when + first created. + 1 = state of pin set to ACTIVE upon power-up or when + first created. + +To change the state of outputs that have been defined use: + + : sets output ID to either ACTIVE or INACTIVE state + returns: , or if turnout ID does not exist +where + ID: the numeric ID (0-32767) of the turnout to control + STATE: the state of the output (0=INACTIVE / 1=ACTIVE) + +**********************************************************************/ + +DCC_PROTOCOL_COMMAND_HANDLER(OutputCommandAdapter, +[](const vector arguments) +{ + if(arguments.empty()) + { + // list all outputs + return OutputManager::get_state_for_dccpp(); + } + else + { + uint16_t outputID = std::stoi(arguments[0]); + if (arguments.size() == 1 && OutputManager::remove(outputID)) + { + // delete output + return COMMAND_SUCCESSFUL_RESPONSE; + } + else if (arguments.size() == 2) + { + // set output state + return OutputManager::set(outputID, arguments[1][0] == 1); + } + else if (arguments.size() == 3) + { + // create output + OutputManager::createOrUpdate(outputID + , (gpio_num_t)std::stoi(arguments[1]) + , std::stoi(arguments[2])); + return COMMAND_SUCCESSFUL_RESPONSE; + } + } + return COMMAND_FAILED_RESPONSE; +}) + +DCC_PROTOCOL_COMMAND_HANDLER(OutputExCommandAdapter, +[](const vector arguments) +{ + uint16_t outputID = std::stoi(arguments[0]); + auto output = OutputManager::getOutput(outputID); + if (output) + { + return output->set(!output->isActive()); + } + return COMMAND_FAILED_RESPONSE; +}) +#endif // CONFIG_GPIO_OUTPUTS \ No newline at end of file diff --git a/components/GPIO/RemoteSensors.cpp b/components/GPIO/RemoteSensors.cpp new file mode 100644 index 00000000..61fd6a37 --- /dev/null +++ b/components/GPIO/RemoteSensors.cpp @@ -0,0 +1,185 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2018 Dan Worth +COPYRIGHT (c) 2018-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "sdkconfig.h" + +#if defined(CONFIG_GPIO_SENSORS) + +#include +#include +#include +#include + +#include "RemoteSensors.h" + +/********************************************************************** + +The ESP32 Command Station supports remote sensor inputs that are connected via a +WiFi connection. Remote Sensors are dynamically created by a remote sensor +reporting its state. + +Note: Remote Sensors should not maintain a persistent connection. Instead they +should connect when a change occurs that should be reported. It is not necessary +for Remote Sensors to report when they are INACTIVE. If a Remote Sensor does not +report within REMOTE_SENSORS_DECAY milliseconds the command station will +automatically transition the Remote Sensor to INACTIVE state if it was +previously ACTIVE. + +The following varations of the "RS" command : + + : Informs the command station of the status of a remote sensor. + : Deletes remote sensor ID. + : Lists all defined remote sensors. + returns: for each defined remote sensor or + if no remote sensors have been defined/found. +where + + ID: the numeric ID (0-32667) of the remote sensor. + STATE: State of the sensors, zero is INACTIVE, non-zero is ACTIVE. + Usage is remote sensor dependent. +**********************************************************************/ + +// TODO: merge this into the base SensorManager code. + +std::vector> remoteSensors; + +void RemoteSensorManager::init() +{ +} + +void RemoteSensorManager::createOrUpdate(const uint16_t id, const uint16_t value) { + // check for duplicate ID + for (const auto& sensor : remoteSensors) + { + if(sensor->getRawID() == id) + { + sensor->setSensorValue(value); + return; + } + } + remoteSensors.push_back(std::make_unique(id, value)); +} + +bool RemoteSensorManager::remove(const uint16_t id) +{ + auto ent = std::find_if(remoteSensors.begin(), remoteSensors.end(), + [id](std::unique_ptr & sensor) -> bool + { + return sensor->getID() == id; + }); + if (ent != remoteSensors.end()) + { + remoteSensors.erase(ent); + return true; + } + return false; +} + +string RemoteSensorManager::getStateAsJson() +{ + string output = "["; + for (const auto& sensor : remoteSensors) + { + if (output.length() > 1) + { + output += ","; + } + output += sensor->toJson(); + } + output += "]"; + return output; +} + +string RemoteSensorManager::get_state_for_dccpp() +{ + if (remoteSensors.empty()) + { + return COMMAND_FAILED_RESPONSE; + } + string status; + for (const auto& sensor : remoteSensors) + { + status += sensor->get_state_for_dccpp(); + } + return status; +} + +RemoteSensor::RemoteSensor(uint16_t id, uint16_t value) : + Sensor(id + CONFIG_REMOTE_SENSORS_FIRST_SENSOR, NON_STORED_SENSOR_PIN, false, false), _rawID(id) +{ + setSensorValue(value); + LOG(CONFIG_GPIO_SENSOR_LOG_LEVEL + , "[RemoteSensors] RemoteSensor(%d) created with Sensor(%d), active: %s, value: %d" + , getRawID(), getID(), isActive() ? JSON_VALUE_TRUE : JSON_VALUE_FALSE, value); +} + +void RemoteSensor::check() +{ + if(isActive() && (esp_timer_get_time() / 1000ULL) > _lastUpdate + CONFIG_REMOTE_SENSORS_DECAY) + { + LOG(INFO, "[RemoteSensors] RemoteSensor(%d) expired, deactivating", getRawID()); + setSensorValue(0); + } +} + +string RemoteSensor::get_state_for_dccpp() +{ + return StringPrintf("", getRawID(), _value); +} + +string RemoteSensor::toJson(bool includeState) +{ + nlohmann::json object = + { + { JSON_ID_NODE, getRawID() }, + { JSON_VALUE_NODE, getSensorValue() }, + { JSON_STATE_NODE, isActive() }, + { JSON_LAST_UPDATE_NODE, getLastUpdate() }, + { JSON_PIN_NODE, getPin() }, + { JSON_PULLUP_NODE, isPullUp() }, + }; + return object.dump(); +} + +DCC_PROTOCOL_COMMAND_HANDLER(RemoteSensorsCommandAdapter, +[](const vector arguments) +{ + if(arguments.empty()) + { + // list all sensors + return RemoteSensorManager::get_state_for_dccpp(); + } + else + { + uint16_t sensorID = std::stoi(arguments[0]); + if (arguments.size() == 1 && RemoteSensorManager::remove(sensorID)) + { + // delete remote sensor + return COMMAND_SUCCESSFUL_RESPONSE; + } + else if (arguments.size() == 2) + { + // create/update remote sensor + RemoteSensorManager::createOrUpdate(sensorID, std::stoi(arguments[1])); + return COMMAND_SUCCESSFUL_RESPONSE; + } + } + return COMMAND_FAILED_RESPONSE; +}) + +#endif // CONFIG_GPIO_SENSORS \ No newline at end of file diff --git a/components/GPIO/S88Sensors.cpp b/components/GPIO/S88Sensors.cpp new file mode 100644 index 00000000..90c25e46 --- /dev/null +++ b/components/GPIO/S88Sensors.cpp @@ -0,0 +1,422 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2017-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +/********************************************************************** + +The ESP32 Command Station supports multiple S88 Sensor busses. + +To have the command station monitor an S88 sensor, first define/edit/delete +an S88 Sensor Bus using the following variations on the "S88" command: + : Creates an S88 Sensor Bus with the specified + ID, DATA PIN, SENSOR COUNT. + returns: if successful and if unsuccessful. + : Deletes definition of S88 Sensor Bus ID and all + associated S88 Sensors on the bus. + returns: if successful and if unsuccessful. + : Lists all S88 Sensor Busses and state of sensors. + returns: for each S88 Sensor Bus or + if no busses have been defined +Note: S88 Sensor Busses will create individual sensors that report via but +they can not be edited/deleted via commands. Attempts to do that will result +in an being returned. + +S88 Sensors are reported in the same manner as generic Sensors: + - for activation of S88 Sensor ID. + - for deactivation of S88 Sensor ID. + +**********************************************************************/ +#include "sdkconfig.h" + +#if defined(CONFIG_GPIO_S88) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "S88Sensors.h" + +///////////////////////////////////////////////////////////////////////////////////// +// S88 Timing values (in microseconds) +///////////////////////////////////////////////////////////////////////////////////// +constexpr uint16_t S88_SENSOR_LOAD_PRE_CLOCK_TIME = 50; +constexpr uint16_t S88_SENSOR_LOAD_POST_RESET_TIME = 50; +constexpr uint16_t S88_SENSOR_CLOCK_PULSE_TIME = 50; +constexpr uint16_t S88_SENSOR_CLOCK_PRE_RESET_TIME = 50; +constexpr uint16_t S88_SENSOR_RESET_PULSE_TIME = 50; +constexpr uint16_t S88_SENSOR_READ_TIME = 25; + +static constexpr const char * S88_SENSORS_JSON_FILE = "s88.json"; + +GPIO_PIN(S88_CLOCK, GpioOutputSafeLow, CONFIG_GPIO_S88_CLOCK_PIN); +GPIO_PIN(S88_LOAD, GpioOutputSafeLow, CONFIG_GPIO_S88_LOAD_PIN); +#if CONFIG_GPIO_S88_RESET_PIN >= 0 +GPIO_PIN(S88_RESET, GpioOutputSafeLow, CONFIG_GPIO_S88_RESET_PIN); +#else +typedef DummyPin S88_RESET_Pin; +#endif + +typedef GpioInitializer S88PinInit; + +void *s88_task(void *param) +{ + S88BusManager *s88 = static_cast(param); + while (true) + { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + s88->poll(); + } +} + +S88BusManager::S88BusManager(openlcb::Node *node) : poller_(node, {this}) +{ +#if CONFIG_GPIO_S88_RESET_PIN >= 0 + LOG(INFO, "[S88] Configuration (clock: %d, reset: %d, load: %d)" + , S88_CLOCK_Pin::pin(), S88_RESET_Pin::pin(), S88_LOAD_Pin::pin()); +#else + LOG(INFO, "[S88] Configuration (clock: %d, load: %d)" + , S88_CLOCK_Pin::pin(), S88_LOAD_Pin::pin()); +#endif + + S88PinInit::hw_init(); + + LOG(INFO, "[S88] Initializing SensorBus list"); + nlohmann::json root = nlohmann::json::parse( + Singleton::instance()->load(S88_SENSORS_JSON_FILE)); + for (auto bus : root[JSON_SENSORS_NODE]) + { + buses_.push_back( + std::make_unique(bus[JSON_ID_NODE], bus[JSON_PIN_NODE] + , bus[JSON_COUNT_NODE])); + } + LOG(INFO, "[S88] Loaded %d Sensor Buses", buses_.size()); + os_thread_create(&taskHandle_, "s88", 1, 2048, s88_task, this); +} + +S88BusManager::~S88BusManager() +{ + poller_.stop(); + vTaskDelete(taskHandle_); +} + +void S88BusManager::clear() +{ + buses_.clear(); +} + +uint16_t S88BusManager::store() +{ + AtomicHolder l(this); + uint16_t count = 0; + string content = "["; + for (const auto& bus : buses_) + { + // only add the seperator if we have already serialized at least one + // bus. + if (content.length() > 1) + { + content += ","; + } + content += bus->toJson(); + count += bus->getSensorCount(); + } + content += "]"; + Singleton::instance()->store(S88_SENSORS_JSON_FILE, content); + return count; +} + +void S88BusManager::poll_33hz(openlcb::WriteHelper *helper, Notifiable *done) +{ + AutoNotify n(done); + + // wake up background task for polling + xTaskNotifyGive(taskHandle_); +} + +void S88BusManager::poll() +{ + AtomicHolder l(this); + for (const auto& sensorBus : buses_) + { + sensorBus->prepForRead(); + } + S88_LOAD_Pin::set(true); + ets_delay_us(S88_SENSOR_LOAD_PRE_CLOCK_TIME); + S88_CLOCK_Pin::set(true); + ets_delay_us(S88_SENSOR_CLOCK_PULSE_TIME); + S88_CLOCK_Pin::set(false); + ets_delay_us(S88_SENSOR_CLOCK_PRE_RESET_TIME); + S88_RESET_Pin::set(true); + ets_delay_us(S88_SENSOR_RESET_PULSE_TIME); + S88_RESET_Pin::set(false); + ets_delay_us(S88_SENSOR_LOAD_POST_RESET_TIME); + S88_LOAD_Pin::set(false); + + ets_delay_us(S88_SENSOR_READ_TIME); + bool keepReading = true; + while (keepReading) + { + keepReading = false; + for (const auto& sensorBus : buses_) + { + if (sensorBus->hasMore()) + { + keepReading = true; + sensorBus->readNext(); + } + } + S88_CLOCK_Pin::set(true); + ets_delay_us(S88_SENSOR_CLOCK_PULSE_TIME); + S88_CLOCK_Pin::set(false); + ets_delay_us(S88_SENSOR_READ_TIME); + } +} + +bool S88BusManager::createOrUpdateBus(const uint8_t id, const gpio_num_t dataPin, const uint16_t sensorCount) +{ + // check for duplicate data pin + for (const auto& sensorBus : buses_) + { + if (sensorBus->getID() != id && sensorBus->getDataPin() == dataPin) + { + LOG_ERROR("[S88] Bus %d is already using data pin %d, rejecting create/update of S88 Bus %d", + sensorBus->getID(), dataPin, id); + return false; + } + } + AtomicHolder l(this); + // check for existing bus to be updated + for (const auto& sensorBus : buses_) + { + if (sensorBus->getID() == id) + { + sensorBus->update(dataPin, sensorCount); + return true; + } + } + if (is_restricted_pin(dataPin)) + { + LOG_ERROR("[S88] Attempt to use a restricted pin: %d", dataPin); + return false; + } + buses_.push_back(std::make_unique(id, dataPin, sensorCount)); + return true; +} + +bool S88BusManager::removeBus(const uint8_t id) +{ + AtomicHolder l(this); + const auto & ent = std::find_if(buses_.begin(), buses_.end(), + [id](std::unique_ptr & bus) -> bool + { + return bus->getID() == id; + }); + if (ent != buses_.end()) + { + buses_.erase(ent); + return true; + } + return false; +} + +string S88BusManager::get_state_as_json() +{ + string state = "["; + for (const auto& sensorBus : buses_) + { + if (state.length() > 1) + { + state += ","; + } + state += sensorBus->toJson(true); + } + state += "]"; + return state; +} + +string S88BusManager::get_state_for_dccpp() +{ + string res; + for (const auto& sensorBus : buses_) + { + res += sensorBus->get_state_for_dccpp(); + } + return res; +} + +S88SensorBus::S88SensorBus(const uint8_t id, const gpio_num_t dataPin, const uint16_t sensorCount) : + _id(id), _dataPin(dataPin), _sensorIDBase((id * CONFIG_GPIO_S88_SENSORS_PER_BUS) + CONFIG_GPIO_S88_FIRST_SENSOR), + _lastSensorID((id * CONFIG_GPIO_S88_SENSORS_PER_BUS) + CONFIG_GPIO_S88_FIRST_SENSOR) +{ + LOG(INFO, "[S88 Bus-%d] Created using data pin %d with %d sensors starting at id %d", + _id, _dataPin, sensorCount, _sensorIDBase); + gpio_pad_select_gpio((gpio_num_t)_dataPin); + ESP_ERROR_CHECK(gpio_set_direction((gpio_num_t)_dataPin, GPIO_MODE_INPUT)); + if (sensorCount > 0) + { + addSensors(sensorCount); + } +} + +void S88SensorBus::update(const gpio_num_t dataPin, const uint16_t sensorCount) +{ + _dataPin = dataPin; + _lastSensorID = _sensorIDBase; + gpio_pad_select_gpio(_dataPin); + ESP_ERROR_CHECK(gpio_set_direction(_dataPin, GPIO_MODE_INPUT)); + for (const auto& sensor : _sensors) + { + sensor->updateID(_lastSensorID++); + } + if (sensorCount < _sensors.size()) + { + removeSensors(_sensors.size() - sensorCount); + } + else if (sensorCount > 0) + { + addSensors(sensorCount - _sensors.size()); + } + LOG(INFO, "[S88 Bus-%d] Updated to use data pin %d with %d sensors", + _id, _dataPin, _sensors.size()); +} + +string S88SensorBus::toJson(bool includeState) +{ + string serialized = StringPrintf( + "{\"%s\":%d,\"%s\":%d,\"%s\":%d,\"%s\":%d" + , JSON_ID_NODE, _id, JSON_PIN_NODE, _dataPin + , JSON_S88_SENSOR_BASE_NODE, _sensorIDBase, JSON_COUNT_NODE, _sensors.size()); + if (includeState) + { + serialized += StringPrintf("\"%s\":\"%s\"", JSON_STATE_NODE, getStateString().c_str()); + } + serialized += "}"; + return serialized; +} + +void S88SensorBus::addSensors(int16_t sensorCount) +{ + const uint16_t startingIndex = _sensors.size(); + for (uint8_t id = 0; id < sensorCount; id++) + { + _sensors.push_back(new S88Sensor(_lastSensorID++, startingIndex + id)); + } +} + +void S88SensorBus::removeSensors(int16_t sensorCount) +{ + if (sensorCount < 0) + { + for (const auto& sensor : _sensors) + { + LOG(CONFIG_GPIO_S88_SENSOR_LOG_LEVEL, "[S88] Sensor(%d) removed" + , sensor->getID()); + } + _sensors.clear(); + } + else + { + for (uint8_t id = 0; id < sensorCount; id++) + { + LOG(CONFIG_GPIO_S88_SENSOR_LOG_LEVEL, "[S88] Sensor(%d) removed" + , _sensors.back()->getID()); + _sensors.pop_back(); + } + } +} + +string S88SensorBus::getStateString() +{ + string state = ""; + for (const auto& sensor : _sensors) + { + if (sensor->isActive()) + { + state += "1"; + } + else + { + state += "0"; + } + } + return state; +} + +void S88SensorBus::readNext() +{ + // sensors need to pull pin LOW for ACTIVE + _sensors[_nextSensorToRead++]->setState(gpio_get_level(_dataPin)); +} + +string S88SensorBus::get_state_for_dccpp() +{ + string status = StringPrintf("", _id, _dataPin, _sensors.size()); + LOG(CONFIG_GPIO_S88_SENSOR_LOG_LEVEL + , "[S88 Bus-%d] Data:%d, Base:%d, Count:%d:" + , _id, _dataPin, _sensorIDBase, _sensors.size()); + for (const auto& sensor : _sensors) + { + LOG(CONFIG_GPIO_S88_SENSOR_LOG_LEVEL, "[S88] Input: %d :: %s" + , sensor->getIndex(), sensor->isActive() ? "ACTIVE" : "INACTIVE"); + status += sensor->get_state_for_dccpp(); + } + return status; +} + +S88Sensor::S88Sensor(uint16_t id, uint16_t index) + : Sensor(id, NON_STORED_SENSOR_PIN, false, false, true), _index(index) +{ + LOG(CONFIG_GPIO_S88_SENSOR_LOG_LEVEL + , "[S88] Sensor(%d) created with index %d", id, _index); + +} + +DCC_PROTOCOL_COMMAND_HANDLER(S88BusCommandAdapter, +[](const vector arguments) +{ + auto s88 = S88BusManager::instance(); + if (arguments.empty()) + { + // list all sensor groups + return s88->get_state_for_dccpp(); + } + else + { + if (arguments.size() == 1 && + s88->removeBus(std::stoi(arguments[0]))) + { + // delete sensor bus + return COMMAND_SUCCESSFUL_RESPONSE; + } + else if (arguments.size() == 3 && + s88->createOrUpdateBus(std::stoi(arguments[0]) + , (gpio_num_t)std::stoi(arguments[1]) + , std::stoi(arguments[2]))) + { + // create sensor bus + return COMMAND_SUCCESSFUL_RESPONSE; + } + } + return COMMAND_FAILED_RESPONSE; +}) + +#endif // CONFIG_GPIO_S88 diff --git a/components/GPIO/Sensors.cpp b/components/GPIO/Sensors.cpp new file mode 100644 index 00000000..dfa2ba84 --- /dev/null +++ b/components/GPIO/Sensors.cpp @@ -0,0 +1,325 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2017-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "sdkconfig.h" + +#if defined(CONFIG_GPIO_SENSORS) + +#include +#include +#include +#include +#include +#include + +#include "Sensors.h" +#include "RemoteSensors.h" + +std::vector> sensors; + +TaskHandle_t SensorManager::_taskHandle; +OSMutex SensorManager::_lock; +static constexpr UBaseType_t SENSOR_TASK_PRIORITY = 1; +static constexpr uint32_t SENSOR_TASK_STACK_SIZE = 2048; + +static constexpr const char * SENSORS_JSON_FILE = "sensors.json"; + +void SensorManager::init() +{ + LOG(INFO, "[Sensors] Initializing sensors"); + nlohmann::json root = nlohmann::json::parse(Singleton::instance()->load(SENSORS_JSON_FILE)); + if(root.contains(JSON_COUNT_NODE)) + { + uint16_t sensorCount = root[JSON_COUNT_NODE].get(); + Singleton::instance()->status("Found %02d Sensors", sensorCount); + for(auto sensor : root[JSON_SENSORS_NODE]) + { + string data = sensor.dump(); + sensors.push_back(std::make_unique(data)); + } + } + LOG(INFO, "[Sensors] Loaded %d sensors", sensors.size()); + xTaskCreate(sensorTask, "SensorManager", SENSOR_TASK_STACK_SIZE, NULL, SENSOR_TASK_PRIORITY, &_taskHandle); +} + +void SensorManager::clear() +{ + sensors.clear(); +} + +uint16_t SensorManager::store() +{ + nlohmann::json root; + uint16_t sensorStoredCount = 0; + for (const auto& sensor : sensors) + { + if (sensor->getPin() != NON_STORED_SENSOR_PIN) + { + root[JSON_SENSORS_NODE].push_back(sensor->toJson()); + sensorStoredCount++; + } + } + root[JSON_COUNT_NODE] = sensorStoredCount; + Singleton::instance()->store(SENSORS_JSON_FILE, root.dump()); + return sensorStoredCount; +} + +void SensorManager::sensorTask(void *param) +{ + while(true) + { + { + OSMutexLock l(&_lock); + for (const auto& sensor : sensors) + { + if (sensor->getPin() != NON_STORED_SENSOR_PIN) + { + sensor->check(); + } + } + } + vTaskDelay(pdMS_TO_TICKS(50)); + } +} + +string SensorManager::getStateAsJson() +{ + string status; + for (const auto& sensor : sensors) + { + status += sensor->toJson(true); + } + return status; +} + +Sensor *SensorManager::getSensor(uint16_t id) +{ + OSMutexLock l(&_lock); + const auto & ent = std::find_if(sensors.begin(), sensors.end(), + [id](std::unique_ptr & sensor) -> bool + { + return sensor->getID() == id; + }); + if (ent != sensors.end()) + { + return ent->get(); + } + return nullptr; +} + +bool SensorManager::createOrUpdate(const uint16_t id, const gpio_num_t pin, const bool pullUp) +{ + if(is_restricted_pin(pin)) + { + return false; + } + OSMutexLock l(&_lock); + auto sens = getSensor(id); + if (sens) + { + sens->update(pin, pullUp); + return true; + } + // add the new sensor + sensors.push_back(std::make_unique(id, pin, pullUp)); + return true; +} + +bool SensorManager::remove(const uint16_t id) +{ + OSMutexLock l(&_lock); + const auto & ent = std::find_if(sensors.begin(), sensors.end(), + [id](std::unique_ptr & sensor) -> bool + { + return sensor->getID() == id; + }); + if (ent != sensors.end()) + { + LOG(INFO, "[Sensors] Removing Sensor(%d)", (*ent)->getID()); + sensors.erase(ent); + return true; + } + return false; +} + +gpio_num_t SensorManager::getSensorPin(const uint16_t id) +{ + auto sens = getSensor(id); + if (sens) + { + return sens->getPin(); + } + return NON_STORED_SENSOR_PIN; +} + +string SensorManager::get_state_for_dccpp() +{ + string res; + for (const auto &sensor : sensors) + { + res += sensor->get_state_for_dccpp(); + } + return res; +} + +Sensor::Sensor(uint16_t sensorID, gpio_num_t pin, bool pullUp, bool announce, bool initialState) + : _sensorID(sensorID), _pin(pin), _pullUp(pullUp), _lastState(initialState) +{ + if (_pin != NON_STORED_SENSOR_PIN) + { + if (announce) + { + LOG(CONFIG_GPIO_SENSOR_LOG_LEVEL + , "[Sensors] Sensor(%d) on pin %d created, pullup %s", _sensorID, _pin + , _pullUp ? "Enabled" : "Disabled"); + } + gpio_pad_select_gpio(_pin); + ESP_ERROR_CHECK(gpio_set_direction(_pin, GPIO_MODE_INPUT)); + if (pullUp) + { + ESP_ERROR_CHECK(gpio_pullup_en(_pin)); + } + } +} + +Sensor::Sensor(string &data) : _lastState(false) +{ + nlohmann::json object = nlohmann::json::parse(data); + _sensorID = object[JSON_ID_NODE]; + _pin = (gpio_num_t)object[JSON_PIN_NODE]; + _pullUp = object[JSON_PULLUP_NODE]; + LOG(CONFIG_GPIO_SENSOR_LOG_LEVEL + , "[Sensors] Sensor(%d) on pin %d loaded, pullup %s", _sensorID, _pin + , _pullUp ? "Enabled" : "Disabled"); + gpio_pad_select_gpio(_pin); + ESP_ERROR_CHECK(gpio_set_direction(_pin, GPIO_MODE_INPUT)); + if (_pullUp) + { + ESP_ERROR_CHECK(gpio_pullup_en(_pin)); + } +} + +string Sensor::toJson(bool includeState) +{ + nlohmann::json object = + { + { JSON_ID_NODE, _sensorID }, + { JSON_PIN_NODE, (uint8_t)_pin }, + { JSON_PULLUP_NODE, _pullUp }, + }; + if (includeState) + { + object[JSON_STATE_NODE] = _lastState; + } + return object.dump(); +} + +void Sensor::update(gpio_num_t pin, bool pullUp) +{ + ESP_ERROR_CHECK(gpio_reset_pin(_pin)); + _pin = pin; + _pullUp = pullUp; + LOG(CONFIG_GPIO_SENSOR_LOG_LEVEL + , "[Sensors] Sensor(%d) on pin %d updated, pullup %s", _sensorID, _pin + , _pullUp ? "Enabled" : "Disabled"); + gpio_pad_select_gpio(_pin); + ESP_ERROR_CHECK(gpio_set_direction(_pin, GPIO_MODE_INPUT)); + if (_pullUp) + { + ESP_ERROR_CHECK(gpio_pullup_en(_pin)); + } +} + +void Sensor::check() +{ + set(gpio_get_level(_pin)); +} + +string Sensor::get_state_for_dccpp() +{ + return StringPrintf("", _sensorID, _pin, _pullUp); +} + +string Sensor::set(bool state) +{ + if (_lastState != state) + { + _lastState = state; + LOG(INFO, "Sensor: %d :: %s", _sensorID, _lastState ? "ACTIVE" : "INACTIVE"); + // TODO: find a way to send this out on the JMRI interface + return StringPrintf("<%c %d>", state ? 'Q' : 'q', _sensorID); + } + return COMMAND_NO_RESPONSE; +} + + +/********************************************************************** + : creates a new sensor ID, with specified PIN and PULLUP + if sensor ID already exists, it is updated with + specificed PIN and PULLUP. + returns: if successful and if unsuccessful (e.g. out of memory) + + : deletes definition of sensor ID. + returns: if successful and if unsuccessful (e.g. ID does not exist) + + : lists all defined sensors. + returns: for each defined sensor or if no sensors + defined + +where + + ID: the numeric ID (0-32767) of the sensor + PIN: the pin number the sensor is connected to + PULLUP: 1=use internal pull-up resistor for PIN, 0=don't use internal pull-up + resistor for PIN + + - for transition of Sensor ID from HIGH state to LOW state + (i.e. the sensor is triggered) + - for transition of Sensor ID from LOW state to HIGH state + (i.e. the sensor is no longer triggered) +**********************************************************************/ + +DCC_PROTOCOL_COMMAND_HANDLER(SensorCommandAdapter, +[](const vector arguments) +{ + if(arguments.empty()) + { + // list all sensors + string status = SensorManager::get_state_for_dccpp(); + status += RemoteSensorManager::get_state_for_dccpp(); + return status; + } + else + { + uint16_t sensorID = std::stoi(arguments[0]); + if (arguments.size() == 1 && SensorManager::remove(sensorID)) + { + // delete turnout + return COMMAND_SUCCESSFUL_RESPONSE; + } + else if (arguments.size() == 3) + { + // create sensor + SensorManager::createOrUpdate(sensorID + , (gpio_num_t)std::stoi(arguments[1]) + , arguments[2][0] == '1'); + return COMMAND_SUCCESSFUL_RESPONSE; + } + } + return COMMAND_FAILED_RESPONSE; +}) +#endif // CONFIG_GPIO_SENSORS \ No newline at end of file diff --git a/components/GPIO/include/Outputs.h b/components/GPIO/include/Outputs.h new file mode 100644 index 00000000..1cffae89 --- /dev/null +++ b/components/GPIO/include/Outputs.h @@ -0,0 +1,111 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2017-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef OUTPUTS_H_ +#define OUTPUTS_H_ + +#include +#include +#include + +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(OutputCommandAdapter, "Z", 0) +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(OutputExCommandAdapter, "Zex", 1) + +const uint8_t OUTPUT_IFLAG_INVERT = BIT0; +const uint8_t OUTPUT_IFLAG_RESTORE_STATE = BIT1; +const uint8_t OUTPUT_IFLAG_FORCE_STATE = BIT2; + +class Output +{ +public: + Output(uint16_t, gpio_num_t, uint8_t); + Output(std::string &); + std::string set(bool=false, bool=true); + void update(gpio_num_t, uint8_t); + std::string toJson(bool=false); + uint16_t getID() + { + return _id; + } + gpio_num_t getPin() + { + return _pin; + } + uint8_t getFlags() + { + return _flags; + } + bool isActive() + { + return _active; + } + std::string get_state_for_dccpp() + { + return StringPrintf("", _id, !_active); + } + std::string getStateAsJson(); + std::string getFlagsAsString() + { + std::string flags = ""; + if((_flags & OUTPUT_IFLAG_INVERT) == OUTPUT_IFLAG_INVERT) + { + flags += "activeLow"; + } + else + { + flags += "activeHigh"; + } + if((_flags & OUTPUT_IFLAG_RESTORE_STATE) == OUTPUT_IFLAG_RESTORE_STATE) + { + if((_flags & OUTPUT_IFLAG_FORCE_STATE) == OUTPUT_IFLAG_FORCE_STATE) + { + flags += ",force(on)"; + } + else + { + flags += ",force(off)"; + } + } + else + { + flags += ",restoreState"; + } + return flags; + } +private: + uint16_t _id; + gpio_num_t _pin; + uint8_t _flags; + bool _active; +}; + +class OutputManager +{ + public: + static void init(); + static void clear(); + static uint16_t store(); + static std::string set(uint16_t, bool=false); + static Output *getOutput(uint16_t); + static bool toggle(uint16_t); + static std::string getStateAsJson(); + static std::string get_state_for_dccpp(); + static bool createOrUpdate(const uint16_t, const gpio_num_t, const uint8_t); + static bool remove(const uint16_t); +}; + +#endif // OUTPUTS_H_ \ No newline at end of file diff --git a/include/RemoteSensors.h b/components/GPIO/include/RemoteSensors.h similarity index 66% rename from include/RemoteSensors.h rename to components/GPIO/include/RemoteSensors.h index 3b999dcd..2d69bc56 100644 --- a/include/RemoteSensors.h +++ b/components/GPIO/include/RemoteSensors.h @@ -1,7 +1,7 @@ /********************************************************************** ESP32 COMMAND STATION -COPYRIGHT (c) 2018-2019 Mike Dunston +COPYRIGHT (c) 2018-2020 Mike Dunston COPYRIGHT (c) 2018 Dan Worth This program is free software: you can redistribute it and/or modify @@ -16,52 +16,54 @@ COPYRIGHT (c) 2018 Dan Worth along with this program. If not, see http://www.gnu.org/licenses **********************************************************************/ -#pragma once +#ifndef REMOTE_SENSORS_H_ +#define REMOTE_SENSORS_H_ + +#include -#include #include "Sensors.h" -#include "DCCppProtocol.h" -class RemoteSensor : public Sensor { +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(RemoteSensorsCommandAdapter, "RS", 0) + +class RemoteSensor : public Sensor +{ public: RemoteSensor(uint16_t, uint16_t=0); virtual ~RemoteSensor() {} - const uint16_t getRawID() { + uint16_t getRawID() { return _rawID; } - const uint16_t getSensorValue() { + uint16_t getSensorValue() + { return _value; } - void setSensorValue(const uint16_t value) { + void setSensorValue(const uint16_t value) + { _value = value; - _lastUpdate = millis(); + _lastUpdate = esp_timer_get_time() / 1000ULL; set(_value != 0); } - const uint32_t getLastUpdate() { + uint32_t getLastUpdate() + { return _lastUpdate; } virtual void check(); - void showSensor(); - virtual void toJson(JsonObject &, bool=false); + std::string get_state_for_dccpp() override; + virtual std::string toJson(bool=false) override; private: uint16_t _rawID; uint16_t _value; uint32_t _lastUpdate; }; -class RemoteSensorManager { +class RemoteSensorManager +{ public: static void init(); - static void show(); static void createOrUpdate(const uint16_t, const uint16_t=0); static bool remove(const uint16_t); - static void getState(JsonArray &); + static std::string getStateAsJson(); + static std::string get_state_for_dccpp(); }; -class RemoteSensorsCommandAdapter : public DCCPPProtocolCommand { -public: - void process(const std::vector); - String getID() { - return "RS"; - } -}; +#endif // REMOTE_SENSORS_H_ \ No newline at end of file diff --git a/include/S88Sensors.h b/components/GPIO/include/S88Sensors.h similarity index 53% rename from include/S88Sensors.h rename to components/GPIO/include/S88Sensors.h index 7c964625..ba942db1 100644 --- a/include/S88Sensors.h +++ b/components/GPIO/include/S88Sensors.h @@ -1,7 +1,7 @@ /********************************************************************** ESP32 COMMAND STATION -COPYRIGHT (c) 2017-2019 Mike Dunston +COPYRIGHT (c) 2017-2020 Mike Dunston 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 @@ -15,12 +15,19 @@ COPYRIGHT (c) 2017-2019 Mike Dunston along with this program. If not, see http://www.gnu.org/licenses **********************************************************************/ -#ifndef _S88_SENSORS_H_ -#define _S88_SENSORS_H_ +#ifndef S88_SENSORS_H_ +#define S88_SENSORS_H_ + +#include +#include + +#include +#include +#include -#include #include "Sensors.h" -#include "DCCppProtocol.h" + +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(S88BusCommandAdapter, "S88", 0) class S88Sensor : public Sensor { public: @@ -33,71 +40,75 @@ class S88Sensor : public Sensor { void updateID(uint16_t newID) { setID(newID); } - const uint16_t getIndex() { + uint16_t getIndex() { return _index; } private: uint16_t _index; }; -class S88SensorBus { +class S88SensorBus +{ public: - S88SensorBus(const uint8_t, const uint8_t, const uint16_t); - S88SensorBus(JsonObject &); - void update(const uint8_t, const uint16_t); - void toJson(JsonObject &, bool=false); + S88SensorBus(const uint8_t, const gpio_num_t, const uint16_t); + void update(const gpio_num_t, const uint16_t); + std::string toJson(bool=false); void addSensors(int16_t); void removeSensors(int16_t); - String getStateString(); - const uint8_t getID() { + std::string getStateString(); + uint8_t getID() + { return _id; } - const uint8_t getDataPin() { + gpio_num_t getDataPin() + { return _dataPin; } - const uint16_t getSensorIDBase() { + uint16_t getSensorIDBase() + { return _sensorIDBase; } - const uint16_t getSensorCount() { + uint16_t getSensorCount() + { return _sensors.size(); } - void prepForRead() { + void prepForRead() + { _nextSensorToRead = 0; } - bool hasMore() { + bool hasMore() + { return _nextSensorToRead < _sensors.size(); } void readNext(); - void show(); + std::string get_state_for_dccpp(); private: uint8_t _id; - uint8_t _dataPin; + gpio_num_t _dataPin; uint16_t _sensorIDBase; uint8_t _nextSensorToRead; uint16_t _lastSensorID; std::vector _sensors; }; -class S88BusManager { +class S88BusManager : public Singleton, public openlcb::Polling + , private Atomic +{ public: - static void init(); - static void clear(); - static uint8_t store(); - static void s88SensorTask(void *param); - static bool createOrUpdateBus(const uint8_t, const uint8_t, const uint16_t); - static bool removeBus(const uint8_t); - static void getState(JsonArray &); + S88BusManager(openlcb::Node *node); + ~S88BusManager(); + void clear(); + uint16_t store(); + void poll_33hz(openlcb::WriteHelper *helper, Notifiable *done) override; + void poll(); + bool createOrUpdateBus(const uint8_t, const gpio_num_t, const uint16_t); + bool removeBus(const uint8_t); + std::string get_state_as_json(); + std::string get_state_for_dccpp(); private: - static TaskHandle_t _taskHandle; - static xSemaphoreHandle _s88SensorLock; -}; - -class S88BusCommandAdapter : public DCCPPProtocolCommand { -public: - void process(const std::vector); - String getID() { - return "S88"; - } + openlcb::RefreshLoop poller_; + std::vector> buses_; + os_thread_t taskHandle_; }; -#endif +#endif // S88_SENSORS_H_ \ No newline at end of file diff --git a/include/Sensors.h b/components/GPIO/include/Sensors.h similarity index 52% rename from include/Sensors.h rename to components/GPIO/include/Sensors.h index 794291c4..7d025ca2 100644 --- a/include/Sensors.h +++ b/components/GPIO/include/Sensors.h @@ -1,7 +1,7 @@ /********************************************************************** ESP32 COMMAND STATION -COPYRIGHT (c) 2017-2019 Mike Dunston +COPYRIGHT (c) 2017-2020 Mike Dunston 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 @@ -15,77 +15,71 @@ COPYRIGHT (c) 2017-2019 Mike Dunston along with this program. If not, see http://www.gnu.org/licenses **********************************************************************/ -#pragma once +#ifndef SENSORS_H_ +#define SENSORS_H_ -#include -#include "DCCppProtocol.h" -#include "WiFiInterface.h" +#include +#include -const int8_t NON_STORED_SENSOR_PIN=-1; +DECLARE_DCC_PROTOCOL_COMMAND_CLASS(SensorCommandAdapter, "S", 0) -class Sensor { +static constexpr gpio_num_t NON_STORED_SENSOR_PIN = (gpio_num_t)-1; + +class Sensor +{ public: - Sensor(uint16_t, int8_t, bool=false, bool=true); - Sensor(JsonObject &); + Sensor(uint16_t, gpio_num_t, bool=false, bool=true, bool=false); + Sensor(std::string &); virtual ~Sensor() {} - void update(uint8_t, bool=false); - virtual void toJson(JsonObject &, bool=false); - const uint16_t getID() { + void update(gpio_num_t, bool=false); + virtual std::string toJson(bool=false); + uint16_t getID() + { return _sensorID; } - const int8_t getPin() { + gpio_num_t getPin() + { return _pin; } - const bool isPullUp() { + bool isPullUp() + { return _pullUp; } - const bool isActive() { + bool isActive() + { return _lastState; } virtual void check(); - void show(); + virtual std::string get_state_for_dccpp(); protected: - void set(bool state) { - if(_lastState != state) { - _lastState = state; - LOG(INFO, "Sensor: %d :: %s", _sensorID, _lastState ? "ACTIVE" : "INACTIVE"); - if(state) { - wifiInterface.print(F(""), _sensorID); - } else { - wifiInterface.print(F(""), _sensorID); - } - } - } - void setID(uint16_t id) { + virtual std::string set(bool); + void setID(uint16_t id) + { _sensorID = id; } private: uint16_t _sensorID; - int8_t _pin; + gpio_num_t _pin; bool _pullUp; bool _lastState; }; -class SensorManager { +class SensorManager +{ public: static void init(); static void clear(); static uint16_t store(); static void sensorTask(void *param); - static void getState(JsonArray &); + static std::string getStateAsJson(); static Sensor *getSensor(uint16_t); - static bool createOrUpdate(const uint16_t, const uint8_t, const bool); + static bool createOrUpdate(const uint16_t, const gpio_num_t, const bool); static bool remove(const uint16_t); - static uint8_t getSensorPin(const uint16_t); + static gpio_num_t getSensorPin(const uint16_t); + static std::string get_state_for_dccpp(); private: static TaskHandle_t _taskHandle; - static xSemaphoreHandle _lock; + static OSMutex _lock; }; -class SensorCommandAdapter : public DCCPPProtocolCommand { -public: - void process(const std::vector); - String getID() { - return "S"; - } -}; +#endif // SENSORS_H_ \ No newline at end of file diff --git a/components/HC12/CMakeLists.txt b/components/HC12/CMakeLists.txt new file mode 100644 index 00000000..74c951b2 --- /dev/null +++ b/components/HC12/CMakeLists.txt @@ -0,0 +1,15 @@ +set(COMPONENT_SRCS + "HC12Radio.cpp" +) + +set(COMPONENT_ADD_INCLUDEDIRS "include" ) + +set(COMPONENT_REQUIRES + "OpenMRNLite" + "driver" + "DCCppProtocol" +) + +register_component() + +set_source_files_properties(HC12Radio.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) \ No newline at end of file diff --git a/components/HC12/HC12Radio.cpp b/components/HC12/HC12Radio.cpp new file mode 100644 index 00000000..9503ffba --- /dev/null +++ b/components/HC12/HC12Radio.cpp @@ -0,0 +1,122 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2018-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "HC12Radio.h" + +#if defined(CONFIG_HC12) + +/// Utility macro for StateFlow early abort +#define LOG_ESP_ERROR_AND_EXIT_FLOW(name, text, cmd) \ +{ \ + esp_err_t res = cmd; \ + if (res != ESP_OK) \ + { \ + LOG_ERROR("[%s] %s: %s" \ + , name, text, esp_err_to_name(res)); \ + return exit(); \ + } \ +} + +/// Utility macro to initialize a UART as part of a StateFlow +#define CONFIGURE_UART(name, uart, speed, rx, tx, rx_buf, tx_buf) \ +{ \ + LOG(INFO \ + , "[%s] Initializing UART(%d) at %u baud on RX %d, TX %d" \ + , name, uart, speed, rx, tx); \ + uart_config_t uart_cfg = \ + { \ + .baud_rate = speed, \ + .data_bits = UART_DATA_8_BITS, \ + .parity = UART_PARITY_DISABLE, \ + .stop_bits = UART_STOP_BITS_1, \ + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, \ + .rx_flow_ctrl_thresh = 0, \ + .use_ref_tick = false \ + }; \ + LOG_ESP_ERROR_AND_EXIT_FLOW(name, "uart_param_config", \ + uart_param_config(uart, &uart_cfg)) \ + LOG_ESP_ERROR_AND_EXIT_FLOW(name, "uart_set_pin", \ + uart_set_pin(uart, tx, rx \ + , UART_PIN_NO_CHANGE \ + , UART_PIN_NO_CHANGE)) \ + LOG_ESP_ERROR_AND_EXIT_FLOW(name, "uart_driver_install", \ + uart_driver_install(uart, rx_buf, tx_buf \ + , 0, NULL, 0)) \ +} + +namespace esp32cs +{ + +HC12Radio::HC12Radio(Service *service, uart_port_t port, gpio_num_t rx + , gpio_num_t tx) + : StateFlowBase(service), uart_(port), rx_(rx), tx_(tx) +{ + start_flow(STATE(initialize)); +} + +StateFlowBase::Action HC12Radio::initialize() +{ + CONFIGURE_UART("hc12", uart_, CONFIG_HC12_BAUD_RATE, rx_, tx_ + , CONFIG_HC12_BUFFER_SIZE, CONFIG_HC12_BUFFER_SIZE) + + uartFd_ = open(StringPrintf("/dev/uart/%d", uart_).c_str() + , O_RDWR | O_NONBLOCK); + if (uartFd_ >= 0) + { + LOG(INFO, "[HC12] Initialized"); + return call_immediately(STATE(wait_for_data)); + } + + // ignore error code here as we are shutting down the interface + uart_driver_delete(uart_); + uartFd_ = -1; + + LOG_ERROR("[HC12] Initialization failure, unable to open UART device: %s" + , strerror(errno)); + return exit(); +} + +StateFlowBase::Action HC12Radio::data_received() +{ + if (helper_.hasError_) + { + LOG_ERROR("[HC12] uart read failed, giving up!"); + return exit(); + } + else if (helper_.remaining_ == RX_BUF_SIZE) + { + return yield_and_call(STATE(wait_for_data)); + } + + tx_buffer_ = std::move(feed(rx_buffer_, RX_BUF_SIZE - helper_.remaining_)); + if (tx_buffer_.length() > 0) + { + return write_repeated(&helper_, uartFd_, tx_buffer_.c_str() + , tx_buffer_.length(), STATE(wait_for_data)); + } + return call_immediately(STATE(wait_for_data)); +} + +StateFlowBase::Action HC12Radio::wait_for_data() +{ + return read_nonblocking(&helper_, uartFd_, rx_buffer_, RX_BUF_SIZE + , STATE(data_received)); +} + +} // namespace esp32cs + +#endif // CONFIG_HC12 \ No newline at end of file diff --git a/components/HC12/Kconfig.projbuild b/components/HC12/Kconfig.projbuild new file mode 100644 index 00000000..84fc4fa0 --- /dev/null +++ b/components/HC12/Kconfig.projbuild @@ -0,0 +1,43 @@ +config HC12 + bool "Enable HC12 Radio interface" + default n + help + The HC12 is a radio receiver that was previously used by some + throttles to wirelessly send packet data to the ESP32 Command + Station. + +menu "HC12 Radio" + depends on HC12 + + config HC12_RX_PIN + int "RX pin" + range 0 39 + + config HC12_TX_PIN + int "TX pin" + range 0 32 + + choice HC12_UART + bool "UART" + default HC12_UART_UART1 + config HC12_UART_UART1 + bool "UART1" + depends on !OPS_RAILCOM_UART1 + config HC12_UART_UART2 + bool "UART2" + depends on !OPS_RAILCOM_UART2 + endchoice + + config HC12_UART + int + default 1 if HC12_UART_UART1 + default 2 if HC12_UART_UART2 + + config HC12_BAUD_RATE + int "BAUD rate for HC12 Radio" + default 19200 + + config HC12_BUFFER_SIZE + int + default 256 +endmenu \ No newline at end of file diff --git a/include/WiFiInterface.h b/components/HC12/include/HC12Radio.h similarity index 51% rename from include/WiFiInterface.h rename to components/HC12/include/HC12Radio.h index 281bd586..db8b7d05 100644 --- a/include/WiFiInterface.h +++ b/components/HC12/include/HC12Radio.h @@ -1,7 +1,7 @@ /********************************************************************** ESP32 COMMAND STATION -COPYRIGHT (c) 2017-2019 Mike Dunston +COPYRIGHT (c) 2018-2020 Mike Dunston 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 @@ -15,18 +15,38 @@ COPYRIGHT (c) 2017-2019 Mike Dunston along with this program. If not, see http://www.gnu.org/licenses **********************************************************************/ -#pragma once +#ifndef HC12_RADIO_H_ +#define HC12_RADIO_H_ -#include +#include +#include +#include +#include +#include -class WiFiInterface { +namespace esp32cs +{ + +class HC12Radio : public StateFlowBase + , private DCCPPProtocolConsumer +{ public: - WiFiInterface(); - void begin(); - void showConfiguration(); - void showInitInfo(); - void send(const String &); - void print(const __FlashStringHelper *fmt, ...); + HC12Radio(Service *, uart_port_t, gpio_num_t, gpio_num_t); +private: + static constexpr uint8_t RX_BUF_SIZE = 64; + StateFlowSelectHelper helper_{this}; + uint8_t rx_buffer_[RX_BUF_SIZE]; + string tx_buffer_; + int uartFd_; + uart_port_t uart_; + gpio_num_t rx_; + gpio_num_t tx_; + + STATE_FLOW_STATE(initialize); + STATE_FLOW_STATE(data_received); + STATE_FLOW_STATE(wait_for_data); }; -extern WiFiInterface wifiInterface; +} // namespace esp32cs + +#endif // HC12_RADIO_H_ \ No newline at end of file diff --git a/components/JmriInterface/CMakeLists.txt b/components/JmriInterface/CMakeLists.txt new file mode 100644 index 00000000..a7968a2e --- /dev/null +++ b/components/JmriInterface/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register( + SRCS JmriInterface.cpp + INCLUDE_DIRS include + PRIV_REQUIRES OpenMRNLite Esp32HttpServer DCCppProtocol +) + +set_source_files_properties(JmriInterface.cpp PROPERTIES COMPILE_FLAGS "-Wno-implicit-fallthrough -Wno-ignored-qualifiers") \ No newline at end of file diff --git a/components/JmriInterface/JmriClientFlow.h b/components/JmriInterface/JmriClientFlow.h new file mode 100644 index 00000000..108bd312 --- /dev/null +++ b/components/JmriInterface/JmriClientFlow.h @@ -0,0 +1,102 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2017-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#ifndef JMRI_CLIENT_FLOW_H_ +#define JMRI_CLIENT_FLOW_H_ + +#include +#include + +class JmriClientFlow : private StateFlowBase, public DCCPPProtocolConsumer +{ +public: + JmriClientFlow(int fd, uint32_t remote_ip, Service *service) + : StateFlowBase(service), DCCPPProtocolConsumer(), fd_(fd) + , remoteIP_(remote_ip) + { + LOG(INFO, "[JMRI %s] Connected", name().c_str()); + bzero(buf_, BUFFER_SIZE); + + struct timeval tm; + tm.tv_sec = 0; + tm.tv_usec = MSEC_TO_USEC(10); + ERRNOCHECK("setsockopt_timeout", + setsockopt(fd_, SOL_SOCKET, SO_RCVTIMEO, &tm, sizeof(tm))); + + start_flow(STATE(read_data)); + } + + virtual ~JmriClientFlow() + { + LOG(INFO, "[JMRI %s] Disconnected", name().c_str()); + ::close(fd_); + } +private: + static const size_t BUFFER_SIZE = 128; + int fd_; + uint32_t remoteIP_; + uint8_t buf_[BUFFER_SIZE]; + size_t buf_used_{0}; + string res_; + StateFlowTimedSelectHelper helper_{this}; + + Action read_data() + { + // clear the buffer of data we have sent back + res_.clear(); + + return read_nonblocking(&helper_, fd_, buf_, BUFFER_SIZE + , STATE(process_data)); + } + + Action process_data() + { + if (helper_.hasError_) + { + return delete_this(); + } + else if (helper_.remaining_ == BUFFER_SIZE) + { + return yield_and_call(STATE(read_data)); + } + else + { + buf_used_ = BUFFER_SIZE - helper_.remaining_; + LOG(VERBOSE, "[JMRI %s] received %zu bytes", name().c_str(), buf_used_); + } + res_.append(feed(buf_, buf_used_)); + buf_used_ = 0; + return yield_and_call(STATE(send_data)); + } + + Action send_data() + { + if(res_.empty()) + { + return yield_and_call(STATE(read_data)); + } + return write_repeated(&helper_, fd_, res_.data(), res_.length() + , STATE(read_data)); + } + + string name() + { + return StringPrintf("%s/%d", ipv4_to_string(remoteIP_).c_str(), fd_); + } +}; + +#endif // JMRI_CLIENT_FLOW_H_ \ No newline at end of file diff --git a/components/JmriInterface/JmriInterface.cpp b/components/JmriInterface/JmriInterface.cpp new file mode 100644 index 00000000..7d9a90a6 --- /dev/null +++ b/components/JmriInterface/JmriInterface.cpp @@ -0,0 +1,65 @@ +/********************************************************************** +ESP32 COMMAND STATION + +COPYRIGHT (c) 2017-2020 Mike Dunston + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses +**********************************************************************/ + +#include "sdkconfig.h" + +#include +#include +#include +#include +#include "JmriClientFlow.h" + +std::unique_ptr listener; + +void init_jmri_interface() +{ + Singleton::instance()->add_event_callback( + [](system_event_t *event) + { + if(event->event_id == SYSTEM_EVENT_STA_GOT_IP || + event->event_id == SYSTEM_EVENT_AP_START) + { + if (!listener) + { + LOG(INFO, "[JMRI] Starting JMRI listener"); + listener.reset( + new SocketListener(CONFIG_JMRI_LISTENER_PORT, + [](int fd) + { + sockaddr_in source; + socklen_t source_len = sizeof(sockaddr_in); + bzero(&source, sizeof(sockaddr_in)); + getpeername(fd, (sockaddr *)&source, &source_len); + // Create new JMRI client and attach it to the Httpd + // instance rather than the default executor. + new JmriClientFlow(fd, ntohl(source.sin_addr.s_addr) + , Singleton::instance()); + }, "jmri")); + Singleton::instance()->mdns_publish( + CONFIG_JMRI_MDNS_SERVICE_NAME, CONFIG_JMRI_LISTENER_PORT); + } + } + else if (event->event_id == SYSTEM_EVENT_STA_LOST_IP || + event->event_id == SYSTEM_EVENT_AP_STOP) + { + LOG(INFO, "[WiFi] Shutting down JMRI listener"); + listener.reset(nullptr); + Singleton::instance()->mdns_unpublish( + CONFIG_JMRI_MDNS_SERVICE_NAME); + } + }); +} \ No newline at end of file diff --git a/components/JmriInterface/Kconfig.projbuild b/components/JmriInterface/Kconfig.projbuild new file mode 100644 index 00000000..59cf3df8 --- /dev/null +++ b/components/JmriInterface/Kconfig.projbuild @@ -0,0 +1,15 @@ +config JMRI + bool "Enable Legacy JMRI DCC++ Interface" + default y + +menu "Legacy JMRI DCC++ Interface" + depends on JMRI + + config JMRI_LISTENER_PORT + int "JMRI Listener port" + default 2560 + + config JMRI_MDNS_SERVICE_NAME + string "mDNS service name" + default "_esp32cs._tcp" +endmenu \ No newline at end of file diff --git a/include/HC12Interface.h b/components/JmriInterface/include/JmriInterface.h similarity index 80% rename from include/HC12Interface.h rename to components/JmriInterface/include/JmriInterface.h index 78117a11..69677605 100644 --- a/include/HC12Interface.h +++ b/components/JmriInterface/include/JmriInterface.h @@ -1,7 +1,7 @@ /********************************************************************** ESP32 COMMAND STATION -COPYRIGHT (c) 2018-2019 Mike Dunston +COPYRIGHT (c) 2017-2020 Mike Dunston 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 @@ -15,13 +15,4 @@ COPYRIGHT (c) 2018-2019 Mike Dunston along with this program. If not, see http://www.gnu.org/licenses **********************************************************************/ -#pragma once - -#include - -class HC12Interface { -public: - static void init(); - static void update(); - static void send(const String &buf); -}; +void init_jmri_interface(); \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/AllTrainNodes.cpp b/components/LCCTrainSearchProtocol/AllTrainNodes.cpp new file mode 100644 index 00000000..fd815b83 --- /dev/null +++ b/components/LCCTrainSearchProtocol/AllTrainNodes.cpp @@ -0,0 +1,711 @@ +/** \copyright + * Copyright (c) 2014, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file AllTrainNodes.hxx + * + * A class that instantiates every train node from the TrainDb. + * + * @author Balazs Racz + * @date 20 May 2014 + */ + +#include "AllTrainNodes.hxx" + +#include "FdiXmlGenerator.hxx" +#include "FindProtocolServer.hxx" +#include "TrainDb.hxx" +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace commandstation +{ + +using openlcb::Defs; +using openlcb::TractionDefs; + +struct AllTrainNodes::Impl +{ + public: + ~Impl() + { + delete eventHandler_; + delete node_; + delete train_; + } + int id; + openlcb::SimpleEventHandler* eventHandler_{nullptr}; + openlcb::Node* node_{nullptr}; + openlcb::TrainImpl* train_{nullptr}; +}; + +void AllTrainNodes::remove_train_impl(int address) +{ + OSMutexLock l(&trainsLock_); + auto it = std::find_if(trains_.begin(), trains_.end(), [address](Impl *impl) + { + return impl->train_->legacy_address() == address; + }); + if (it != trains_.end()) + { + Impl *impl = (*it); + impl->node_->iface()->delete_local_node(impl->node_); + delete impl; + trains_.erase(it); + } +} + +openlcb::TrainImpl* AllTrainNodes::get_train_impl(openlcb::NodeID id, bool allocate) +{ + auto it = find_node(id, allocate); + if (it) + { + return it->train_; + } + return nullptr; +} + +openlcb::TrainImpl* AllTrainNodes::get_train_impl(DccMode drive_type, int address) +{ + { + OSMutexLock l(&trainsLock_); + auto it = std::find_if(trains_.begin(), trains_.end(), [address](Impl *impl) + { + return impl->train_->legacy_address() == address; + }); + if (it != trains_.end()) + { + return (*it)->train_; + } + } + return find_node(allocate_node(drive_type, address))->train_; +} + +AllTrainNodes::Impl* AllTrainNodes::find_node(openlcb::Node* node) +{ + { + OSMutexLock l(&trainsLock_); + auto it = std::find_if(trains_.begin(), trains_.end(), + [node](Impl *impl) + { + return impl->node_ == node; + }); + if (it != trains_.end()) + { + return *it; + } + } + // no active train was found with the provided node reference, try to find + // one via the node-id instead. + if (node != nullptr && node->node_id()) + { + return find_node(node->node_id()); + } + return nullptr; +} + +AllTrainNodes::Impl* AllTrainNodes::find_node(openlcb::NodeID node_id, bool allocate) +{ + { + OSMutexLock l(&trainsLock_); + auto it = std::find_if(trains_.begin(), trains_.end(), [node_id](Impl *impl) + { + return impl->node_->node_id() == node_id; + }); + if (it != trains_.end()) + { + return *it; + } + } + if (!allocate) + { + return nullptr; + } + + // no active train was found having the provided node id, search for a train + // in the db that should have the provided node id and return it instead if + // found. + dcc::TrainAddressType type; + uint32_t addr = 0; + if (TractionDefs::legacy_address_from_train_node_id(node_id, &type, &addr)) + { + LOG(CONFIG_LCC_TSP_LOG_LEVEL, "[TrainSearch] Checking db for node id: %s" + , uint64_to_string_hex(node_id).c_str()); + auto train = db_->find_entry(node_id, addr); + if (train != nullptr) + { + LOG(CONFIG_LCC_TSP_LOG_LEVEL, "[TrainSearch] Matched %s, creating node" + , train->identifier().c_str()); + return create_impl(train->file_offset() + , train->get_legacy_drive_mode() + , train->get_legacy_address()); + } + } + else + { + LOG(CONFIG_LCC_TSP_LOG_LEVEL + , "[TrainSearch] node id %s doesn't look like a train node, ignoring" + , uint64_to_string_hex(node_id).c_str()); + } + + // no active train or db entry was found for the node id, give up + return nullptr; +} + +/// Returns a traindb entry or nullptr if the id is too high. +std::shared_ptr AllTrainNodes::get_traindb_entry(int id) +{ + return db_->get_entry(id); +} + +/// Returns a node id or 0 if the id is not known to be a train. +openlcb::NodeID AllTrainNodes::get_train_node_id(int id, bool allocate) +{ + { + OSMutexLock l(&trainsLock_); + if (id < (int)trains_.size() && trains_[id]->node_) + { + return trains_[id]->node_->node_id(); + } + } + if (!allocate) + { + return 0; + } + + LOG(CONFIG_LCC_TSP_LOG_LEVEL + , "[TrainSearch] no active train with index %d, checking db", id); + auto db_entry = db_->get_entry(id); + if (db_entry != nullptr) + { + LOG(CONFIG_LCC_TSP_LOG_LEVEL + , "[TrainSearch] found existing db entry %s, creating node %s" + , db_entry->identifier().c_str() + , uint64_to_string_hex(db_entry->get_traction_node()).c_str()); + create_impl(id, db_entry->get_legacy_drive_mode() + , db_entry->get_legacy_address()); + return db_entry->get_traction_node(); + } + + LOG(CONFIG_LCC_TSP_LOG_LEVEL + , "[TrainSearch] no train node found for index %d, giving up", id); + return 0; +} + +class AllTrainNodes::TrainSnipHandler : public openlcb::IncomingMessageStateFlow +{ + public: + TrainSnipHandler(AllTrainNodes* parent, openlcb::SimpleInfoFlow* info_flow) + : IncomingMessageStateFlow(parent->tractionService_->iface()), + parent_(parent), + responseFlow_(info_flow) + { + iface()->dispatcher()->register_handler( + this, Defs::MTI::MTI_IDENT_INFO_REQUEST, Defs::MTI::MTI_EXACT); + } + ~TrainSnipHandler() + { + iface()->dispatcher()->unregister_handler( + this, Defs::MTI::MTI_IDENT_INFO_REQUEST, Defs::MTI::MTI_EXACT); + } + + Action entry() override + { + // Let's find the train ID. + impl_ = parent_->find_node(nmsg()->dstNode); + if (!impl_) return release_and_exit(); + return allocate_and_call(responseFlow_, STATE(send_response_request)); + } + + Action send_response_request() + { + auto* b = get_allocation_result(responseFlow_); + auto entry = parent_->get_traindb_entry(impl_->id); + if (entry.get()) + { + snipName_ = entry->get_train_name(); + } + else + { + snipName_.clear(); + } + snipResponse_[6].data = snipName_.c_str(); + b->data()->reset(nmsg(), snipResponse_, Defs::MTI_IDENT_INFO_REPLY); + // We must wait for the data to be sent out because we have a static member + // that we are changing constantly. + b->set_done(n_.reset(this)); + responseFlow_->send(b); + release(); + return wait_and_call(STATE(send_done)); + } + + Action send_done() + { + return exit(); + } + + private: + AllTrainNodes* parent_; + openlcb::SimpleInfoFlow* responseFlow_; + AllTrainNodes::Impl* impl_; + BarrierNotifiable n_; + string snipName_; + static openlcb::SimpleInfoDescriptor snipResponse_[]; +}; + +openlcb::SimpleInfoDescriptor AllTrainNodes::TrainSnipHandler::snipResponse_[] = +{ + {openlcb::SimpleInfoDescriptor::LITERAL_BYTE, 4, 0, nullptr}, + {openlcb::SimpleInfoDescriptor::C_STRING, 0, 0, + openlcb::SNIP_STATIC_DATA.manufacturer_name}, + {openlcb::SimpleInfoDescriptor::C_STRING, 41, 0, "Virtual train node"}, + {openlcb::SimpleInfoDescriptor::C_STRING, 0, 0, "n/a"}, + {openlcb::SimpleInfoDescriptor::C_STRING, 0, 0, + openlcb::SNIP_STATIC_DATA.software_version}, + {openlcb::SimpleInfoDescriptor::LITERAL_BYTE, 2, 0, nullptr}, + {openlcb::SimpleInfoDescriptor::C_STRING, 63, 1, nullptr}, + {openlcb::SimpleInfoDescriptor::C_STRING, 0, 0, "n/a"}, + {openlcb::SimpleInfoDescriptor::END_OF_DATA, 0, 0, 0} +}; + +class AllTrainNodes::TrainPipHandler : public openlcb::IncomingMessageStateFlow +{ + public: + TrainPipHandler(AllTrainNodes* parent) + : IncomingMessageStateFlow(parent->tractionService_->iface()), + parent_(parent) + { + iface()->dispatcher()->register_handler( + this, Defs::MTI::MTI_PROTOCOL_SUPPORT_INQUIRY, Defs::MTI::MTI_EXACT); + } + + ~TrainPipHandler() + { + iface()->dispatcher()->unregister_handler( + this, Defs::MTI::MTI_PROTOCOL_SUPPORT_INQUIRY, Defs::MTI::MTI_EXACT); + } + + private: + Action entry() override { + if (parent_->find_node(nmsg()->dstNode) == nullptr) { + return release_and_exit(); + } + + return allocate_and_call(iface()->addressed_message_write_flow(), + STATE(fill_response_buffer)); + } + + Action fill_response_buffer() { + // Grabs our allocated buffer. + auto* b = get_allocation_result(iface()->addressed_message_write_flow()); + auto reply = pipReply_; + // Fills in response. We use node_id_to_buffer because that converts a + // 48-bit value to a big-endian byte string. + b->data()->reset(Defs::MTI_PROTOCOL_SUPPORT_REPLY, + nmsg()->dstNode->node_id(), nmsg()->src, + openlcb::node_id_to_buffer(reply)); + + // Passes the response to the addressed message write flow. + iface()->addressed_message_write_flow()->send(b); + + return release_and_exit(); + } + + AllTrainNodes* parent_; + static constexpr uint64_t pipReply_ = + Defs::SIMPLE_PROTOCOL_SUBSET | Defs::DATAGRAM | + Defs::MEMORY_CONFIGURATION | Defs::EVENT_EXCHANGE | + Defs::SIMPLE_NODE_INFORMATION | Defs::TRACTION_CONTROL | + Defs::TRACTION_FDI | Defs::CDI; +}; + +class AllTrainNodes::TrainFDISpace : public openlcb::MemorySpace +{ + public: + TrainFDISpace(AllTrainNodes* parent) : parent_(parent) {} + + bool set_node(openlcb::Node* node) override + { + if (impl_ && impl_->node_ == node) + { + // same node. + return true; + } + impl_ = parent_->find_node(node); + if (impl_ != nullptr) + { + reset_file(); + return true; + } + return false; + } + + address_t max_address() override + { + // We don't really know how long this space is; 16 MB is an upper bound. + return 16 << 20; + } + + size_t read(address_t source, uint8_t* dst, size_t len, errorcode_t* error, + Notifiable* again) override + { + if (source <= gen_.file_offset()) + { + reset_file(); + } + ssize_t result = gen_.read(source, dst, len); + if (result < 0) + { + LOG_ERROR("[TrainFDI] Read failure: %u, %zu: %zu (%s)", source, len + , result, strerror(errno)); + *error = Defs::ERROR_PERMANENT; + return 0; + } + if (result == 0) + { + LOG(VERBOSE, "[TrainFDI] Out-of-bounds read: %u, %zu", source, len); + *error = openlcb::MemoryConfigDefs::ERROR_OUT_OF_BOUNDS; + } + else + { + *error = 0; + } + return result; + } + + private: + void reset_file() + { + auto e = parent_->get_traindb_entry(impl_->id); + e->start_read_functions(); + gen_.reset(std::move(e)); + } + + FdiXmlGenerator gen_; + AllTrainNodes* parent_; + // Train object structure. + Impl* impl_{nullptr}; +}; + +class AllTrainNodes::TrainConfigSpace : public openlcb::FileMemorySpace +{ + public: + TrainConfigSpace(int fd, AllTrainNodes* parent, size_t file_end) + : FileMemorySpace(fd, file_end), parent_(parent) {} + + bool set_node(openlcb::Node* node) override + { + if (impl_ && impl_->node_ == node) + { + // same node. + return true; + } + impl_ = parent_->find_node(node); + if (impl_ == nullptr) + { + return false; + } + auto entry = parent_->db_->get_entry(impl_->id); + if (!entry) + { + return false; + } + int offset = entry->file_offset(); + if (offset < 0) + { + return false; + } + offset_ = offset; + return true; + } + + size_t read(address_t source, uint8_t* dst, size_t len, errorcode_t* error, + Notifiable* again) override + { + return FileMemorySpace::read(source + offset_, dst, len, error, again); + } + + size_t write(address_t destination, const uint8_t* data, size_t len, + errorcode_t* error, Notifiable* again) override + { + return FileMemorySpace::write(destination + offset_, data, len, error, + again); + } + + private: + unsigned offset_; + AllTrainNodes* parent_; + // Train object structure. + Impl* impl_{nullptr}; +}; + +extern const char TRAINCDI_DATA[]; +extern const size_t TRAINCDI_SIZE; +extern const char TRAINTMPCDI_DATA[]; +extern const size_t TRAINTMPCDI_SIZE; + +class AllTrainNodes::TrainCDISpace : public openlcb::MemorySpace +{ + public: + TrainCDISpace(AllTrainNodes* parent) : parent_(parent) {} + + bool set_node(openlcb::Node* node) override + { + if (impl_ && impl_->node_ == node) + { + // same node. + return true; + } + impl_ = parent_->find_node(node); + if (impl_ == nullptr) + { + return false; + } + auto entry = parent_->db_->get_entry(impl_->id); + if (!entry) + { + return false; + } + int offset = entry->file_offset(); + if (offset < 0) + { + proxySpace_ = parent_->ro_tmp_train_cdi_; + } + else + { + proxySpace_ = parent_->ro_train_cdi_; + } + return true; + } + + address_t max_address() override + { + return proxySpace_->max_address(); + } + + size_t read(address_t source, uint8_t* dst, size_t len, errorcode_t* error, + Notifiable* again) override + { + return proxySpace_->read(source, dst, len, error, again); + } + + AllTrainNodes* parent_; + // Train object structure. + Impl* impl_{nullptr}; + openlcb::MemorySpace* proxySpace_; +}; + +class AllTrainNodes::TrainIdentifyHandler : + public openlcb::IncomingMessageStateFlow +{ +public: + TrainIdentifyHandler(AllTrainNodes *parent) + : IncomingMessageStateFlow(parent->tractionService_->iface()) + , parent_(parent) + { + iface()->dispatcher()->register_handler( + this, Defs::MTI::MTI_VERIFY_NODE_ID_GLOBAL, Defs::MTI::MTI_EXACT); + } + + ~TrainIdentifyHandler() + { + iface()->dispatcher()->unregister_handler( + this, Defs::MTI_VERIFY_NODE_ID_GLOBAL, Defs::MTI::MTI_EXACT); + } + + /// Handler callback for incoming messages. + IncomingMessageStateFlow::Action entry() override + { + openlcb::GenMessage *m = message()->data(); + if (!m->payload.empty() && m->payload.size() == 6) + { + target_ = openlcb::buffer_to_node_id(m->payload); + LOG(CONFIG_LCC_TSP_LOG_LEVEL + , "[TrainIdent] received global identify for node %s" + , uint64_to_string_hex(target_).c_str()); + openlcb::NodeID masked = target_ & TractionDefs::NODE_ID_MASK; + if ((masked == TractionDefs::NODE_ID_DCC || + masked == TractionDefs::NODE_ID_MARKLIN_MOTOROLA || + masked == 0x050100000000ULL) && // TODO: move this constant into TractionDefs + parent_->find_node(target_) != nullptr) + { + LOG(CONFIG_LCC_TSP_LOG_LEVEL + , "[TrainIdent] matched a known train db entry"); + release(); + return allocate_and_call(iface()->global_message_write_flow(), + STATE(send_train_ident)); + } + } + return release_and_exit(); + } +private: + AllTrainNodes *parent_; + openlcb::NodeID target_; + + IncomingMessageStateFlow::Action send_train_ident() + { + auto *b = + get_allocation_result(iface()->global_message_write_flow()); + openlcb::GenMessage *m = b->data(); + m->reset(Defs::MTI_VERIFIED_NODE_ID_NUMBER, target_ + , openlcb::node_id_to_buffer(target_)); + iface()->global_message_write_flow()->send(b); + return exit(); + } +}; + +AllTrainNodes::AllTrainNodes(TrainDb* db, + openlcb::TrainService* traction_service, + openlcb::SimpleInfoFlow* info_flow, + openlcb::MemoryConfigHandler* memory_config, + openlcb::MemorySpace* ro_train_cdi, + openlcb::MemorySpace* ro_tmp_train_cdi) + : db_(db), + tractionService_(traction_service), + memoryConfigService_(memory_config), + ro_train_cdi_(ro_train_cdi), + ro_tmp_train_cdi_(ro_tmp_train_cdi), + snipHandler_(new TrainSnipHandler(this, info_flow)), + pipHandler_(new TrainPipHandler(this)) +{ + HASSERT(ro_train_cdi_->read_only()); + HASSERT(ro_tmp_train_cdi_->read_only()); + + fdiSpace_.reset(new TrainFDISpace(this)); + memoryConfigService_->registry()->insert( + nullptr, openlcb::MemoryConfigDefs::SPACE_FDI, fdiSpace_.get()); + cdiSpace_.reset(new TrainCDISpace(this)); + memoryConfigService_->registry()->insert( + nullptr, openlcb::MemoryConfigDefs::SPACE_CDI, cdiSpace_.get()); + findProtocolServer_.reset(new FindProtocolServer(this)); + trainIdentHandler_.reset(new TrainIdentifyHandler(this)); +} + +AllTrainNodes::Impl* AllTrainNodes::create_impl(int train_id, DccMode mode, + int address) +{ + Impl* impl = new Impl; + impl->id = train_id; + switch (mode) { + case MARKLIN_OLD: { + LOG(CONFIG_LCC_TSP_LOG_LEVEL, "New Marklin (old) train %d", address); + impl->train_ = new dcc::MMOldTrain(dcc::MMAddress(address)); + break; + } + case MARKLIN_DEFAULT: + case MARKLIN_NEW: + /// @todo (balazs.racz) implement marklin twoaddr train drive mode. + case MARKLIN_TWOADDR: { + LOG(CONFIG_LCC_TSP_LOG_LEVEL, "New Marklin (new) train %d", address); + impl->train_ = new dcc::MMNewTrain(dcc::MMAddress(address)); + break; + } + /// @todo (balazs.racz) implement dcc 14 train drive mode. + case DCC_14: + case DCC_14_LONG_ADDRESS: + case DCC_28: + case DCC_28_LONG_ADDRESS: { + LOG(CONFIG_LCC_TSP_LOG_LEVEL, "New DCC-14/28 train %d", address); + if ((mode & DCC_LONG_ADDRESS) || address >= 128) { + impl->train_ = new dcc::Dcc28Train(dcc::DccLongAddress(address)); + } else { + impl->train_ = new dcc::Dcc28Train(dcc::DccShortAddress(address)); + } + break; + } + case DCC_128: + case DCC_128_LONG_ADDRESS: { + LOG(CONFIG_LCC_TSP_LOG_LEVEL, "New DCC-128 train %d", address); + if ((mode & DCC_LONG_ADDRESS) || address >= 128) { + impl->train_ = new dcc::Dcc128Train(dcc::DccLongAddress(address)); + } else { + impl->train_ = new dcc::Dcc128Train(dcc::DccShortAddress(address)); + } + break; + } + default: + impl->train_ = nullptr; + LOG_ERROR("Unhandled train drive mode."); + } + if (impl->train_) { + { + OSMutexLock l(&trainsLock_); + trains_.push_back(impl); + } + impl->node_ = + new openlcb::TrainNodeForProxy(tractionService_, impl->train_); + impl->eventHandler_ = + new openlcb::FixedEventProducer( + impl->node_); + return impl; + } else { + delete impl; + return nullptr; + } +} + +size_t AllTrainNodes::size() +{ + return std::max(trains_.size(), db_->size()); +} + +bool AllTrainNodes::is_valid_train_node(openlcb::Node *node) +{ + return find_node(node) != nullptr; +} + +bool AllTrainNodes::is_valid_train_node(openlcb::NodeID node_id, bool allocate) +{ + return find_node(node_id, allocate) != nullptr; +} + +openlcb::NodeID AllTrainNodes::allocate_node(DccMode drive_type, int address) +{ + Impl* impl = create_impl(-1, drive_type, address); + if (!impl) return 0; // failed. + impl->id = db_->add_dynamic_entry(address, drive_type); + return impl->node_->node_id(); +} + +AllTrainNodes::~AllTrainNodes() +{ + OSMutexLock l(&trainsLock_); + for (auto* t : trains_) { + delete t; + } + memoryConfigService_->registry()->erase( + nullptr, openlcb::MemoryConfigDefs::SPACE_FDI, fdiSpace_.get()); + memoryConfigService_->registry()->erase( + nullptr, openlcb::MemoryConfigDefs::SPACE_CDI, cdiSpace_.get()); +} + +} // namespace commandstation \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/CMakeLists.txt b/components/LCCTrainSearchProtocol/CMakeLists.txt new file mode 100644 index 00000000..d353993a --- /dev/null +++ b/components/LCCTrainSearchProtocol/CMakeLists.txt @@ -0,0 +1,20 @@ +set(COMPONENT_SRCS + "AllTrainNodes.cpp" + "FdiXmlGenerator.cpp" + "FindProtocolDefs.cpp" + "XmlGenerator.cpp" +) + +set(COMPONENT_ADD_INCLUDEDIRS + "include" +) + +set(COMPONENT_REQUIRES + "OpenMRNLite" +) + +register_component() + +set_source_files_properties(AllTrainNodes.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(FindProtocolDefs.cpp PROPERTIES COMPILE_FLAGS "-Wno-type-limits -Wno-ignored-qualifiers") +set_source_files_properties(FdiXmlGenerator.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) diff --git a/components/LCCTrainSearchProtocol/FdiXmlGenerator.cpp b/components/LCCTrainSearchProtocol/FdiXmlGenerator.cpp new file mode 100644 index 00000000..93addaa2 --- /dev/null +++ b/components/LCCTrainSearchProtocol/FdiXmlGenerator.cpp @@ -0,0 +1,160 @@ +/** \copyright + * Copyright (c) 2016, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file FdiXmlGenerator.cxx + * + * Train FDI generator. + * + * @author Balazs Racz + * @date 16 Jan 2016 + */ + +#include "FdiXmlGenerator.hxx" +#include "TrainDb.hxx" + +namespace commandstation { + +static const char kFdiXmlHead[] = R"( + + + +)"; +static const char kFdiXmlTail[] = ""; + +static const char kFdiXmlBinaryFunction[] = + R"( +)"; + +static const char kFdiXmlMomentaryFunction[] = + R"( +)"; + +struct FunctionLabel { + uint8_t fn; + const char* label; +}; + +static const FunctionLabel labels[] = { // + {LIGHT, "Light"}, + {BEAMER, "Beamer"}, + {BELL, "Bell"}, + {HORN, "Horn"}, + {SHUNT, "Shunt"}, + {PANTO, "Pantgr"}, + {SMOKE, "Smoke"}, + {ABV, "Mom off"}, + {WHISTLE, "Whistle"}, + {SOUND, "Sound"}, + // FNT11 is skipped, rendered as "F" + {SPEECH, "Announce"}, + {ENGINE, "Engine"}, + {LIGHT1, "Light1"}, + {LIGHT2, "Light2"}, + {TELEX, "Coupler"}, + {0, nullptr}}; + +const char* label_for_function(uint8_t type) { + const FunctionLabel* r = labels; + while (r->fn) { + if ((r->fn & ~MOMENTARY) == (type & ~MOMENTARY)) return r->label; + ++r; + } + return nullptr; +} + +void FdiXmlGenerator::reset(std::shared_ptr lok) { + state_ = STATE_START; + entry_ = lok; + internal_reset(); +} + +void FdiXmlGenerator::generate_more() { + while (true) { + switch (state_) { + case STATE_XMLHEAD: { + add_to_output(from_const_string(kFdiXmlHead)); + nextFunction_ = 0; + state_ = STATE_START_FN; + return; + } + case STATE_START_FN: { + while ( + nextFunction_ <= entry_->get_max_fn() && + (entry_->get_function_label(nextFunction_) == FN_NONEXISTANT || + entry_->get_function_label(nextFunction_) == FN_UNINITIALIZED)) { + ++nextFunction_; + } + if (nextFunction_ > entry_->get_max_fn()) { + state_ = STATE_NO_MORE_FN; + continue; + } + if (entry_->get_function_label(nextFunction_) & 0x80) { + add_to_output(from_const_string(kFdiXmlMomentaryFunction)); + } else { + add_to_output(from_const_string(kFdiXmlBinaryFunction)); + } + state_ = STATE_FN_NAME; + return; + } + case STATE_FN_NAME: { + add_to_output(from_const_string("")); + const char* label = + label_for_function(entry_->get_function_label(nextFunction_)); + if (!label) { + add_to_output(from_const_string("F")); + add_to_output(from_integer(nextFunction_)); + } else { + add_to_output(from_const_string(label)); + } + add_to_output(from_const_string("\n")); + state_ = STATE_FN_NUMBER; + return; + } + case STATE_FN_NUMBER: { + add_to_output(from_const_string("")); + add_to_output(from_integer(nextFunction_)); + add_to_output(from_const_string("\n\n")); + state_ = STATE_FN_END; + return; + } + case STATE_FN_END: { + ++nextFunction_; + state_ = STATE_START_FN; + continue; + } + case STATE_NO_MORE_FN: { + add_to_output(from_const_string(kFdiXmlTail)); + state_ = STATE_EOF; + return; + } + case STATE_EOF: { + return; + } + } + } +} + +} // namespace commandstation \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/FindProtocolDefs.cpp b/components/LCCTrainSearchProtocol/FindProtocolDefs.cpp new file mode 100644 index 00000000..57d2b16a --- /dev/null +++ b/components/LCCTrainSearchProtocol/FindProtocolDefs.cpp @@ -0,0 +1,316 @@ +/** \copyright + * Copyright (c) 2014-2016, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file FindProtocolDefs.cxx + * + * Definitions for the train node find protocol. + * + * @author Balazs Racz + * @date 19 Feb 2016 + */ + +#include "FindProtocolDefs.hxx" +#include "TrainDb.hxx" +#include "ExternalTrainDbEntry.hxx" +#include + +namespace commandstation { + +/// Specifies what kind of train to allocate when the drive mode is left as +/// default / unspecified. +uint8_t FindProtocolDefs::DEFAULT_DRIVE_MODE = DCC_128; + +/// Specifies what kind of train to allocate when the drive mode is set as +/// MARKLIN_ANY. +uint8_t FindProtocolDefs::DEFAULT_MARKLIN_DRIVE_MODE = MARKLIN_NEW; + +/// Specifies what kind of train to allocate when the drive mode is set as +/// DCC_ANY. +uint8_t FindProtocolDefs::DEFAULT_DCC_DRIVE_MODE = DCC_128; + +namespace { +/// @returns true for a character that is a digit. +bool is_number(char c) { return ('0' <= c) && (c <= '9'); } + +/// @returns the same bitmask as match_query_to_node. +uint8_t attempt_match(const string name, unsigned pos, openlcb::EventId event) { + int count_matches = 0; + for (int shift = FindProtocolDefs::TRAIN_FIND_MASK - 4; + shift >= FindProtocolDefs::TRAIN_FIND_MASK_LOW; shift -= 4) { + uint8_t nibble = (event >> shift) & 0xf; + if ((0 <= nibble) && (nibble <= 9)) { + while (pos < name.size() && !is_number(name[pos])) ++pos; + if (pos > name.size()) return 0; + if ((name[pos] - '0') != nibble) return 0; + ++pos; + ++count_matches; + continue; + } else { + continue; + } + } + // If we are here, we have exhausted (and matched) all digits that were sent + // by the query. + if (count_matches == 0) { + // Query had no digits. That's weird and should be incompliant. + return 0; + } + // Look for more digits in the name. + while (pos < name.size() && !is_number(name[pos])) ++pos; + if (pos < name.size()) { + if (event & FindProtocolDefs::EXACT) { + return 0; + } + return FindProtocolDefs::MATCH_ANY; + } else { + return FindProtocolDefs::EXACT | FindProtocolDefs::MATCH_ANY; + } +} + +} // namespace + +// static +unsigned FindProtocolDefs::query_to_address(openlcb::EventId event, + DccMode* mode) { + unsigned supplied_address = 0; + bool has_prefix_zero = false; + for (int shift = TRAIN_FIND_MASK - 4; shift >= TRAIN_FIND_MASK_LOW; + shift -= 4) { + uint8_t nibble = (event >> shift) & 0xf; + if (0 == nibble && 0 == supplied_address) { + has_prefix_zero = true; + } + if ((0 <= nibble) && (nibble <= 9)) { + supplied_address *= 10; + supplied_address += nibble; + continue; + } + // For the moment we just ignore every non-numeric character. Including + // gluing together all digits entered by the user into one big number. + } + uint8_t drive_type = event & DCCMODE_PROTOCOL_MASK; + if (event & ALLOCATE) { + // If we are allocating, then we fill in defaults for drive modes. + if (drive_type == DCCMODE_DEFAULT) { + drive_type = DEFAULT_DRIVE_MODE; + } else if (drive_type == MARKLIN_DEFAULT) { + drive_type = DEFAULT_MARKLIN_DRIVE_MODE; + } else if (drive_type == DCC_DEFAULT) { + drive_type = DEFAULT_DCC_DRIVE_MODE; + } else if (drive_type == (DCC_DEFAULT | DCC_LONG_ADDRESS)) { + drive_type |= (DEFAULT_DCC_DRIVE_MODE & DCC_SS_MASK); + } + } + + if (has_prefix_zero && // + (((drive_type & DCC_ANY_MASK) == DCC_ANY) || (drive_type == DCCMODE_DEFAULT))) { + drive_type |= commandstation::DCC_DEFAULT | commandstation::DCC_LONG_ADDRESS; + } + *mode = static_cast(drive_type); + return supplied_address; +} + +// static +openlcb::EventId FindProtocolDefs::address_to_query(unsigned address, + bool exact, DccMode mode) { + uint64_t event = TRAIN_FIND_BASE; + int shift = TRAIN_FIND_MASK_LOW; + while (address) { + event |= (address % 10) << shift; + shift += 4; + address /= 10; + } + while (shift < TRAIN_FIND_MASK) { + event |= UINT64_C(0xF) << shift; + shift += 4; + } + if (exact) { + event |= EXACT; + } + event |= mode & DCCMODE_PROTOCOL_MASK; + return event; +} + +// static +uint8_t FindProtocolDefs::match_query_to_node(openlcb::EventId event, + TrainDbEntry* train) { + // empty search should match everything. + if (event == openlcb::TractionDefs::IS_TRAIN_EVENT) { + return MATCH_ANY | ADDRESS_ONLY | EXACT; + } + unsigned legacy_address = train->get_legacy_address(); + DccMode mode; + unsigned supplied_address = query_to_address(event, &mode); + bool has_address_prefix_match = false; + auto desired_address_type = dcc_mode_to_address_type(mode, supplied_address); + auto actual_address_type = dcc_mode_to_address_type(train->get_legacy_drive_mode(), + legacy_address); + if (supplied_address == legacy_address) { + if (actual_address_type == dcc::TrainAddressType::UNSUPPORTED || + desired_address_type == dcc::TrainAddressType::UNSPECIFIED || + desired_address_type == actual_address_type) { + // If the caller did not specify the drive mode, or the drive mode + // matches. + return MATCH_ANY | ADDRESS_ONLY | EXACT; + } else { + LOG(INFO, "exact match failed due to mode: desired %d actual %d", + static_cast(desired_address_type), static_cast(actual_address_type)); + } + has_address_prefix_match = ((event & EXACT) == 0); + } + if ((event & EXACT) == 0) { + // Search for the supplied number being a prefix of the existing addresses. + unsigned address_prefix = legacy_address / 10; + while (address_prefix) { + if (address_prefix == supplied_address) { + has_address_prefix_match = true; + break; + } + address_prefix /= 10; + } + } + if ((mode != DCCMODE_DEFAULT) && (event & EXACT) && (event & ALLOCATE)) { + // Request specified a drive mode and allocation. We check the drive mode + // to match. + if (desired_address_type != actual_address_type) { + return 0; + } + } + if (event & ADDRESS_ONLY) { + if (((event & EXACT) != 0) || (!has_address_prefix_match)) return 0; + return MATCH_ANY | ADDRESS_ONLY; + } + // Match against the train name string. + uint8_t first_name_match = 0xFF; + uint8_t best_name_match = 0; + string name = train->get_train_name(); + // Find the beginning of numeric components in the train name + unsigned pos = 0; + while (pos < name.size()) { + if (is_number(name[pos])) { + uint8_t current_match = attempt_match(name, pos, event); + if (first_name_match == 0xff) { + first_name_match = current_match; + best_name_match = current_match; + } + if ((!best_name_match && current_match) || (current_match & EXACT)) { + // We overwrite the best name match if there was no previous match, or + // if the current match is exact. This is somewhat questionable, + // because it will allow an exact match on the middle of the train name + // even though there may have been numbers before. However, this is + // arguably okay in case the train name is a model number and a cab + // number, and we're matching against the cab number, e.g. + // "Re 4/4 11239" which should be exact-matching the query 11239. + best_name_match = current_match; + } + // Skip through the sequence of numbers + while (pos < name.size() && is_number(name[pos])) { + ++pos; + } + } else { + // non number: skip + ++pos; + } + } + if (first_name_match == 0xff) { + // No numbers in the train name. + best_name_match = 0; + } + if (((best_name_match & EXACT) == 0) && has_address_prefix_match && + ((event & EXACT) == 0)) { + // We prefer a partial address match over a non-exact name match. If + // address_prefix_match == true then the query had the EXACT bit false. + return MATCH_ANY | ADDRESS_ONLY; + } + if ((event & EXACT) && !(best_name_match & EXACT)) return 0; + return best_name_match; +} + +// static +openlcb::EventId FindProtocolDefs::input_to_event(const string& input) { + uint64_t event = TRAIN_FIND_BASE; + int shift = TRAIN_FIND_MASK - 4; + unsigned pos = 0; + bool has_space = true; + uint32_t qry = 0xFFFFFFFF; + while (shift >= TRAIN_FIND_MASK_LOW && pos < input.size()) { + if (is_number(input[pos])) { + qry <<= 4; + qry |= input[pos] - '0'; + has_space = false; + shift -= 4; + } else { + if (!has_space) { + qry <<= 4; + qry |= 0xF; + shift -= 4; + } + has_space = true; + } + pos++; + } + event |= uint64_t(qry & 0xFFFFFF) << TRAIN_FIND_MASK_LOW; + unsigned flags = 0; + if ((input[0] == '0') || (input.back() == 'L')) { + flags |= commandstation::DCC_ANY | commandstation::DCC_LONG_ADDRESS; + } else if (input.back() == 'M') { + flags |= MARKLIN_NEW; + } else if (input.back() == 'm') { + flags |= MARKLIN_OLD; + } else if (input.back() == 'S') { + flags |= DCC_ANY; + } + event &= ~UINT64_C(0xff); + event |= (flags & 0xff); + + return event; +} + +openlcb::EventId FindProtocolDefs::input_to_search(const string& input) { + if (input.empty()) { + return openlcb::TractionDefs::IS_TRAIN_EVENT; + } + auto event = input_to_event(input); + return event; +} + +openlcb::EventId FindProtocolDefs::input_to_allocate(const string& input) { + if (input.empty()) { + return 0; + } + auto event = input_to_event(input); + event |= (FindProtocolDefs::ALLOCATE | FindProtocolDefs::EXACT); + return event; +} + +uint8_t FindProtocolDefs::match_query_to_train(openlcb::EventId event, + const string& name, + unsigned address, DccMode mode) { + ExternalTrainDbEntry entry(name, address, mode); + return match_query_to_node(event, &entry); +} + +} // namespace commandstation \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/Kconfig.projbuild b/components/LCCTrainSearchProtocol/Kconfig.projbuild new file mode 100644 index 00000000..664a7905 --- /dev/null +++ b/components/LCCTrainSearchProtocol/Kconfig.projbuild @@ -0,0 +1,29 @@ +menu "LCC Train Search" +############################################################################### +# +# Log level constants from from components/OpenMRNLite/src/utils/logging.h +# +# ALWAYS : -1 +# FATAL : 0 +# LEVEL_ERROR : 1 +# WARNING : 2 +# INFO : 3 +# VERBOSE : 4 +# +# Note that FATAL will cause the MCU to reboot! +# +############################################################################### + choice LCC_TSP_LOGGING + bool "Log level" + default LCC_TSP_LOGGING_MINIMAL + config LCC_TSP_LOGGING_VERBOSE + bool "Verbose" + config LCC_TSP_LOGGING_MINIMAL + bool "Minimal" + endchoice + config LCC_TSP_LOG_LEVEL + int + default 4 if LCC_TSP_LOGGING_MINIMAL + default 3 if LCC_TSP_LOGGING_VERBOSE + default 5 +endmenu \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/XmlGenerator.cpp b/components/LCCTrainSearchProtocol/XmlGenerator.cpp new file mode 100644 index 00000000..1e32ea6f --- /dev/null +++ b/components/LCCTrainSearchProtocol/XmlGenerator.cpp @@ -0,0 +1,127 @@ +/** \copyright + * Copyright (c) 2015, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file XmlGenerator.cxx + * + * Interface for generating XML files on-the-fly on small-memory machines. + * + * @author Balazs Racz + * @date 16 Jan 2016 + */ + +#include "XmlGenerator.hxx" +#include + +namespace commandstation { + +ssize_t XmlGenerator::read(size_t offset, void* buf, size_t len) { + if (offset < fileOffset_) { + return -1; + } + offset -= fileOffset_; + char* output = static_cast(buf); + + while (len > 0) { + if (pendingActions_.empty()) { + generate_more(); + if (pendingActions_.empty()) { + // EOF. + break; + } + TypedQueue reversed; + while (!pendingActions_.empty()) { + reversed.push_front(pendingActions_.pop_front()); + } + std::swap(pendingActions_, reversed); + init_front_action(); + } + + const char* b = get_front_buffer(); + bufferOffset_ = 0; + /*if (offset <= bufferOffset_) { + bufferOffset_ = offset; + offset = 0; + }*/ + // Skip data that we don't need. + while (*b && offset > 0) { + --offset; + ++b; + ++bufferOffset_; + } + // Copy data from the front action buffer. + while (*b && len > 0) { + *output++ = *b++; + len--; + bufferOffset_++; + } + if (!*b) { + // Consume front of the actions. + delete pendingActions_.pop_front(); + fileOffset_ += bufferOffset_; + if (!pendingActions_.empty()) { + init_front_action(); + } + } + } + return output - static_cast(buf); +} + +const char* XmlGenerator::get_front_buffer() { + switch (pendingActions_.front()->type) { + case RENDER_INT: { + return buffer_; + } + case CONST_LITERAL: { + return static_cast(pendingActions_.front()->pointer); + } + default: + DIE("Unknown XML generation action."); + } +} + +void XmlGenerator::init_front_action() { + bufferOffset_ = 0; + switch (pendingActions_.front()->type) { + case RENDER_INT: { + integer_to_buffer(pendingActions_.front()->integer, buffer_); + break; + } + case CONST_LITERAL: { + break; + } + default: + DIE("Unknown XML generation action."); + } +} + +void XmlGenerator::internal_reset() { + fileOffset_ = 0; + while (!pendingActions_.empty()) { + delete pendingActions_.pop_front(); + } +} + +} // namespace commandstation \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/include/AllTrainNodes.hxx b/components/LCCTrainSearchProtocol/include/AllTrainNodes.hxx new file mode 100644 index 00000000..ea51069a --- /dev/null +++ b/components/LCCTrainSearchProtocol/include/AllTrainNodes.hxx @@ -0,0 +1,160 @@ +/** \copyright + * Copyright (c) 2014, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file AllTrainNodes.hxx + * + * A class that instantiates every train node from the TrainDb. + * + * @author Balazs Racz + * @date 20 May 2014 + */ + +#ifndef _BRACZ_COMMANDSTATION_ALLTRAINNODES_HXX_ +#define _BRACZ_COMMANDSTATION_ALLTRAINNODES_HXX_ + +#include +#include + +#include +#include + +#include "TrainDb.hxx" + +namespace openlcb +{ +class Node; +class TrainService; +class TrainImpl; +class MemoryConfigHandler; +class IncomingMessageStateFlow; +} + +namespace commandstation +{ +class FindProtocolServer; + +class AllTrainNodes : public Singleton +{ + public: + AllTrainNodes(TrainDb* db, openlcb::TrainService* traction_service, + openlcb::SimpleInfoFlow* info_flow, + openlcb::MemoryConfigHandler* memory_config, + openlcb::MemorySpace* train_cdi, + openlcb::MemorySpace* tmp_train_cdi); + ~AllTrainNodes(); + + /// Removes a TrainImpl for the requested address if it exists. + void remove_train_impl(int address); + + openlcb::TrainImpl* get_train_impl(openlcb::NodeID id, bool allocate=true); + + /// Finds or creates a TrainImpl for the requested address and drive_type. + /// @param drive_type is the drive type for the loco to create if it doesn't exist. + /// @param address is the legacy address of the loco to find or create. + openlcb::TrainImpl* get_train_impl(DccMode drive_type, int address); + + /// Returns a traindb entry or nullptr if the id is too high. + std::shared_ptr get_traindb_entry(int id); + + /// Returns a node id or 0 if the id is not known to be a train. + openlcb::NodeID get_train_node_id(int id, bool allocate=true); + + /// Creates a new train node based on the given address and drive mode. + /// @param drive_type describes what kind of train node this should be + /// @param address is the hardware (legacy) address + /// @return 0 if the allocation fails (invalid arguments) + openlcb::NodeID allocate_node(DccMode drive_type, int address); + + /// Return the maximum number of locomotives currently being serviced. + size_t size(); + + /// @return true if the provided node is a known/active train. + bool is_valid_train_node(openlcb::Node *node); + + /// @return true if the provided node id is a known/active train. + bool is_valid_train_node(openlcb::NodeID node_id, bool allocate=true); + + private: + // ==== Interface for children ==== + struct Impl; + + /// A child can look up if a local node is actually a Train node. If so, the + /// Impl structure will be returned. If the node is not known (or not a train + /// node maintained by this object), we return nullptr. + Impl* find_node(openlcb::Node* node); + Impl* find_node(openlcb::NodeID node_id, bool allocate=true); + + /// Helper function to create lok objects. Adds a new Impl structure to + /// impl_. + Impl* create_impl(int train_id, DccMode mode, int address); + + // Externally owned. + TrainDb* db_; + openlcb::TrainService* tractionService_; + openlcb::MemoryConfigHandler* memoryConfigService_; + openlcb::MemorySpace* ro_train_cdi_; + openlcb::MemorySpace* ro_tmp_train_cdi_; + + /// All train nodes that we know about. + std::vector trains_; + + /// Lock to protect trains_. + OSMutex trainsLock_; + + friend class FindProtocolServer; + std::unique_ptr findProtocolServer_; + + // Implementation objects that we carry for various protocols. + class TrainSnipHandler; + friend class TrainSnipHandler; + std::unique_ptr snipHandler_; + + class TrainPipHandler; + friend class TrainPipHandler; + std::unique_ptr pipHandler_; + + class TrainFDISpace; + friend class TrainFDISpace; + std::unique_ptr fdiSpace_; + + class TrainConfigSpace; + friend class TrainConfigSpace; + std::unique_ptr configSpace_; + + class TrainCDISpace; + friend class TrainCDISpace; + std::unique_ptr cdiSpace_; + + class TrainIdentifyHandler; + friend class TrainIdentifyHandler; + std::unique_ptr trainIdentHandler_; +}; + +openlcb::TrainImpl *create_train_node_helper(DccMode mode, int address); + +} // namespace commandstation + +#endif /* _BRACZ_COMMANDSTATION_ALLTRAINNODES_HXX_ */ \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/include/ExternalTrainDbEntry.hxx b/components/LCCTrainSearchProtocol/include/ExternalTrainDbEntry.hxx new file mode 100644 index 00000000..f17dfd87 --- /dev/null +++ b/components/LCCTrainSearchProtocol/include/ExternalTrainDbEntry.hxx @@ -0,0 +1,83 @@ +/** \copyright + * Copyright (c) 2014-2016, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file ExternalTrainDbEntry.hxx + * + * + * + * @author Balazs Racz + * @date Jul 2 2016 + */ + +#ifndef _COMMANDSTATION_EXTERNALTRAINDBENTRY_HXX_ +#define _COMMANDSTATION_EXTERNALTRAINDBENTRY_HXX_ + +#include "TrainDb.hxx" + +namespace commandstation { + +class ExternalTrainDbEntry : public TrainDbEntry { + public: + ExternalTrainDbEntry(const string& name, int address, DccMode mode = DCC_28) : name_(name), address_(address), mode_(mode) {} + + /** Returns an internal identifier that uniquely defines where this traindb + * entry was allocated from. */ + string identifier() override { return ""; } + + /** Retrieves the NMRAnet NodeID for the virtual node that represents a + * particular train known to the database. + */ + openlcb::NodeID get_traction_node() override { return 0; } + + /** Retrieves the name of the train. */ + string get_train_name() override { return name_; } + + /** Retrieves the legacy address of the train. */ + int get_legacy_address() override { return address_; } + + /** Retrieves the traction drive mode of the train. */ + DccMode get_legacy_drive_mode() override { return mode_; } + + /** Retrieves the label assigned to a given function, or FN_NONEXISTANT if + the function does not exist. */ + unsigned get_function_label(unsigned fn_id) override { return 0; } + + /** Returns the largest valid function ID for this train, or -1 if the train + has no functions. */ + int get_max_fn() override { return 0; } + + /** Setup for get_max_fn(). */ + void start_read_functions() override { } + + string name_; + int address_; + DccMode mode_; +}; + + +} // namespace commandstaiton + +#endif // _COMMANDSTATION_EXTERNALTRAINDBENTRY_HXX_ \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/include/FdiXmlGenerator.hxx b/components/LCCTrainSearchProtocol/include/FdiXmlGenerator.hxx new file mode 100644 index 00000000..bb66d589 --- /dev/null +++ b/components/LCCTrainSearchProtocol/include/FdiXmlGenerator.hxx @@ -0,0 +1,65 @@ +/** \copyright + * Copyright (c) 2016, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file FdiXmlGenerator.hxx + * + * Train FDI generator. + * + * @author Balazs Racz + * @date 16 Jan 2016 + */ + +#include "TrainDb.hxx" +#include "XmlGenerator.hxx" + +namespace commandstation { + +class FdiXmlGenerator : public XmlGenerator { + public: + /// Call this after the lokdb on entry was overwritten with the new loco's + /// data. + void reset(std::shared_ptr entry); + + private: + void generate_more() override; + + enum State { + STATE_START = 0, + STATE_XMLHEAD = STATE_START, + STATE_START_FN, + STATE_FN_NAME, + STATE_FN_NUMBER, + STATE_FN_END, + STATE_NO_MORE_FN, + STATE_EOF + }; + + State state_; + std::shared_ptr entry_; + int nextFunction_; +}; + +} // namespace commandstation \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/include/FindProtocolDefs.hxx b/components/LCCTrainSearchProtocol/include/FindProtocolDefs.hxx new file mode 100644 index 00000000..a7a5f1ab --- /dev/null +++ b/components/LCCTrainSearchProtocol/include/FindProtocolDefs.hxx @@ -0,0 +1,177 @@ +/** \copyright + * Copyright (c) 2014-2016, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file FindProtocolDefs.hxx + * + * Definitions for the train node find protocol. + * + * @author Balazs Racz + * @date 18 Feb 2016 + */ + +#ifndef _COMMANDSTATION_FINDPROTOCOLDEFS_HXX_ +#define _COMMANDSTATION_FINDPROTOCOLDEFS_HXX_ + +#include +#include "TrainDbDefs.hxx" + +namespace commandstation { + +class TrainDbEntry; + +struct FindProtocolDefs { + // static constexpr EventID + enum { + TRAIN_FIND_BASE = 0x090099FF00000000U, + }; + + // Command byte definitions + enum { + // What is the mask value for the event registry entry. + TRAIN_FIND_MASK = 32, + // Where does the command byte start. + TRAIN_FIND_MASK_LOW = 8, + + ALLOCATE = 0x80, + // SEARCH = 0x00, + + EXACT = 0x40, + // SUBSTRING = 0x00, + + ADDRESS_ONLY = 0x20, + // ADDRESS_NAME_CABNUMBER = 0x00 + + // Bits 0-4 are a DccMode enum. + + // Match response information. This bit is not used in the network + // protocol. The bit will be set in the matched result, while cleared for a + // no-match. + MATCH_ANY = 0x01, + }; + + static_assert((TRAIN_FIND_BASE & ((1ULL << TRAIN_FIND_MASK) - 1)) == 0, + "TRAIN_FIND_BASE is not all zero on the bottom"); + + // Search nibble definitions. + enum { + NIBBLE_UNUSED = 0xf, + NIBBLE_SPACE = 0xe, + NIBBLE_STAR = 0xd, + NIBBLE_QN = 0xc, + NIBBLE_HASH = 0xb, + }; + + /// @param event is an openlcb event ID + /// @return true if that event ID belong to the find protocol event range. + static bool is_find_event(openlcb::EventId event) { + return (event >> TRAIN_FIND_MASK) == (TRAIN_FIND_BASE >> TRAIN_FIND_MASK); + } + + /** Compares an incoming search query to a given train node. Returns 0 for a + no-match. Returns a bitfield of match types for a match. valid bits are + MATCH_ANY (always set), ADDRESS_ONLY (set when the match occurred in the + address), EXACT (clear for prefix match). */ + static uint8_t match_query_to_node(openlcb::EventId event, TrainDbEntry* train); + + /** Compares an incoming search query to a train node described by the major + * parameters only. mode should be set to 0 for ignore, or DCC_LONG_ADDRESS. + * Returns a bitfield of match types for a match. valid bits are MATCH_ANY + * (always set), ADDRESS_ONLY (set when the match occurred in the address), + * EXACT (clear for prefix match). + */ + static uint8_t match_query_to_train(openlcb::EventId event, + const string& name, unsigned address, + DccMode mode); + + /** Converts a find protocol query to an address and desired DccMode + information. Will take into account prefix zeros for forcing a dcc long + address, as well as all mode and flag bits coming in via the query. + + @param mode (can't be null) will be filled in with the Dcc Mode: the + bottom 3 bits as specified by the incoming query, or zero if the query + did not specify a preference. If the query started with a prefix of zero + (typed by the user) or DCC_FORCE_LONG_ADDRESS was set in the query, the + DccMode will have the force long address bit set. + + @returns the new legacy_address. */ + static unsigned query_to_address(openlcb::EventId query, DccMode* mode); + + /** Translates an address as punched in by a (dumb) throttle to a query to + * issue on the OpenLCB bus as a find protocol request. + * + * @param address is the numeric value that the user typed. + * @param exact should be true if only exact matches shall be retrieved. + * @param mode should be set most of the time to OLCBUSER to specify that we + * don't care about the address type, but can also be set to + * DCC_LONG_ADDRESS. + */ + static openlcb::EventId address_to_query(unsigned address, bool exact, DccMode mode); + + /** Translates a sequence of input digits punched in by a throttle to a query + * to issue on the OpenLCB bus as a find protocol request. + * + * @param input is the sequence of numbers that the user typed. This is + * expected to have form like '415' or '021' or '474014' + * @return an event ID representing the search. This event ID could be + * IS_TRAIN_EVENT. + */ + static openlcb::EventId input_to_search(const string& input); + + /** Translates a sequence of input digits punched in by a throttle to an + * allocate request to issue on the OpenLCB bus. + * + * @param input is the sequence of numbers that the user typed. This is + * expected to have form like '415' or '021' or '474014'. You can add a + * leading zero to force DCC long address, a trailing M to force a Marklin + * locomotive. + * @return an event ID representing the search. This event ID will be zero if + * the user input is invalid. + */ + static openlcb::EventId input_to_allocate(const string& input); + + /// Specifies what kind of train to allocate when the drive mode is left as + /// default / unspecified. + static uint8_t DEFAULT_DRIVE_MODE; + + /// Specifies what kind of train to allocate when the drive mode is set as + /// MARKLIN_ANY. + static uint8_t DEFAULT_MARKLIN_DRIVE_MODE; + + /// Specifies what kind of train to allocate when the drive mode is set as + /// DCC_ANY. + static uint8_t DEFAULT_DCC_DRIVE_MODE; + + private: + /// Helper function for the input_to_* calls. + static openlcb::EventId input_to_event(const string& input); + + // Not instantiatable class. + FindProtocolDefs(); +}; + +} // namespace commandstation + +#endif // _COMMANDSTATION_FINDPROTOCOLDEFS_HXX_ \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/include/FindProtocolServer.hxx b/components/LCCTrainSearchProtocol/include/FindProtocolServer.hxx new file mode 100644 index 00000000..57ed4133 --- /dev/null +++ b/components/LCCTrainSearchProtocol/include/FindProtocolServer.hxx @@ -0,0 +1,341 @@ +/** \copyright + * Copyright (c) 2014-2016, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file FindProtocolDefs.hxx + * + * Implementation of the find protocol event handler. + * + * @author Balazs Racz + * @date 18 Feb 2016 + */ + +#ifndef _COMMANDSTATION_FINDPROTOCOLSERVER_HXX_ +#define _COMMANDSTATION_FINDPROTOCOLSERVER_HXX_ + +#include "FindProtocolDefs.hxx" +#include "AllTrainNodes.hxx" +#include +#include + +namespace commandstation { + +class FindProtocolServer : public openlcb::SimpleEventHandler { + public: + FindProtocolServer(AllTrainNodes *nodes) : parent_(nodes) { + openlcb::EventRegistry::instance()->register_handler( + EventRegistryEntry(this, FindProtocolDefs::TRAIN_FIND_BASE), + FindProtocolDefs::TRAIN_FIND_MASK); + } + + ~FindProtocolServer() { + openlcb::EventRegistry::instance()->unregister_handler(this); + } + + void handle_identify_global(const EventRegistryEntry ®istry_entry, + EventReport *event, + BarrierNotifiable *done) override { + AutoNotify an(done); + + if (event && event->dst_node) { + // Identify addressed + if (!parent_->is_valid_train_node(event->dst_node)) { + LOG(VERBOSE, "not a valid train node: %s, ignoring" + , uint64_to_string_hex(event->dst_node->node_id()).c_str()); + return; + } + static_assert(((FindProtocolDefs::TRAIN_FIND_BASE >> + FindProtocolDefs::TRAIN_FIND_MASK) & + 1) == 1, + "The lowermost bit of the TRAIN_FIND_BASE must be 1 or " + "else the event produced range encoding must be updated."); + event->event_write_helper<1>()->WriteAsync( + event->dst_node, openlcb::Defs::MTI_PRODUCER_IDENTIFIED_RANGE, + openlcb::WriteHelper::global(), + openlcb::eventid_to_buffer(FindProtocolDefs::TRAIN_FIND_BASE), + done->new_child()); + } else { + // Identify global + + if (pendingGlobalIdentify_) { + // We have not started processing the global identify yet. Swallow this + // one. + LOG(VERBOSE, "discarding duplicate global identify"); + return; + } + // We do a synchronous alloc here but there isn't a much better choice. + auto *b = flow_.alloc(); + // Can't do this -- see handleidentifyproducer. + // b->set_done(done->new_child()); + pendingGlobalIdentify_ = true; + b->data()->reset(REQUEST_GLOBAL_IDENTIFY); + flow_.send(b); + } + } + + void handle_identify_producer(const EventRegistryEntry ®istry_entry, + EventReport *event, + BarrierNotifiable *done) override { + AutoNotify an(done); + + auto *b = flow_.alloc(); + // This would be nice in that we would prevent allocating more buffers + // while the previous request is serviced. However, thereby we also block + // the progress on the event handler flow, which will cause deadlocks, + // since servicing the request involves sending events out that will be + // looped back. + // + // b->set_done(done->new_child()); + b->data()->reset(event->event); + flow_.send(b); + }; + + // For testing. + bool is_idle() { return flow_.is_waiting(); } + + private: + enum { + // Send this in the event_ field if there is a global identify + // pending. This is not a valid EventID, because the upper byte is 0. + REQUEST_GLOBAL_IDENTIFY = 0x0001000000000000U, + }; + struct Request { + void reset(openlcb::EventId event) { event_ = event; } + EventId event_; + }; + + class FindProtocolFlow : public StateFlow, QList<1> > { + public: + FindProtocolFlow(FindProtocolServer *parent) + : StateFlow(parent->parent_->tractionService_), parent_(parent) + , tractionService_(parent->parent_->tractionService_) {} + + Action entry() override { + eventId_ = message()->data()->event_; + release(); + if (eventId_ == REQUEST_GLOBAL_IDENTIFY) { + if (!parent_->pendingGlobalIdentify_) { + LOG(VERBOSE, "duplicate global identify"); + // Duplicate global identify, or the previous one was already handled. + return exit(); + } + parent_->pendingGlobalIdentify_ = false; + } + LOG(VERBOSE, "starting iteration"); + nextTrainId_ = 0; + hasMatches_ = false; + return call_immediately(STATE(iterate)); + } + + Action iterate() { + LOG(VERBOSE, "iterate nextTrainId: %d", nextTrainId_); + if (nextTrainId_ >= nodes()->size()) { + return call_immediately(STATE(iteration_done)); + } + if (eventId_ == REQUEST_GLOBAL_IDENTIFY) { + if (parent_->pendingGlobalIdentify_) { + LOG(VERBOSE, "restart iteration (new event)"); + // Another notification arrived. Start iteration from 0. + nextTrainId_ = 0; + parent_->pendingGlobalIdentify_ = false; + return again(); + } + return allocate_and_call( + tractionService_->iface()->global_message_write_flow(), + STATE(send_response)); + } + auto db_entry = nodes()->get_traindb_entry(nextTrainId_); + if (!db_entry) return call_immediately(STATE(next_iterate)); + if (FindProtocolDefs::match_query_to_node(eventId_, db_entry.get())) { + LOG(VERBOSE, "found match %s / %s", uint64_to_string_hex(eventId_).c_str(), uint64_to_string_hex(db_entry->get_traction_node()).c_str()); + hasMatches_ = true; + return allocate_and_call( + tractionService_->iface()->global_message_write_flow(), + STATE(send_response)); + } + return yield_and_call(STATE(next_iterate)); + } + + Action send_response() { + auto *b = get_allocation_result( + tractionService_->iface()->global_message_write_flow()); + b->set_done(bn_.reset(this)); + if (eventId_ == REQUEST_GLOBAL_IDENTIFY) { + b->data()->reset( + openlcb::Defs::MTI_PRODUCER_IDENTIFIED_RANGE, + nodes()->get_train_node_id(nextTrainId_), + openlcb::eventid_to_buffer(FindProtocolDefs::TRAIN_FIND_BASE)); + } else { + b->data()->reset(openlcb::Defs::MTI_PRODUCER_IDENTIFIED_VALID, + nodes()->get_train_node_id(nextTrainId_), + openlcb::eventid_to_buffer(eventId_)); + } + b->data()->set_flag_dst(openlcb::GenMessage::WAIT_FOR_LOCAL_LOOPBACK); + parent_->parent_->tractionService_->iface() + ->global_message_write_flow() + ->send(b); + + return wait_and_call(STATE(next_iterate)); + } + + Action next_iterate() { + ++nextTrainId_; + return call_immediately(STATE(iterate)); + } + + Action iteration_done() { + LOG(VERBOSE, "iteration_done"); + if (!hasMatches_ && (eventId_ & FindProtocolDefs::ALLOCATE)) { + LOG(VERBOSE, "no match, allocating"); + // TODO: we should wait some time, maybe 200 msec for any responses + // from other nodes, possibly a deadrail train node, before we actually + // allocate a new train node. + DccMode mode; + unsigned address = FindProtocolDefs::query_to_address(eventId_, &mode); + newNodeId_ = nodes()->allocate_node(mode, address); + if (!newNodeId_) { + LOG(WARNING, "Decided to allocate node but failed. type=%d addr=%d", + (int)mode, (int)address); + return exit(); + } + return call_immediately(STATE(wait_for_new_node)); + } + LOG(VERBOSE, "no match, no allocate"); + return exit(); + } + + /// Yields until the new node is initiaqlized and we are allowed to send + /// traffic out from it. + Action wait_for_new_node() { + openlcb::Node *n = + tractionService_->iface()->lookup_local_node(newNodeId_); + HASSERT(n); + if (n->is_initialized()) { + return call_immediately(STATE(new_node_reply)); + } else { + return sleep_and_call(&timer_, MSEC_TO_NSEC(1), STATE(wait_for_new_node)); + } + } + + Action new_node_reply() { + return allocate_and_call( + tractionService_->iface()->global_message_write_flow(), + STATE(send_new_node_response)); + } + + Action send_new_node_response() { + auto *b = get_allocation_result( + tractionService_->iface()->global_message_write_flow()); + b->data()->reset(openlcb::Defs::MTI_PRODUCER_IDENTIFIED_VALID, newNodeId_, + openlcb::eventid_to_buffer(eventId_)); + tractionService_->iface()->global_message_write_flow()->send(b); + return exit(); + } + + private: + AllTrainNodes *nodes() { return parent_->parent_; } + FindProtocolServer *parent_; + openlcb::TrainService* tractionService_; + + openlcb::EventId eventId_; + union { + unsigned nextTrainId_; + openlcb::NodeID newNodeId_; + }; + BarrierNotifiable bn_; + bool hasMatches_; + StateFlowTimer timer_{this}; + }; + + AllTrainNodes *parent_; + + /// Set to true when a global identify message is received. When a global + /// identify starts processing, it shall be set to false. If a global + /// identify request arrives with no pendingGlobalIdentify_, that is a + /// duplicate request that can be ignored. + bool pendingGlobalIdentify_{false}; + + FindProtocolFlow flow_{this}; +}; + +class SingleNodeFindProtocolServer : public openlcb::SimpleEventHandler { + public: + using Node = openlcb::Node; + + SingleNodeFindProtocolServer(Node *node, TrainDbEntry *db_entry) + : node_(node), dbEntry_(db_entry) { + openlcb::EventRegistry::instance()->register_handler( + EventRegistryEntry(this, FindProtocolDefs::TRAIN_FIND_BASE), + FindProtocolDefs::TRAIN_FIND_MASK); + } + + ~SingleNodeFindProtocolServer() { + openlcb::EventRegistry::instance()->unregister_handler(this); + } + + void handle_identify_global(const EventRegistryEntry ®istry_entry, + EventReport *event, + BarrierNotifiable *done) override { + AutoNotify an(done); + + if (event && event->dst_node) { + // Identify addressed + if (event->dst_node != node_) return; + } + + static_assert(((FindProtocolDefs::TRAIN_FIND_BASE >> + FindProtocolDefs::TRAIN_FIND_MASK) & + 1) == 1, + "The lowermost bit of the TRAIN_FIND_BASE must be 1 or " + "else the event produced range encoding must be updated."); + event->event_write_helper<1>()->WriteAsync( + event->dst_node, openlcb::Defs::MTI_PRODUCER_IDENTIFIED_RANGE, + openlcb::WriteHelper::global(), + openlcb::eventid_to_buffer(FindProtocolDefs::TRAIN_FIND_BASE), + done->new_child()); + } + + void handle_identify_producer(const EventRegistryEntry ®istry_entry, + EventReport *event, + BarrierNotifiable *done) override { + AutoNotify an(done); + + if (FindProtocolDefs::match_query_to_node(event->event, dbEntry_)) { + event->event_write_helper<1>()->WriteAsync( + node_, openlcb::Defs::MTI_PRODUCER_IDENTIFIED_VALID, + openlcb::WriteHelper::global(), + openlcb::eventid_to_buffer(event->event), + done->new_child()); + } + }; + + private: + Node* node_; + TrainDbEntry* dbEntry_; +}; + +} // namespace commandstation + +#endif // _COMMANDSTATION_FINDPROTOCOLSERVER_HXX_ \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/include/ProgrammingTrackSpaceConfig.hxx b/components/LCCTrainSearchProtocol/include/ProgrammingTrackSpaceConfig.hxx new file mode 100644 index 00000000..f5698285 --- /dev/null +++ b/components/LCCTrainSearchProtocol/include/ProgrammingTrackSpaceConfig.hxx @@ -0,0 +1,129 @@ +/** \copyright + * Copyright (c) 2018, Balazs Racz + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are strictly prohibited without written consent. + * + * 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. + * + * \file ProgrammingTrackSpaceConfig.hxx + * + * CDI configuration for the CV space to access the programming track flow. + * + * @author Balazs Racz + * @date 2 June 2018 + */ + +#ifndef _COMMANDSTATION_PROGRAMMINGTRACKSPACECONFIG_HXX_ +#define _COMMANDSTATION_PROGRAMMINGTRACKSPACECONFIG_HXX_ + +#include +#include + +namespace commandstation { + +static const char OPERATING_MODE_MAP_VALUES[] = R"( +0Disabled +1Direct mode +2POM mode +10Advanced mode +)"; + + +CDI_GROUP(ProgrammingTrackSpaceConfigAdvanced); +CDI_GROUP_ENTRY( + repeat_verify, openlcb::Uint32ConfigEntry, + Name("Repeat count for verify packets"), + Description("How many times a direct mode bit verify packet needs to be " + "repeated for an acknowledgement to be generated."), + Default(3), Min(0), Max(255)); +CDI_GROUP_ENTRY( + repeat_cooldown_reset, openlcb::Uint32ConfigEntry, + Name("Repeat count for reset packets after verify"), + Description("How many reset packets to send after a verify."), + Default(6), Min(0), Max(255)); +CDI_GROUP_END(); + +CDI_GROUP(ProgrammingTrackSpaceConfig, Segment(openlcb::MemoryConfigDefs::SPACE_DCC_CV), Offset(0x7F100000), + Name("Programming track operation"), + Description("Use this component to read and write CVs on the " + "programming track of the command station.")); + +enum OperatingMode { + PROG_DISABLED = 0, + DIRECT_MODE = 1, + POM_MODE = 2, + ADVANCED = 10, +}; + +CDI_GROUP_ENTRY(mode, openlcb::Uint32ConfigEntry, Name("Operating mode"), MapValues(OPERATING_MODE_MAP_VALUES)); +CDI_GROUP_ENTRY(cv, openlcb::Uint32ConfigEntry, Name("CV number"), Description("Number of CV to read or write (1..1024)."), Default(0), Min(0), Max(1024)); +CDI_GROUP_ENTRY( + value, openlcb::Uint32ConfigEntry, Name("CV value"), + Description( + "Set 'Operating mode' and 'CV number' first, then: hit 'Refresh' to " + "read the entire CV, or enter a value and hit 'Write' to set the CV."), + Default(0), Min(0), Max(255)); +CDI_GROUP_ENTRY( + bit_write_value, openlcb::Uint32ConfigEntry, Name("Bit change"), + Description( + "Set 'Operating mode' and 'CV number' first, then: write 1064 to set " + "the single bit whose value is 64, or 2064 to clear that bit. Write " + "100 to 107 to set bit index 0 to 7, or 200 to 207 to clear bit 0 to " + "7. Values outside of these two ranges do nothing."), + Default(1000), Min(100), Max(2128)); +CDI_GROUP_ENTRY(bit_value_string, openlcb::StringConfigEntry<24>, + Name("Read bits decomposition"), + Description("Hit Refresh on this line after reading a CV value " + "to see which bits are set.")); +CDI_GROUP_ENTRY(advanced, ProgrammingTrackSpaceConfigAdvanced, + Name("Advanced settings")); +struct Shadow; +CDI_GROUP_END(); + +/// This shadow structure is declared to be parallel to the CDI entries. +struct ProgrammingTrackSpaceConfig::Shadow { + uint32_t mode; + uint32_t cv; + uint32_t value; + uint32_t bit_write_value; + char bit_value_string[24]; + uint32_t verify_repeats; + uint32_t verify_cooldown_repeats; +}; + +#if __GNUC__ > 6 +#define SHADOW_OFFSETOF(entry) \ + (offsetof(ProgrammingTrackSpaceConfig::Shadow, entry)) +#else +#define SHADOW_OFFSETOF(entry) \ + ((uintptr_t) & ((ProgrammingTrackSpaceConfig::Shadow*)nullptr)->entry) +#endif + +static_assert(SHADOW_OFFSETOF(cv) == + ProgrammingTrackSpaceConfig::zero_offset_this().cv().offset(), + "Offset of CV field does not match."); + +static_assert(SHADOW_OFFSETOF(verify_cooldown_repeats) == + ProgrammingTrackSpaceConfig::zero_offset_this() + .advanced() + .repeat_cooldown_reset() + .offset(), + "Offset of repeat cooldown reset field does not match."); + +} // namespace commandstation + + + +#endif // _COMMANDSTATION_PROGRAMMINGTRACKSPACECONFIG_HXX_ \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/include/TrainDb.hxx b/components/LCCTrainSearchProtocol/include/TrainDb.hxx new file mode 100644 index 00000000..3ef8f6e6 --- /dev/null +++ b/components/LCCTrainSearchProtocol/include/TrainDb.hxx @@ -0,0 +1,124 @@ +/** \copyright + * Copyright (c) 2014, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file TrainDb.hxx + * + * Interface for accessing a train database for the mobile station lookup. + * + * @author Balazs Racz + * @date 18 May 2014 + */ + +#ifndef _MOBILESTATION_TRAINDB_HXX_ +#define _MOBILESTATION_TRAINDB_HXX_ + +#include +#include +#include +#include "TrainDbDefs.hxx" +#include "TrainDbCdi.hxx" + +namespace commandstation +{ + +class TrainDbEntry +{ +public: + virtual ~TrainDbEntry() {} + + /** Returns an internal identifier that uniquely defines where this traindb + * entry was allocated from. */ + virtual string identifier() = 0; + + /** Retrieves the NMRAnet NodeID for the virtual node that represents a + * particular train known to the database. + */ + virtual openlcb::NodeID get_traction_node() = 0; + + /** Retrieves the name of the train. */ + virtual string get_train_name() = 0; + + /** Retrieves the legacy address of the train. */ + virtual int get_legacy_address() = 0; + + /** Retrieves the traction drive mode of the train. */ + virtual DccMode get_legacy_drive_mode() = 0; + + /** Retrieves the label assigned to a given function, or FN_NONEXISTANT if + the function does not exist. */ + virtual unsigned get_function_label(unsigned fn_id) = 0; + + /** Returns the largest valid function ID for this train, or -1 if the train + has no functions. */ + virtual int get_max_fn() = 0; + + /** If non-negative, represents a file offset in the openlcb CONFIG_FILENAME + * file where this train has its data stored. */ + virtual int file_offset() { return -1; } + + /** Notifies that we are going to read all functions. Sometimes a + * re-initialization is helpful at this point. */ + virtual void start_read_functions() = 0; +}; + +class TrainDb +{ + public: + /** @returns the number of traindb entries. The valid train IDs will then be + * 0 <= id < size(). */ + virtual size_t size() = 0; + + /** Returns true if a train of a specific identifier is known to the + * traindb. + * @param train_id is the train identifier. Valid values: anything. Typical values: + * 0..NUM_TRAINS*/ + virtual bool is_train_id_known(unsigned train_id) = 0; + + /** Returns true if a train of a specific identifier is known to the + * traindb. + * @param train_id is the node id of the train being queried. + */ + virtual bool is_train_id_known(openlcb::NodeID train_id) = 0; + + /** Returns a train DB entry if the train ID is known, otherwise nullptr. The + ownership of the entry is not transferred. */ + virtual std::shared_ptr get_entry(unsigned train_id) = 0; + + /** Searches for an entry by the traction node ID. Returns nullptr if not + * found. @param hint is a train_id that might be a match. */ + virtual std::shared_ptr find_entry(openlcb::NodeID traction_node_id, + unsigned hint = 0) = 0; + + /** Inserts a given entry into the train database. + * @param address the locomotive address to create. + * @param mode the operating mode for the new locomotive. + * @returns the new train_id for the given entry. */ + virtual unsigned add_dynamic_entry(uint16_t address, DccMode mode) = 0; +}; + +} // namespace commandstation + +#endif // _MOBILESTATION_TRAINDB_HXX_ \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/include/TrainDbCdi.hxx b/components/LCCTrainSearchProtocol/include/TrainDbCdi.hxx new file mode 100644 index 00000000..2958a266 --- /dev/null +++ b/components/LCCTrainSearchProtocol/include/TrainDbCdi.hxx @@ -0,0 +1,149 @@ +/** \copyright + * Copyright (c) 2014-2016, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file TrainDbCdi.hxx + * + * CDI entry defining the commandstation traindb entry. + * + * @author Balazs Racz + * @date 8 Feb 2016 + */ + +#ifndef _BRACZ_COMMANDSTATION_TRAINDBCDI_HXX_ +#define _BRACZ_COMMANDSTATION_TRAINDBCDI_HXX_ + +#include +#include "TrainDbDefs.hxx" +#include "ProgrammingTrackSpaceConfig.hxx" + +namespace commandstation { + +static const char MOMENTARY_MAP[] = + "0Latching" + "1Momentary"; + +static const char FNDISPLAY_MAP[] = + "0Unavailable" + "1Light" + "2Beamer" + "3Bell" + "4Horn" + "5Shunting mode" + "6Pantograph" + "7Smoke" + "8Momentum off" + "9Whistle" + "10Sound" + "11F" + "12Announce" + "13Engine" + "14Light1" + "15Light2" + "17Uncouple" + "255Unavailable_"; + +CDI_GROUP(TrainDbCdiFunctionGroup, Name("Functions"), + Description("Defines what each function button does.")); +CDI_GROUP_ENTRY(icon, openlcb::Uint8ConfigEntry, Name("Display"), + Description("Defines how throttles display this function."), + Default(FN_NONEXISTANT), MapValues(FNDISPLAY_MAP)); +CDI_GROUP_ENTRY(is_momentary, openlcb::Uint8ConfigEntry, Name("Momentary"), + Description( + "Momentary functions are automatically turned off when you " + "release the respective button on the throttles."), + MapValues(MOMENTARY_MAP), Default(0)); +CDI_GROUP_END(); + +using TrainDbCdiRepFunctionGroup = + openlcb::RepeatedGroup; + +CDI_GROUP(F0Group, Name("F0"), + Description("F0 is permanently assigned to Light.")); +CDI_GROUP_ENTRY(blank, openlcb::EmptyGroup); +CDI_GROUP_END(); + +CDI_GROUP(TrainDbCdiAllFunctionGroup); +CDI_GROUP_ENTRY(f0, F0Group); +CDI_GROUP_ENTRY(all_functions, TrainDbCdiRepFunctionGroup, RepName("Fn")); +CDI_GROUP_END(); + +static const char DCC_DRIVE_MODE_MAP[] = + "0Unused" + "10DCC 28-step" + "11DCC 128-step" + "5Marklin-Motorola " + "I" + "6Marklin-Motorola " + "II" + "14DCC 28-step (forced long " + "address)" + "15DCC 128-step (forced long " + "address)"; + +CDI_GROUP(TrainDbCdiEntry, Description("Configures a single train")); +CDI_GROUP_ENTRY(address, openlcb::Uint16ConfigEntry, Name("Address"), + Description("Track protocol address of the train."), + Default(0)); +CDI_GROUP_ENTRY( + mode, openlcb::Uint8ConfigEntry, Name("Protocol"), + Description("Protocol to use on the track for driving this train."), + MapValues(DCC_DRIVE_MODE_MAP), Default(DCC_28)); +CDI_GROUP_ENTRY(name, openlcb::StringConfigEntry<16>, Name("Name"), + Description("Identifies the train node on the LCC bus.")); +CDI_GROUP_ENTRY(functions, TrainDbCdiAllFunctionGroup); +CDI_GROUP_END(); + +CDI_GROUP(TrainSegment, Segment(openlcb::MemoryConfigDefs::SPACE_CONFIG)); +CDI_GROUP_ENTRY(train, TrainDbCdiEntry); +CDI_GROUP_END(); + +CDI_GROUP(TrainConfigDef, MainCdi()); +// We do not support ACDI and we do not support adding the +// information in here because both of these vary train by train. +CDI_GROUP_ENTRY(ident, openlcb::Identification, Model("Virtual train node")); +CDI_GROUP_ENTRY(train, TrainSegment); +CDI_GROUP_ENTRY(cv, ProgrammingTrackSpaceConfig); +CDI_GROUP_END(); + +CDI_GROUP(TmpTrainSegment, Segment(openlcb::MemoryConfigDefs::SPACE_CONFIG), + Offset(0), Name("Non-stored train"), + Description( + "This train is not part of the train database, thus no " + "configuration settings can be changed on it.")); +CDI_GROUP_END(); + +/// This alternate CDI for a virtual train node will be in use for trains that +/// are not coming from the database. It will not offer any settings for the +/// user. +CDI_GROUP(TrainTmpConfigDef, MainCdi()); +CDI_GROUP_ENTRY(ident, openlcb::Identification, Model("Virtual train node")); +CDI_GROUP_ENTRY(train, TmpTrainSegment); +CDI_GROUP_ENTRY(cv, ProgrammingTrackSpaceConfig); +CDI_GROUP_END(); + +} // namespace commandstation + +#endif // _BRACZ_COMMANDSTATION_TRAINDBCDI_HXX_ \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/include/TrainDbDefs.hxx b/components/LCCTrainSearchProtocol/include/TrainDbDefs.hxx new file mode 100644 index 00000000..821eb737 --- /dev/null +++ b/components/LCCTrainSearchProtocol/include/TrainDbDefs.hxx @@ -0,0 +1,205 @@ +/** \copyright + * Copyright (c) 2016, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file TrainDbDefs.hxx + * + * Common definitions for the train database. + * + * @author Balazs Racz + * @date 13 Feb 2016 + */ + +#ifndef _COMMANDSTATION_TRAINDBDEFS_HXX_ +#define _COMMANDSTATION_TRAINDBDEFS_HXX_ + +#include +#include + +namespace commandstation { + +#define DCC_MAX_FN 29 + + +enum Symbols { + FN_NONEXISTANT = 0, + LIGHT = 1, + BEAMER = 2, + BELL = 3, + HORN = 128 + 4, + SHUNT = 5, + PANTO = 6, + SMOKE = 7, + ABV = 8, + WHISTLE = 128 + 9, + SOUND = 10, + FNT11 = 11, + SPEECH = 128 + 12, + ENGINE = 13, + LIGHT1 = 14, + LIGHT2 = 15, + TELEX = 128 + 17, + FN_UNKNOWN = 127, + MOMENTARY = 128, + FNP = 139, + SOUNDP = 141, + // Empty eeprom will have these bytes. + FN_UNINITIALIZED = 255, +}; + +// Use cases we need to support: +// +// - search, any protocol +// - search, specific address mode (dcc-short, dcc-long, marklin, dead rail) +// - allocate, any protocol +// - allocate, specific address mode (dcc-short, dcc-long, marklin, dead rail) +// - allocate, specific address mode and speed steps +// +// Dead rail is an address mode. +// Marklin old, new, twoaddr is a speed step mode +// DCC 14, 28, 128 is a speed step mode +// MFX is an address mode but unclear what the address is actually +// +// DCC protocol options: 3 bits: short/long; default-14-28-128. +// Marklin protocol options: old-new-twoaddr. Mfx? +// base protocol: dcc, marklin, deadrail +// dcc options: 3 bits +// marklin options: 2 bits (f0, f4, f8, mfx) +// additional protocols: selectrix, mth?, +// what if we don't want to express a preference? +// most settings have to have a neutral choice. +// eg. dcc default; dcc force long +// dcc default-14-28-128 +// marklin default could be marklin new +// +// Search protocol reserves bits 0x80, 0x40, 0x20 +// This leaves 5 bits for protocol. Protocol base = 2 bits; protocol options = 3 bits. +// protocol base: bits 0,1: +// . 0x0 : default / unspecified, olcbuser, all marklin fits here. +// . 0x1 : dcc +// . 0x2, 0x3 : reserved (expansion) +// +// marklin option bits: 2 bits should be default, force f0, force f8, force mfx +// third option bit should be reserved, check as zero +// +// default protocol needs to have the expansion bits reserved, check as zero. +// +// can we fold default and marklin over each other? +// two bits: 00 +// force long == 1 => marklin +// 00 0 00: default +// 00 0 01: olcb direct +// 00 0 10: mfx ? +// +// 00 1 00: marklin default +// 00 1 01: marklin old +// 00 1 10: marklin new +// 00 1 11: marklin 2-address +// +// 01 0 00: dcc (default) +// 01 1 00: dcc long +// 01 0 01: dcc 14 +// 01 1 01: dcc 14 long does not exist (all long address capable dcc decoders support 28) +// 01 0 10: dcc 28 +// 01 1 10: dcc 28 long +// 01 0 11: dcc 128 +// 01 1 11: dcc 128 long + + + +enum DccMode { + DCCMODE_DEFAULT = 0, + DCCMODE_FAKE_DRIVE = 1, + DCCMODE_OLCBUSER = 1, + + /// Value for testing whether the protocol is a Markin-Motorola protocol + /// variant. + MARKLIN_ANY = 0b00100, + /// Mask for testing whether the protocol is a Markin-Motorola protocol + /// variant. + MARKLIN_ANY_MASK = 0b11100, + /// Acquisition for a Marklin locomotive with default setting. + MARKLIN_DEFAULT = MARKLIN_ANY, + /// Force MM protocol version 1 (F0 only). + MARKLIN_OLD = MARKLIN_ANY | 1, + /// Force MM protocol version 2 (F0-F4). + MARKLIN_NEW = MARKLIN_ANY | 2, + /// Force MM protocol version 2 with subsequent address for more functions + /// (F0-F8). + MARKLIN_TWOADDR = MARKLIN_ANY | 3, + /// Alias for MFX locmotives (to be driven with Marklin v2 protocol for now). + MFX = MARKLIN_NEW, + + /// value for testing whether the protocol is a DCC variant + DCC_ANY = 0b01000, + /// mask for testing whether the protocol is a DCC variant + DCC_ANY_MASK = 0b11000, + + /// Acquisition for DCC locomotive with default settings. + DCC_DEFAULT = DCC_ANY, + /// Force long address for DCC. If clear, uses default address type by number. + DCC_LONG_ADDRESS = 0b00100, + /// Mask for the DCC speed step setting. + DCC_SS_MASK = 0b00011, + /// Unpecified / default speed step setting. + DCC_DEFAULT_SS = DCC_DEFAULT, + /// Force 14 SS mode + DCC_14 = DCC_ANY | 1, + /// Force 28 SS mode + DCC_28 = DCC_ANY | 2, + /// Force 128 SS mode + DCC_128 = DCC_ANY | 3, + /// Force 14 SS mode & long address (this is meaningless). + DCC_14_LONG_ADDRESS = DCC_14 | DCC_LONG_ADDRESS, + /// Force 28 SS mode & long address. + DCC_28_LONG_ADDRESS = DCC_28 | DCC_LONG_ADDRESS, + /// Force 128 SS mode & long address. + DCC_128_LONG_ADDRESS = DCC_128 | DCC_LONG_ADDRESS, + + /// Bit mask for the protocol field only. + DCCMODE_PROTOCOL_MASK = 0b11111, +}; + +inline dcc::TrainAddressType dcc_mode_to_address_type(DccMode mode, + uint32_t address) { + if (mode == DCCMODE_DEFAULT) { + return dcc::TrainAddressType::UNSPECIFIED; + } + if ((mode & MARKLIN_ANY_MASK) == MARKLIN_ANY) { + return dcc::TrainAddressType::MM; + } + if ((mode & DCC_ANY_MASK) == DCC_ANY) { + if ((mode & DCC_LONG_ADDRESS) || (address >= 128)) { + return dcc::TrainAddressType::DCC_LONG_ADDRESS; + } + return dcc::TrainAddressType::DCC_SHORT_ADDRESS; + } + LOG(INFO, "Unsupported drive mode %d (0x%02x)", mode, mode); + return dcc::TrainAddressType::UNSUPPORTED; +} + +} // namespace commandstation + +#endif // _COMMANDSTATION_TRAINDBDEFS_HXX_ \ No newline at end of file diff --git a/components/LCCTrainSearchProtocol/include/XmlGenerator.hxx b/components/LCCTrainSearchProtocol/include/XmlGenerator.hxx new file mode 100644 index 00000000..1cae24c6 --- /dev/null +++ b/components/LCCTrainSearchProtocol/include/XmlGenerator.hxx @@ -0,0 +1,127 @@ +/** \copyright + * Copyright (c) 2015, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file XmlGenerator.hxx + * + * Interface for generating XML files on-the-fly on small-memory machines. + * + * @author Balazs Racz + * @date 16 Jan 2016 + */ + +#ifndef _BRACZ_MOBILESTATION_XMLGENERATOR_HXX_ +#define _BRACZ_MOBILESTATION_XMLGENERATOR_HXX_ + +#include +#include + +namespace commandstation { + + +class XmlGenerator { + public: + XmlGenerator() {} + + /// Reads from the buffer, or generates more data to read. Returns the number + /// of bytes written to buf. Returns a short read (including 0) if and only + /// if EOF is reached. + ssize_t read(size_t offset, void* buf, size_t len); + + size_t file_offset() { + return fileOffset_; + } + + protected: + struct GeneratorAction; + + /// This function will be called repeatedly in order to fill in the output + /// buffer. Each call must call add_to_output at least once unless the EOF is + /// reached. + virtual void generate_more() = 0; + + /// Call this method from the driver API in order to + void internal_reset(); + + /// Call this function from generate_more to extend the output buffer. + void add_to_output(GeneratorAction* action) { + pendingActions_.push_front(action); + } + + + GeneratorAction* from_const_string(const char* data) { + GeneratorAction* a = new GeneratorAction; + a->type = CONST_LITERAL; + a->pointer = data; + return a; + } + + GeneratorAction* from_integer(int data) { + GeneratorAction* a = new GeneratorAction; + a->type = RENDER_INT; + a->integer = data; + return a; + } + + struct GeneratorAction : public QMember { + uint8_t type; + union { + const void* pointer; + int integer; + }; + }; + + private: + friend class TestEmptyXmlGenerator; + + enum ActionType { + CONST_LITERAL, + RENDER_INT, + }; + + /// Sets up the internal structures needed based on the action in the front + /// of the pendingQueue_. + void init_front_action(); + + /// Returns the pointer to the data representing the front action. + const char* get_front_buffer(); + + /// Actions that were generated by the last call of generate_more(). Note + /// that the order of these action is REVERSED during the call to + /// generate_more(). + TypedQueue pendingActions_; + + /// The offset (in the file) of the first byte of the first Action in + /// pendingActions_. + size_t fileOffset_; + + unsigned bufferOffset_; + /// For rendering integers. + char buffer_[16]; +}; + +} // namespace commandstation + +#endif // _BRACZ_MOBILESTATION_XMLGENERATOR_HXX_ \ No newline at end of file diff --git a/components/NeoNextion/CMakeLists.txt b/components/NeoNextion/CMakeLists.txt new file mode 100644 index 00000000..09846ab4 --- /dev/null +++ b/components/NeoNextion/CMakeLists.txt @@ -0,0 +1,7 @@ +#set(COMPONENT_SRCDIRS "src" ) + +#set(COMPONENT_ADD_INCLUDEDIRS "include" ) + +#set(COMPONENT_REQUIRES "driver" ) + +#register_component() \ No newline at end of file diff --git a/lib/NeoNextion/src/INextionBooleanValued.h b/components/NeoNextion/include/INextionBooleanValued.h similarity index 93% rename from lib/NeoNextion/src/INextionBooleanValued.h rename to components/NeoNextion/include/INextionBooleanValued.h index 1ddba388..9149ffc3 100644 --- a/lib/NeoNextion/src/INextionBooleanValued.h +++ b/components/NeoNextion/include/INextionBooleanValued.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_INEXTIONBOOLEANVALUED #define __NEONEXTION_INEXTIONBOOLEANVALUED -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionNumericalValued.h" #include "NextionTypes.h" @@ -20,7 +20,7 @@ class INextionBooleanValued : private INextionNumericalValued * \copydoc INextionWidget::INextionWidget */ INextionBooleanValued(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionNumericalValued(nex, page, component, name) { diff --git a/lib/NeoNextion/src/INextionCallback.h b/components/NeoNextion/include/INextionCallback.h similarity index 100% rename from lib/NeoNextion/src/INextionCallback.h rename to components/NeoNextion/include/INextionCallback.h diff --git a/lib/NeoNextion/src/INextionColourable.h b/components/NeoNextion/include/INextionColourable.h similarity index 81% rename from lib/NeoNextion/src/INextionColourable.h rename to components/NeoNextion/include/INextionColourable.h index eeba47a7..18571530 100644 --- a/lib/NeoNextion/src/INextionColourable.h +++ b/components/NeoNextion/include/INextionColourable.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_INEXTIONCOLOURABLE #define __NEONEXTION_INEXTIONCOLOURABLE -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionWidget.h" #include "NextionTypes.h" @@ -15,7 +15,7 @@ class INextionColourable : public virtual INextionWidget { public: INextionColourable(Nextion &nex, uint8_t page, uint8_t component, - const String &name); + const std::string &name); bool setForegroundColour(uint32_t colour, bool refresh = true); uint32_t getForegroundColour(); @@ -29,8 +29,8 @@ class INextionColourable : public virtual INextionWidget bool setEventBackgroundColour(uint32_t colour, bool refresh = true); uint32_t getEventBackgroundColour(); - bool setColour(const String &type, uint32_t colour, bool refresh); - uint32_t getColour(const String &type); + bool setColour(const std::string &type, uint32_t colour, bool refresh); + uint32_t getColour(const std::string &type); bool afterSet(bool result, bool refresh); }; diff --git a/lib/NeoNextion/src/INextionFontStyleable.h b/components/NeoNextion/include/INextionFontStyleable.h similarity index 91% rename from lib/NeoNextion/src/INextionFontStyleable.h rename to components/NeoNextion/include/INextionFontStyleable.h index fcb6ae44..7c352b1b 100644 --- a/lib/NeoNextion/src/INextionFontStyleable.h +++ b/components/NeoNextion/include/INextionFontStyleable.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_INEXTIONFONTSTYLEABLE #define __NEONEXTION_INEXTIONFONTSTYLEABLE -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionWidget.h" #include "NextionTypes.h" @@ -15,7 +15,7 @@ class INextionFontStyleable : public virtual INextionWidget { public: INextionFontStyleable(Nextion &nex, uint8_t page, uint8_t component, - const String &name); + const std::string &name); bool setFont(uint8_t id, bool refresh = true); uint8_t getFont(); diff --git a/lib/NeoNextion/src/INextionNumericalValued.h b/components/NeoNextion/include/INextionNumericalValued.h similarity index 93% rename from lib/NeoNextion/src/INextionNumericalValued.h rename to components/NeoNextion/include/INextionNumericalValued.h index ddf8c4e2..3efda4ba 100644 --- a/lib/NeoNextion/src/INextionNumericalValued.h +++ b/components/NeoNextion/include/INextionNumericalValued.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_INEXTIONNUMERICALVALUED #define __NEONEXTION_INEXTIONNUMERICALVALUED -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionWidget.h" #include "NextionTypes.h" @@ -20,7 +20,7 @@ class INextionNumericalValued : public virtual INextionWidget * \copydoc INextionWidget::INextionWidget */ INextionNumericalValued(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) { } diff --git a/lib/NeoNextion/src/INextionStringValued.h b/components/NeoNextion/include/INextionStringValued.h similarity index 85% rename from lib/NeoNextion/src/INextionStringValued.h rename to components/NeoNextion/include/INextionStringValued.h index 12c83d59..66124b84 100644 --- a/lib/NeoNextion/src/INextionStringValued.h +++ b/components/NeoNextion/include/INextionStringValued.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_INEXTIONSTRINGVALUED #define __NEONEXTION_INEXTIONSTRINGVALUED -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionWidget.h" #include "NextionTypes.h" @@ -20,7 +20,7 @@ class INextionStringValued : public virtual INextionWidget * \copydoc INextionWidget::INextionWidget */ INextionStringValued(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) { } @@ -32,7 +32,7 @@ class INextionStringValued : public virtual INextionWidget * \return Actual length of string * \see INextionStringValued::setText */ - size_t getText(String &buffer) + size_t getText(std::string &buffer) { return getStringProperty("txt", buffer); } @@ -43,7 +43,7 @@ class INextionStringValued : public virtual INextionWidget * \return True if successful * \see INextionStringValued::getText */ - bool setText(const String &buffer) + bool setText(const std::string &buffer) { return setStringProperty("txt", buffer); } @@ -56,7 +56,7 @@ class INextionStringValued : public virtual INextionWidget */ bool setTextAsNumber(uint32_t value) { - return setStringProperty("txt", String(value)); + return setStringProperty("txt", std::to_string(value)); } /*! @@ -66,10 +66,10 @@ class INextionStringValued : public virtual INextionWidget */ uint32_t getTextAsNumber() { - String buffer; + std::string buffer; if (getStringProperty("txt", buffer)) { - return buffer.toInt(); + return std::stoi(buffer); } else return 0; diff --git a/lib/NeoNextion/src/INextionTouchable.h b/components/NeoNextion/include/INextionTouchable.h similarity index 90% rename from lib/NeoNextion/src/INextionTouchable.h rename to components/NeoNextion/include/INextionTouchable.h index d71627e9..1371d47f 100644 --- a/lib/NeoNextion/src/INextionTouchable.h +++ b/components/NeoNextion/include/INextionTouchable.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_INEXTIONTOUCHABLE #define __NEONEXTION_INEXTIONTOUCHABLE -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionWidget.h" #include "INextionCallback.h" #include "NextionCallbackFunctionHandler.h" @@ -16,7 +16,7 @@ class INextionTouchable : public virtual INextionWidget { public: INextionTouchable(Nextion &nex, uint8_t page, uint8_t component, - const String &name); + const std::string &name); bool processEvent(uint8_t pageID, uint8_t componentID, uint8_t eventType); diff --git a/lib/NeoNextion/src/INextionWidget.h b/components/NeoNextion/include/INextionWidget.h similarity index 53% rename from lib/NeoNextion/src/INextionWidget.h rename to components/NeoNextion/include/INextionWidget.h index 5a4edc90..364b2b48 100644 --- a/lib/NeoNextion/src/INextionWidget.h +++ b/components/NeoNextion/include/INextionWidget.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_INEXTIONWIDGET #define __NEONEXTION_INEXTIONWIDGET -#include "Nextion.h" +#include "NeoNextion.h" /*! * \class INextionWidget @@ -16,16 +16,16 @@ class INextionWidget { public: INextionWidget(Nextion &nex, uint8_t page, uint8_t component, - const String &name); + const std::string &name); uint8_t getPageID(); uint8_t getComponentID(); - bool setNumberProperty(const String &propertyName, uint32_t value); - uint32_t getNumberProperty(const String &propertyName); - bool setPropertyCommand(const String &command, uint32_t value); - bool setStringProperty(const String &propertyName, const String &value); - size_t getStringProperty(const String &propertyName, String &buffer); + bool setNumberProperty(const std::string &propertyName, uint32_t value); + uint32_t getNumberProperty(const std::string &propertyName); + bool setPropertyCommand(const std::string &command, uint32_t value); + bool setStringProperty(const std::string &propertyName, const std::string &value); + size_t getStringProperty(const std::string &propertyName, std::string &buffer); bool show(); bool hide(); @@ -33,14 +33,14 @@ class INextionWidget bool disable(); protected: - void sendCommand(const String &format, ...); - bool sendCommandWithWait(const String &format, ...); + void sendCommand(const std::string &format, ...); + bool sendCommandWithWait(const std::string &format, ...); protected: Nextion &m_nextion; //!< Reference to the Nextion driver uint8_t m_pageID; //!< ID of page this widget is on uint8_t m_componentID; //!< Component ID of this widget - const String m_name; //!< Name of this widget + const std::string m_name; //!< Name of this widget }; #endif diff --git a/lib/NeoNextion/src/Nextion.h b/components/NeoNextion/include/NeoNextion.h similarity index 78% rename from lib/NeoNextion/src/Nextion.h rename to components/NeoNextion/include/NeoNextion.h index 09abd516..f1081a77 100644 --- a/lib/NeoNextion/src/Nextion.h +++ b/components/NeoNextion/include/NeoNextion.h @@ -3,14 +3,13 @@ #ifndef __NEONEXTION_NEXTION #define __NEONEXTION_NEXTION -#if defined(SPARK) || defined(PLATFORM_ID) -#include "application.h" -extern char *itoa(int a, char *buffer, unsigned char radix); -#else -#include -#endif +#include + +#include "sdkconfig.h" -#include +#include +#include +#include #include "NextionTypes.h" @@ -33,13 +32,13 @@ struct ITouchableListItem class Nextion { public: - Nextion(Stream &stream, bool flushSerialBeforeTx = true); + Nextion(uint8_t uartNum, long baud, uint8_t rx_pin, uint8_t tx_pin, bool flushSerialBeforeTx = true); bool init(); void poll(); bool refresh(); - bool refresh(const String &objectName); + bool refresh(const std::string &objectName); bool sleep(); bool wake(); @@ -53,7 +52,7 @@ class Nextion bool drawPicture(uint16_t x, uint16_t y, uint8_t id); bool drawPicture(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t id); bool drawStr(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t fontID, - const String &str, uint32_t bgColour = NEX_COL_BLACK, + const std::string &str, uint32_t bgColour = NEX_COL_BLACK, uint32_t fgColour = NEX_COL_WHITE, uint8_t bgType = NEX_BG_SOLIDCOLOUR, NextionFontAlignment xCentre = NEX_FA_CENTRE, @@ -65,15 +64,15 @@ class Nextion bool drawCircle(uint16_t x, uint16_t y, uint16_t r, uint32_t colour); void registerTouchable(INextionTouchable *touchable); - void sendCommand(const String &command); + void sendCommand(const std::string &command); void sendCommand(const char *format, ...); void sendCommand(const char *format, va_list args); bool checkCommandComplete(); bool receiveNumber(uint32_t *number); - size_t receiveString(String &buffer, bool stringHeader=true); + size_t receiveString(std::string &buffer, bool stringHeader=true); private: - Stream &m_serialPort; //!< Serial port device is attached to + uart_port_t m_serialPort; //!< Serial port device is attached to uint32_t m_timeout; //!< Serial communication timeout in ms bool m_flushSerialBeforeTx; //!< Flush serial port before transmission ITouchableListItem *m_touchableList; //!< LInked list of INextionTouchable diff --git a/lib/NeoNextion/src/NextionButton.h b/components/NeoNextion/include/NextionButton.h similarity index 96% rename from lib/NeoNextion/src/NextionButton.h rename to components/NeoNextion/include/NextionButton.h index 84e51639..03d87a6a 100644 --- a/lib/NeoNextion/src/NextionButton.h +++ b/components/NeoNextion/include/NextionButton.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONBUTTON #define __NEONEXTION_NEXTIONBUTTON -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" #include "INextionColourable.h" #include "INextionStringValued.h" @@ -22,7 +22,7 @@ class NextionButton : public INextionTouchable, /*! * \copydoc INextionWidget::INextionWidget */ - NextionButton(Nextion &nex, uint8_t page, uint8_t component, const String &name) + NextionButton(Nextion &nex, uint8_t page, uint8_t component, const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) , INextionColourable(nex, page, component, name) diff --git a/lib/NeoNextion/src/NextionCallbackFunctionHandler.h b/components/NeoNextion/include/NextionCallbackFunctionHandler.h similarity index 100% rename from lib/NeoNextion/src/NextionCallbackFunctionHandler.h rename to components/NeoNextion/include/NextionCallbackFunctionHandler.h diff --git a/lib/NeoNextion/src/NextionCheckbox.h b/components/NeoNextion/include/NextionCheckbox.h similarity index 92% rename from lib/NeoNextion/src/NextionCheckbox.h rename to components/NeoNextion/include/NextionCheckbox.h index 000cd4d1..d6166b88 100644 --- a/lib/NeoNextion/src/NextionCheckbox.h +++ b/components/NeoNextion/include/NextionCheckbox.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONCHECKBOX #define __NEONEXTION_NEXTIONCHECKBOX -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" #include "INextionColourable.h" #include "INextionBooleanValued.h" @@ -21,7 +21,7 @@ class NextionCheckbox : public INextionTouchable, * \copydoc INextionWidget::INextionWidget */ NextionCheckbox(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) , INextionColourable(nex, page, component, name) diff --git a/lib/NeoNextion/src/NextionCrop.h b/components/NeoNextion/include/NextionCrop.h similarity index 89% rename from lib/NeoNextion/src/NextionCrop.h rename to components/NeoNextion/include/NextionCrop.h index 40734ad6..26f4470e 100644 --- a/lib/NeoNextion/src/NextionCrop.h +++ b/components/NeoNextion/include/NextionCrop.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONCROP #define __NEONEXTION_NEXTIONCROP -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" /*! @@ -13,7 +13,7 @@ class NextionCrop : public INextionTouchable { public: - NextionCrop(Nextion &nex, uint8_t page, uint8_t component, const String &name); + NextionCrop(Nextion &nex, uint8_t page, uint8_t component, const std::string &name); uint16_t getPictureID(); bool setPictureID(uint16_t id); diff --git a/lib/NeoNextion/src/NextionDualStateButton.h b/components/NeoNextion/include/NextionDualStateButton.h similarity index 91% rename from lib/NeoNextion/src/NextionDualStateButton.h rename to components/NeoNextion/include/NextionDualStateButton.h index fa46ae2c..3eae3927 100644 --- a/lib/NeoNextion/src/NextionDualStateButton.h +++ b/components/NeoNextion/include/NextionDualStateButton.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONDUALSTATEBUTTON #define __NEONEXTION_NEXTIONDUALSTATEBUTTON -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" #include "INextionColourable.h" #include "INextionBooleanValued.h" @@ -21,7 +21,7 @@ class NextionDualStateButton : public INextionTouchable, * \copydoc INextionWidget::INextionWidget */ NextionDualStateButton(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) , INextionColourable(nex, page, component, name) diff --git a/lib/NeoNextion/src/NextionGauge.h b/components/NeoNextion/include/NextionGauge.h similarity index 94% rename from lib/NeoNextion/src/NextionGauge.h rename to components/NeoNextion/include/NextionGauge.h index f7e56c20..915b373f 100644 --- a/lib/NeoNextion/src/NextionGauge.h +++ b/components/NeoNextion/include/NextionGauge.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONGAUGE #define __NEONEXTION_NEXTIONGAUGE -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" #include "INextionColourable.h" #include "INextionNumericalValued.h" @@ -20,7 +20,7 @@ class NextionGauge : public INextionTouchable, /*! * \copydoc INextionWidget::INextionWidget */ - NextionGauge(Nextion &nex, uint8_t page, uint8_t component, const String &name) + NextionGauge(Nextion &nex, uint8_t page, uint8_t component, const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) , INextionColourable(nex, page, component, name) diff --git a/lib/NeoNextion/src/NextionHotspot.h b/components/NeoNextion/include/NextionHotspot.h similarity index 88% rename from lib/NeoNextion/src/NextionHotspot.h rename to components/NeoNextion/include/NextionHotspot.h index eaf4cdfa..aa3596ec 100644 --- a/lib/NeoNextion/src/NextionHotspot.h +++ b/components/NeoNextion/include/NextionHotspot.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONHOTSPOT #define __NEONEXTION_NEXTIONHOTSPOT -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionWidget.h" #include "INextionTouchable.h" @@ -18,7 +18,7 @@ class NextionHotspot : public INextionTouchable * \copydoc INextionWidget::INextionWidget */ NextionHotspot(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) { diff --git a/lib/NeoNextion/src/NextionNumber.h b/components/NeoNextion/include/NextionNumber.h similarity index 95% rename from lib/NeoNextion/src/NextionNumber.h rename to components/NeoNextion/include/NextionNumber.h index 556ccba2..3a08b316 100644 --- a/lib/NeoNextion/src/NextionNumber.h +++ b/components/NeoNextion/include/NextionNumber.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONNUMBER #define __NEONEXTION_NEXTIONNUMBER -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" #include "INextionColourable.h" #include "INextionNumericalValued.h" @@ -22,7 +22,7 @@ class NextionNumber : public INextionTouchable, /*! * \copydoc INextionWidget::INextionWidget */ - NextionNumber(Nextion &nex, uint8_t page, uint8_t component, const String &name) + NextionNumber(Nextion &nex, uint8_t page, uint8_t component, const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) , INextionColourable(nex, page, component, name) diff --git a/lib/NeoNextion/src/NextionPage.h b/components/NeoNextion/include/NextionPage.h similarity index 87% rename from lib/NeoNextion/src/NextionPage.h rename to components/NeoNextion/include/NextionPage.h index 1cb08052..d55046f4 100644 --- a/lib/NeoNextion/src/NextionPage.h +++ b/components/NeoNextion/include/NextionPage.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONPAGE #define __NEONEXTION_NEXTIONPAGE -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionWidget.h" /*! @@ -13,7 +13,7 @@ class NextionPage : public INextionWidget { public: - NextionPage(Nextion &nex, uint8_t page, uint8_t component, const String &name); + NextionPage(Nextion &nex, uint8_t page, uint8_t component, const std::string &name); bool show(); bool isShown(); diff --git a/lib/NeoNextion/src/NextionPicture.h b/components/NeoNextion/include/NextionPicture.h similarity index 85% rename from lib/NeoNextion/src/NextionPicture.h rename to components/NeoNextion/include/NextionPicture.h index 99d04d82..dce96c5f 100644 --- a/lib/NeoNextion/src/NextionPicture.h +++ b/components/NeoNextion/include/NextionPicture.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONPICTURE #define __NEONEXTION_NEXTIONPICTURE -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" /*! @@ -14,7 +14,7 @@ class NextionPicture : public INextionTouchable { public: NextionPicture(Nextion &nex, uint8_t page, uint8_t component, - const String &name); + const std::string &name); uint16_t getPictureID(); bool setPictureID(uint16_t id); diff --git a/lib/NeoNextion/src/NextionProgressBar.h b/components/NeoNextion/include/NextionProgressBar.h similarity index 92% rename from lib/NeoNextion/src/NextionProgressBar.h rename to components/NeoNextion/include/NextionProgressBar.h index b61220bb..059279c6 100644 --- a/lib/NeoNextion/src/NextionProgressBar.h +++ b/components/NeoNextion/include/NextionProgressBar.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONPROGRESSBAR #define __NEONEXTION_NEXTIONPROGRESSBAR -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" #include "INextionColourable.h" #include "INextionNumericalValued.h" @@ -21,7 +21,7 @@ class NextionProgressBar : public INextionTouchable, * \copydoc INextionWidget::INextionWidget */ NextionProgressBar(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) , INextionColourable(nex, page, component, name) diff --git a/lib/NeoNextion/src/NextionRadioButton.h b/components/NeoNextion/include/NextionRadioButton.h similarity index 92% rename from lib/NeoNextion/src/NextionRadioButton.h rename to components/NeoNextion/include/NextionRadioButton.h index 160843f6..c1ab44ec 100644 --- a/lib/NeoNextion/src/NextionRadioButton.h +++ b/components/NeoNextion/include/NextionRadioButton.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONRADIOBUTTON #define __NEONEXTION_NEXTIONRADIOBUTTON -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" #include "INextionColourable.h" #include "INextionBooleanValued.h" @@ -21,7 +21,7 @@ class NextionRadioButton : public INextionTouchable, * \copydoc INextionWidget::INextionWidget */ NextionRadioButton(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) , INextionColourable(nex, page, component, name) diff --git a/lib/NeoNextion/src/NextionSlider.h b/components/NeoNextion/include/NextionSlider.h similarity index 94% rename from lib/NeoNextion/src/NextionSlider.h rename to components/NeoNextion/include/NextionSlider.h index 5c970831..99a44824 100644 --- a/lib/NeoNextion/src/NextionSlider.h +++ b/components/NeoNextion/include/NextionSlider.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONSLIDER #define __NEONEXTION_NEXTIONSLIDER -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" #include "INextionColourable.h" #include "INextionNumericalValued.h" @@ -20,7 +20,7 @@ class NextionSlider : public INextionTouchable, /*! * \copydoc INextionWidget::INextionWidget */ - NextionSlider(Nextion &nex, uint8_t page, uint8_t component, const String &name) + NextionSlider(Nextion &nex, uint8_t page, uint8_t component, const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) , INextionColourable(nex, page, component, name) diff --git a/lib/NeoNextion/src/NextionSlidingText.h b/components/NeoNextion/include/NextionSlidingText.h similarity index 93% rename from lib/NeoNextion/src/NextionSlidingText.h rename to components/NeoNextion/include/NextionSlidingText.h index 58f20b64..6c78638a 100644 --- a/lib/NeoNextion/src/NextionSlidingText.h +++ b/components/NeoNextion/include/NextionSlidingText.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONSLIDINGTEXT #define __NEONEXTION_NEXTIONSLIDINGTEXT -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" #include "INextionColourable.h" #include "INextionStringValued.h" @@ -23,7 +23,7 @@ class NextionSlidingText : public INextionTouchable, * \copydoc INextionWidget::INextionWidget */ NextionSlidingText(Nextion &nex, uint8_t page, uint8_t component, - const String &name); + const std::string &name); bool setScrolling(bool scroll); bool isScrolling(); diff --git a/lib/NeoNextion/src/NextionText.h b/components/NeoNextion/include/NextionText.h similarity index 95% rename from lib/NeoNextion/src/NextionText.h rename to components/NeoNextion/include/NextionText.h index 9933f1ea..f3c870cb 100644 --- a/lib/NeoNextion/src/NextionText.h +++ b/components/NeoNextion/include/NextionText.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONTEXT #define __NEONEXTION_NEXTIONTEXT -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" #include "INextionColourable.h" #include "INextionStringValued.h" @@ -22,7 +22,7 @@ class NextionText : public INextionTouchable, /*! * \copydoc INextionWidget::INextionWidget */ - NextionText(Nextion &nex, uint8_t page, uint8_t component, const String &name) + NextionText(Nextion &nex, uint8_t page, uint8_t component, const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) , INextionColourable(nex, page, component, name) diff --git a/lib/NeoNextion/src/NextionTimer.h b/components/NeoNextion/include/NextionTimer.h similarity index 89% rename from lib/NeoNextion/src/NextionTimer.h rename to components/NeoNextion/include/NextionTimer.h index b9f3b5da..9cf95f2e 100644 --- a/lib/NeoNextion/src/NextionTimer.h +++ b/components/NeoNextion/include/NextionTimer.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONTIMER #define __NEONEXTION_NEXTIONTIMER -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" /*! @@ -13,7 +13,7 @@ class NextionTimer : public INextionTouchable { public: - NextionTimer(Nextion &nex, uint8_t page, uint8_t component, const String &name); + NextionTimer(Nextion &nex, uint8_t page, uint8_t component, const std::string &name); uint32_t getCycle(); bool setCycle(uint32_t cycle); diff --git a/lib/NeoNextion/src/NextionTypes.h b/components/NeoNextion/include/NextionTypes.h similarity index 100% rename from lib/NeoNextion/src/NextionTypes.h rename to components/NeoNextion/include/NextionTypes.h diff --git a/lib/NeoNextion/src/NextionVariableNumeric.h b/components/NeoNextion/include/NextionVariableNumeric.h similarity index 88% rename from lib/NeoNextion/src/NextionVariableNumeric.h rename to components/NeoNextion/include/NextionVariableNumeric.h index 74a9ff72..5ac964e5 100644 --- a/lib/NeoNextion/src/NextionVariableNumeric.h +++ b/components/NeoNextion/include/NextionVariableNumeric.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONVARIABLENUMERIC #define __NEONEXTION_NEXTIONVARIABLENUMERIC -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionNumericalValued.h" /*! @@ -17,7 +17,7 @@ class NextionVariableNumeric : public INextionNumericalValued * \copydoc INextionWidget::INextionWidget */ NextionVariableNumeric(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionNumericalValued(nex, page, component, name) { diff --git a/lib/NeoNextion/src/NextionVariableString.h b/components/NeoNextion/include/NextionVariableString.h similarity index 88% rename from lib/NeoNextion/src/NextionVariableString.h rename to components/NeoNextion/include/NextionVariableString.h index facd024a..5febe933 100644 --- a/lib/NeoNextion/src/NextionVariableString.h +++ b/components/NeoNextion/include/NextionVariableString.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONVARIABLESTRING #define __NEONEXTION_NEXTIONVARIABLESTRING -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionWidget.h" #include "INextionStringValued.h" @@ -18,7 +18,7 @@ class NextionVariableString : public INextionStringValued * \copydoc INextionWidget::INextionWidget */ NextionVariableString(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionStringValued(nex, page, component, name) { diff --git a/lib/NeoNextion/src/NextionWaveform.h b/components/NeoNextion/include/NextionWaveform.h similarity index 92% rename from lib/NeoNextion/src/NextionWaveform.h rename to components/NeoNextion/include/NextionWaveform.h index 52d1ef48..b34cbe64 100644 --- a/lib/NeoNextion/src/NextionWaveform.h +++ b/components/NeoNextion/include/NextionWaveform.h @@ -3,7 +3,7 @@ #ifndef __NEONEXTION_NEXTIONWAVEFORM #define __NEONEXTION_NEXTIONWAVEFORM -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" #include "INextionColourable.h" @@ -15,7 +15,7 @@ class NextionWaveform : public INextionTouchable, public INextionColourable { public: NextionWaveform(Nextion &nex, uint8_t page, uint8_t component, - const String &name); + const std::string &name); bool addValue(uint8_t channel, uint8_t value); diff --git a/lib/NeoNextion/keywords.txt b/components/NeoNextion/keywords.txt similarity index 100% rename from lib/NeoNextion/keywords.txt rename to components/NeoNextion/keywords.txt diff --git a/lib/NeoNextion/library.json b/components/NeoNextion/library.json similarity index 100% rename from lib/NeoNextion/library.json rename to components/NeoNextion/library.json diff --git a/lib/NeoNextion/library.properties b/components/NeoNextion/library.properties similarity index 100% rename from lib/NeoNextion/library.properties rename to components/NeoNextion/library.properties diff --git a/lib/NeoNextion/src/INextionColourable.cpp b/components/NeoNextion/src/INextionColourable.cpp similarity index 95% rename from lib/NeoNextion/src/INextionColourable.cpp rename to components/NeoNextion/src/INextionColourable.cpp index 350ae5bb..f1ef9f89 100644 --- a/lib/NeoNextion/src/INextionColourable.cpp +++ b/components/NeoNextion/src/INextionColourable.cpp @@ -6,7 +6,7 @@ * \copydoc INextionWidget::INextionWidget */ INextionColourable::INextionColourable(Nextion &nex, uint8_t page, - uint8_t component, const String &name) + uint8_t component, const std::string &name) : INextionWidget(nex, page, component, name) { } @@ -106,7 +106,7 @@ uint32_t INextionColourable::getEventBackgroundColour() * \param refresh If the widget should be refreshed * \return True if successful */ -bool INextionColourable::setColour(const String &type, uint32_t colour, bool refresh) +bool INextionColourable::setColour(const std::string &type, uint32_t colour, bool refresh) { return afterSet(setNumberProperty(type, colour), refresh); } @@ -117,7 +117,7 @@ bool INextionColourable::setColour(const String &type, uint32_t colour, bool ref * \return Colour (may also return 0 in case of error) * \see INextionColourable::setColour */ -uint32_t INextionColourable::getColour(const String &type) +uint32_t INextionColourable::getColour(const std::string &type) { return getNumberProperty(type); } diff --git a/lib/NeoNextion/src/INextionFontStyleable.cpp b/components/NeoNextion/src/INextionFontStyleable.cpp similarity index 97% rename from lib/NeoNextion/src/INextionFontStyleable.cpp rename to components/NeoNextion/src/INextionFontStyleable.cpp index a3d46c00..37278052 100644 --- a/lib/NeoNextion/src/INextionFontStyleable.cpp +++ b/components/NeoNextion/src/INextionFontStyleable.cpp @@ -7,7 +7,7 @@ */ INextionFontStyleable::INextionFontStyleable(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) { } diff --git a/lib/NeoNextion/src/INextionTouchable.cpp b/components/NeoNextion/src/INextionTouchable.cpp similarity index 96% rename from lib/NeoNextion/src/INextionTouchable.cpp rename to components/NeoNextion/src/INextionTouchable.cpp index d0a49c31..1b9e23ed 100644 --- a/lib/NeoNextion/src/INextionTouchable.cpp +++ b/components/NeoNextion/src/INextionTouchable.cpp @@ -6,7 +6,7 @@ * \copydoc INextionWidget::INextionWidget */ INextionTouchable::INextionTouchable(Nextion &nex, uint8_t page, - uint8_t component, const String &name) + uint8_t component, const std::string &name) : INextionWidget(nex, page, component, name) , m_callback(NULL) { diff --git a/lib/NeoNextion/src/INextionWidget.cpp b/components/NeoNextion/src/INextionWidget.cpp similarity index 80% rename from lib/NeoNextion/src/INextionWidget.cpp rename to components/NeoNextion/src/INextionWidget.cpp index 2b729fb0..de2958c0 100644 --- a/lib/NeoNextion/src/INextionWidget.cpp +++ b/components/NeoNextion/src/INextionWidget.cpp @@ -10,7 +10,7 @@ * \param name Name of this widget */ INextionWidget::INextionWidget(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : m_nextion(nex) , m_pageID(page) , m_componentID(component) @@ -42,7 +42,7 @@ uint8_t INextionWidget::getComponentID() * \param value Value * \return True if successful */ -bool INextionWidget::setNumberProperty(const String &propertyName, uint32_t value) +bool INextionWidget::setNumberProperty(const std::string &propertyName, uint32_t value) { return sendCommandWithWait("%s.%s=%d", m_name.c_str(), propertyName.c_str(), value); } @@ -52,7 +52,7 @@ bool INextionWidget::setNumberProperty(const String &propertyName, uint32_t valu * \param propertyName Name of the property * \return Value (may also return 0 in case of error) */ -uint32_t INextionWidget::getNumberProperty(const String &propertyName) +uint32_t INextionWidget::getNumberProperty(const std::string &propertyName) { sendCommand("get %s.%s", m_name.c_str(), propertyName.c_str()); uint32_t id; @@ -68,7 +68,7 @@ uint32_t INextionWidget::getNumberProperty(const String &propertyName) * \param value Value * \return True if successful */ -bool INextionWidget::setStringProperty(const String &propertyName, const String &value) +bool INextionWidget::setStringProperty(const std::string &propertyName, const std::string &value) { return sendCommandWithWait("%s.%s=\"%s\"", m_name.c_str(), propertyName.c_str(), value.c_str()); } @@ -80,13 +80,13 @@ bool INextionWidget::setStringProperty(const String &propertyName, const String * \param len Maximum length of value * \return Actual length of value */ -size_t INextionWidget::getStringProperty(const String &propertyName, String &buffer) +size_t INextionWidget::getStringProperty(const std::string &propertyName, std::string &buffer) { sendCommand("get %s.%s", m_name.c_str(), propertyName.c_str()); return m_nextion.receiveString(buffer); } -void INextionWidget::sendCommand(const String &format, ...) +void INextionWidget::sendCommand(const std::string &format, ...) { va_list args; va_start(args, format); @@ -94,7 +94,7 @@ void INextionWidget::sendCommand(const String &format, ...) va_end(args); } -bool INextionWidget::sendCommandWithWait(const String &format, ...) +bool INextionWidget::sendCommandWithWait(const std::string &format, ...) { va_list args; va_start(args, format); @@ -104,7 +104,7 @@ bool INextionWidget::sendCommandWithWait(const String &format, ...) return m_nextion.checkCommandComplete(); } -bool INextionWidget::setPropertyCommand(const String &command, uint32_t value) +bool INextionWidget::setPropertyCommand(const std::string &command, uint32_t value) { m_nextion.sendCommand("%s %s,%ld", command.c_str(), m_name.c_str(), value); return m_nextion.checkCommandComplete(); diff --git a/lib/NeoNextion/src/Nextion.cpp b/components/NeoNextion/src/Nextion.cpp similarity index 71% rename from lib/NeoNextion/src/Nextion.cpp rename to components/NeoNextion/src/Nextion.cpp index 862acc44..d9c9174d 100644 --- a/lib/NeoNextion/src/Nextion.cpp +++ b/components/NeoNextion/src/Nextion.cpp @@ -1,23 +1,33 @@ /*! \file */ -#include "Nextion.h" +#include "NeoNextion.h" #include "INextionTouchable.h" -// #define NEXTION_DEBUG 1 - /*! * \brief Creates a new device driver. * \param stream Stream (serial port) the device is connected to * \param flushSerialBeforeTx If the serial port should be flushed before * transmission */ -Nextion::Nextion(Stream &stream, bool flushSerialBeforeTx) - : m_serialPort(stream) - , m_timeout(500) +Nextion::Nextion(uint8_t uartNum, long baud, uint8_t rx_pin, uint8_t tx_pin, bool flushSerialBeforeTx) + : m_timeout(500) , m_flushSerialBeforeTx(flushSerialBeforeTx) , m_touchableList(NULL) { - m_serialPort.setTimeout(100); + m_serialPort = (uart_port_t)(UART_NUM_0 + uartNum); + uart_config_t uart_config = + { + .baud_rate = baud, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .rx_flow_ctrl_thresh = 0, + .use_ref_tick = false + }; + uart_param_config(m_serialPort, &uart_config); + uart_set_pin(m_serialPort, tx_pin, rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + uart_driver_install(m_serialPort, 2*1024, 0, 0, NULL, 0); } /*! @@ -42,41 +52,35 @@ bool Nextion::init() */ void Nextion::poll() { - while (m_serialPort.available() > 0) + size_t ready{0}; + uart_get_buffered_data_len(m_serialPort, &ready); + while (ready >= 7) { - char c = m_serialPort.read(); - - if (c == NEX_RET_EVENT_TOUCH_HEAD) + uint8_t buffer[7] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + if (uart_read_bytes(m_serialPort, buffer, 7, 0) == 7) { - delay(10); - - if (m_serialPort.available() >= 6) + if (buffer[0] == NEX_RET_EVENT_TOUCH_HEAD && + buffer[4] == 0xFF && + buffer[5] == 0xFF && + buffer[6] == 0xFF) { - static uint8_t buffer[8]; - buffer[0] = c; - - uint8_t i; - for (i = 1; i < 7; i++) - buffer[i] = m_serialPort.read(); - buffer[i] = 0x00; - - if (buffer[4] == 0xFF && buffer[5] == 0xFF && buffer[6] == 0xFF) + ITouchableListItem *item = m_touchableList; + while (item != NULL && + !item->item->processEvent(buffer[1], buffer[2], buffer[3])) { - ITouchableListItem *item = m_touchableList; - while (item != NULL) - { - item->item->processEvent(buffer[1], buffer[2], buffer[3]); - item = item->next; - } + item = item->next; } -#if NEXTION_DEBUG - else - { - printf("Nextion: %02x %02x %02x %02x %02x %02x\n", buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5]); - } -#endif + } + else + { + printf("Nextion: %02x %02x %02x %02x %02x %02x %02x\n", buffer[0] + , buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6]); } } + else + { + printf("Nextion: short read\n"); + } } } @@ -95,7 +99,7 @@ bool Nextion::refresh() * \param objectName Name of the object to refresh * \return True if successful */ -bool Nextion::refresh(const String &objectName) +bool Nextion::refresh(const std::string &objectName) { sendCommand("ref %s", objectName.c_str()); return checkCommandComplete(); @@ -164,12 +168,16 @@ uint8_t Nextion::getCurrentPage() uint8_t temp[5] = {0}; - if (sizeof(temp) != m_serialPort.readBytes((char *)temp, sizeof(temp))) + if (sizeof(temp) != uart_read_bytes(m_serialPort, temp, sizeof(temp), pdMS_TO_TICKS(10))) + { return 0; + } if (temp[0] == NEX_RET_CURRENT_PAGE_ID_HEAD && temp[2] == 0xFF && temp[3] == 0xFF && temp[4] == 0xFF) + { return temp[1]; + } return 0; } @@ -230,7 +238,7 @@ bool Nextion::drawPicture(uint16_t x, uint16_t y, uint16_t w, uint16_t h, * \return True if successful */ bool Nextion::drawStr(uint16_t x, uint16_t y, uint16_t w, uint16_t h, - uint8_t fontID, const String &str, uint32_t bgColour, + uint8_t fontID, const std::string &str, uint32_t bgColour, uint32_t fgColour, uint8_t bgType, NextionFontAlignment xCentre, NextionFontAlignment yCentre) @@ -323,22 +331,19 @@ void Nextion::registerTouchable(INextionTouchable *touchable) * \brief Sends a command to the device. * \param command Command to send */ -void Nextion::sendCommand(const String &command) +void Nextion::sendCommand(const std::string &command) { + char end_bytes[3] = {0xFF, 0xFF, 0xFF}; if (m_flushSerialBeforeTx) { - while(m_serialPort.available()) { - m_serialPort.read(); - } + uart_flush_input(m_serialPort); } + #if NEXTION_DEBUG printf("Nextion: TX: %s\n", command.c_str()); #endif - - m_serialPort.print(command); - m_serialPort.write(0xFF); - m_serialPort.write(0xFF); - m_serialPort.write(0xFF); + uart_write_bytes(m_serialPort, command.c_str(), command.length()); + uart_write_bytes(m_serialPort, end_bytes, 3); } void Nextion::sendCommand(const char *format, ...) { @@ -351,7 +356,7 @@ void Nextion::sendCommand(const char *format, ...) { void Nextion::sendCommand(const char *format, va_list args) { char buf[512] = {0}; vsnprintf(buf, sizeof(buf), format, args); - sendCommand(String(buf)); + sendCommand(buf); } /*! @@ -362,21 +367,19 @@ bool Nextion::checkCommandComplete() { bool ret = false; uint8_t temp[4] = {0}; - uint8_t bytesRead = m_serialPort.readBytes((char *)temp, sizeof(temp)); + size_t bytesRead = uart_read_bytes(m_serialPort, temp, sizeof(temp), pdMS_TO_TICKS(10)); if (bytesRead != sizeof(temp)) { #if NEXTION_DEBUG - printf("Nextion: short read: %d\n", bytesRead); -#endif + printf("Nextion: checkCommandComplete short read: %d\n", bytesRead); + printf("Nextion: %02x %02x %02x %02x, ret: %d\n", temp[0], temp[1], temp[2], temp[3], ret); +#endif // NEXTION_DEBUG } else if (temp[0] == NEX_RET_CMD_FINISHED && temp[1] == 0xFF && temp[2] == 0xFF && temp[3] == 0xFF) { ret = true; } -#if NEXTION_DEBUG - printf("Nextion: %02x %02x %02x %02x, ret: %d\n", temp[0], temp[1], temp[2], temp[3], ret); -#endif return ret; } @@ -392,9 +395,12 @@ bool Nextion::receiveNumber(uint32_t *number) if (!number) return false; + size_t bytesRead = uart_read_bytes(m_serialPort, temp, sizeof(temp), pdMS_TO_TICKS(10)); - if (sizeof(temp) != m_serialPort.readBytes((char *)temp, sizeof(temp))) + if (bytesRead != sizeof(temp)) + { return false; + } if (temp[0] == NEX_RET_NUMBER_HEAD && temp[5] == 0xFF && temp[6] == 0xFF && temp[7] == 0xFF) @@ -411,43 +417,58 @@ bool Nextion::receiveNumber(uint32_t *number) * \param buffer Pointer to buffer to store string in * \return Actual length of string received */ -size_t Nextion::receiveString(String &buffer, bool stringHeader) { +size_t Nextion::receiveString(std::string &buffer, bool stringHeader) { bool have_header_flag = !stringHeader; uint8_t flag_count = 0; - uint32_t start = millis(); + uint32_t start = esp_timer_get_time() / 1000ULL; buffer.reserve(128); - while (millis() - start <= m_timeout) + while (esp_timer_get_time() - start <= m_timeout) { - while (m_serialPort.available()) + size_t ready = 0; + uart_get_buffered_data_len(m_serialPort, &ready); + while (ready > 0) { - uint8_t c = m_serialPort.read(); - if (!have_header_flag && c == NEX_RET_STRING_HEAD) { + uint8_t ch; + uart_read_bytes(m_serialPort, &ch, 1, pdMS_TO_TICKS(10)); + if (!have_header_flag && ch == NEX_RET_STRING_HEAD) + { have_header_flag = true; - } else if (have_header_flag) { - if (c == NEX_RET_CMD_FINISHED) { + } + else if (have_header_flag) + { + if (ch == NEX_RET_CMD_FINISHED) + { // it appears that we received a "previous command completed successfully" // response. Discard the next three bytes which will be 0xFF so we can - // advance to the actual response we are wanting. - m_serialPort.read(); - m_serialPort.read(); - m_serialPort.read(); - } else if (c == 0xFF) { + // advance toh the actual response we are wanting. + uint8_t temp[3]; + uart_read_bytes(m_serialPort, temp, sizeof(temp), pdMS_TO_TICKS(10)); + } + else if (ch == 0xFF) + { flag_count++; - } else if (c == 0x05 && !stringHeader) { + } + else if (ch == 0x05 && !stringHeader) + { // this is a special case for the "connect" command flag_count = 3; - } else if (c < 0x20 || c > 0x7F) { + } + else if (ch < 0x20 || ch > 0x7F) + { // discard non-printable character - } else { - buffer.concat((char)c); + } + else + { + buffer += (char)ch; } } + uart_get_buffered_data_len(m_serialPort, &ready); } - if (flag_count >= 3) { + if (flag_count >= 3) + { break; } } - buffer.trim(); return buffer.length(); } diff --git a/lib/NeoNextion/src/NextionCrop.cpp b/components/NeoNextion/src/NextionCrop.cpp similarity index 91% rename from lib/NeoNextion/src/NextionCrop.cpp rename to components/NeoNextion/src/NextionCrop.cpp index e22c00a8..c175a84d 100644 --- a/lib/NeoNextion/src/NextionCrop.cpp +++ b/components/NeoNextion/src/NextionCrop.cpp @@ -7,7 +7,7 @@ * \copydoc INextionWidget::INextionWidget */ NextionCrop::NextionCrop(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) { diff --git a/lib/NeoNextion/src/NextionPage.cpp b/components/NeoNextion/src/NextionPage.cpp similarity index 92% rename from lib/NeoNextion/src/NextionPage.cpp rename to components/NeoNextion/src/NextionPage.cpp index 9291326b..3f3845ef 100644 --- a/lib/NeoNextion/src/NextionPage.cpp +++ b/components/NeoNextion/src/NextionPage.cpp @@ -6,7 +6,7 @@ * \copydoc INextionWidget::INextionWidget */ NextionPage::NextionPage(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) { } diff --git a/lib/NeoNextion/src/NextionPicture.cpp b/components/NeoNextion/src/NextionPicture.cpp similarity index 92% rename from lib/NeoNextion/src/NextionPicture.cpp rename to components/NeoNextion/src/NextionPicture.cpp index acec8086..c7bd863c 100644 --- a/lib/NeoNextion/src/NextionPicture.cpp +++ b/components/NeoNextion/src/NextionPicture.cpp @@ -7,7 +7,7 @@ * \copydoc INextionWidget::INextionWidget */ NextionPicture::NextionPicture(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) { diff --git a/lib/NeoNextion/src/NextionSlidingText.cpp b/components/NeoNextion/src/NextionSlidingText.cpp similarity index 98% rename from lib/NeoNextion/src/NextionSlidingText.cpp rename to components/NeoNextion/src/NextionSlidingText.cpp index 5baa53fc..63366f08 100644 --- a/lib/NeoNextion/src/NextionSlidingText.cpp +++ b/components/NeoNextion/src/NextionSlidingText.cpp @@ -6,7 +6,7 @@ * \copydoc INextionWidget::INextionWidget */ NextionSlidingText::NextionSlidingText(Nextion &nex, uint8_t page, - uint8_t component, const String &name) + uint8_t component, const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) , INextionColourable(nex, page, component, name) diff --git a/lib/NeoNextion/src/NextionTimer.cpp b/components/NeoNextion/src/NextionTimer.cpp similarity index 94% rename from lib/NeoNextion/src/NextionTimer.cpp rename to components/NeoNextion/src/NextionTimer.cpp index 8063389f..399a66f1 100644 --- a/lib/NeoNextion/src/NextionTimer.cpp +++ b/components/NeoNextion/src/NextionTimer.cpp @@ -6,7 +6,7 @@ * \copydoc INextionWidget::INextionWidget */ NextionTimer::NextionTimer(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) { diff --git a/lib/NeoNextion/src/NextionWaveform.cpp b/components/NeoNextion/src/NextionWaveform.cpp similarity index 85% rename from lib/NeoNextion/src/NextionWaveform.cpp rename to components/NeoNextion/src/NextionWaveform.cpp index b0e2fbd6..cfb9b37a 100644 --- a/lib/NeoNextion/src/NextionWaveform.cpp +++ b/components/NeoNextion/src/NextionWaveform.cpp @@ -7,7 +7,7 @@ * \copydoc INextionWidget::INextionWidget */ NextionWaveform::NextionWaveform(Nextion &nex, uint8_t page, uint8_t component, - const String &name) + const std::string &name) : INextionWidget(nex, page, component, name) , INextionTouchable(nex, page, component, name) , INextionColourable(nex, page, component, name) @@ -25,11 +25,7 @@ bool NextionWaveform::addValue(uint8_t channel, uint8_t value) if (channel > 3) return false; - size_t commandLen = 22; - char commandBuffer[commandLen]; - snprintf(commandBuffer, commandLen, "add %d,%d,%d", m_componentID, channel, - value); - sendCommand(commandBuffer, false); + sendCommand("add %d,%d,%d", m_componentID, channel, value); /* TODO: this check still fails but the command does actually work */ /* return m_nextion.checkCommandComplete(); */ @@ -46,9 +42,8 @@ bool NextionWaveform::addValue(uint8_t channel, uint8_t value) bool NextionWaveform::setChannelColour(uint8_t channel, uint32_t colour, bool refresh) { - char buffer[5]; - snprintf(buffer, 5, "pco%d", channel); - return setColour(buffer, colour, refresh); + std::string cmd = "pco" + std::to_string(channel); + return setColour(cmd, colour, refresh); } /*! @@ -58,9 +53,8 @@ bool NextionWaveform::setChannelColour(uint8_t channel, uint32_t colour, */ uint32_t NextionWaveform::getChannelColour(uint8_t channel) { - char buffer[5]; - snprintf(buffer, 5, "pco%d", channel); - return getColour(buffer); + std::string cmd = "pco" + std::to_string(channel); + return getColour(cmd); } /*! diff --git a/components/NeoPixelBus/CMakeLists.txt b/components/NeoPixelBus/CMakeLists.txt new file mode 100644 index 00000000..b86345c2 --- /dev/null +++ b/components/NeoPixelBus/CMakeLists.txt @@ -0,0 +1,9 @@ +set(COMPONENT_SRCDIRS + "src/internal" +) + +set(COMPONENT_ADD_INCLUDEDIRS + "src" +) + +register_component() \ No newline at end of file diff --git a/components/NeoPixelBus/COPYING b/components/NeoPixelBus/COPYING new file mode 100644 index 00000000..153d416d --- /dev/null +++ b/components/NeoPixelBus/COPYING @@ -0,0 +1,165 @@ + 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. \ No newline at end of file diff --git a/components/NeoPixelBus/ReadMe.md b/components/NeoPixelBus/ReadMe.md new file mode 100644 index 00000000..1ab34023 --- /dev/null +++ b/components/NeoPixelBus/ReadMe.md @@ -0,0 +1,43 @@ +# NeoPixelBus + +NOTE: This library has been modified for ESP32CommandStation as follows: +1. Remove Arduino references +2. Remove HTML color support (removes large chunk of static allocations) +3. Only ESP32 RMT output support +4. Remove NeoPixelAnimator (unused and has hard dependency on millis() for timing) + +[![Donate](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6AA97KE54UJR4) + +Arduino NeoPixel library + +A library to control one wire protocol RGB and RGBW leds like SK6812, WS2811, WS2812 and WS2813 that are commonly refered to as NeoPixels and two wire protocol RGB like APA102 commonly refered to as DotStars. +Supports most Arduino platforms. +This is the most functional library for the Esp8266 as it provides solutions for all Esp8266 module types even when WiFi is used. + + +Please read this best practices link before connecting your NeoPixels, it will save you a lot of time and effort. +[Adafruit NeoPixel Best Practices](https://learn.adafruit.com/adafruit-neopixel-uberguide/best-practices) + +For quick questions jump on Gitter and ask away. +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Makuna/NeoPixelBus?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +For bugs, make sure there isn't an active issue and then create one. + +## Why this library and not FastLED or some other library? +See [Why this Library in the Wiki](https://github.com/Makuna/NeoPixelBus/wiki/Library-Comparisons). + +## Documentation +[See Wiki](https://github.com/Makuna/NeoPixelBus/wiki) + +## Installing This Library (prefered, you just want to use it) +Open the Library Manager and search for "NeoPixelBus by Makuna" and install + +## Installing This Library From GitHub (advanced, you want to contribute) +Create a directory in your Arduino\Library folder named "NeoPixelBus" +Clone (Git) this project into that folder. +It should now show up in the import list when you restart Arduino IDE. + + + + + diff --git a/components/NeoPixelBus/keywords.txt b/components/NeoPixelBus/keywords.txt new file mode 100644 index 00000000..4734aca3 --- /dev/null +++ b/components/NeoPixelBus/keywords.txt @@ -0,0 +1,275 @@ +####################################### +# Syntax Coloring Map NeoPixelBus +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +NeoPixelBus KEYWORD1 +RgbwColor KEYWORD1 +RgbColor KEYWORD1 +HslColor KEYWORD1 +HsbColor KEYWORD1 +HtmlColor KEYWORD1 +NeoGrbFeature KEYWORD1 +NeoGrbwFeature KEYWORD1 +NeoRgbwFeature KEYWORD1 +NeoRgbFeature KEYWORD1 +NeoBrgFeature KEYWORD1 +NeoRbgFeature KEYWORD1 +DotStarBgrFeature KEYWORD1 +DotStarLbgrFeature KEYWORD1 +Neo800KbpsMethod KEYWORD1 +Neo400KbpsMethod KEYWORD1 +NeoWs2813Method KEYWORD1 +NeoWs2812xMethod KEYWORD1 +NeoWs2812Method KEYWORD1 +NeoSk6812Method KEYWORD1 +NeoLc8812Method KEYWORD1 +NeoApa106Method KEYWORD1 +NeoEsp8266DmaWs2812xMethod KEYWORD1 +NeoEsp8266DmaSk6812Method KEYWORD1 +NeoEsp8266DmaApa106Method KEYWORD1 +NeoEsp8266Dma800KbpsMethod KEYWORD1 +NeoEsp8266Dma400KbpsMethod KEYWORD1 +NeoEsp8266Uart0Ws2813Method KEYWORD1 +NeoEsp8266Uart0Ws2812xMethod KEYWORD1 +NeoEsp8266Uart0Ws2812Method KEYWORD1 +NeoEsp8266Uart0Sk6812Method KEYWORD1 +NeoEsp8266Uart0Lc8812Method KEYWORD1 +NeoEsp8266Uart0Apa106Method KEYWORD1 +NeoEsp8266Uart0800KbpsMethod KEYWORD1 +NeoEsp8266Uart0400KbpsMethod KEYWORD1 +NeoEsp8266AsyncUart0Ws2813Method KEYWORD1 +NeoEsp8266AsyncUart0Ws2812xMethod KEYWORD1 +NeoEsp8266AsyncUart0Ws2812Method KEYWORD1 +NeoEsp8266AsyncUart0Sk6812Method KEYWORD1 +NeoEsp8266AsyncUart0Lc8812Method KEYWORD1 +NeoEsp8266AsyncUart0Apa106Method KEYWORD1 +NeoEsp8266AsyncUart0800KbpsMethod KEYWORD1 +NeoEsp8266AsyncUart0400KbpsMethod KEYWORD1 +NeoEsp8266Uart1Ws2813Method KEYWORD1 +NeoEsp8266Uart1Ws2812xMethod KEYWORD1 +NeoEsp8266Uart1Ws2812Method KEYWORD1 +NeoEsp8266Uart1Sk6812Method KEYWORD1 +NeoEsp8266Uart1Lc8812Method KEYWORD1 +NeoEsp8266Uart1Apa106Method KEYWORD1 +NeoEsp8266Uart1800KbpsMethod KEYWORD1 +NeoEsp8266Uart1400KbpsMethod KEYWORD1 +NeoEsp8266AsyncUart1Ws2813Method KEYWORD1 +NeoEsp8266AsyncUart1Ws2812xMethod KEYWORD1 +NeoEsp8266AsyncUart1Ws2812Method KEYWORD1 +NeoEsp8266AsyncUart1Sk6812Method KEYWORD1 +NeoEsp8266AsyncUart1Lc8812Method KEYWORD1 +NeoEsp8266AsyncUart1Apa106Method KEYWORD1 +NeoEsp8266AsyncUart1800KbpsMethod KEYWORD1 +NeoEsp8266AsyncUart1400KbpsMethod KEYWORD1 +NeoEsp8266BitBangWs2813Method KEYWORD1 +NeoEsp8266BitBangWs2812xMethod KEYWORD1 +NeoEsp8266BitBangWs2812Method KEYWORD1 +NeoEsp8266BitBangSk6812Method KEYWORD1 +NeoEsp8266BitBangLc8812Method KEYWORD1 +NeoEsp8266BitBangApa106Method KEYWORD1 +NeoEsp8266BitBang800KbpsMethod KEYWORD1 +NeoEsp8266BitBang400KbpsMethod KEYWORD1 +NeoEsp32Rmt0Ws2812xMethod KEYWORD1 +NeoEsp32Rmt0Sk6812Method KEYWORD1 +NeoEsp32Rmt0Apa106Method KEYWORD1 +NeoEsp32Rmt0800KbpsMethod KEYWORD1 +NeoEsp32Rmt0400KbpsMethod KEYWORD1 +NeoEsp32Rmt1Ws2812xMethod KEYWORD1 +NeoEsp32Rmt1Sk6812Method KEYWORD1 +NeoEsp32Rmt1Apa106Method KEYWORD1 +NeoEsp32Rmt1800KbpsMethod KEYWORD1 +NeoEsp32Rmt1400KbpsMethod KEYWORD1 +NeoEsp32Rmt2Ws2812xMethod KEYWORD1 +NeoEsp32Rmt2Sk6812Method KEYWORD1 +NeoEsp32Rmt2Apa106Method KEYWORD1 +NeoEsp32Rmt2800KbpsMethod KEYWORD1 +NeoEsp32Rmt2400KbpsMethod KEYWORD1 +NeoEsp32Rmt3Ws2812xMethod KEYWORD1 +NeoEsp32Rmt3Sk6812Method KEYWORD1 +NeoEsp32Rmt3Apa106Method KEYWORD1 +NeoEsp32Rmt3800KbpsMethod KEYWORD1 +NeoEsp32Rmt3400KbpsMethod KEYWORD1 +NeoEsp32Rmt4Ws2812xMethod KEYWORD1 +NeoEsp32Rmt4Sk6812Method KEYWORD1 +NeoEsp32Rmt4Apa106Method KEYWORD1 +NeoEsp32Rmt4800KbpsMethod KEYWORD1 +NeoEsp32Rmt4400KbpsMethod KEYWORD1 +NeoEsp32Rmt5Ws2812xMethod KEYWORD1 +NeoEsp32Rmt5Sk6812Method KEYWORD1 +NeoEsp32Rmt5Apa106Method KEYWORD1 +NeoEsp32Rmt5800KbpsMethod KEYWORD1 +NeoEsp32Rmt5400KbpsMethod KEYWORD1 +NeoEsp32Rmt6Ws2812xMethod KEYWORD1 +NeoEsp32Rmt6Sk6812Method KEYWORD1 +NeoEsp32Rmt6Apa106Method KEYWORD1 +NeoEsp32Rmt6800KbpsMethod KEYWORD1 +NeoEsp32Rmt6400KbpsMethod KEYWORD1 +NeoEsp32Rmt7Ws2812xMethod KEYWORD1 +NeoEsp32Rmt7Sk6812Method KEYWORD1 +NeoEsp32Rmt7Apa106Method KEYWORD1 +NeoEsp32Rmt7800KbpsMethod KEYWORD1 +NeoEsp32Rmt7400KbpsMethod KEYWORD1 +NeoEsp32BitBangWs2813Method KEYWORD1 +NeoEsp32BitBangWs2812xMethod KEYWORD1 +NeoEsp32BitBangWs2812Method KEYWORD1 +NeoEsp32BitBangSk6812Method KEYWORD1 +NeoEsp32BitBangLc8812Method KEYWORD1 +NeoEsp32BitBangApa106Method KEYWORD1 +NeoEsp32BitBang800KbpsMethod KEYWORD1 +NeoEsp32BitBang400KbpsMethod KEYWORD1 +DotStarMethod KEYWORD1 +DotStarSpiMethod KEYWORD1 +NeoPixelAnimator KEYWORD1 +AnimUpdateCallback KEYWORD1 +AnimationParam KEYWORD1 +NeoEase KEYWORD1 +AnimEaseFunction KEYWORD1 +RowMajorLayout KEYWORD1 +RowMajor90Layout KEYWORD1 +RowMajor180Layout KEYWORD1 +RowMajor270Layout KEYWORD1 +RowMajorAlternatingLayout KEYWORD1 +RowMajorAlternating90Layout KEYWORD1 +RowMajorAlternating180Layout KEYWORD1 +RowMajorAlternating270Layout KEYWORD1 +ColumnMajorLayout KEYWORD1 +ColumnMajor90Layout KEYWORD1 +ColumnMajor180Layout KEYWORD1 +ColumnMajor270Layout KEYWORD1 +ColumnMajorAlternatingLayout KEYWORD1 +ColumnMajorAlternating90Layout KEYWORD1 +ColumnMajorAlternating180Layout KEYWORD1 +ColumnMajorAlternating270Layout KEYWORD1 +NeoTopology KEYWORD1 +NeoRingTopology KEYWORD1 +NeoTiles KEYWORD1 +NeoMosaic KEYWORD1 +NeoGammaEquationMethod KEYWORD1 +NeoGammaTableMethod KEYWORD1 +NeoGamma KEYWORD1 +NeoHueBlendShortestDistance KEYWORD1 +NeoHueBlendLongestDistance KEYWORD1 +NeoHueBlendClockwiseDirection KEYWORD1 +NeoHueBlendCounterClockwiseDirection KEYWORD1 +NeoBufferContext KEYWORD1 +LayoutMapCallback KEYWORD1 +NeoBufferMethod KEYWORD1 +NeoBufferProgmemMethod KEYWORD1 +NeoBuffer KEYWORD1 +NeoVerticalSpriteSheet KEYWORD1 +NeoBitmapFile KEYWORD1 +HtmlShortColorNames KEYWORD1 +HtmlColorNames KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +Begin KEYWORD2 +Show KEYWORD2 +CanShow KEYWORD2 +ClearTo KEYWORD2 +RotateLeft KEYWORD2 +ShiftLeft KEYWORD2 +RotateRight KEYWORD2 +ShiftRight KEYWORD2 +IsDirty KEYWORD2 +Dirty KEYWORD2 +ResetDirty KEYWORD2 +Pixels KEYWORD2 +PixelsSize KEYWORD2 +PixelCount KEYWORD2 +SetPixelColor KEYWORD2 +GetPixelColor KEYWORD2 +SwapPixelColor KEYWORD2 +CalculateBrightness KEYWORD2 +Darken KEYWORD2 +Lighten KEYWORD2 +LinearBlend KEYWORD2 +BilinearBlend KEYWORD2 +IsAnimating KEYWORD2 +NextAvailableAnimation KEYWORD2 +StartAnimation KEYWORD2 +StopAnimation KEYWORD2 +RestartAnimation KEYWORD2 +IsAnimationActive KEYWORD2 +AnimationDuration KEYWORD2 +ChangeAnimationDuration KEYWORD2 +UpdateAnimations KEYWORD2 +IsPaused KEYWORD2 +Pause KEYWORD2 +Resume KEYWORD2 +getTimeScale KEYWORD2 +setTimeScale KEYWORD2 +QuadraticIn KEYWORD2 +QuadraticOut KEYWORD2 +QuadraticInOut KEYWORD2 +QuadraticCenter KEYWORD2 +CubicIn KEYWORD2 +CubicOut KEYWORD2 +CubicInOut KEYWORD2 +CubicCenter KEYWORD2 +QuarticIn KEYWORD2 +QuarticOut KEYWORD2 +QuarticInOut KEYWORD2 +QuarticCenter KEYWORD2 +QuinticIn KEYWORD2 +QuinticOut KEYWORD2 +QuinticInOut KEYWORD2 +QuinticCenter KEYWORD2 +SinusoidalIn KEYWORD2 +SinusoidalOut KEYWORD2 +SinusoidalInOut KEYWORD2 +SinusoidalCenter KEYWORD2 +ExponentialIn KEYWORD2 +ExponentialOut KEYWORD2 +ExponentialInOut KEYWORD2 +ExponentialCenter KEYWORD2 +CircularIn KEYWORD2 +CircularOut KEYWORD2 +CircularInOut KEYWORD2 +CircularCenter KEYWORD2 +Gamma KEYWORD2 +Map KEYWORD2 +MapProbe KEYWORD2 +getWidth KEYWORD2 +getHeight KEYWORD2 +RingPixelShift KEYWORD2 +RingPixelRotate KEYWORD2 +getCountOfRings KEYWORD2 +getPixelCountAtRing KEYWORD2 +getPixelCount KEYWORD2 +TopologyHint KEYWORD2 +Correct KEYWORD2 +SpriteWidth KEYWORD2 +SpriteHeight KEYWORD2 +SpriteCount KEYWORD2 +Blt KEYWORD2 +Width KEYWORD2 +Height KEYWORD2 +Parse KEYWORD2 +ToString KEYWORD2 +ToNumericalString KEYWORD2 + + +####################################### +# Constants (LITERAL1) +####################################### + +NEO_MILLISECONDS LITERAL1 +NEO_CENTISECONDS LITERAL1 +NEO_DECISECONDS LITERAL1 +NEO_SECONDS LITERAL1 +NEO_DECASECONDS LITERAL1 +AnimationState_Started LITERAL1 +AnimationState_Progress LITERAL1 +AnimationState_Completed LITERAL1 +NeoTopologyHint_FirstOnPanel LITERAL1 +NeoTopologyHint_InPanel LITERAL1 +NeoTopologyHint_LastOnPanel LITERAL1 +NeoTopologyHint_OutOfBounds LITERAL1 +PixelIndex_OutOfBounds LITERAL1 \ No newline at end of file diff --git a/components/NeoPixelBus/library.json b/components/NeoPixelBus/library.json new file mode 100644 index 00000000..d0cef014 --- /dev/null +++ b/components/NeoPixelBus/library.json @@ -0,0 +1,14 @@ +{ + "name": "NeoPixelBus", + "keywords": "NeoPixel, WS2811, WS2812, WS2813, SK6812, DotStar, APA102, RGB, RGBW", + "description": "A library that makes controlling NeoPixels (WS2811, WS2812, WS2813 & SK6812) and DotStars (APA102) easy. Supports most Arduino platforms. Support for RGBW pixels. Includes seperate RgbColor, RgbwColor, HslColor, and HsbColor objects. Includes an animator class that helps create asyncronous animations. For Esp8266 it has three methods of sending NeoPixel data, DMA, UART, and Bit Bang. For Esp32 it has two base methods of sending NeoPixel data, i2s and RMT. For all platforms, there are two methods of sending DotStar data, hardware SPI and software SPI.", + "homepage": "https://github.com/Makuna/NeoPixelBus/wiki", + "repository": { + "type": "git", + "url": "https://github.com/Makuna/NeoPixelBus" + }, + "version": "2.5.0", + "frameworks": "*", + "platforms": "*" +} + diff --git a/components/NeoPixelBus/library.properties b/components/NeoPixelBus/library.properties new file mode 100644 index 00000000..5cac2ca1 --- /dev/null +++ b/components/NeoPixelBus/library.properties @@ -0,0 +1,9 @@ +name=NeoPixelBus by Makuna +version=2.5.0 +author=Michael C. Miller (makuna@live.com) +maintainer=Michael C. Miller (makuna@live.com) +sentence=A library that makes controlling NeoPixels (WS2811, WS2812, WS2813 & SK6812) and DotStars (APA102) easy. +paragraph=Supports most Arduino platforms, including Esp8266 and Esp32. Support for RGBW pixels. Includes seperate RgbColor, RgbwColor, HslColor, and HsbColor objects. Includes an animator class that helps create asyncronous animations. Supports Matrix layout of pixels. Includes Gamma corretion object. For Esp8266 it has three methods of sending NeoPixel data, DMA, UART, and Bit Bang. For Esp32 it has two base methods of sending NeoPixel data, i2s and RMT. For all platforms, there are two methods of sending DotStar data, hardware SPI and software SPI. +category=Display +url=https://github.com/Makuna/NeoPixelBus/wiki +architectures=* \ No newline at end of file diff --git a/components/NeoPixelBus/src/NeoPixelBrightnessBus.h b/components/NeoPixelBus/src/NeoPixelBrightnessBus.h new file mode 100644 index 00000000..2e24e603 --- /dev/null +++ b/components/NeoPixelBus/src/NeoPixelBrightnessBus.h @@ -0,0 +1,162 @@ +/*------------------------------------------------------------------------- +NeoPixelBus library wrapper template class that provides overall brightness control + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +#pragma once + +#include +#include "NeoPixelBus.h" + +template class NeoPixelBrightnessBus : + public NeoPixelBus +{ +private: + void ConvertColor(typename T_COLOR_FEATURE::ColorObject* color) + { + if (_brightness) + { + uint8_t* ptr = (uint8_t*) color; + uint8_t* ptrEnd = ptr + T_COLOR_FEATURE::PixelSize; + + while (ptr != ptrEnd) + { + uint16_t value = *ptr; + *ptr++ = (value * _brightness) >> 8; + } + } + } + + void RecoverColor(typename T_COLOR_FEATURE::ColorObject* color) const + { + if (_brightness) + { + uint8_t* ptr = (uint8_t*) color; + uint8_t* ptrEnd = ptr + T_COLOR_FEATURE::PixelSize; + + while (ptr != ptrEnd) + { + uint16_t value = *ptr; + *ptr++ = (value << 8) / _brightness; + } + } + } + +public: + NeoPixelBrightnessBus(uint16_t countPixels, uint8_t pin) : + NeoPixelBus(countPixels, pin), + _brightness(0) + { + } + + NeoPixelBrightnessBus(uint16_t countPixels, uint8_t pinClock, uint8_t pinData) : + NeoPixelBus(countPixels, pinClock, pinData), + _brightness(0) + { + } + + NeoPixelBrightnessBus(uint16_t countPixels) : + NeoPixelBus(countPixels), + _brightness(0) + { + } + + void SetBrightness(uint8_t brightness) + { + // Due to using fixed point math, we modifiy the brightness + // before storing making the math faster + uint8_t newBrightness = brightness + 1; + + // Only update if there is a change + if (newBrightness != _brightness) + { + // calculate a scale to modify from old brightness to new brightness + // + uint8_t oldBrightness = _brightness - 1; // unmodify brightness value + uint16_t scale; + + if (oldBrightness == 0) + { + scale = 0; // Avoid divide by 0 + } + else if (brightness == 255) + { + scale = 65535 / oldBrightness; + } + else + { + scale = (((uint16_t)newBrightness << 8) - 1) / oldBrightness; + } + + // re-scale existing pixels + // + uint8_t* ptr = this->Pixels(); + uint8_t* ptrEnd = ptr + this->PixelsSize(); + while (ptr != ptrEnd) + { + uint16_t value = *ptr; + *ptr++ = (value * scale) >> 8; + } + + _brightness = newBrightness; + this->Dirty(); + } + } + + uint8_t GetBrightness() const + { + return _brightness - 1; + } + + void SetPixelColor(uint16_t indexPixel, typename T_COLOR_FEATURE::ColorObject color) + { + ConvertColor(&color); + NeoPixelBus::SetPixelColor(indexPixel, color); + } + + typename T_COLOR_FEATURE::ColorObject GetPixelColor(uint16_t indexPixel) const + { + typename T_COLOR_FEATURE::ColorObject color = NeoPixelBus::GetPixelColor(indexPixel); + RecoverColor(&color); + return color; + } + + void ClearTo(typename T_COLOR_FEATURE::ColorObject color) + { + ConvertColor(&color); + NeoPixelBus::ClearTo(color); + }; + + void ClearTo(typename T_COLOR_FEATURE::ColorObject color, uint16_t first, uint16_t last) + { + ConvertColor(&color); + NeoPixelBus::ClearTo(color, first, last); + } + + +protected: + uint8_t _brightness; +}; + + diff --git a/components/NeoPixelBus/src/NeoPixelBus.h b/components/NeoPixelBus/src/NeoPixelBus.h new file mode 100644 index 00000000..f78081bd --- /dev/null +++ b/components/NeoPixelBus/src/NeoPixelBus.h @@ -0,0 +1,375 @@ +/*------------------------------------------------------------------------- +NeoPixel library + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +// '_state' flags for internal state +#define NEO_DIRTY 0x80 // a change was made to pixel data that requires a show + +#include "internal/NeoHueBlend.h" + +#include "internal/RgbColor.h" +#include "internal/HslColor.h" +#include "internal/HsbColor.h" +#include "internal/RgbwColor.h" + +#include "internal/NeoColorFeatures.h" + +#include "internal/Layouts.h" +#include "internal/NeoTopology.h" +#include "internal/NeoRingTopology.h" +#include "internal/NeoTiles.h" +#include "internal/NeoMosaic.h" + +#include "internal/NeoBufferContext.h" +#include "internal/NeoBuffer.h" +#include "internal/NeoSpriteSheet.h" +#include "internal/NeoDib.h" +#include "internal/NeoBitmapFile.h" + +#include "internal/NeoEase.h" +#include "internal/NeoGamma.h" + +#include "internal/NeoEsp32RmtMethod.h" + +template class NeoPixelBus +{ +public: + // Constructor: number of LEDs, pin number + // NOTE: Pin Number maybe ignored due to hardware limitations of the method. + + NeoPixelBus(uint16_t countPixels, uint8_t pin) : + _countPixels(countPixels), + _state(0), + _method(pin, countPixels, T_COLOR_FEATURE::PixelSize) + { + } + + NeoPixelBus(uint16_t countPixels, uint8_t pinClock, uint8_t pinData) : + _countPixels(countPixels), + _state(0), + _method(pinClock, pinData, countPixels, T_COLOR_FEATURE::PixelSize) + { + } + + NeoPixelBus(uint16_t countPixels) : + _countPixels(countPixels), + _state(0), + _method(countPixels, T_COLOR_FEATURE::PixelSize) + { + } + + ~NeoPixelBus() + { + } + + operator NeoBufferContext() + { + Dirty(); // we assume you are playing with bits + return NeoBufferContext(_method.getPixels(), _method.getPixelsSize()); + } + + void Begin() + { + _method.Initialize(); + Dirty(); + } + + // used by DotStartSpiMethod if pins can be configured + void Begin(int8_t sck, int8_t miso, int8_t mosi, int8_t ss) + { + _method.Initialize(sck, miso, mosi, ss); + Dirty(); + } + + void Show(bool maintainBufferConsistency = true) + { + if (!IsDirty()) + { + return; + } + + _method.Update(maintainBufferConsistency); + + ResetDirty(); + } + + inline bool CanShow() const + { + return _method.IsReadyToUpdate(); + }; + + bool IsDirty() const + { + return (_state & NEO_DIRTY); + }; + + void Dirty() + { + _state |= NEO_DIRTY; + }; + + void ResetDirty() + { + _state &= ~NEO_DIRTY; + }; + + uint8_t* Pixels() + { + return _method.getPixels(); + }; + + size_t PixelsSize() const + { + return _method.getPixelsSize(); + }; + + size_t PixelSize() const + { + return T_COLOR_FEATURE::PixelSize; + }; + + uint16_t PixelCount() const + { + return _countPixels; + }; + + void SetPixelColor(uint16_t indexPixel, typename T_COLOR_FEATURE::ColorObject color) + { + if (indexPixel < _countPixels) + { + T_COLOR_FEATURE::applyPixelColor(_method.getPixels(), indexPixel, color); + Dirty(); + } + }; + + typename T_COLOR_FEATURE::ColorObject GetPixelColor(uint16_t indexPixel) const + { + if (indexPixel < _countPixels) + { + return T_COLOR_FEATURE::retrievePixelColor(_method.getPixels(), indexPixel); + } + else + { + // Pixel # is out of bounds, this will get converted to a + // color object type initialized to 0 (black) + return 0; + } + }; + + void ClearTo(typename T_COLOR_FEATURE::ColorObject color) + { + uint8_t temp[T_COLOR_FEATURE::PixelSize]; + uint8_t* pixels = _method.getPixels(); + + T_COLOR_FEATURE::applyPixelColor(temp, 0, color); + + T_COLOR_FEATURE::replicatePixel(pixels, temp, _countPixels); + + Dirty(); + }; + + void ClearTo(typename T_COLOR_FEATURE::ColorObject color, uint16_t first, uint16_t last) + { + if (first < _countPixels && + last < _countPixels && + first <= last) + { + uint8_t temp[T_COLOR_FEATURE::PixelSize]; + uint8_t* pixels = _method.getPixels(); + uint8_t* pFront = T_COLOR_FEATURE::getPixelAddress(pixels, first); + + T_COLOR_FEATURE::applyPixelColor(temp, 0, color); + + T_COLOR_FEATURE::replicatePixel(pFront, temp, last - first + 1); + + Dirty(); + } + } + + void RotateLeft(uint16_t rotationCount) + { + if ((_countPixels - 1) >= rotationCount) + { + _rotateLeft(rotationCount, 0, _countPixels - 1); + } + } + + void RotateLeft(uint16_t rotationCount, uint16_t first, uint16_t last) + { + if (first < _countPixels && + last < _countPixels && + first < last && + (last - first) >= rotationCount) + { + _rotateLeft(rotationCount, first, last); + } + } + + void ShiftLeft(uint16_t shiftCount) + { + if ((_countPixels - 1) >= shiftCount) + { + _shiftLeft(shiftCount, 0, _countPixels - 1); + Dirty(); + } + } + + void ShiftLeft(uint16_t shiftCount, uint16_t first, uint16_t last) + { + if (first < _countPixels && + last < _countPixels && + first < last && + (last - first) >= shiftCount) + { + _shiftLeft(shiftCount, first, last); + Dirty(); + } + } + + void RotateRight(uint16_t rotationCount) + { + if ((_countPixels - 1) >= rotationCount) + { + _rotateRight(rotationCount, 0, _countPixels - 1); + } + } + + void RotateRight(uint16_t rotationCount, uint16_t first, uint16_t last) + { + if (first < _countPixels && + last < _countPixels && + first < last && + (last - first) >= rotationCount) + { + _rotateRight(rotationCount, first, last); + } + } + + void ShiftRight(uint16_t shiftCount) + { + if ((_countPixels - 1) >= shiftCount) + { + _shiftRight(shiftCount, 0, _countPixels - 1); + Dirty(); + } + } + + void ShiftRight(uint16_t shiftCount, uint16_t first, uint16_t last) + { + if (first < _countPixels && + last < _countPixels && + first < last && + (last - first) >= shiftCount) + { + _shiftRight(shiftCount, first, last); + Dirty(); + } + } + + void SwapPixelColor(uint16_t indexPixelOne, uint16_t indexPixelTwo) + { + auto colorOne = GetPixelColor(indexPixelOne); + auto colorTwo = GetPixelColor(indexPixelTwo); + + SetPixelColor(indexPixelOne, colorTwo); + SetPixelColor(indexPixelTwo, colorOne); + }; + +protected: + const uint16_t _countPixels; // Number of RGB LEDs in strip + + uint8_t _state; // internal state + T_METHOD _method; + + void _rotateLeft(uint16_t rotationCount, uint16_t first, uint16_t last) + { + // store in temp + uint8_t temp[rotationCount * T_COLOR_FEATURE::PixelSize]; + uint8_t* pixels = _method.getPixels(); + + uint8_t* pFront = T_COLOR_FEATURE::getPixelAddress(pixels, first); + + T_COLOR_FEATURE::movePixelsInc(temp, pFront, rotationCount); + + // shift data + _shiftLeft(rotationCount, first, last); + + // move temp back + pFront = T_COLOR_FEATURE::getPixelAddress(pixels, last - (rotationCount - 1)); + T_COLOR_FEATURE::movePixelsInc(pFront, temp, rotationCount); + + Dirty(); + } + + void _shiftLeft(uint16_t shiftCount, uint16_t first, uint16_t last) + { + uint16_t front = first + shiftCount; + uint16_t count = last - front + 1; + + uint8_t* pixels = _method.getPixels(); + uint8_t* pFirst = T_COLOR_FEATURE::getPixelAddress(pixels, first); + uint8_t* pFront = T_COLOR_FEATURE::getPixelAddress(pixels, front); + + T_COLOR_FEATURE::movePixelsInc(pFirst, pFront, count); + + // intentional no dirty + } + + void _rotateRight(uint16_t rotationCount, uint16_t first, uint16_t last) + { + // store in temp + uint8_t temp[rotationCount * T_COLOR_FEATURE::PixelSize]; + uint8_t* pixels = _method.getPixels(); + + uint8_t* pFront = T_COLOR_FEATURE::getPixelAddress(pixels, last - (rotationCount - 1)); + + T_COLOR_FEATURE::movePixelsDec(temp, pFront, rotationCount); + + // shift data + _shiftRight(rotationCount, first, last); + + // move temp back + pFront = T_COLOR_FEATURE::getPixelAddress(pixels, first); + T_COLOR_FEATURE::movePixelsDec(pFront, temp, rotationCount); + + Dirty(); + } + + void _shiftRight(uint16_t shiftCount, uint16_t first, uint16_t last) + { + uint16_t front = first + shiftCount; + uint16_t count = last - front + 1; + + uint8_t* pixels = _method.getPixels(); + uint8_t* pFirst = T_COLOR_FEATURE::getPixelAddress(pixels, first); + uint8_t* pFront = T_COLOR_FEATURE::getPixelAddress(pixels, front); + + T_COLOR_FEATURE::movePixelsDec(pFront, pFirst, count); + // intentional no dirty + } +}; + + diff --git a/components/NeoPixelBus/src/internal/HsbColor.cpp b/components/NeoPixelBus/src/internal/HsbColor.cpp new file mode 100644 index 00000000..2a4e6e35 --- /dev/null +++ b/components/NeoPixelBus/src/internal/HsbColor.cpp @@ -0,0 +1,67 @@ +/*------------------------------------------------------------------------- +HsbColor provides a color object that can be directly consumed by NeoPixelBus + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +#include "RgbColor.h" +#include "HsbColor.h" + +HsbColor::HsbColor(const RgbColor& color) +{ + // convert colors to float between (0.0 - 1.0) + float r = color.R / 255.0f; + float g = color.G / 255.0f; + float b = color.B / 255.0f; + + float max = (r > g && r > b) ? r : (g > b) ? g : b; + float min = (r < g && r < b) ? r : (g < b) ? g : b; + + float d = max - min; + + float h = 0.0; + float v = max; + float s = (v == 0.0f) ? 0 : (d / v); + + if (d != 0.0f) + { + if (r == max) + { + h = (g - b) / d + (g < b ? 6.0f : 0.0f); + } + else if (g == max) + { + h = (b - r) / d + 2.0f; + } + else + { + h = (r - g) / d + 4.0f; + } + h /= 6.0f; + } + + + H = h; + S = s; + B = v; +} diff --git a/components/NeoPixelBus/src/internal/HsbColor.h b/components/NeoPixelBus/src/internal/HsbColor.h new file mode 100644 index 00000000..40e73651 --- /dev/null +++ b/components/NeoPixelBus/src/internal/HsbColor.h @@ -0,0 +1,113 @@ + +/*------------------------------------------------------------------------- +HsbColor provides a color object that can be directly consumed by NeoPixelBus + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +#include "RgbColor.h" + +// ------------------------------------------------------------------------ +// HsbColor represents a color object that is represented by Hue, Saturation, Brightness +// component values. It contains helpful color routines to manipulate the +// color. +// ------------------------------------------------------------------------ +struct HsbColor +{ + // ------------------------------------------------------------------------ + // Construct a HsbColor using H, S, B values (0.0 - 1.0) + // ------------------------------------------------------------------------ + HsbColor(float h, float s, float b) : + H(h), S(s), B(b) + { + }; + + // ------------------------------------------------------------------------ + // Construct a HsbColor using RgbColor + // ------------------------------------------------------------------------ + HsbColor(const RgbColor& color); + + // ------------------------------------------------------------------------ + // Construct a HsbColor that will have its values set in latter operations + // CAUTION: The H,S,B members are not initialized and may not be consistent + // ------------------------------------------------------------------------ + HsbColor() + { + }; + + // ------------------------------------------------------------------------ + // LinearBlend between two colors by the amount defined by progress variable + // left - the color to start the blend at + // right - the color to end the blend at + // progress - (0.0 - 1.0) value where 0.0 will return left and 1.0 will return right + // and a value between will blend the color weighted linearly between them + // ------------------------------------------------------------------------ + template static HsbColor LinearBlend(const HsbColor& left, + const HsbColor& right, + float progress) + { + return HsbColor(T_NEOHUEBLEND::HueBlend(left.H, right.H, progress), + left.S + ((right.S - left.S) * progress), + left.B + ((right.B - left.B) * progress)); + } + + // ------------------------------------------------------------------------ + // BilinearBlend between four colors by the amount defined by 2d variable + // c00 - upper left quadrant color + // c01 - upper right quadrant color + // c10 - lower left quadrant color + // c11 - lower right quadrant color + // x - unit value (0.0 - 1.0) that defines the blend progress in horizontal space + // y - unit value (0.0 - 1.0) that defines the blend progress in vertical space + // ------------------------------------------------------------------------ + template static HsbColor BilinearBlend(const HsbColor& c00, + const HsbColor& c01, + const HsbColor& c10, + const HsbColor& c11, + float x, + float y) + { + float v00 = (1.0f - x) * (1.0f - y); + float v10 = x * (1.0f - y); + float v01 = (1.0f - x) * y; + float v11 = x * y; + + return HsbColor( + T_NEOHUEBLEND::HueBlend( + T_NEOHUEBLEND::HueBlend(c00.H, c10.H, x), + T_NEOHUEBLEND::HueBlend(c01.H, c11.H, x), + y), + c00.S * v00 + c10.S * v10 + c01.S * v01 + c11.S * v11, + c00.B * v00 + c10.B * v10 + c01.B * v01 + c11.B * v11); + }; + + // ------------------------------------------------------------------------ + // Hue, Saturation, Brightness color members + // ------------------------------------------------------------------------ + + float H; + float S; + float B; +}; + diff --git a/components/NeoPixelBus/src/internal/HslColor.cpp b/components/NeoPixelBus/src/internal/HslColor.cpp new file mode 100644 index 00000000..eb0a66d8 --- /dev/null +++ b/components/NeoPixelBus/src/internal/HslColor.cpp @@ -0,0 +1,71 @@ +/*------------------------------------------------------------------------- +HslColor provides a color object that can be directly consumed by NeoPixelBus + + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +#include "HslColor.h" + + +HslColor::HslColor(const RgbColor& color) +{ + // convert colors to float between (0.0 - 1.0) + float r = color.R / 255.0f; + float g = color.G / 255.0f; + float b = color.B / 255.0f; + + float max = (r > g && r > b) ? r : (g > b) ? g : b; + float min = (r < g && r < b) ? r : (g < b) ? g : b; + + float h, s, l; + l = (max + min) / 2.0f; + + if (max == min) + { + h = s = 0.0f; + } + else + { + float d = max - min; + s = (l > 0.5f) ? d / (2.0f - (max + min)) : d / (max + min); + + if (r > g && r > b) + { + h = (g - b) / d + (g < b ? 6.0f : 0.0f); + } + else if (g > b) + { + h = (b - r) / d + 2.0f; + } + else + { + h = (r - g) / d + 4.0f; + } + h /= 6.0f; + } + + H = h; + S = s; + L = l; +} diff --git a/components/NeoPixelBus/src/internal/HslColor.h b/components/NeoPixelBus/src/internal/HslColor.h new file mode 100644 index 00000000..a03ab000 --- /dev/null +++ b/components/NeoPixelBus/src/internal/HslColor.h @@ -0,0 +1,114 @@ +/*------------------------------------------------------------------------- +HslColor provides a color object that can be directly consumed by NeoPixelBus + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +#include +#include "RgbColor.h" + +// ------------------------------------------------------------------------ +// HslColor represents a color object that is represented by Hue, Saturation, Lightness +// component values. It contains helpful color routines to manipulate the +// color. +// ------------------------------------------------------------------------ +struct HslColor +{ + + // ------------------------------------------------------------------------ + // Construct a HslColor using H, S, L values (0.0 - 1.0) + // L should be limited to between (0.0 - 0.5) + // ------------------------------------------------------------------------ + HslColor(float h, float s, float l) : + H(h), S(s), L(l) + { + }; + + // ------------------------------------------------------------------------ + // Construct a HslColor using RgbColor + // ------------------------------------------------------------------------ + HslColor(const RgbColor& color); + + // ------------------------------------------------------------------------ + // Construct a HslColor that will have its values set in latter operations + // CAUTION: The H,S,L members are not initialized and may not be consistent + // ------------------------------------------------------------------------ + HslColor() + { + }; + + // ------------------------------------------------------------------------ + // LinearBlend between two colors by the amount defined by progress variable + // left - the color to start the blend at + // right - the color to end the blend at + // progress - (0.0 - 1.0) value where 0.0 will return left and 1.0 will return right + // and a value between will blend the color weighted linearly between them + // ------------------------------------------------------------------------ + template static HslColor LinearBlend(const HslColor& left, + const HslColor& right, + float progress) + { + return HslColor(T_NEOHUEBLEND::HueBlend(left.H, right.H, progress), + left.S + ((right.S - left.S) * progress), + left.L + ((right.L - left.L) * progress)); + }; + + // ------------------------------------------------------------------------ + // BilinearBlend between four colors by the amount defined by 2d variable + // c00 - upper left quadrant color + // c01 - upper right quadrant color + // c10 - lower left quadrant color + // c11 - lower right quadrant color + // x - unit value (0.0 - 1.0) that defines the blend progress in horizontal space + // y - unit value (0.0 - 1.0) that defines the blend progress in vertical space + // ------------------------------------------------------------------------ + template static HslColor BilinearBlend(const HslColor& c00, + const HslColor& c01, + const HslColor& c10, + const HslColor& c11, + float x, + float y) + { + float v00 = (1.0f - x) * (1.0f - y); + float v10 = x * (1.0f - y); + float v01 = (1.0f - x) * y; + float v11 = x * y; + + return HslColor( + T_NEOHUEBLEND::HueBlend( + T_NEOHUEBLEND::HueBlend(c00.H, c10.H, x), + T_NEOHUEBLEND::HueBlend(c01.H, c11.H, x), + y), + c00.S * v00 + c10.S * v10 + c01.S * v01 + c11.S * v11, + c00.L * v00 + c10.L * v10 + c01.L * v01 + c11.L * v11); + }; + + // ------------------------------------------------------------------------ + // Hue, Saturation, Lightness color members + // ------------------------------------------------------------------------ + float H; + float S; + float L; +}; + diff --git a/components/NeoPixelBus/src/internal/Layouts.h b/components/NeoPixelBus/src/internal/Layouts.h new file mode 100644 index 00000000..bd147bc3 --- /dev/null +++ b/components/NeoPixelBus/src/internal/Layouts.h @@ -0,0 +1,428 @@ +#pragma once +/*------------------------------------------------------------------------- +Layout provides a collection of class objects that are used with NeoTopology +object. +They define the specific layout of pixels and do the math to change the 2d +cordinate space to 1d cordinate space + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +#include + +const uint16_t PixelIndex_OutOfBounds = 0xffff; + +//----------------------------------------------------------------------------- +// RowMajor +//----------------------------------------------------------------------------- + +class RowMajorLayout; +class RowMajor90Layout; +class RowMajor180Layout; +class RowMajor270Layout; + +class RowMajorTilePreference +{ +public: + typedef RowMajorLayout EvenRowEvenColumnLayout; + typedef RowMajor270Layout EvenRowOddColumnLayout; + typedef RowMajor90Layout OddRowEvenColumnLayout; + typedef RowMajor180Layout OddRowOddColumnLayout; +}; + +// layout example of 4x4 +// 00 01 02 03 +// 04 05 06 07 +// 08 09 10 11 +// 12 13 14 15 +// +class RowMajorLayout : public RowMajorTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t /* height */, uint16_t x, uint16_t y) + { + return x + y * width; + } +}; + +// layout example of 4x4 +// 12 08 04 00 +// 13 09 05 01 +// 14 10 06 02 +// 15 11 07 03 +// +class RowMajor90Layout : public RowMajorTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t height, uint16_t x, uint16_t y) + { + return (width - 1 - x) * height + y; + } +}; + +// layout example of 4x4 +// 15 14 13 12 +// 11 10 09 08 +// 07 06 05 04 +// 03 02 01 00 +// +class RowMajor180Layout : public RowMajorTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t height, uint16_t x, uint16_t y) + { + return (width - 1 - x) + (height - 1 - y) * width; + } +}; + +// layout example of 4x4 +// 03 07 11 15 +// 02 06 10 14 +// 01 05 09 13 +// 00 04 08 12 +// +class RowMajor270Layout : public RowMajorTilePreference +{ +public: + static uint16_t Map(uint16_t /* width */, uint16_t height, uint16_t x, uint16_t y) + { + return x * height + (height - 1 - y); + } +}; + + +//----------------------------------------------------------------------------- +// ColumnMajor +//----------------------------------------------------------------------------- + +class ColumnMajorLayout; +class ColumnMajor90Layout; +class ColumnMajor180Layout; +class ColumnMajor270Layout; + +class ColumnMajorTilePreference +{ +public: + typedef ColumnMajorLayout EvenRowEvenColumnLayout; + typedef ColumnMajor270Layout EvenRowOddColumnLayout; + typedef ColumnMajor90Layout OddRowEvenColumnLayout; + typedef ColumnMajor180Layout OddRowOddColumnLayout; +}; + +// layout example of 4x4 +// 00 04 08 12 +// 01 05 09 13 +// 02 06 10 14 +// 03 07 11 15 +// +class ColumnMajorLayout : public ColumnMajorTilePreference +{ +public: + static uint16_t Map(uint16_t /* width */, uint16_t height, uint16_t x, uint16_t y) + { + return x * height + y; + } +}; + +// layout example of 4x4 +// 03 02 01 00 +// 07 06 05 04 +// 11 10 09 08 +// 15 14 13 12 +// +class ColumnMajor90Layout : public ColumnMajorTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t /* height */, uint16_t x, uint16_t y) + { + return (width - 1 - x) + y * width; + } +}; + +// layout example of 4x4 +// 15 11 07 03 +// 14 10 06 02 +// 13 09 05 01 +// 12 08 04 00 +// +class ColumnMajor180Layout : public ColumnMajorTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t height, uint16_t x, uint16_t y) + { + return (width - 1 - x) * height + (height - 1 - y); + } +}; + +// layout example of 4x4 +// 12 13 14 15 +// 08 09 10 11 +// 04 05 06 07 +// 00 01 02 03 +// +class ColumnMajor270Layout : public ColumnMajorTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t height, uint16_t x, uint16_t y) + { + return x + (height - 1 - y) * width; + } +}; + + +//----------------------------------------------------------------------------- +// RowMajorAlternating +//----------------------------------------------------------------------------- + +class RowMajorAlternating270Layout; +class RowMajorAlternating90Layout; + +class RowMajorAlternatingTilePreference +{ +public: + typedef RowMajorAlternating270Layout EvenRowEvenColumnLayout; + typedef RowMajorAlternating270Layout EvenRowOddColumnLayout; + typedef RowMajorAlternating90Layout OddRowEvenColumnLayout; + typedef RowMajorAlternating90Layout OddRowOddColumnLayout; +}; + +// layout example of 4x4 +// 00 01 02 03 +// 07 06 05 04 +// 08 09 10 11 +// 15 14 13 12 +// +class RowMajorAlternatingLayout : public RowMajorAlternatingTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t /* height */, uint16_t x, uint16_t y) + { + uint16_t index = y * width; + + if (y & 0x0001) + { + index += ((width - 1) - x); + } + else + { + index += x; + } + return index; + } +}; + +// layout example of 4x4 +// 15 08 07 00 +// 14 09 06 01 +// 13 10 05 02 +// 12 11 04 03 +// +class RowMajorAlternating90Layout : public RowMajorAlternatingTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t height, uint16_t x, uint16_t y) + { + uint16_t mx = ((width - 1) - x); + uint16_t index = mx * height; + + if (mx & 0x0001) + { + index += ((height - 1) - y); + } + else + { + index += y; + } + return index; + } +}; + +// layout example of 4x4 +// 12 13 14 15 +// 11 10 09 08 +// 04 05 06 07 +// 03 02 01 00 +// +class RowMajorAlternating180Layout : public RowMajorAlternatingTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t height, uint16_t x, uint16_t y) + { + uint16_t my = ((height - 1) - y); + uint16_t index = my * width; + + if (my & 0x0001) + { + index += x; + } + else + { + index += ((width - 1) - x); + } + return index; + } +}; + +// layout example of 4x4 +// 03 04 11 12 +// 02 05 10 13 +// 01 06 09 14 +// 00 07 08 15 +// +class RowMajorAlternating270Layout : public RowMajorAlternatingTilePreference +{ +public: + static uint16_t Map(uint16_t /* width */, uint16_t height, uint16_t x, uint16_t y) + { + uint16_t index = x * height; + + if (x & 0x0001) + { + index += y; + } + else + { + index += ((height - 1) - y); + } + return index; + } +}; + + +//----------------------------------------------------------------------------- +// ColumnMajorAlternating +//----------------------------------------------------------------------------- + +class ColumnMajorAlternatingLayout; +class ColumnMajorAlternating180Layout; + +class ColumnMajorAlternatingTilePreference +{ +public: + typedef ColumnMajorAlternatingLayout EvenRowEvenColumnLayout; + typedef ColumnMajorAlternatingLayout EvenRowOddColumnLayout; + typedef ColumnMajorAlternating180Layout OddRowEvenColumnLayout; + typedef ColumnMajorAlternating180Layout OddRowOddColumnLayout; +}; + +// layout example of 4x4 +// 00 07 08 15 +// 01 06 09 14 +// 02 05 10 13 +// 03 04 11 12 +// +class ColumnMajorAlternatingLayout : public ColumnMajorAlternatingTilePreference +{ +public: + static uint16_t Map(uint16_t /* width */, uint16_t height, uint16_t x, uint16_t y) + { + uint16_t index = x * height; + + if (x & 0x0001) + { + index += ((height - 1) - y); + } + else + { + index += y; + } + return index; + } +}; + +// layout example of 4x4 +// 03 02 01 00 +// 04 05 06 07 +// 11 10 09 08 +// 12 13 14 15 +// +class ColumnMajorAlternating90Layout : public ColumnMajorAlternatingTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t /* height */, uint16_t x, uint16_t y) + { + uint16_t index = y * width; + + if (y & 0x0001) + { + index += x; + } + else + { + index += ((width - 1) - x); + } + return index; + } +}; + +// layout example of 4x4 +// 12 11 04 03 +// 13 10 05 02 +// 14 09 06 01 +// 15 08 07 00 +// +class ColumnMajorAlternating180Layout : public ColumnMajorAlternatingTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t height, uint16_t x, uint16_t y) + { + uint16_t mx = ((width - 1) - x); + uint16_t index = mx * height; + + if (mx & 0x0001) + { + index += y; + } + else + { + index += ((height - 1) - y); + } + return index; + } +}; + +// layout example of 4x4 +// 15 14 13 12 +// 08 09 10 11 +// 07 06 05 04 +// 00 01 02 03 +// +class ColumnMajorAlternating270Layout : public ColumnMajorAlternatingTilePreference +{ +public: + static uint16_t Map(uint16_t width, uint16_t height, uint16_t x, uint16_t y) + { + uint16_t my = ((height - 1) - y); + uint16_t index = my * width; + + if (my & 0x0001) + { + index += ((width - 1) - x); + } + else + { + index += x; + } + return index; + } +}; diff --git a/components/NeoPixelBus/src/internal/NeoBitmapFile.h b/components/NeoPixelBus/src/internal/NeoBitmapFile.h new file mode 100644 index 00000000..853a8f50 --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoBitmapFile.h @@ -0,0 +1,390 @@ +/*------------------------------------------------------------------------- +NeoPixel library + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +#include + +const uint16_t c_BitmapFileId = 0x4d42; // "BM" + +#pragma pack(push, 2) +struct BitmapFileHeader +{ + uint16_t FileId; // only c_BitmapFileId is supported + uint32_t FileSize; + uint16_t Reserved0; + uint16_t Reserved1; + uint32_t PixelAddress; +}; + +struct BitmapInfoHeader +{ + uint32_t Size; + int32_t Width; + int32_t Height; + uint16_t Planes; // only support 1 + uint16_t BitsPerPixel; // only support 24 and 32 + uint32_t Compression; // only support BI_Rgb + uint32_t RawDateSize; // can be zero + int32_t XPpm; + int32_t YPpm; + uint32_t PaletteLength; + uint32_t ImportantColorCount; +}; +#pragma pack(pop) + +enum BmpCompression +{ + BI_Rgb, + BI_Rle8, + BI_Rle4, + BI_Bitfields, + BI_Jpeg, + BI_Png, + BI_AlphaBitfields, + BI_Cmyk = 11, + BI_CmykRle8, + BI_CmykRle4 +}; + +template class NeoBitmapFile +{ +public: + NeoBitmapFile() : + _fileAddressPixels(0), + _width(0), + _height(0), + _sizeRow(0), + _bytesPerPixel(0), + _bottomToTop(true) + { + } + + ~NeoBitmapFile() + { + _file.close(); + } + + bool Begin(T_FILE_METHOD file) + { + if (_file) + { + _file.close(); + } + + if (!file || !file.seek(0)) + { + goto error; + } + + _file = file; + + BitmapFileHeader bmpHeader; + BitmapInfoHeader bmpInfoHeader; + size_t result; + + result = _file.read((uint8_t*)(&bmpHeader), sizeof(bmpHeader)); + + if (result != sizeof(bmpHeader) || + bmpHeader.FileId != c_BitmapFileId || + bmpHeader.FileSize != _file.size()) + { + goto error; + } + + result = _file.read((uint8_t*)(&bmpInfoHeader), sizeof(bmpInfoHeader)); + + if (result != sizeof(bmpInfoHeader) || + result != bmpInfoHeader.Size || + 1 != bmpInfoHeader.Planes || + BI_Rgb != bmpInfoHeader.Compression) + { + goto error; + } + + if (!(24 == bmpInfoHeader.BitsPerPixel || + 32 == bmpInfoHeader.BitsPerPixel)) + { + goto error; + } + + // save the interesting information + _width = abs(bmpInfoHeader.Width); + _height = abs(bmpInfoHeader.Height); + _fileAddressPixels = bmpHeader.PixelAddress; + // negative height means rows are top to bottom + _bottomToTop = (bmpInfoHeader.Height > 0); + // rows are 32 bit aligned so they may have padding on each row + _sizeRow = (bmpInfoHeader.BitsPerPixel * _width + 31) / 32 * 4; + _bytesPerPixel = bmpInfoHeader.BitsPerPixel / 8; + + return true; + + error: + _fileAddressPixels = 0; + _width = 0; + _height = 0; + _sizeRow = 0; + _bytesPerPixel = 0; + + _file.close(); + return false; + }; + + size_t PixelSize() const + { + return T_COLOR_FEATURE::PixelSize; + }; + + uint16_t PixelCount() const + { + return _width * _height; + }; + + uint16_t Width() const + { + return _width; + }; + + uint16_t Height() const + { + return _height; + }; + + typename T_COLOR_FEATURE::ColorObject GetPixelColor(int16_t x, int16_t y) + { + if (x < 0 || x >= _width || y < 0 || y >= _height) + { + // Pixel # is out of bounds, this will get converted to a + // color object type initialized to 0 (black) + return 0; + } + + typename T_COLOR_FEATURE::ColorObject color; + if (!seek(x, y) || !readPixel(&color)) + { + return 0; + } + + return color; + }; + + + template void Render(NeoBufferContext destBuffer, + T_SHADER& shader, + uint16_t indexPixel, + int16_t xSrc, + int16_t ySrc, + int16_t wSrc) + { + const uint16_t destPixelCount = destBuffer.PixelCount(); + typename T_COLOR_FEATURE::ColorObject color(0); + xSrc = constrainX(xSrc); + ySrc = constrainY(ySrc); + + if (seek(xSrc, ySrc)) + { + for (int16_t x = 0; x < wSrc && indexPixel < destPixelCount; x++, indexPixel++) + { + if ((uint16_t)xSrc < _width) + { + if (readPixel(&color)) + { + color = shader.Apply(indexPixel, color); + xSrc++; + } + } + + T_COLOR_FEATURE::applyPixelColor(destBuffer.Pixels, indexPixel, color); + } + } + } + + void Blt(NeoBufferContext destBuffer, + uint16_t indexPixel, + int16_t xSrc, + int16_t ySrc, + int16_t wSrc) + { + NeoShaderNop shaderNop; + + Render>(destBuffer, shaderNop, indexPixel, xSrc, ySrc, wSrc); + }; + + template void Render(NeoBufferContext destBuffer, + T_SHADER& shader, + int16_t xDest, + int16_t yDest, + int16_t xSrc, + int16_t ySrc, + int16_t wSrc, + int16_t hSrc, + LayoutMapCallback layoutMap) + { + const uint16_t destPixelCount = destBuffer.PixelCount(); + typename T_COLOR_FEATURE::ColorObject color(0); + + for (int16_t y = 0; y < hSrc; y++) + { + int16_t xFile = constrainX(xSrc); + int16_t yFile = constrainY(ySrc + y); + + if (seek(xFile, yFile)) + { + for (int16_t x = 0; x < wSrc; x++) + { + uint16_t indexDest = layoutMap(xDest + x, yDest + y); + + if ((uint16_t)xFile < _width) + { + if (readPixel(&color)) + { + color = shader.Apply(indexDest, color); + xFile++; + } + } + + if (indexDest < destPixelCount) + { + T_COLOR_FEATURE::applyPixelColor(destBuffer.Pixels, indexDest, color); + } + } + } + } + }; + + void Blt(NeoBufferContext destBuffer, + int16_t xDest, + int16_t yDest, + int16_t xSrc, + int16_t ySrc, + int16_t wSrc, + int16_t hSrc, + LayoutMapCallback layoutMap) + { + NeoShaderNop shaderNop; + + Render>(destBuffer, + shaderNop, + xDest, + yDest, + xSrc, + ySrc, + wSrc, + hSrc, + layoutMap); + }; + + +private: + T_FILE_METHOD _file; + uint32_t _fileAddressPixels; + uint16_t _width; + uint16_t _height; + uint32_t _sizeRow; + uint8_t _bytesPerPixel; + bool _bottomToTop; + + int16_t constrainX(int16_t x) const + { + if (x < 0) + { + x = 0; + } + else if ((uint16_t)x >= _width) + { + x = _width - 1; + } + return x; + }; + + int16_t constrainY(int16_t y) const + { + if (y < 0) + { + y = 0; + } + else if ((uint16_t)y >= _height) + { + y = _height - 1; + } + return y; + }; + + bool seek(int16_t x, int16_t y) + { + if (_bottomToTop) + { + y = (_height - 1) - y; + } + + uint32_t pos = y * _sizeRow + x * _bytesPerPixel; + pos += _fileAddressPixels; + + return _file.seek(pos); + }; + + bool readPixel(RgbColor* color) + { + uint8_t bgr[4]; + int result; + + result = _file.read(bgr, _bytesPerPixel); + + if (result != _bytesPerPixel) + { + *color = 0; + return false; + } + + color->B = bgr[0]; + color->G = bgr[1]; + color->R = bgr[2]; + + return true; + }; + + bool readPixel(RgbwColor* color) + { + uint8_t bgr[4]; + int result; + + bgr[3] = 0; // init white channel as read maybe only 3 bytes + result = _file.read(bgr, _bytesPerPixel); + + if (result != _bytesPerPixel) + { + *color = 0; + return false; + } + + color->B = bgr[0]; + color->G = bgr[1]; + color->R = bgr[2]; + color->W = bgr[3]; + + return true; + }; +}; \ No newline at end of file diff --git a/components/NeoPixelBus/src/internal/NeoBuffer.h b/components/NeoPixelBus/src/internal/NeoBuffer.h new file mode 100644 index 00000000..e4c9423d --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoBuffer.h @@ -0,0 +1,185 @@ +/*------------------------------------------------------------------------- +NeoPixel library + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +#include +#include + +typedef std::function LayoutMapCallback; + +#ifndef PGM_P +#define PGM_P const char * +#endif +#ifndef PGM_VOID_P +#define PGM_VOID_P const void * +#endif + +template class NeoBuffer +{ +public: + NeoBuffer(uint16_t width, + uint16_t height, + PGM_VOID_P pixels) : + _method(width, height, pixels) + { + } + + operator NeoBufferContext() + { + return _method; + } + + uint16_t PixelCount() const + { + return _method.PixelCount(); + }; + + uint16_t Width() const + { + return _method.Width(); + }; + + uint16_t Height() const + { + return _method.Height(); + }; + + void SetPixelColor( + int16_t x, + int16_t y, + typename T_BUFFER_METHOD::ColorObject color) + { + _method.SetPixelColor(pixelIndex(x, y), color); + }; + + typename T_BUFFER_METHOD::ColorObject GetPixelColor( + int16_t x, + int16_t y) const + { + return _method.GetPixelColor(pixelIndex(x, y)); + }; + + void ClearTo(typename T_BUFFER_METHOD::ColorObject color) + { + _method.ClearTo(color); + }; + + void Blt(NeoBufferContext destBuffer, + uint16_t indexPixel) + { + uint16_t destPixelCount = destBuffer.PixelCount(); + // validate indexPixel + if (indexPixel >= destPixelCount) + { + return; + } + + // calc how many we can copy + uint16_t copyCount = destPixelCount - indexPixel; + uint16_t srcPixelCount = PixelCount(); + if (copyCount > srcPixelCount) + { + copyCount = srcPixelCount; + } + + uint8_t* pDest = T_BUFFER_METHOD::ColorFeature::getPixelAddress(destBuffer.Pixels, indexPixel); + _method.CopyPixels(pDest, _method.Pixels(), copyCount); + } + + void Blt(NeoBufferContext destBuffer, + int16_t xDest, + int16_t yDest, + int16_t xSrc, + int16_t ySrc, + int16_t wSrc, + int16_t hSrc, + LayoutMapCallback layoutMap) + { + uint16_t destPixelCount = destBuffer.PixelCount(); + + for (int16_t y = 0; y < hSrc; y++) + { + for (int16_t x = 0; x < wSrc; x++) + { + uint16_t indexDest = layoutMap(xDest + x, yDest + y); + + if (indexDest < destPixelCount) + { + const uint8_t* pSrc = T_BUFFER_METHOD::ColorFeature::getPixelAddress(_method.Pixels(), pixelIndex(xSrc + x, ySrc + y)); + uint8_t* pDest = T_BUFFER_METHOD::ColorFeature::getPixelAddress(destBuffer.Pixels, indexDest); + + _method.CopyPixels(pDest, pSrc, 1); + } + } + } + } + + void Blt(NeoBufferContext destBuffer, + int16_t xDest, + int16_t yDest, + LayoutMapCallback layoutMap) + { + Blt(destBuffer, xDest, yDest, 0, 0, Width(), Height(), layoutMap); + } + + template void Render(NeoBufferContext destBuffer, T_SHADER& shader) + { + uint16_t countPixels = destBuffer.PixelCount(); + + if (countPixels > _method.PixelCount()) + { + countPixels = _method.PixelCount(); + } + + for (uint16_t indexPixel = 0; indexPixel < countPixels; indexPixel++) + { + typename T_BUFFER_METHOD::ColorObject color; + + shader.Apply(indexPixel, (uint8_t*)(&color), _method.Pixels() + (indexPixel * _method.PixelSize())); + + T_BUFFER_METHOD::ColorFeature::applyPixelColor(destBuffer.Pixels, indexPixel, color); + } + } + +private: + T_BUFFER_METHOD _method; + + uint16_t pixelIndex( + int16_t x, + int16_t y) const + { + uint16_t result = PixelIndex_OutOfBounds; + + if (x >= 0 && + (uint16_t)x < Width() && + y >= 0 && + (uint16_t)y < Height()) + { + result = x + y * Width(); + } + return result; + } +}; \ No newline at end of file diff --git a/components/NeoPixelBus/src/internal/NeoBufferContext.h b/components/NeoPixelBus/src/internal/NeoBufferContext.h new file mode 100644 index 00000000..8539a0cf --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoBufferContext.h @@ -0,0 +1,50 @@ +/*------------------------------------------------------------------------- +NeoPixel library + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +#include + +// This is used to allow a template classes that share common buffer concept to +// be able to pass that common information to functions +// The template classes just need to expose a conversion operator to this type +template struct NeoBufferContext +{ + NeoBufferContext(uint8_t* pixels, + size_t sizePixels) : + Pixels(pixels), + SizePixels(sizePixels) + { + } + + uint16_t PixelCount() const + { + return SizePixels / T_COLOR_FEATURE::PixelSize; + }; + + uint8_t* Pixels; + const size_t SizePixels; + +}; \ No newline at end of file diff --git a/components/NeoPixelBus/src/internal/NeoColorFeatures.h b/components/NeoPixelBus/src/internal/NeoColorFeatures.h new file mode 100644 index 00000000..8358ed68 --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoColorFeatures.h @@ -0,0 +1,286 @@ +/*------------------------------------------------------------------------- +NeoPixelFeatures provides feature classes to describe color order and +color depth for NeoPixelBus template class + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +#include + +class Neo3Elements +{ +public: + static const size_t PixelSize = 3; + + static uint8_t* getPixelAddress(uint8_t* pPixels, uint16_t indexPixel) + { + return pPixels + indexPixel * PixelSize; + } + static const uint8_t* getPixelAddress(const uint8_t* pPixels, uint16_t indexPixel) + { + return pPixels + indexPixel * PixelSize; + } + + static void replicatePixel(uint8_t* pPixelDest, const uint8_t* pPixelSrc, uint16_t count) + { + uint8_t* pEnd = pPixelDest + (count * PixelSize); + while (pPixelDest < pEnd) + { + *pPixelDest++ = pPixelSrc[0]; + *pPixelDest++ = pPixelSrc[1]; + *pPixelDest++ = pPixelSrc[2]; + } + } + + static void movePixelsInc(uint8_t* pPixelDest, const uint8_t* pPixelSrc, uint16_t count) + { + uint8_t* pEnd = pPixelDest + (count * PixelSize); + while (pPixelDest < pEnd) + { + *pPixelDest++ = *pPixelSrc++; + *pPixelDest++ = *pPixelSrc++; + *pPixelDest++ = *pPixelSrc++; + } + } + static void movePixelsDec(uint8_t* pPixelDest, const uint8_t* pPixelSrc, uint16_t count) + { + uint8_t* pDestBack = pPixelDest + (count * PixelSize); + const uint8_t* pSrcBack = pPixelSrc + (count * PixelSize); + while (pDestBack > pPixelDest) + { + *--pDestBack = *--pSrcBack; + *--pDestBack = *--pSrcBack; + *--pDestBack = *--pSrcBack; + } + } + + typedef RgbColor ColorObject; +}; + +class Neo4Elements +{ +public: + static const size_t PixelSize = 4; + + static uint8_t* getPixelAddress(uint8_t* pPixels, uint16_t indexPixel) + { + return pPixels + indexPixel * PixelSize; + } + static const uint8_t* getPixelAddress(const uint8_t* pPixels, uint16_t indexPixel) + { + return pPixels + indexPixel * PixelSize; + } + + static void replicatePixel(uint8_t* pPixelDest, const uint8_t* pPixelSrc, uint16_t count) + { + uint32_t* pDest = (uint32_t*)pPixelDest; + const uint32_t* pSrc = (const uint32_t*)pPixelSrc; + + uint32_t* pEnd = pDest + count; + while (pDest < pEnd) + { + *pDest++ = *pSrc; + } + } + + static void movePixelsInc(uint8_t* pPixelDest, const uint8_t* pPixelSrc, uint16_t count) + { + uint32_t* pDest = (uint32_t*)pPixelDest; + const uint32_t* pSrc = (uint32_t*)pPixelSrc; + uint32_t* pEnd = pDest + count; + while (pDest < pEnd) + { + *pDest++ = *pSrc++; + } + } + + static void movePixelsDec(uint8_t* pPixelDest, const uint8_t* pPixelSrc, uint16_t count) + { + uint32_t* pDest = (uint32_t*)pPixelDest; + const uint32_t* pSrc = (uint32_t*)pPixelSrc; + uint32_t* pDestBack = pDest + count; + const uint32_t* pSrcBack = pSrc + count; + while (pDestBack > pDest) + { + *--pDestBack = *--pSrcBack; + } + } + + typedef RgbwColor ColorObject; +}; + +class NeoGrbFeature : public Neo3Elements +{ +public: + static void applyPixelColor(uint8_t* pPixels, uint16_t indexPixel, ColorObject color) + { + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + *p++ = color.G; + *p++ = color.R; + *p = color.B; + } + + static ColorObject retrievePixelColor(uint8_t* pPixels, uint16_t indexPixel) + { + ColorObject color; + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + color.G = *p++; + color.R = *p++; + color.B = *p; + + return color; + } +}; + +class NeoGrbwFeature : public Neo4Elements +{ +public: + static void applyPixelColor(uint8_t* pPixels, uint16_t indexPixel, ColorObject color) + { + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + *p++ = color.G; + *p++ = color.R; + *p++ = color.B; + *p = color.W; + } + + static ColorObject retrievePixelColor(uint8_t* pPixels, uint16_t indexPixel) + { + ColorObject color; + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + color.G = *p++; + color.R = *p++; + color.B = *p++; + color.W = *p; + + + return color; + } +}; + +class NeoRgbwFeature : public Neo4Elements +{ +public: + static void applyPixelColor(uint8_t* pPixels, uint16_t indexPixel, ColorObject color) + { + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + *p++ = color.R; + *p++ = color.G; + *p++ = color.B; + *p = color.W; + } + + static ColorObject retrievePixelColor(uint8_t* pPixels, uint16_t indexPixel) + { + ColorObject color; + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + color.R = *p++; + color.G = *p++; + color.B = *p++; + color.W = *p; + + return color; + } +}; + +class NeoRgbFeature : public Neo3Elements +{ +public: + static void applyPixelColor(uint8_t* pPixels, uint16_t indexPixel, ColorObject color) + { + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + *p++ = color.R; + *p++ = color.G; + *p = color.B; + } + + static ColorObject retrievePixelColor(uint8_t* pPixels, uint16_t indexPixel) + { + ColorObject color; + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + color.R = *p++; + color.G = *p++; + color.B = *p; + + return color; + } +}; + +class NeoBrgFeature : public Neo3Elements +{ +public: + static void applyPixelColor(uint8_t* pPixels, uint16_t indexPixel, ColorObject color) + { + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + *p++ = color.B; + *p++ = color.R; + *p = color.G; + } + + static ColorObject retrievePixelColor(uint8_t* pPixels, uint16_t indexPixel) + { + ColorObject color; + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + color.B = *p++; + color.R = *p++; + color.G = *p; + + return color; + } +}; + +class NeoRbgFeature : public Neo3Elements +{ +public: + static void applyPixelColor(uint8_t* pPixels, uint16_t indexPixel, ColorObject color) + { + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + *p++ = color.R; + *p++ = color.B; + *p = color.G; + } + + static ColorObject retrievePixelColor(uint8_t* pPixels, uint16_t indexPixel) + { + ColorObject color; + uint8_t* p = getPixelAddress(pPixels, indexPixel); + + color.R = *p++; + color.B = *p++; + color.G = *p; + + return color; + } +}; diff --git a/components/NeoPixelBus/src/internal/NeoDib.h b/components/NeoPixelBus/src/internal/NeoDib.h new file mode 100644 index 00000000..6f80c76f --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoDib.h @@ -0,0 +1,191 @@ +/*------------------------------------------------------------------------- +NeoPixel library + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +#include + +template class NeoShaderNop +{ +public: + NeoShaderNop() + { + } + + bool IsDirty() const + { + return true; + }; + + void Dirty() + { + }; + + void ResetDirty() + { + }; + + T_COLOR_OBJECT Apply(uint16_t, T_COLOR_OBJECT color) + { + return color; + }; +}; + +class NeoShaderBase +{ +public: + NeoShaderBase() : + _state(0) + { + } + + bool IsDirty() const + { + return (_state & NEO_DIRTY); + }; + + void Dirty() + { + _state |= NEO_DIRTY; + }; + + void ResetDirty() + { + _state &= ~NEO_DIRTY; + }; + +protected: + uint8_t _state; // internal state +}; + +template class NeoDib +{ +public: + NeoDib(uint16_t countPixels) : + _countPixels(countPixels), + _state(0) + { + _pixels = (T_COLOR_OBJECT*)malloc(PixelsSize()); + ResetDirty(); + } + + ~NeoDib() + { + free((uint8_t*)_pixels); + } + + T_COLOR_OBJECT* Pixels() const + { + return _pixels; + }; + + uint16_t PixelCount() const + { + return _countPixels; + }; + + size_t PixelsSize() const + { + return _countPixels * PixelSize(); + }; + + size_t PixelSize() const + { + return sizeof(T_COLOR_OBJECT); + }; + + void SetPixelColor( + uint16_t indexPixel, + T_COLOR_OBJECT color) + { + if (indexPixel < PixelCount()) + { + _pixels[indexPixel] = color; + Dirty(); + } + }; + + T_COLOR_OBJECT GetPixelColor( + uint16_t indexPixel) const + { + if (indexPixel >= PixelCount()) + { + return 0; + } + return _pixels[indexPixel]; + }; + + void ClearTo(T_COLOR_OBJECT color) + { + for (uint16_t pixel = 0; pixel < PixelCount(); pixel++) + { + _pixels[pixel] = color; + } + Dirty(); + }; + + template void Render(NeoBufferContext destBuffer, + T_SHADER& shader) + { + if (IsDirty() || shader.IsDirty()) + { + uint16_t countPixels = destBuffer.PixelCount(); + + if (countPixels > _countPixels) + { + countPixels = _countPixels; + } + + for (uint16_t indexPixel = 0; indexPixel < countPixels; indexPixel++) + { + T_COLOR_OBJECT color = shader.Apply(indexPixel, _pixels[indexPixel]); + T_COLOR_FEATURE::applyPixelColor(destBuffer.Pixels, indexPixel, color); + } + + shader.ResetDirty(); + ResetDirty(); + } + } + + bool IsDirty() const + { + return (_state & NEO_DIRTY); + }; + + void Dirty() + { + _state |= NEO_DIRTY; + }; + + void ResetDirty() + { + _state &= ~NEO_DIRTY; + }; + +private: + const uint16_t _countPixels; // Number of RGB LEDs in strip + T_COLOR_OBJECT* _pixels; + uint8_t _state; // internal state +}; \ No newline at end of file diff --git a/components/NeoPixelBus/src/internal/NeoEase.h b/components/NeoPixelBus/src/internal/NeoEase.h new file mode 100644 index 00000000..c03a2ff9 --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoEase.h @@ -0,0 +1,314 @@ +/*------------------------------------------------------------------------- +NeoEase provides animation curve equations for animation support. + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +#pragma once + +#undef max +#undef min +#include +#define _USE_MATH_DEFINES +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif +#ifndef M_PI_2 +#define M_PI_2 1.57079632679489661923 +#endif + +typedef std::function AnimEaseFunction; + +class NeoEase +{ +public: + static float Linear(float unitValue) + { + return unitValue; + } + + static float QuadraticIn(float unitValue) + { + return unitValue * unitValue; + } + + static float QuadraticOut(float unitValue) + { + return (-unitValue * (unitValue - 2.0f)); + } + + static float QuadraticInOut(float unitValue) + { + unitValue *= 2.0f; + if (unitValue < 1.0f) + { + return (0.5f * unitValue * unitValue); + } + else + { + unitValue -= 1.0f; + return (-0.5f * (unitValue * (unitValue - 2.0f) - 1.0f)); + } + } + + static float QuadraticCenter(float unitValue) + { + unitValue *= 2.0f; + if (unitValue < 1.0f) + { + return (-0.5f * (unitValue * unitValue - 2.0f)); + } + else + { + unitValue -= 1.0f; + return (0.5f * (unitValue * unitValue + 1.0f)); + } + } + + static float CubicIn(float unitValue) + { + return (unitValue * unitValue * unitValue); + } + + static float CubicOut(float unitValue) + { + unitValue -= 1.0f; + return (unitValue * unitValue * unitValue + 1); + } + + static float CubicInOut(float unitValue) + { + unitValue *= 2.0f; + if (unitValue < 1.0f) + { + return (0.5f * unitValue * unitValue * unitValue); + } + else + { + unitValue -= 2.0f; + return (0.5f * (unitValue * unitValue * unitValue + 2.0f)); + } + } + + static float CubicCenter(float unitValue) + { + unitValue *= 2.0f; + unitValue -= 1.0f; + return (0.5f * (unitValue * unitValue * unitValue) + 1); + } + + static float QuarticIn(float unitValue) + { + return (unitValue * unitValue * unitValue * unitValue); + } + + static float QuarticOut(float unitValue) + { + unitValue -= 1.0f; + return -(unitValue * unitValue * unitValue * unitValue - 1); + } + + static float QuarticInOut(float unitValue) + { + unitValue *= 2.0f; + if (unitValue < 1.0f) + { + return (0.5f * unitValue * unitValue * unitValue * unitValue); + } + else + { + unitValue -= 2.0f; + return (-0.5f * (unitValue * unitValue * unitValue * unitValue - 2.0f)); + } + } + + static float QuarticCenter(float unitValue) + { + unitValue *= 2.0f; + unitValue -= 1.0f; + if (unitValue < 0.0f) + { + return (-0.5f * (unitValue * unitValue * unitValue * unitValue - 1.0f)); + } + else + { + return (0.5f * (unitValue * unitValue * unitValue * unitValue + 1.0f)); + } + } + + static float QuinticIn(float unitValue) + { + return (unitValue * unitValue * unitValue * unitValue * unitValue); + } + + static float QuinticOut(float unitValue) + { + unitValue -= 1.0f; + return (unitValue * unitValue * unitValue * unitValue * unitValue + 1.0f); + } + + static float QuinticInOut(float unitValue) + { + unitValue *= 2.0f; + if (unitValue < 1.0f) + { + return (0.5f * unitValue * unitValue * unitValue * unitValue * unitValue); + } + else + { + unitValue -= 2.0f; + return (0.5f * (unitValue * unitValue * unitValue * unitValue * unitValue + 2.0f)); + } + } + + static float QuinticCenter(float unitValue) + { + unitValue *= 2.0f; + unitValue -= 1.0f; + return (0.5f * (unitValue * unitValue * unitValue * unitValue * unitValue + 1.0f)); + } + + static float SinusoidalIn(float unitValue) + { + return (-cos(unitValue * M_PI_2) + 1.0f); + } + + static float SinusoidalOut(float unitValue) + { + return (sin(unitValue * M_PI_2)); + } + + static float SinusoidalInOut(float unitValue) + { + return -0.5 * (cos(M_PI * unitValue) - 1.0f); + } + + static float SinusoidalCenter(float unitValue) + { + if (unitValue < 0.5f) + { + return (0.5 * sin(M_PI * unitValue)); + } + else + { + return (-0.5 * (cos(M_PI * (unitValue-0.5f)) + 1.0f)); + } + + } + + static float ExponentialIn(float unitValue) + { + return (pow(2, 10.0f * (unitValue - 1.0f))); + } + + static float ExponentialOut(float unitValue) + { + return (-pow(2, -10.0f * unitValue) + 1.0f); + } + + static float ExponentialInOut(float unitValue) + { + unitValue *= 2.0f; + if (unitValue < 1.0f) + { + return (0.5f * pow(2, 10.0f * (unitValue - 1.0f))); + } + else + { + unitValue -= 1.0f; + return (0.5f * (-pow(2, -10.0f * unitValue) + 2.0f)); + } + } + + static float ExponentialCenter(float unitValue) + { + unitValue *= 2.0f; + if (unitValue < 1.0f) + { + return (0.5f * (-pow(2, -10.0f * unitValue) + 1.0f)); + } + else + { + unitValue -= 2.0f; + return (0.5f * (pow(2, 10.0f * unitValue) + 1.0f)); + } + } + + static float CircularIn(float unitValue) + { + if (unitValue == 1.0f) + { + return 1.0f; + } + else + { + return (-(sqrt(1.0f - unitValue * unitValue) - 1.0f)); + } + } + + static float CircularOut(float unitValue) + { + unitValue -= 1.0f; + return (sqrt(1.0f - unitValue * unitValue)); + } + + static float CircularInOut(float unitValue) + { + unitValue *= 2.0f; + if (unitValue < 1.0f) + { + return (-0.5f * (sqrt(1.0f - unitValue * unitValue) - 1)); + } + else + { + unitValue -= 2.0f; + return (0.5f * (sqrt(1.0f - unitValue * unitValue) + 1.0f)); + } + } + + static float CircularCenter(float unitValue) + { + unitValue *= 2.0f; + unitValue -= 1.0f; + if (unitValue == 0.0f) + { + return 1.0f; + } + else if (unitValue < 0.0f) + { + return (0.5f * sqrt(1.0f - unitValue * unitValue)); + } + else + { + unitValue -= 2.0f; + return (-0.5f * (sqrt(1.0f - unitValue * unitValue) - 1.0f ) + 0.5f); + } + } + + static float Gamma(float unitValue) + { + return pow(unitValue, 1.0f / 0.45f); + } +}; \ No newline at end of file diff --git a/components/NeoPixelBus/src/internal/NeoEsp32RmtMethod.h b/components/NeoPixelBus/src/internal/NeoEsp32RmtMethod.h new file mode 100644 index 00000000..f3445985 --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoEsp32RmtMethod.h @@ -0,0 +1,415 @@ +/*------------------------------------------------------------------------- +NeoPixel library helper functions for Esp32. + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +#pragma once + +#include + +#ifdef ESP32 + +/* General Reference documentation for the APIs used in this implementation +LOW LEVEL: (what is actually used) +DOCS: https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/rmt.html +EXAMPLE: https://github.com/espressif/esp-idf/blob/826ff7186ae07dc81e960a8ea09ebfc5304bfb3b/examples/peripherals/rmt_tx/main/rmt_tx_main.c + +HIGHER LEVEL: +NO TRANSLATE SUPPORT so this was not used +NOTE: https://github.com/espressif/arduino-esp32/commit/50d142950d229b8fabca9b749dc4a5f2533bc426 +Esp32-hal-rmt.h +Esp32-hal-rmt.c +*/ + +#include + +class NeoEsp32RmtSpeedBase +{ +public: + // ClkDiv of 2 provides for good resolution and plenty of reset resolution; but + // a ClkDiv of 1 will provide enough space for the longest reset and does show + // little better pulse accuracy + const static uint8_t RmtClockDivider = 2; + + inline constexpr static uint32_t FromNs(uint32_t ns) + { + return ns / NsPerRmtTick; + } + // this is used rather than the rmt_item32_t as you can't correctly initialize + // it as a static constexpr within the template + inline constexpr static uint32_t Item32Val(uint16_t nsHigh, uint16_t nsLow) + { + return (FromNs(nsLow) << 16) | (1 << 15) | (FromNs(nsHigh)); + } + +public: + const static uint32_t RmtCpu = 80000000L; // 80 mhz RMT clock + const static uint32_t NsPerSecond = 1000000000L; + const static uint32_t RmtTicksPerSecond = (RmtCpu / RmtClockDivider); + const static uint32_t NsPerRmtTick = (NsPerSecond / RmtTicksPerSecond); // about 25 +}; + +class NeoEsp32RmtSpeedWs2811 : public NeoEsp32RmtSpeedBase +{ +public: + const static uint32_t RmtBit0 = Item32Val(300, 950); + const static uint32_t RmtBit1 = Item32Val(900, 350); + const static uint16_t RmtDurationReset = FromNs(300000); // 300us +}; + +class NeoEsp32RmtSpeedWs2812x : public NeoEsp32RmtSpeedBase +{ +public: + const static uint32_t RmtBit0 = Item32Val(400, 850); + const static uint32_t RmtBit1 = Item32Val(800, 450); + const static uint16_t RmtDurationReset = FromNs(300000); // 300us +}; + +class NeoEsp32RmtSpeedSk6812 : public NeoEsp32RmtSpeedBase +{ +public: + const static uint32_t RmtBit0 = Item32Val(400, 850); + const static uint32_t RmtBit1 = Item32Val(800, 450); + const static uint16_t RmtDurationReset = FromNs(80000); // 80us +}; + +class NeoEsp32RmtSpeed800Kbps : public NeoEsp32RmtSpeedBase +{ +public: + const static uint32_t RmtBit0 = Item32Val(400, 850); + const static uint32_t RmtBit1 = Item32Val(800, 450); + const static uint16_t RmtDurationReset = FromNs(50000); // 50us +}; + +class NeoEsp32RmtSpeed400Kbps : public NeoEsp32RmtSpeedBase +{ +public: + const static uint32_t RmtBit0 = Item32Val(800, 1700); + const static uint32_t RmtBit1 = Item32Val(1600, 900); + const static uint16_t RmtDurationReset = FromNs(50000); // 50us +}; + +class NeoEsp32RmtSpeedApa106 : public NeoEsp32RmtSpeedBase +{ +public: + const static uint32_t RmtBit0 = Item32Val(400, 1250); + const static uint32_t RmtBit1 = Item32Val(1250, 400); + const static uint16_t RmtDurationReset = FromNs(50000); // 50us +}; + +class NeoEsp32RmtChannel0 +{ +public: + const static rmt_channel_t RmtChannelNumber = RMT_CHANNEL_0; +}; + +class NeoEsp32RmtChannel1 +{ +public: + const static rmt_channel_t RmtChannelNumber = RMT_CHANNEL_1; +}; + +class NeoEsp32RmtChannel2 +{ +public: + const static rmt_channel_t RmtChannelNumber = RMT_CHANNEL_2; +}; + +class NeoEsp32RmtChannel3 +{ +public: + const static rmt_channel_t RmtChannelNumber = RMT_CHANNEL_3; +}; + +class NeoEsp32RmtChannel4 +{ +public: + const static rmt_channel_t RmtChannelNumber = RMT_CHANNEL_4; +}; + +class NeoEsp32RmtChannel5 +{ +public: + const static rmt_channel_t RmtChannelNumber = RMT_CHANNEL_5; +}; + +class NeoEsp32RmtChannel6 +{ +public: + const static rmt_channel_t RmtChannelNumber = RMT_CHANNEL_6; +}; + +class NeoEsp32RmtChannel7 +{ +public: + const static rmt_channel_t RmtChannelNumber = RMT_CHANNEL_7; +}; + +template class NeoEsp32RmtMethodBase +{ +public: + NeoEsp32RmtMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize) : + _pin(pin) + { + _pixelsSize = pixelCount * elementSize; + + _pixelsEditing = static_cast(malloc(_pixelsSize)); + memset(_pixelsEditing, 0x00, _pixelsSize); + + if ((_pixelsSize * 8) < 128) + { + _encodedPixels = static_cast(malloc(sizeof(rmt_item32_t) * _pixelsSize * 8)); + } + else + { + _pixelsSending = static_cast(malloc(_pixelsSize)); + // no need to initialize it, it gets overwritten on every send + } + } + + ~NeoEsp32RmtMethodBase() + { + // wait until the last send finishes before destructing everything + // arbitrary time out of 10 seconds + rmt_wait_tx_done(T_CHANNEL::RmtChannelNumber, 10000 / portTICK_PERIOD_MS); + + rmt_driver_uninstall(T_CHANNEL::RmtChannelNumber); + + free(_pixelsEditing); + if (_encodedPixels) + { + free(_encodedPixels); + } + else + { + free(_pixelsSending); + } + } + + + bool IsReadyToUpdate() const + { + return (ESP_OK == rmt_wait_tx_done(T_CHANNEL::RmtChannelNumber, 0)); + } + + void Initialize() + { + rmt_config_t config; + + config.rmt_mode = RMT_MODE_TX; + config.channel = T_CHANNEL::RmtChannelNumber; + config.gpio_num = static_cast(_pin); + config.mem_block_num = 1; + if(_encodedPixels) + { + config.mem_block_num = 2; + } + config.tx_config.loop_en = false; + + config.tx_config.idle_output_en = true; + config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; + + config.tx_config.carrier_en = false; + config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; + + config.clk_div = T_SPEED::RmtClockDivider; + + ESP_ERROR_CHECK(rmt_config(&config)); + ESP_ERROR_CHECK(rmt_driver_install(T_CHANNEL::RmtChannelNumber, 0, 0)); + if(_pixelsSending) + { + ESP_ERROR_CHECK(rmt_translator_init(T_CHANNEL::RmtChannelNumber, _translate)); + } + } + + void Update(bool maintainBufferConsistency) + { + // wait for not actively sending data + // this will time out at 10 seconds, an arbitrarily long period of time + // and do nothing if this happens + if (ESP_OK == rmt_wait_tx_done(T_CHANNEL::RmtChannelNumber, 10000 / portTICK_PERIOD_MS)) + { + if (_encodedPixels) + { + size_t translated{0}, count{0}; + _translate((void *)_pixelsEditing, _encodedPixels, _pixelsSize, _pixelsSize * 8, &translated, &count); + rmt_write_items(T_CHANNEL::RmtChannelNumber, _encodedPixels, count, false); + } + else + { + // now start the RMT transmit with the editing buffer before we swap + rmt_write_sample(T_CHANNEL::RmtChannelNumber, _pixelsEditing, _pixelsSize, false); + + if (maintainBufferConsistency) + { + // copy editing to sending, + // this maintains the contract that "colors present before will + // be the same after", otherwise GetPixelColor will be inconsistent + memcpy(_pixelsSending, _pixelsEditing, _pixelsSize); + } + + // swap so the user can modify without affecting the async operation + std::swap(_pixelsSending, _pixelsEditing); + } + } + } + + uint8_t* getPixels() const + { + return _pixelsEditing; + }; + + size_t getPixelsSize() const + { + return _pixelsSize; + } + +private: + const uint8_t _pin; // output pin number + + size_t _pixelsSize; // Size of '_pixels' buffer + uint8_t* _pixelsEditing; // Holds LED color values exposed for get and set + uint8_t* _pixelsSending{nullptr}; // Holds LED color values used to async send using RMT + rmt_item32_t *_encodedPixels{nullptr}; // pre-encoded pixel data + + + // stranslate NeoPixelBuffer into RMT buffer + // this is done on the fly so we don't require a send buffer in raw RMT format + // which would be 32x larger than the primary buffer + static void IRAM_ATTR _translate(const void* src, + rmt_item32_t* dest, + size_t src_size, + size_t wanted_num, + size_t* translated_size, + size_t* item_num) + { + if (src == NULL || dest == NULL) + { + *translated_size = 0; + *item_num = 0; + return; + } + + size_t size = 0; + size_t num = 0; + const uint8_t* psrc = static_cast(src); + rmt_item32_t* pdest = dest; + + for (;;) + { + uint8_t data = *psrc; + + for (uint8_t bit = 0; bit < 8; bit++) + { + pdest->val = (data & 0x80) ? T_SPEED::RmtBit1 : T_SPEED::RmtBit0; + pdest++; + data <<= 1; + } + num += 8; + size++; + + // if this is the last byte we need to adjust the length of the last pulse + if (size >= src_size) + { + // extend the last bits LOW value to include the full reset signal length + pdest--; + pdest->duration1 = T_SPEED::RmtDurationReset; + // and stop updating data to send + break; + } + + if (num >= wanted_num) + { + // stop updating data to send + break; + } + + psrc++; + } + + *translated_size = size; + *item_num = num; + } +}; + +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt0Ws2811Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt0Ws2812xMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt0Sk6812Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt0Apa106Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt0800KbpsMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt0400KbpsMethod; + +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt1Ws2811Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt1Ws2812xMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt1Sk6812Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt1Apa106Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt1800KbpsMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt1400KbpsMethod; + +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt2Ws2811Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt2Ws2812xMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt2Sk6812Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt2Apa106Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt2800KbpsMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt2400KbpsMethod; + +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt3Ws2811Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt3Ws2812xMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt3Sk6812Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt3Apa106Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt3800KbpsMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt3400KbpsMethod; + +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt4Ws2811Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt4Ws2812xMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt4Sk6812Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt4Apa106Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt4800KbpsMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt4400KbpsMethod; + +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt5Ws2811Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt5Ws2812xMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt5Sk6812Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt5Apa106Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt5800KbpsMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt5400KbpsMethod; + +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt6Ws2811Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt6Ws2812xMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt6Sk6812Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt6Apa106Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt6800KbpsMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt6400KbpsMethod; + +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt7Ws2811Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt7Ws2812xMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt7Sk6812Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt7Apa106Method; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt7800KbpsMethod; +typedef NeoEsp32RmtMethodBase NeoEsp32Rmt7400KbpsMethod; + +// RMT is NOT the default method for Esp32, +// you are required to use a specific channel listed above + +#endif \ No newline at end of file diff --git a/components/NeoPixelBus/src/internal/NeoGamma.cpp b/components/NeoPixelBus/src/internal/NeoGamma.cpp new file mode 100644 index 00000000..df0a455e --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoGamma.cpp @@ -0,0 +1,49 @@ +/*------------------------------------------------------------------------- +NeoPixelGamma class is used to correct RGB colors for human eye gamma levels + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +#include "RgbColor.h" +#include "RgbwColor.h" +#include "NeoEase.h" +#include "NeoGamma.h" + +const uint8_t NeoGammaTableMethod::_table[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, + 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11, 11, 11, + 12, 12, 13, 13, 14, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, + 19, 20, 20, 21, 22, 22, 23, 23, 24, 25, 25, 26, 26, 27, 28, 28, + 29, 30, 30, 31, 32, 33, 33, 34, 35, 35, 36, 37, 38, 39, 39, 40, + 41, 42, 43, 43, 44, 45, 46, 47, 48, 49, 50, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 71, + 72, 73, 74, 75, 76, 77, 78, 80, 81, 82, 83, 84, 86, 87, 88, 89, + 91, 92, 93, 94, 96, 97, 98, 100, 101, 102, 104, 105, 106, 108, 109, 110, + 112, 113, 115, 116, 118, 119, 121, 122, 123, 125, 126, 128, 130, 131, 133, 134, + 136, 137, 139, 140, 142, 144, 145, 147, 149, 150, 152, 154, 155, 157, 159, 160, + 162, 164, 166, 167, 169, 171, 173, 175, 176, 178, 180, 182, 184, 186, 187, 189, + 191, 193, 195, 197, 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 221, + 223, 225, 227, 229, 231, 233, 235, 238, 240, 242, 244, 246, 248, 251, 253, 255 +}; \ No newline at end of file diff --git a/components/NeoPixelBus/src/internal/NeoGamma.h b/components/NeoPixelBus/src/internal/NeoGamma.h new file mode 100644 index 00000000..a6669c65 --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoGamma.h @@ -0,0 +1,79 @@ +/*------------------------------------------------------------------------- +NeoGamma class is used to correct RGB colors for human eye gamma levels equally +across all color channels + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +#include +#include "NeoEase.h" +#include "RgbColor.h" +#include "RgbwColor.h" + +// NeoGammaEquationMethod uses no memory but is slower than NeoGammaTableMethod +class NeoGammaEquationMethod +{ +public: + static uint8_t Correct(uint8_t value) + { + return static_cast(255.0f * NeoEase::Gamma(value / 255.0f) + 0.5f); + } +}; + +// NeoGammaTableMethod uses 256 bytes of memory, but is significantly faster +class NeoGammaTableMethod +{ +public: + static uint8_t Correct(uint8_t value) + { + return _table[value]; + } + +private: + static const uint8_t _table[256]; +}; + + +// use one of the method classes above as a converter for this template class +template class NeoGamma +{ +public: + RgbColor Correct(const RgbColor& original) + { + return RgbColor(T_METHOD::Correct(original.R), + T_METHOD::Correct(original.G), + T_METHOD::Correct(original.B)); + } + + RgbwColor Correct(const RgbwColor& original) + { + return RgbwColor(T_METHOD::Correct(original.R), + T_METHOD::Correct(original.G), + T_METHOD::Correct(original.B), + T_METHOD::Correct(original.W) ); + } +}; + + + diff --git a/components/NeoPixelBus/src/internal/NeoHueBlend.h b/components/NeoPixelBus/src/internal/NeoHueBlend.h new file mode 100644 index 00000000..d77a58fb --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoHueBlend.h @@ -0,0 +1,118 @@ +/*------------------------------------------------------------------------- +NeoHueBlend provides method objects that can be directly consumed by +blend template functions in HslColor and HsbColor + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +class NeoHueBlendBase +{ +protected: + static float FixWrap(float value) + { + if (value < 0.0f) + { + value += 1.0f; + } + else if (value > 1.0f) + { + value -= 1.0f; + } + return value; + } +}; + +class NeoHueBlendShortestDistance : NeoHueBlendBase +{ +public: + static float HueBlend(float left, float right, float progress) + { + float delta = right - left; + float base = left; + if (delta > 0.5f) + { + base = right; + delta = 1.0f - delta; + progress = 1.0f - progress; + } + else if (delta < -0.5f) + { + delta = 1.0f + delta; + } + return FixWrap(base + (delta) * progress); + }; +}; + +class NeoHueBlendLongestDistance : NeoHueBlendBase +{ +public: + static float HueBlend(float left, float right, float progress) + { + float delta = right - left; + float base = left; + if (delta < 0.5f && delta >= 0.0f) + { + base = right; + delta = 1.0f - delta; + progress = 1.0f - progress; + } + else if (delta > -0.5f && delta < 0.0f) + { + delta = 1.0f + delta; + } + return FixWrap(base + delta * progress); + }; +}; + +class NeoHueBlendClockwiseDirection : NeoHueBlendBase +{ +public: + static float HueBlend(float left, float right, float progress) + { + float delta = right - left; + float base = left; + if (delta < 0.0f) + { + delta = 1.0f + delta; + } + + return FixWrap(base + delta * progress); + }; +}; + +class NeoHueBlendCounterClockwiseDirection : NeoHueBlendBase +{ +public: + static float HueBlend(float left, float right, float progress) + { + float delta = right - left; + float base = left; + if (delta > 0.0f) + { + delta = delta - 1.0f; + } + + return FixWrap(base + delta * progress); + }; +}; diff --git a/components/NeoPixelBus/src/internal/NeoMosaic.h b/components/NeoPixelBus/src/internal/NeoMosaic.h new file mode 100644 index 00000000..944b7d00 --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoMosaic.h @@ -0,0 +1,193 @@ +#pragma once + +#include + +/*------------------------------------------------------------------------- +Mosiac provides a mapping feature of a 2d cordinate to linear 1d cordinate +It is used to map tiles of matricies of NeoPixels to a index on the NeoPixelBus +where the the matricies use a set of prefered topology and the tiles of +those matricies use the RowMajorAlternating layout + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + + +//----------------------------------------------------------------------------- +// class NeoMosaic +// Complex Tile layout class that reduces distance of the interconnects between +// the tiles by using different rotations of the layout at specific locations +// +// T_LAYOUT = the layout used for matrix panel (rotation is ignored) +// +// NOTE: The tiles in the mosaic are always laid out using RowMajorAlternating +// +//----------------------------------------------------------------------------- + +template class NeoMosaic +{ +public: + NeoMosaic(uint16_t topoWidth, uint16_t topoHeight, + uint16_t mosaicWidth, uint16_t mosaicHeight) : + _topoWidth(topoWidth), + _topoHeight(topoHeight), + _mosaicWidth(mosaicWidth), + _mosaicHeight(mosaicHeight) + { + } + + uint16_t Map(int16_t x, int16_t y) const + { + uint16_t totalWidth = getWidth(); + uint16_t totalHeight = getHeight(); + + if (x >= totalWidth) + { + x = totalWidth - 1; + } + else if (x < 0) + { + x = 0; + } + + if (y >= totalHeight) + { + y = totalHeight - 1; + } + else if (y < 0) + { + y = 0; + } + + uint16_t localIndex; + uint16_t tileOffset; + + calculate(x, y, &localIndex, &tileOffset); + + return localIndex + tileOffset; + } + + uint16_t MapProbe(int16_t x, int16_t y) const + { + uint16_t totalWidth = getWidth(); + uint16_t totalHeight = getHeight(); + + if (x < 0 || x >= totalWidth || y < 0 || y >= totalHeight) + { + return totalWidth * totalHeight; // count, out of bounds + } + + uint16_t localIndex; + uint16_t tileOffset; + + calculate(x, y, &localIndex, &tileOffset); + + return localIndex + tileOffset; + } + + NeoTopologyHint TopologyHint(int16_t x, int16_t y) const + { + uint16_t totalWidth = getWidth(); + uint16_t totalHeight = getHeight(); + + if (x < 0 || x >= totalWidth || y < 0 || y >= totalHeight) + { + return NeoTopologyHint_OutOfBounds; + } + + uint16_t localIndex; + uint16_t tileOffset; + NeoTopologyHint result; + + calculate(x, y, &localIndex, &tileOffset); + + if (localIndex == 0) + { + result = NeoTopologyHint_FirstOnPanel; + } + else if (localIndex == (_topoWidth * _topoHeight - 1)) + { + result = NeoTopologyHint_LastOnPanel; + } + else + { + result = NeoTopologyHint_InPanel; + } + + return result; + } + + uint16_t getWidth() const + { + return _topoWidth * _mosaicWidth; + } + + uint16_t getHeight() const + { + return _topoHeight * _mosaicHeight; + } + +private: + const uint16_t _topoWidth; + const uint16_t _topoHeight; + const uint16_t _mosaicWidth; + const uint16_t _mosaicHeight; + + void calculate(uint16_t x, uint16_t y, uint16_t* pLocalIndex, uint16_t* pTileOffset) const + { + uint16_t tileX = x / _topoWidth; + uint16_t topoX = x % _topoWidth; + + uint16_t tileY = y / _topoHeight; + uint16_t topoY = y % _topoHeight; + + *pTileOffset = RowMajorAlternatingLayout::Map(_mosaicWidth, + _mosaicHeight, + tileX, + tileY) * _topoWidth * _topoHeight; + + if (tileX & 0x0001) + { + // odd columns + if (tileY & 0x0001) + { + *pLocalIndex = T_LAYOUT::OddRowOddColumnLayout::Map(_topoWidth, _topoHeight, topoX, topoY); + } + else + { + *pLocalIndex = T_LAYOUT::EvenRowOddColumnLayout::Map(_topoWidth, _topoHeight, topoX, topoY); + } + } + else + { + // even columns + if (tileY & 0x0001) + { + *pLocalIndex = T_LAYOUT::OddRowEvenColumnLayout::Map(_topoWidth, _topoHeight, topoX, topoY); + } + else + { + *pLocalIndex = T_LAYOUT::EvenRowEvenColumnLayout::Map(_topoWidth, _topoHeight, topoX, topoY); + } + } + } +}; \ No newline at end of file diff --git a/components/NeoPixelBus/src/internal/NeoRingTopology.h b/components/NeoPixelBus/src/internal/NeoRingTopology.h new file mode 100644 index 00000000..8f3bbe89 --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoRingTopology.h @@ -0,0 +1,118 @@ +#pragma once + +#include + +/*------------------------------------------------------------------------- +NeoRingTopology provides a mapping feature of a 2d polar cordinate to a +linear 1d cordinate. +It is used to map a series of concentric rings of NeoPixels to a index on +the NeoPixelBus. + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +template class NeoRingTopology : protected T_LAYOUT +{ +public: + NeoRingTopology() + { + } + + uint16_t Map(uint8_t ring, uint16_t pixel) const + { + if (pixel >= getPixelCountAtRing(ring)) + { + return 0; // invalid ring and/or pixel argument, always return a valid value, the first one + } + + return _map(ring, pixel); + } + + uint16_t MapProbe(uint8_t ring, uint16_t pixel) const + { + if (pixel >= getPixelCountAtRing(ring)) + { + return getPixelCount(); // total count, out of bounds + } + + return _map(ring, pixel); + } + + uint16_t RingPixelShift(uint8_t ring, uint16_t pixel, int16_t shift) + { + int32_t ringPixel = pixel; + ringPixel += shift; + + if (ringPixel < 0) + { + ringPixel = 0; + } + else + { + uint16_t count = getPixelCountAtRing(ring); + if (ringPixel >= count) + { + ringPixel = count - 1; + } + } + return ringPixel; + } + + uint16_t RingPixelRotate(uint8_t ring, uint16_t pixel, int16_t rotate) + { + int32_t ringPixel = pixel; + ringPixel += rotate; + return ringPixel % getPixelCountAtRing(ring); + } + + uint8_t getCountOfRings() const + { + return _ringCount() - 1; // minus one as the Rings includes the extra value + } + + uint16_t getPixelCountAtRing(uint8_t ring) const + { + if (ring >= getCountOfRings()) + { + return 0; // invalid, no pixels + } + + return T_LAYOUT::Rings[ring + 1] - T_LAYOUT::Rings[ring]; // using the extra value for count calc + } + + uint16_t getPixelCount() const + { + return T_LAYOUT::Rings[_ringCount() - 1]; // the last entry is the total count + } + +private: + uint16_t _map(uint8_t ring, uint16_t pixel) const + { + return T_LAYOUT::Rings[ring] + pixel; + } + + uint8_t _ringCount() const + { + return sizeof(T_LAYOUT::Rings) / sizeof(T_LAYOUT::Rings[0]); + } +}; diff --git a/components/NeoPixelBus/src/internal/NeoSpriteSheet.h b/components/NeoPixelBus/src/internal/NeoSpriteSheet.h new file mode 100644 index 00000000..41bf1594 --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoSpriteSheet.h @@ -0,0 +1,164 @@ +/*------------------------------------------------------------------------- +NeoPixel library + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +#include + +template class NeoVerticalSpriteSheet +{ +public: + NeoVerticalSpriteSheet(uint16_t width, + uint16_t height, + uint16_t spriteHeight, + PGM_VOID_P pixels) : + _method(width, height, pixels), + _spriteHeight(spriteHeight), + _spriteCount(height / spriteHeight) + { + } + + operator NeoBufferContext() + { + return _method; + } + + uint16_t SpriteWidth() const + { + return _method.Width(); + }; + + uint16_t SpriteHeight() const + { + return _spriteHeight; + }; + + uint16_t SpriteCount() const + { + return _spriteCount; + } + + void SetPixelColor(uint16_t indexSprite, + int16_t x, + int16_t y, + typename T_BUFFER_METHOD::ColorObject color) + { + _method.SetPixelColor(pixelIndex(indexSprite, x, y), color); + }; + + typename T_BUFFER_METHOD::ColorObject GetPixelColor(uint16_t indexSprite, + int16_t x, + int16_t y) const + { + return _method.GetPixelColor(pixelIndex(indexSprite, x, y)); + }; + + void ClearTo(typename T_BUFFER_METHOD::ColorObject color) + { + _method.ClearTo(color); + }; + + void Blt(NeoBufferContext destBuffer, + uint16_t indexPixel, + uint16_t indexSprite) + { + uint16_t destPixelCount = destBuffer.PixelCount(); + // validate indexPixel + if (indexPixel >= destPixelCount) + { + return; + } + + // validate indexSprite + if (indexSprite >= _spriteCount) + { + return; + } + // calc how many we can copy + uint16_t copyCount = destPixelCount - indexPixel; + + if (copyCount > SpriteWidth()) + { + copyCount = SpriteWidth(); + } + + uint8_t* pDest = T_BUFFER_METHOD::ColorFeature::getPixelAddress(destBuffer.Pixels, indexPixel); + const uint8_t* pSrc = T_BUFFER_METHOD::ColorFeature::getPixelAddress(_method.Pixels(), pixelIndex(indexSprite, 0, 0)); + _method.CopyPixels(pDest, pSrc, copyCount); + } + + void Blt(NeoBufferContext destBuffer, + int16_t x, + int16_t y, + uint16_t indexSprite, + LayoutMapCallback layoutMap) + { + if (indexSprite >= _spriteCount) + { + return; + } + uint16_t destPixelCount = destBuffer.PixelCount(); + + for (int16_t srcY = 0; srcY < SpriteHeight(); srcY++) + { + for (int16_t srcX = 0; srcX < SpriteWidth(); srcX++) + { + uint16_t indexDest = layoutMap(srcX + x, srcY + y); + + if (indexDest < destPixelCount) + { + const uint8_t* pSrc = T_BUFFER_METHOD::ColorFeature::getPixelAddress(_method.Pixels(), pixelIndex(indexSprite, srcX, srcY)); + uint8_t* pDest = T_BUFFER_METHOD::ColorFeature::getPixelAddress(destBuffer.Pixels, indexDest); + + _method.CopyPixels(pDest, pSrc, 1); + } + } + } + + } + +private: + T_BUFFER_METHOD _method; + + const uint16_t _spriteHeight; + const uint16_t _spriteCount; + + uint16_t pixelIndex(uint16_t indexSprite, + int16_t x, + int16_t y) const + { + uint16_t result = PixelIndex_OutOfBounds; + + if (indexSprite < _spriteCount && + x >= 0 && + (uint16_t)x < SpriteWidth() && + y >= 0 && + (uint16_t)y < SpriteHeight()) + { + result = x + y * SpriteWidth() + indexSprite * _spriteHeight * SpriteWidth(); + } + return result; + } +}; \ No newline at end of file diff --git a/components/NeoPixelBus/src/internal/NeoTiles.h b/components/NeoPixelBus/src/internal/NeoTiles.h new file mode 100644 index 00000000..083ba6b9 --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoTiles.h @@ -0,0 +1,160 @@ +#pragma once + +#include + +/*------------------------------------------------------------------------- +NeoTiles provides a mapping feature of a 2d cordinate to linear 1d cordinate +It is used to map tiles of matricies of NeoPixels to a index on the NeoPixelBus +where the the matricies use one topology and the tiles of those matricies can +use another + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +//----------------------------------------------------------------------------- +// class NeoTiles +// Simple template Tile layout class +// T_MATRIX_LAYOUT = the layout used on the pixel matrix panel (a tile) +// T_TILE_LAYOUT = the layout used for the tiles. +// +//----------------------------------------------------------------------------- +template class NeoTiles +{ +public: + NeoTiles(uint16_t topoWidth, uint16_t topoHeight, + uint16_t tilesWidth, uint16_t tilesHeight) : + _topo(topoWidth, topoHeight), + _width(tilesWidth), + _height(tilesHeight) + { + } + + uint16_t Map(int16_t x, int16_t y) const + { + uint16_t totalWidth = getWidth(); + uint16_t totalHeight = getHeight(); + + if (x >= totalWidth) + { + x = totalWidth - 1; + } + else if (x < 0) + { + x = 0; + } + + if (y >= totalHeight) + { + y = totalHeight - 1; + } + else if (y < 0) + { + y = 0; + } + + uint16_t localIndex; + uint16_t tileOffset; + + calculate(x, y, &localIndex, &tileOffset); + + return localIndex + tileOffset; + } + + uint16_t MapProbe(int16_t x, int16_t y) const + { + uint16_t totalWidth = getWidth(); + uint16_t totalHeight = getHeight(); + + if (x < 0 || x >= totalWidth || y < 0 || y >= totalHeight) + { + return totalWidth * totalHeight; // count, out of bounds + } + + uint16_t localIndex; + uint16_t tileOffset; + + calculate(x, y, &localIndex, &tileOffset); + + return localIndex + tileOffset; + } + + NeoTopologyHint TopologyHint(int16_t x, int16_t y) const + { + uint16_t totalWidth = getWidth(); + uint16_t totalHeight = getHeight(); + + if (x < 0 || x >= totalWidth || y < 0 || y >= totalHeight) + { + return NeoTopologyHint_OutOfBounds; + } + + uint16_t localIndex; + uint16_t tileOffset; + NeoTopologyHint result; + + calculate(x, y, &localIndex, &tileOffset); + + if (localIndex == 0) + { + result = NeoTopologyHint_FirstOnPanel; + } + else if (localIndex == (_topo.getWidth() * _topo.getHeight() - 1)) + { + result = NeoTopologyHint_LastOnPanel; + } + else + { + result = NeoTopologyHint_InPanel; + } + + return result; + } + + uint16_t getWidth() const + { + return _width * _topo.getWidth(); + } + + uint16_t getHeight() const + { + return _height * _topo.getHeight(); + } + +private: + const NeoTopology _topo; + const uint16_t _width; + const uint16_t _height; + + void calculate(uint16_t x, uint16_t y, uint16_t* pLocalIndex, uint16_t* pTileOffset) const + { + uint16_t tileX = x / _topo.getWidth(); + uint16_t topoX = x % _topo.getWidth(); + + uint16_t tileY = y / _topo.getHeight(); + uint16_t topoY = y % _topo.getHeight(); + + *pTileOffset = T_TILE_LAYOUT::Map(_width, _height, tileX, tileY) * _topo.getWidth() * _topo.getHeight(); + *pLocalIndex = _topo.Map(topoX, topoY); + } +}; + diff --git a/components/NeoPixelBus/src/internal/NeoTopology.h b/components/NeoPixelBus/src/internal/NeoTopology.h new file mode 100644 index 00000000..2ac81af3 --- /dev/null +++ b/components/NeoPixelBus/src/internal/NeoTopology.h @@ -0,0 +1,93 @@ +#pragma once + +#include + +/*------------------------------------------------------------------------- +NeoTopology provides a mapping feature of a 2d cordinate to linear 1d cordinate +It is used to map a matrix of NeoPixels to a index on the NeoPixelBus + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +enum NeoTopologyHint +{ + NeoTopologyHint_FirstOnPanel, + NeoTopologyHint_InPanel, + NeoTopologyHint_LastOnPanel, + NeoTopologyHint_OutOfBounds +}; + +template class NeoTopology +{ +public: + NeoTopology(uint16_t width, uint16_t height) : + _width(width), + _height(height) + { + + } + + uint16_t Map(int16_t x, int16_t y) const + { + if (x >= _width) + { + x = _width - 1; + } + else if (x < 0) + { + x = 0; + } + if (y >= _height) + { + y = _height - 1; + } + else if (y < 0) + { + y = 0; + } + return T_LAYOUT::Map(_width, _height, x, y); + } + + uint16_t MapProbe(int16_t x, int16_t y) const + { + if (x < 0 || x >= _width || y < 0 || y >= _height) + { + return _width * _height; // count, out of bounds + } + return T_LAYOUT::Map(_width, _height, x, y); + } + + uint16_t getWidth() const + { + return _width; + } + + uint16_t getHeight() const + { + return _height; + } + +private: + const uint16_t _width; + const uint16_t _height; +}; diff --git a/components/NeoPixelBus/src/internal/RgbColor.cpp b/components/NeoPixelBus/src/internal/RgbColor.cpp new file mode 100644 index 00000000..67c2f2aa --- /dev/null +++ b/components/NeoPixelBus/src/internal/RgbColor.cpp @@ -0,0 +1,237 @@ +/*------------------------------------------------------------------------- +RgbColor provides a color object that can be directly consumed by NeoPixelBus + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +#include "RgbColor.h" +#include "HslColor.h" +#include "HsbColor.h" + +static float _CalcColor(float p, float q, float t) +{ + if (t < 0.0f) + t += 1.0f; + if (t > 1.0f) + t -= 1.0f; + + if (t < 1.0f / 6.0f) + return p + (q - p) * 6.0f * t; + + if (t < 0.5f) + return q; + + if (t < 2.0f / 3.0f) + return p + ((q - p) * (2.0f / 3.0f - t) * 6.0f); + + return p; +} + +RgbColor::RgbColor(const HslColor& color) +{ + float r; + float g; + float b; + + float h = color.H; + float s = color.S; + float l = color.L; + + + if (color.S == 0.0f || color.L == 0.0f) + { + r = g = b = l; // achromatic or black + } + else + { + float q = l < 0.5f ? l * (1.0f + s) : l + s - (l * s); + float p = 2.0f * l - q; + r = _CalcColor(p, q, h + 1.0f / 3.0f); + g = _CalcColor(p, q, h); + b = _CalcColor(p, q, h - 1.0f / 3.0f); + } + + R = (uint8_t)(r * 255.0f); + G = (uint8_t)(g * 255.0f); + B = (uint8_t)(b * 255.0f); +} + +RgbColor::RgbColor(const HsbColor& color) +{ + float r; + float g; + float b; + + float h = color.H; + float s = color.S; + float v = color.B; + + if (color.S == 0.0f) + { + r = g = b = v; // achromatic or black + } + else + { + if (h < 0.0f) + { + h += 1.0f; + } + else if (h >= 1.0f) + { + h -= 1.0f; + } + h *= 6.0f; + int i = (int)h; + float f = h - i; + float q = v * (1.0f - s * f); + float p = v * (1.0f - s); + float t = v * (1.0f - s * (1.0f - f)); + switch (i) + { + case 0: + r = v; + g = t; + b = p; + break; + case 1: + r = q; + g = v; + b = p; + break; + case 2: + r = p; + g = v; + b = t; + break; + case 3: + r = p; + g = q; + b = v; + break; + case 4: + r = t; + g = p; + b = v; + break; + default: + r = v; + g = p; + b = q; + break; + } + } + + R = (uint8_t)(r * 255.0f); + G = (uint8_t)(g * 255.0f); + B = (uint8_t)(b * 255.0f); +} + +uint8_t RgbColor::CalculateBrightness() const +{ + return (uint8_t)(((uint16_t)R + (uint16_t)G + (uint16_t)B) / 3); +} + +void RgbColor::Darken(uint8_t delta) +{ + if (R > delta) + { + R -= delta; + } + else + { + R = 0; + } + + if (G > delta) + { + G -= delta; + } + else + { + G = 0; + } + + if (B > delta) + { + B -= delta; + } + else + { + B = 0; + } +} + +void RgbColor::Lighten(uint8_t delta) +{ + if (R < 255 - delta) + { + R += delta; + } + else + { + R = 255; + } + + if (G < 255 - delta) + { + G += delta; + } + else + { + G = 255; + } + + if (B < 255 - delta) + { + B += delta; + } + else + { + B = 255; + } +} + +RgbColor RgbColor::LinearBlend(const RgbColor& left, const RgbColor& right, float progress) +{ + return RgbColor( left.R + ((right.R - left.R) * progress), + left.G + ((right.G - left.G) * progress), + left.B + ((right.B - left.B) * progress)); +} + +RgbColor RgbColor::BilinearBlend(const RgbColor& c00, + const RgbColor& c01, + const RgbColor& c10, + const RgbColor& c11, + float x, + float y) +{ + float v00 = (1.0f - x) * (1.0f - y); + float v10 = x * (1.0f - y); + float v01 = (1.0f - x) * y; + float v11 = x * y; + + return RgbColor( + c00.R * v00 + c10.R * v10 + c01.R * v01 + c11.R * v11, + c00.G * v00 + c10.G * v10 + c01.G * v01 + c11.G * v11, + c00.B * v00 + c10.B * v10 + c01.B * v01 + c11.B * v11); +} \ No newline at end of file diff --git a/components/NeoPixelBus/src/internal/RgbColor.h b/components/NeoPixelBus/src/internal/RgbColor.h new file mode 100644 index 00000000..15715288 --- /dev/null +++ b/components/NeoPixelBus/src/internal/RgbColor.h @@ -0,0 +1,142 @@ +/*------------------------------------------------------------------------- +RgbColor provides a color object that can be directly consumed by NeoPixelBus + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +#include + +struct HslColor; +struct HsbColor; + +// ------------------------------------------------------------------------ +// RgbColor represents a color object that is represented by Red, Green, Blue +// component values. It contains helpful color routines to manipulate the +// color. +// ------------------------------------------------------------------------ +struct RgbColor +{ + // ------------------------------------------------------------------------ + // Construct a RgbColor using R, G, B values (0-255) + // ------------------------------------------------------------------------ + RgbColor(uint8_t r, uint8_t g, uint8_t b) : + R(r), G(g), B(b) + { + }; + + // ------------------------------------------------------------------------ + // Construct a RgbColor using a single brightness value (0-255) + // This works well for creating gray tone colors + // (0) = black, (255) = white, (128) = gray + // ------------------------------------------------------------------------ + RgbColor(uint8_t brightness) : + R(brightness), G(brightness), B(brightness) + { + }; + + // ------------------------------------------------------------------------ + // Construct a RgbColor using HslColor + // ------------------------------------------------------------------------ + RgbColor(const HslColor& color); + + // ------------------------------------------------------------------------ + // Construct a RgbColor using HsbColor + // ------------------------------------------------------------------------ + RgbColor(const HsbColor& color); + + // ------------------------------------------------------------------------ + // Construct a RgbColor that will have its values set in latter operations + // CAUTION: The R,G,B members are not initialized and may not be consistent + // ------------------------------------------------------------------------ + RgbColor() + { + }; + + // ------------------------------------------------------------------------ + // Comparison operators + // ------------------------------------------------------------------------ + bool operator==(const RgbColor& other) const + { + return (R == other.R && G == other.G && B == other.B); + }; + + bool operator!=(const RgbColor& other) const + { + return !(*this == other); + }; + + // ------------------------------------------------------------------------ + // CalculateBrightness will calculate the overall brightness + // NOTE: This is a simple linear brightness + // ------------------------------------------------------------------------ + uint8_t CalculateBrightness() const; + + // ------------------------------------------------------------------------ + // Darken will adjust the color by the given delta toward black + // NOTE: This is a simple linear change + // delta - (0-255) the amount to dim the color + // ------------------------------------------------------------------------ + void Darken(uint8_t delta); + + // ------------------------------------------------------------------------ + // Lighten will adjust the color by the given delta toward white + // NOTE: This is a simple linear change + // delta - (0-255) the amount to lighten the color + // ------------------------------------------------------------------------ + void Lighten(uint8_t delta); + + // ------------------------------------------------------------------------ + // LinearBlend between two colors by the amount defined by progress variable + // left - the color to start the blend at + // right - the color to end the blend at + // progress - (0.0 - 1.0) value where 0 will return left and 1.0 will return right + // and a value between will blend the color weighted linearly between them + // ------------------------------------------------------------------------ + static RgbColor LinearBlend(const RgbColor& left, const RgbColor& right, float progress); + + // ------------------------------------------------------------------------ + // BilinearBlend between four colors by the amount defined by 2d variable + // c00 - upper left quadrant color + // c01 - upper right quadrant color + // c10 - lower left quadrant color + // c11 - lower right quadrant color + // x - unit value (0.0 - 1.0) that defines the blend progress in horizontal space + // y - unit value (0.0 - 1.0) that defines the blend progress in vertical space + // ------------------------------------------------------------------------ + static RgbColor BilinearBlend(const RgbColor& c00, + const RgbColor& c01, + const RgbColor& c10, + const RgbColor& c11, + float x, + float y); + + // ------------------------------------------------------------------------ + // Red, Green, Blue color members (0-255) where + // (0,0,0) is black and (255,255,255) is white + // ------------------------------------------------------------------------ + uint8_t R; + uint8_t G; + uint8_t B; +}; + diff --git a/components/NeoPixelBus/src/internal/RgbwColor.cpp b/components/NeoPixelBus/src/internal/RgbwColor.cpp new file mode 100644 index 00000000..a1ee31ef --- /dev/null +++ b/components/NeoPixelBus/src/internal/RgbwColor.cpp @@ -0,0 +1,165 @@ +/*------------------------------------------------------------------------- +RgbwColor provides a color object that can be directly consumed by NeoPixelBus + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ + +#include "RgbColor.h" +#include "HslColor.h" +#include "HsbColor.h" +#include "RgbwColor.h" + +RgbwColor::RgbwColor(const HslColor& color) +{ + RgbColor rgbColor(color); + *this = rgbColor; +} + +RgbwColor::RgbwColor(const HsbColor& color) +{ + RgbColor rgbColor(color); + *this = rgbColor; +} + +uint8_t RgbwColor::CalculateBrightness() const +{ + uint8_t colorB = (uint8_t)(((uint16_t)R + (uint16_t)G + (uint16_t)B) / 3); + if (W > colorB) + { + return W; + } + else + { + return colorB; + } +} + +void RgbwColor::Darken(uint8_t delta) +{ + if (R > delta) + { + R -= delta; + } + else + { + R = 0; + } + + if (G > delta) + { + G -= delta; + } + else + { + G = 0; + } + + if (B > delta) + { + B -= delta; + } + else + { + B = 0; + } + + if (W > delta) + { + W -= delta; + } + else + { + W = 0; + } +} + +void RgbwColor::Lighten(uint8_t delta) +{ + if (IsColorLess()) + { + if (W < 255 - delta) + { + W += delta; + } + else + { + W = 255; + } + } + else + { + if (R < 255 - delta) + { + R += delta; + } + else + { + R = 255; + } + + if (G < 255 - delta) + { + G += delta; + } + else + { + G = 255; + } + + if (B < 255 - delta) + { + B += delta; + } + else + { + B = 255; + } + } +} + +RgbwColor RgbwColor::LinearBlend(const RgbwColor& left, const RgbwColor& right, float progress) +{ + return RgbwColor( left.R + ((right.R - left.R) * progress), + left.G + ((right.G - left.G) * progress), + left.B + ((right.B - left.B) * progress), + left.W + ((right.W - left.W) * progress) ); +} + +RgbwColor RgbwColor::BilinearBlend(const RgbwColor& c00, + const RgbwColor& c01, + const RgbwColor& c10, + const RgbwColor& c11, + float x, + float y) +{ + float v00 = (1.0f - x) * (1.0f - y); + float v10 = x * (1.0f - y); + float v01 = (1.0f - x) * y; + float v11 = x * y; + + return RgbwColor( + c00.R * v00 + c10.R * v10 + c01.R * v01 + c11.R * v11, + c00.G * v00 + c10.G * v10 + c01.G * v01 + c11.G * v11, + c00.B * v00 + c10.B * v10 + c01.B * v01 + c11.B * v11, + c00.W * v00 + c10.W * v10 + c01.W * v01 + c11.W * v11 ); +} \ No newline at end of file diff --git a/components/NeoPixelBus/src/internal/RgbwColor.h b/components/NeoPixelBus/src/internal/RgbwColor.h new file mode 100644 index 00000000..da0f1fe6 --- /dev/null +++ b/components/NeoPixelBus/src/internal/RgbwColor.h @@ -0,0 +1,173 @@ +/*------------------------------------------------------------------------- +RgbwColor provides a color object that can be directly consumed by NeoPixelBus + +Written by Michael C. Miller. + +I invest time and resources providing this open source code, +please support me by dontating (see https://github.com/Makuna/NeoPixelBus) + +------------------------------------------------------------------------- +This file is part of the Makuna/NeoPixelBus library. + +NeoPixelBus is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +NeoPixelBus is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with NeoPixel. If not, see +. +-------------------------------------------------------------------------*/ +#pragma once + +#include + +struct RgbColor; +struct HslColor; +struct HsbColor; + +// ------------------------------------------------------------------------ +// RgbwColor represents a color object that is represented by Red, Green, Blue +// component values and an extra White component. It contains helpful color +// routines to manipulate the color. +// ------------------------------------------------------------------------ +struct RgbwColor +{ + // ------------------------------------------------------------------------ + // Construct a RgbwColor using R, G, B, W values (0-255) + // ------------------------------------------------------------------------ + RgbwColor(uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) : + R(r), G(g), B(b), W(w) + { + }; + + // ------------------------------------------------------------------------ + // Construct a RgbColor using a single brightness value (0-255) + // This works well for creating gray tone colors + // (0) = black, (255) = white, (128) = gray + // ------------------------------------------------------------------------ + RgbwColor(uint8_t brightness) : + R(0), G(0), B(0), W(brightness) + { + }; + + // ------------------------------------------------------------------------ + // Construct a RgbwColor using RgbColor + // ------------------------------------------------------------------------ + RgbwColor(const RgbColor& color) : + R(color.R), + G(color.G), + B(color.B), + W(0) + { + }; + + // ------------------------------------------------------------------------ + // Construct a RgbwColor using HslColor + // ------------------------------------------------------------------------ + RgbwColor(const HslColor& color); + + // ------------------------------------------------------------------------ + // Construct a RgbwColor using HsbColor + // ------------------------------------------------------------------------ + RgbwColor(const HsbColor& color); + + // ------------------------------------------------------------------------ + // Construct a RgbwColor that will have its values set in latter operations + // CAUTION: The R,G,B, W members are not initialized and may not be consistent + // ------------------------------------------------------------------------ + RgbwColor() + { + }; + + // ------------------------------------------------------------------------ + // Comparison operators + // ------------------------------------------------------------------------ + bool operator==(const RgbwColor& other) const + { + return (R == other.R && G == other.G && B == other.B && W == other.W); + }; + + bool operator!=(const RgbwColor& other) const + { + return !(*this == other); + }; + + // ------------------------------------------------------------------------ + // Returns if the color is grey, all values are equal other than white + // ------------------------------------------------------------------------ + bool IsMonotone() const + { + return (R == B && R == G); + }; + + // ------------------------------------------------------------------------ + // Returns if the color components are all zero, the white component maybe + // anything + // ------------------------------------------------------------------------ + bool IsColorLess() const + { + return (R == 0 && B == 0 && G == 0); + }; + + // ------------------------------------------------------------------------ + // CalculateBrightness will calculate the overall brightness + // NOTE: This is a simple linear brightness + // ------------------------------------------------------------------------ + uint8_t CalculateBrightness() const; + + // ------------------------------------------------------------------------ + // Darken will adjust the color by the given delta toward black + // NOTE: This is a simple linear change + // delta - (0-255) the amount to dim the color + // ------------------------------------------------------------------------ + void Darken(uint8_t delta); + + // ------------------------------------------------------------------------ + // Lighten will adjust the color by the given delta toward white + // NOTE: This is a simple linear change + // delta - (0-255) the amount to lighten the color + // ------------------------------------------------------------------------ + void Lighten(uint8_t delta); + + // ------------------------------------------------------------------------ + // LinearBlend between two colors by the amount defined by progress variable + // left - the color to start the blend at + // right - the color to end the blend at + // progress - (0.0 - 1.0) value where 0 will return left and 1.0 will return right + // and a value between will blend the color weighted linearly between them + // ------------------------------------------------------------------------ + static RgbwColor LinearBlend(const RgbwColor& left, const RgbwColor& right, float progress); + + // ------------------------------------------------------------------------ + // BilinearBlend between four colors by the amount defined by 2d variable + // c00 - upper left quadrant color + // c01 - upper right quadrant color + // c10 - lower left quadrant color + // c11 - lower right quadrant color + // x - unit value (0.0 - 1.0) that defines the blend progress in horizontal space + // y - unit value (0.0 - 1.0) that defines the blend progress in vertical space + // ------------------------------------------------------------------------ + static RgbwColor BilinearBlend(const RgbwColor& c00, + const RgbwColor& c01, + const RgbwColor& c10, + const RgbwColor& c11, + float x, + float y); + + // ------------------------------------------------------------------------ + // Red, Green, Blue, White color members (0-255) where + // (0,0,0,0) is black and (255,255,255, 0) and (0,0,0,255) is white + // Note (255,255,255,255) is extreme bright white + // ------------------------------------------------------------------------ + uint8_t R; + uint8_t G; + uint8_t B; + uint8_t W; +}; + diff --git a/components/OpenMRNLite/CMakeLists.txt b/components/OpenMRNLite/CMakeLists.txt new file mode 100644 index 00000000..d4462bc7 --- /dev/null +++ b/components/OpenMRNLite/CMakeLists.txt @@ -0,0 +1,81 @@ +set(src_dirs + "src/dcc" + "src/executor" + "src/freertos_drivers/esp32" + "src/freertos_drivers/arduino" + "src/openlcb" + "src/os" + "src/utils" +) + +if(CONFIG_ENABLE_ARDUINO_DEPENDS) + list(APPEND src_dirs "src") +endif() + +set(COMPONENT_SRCDIRS "${src_dirs}" ) + +set(COMPONENT_ADD_INCLUDEDIRS "src" ) + +set(COMPONENT_REQUIRES "mdns" ) + +register_component() + +############################################################################### +# Add required compilation flags for customization of OpenMRNLite +############################################################################### + +target_compile_options(${COMPONENT_LIB} PUBLIC -DESP32 -DLOCKED_LOGGING) + +set_source_files_properties(src/utils/FileUtils.cpp PROPERTIES COMPILE_FLAGS -Wno-type-limits) +set_source_files_properties(src/dcc/Loco.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/dcc/SimpleUpdateLoop.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/dcc/UpdateLoop.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/executor/Executor.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/executor/StateFlow.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/executor/Service.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/executor/Timer.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/freertos_drivers/arduino/CpuLoad.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/freertos_drivers/esp32/Esp32WiFiManager.cpp PROPERTIES COMPILE_FLAGS + "-Wno-ignored-qualifiers -DESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL=VERBOSE -DESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL=VERBOSE") +set_source_files_properties(src/openlcb/AliasAllocator.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/BroadcastTime.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/BroadcastTimeClient.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/BroadcastTimeServer.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/ConfigEntry.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/ConfigUpdateFlow.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/DccAccyProducer.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/Datagram.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/DatagramCan.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/DatagramTcp.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/DefaultNode.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/EventHandler.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/EventHandlerContainer.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/EventHandlerTemplates.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/EventService.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/If.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/IfCan.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/IfImpl.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/IfTcp.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/MemoryConfig.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/NodeBrowser.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/NodeInitializeFlow.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/NonAuthoritativeEventProducer.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/PIPClient.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/RoutingLogic.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/SimpleNodeInfo.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/SimpleNodeInfoMockUserFile.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/SimpleStack.cpp PROPERTIES COMPILE_FLAGS "-Wno-ignored-qualifiers -Wno-implicit-fallthrough") +set_source_files_properties(src/openlcb/TractionCvSpace.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/TractionDefs.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/TractionProxy.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/TractionTestTrain.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/TractionThrottle.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/TractionTrain.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/openlcb/WriteHelper.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/utils/CanIf.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/utils/GcTcpHub.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/utils/GridConnectHub.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/utils/HubDevice.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/utils/HubDeviceSelect.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/utils/SocketClient.cpp PROPERTIES COMPILE_FLAGS -Wno-ignored-qualifiers) +set_source_files_properties(src/utils/socket_listener.cpp PROPERTIES COMPILE_FLAGS -DSOCKET_LISTENER_CONNECT_LOG_LEVEL=VERBOSE) diff --git a/lib/OpenMRNLite/CONTRIBUTING.md b/components/OpenMRNLite/CONTRIBUTING.md similarity index 100% rename from lib/OpenMRNLite/CONTRIBUTING.md rename to components/OpenMRNLite/CONTRIBUTING.md diff --git a/lib/OpenMRNLite/LICENSE b/components/OpenMRNLite/LICENSE similarity index 100% rename from lib/OpenMRNLite/LICENSE rename to components/OpenMRNLite/LICENSE diff --git a/lib/OpenMRNLite/README.md b/components/OpenMRNLite/README.md similarity index 76% rename from lib/OpenMRNLite/README.md rename to components/OpenMRNLite/README.md index 8ff90070..39308cba 100644 --- a/lib/OpenMRNLite/README.md +++ b/components/OpenMRNLite/README.md @@ -34,41 +34,27 @@ For the hardware CAN support they will require two additional GPIO pins. WiFi does not require any additional GPIO pins. ## ESP32 WiFi support -The ESP32 WiFi stack has issues at times but is generally stable. If you -observe failures in connecting to WiFi add the following compiler option -to turn on additional diagnostic output from the -[WiFi](https://github.com/espressif/arduino-esp32/tree/master/libraries/WiFi) -library: - `-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG` -This will give additional output on the Serial console which can help -to resolve the connection issue. The following is an example of the output -which may be observed: -``` - [D][WiFiGeneric.cpp:342] _eventCallback(): Event: 5 - STA_DISCONNECTED - [W][WiFiGeneric.cpp:357] _eventCallback(): Reason: 2 - AUTH_EXPIRE - [D][WiFiGeneric.cpp:342] _eventCallback(): Event: 0 - WIFI_READY - [D][WiFiGeneric.cpp:342] _eventCallback(): Event: 2 - STA_START - [D][WiFiGeneric.cpp:342] _eventCallback(): Event: 2 - STA_START - [D][WiFiGeneric.cpp:342] _eventCallback(): Event: 5 - STA_DISCONNECTED -``` - -If you observe this output, this generally means there was a timeout condition -where the ESP32 did not receive a response from the access point. It generally -means that the ESP32 is too far from the AP for the AP to hear what the ESP32 -is transmitting. Additional options to try if this does not resolve the -connection issues: -1. If the ESP32 board supports an external WiFi antenna use one, this will -provide a higher signal strength which should allow a more successful -connection. -2. Clear the persistent WiFi connection details from NVS: -```C - #include - nvs_flash_init(); -``` - -This should not be done very often (i.e. do not do it at every startup!), but -is otherwise harmless. The same can also be achieved by using a flash erase -tool `esptool.py erase_flash ...` and reflashing the ESP32. +The Esp32WiFiManager should be used to manage the ESP32's WiFi connection to +the SSID, create a Soft AP (max of 4 clients connected to it) or to create both +a Soft AP and connect to an SSID. See the ESP32IOBoard example for how to use +the Esp32WiFiManager for how to connect to an SSID and have it automatically +establish an uplink to a GridConnect based TCP/IP Hub. Additional configuration +parameters can be configured via the CDI interface. + +## Using an SD card for configuration data +When using an SD card for storage of the OpenMRN configuration data it is +recommended to use an AutoSyncFileFlow to ensure the OpenMRN configuration +data is persisted to the SD card. The default configuration of the SD virtual +file system driver is to use a 512 byte per-file cache and only persist +on-demand or when reading/writing outside this cached space. The +AutoSyncFileFlow will ensure the configuration file is synchronized to the SD +card on a regular basis. The SD VFS driver will check that the file has pending +changes before synchronizing them to the SD card and when no changes are +necessary no action is taken by the synchronization call. + +Note that the Arduino-esp32 SPIFFS library and the underlying SPIFFS VFS driver +does use a cache but the AutoSyncFileFlow is not necessary due to the nature of +the SPIFFS file system (monolithic blob containing all files concatenated). ## ESP32 Hardware CAN support The ESP32 has a built in CAN controller and needs an external CAN transceiver diff --git a/lib/OpenMRNLite/keywords.txt b/components/OpenMRNLite/keywords.txt similarity index 100% rename from lib/OpenMRNLite/keywords.txt rename to components/OpenMRNLite/keywords.txt diff --git a/lib/OpenMRNLite/library.json b/components/OpenMRNLite/library.json similarity index 100% rename from lib/OpenMRNLite/library.json rename to components/OpenMRNLite/library.json diff --git a/lib/OpenMRNLite/library.properties b/components/OpenMRNLite/library.properties similarity index 100% rename from lib/OpenMRNLite/library.properties rename to components/OpenMRNLite/library.properties diff --git a/lib/OpenMRNLite/src/OpenMRNLite.cpp b/components/OpenMRNLite/src/OpenMRNLite.cpp similarity index 92% rename from lib/OpenMRNLite/src/OpenMRNLite.cpp rename to components/OpenMRNLite/src/OpenMRNLite.cpp index 6749c43c..497ad489 100644 --- a/lib/OpenMRNLite/src/OpenMRNLite.cpp +++ b/components/OpenMRNLite/src/OpenMRNLite.cpp @@ -44,16 +44,20 @@ OpenMRN::OpenMRN(openlcb::NodeID node_id) #ifdef ESP32 extern "C" { +#ifndef OPENMRN_EXCLUDE_REBOOT_IMPL /// Reboots the ESP32 via the arduino-esp32 provided restart function. void reboot() { ESP.restart(); } +#endif // OPENMRN_EXCLUDE_REBOOT_IMPL +#ifndef OPENMRN_EXCLUDE_FREE_HEAP_IMPL ssize_t os_get_free_heap() { return ESP.getFreeHeap(); } +#endif // OPENMRN_EXCLUDE_FREE_HEAP_IMPL } #endif // ESP32 diff --git a/lib/OpenMRNLite/src/OpenMRNLite.h b/components/OpenMRNLite/src/OpenMRNLite.h similarity index 85% rename from lib/OpenMRNLite/src/OpenMRNLite.h rename to components/OpenMRNLite/src/OpenMRNLite.h index 4842d0ec..e46c3265 100644 --- a/lib/OpenMRNLite/src/OpenMRNLite.h +++ b/components/OpenMRNLite/src/OpenMRNLite.h @@ -42,9 +42,10 @@ #include "freertos_drivers/arduino/Can.hxx" #include "freertos_drivers/arduino/WifiDefs.hxx" #include "openlcb/SimpleStack.hxx" +#include "utils/FileUtils.hxx" #include "utils/GridConnectHub.hxx" +#include "utils/logging.h" #include "utils/Uninitialized.hxx" -#include "utils/FileUtils.hxx" #if defined(ESP32) @@ -56,11 +57,13 @@ namespace openmrn_arduino { /// Default stack size to use for all OpenMRN tasks on the ESP32 platform. constexpr uint32_t OPENMRN_STACK_SIZE = 4096L; -/// Default thread priority for any OpenMRN owned tasks on the ESP32 -/// platform. ESP32 hardware CAN RX and TX tasks run at lower priority -/// (-1 and -2 respectively) of this default priority to ensure timely -/// consumption of CAN frames from the hardware driver. -constexpr UBaseType_t OPENMRN_TASK_PRIORITY = ESP_TASK_TCPIP_PRIO; +/// Default thread priority for any OpenMRN owned tasks on the ESP32 platform. +/// ESP32 hardware CAN RX and TX tasks run at lower priority (-1 and -2 +/// respectively) of this default priority to ensure timely consumption of CAN +/// frames from the hardware driver. +/// Note: This is set to one priority level lower than the TCP/IP task uses on +/// the ESP32. +constexpr UBaseType_t OPENMRN_TASK_PRIORITY = ESP_TASK_TCPIP_PRIO - 1; } // namespace openmrn_arduino @@ -74,6 +77,12 @@ constexpr UBaseType_t OPENMRN_TASK_PRIORITY = ESP_TASK_TCPIP_PRIO; #endif // ESP32 +#ifdef ARDUINO_ARCH_STM32 + +#include "freertos_drivers/stm32/Stm32Can.hxx" + +#endif + namespace openmrn_arduino { /// Bridge class that connects an Arduino API style serial port (sending CAN @@ -121,7 +130,7 @@ template class SerialBridge : public Executable size_t to_write = writeBuffer_->data()->size() - writeOfs_; if (len > to_write) len = to_write; - port_->write(writeBuffer_->data()->data() + writeOfs_, len); + port_->write((const uint8_t*)writeBuffer_->data()->data() + writeOfs_, len); writeOfs_ += len; if (writeOfs_ >= writeBuffer_->data()->size()) { @@ -143,7 +152,7 @@ template class SerialBridge : public Executable auto *b = txtHub_.alloc(); b->data()->skipMember_ = &writePort_; b->data()->resize(av); - port_->read(b->data()->data(), b->data()->size()); + port_->readBytes((char*)b->data()->data(), b->data()->size()); txtHub_.send(b); } @@ -343,36 +352,69 @@ class OpenMRN : private Executable { for (auto *e : loopMembers_) { -#if defined(ESP32) +#if defined(ESP32) && CONFIG_TASK_WDT // Feed the watchdog so it doesn't reset the ESP32 esp_task_wdt_reset(); -#endif // ESP32 +#endif // ESP32 && CONFIG_TASK_WDT e->run(); } } #ifndef OPENMRN_FEATURE_SINGLE_THREADED + /// Entry point for the executor thread when @ref start_executor_thread is + /// called with donate_current_thread set to false. static void thread_entry(void *arg) { OpenMRN *p = (OpenMRN *)arg; - p->stack()->executor()->thread_body(); + p->loop_executor(); + } + + /// Donates the calling thread to the @ref Executor. + /// + /// Note: this method will not return until the @ref Executor has shutdown. + void loop_executor() + { +#if defined(ESP32) && CONFIG_TASK_WDT + uint32_t current_core = xPortGetCoreID(); + TaskHandle_t idleTask = xTaskGetIdleTaskHandleForCPU(current_core); + // check if watchdog is enabled and print a warning if it is + if (esp_task_wdt_status(idleTask) == ESP_OK) + { + LOG(WARNING, "WDT detected as enabled on core %d!", current_core); + } +#endif // ESP32 && CONFIG_TASK_WDT + haveExecutorThread_ = true; + + // donate this thread to the executor + stack_->executor()->thread_body(); } + /// Starts a thread for the @ref Executor used by OpenMRN. + /// + /// Note: On the ESP32 the watchdog timer is disabled for the PRO_CPU prior + /// to starting the background task for the @ref Executor. void start_executor_thread() { haveExecutorThread_ = true; #ifdef ESP32 - xTaskCreatePinnedToCore(&thread_entry, "OpenMRN", OPENMRN_STACK_SIZE, - this, OPENMRN_TASK_PRIORITY, nullptr, 0); - // Remove IDLE0 task watchdog, because the openmrn task sometimes uses - // 100% cpu and it is pinned to CPU 0. +#if CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0 + // Remove IDLE0 task watchdog, because the openmrn task sometimes + // uses 100% cpu and it is pinned to CPU 0. disableCore0WDT(); +#endif // CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0 + xTaskCreatePinnedToCore(&thread_entry // entry point + , "OpenMRN" // task name + , OPENMRN_STACK_SIZE // stack size + , this // entry point arg + , OPENMRN_TASK_PRIORITY // priority + , nullptr // task handle + , PRO_CPU_NUM); // cpu core #else stack_->executor()->start_thread( "OpenMRN", OPENMRN_TASK_PRIORITY, OPENMRN_STACK_SIZE); -#endif +#endif // ESP32 } -#endif +#endif // OPENMRN_FEATURE_SINGLE_THREADED /// Adds a serial port to the stack speaking the gridconnect protocol, for /// example to do a USB connection to a computer. This is the protocol that @@ -426,6 +468,8 @@ class OpenMRN : private Executable string cdi_string; ConfigDef cfg(config.offset()); cfg.config_renderer().render_cdi(&cdi_string); + + cdi_string += '\0'; bool need_write = false; FILE *ff = fopen(filename, "rb"); diff --git a/lib/OpenMRNLite/src/can_frame.h b/components/OpenMRNLite/src/can_frame.h similarity index 78% rename from lib/OpenMRNLite/src/can_frame.h rename to components/OpenMRNLite/src/can_frame.h index 3c57268b..6b6cc156 100644 --- a/lib/OpenMRNLite/src/can_frame.h +++ b/components/OpenMRNLite/src/can_frame.h @@ -63,29 +63,38 @@ (_frame).can_id += ((_value) & CAN_SFF_MASK); \ } -#elif defined (__nuttx__) || defined (__FreeRTOS__) || defined (__MACH__) || defined (__WIN32__) || defined(__EMSCRIPTEN__) || defined(ESP_NONOS) || defined(ARDUINO) +#elif defined (__nuttx__) || defined (__FreeRTOS__) || defined (__MACH__) || \ + defined (__WIN32__) || defined (__EMSCRIPTEN__) || \ + defined (ESP_NONOS) || defined (ARDUINO) || defined (ESP32) #include struct can_frame { - uint32_t can_id; /**< 11- or 29-bit ID (3-bits unsed) */ + union + { + uint32_t raw[4]; + struct + { + uint32_t can_id; /**< 11- or 29-bit ID (3-bits unsed) */ - uint8_t can_dlc : 4; /**< 4-bit DLC */ - uint8_t can_rtr : 1; /**< RTR indication */ - uint8_t can_eff : 1; /**< Extended ID indication */ - uint8_t can_err : 1; /**< @todo not supported by nuttx */ - uint8_t can_res : 1; /**< Unused */ + uint8_t can_dlc : 4; /**< 4-bit DLC */ + uint8_t can_rtr : 1; /**< RTR indication */ + uint8_t can_eff : 1; /**< Extended ID indication */ + uint8_t can_err : 1; /**< @todo not supported by nuttx */ + uint8_t can_res : 1; /**< Unused */ - uint8_t pad; /**< padding */ - uint8_t res0; /**< reserved */ - uint8_t res1; /**< reserved */ + uint8_t pad; /**< padding */ + uint8_t res0; /**< reserved */ + uint8_t res1; /**< reserved */ - union - { - /** CAN message data (64-bit) */ - uint64_t data64 __attribute__((aligned(8))); - /** CAN message data (0-8 byte) */ - uint8_t data[8] __attribute__((aligned(8))); + union + { + /** CAN message data (64-bit) */ + uint64_t data64 __attribute__((aligned(8))); + /** CAN message data (0-8 byte) */ + uint8_t data[8] __attribute__((aligned(8))); + }; + }; }; }; diff --git a/lib/OpenMRNLite/src/dcc/Address.hxx b/components/OpenMRNLite/src/dcc/Address.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/Address.hxx rename to components/OpenMRNLite/src/dcc/Address.hxx diff --git a/components/OpenMRNLite/src/dcc/DccDebug.cpp b/components/OpenMRNLite/src/dcc/DccDebug.cpp new file mode 100644 index 00000000..7564b437 --- /dev/null +++ b/components/OpenMRNLite/src/dcc/DccDebug.cpp @@ -0,0 +1,262 @@ +/** \copyright + * Copyright (c) 2017, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file DccDebug.cxx + * + * Defines helper functions for debugging DCC packets + * + * @author Balazs Racz + * @date 28 Dec 2017 + */ + +#include "dcc/DccDebug.hxx" +#include "utils/StringPrintf.hxx" + +namespace dcc +{ + +string packet_to_string(const DCCPacket &pkt, bool bin_payload) +{ + if (pkt.packet_header.is_pkt) + { + return StringPrintf("[cmd:%u]", pkt.command_header.cmd); + } + // First render the option bits + string options; + if (pkt.packet_header.is_marklin) + { + options += "[marklin]"; + } + else + { + options += "[dcc]"; + } + if (pkt.packet_header.send_long_preamble) + { + options += "[long_preamble]"; + } + if (pkt.packet_header.sense_ack) + { + options += "[sense_ack]"; + } + if (pkt.packet_header.rept_count) + { + options += + StringPrintf("[repeat %u times]", pkt.packet_header.rept_count + 1); + } + if (!pkt.dlc) + { + return options + " no payload"; + } + if (bin_payload || pkt.packet_header.is_marklin) + { + options += "["; + for (unsigned i = 0; i < pkt.dlc; ++i) + { + options += StringPrintf("%02x ", pkt.payload[i]); + } + options.pop_back(); + options += "]"; + } + if (pkt.packet_header.is_marklin) { + return options; + } + unsigned ofs = 0; + bool is_idle_packet = false; + bool is_basic_accy_packet = false; + unsigned accy_address = 0; + if (pkt.payload[ofs] == 0xff) + { + options += " Idle packet"; + ofs++; + if (pkt.payload[ofs] != 0) + { + options += StringPrintf(" unexpected[0x%02x]", pkt.payload[ofs]); + } + is_idle_packet = true; + } + else if (pkt.payload[ofs] == 0) + { + options += " Broadcast"; + ofs++; + } + else if ((pkt.payload[ofs] & 0x80) == 0) + { + options += StringPrintf(" Short Address %u", pkt.payload[ofs]); + ofs++; + } + else if ((pkt.payload[ofs] & 0xC0) == 0x80) + { + // accessory decoder + is_basic_accy_packet = true; + accy_address = (pkt.payload[ofs] & 0b111111) << 3; + ofs++; + } + else if (pkt.payload[ofs] >= 192 && pkt.payload[ofs] <= 231) + { + // long address + unsigned addr = pkt.payload[ofs] & 0x3F; + addr <<= 8; + ofs++; + addr |= pkt.payload[ofs]; + ofs++; + options += StringPrintf(" Long Address %u", addr); + } + uint8_t cmd = pkt.payload[ofs]; + ofs++; + if (is_basic_accy_packet && ((cmd & 0x80) == 0x80)) + { + accy_address |= cmd & 0b111; + cmd >>= 3; + bool is_activate = cmd & 1; + cmd >>= 1; + accy_address |= ((~cmd) & 0b111) << 9; + options += StringPrintf(" Accy %u %s", accy_address, + is_activate ? "activate" : "deactivate"); + } + else if ((cmd & 0xC0) == 0x40) + { + // Speed and direction + bool is_forward = (cmd & 0x20) != 0; + options += " SPD "; + options += is_forward ? 'F' : 'R'; + uint8_t speed = ((cmd & 0xF) << 1) | ((cmd & 0x10) >> 4); + switch (speed) + { + case 0: + options += " 0"; + break; + case 1: + options += " 0'"; + break; + case 2: + options += " E-STOP"; + break; + case 3: + options += " E-STOP'"; + break; + default: + options += StringPrintf(" %u", speed - 3); + } + } + else if (cmd == 0x3F) { + // 128-speed step + uint8_t val = pkt.payload[ofs]; + ofs++; + bool is_forward = (val & 0x80) != 0; + uint8_t speed = val & 0x7F; + options += " SPD128 "; + options += is_forward ? 'F' : 'R'; + switch (speed) + { + case 0: + options += " 0"; + break; + case 1: + options += " E-STOP"; + break; + default: + options += StringPrintf(" %u", speed - 1); + } + } + else if ((cmd >> 5) == 0b100) + { + // function group 0 + options += StringPrintf(" F[0-4]=%d%d%d%d%d", (cmd >> 4) & 1, + (cmd >> 0) & 1, (cmd >> 1) & 1, (cmd >> 2) & 1, (cmd >> 3) & 1); + } + else if ((cmd >> 5) == 0b101) + { + // function group 1 or 2 + if (cmd & 0x10) + { + options += " F[5-8]="; + } + else + { + options += " F[9-12]="; + } + options += StringPrintf("%d%d%d%d", (cmd >> 0) & 1, (cmd >> 1) & 1, + (cmd >> 2) & 1, (cmd >> 3) & 1); + } + else if ((cmd >> 5) == 0b110) + { + // expansion + uint8_t c = cmd & 0x1F; + if ((c & ~1) == 0b11110) + { + if (c & 1) + { + options += " F[21-28]="; + } + else + { + options += " F[13-20]="; + } + c = pkt.payload[ofs]; + ofs++; + for (int i = 0; i < 8; ++i, c >>= 1) + options += '0' + (c & 1); + } + else + { + /// @todo + } + } + else if (cmd == 0 && is_idle_packet) + { + } + + // checksum of packet + if (ofs == pkt.dlc && pkt.packet_header.skip_ec == 0) + { + // EC skipped. + } + else if (((ofs + 1) == pkt.dlc) && pkt.packet_header.skip_ec == 1) + { + uint8_t x = 0; + for (unsigned i = 0; i + 1 < pkt.dlc; ++i) + { + x ^= pkt.payload[i]; + } + if (x != pkt.payload[pkt.dlc - 1]) + { + options += StringPrintf(" [bad EC expected 0x%02x actual 0x%02x]", + x, pkt.payload[pkt.dlc - 1]); + } + } + else + { + options += StringPrintf(" [bad dlc, exp %u, actual %u]", ofs, pkt.dlc); + while (ofs < pkt.dlc) + { + options += StringPrintf(" 0x%02x", pkt.payload[ofs++]); + } + } + return options; +} + +} // namespace dcc diff --git a/lib/OpenMRNLite/src/dcc/DccDebug.hxx b/components/OpenMRNLite/src/dcc/DccDebug.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/DccDebug.hxx rename to components/OpenMRNLite/src/dcc/DccDebug.hxx diff --git a/components/OpenMRNLite/src/dcc/DccOutput.hxx b/components/OpenMRNLite/src/dcc/DccOutput.hxx new file mode 100644 index 00000000..807f640b --- /dev/null +++ b/components/OpenMRNLite/src/dcc/DccOutput.hxx @@ -0,0 +1,370 @@ +/** \copyright + * Copyright (c) 2020, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file DccOutput.hxx + * + * Common definitions (base classes) for DCC output drivers. The role of the + * output driver is to control the output enable / disable and railcom cutout. + * + * @author Balazs Racz + * @date 19 Apr 2020 + */ + +#ifndef _DCC_DCCOUTPUT_HXX_ +#define _DCC_DCCOUTPUT_HXX_ + +#include +#include + +/// Virtual base class for controlling outputs. All of these functions are okay +/// to call from interrupts (including non-kernel-compatible interrupts under +/// FreeRTOS). +class DccOutput +{ +public: + /// Enumeration describing different outputs. + enum Type : int + { + /// DCC output of the integrated booster. + TRACK = 1, + /// DCC output of the program track. + PGM = 2, + /// DCC output going towards the LCC cable. + LCC = 3, + }; + + /// Values of a bit mask why we might want to disable a given DCC output. + enum class DisableReason : uint8_t + { + /// Set as 1 during construction time, to be cleared by the application + /// when the initialization is complete. + INITIALIZATION_PENDING = 1, + /// User decided via a persistent configuration that this output should + /// not be enabled. + CONFIG_SETTING = 2, + /// A network message requested global emergency off. + GLOBAL_EOFF = 4, + /// Short detector says this output is shorted. + SHORTED = 8, + /// The system is in thermal shutdown. + THERMAL = 16, + /// This output should be off due to the conflict between program track + /// and normal operation mode. + PGM_TRACK_LOCKOUT = 32, + }; + + /// Disables the output, marking in a bitmask why. + virtual void disable_output_for_reason(DisableReason bit) = 0; + + /// Removes a disable reason flag. All the flags need to be cleared in + /// order to enable the output. + virtual void clear_disable_output_for_reason(DisableReason bit) = 0; + + /// Sets or clears a disable reason. + /// @param bit the disable reason + /// @param value if true, bit set to disable output, if false, bit cleared + /// to not disable output. + void override_disable_bit_for_reason(DisableReason bit, bool value) + { + if (value) + { + disable_output_for_reason(bit); + } + else + { + clear_disable_output_for_reason(bit); + } + } + + /// @return Bitmask of all currently set disable reasons. + virtual uint8_t get_disable_output_reasons() = 0; + + /// Defines the values for the railcom cutout enabled setting. + enum class RailcomCutout + { + /// Generate no railcom cutout. + DISABLED = 0, + /// Generate short cutout (ch1 only). + SHORT_CUTOUT = 1, + /// Generate long cutout (standard size; ch1+ch2). + LONG_CUTOUT = 2 + }; + + /// Specifies whether there should be a railcom cutout on this output. + virtual void set_railcom_cutout_enabled(RailcomCutout cutout) = 0; +}; + +/// Public API accessor for applications to get the object representing the +/// output hardware. +/// @param type which output reference to get. +/// @return an object to be used by the application to control the output. +DccOutput *get_dcc_output(DccOutput::Type type); + +/// Default implementation class of the DccOutput that proxies the calls to a +/// hardware-specific static structure. +template class DccOutputImpl : public DccOutput +{ +public: + constexpr DccOutputImpl() + { + } + + /// Disables the output, marking in a bitmask why. + void disable_output_for_reason(DisableReason bit) override + { + HW::set_disable_reason(bit); + } + /// Removes a disable reason flag. All the flags need to be cleared in + /// order to enable the output. + void clear_disable_output_for_reason(DisableReason bit) override + { + HW::clear_disable_reason(bit); + } + + /// Specifies whether there should be a railcom cutout on this output. + void set_railcom_cutout_enabled(RailcomCutout cutout) override + { + HW::isRailcomCutoutEnabled_ = (uint8_t)cutout; + } + + /// @return Bitmask of all currently set disable reasons. + uint8_t get_disable_output_reasons() override + { + return HW::outputDisableReasons_; + } + + /// @return the default instance created during initialization. + static constexpr DccOutput *instance() + { + return const_cast( + static_cast(&instance_)); + } + +private: + /// Default instance to be used. + static const DccOutputImpl instance_; +}; + +/// Allocates the storage by the linker for the static default instance. +template const DccOutputImpl DccOutputImpl::instance_; + +/// This structure represents a single output channel for a DCC command +/// station. The DCC driver uses these structures, with the business logic +/// filled in by the hardware implementor. +/// +/// @param OUTPUT_NUMBER 0,1,... the number of outputs. Each output is +/// independently controlled. +template struct DccOutputHw +{ +public: + /// Bitmask of why this output should be disabled. If zero, the output + /// should be enabled. If non-zero, the output should be disabled. A + /// variety of system components own one bit in this bitmask each; see { + /// \link DccOutput::DisableReason } These bits are all set by the + /// application. The DCC Driver will only read this variable, and enable + /// the output if all bits are zero. + static std::atomic_uint8_t outputDisableReasons_; + + /// 0 if we should not produce a railcom cutout; 1 for short cutout; 2 for + /// regular cutout. Set by the application and read by the DCC driver. + static std::atomic_uint8_t isRailcomCutoutEnabled_; + + /// 1 if we are in a railcom cutout currently. Set and cleared by the + /// driver before calling the start/stop railcom cutout functions. + static std::atomic_uint8_t isRailcomCutoutActive_; + + /// Called by the driver to decide whether to make this channel participate + /// in the railcom cutout. + static bool need_railcom_cutout() + { + return (outputDisableReasons_ == 0) && (isRailcomCutoutEnabled_ != 0); + } + + /// Called once after the railcom cutout is done to decide whether this + /// output should be reenabled. + static bool should_be_enabled() + { + return outputDisableReasons_ == 0; + } + + /// Clears a disable reason. If all disable reasons are clear, the output + /// will be enabled by the DCC driver at the beginning of the next packet. + static void clear_disable_reason(DccOutput::DisableReason bit) + { + outputDisableReasons_ &= ~((uint8_t)bit); + } + +protected: + /// Set one bit in the disable reasons bit field. This is protected, + /// because the implementation / child class should have a composite + /// function that sets the bit and disables the output in one call. + static void set_disable_reason_impl(DccOutput::DisableReason bit) + { + outputDisableReasons_ |= (uint8_t)bit; + } + +private: + /// Private constructor. These objects cannot be initialized and must only + /// have static members. + DccOutputHw(); +}; + +template +std::atomic_uint8_t DccOutputHw::outputDisableReasons_ { + (uint8_t)DccOutput::DisableReason::INITIALIZATION_PENDING}; +template +std::atomic_uint8_t DccOutputHw::isRailcomCutoutEnabled_ { + (uint8_t)DccOutput::RailcomCutout::LONG_CUTOUT}; +template std::atomic_uint8_t DccOutputHw::isRailcomCutoutActive_ {0}; + +/// Interface that the actual outputs have to implement in their +/// hardware-specific classes. +template struct DccOutputHwDummy : public DccOutputHw +{ +public: + /// Called once during hw_preinit boot state. + static void hw_preinit(void) + { + } + + /// Invoked at the beginning of a railcom cutout. @return the number of usec + /// to wait before invoking phase2. + static unsigned start_railcom_cutout_phase1(void) + { + return 0; + } + + /// Invoked at the beginning of a railcom cutout after the delay. @return + /// number of usec to delay before enabling railcom UART receive. + static unsigned start_railcom_cutout_phase2(void) + { + return 0; + } + + /// Invoked at the end of a railcom cutout. @return the number of usec to + /// wait before invoking phase2. + static unsigned stop_railcom_cutout_phase1(void) + { + return 0; + } + + /// Invoked at the end of a railcom cutout. + static void stop_railcom_cutout_phase2(void) + { + } + + /// Called once every packet by the driver, typically before the preamble, + /// if the output is supposed to be on. + static void enable_output(void) + { + } + + /// A dummy output never needs a railcom cutout. + static bool need_railcom_cutout() + { + return false; + } + + static void set_disable_reason(DccOutput::DisableReason bit) + { + DccOutputHwDummy::set_disable_reason_impl(bit); + // no actual output needs to be disabled. + } +}; + +/// Generic implementation of the actual HW output with a booster enable and a +/// railcom enable GPIO. +/// @param N is the output number +/// @param BOOSTER_ENABLE is a GPIO structure that turns the output +/// on/off. set(true) is on. +/// @param RAILCOM_ENABLE is a GPIO structure that turns the RailCom FETs on. +/// set(true) starts the cutout. +/// @param DELAY_ON_1 is the number of usec to delay from BOOSTER_ENABLE off to +/// RAILCOM_ENABLE on. +/// @param DELAY_ON_2 is the number of usec to delay from RAILCOM_ENABLE on to +/// railcom UART on. +/// @param DELAY_OFF is the number of usec to delay from RAILCOM_ENABLE off to +/// booster enable on. +template +struct DccOutputHwReal : public DccOutputHw +{ +public: + /// Called once during hw_preinit boot state. + static void hw_preinit(void) + { + BOOSTER_ENABLE::hw_init(); + BOOSTER_ENABLE::set(false); + RAILCOM_ENABLE::hw_init(); + RAILCOM_ENABLE::set(false); + } + + /// Invoked at the beginning of a railcom cutout. @return the number of usec + /// to wait before invoking phase2. + static unsigned start_railcom_cutout_phase1(void) + { + BOOSTER_ENABLE::set(false); + return DELAY_ON_1; + } + + /// Invoked at the beginning of a railcom cutout after the delay. @return + /// number of usec to delay before enabling railcom UART receive. + static unsigned start_railcom_cutout_phase2(void) + { + RAILCOM_ENABLE::set(true); + return DELAY_ON_2; + } + + /// Invoked at the end of a railcom cutout. @return the number of usec to + /// wait before invoking phase2. + static unsigned stop_railcom_cutout_phase1(void) + { + RAILCOM_ENABLE::set(false); + return DELAY_OFF; + } + + /// Invoked at the end of a railcom cutout. + static void stop_railcom_cutout_phase2(void) + { + /// @todo consider checking whether output disable reason == 0 then + /// enable the output? + } + + /// Called once every packet by the driver, typically before the preamble, + /// if the output is supposed to be on. + static void enable_output(void) + { + BOOSTER_ENABLE::set(true); + } + + static void set_disable_reason(DccOutput::DisableReason bit) + { + DccOutputHw::set_disable_reason_impl(bit); + BOOSTER_ENABLE::set(false); + } +}; + +#endif // _DCC_DCCOUTPUT_HXX_ diff --git a/lib/OpenMRNLite/src/dcc/Defs.hxx b/components/OpenMRNLite/src/dcc/Defs.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/Defs.hxx rename to components/OpenMRNLite/src/dcc/Defs.hxx diff --git a/lib/OpenMRNLite/src/dcc/FakeTrackIf.hxx b/components/OpenMRNLite/src/dcc/FakeTrackIf.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/FakeTrackIf.hxx rename to components/OpenMRNLite/src/dcc/FakeTrackIf.hxx diff --git a/lib/OpenMRNLite/src/dcc/LocalTrackIf.hxx b/components/OpenMRNLite/src/dcc/LocalTrackIf.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/LocalTrackIf.hxx rename to components/OpenMRNLite/src/dcc/LocalTrackIf.hxx diff --git a/components/OpenMRNLite/src/dcc/Loco.cpp b/components/OpenMRNLite/src/dcc/Loco.cpp new file mode 100644 index 00000000..cad1533b --- /dev/null +++ b/components/OpenMRNLite/src/dcc/Loco.cpp @@ -0,0 +1,276 @@ +/** \copyright + * Copyright (c) 2014, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file Loco.cxx + * + * Defines a simple DCC locomotive. + * + * @author Balazs Racz + * @date 10 May 2014 + */ + +#include "dcc/Loco.hxx" + +#include "utils/logging.h" +#include "dcc/UpdateLoop.hxx" + +namespace dcc +{ + +/// Forces compilation of all existing train implementations even though many +/// are actually templates. This avoid needing to put all this code into a .hxx +/// file. +extern void createtrains(); + +template <> DccTrain::~DccTrain() +{ + packet_processor_remove_refresh_source(this); +} + +template <> DccTrain::~DccTrain() +{ + packet_processor_remove_refresh_source(this); +} + +unsigned Dcc28Payload::get_fn_update_code(unsigned address) +{ + if (address < 5) + { + return FUNCTION0; + } + else if (address < 9) + { + return FUNCTION5; + } + else if (address < 13) + { + return FUNCTION9; + } + else if (address < 21) + { + return FUNCTION13; + } + else if (address <= 28) + { + return FUNCTION21; + } + return SPEED; +} + +// Generates next outgoing packet. +template +void DccTrain::get_next_packet(unsigned code, Packet *packet) +{ + packet->start_dcc_packet(); + if (this->p.isShortAddress_) + { + packet->add_dcc_address(DccShortAddress(this->p.address_)); + } + else + { + packet->add_dcc_address(DccLongAddress(this->p.address_)); + } + if (code == REFRESH) + { + code = MIN_REFRESH + this->p.nextRefresh_++; + if (this->p.nextRefresh_ > MAX_REFRESH - MIN_REFRESH) + { + this->p.nextRefresh_ = 0; + } + } + else + { + // User action. Up repeat count. + packet->packet_header.rept_count = 2; + } + switch (code) + { + case FUNCTION0: + { + packet->add_dcc_function0_4(this->p.fn_ & 0x1F); + return; + } + case FUNCTION5: + { + packet->add_dcc_function5_8(this->p.fn_ >> 5); + return; + } + case FUNCTION9: + { + packet->add_dcc_function9_12(this->p.fn_ >> 9); + return; + } + case FUNCTION13: + { + packet->add_dcc_function13_20(this->p.fn_ >> 13); + return; + } + case FUNCTION21: + { + packet->add_dcc_function21_28(this->p.fn_ >> 21); + return; + } + case ESTOP: + { + this->p.add_dcc_estop_to_packet(packet); + packet->packet_header.rept_count = 3; + return; + } + default: + LOG(WARNING, "Unknown packet generation code: %x", code); + // fall through + case SPEED: + { + if (this->p.directionChanged_) + { + // packet->packet_header.rept_count = 2; + this->p.directionChanged_ = 0; + } + // packet->packet_header.rept_count = 1; + this->p.add_dcc_speed_to_packet(packet); + return; + } + } +} + +MMOldTrain::MMOldTrain(MMAddress a) +{ + p.address_ = a.value; + packet_processor_add_refresh_source(this); +} + +MMOldTrain::~MMOldTrain() +{ + packet_processor_remove_refresh_source(this); +} + +// Generates next outgoing packet. +void MMOldTrain::get_next_packet(unsigned code, Packet *packet) +{ + packet->start_mm_packet(); + packet->add_mm_address(MMAddress(p.address_), p.fn_ & 1); + + if (code == ESTOP) + { + packet->add_mm_speed( + Packet::EMERGENCY_STOP); // will change the direction. + p.direction_ = !p.direction_; + p.directionChanged_ = 0; + } + else if (p.directionChanged_) + { + packet->add_mm_speed(Packet::CHANGE_DIR); + p.directionChanged_ = 0; + } + else + { + packet->add_mm_speed(p.speed_); + if (code != REFRESH) + { + packet->packet_header.rept_count = 2; + } + } +} + +MMNewTrain::MMNewTrain(MMAddress a) +{ + p.address_ = a.value; + packet_processor_add_refresh_source(this); +} + +MMNewTrain::~MMNewTrain() +{ + packet_processor_remove_refresh_source(this); +} + +// Generates next outgoing packet. +void MMNewTrain::get_next_packet(unsigned code, Packet *packet) +{ + packet->start_mm_packet(); + packet->add_mm_address(MMAddress(p.address_), p.fn_ & 1); + + if (code == REFRESH) + { + unsigned r = p.nextRefresh_; + if ((r & 1) == 0) { + code = SPEED; + } else { + // TODO(bracz): check if this refresh cycle confuses the marklin + // engines' directional state. + r >>= 1; + r += MM_F1; + code = r; + } + if (p.nextRefresh_ == MM_MAX_REFRESH) { + p.nextRefresh_ = 0; + } else { + ++p.nextRefresh_; + } + } + else + { + packet->packet_header.rept_count = 2; + } + if (code == ESTOP) + { + packet->add_mm_new_speed( + !p.direction_, + Packet::EMERGENCY_STOP); // will change the direction. + p.direction_ = !p.direction_; + p.directionChanged_ = 0; + } + else if (code == SPEED) + { + if (p.directionChanged_) + { + packet->add_mm_new_speed(!p.direction_, Packet::CHANGE_DIR); + p.directionChanged_ = 0; + p.nextRefresh_ = 0; // sends another speed packet + packet->mm_shift(); + } + else + { + packet->add_mm_new_speed(!p.direction_, p.speed_); + packet->mm_shift(); + } + } + else if (MM_F1 <= code && code <= MM_F4) + { + unsigned fnum = code + 1 - MM_F1; + packet->add_mm_new_fn(fnum, p.fn_ & (1 << fnum), p.speed_); + //packet->mm_shift(); + //packet->add_mm_new_speed(!p.direction_, p.speed_); + } +} + +void createtrains() { + Dcc28Train train1(DccShortAddress(1)); + Dcc128Train train2(DccShortAddress(1)); + MMNewTrain train3(MMAddress(1)); + MMOldTrain train4(MMAddress(1)); +} + +} // namespace dcc diff --git a/lib/OpenMRNLite/src/dcc/Loco.hxx b/components/OpenMRNLite/src/dcc/Loco.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/Loco.hxx rename to components/OpenMRNLite/src/dcc/Loco.hxx diff --git a/lib/OpenMRNLite/src/dcc/Packet.cpp b/components/OpenMRNLite/src/dcc/Packet.cpp similarity index 100% rename from lib/OpenMRNLite/src/dcc/Packet.cpp rename to components/OpenMRNLite/src/dcc/Packet.cpp diff --git a/lib/OpenMRNLite/src/dcc/Packet.hxx b/components/OpenMRNLite/src/dcc/Packet.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/Packet.hxx rename to components/OpenMRNLite/src/dcc/Packet.hxx diff --git a/lib/OpenMRNLite/src/dcc/PacketFlowInterface.hxx b/components/OpenMRNLite/src/dcc/PacketFlowInterface.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/PacketFlowInterface.hxx rename to components/OpenMRNLite/src/dcc/PacketFlowInterface.hxx diff --git a/lib/OpenMRNLite/src/dcc/PacketSource.hxx b/components/OpenMRNLite/src/dcc/PacketSource.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/PacketSource.hxx rename to components/OpenMRNLite/src/dcc/PacketSource.hxx diff --git a/lib/OpenMRNLite/src/dcc/ProgrammingTrackBackend.hxx b/components/OpenMRNLite/src/dcc/ProgrammingTrackBackend.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/ProgrammingTrackBackend.hxx rename to components/OpenMRNLite/src/dcc/ProgrammingTrackBackend.hxx diff --git a/lib/OpenMRNLite/src/dcc/RailCom.cpp b/components/OpenMRNLite/src/dcc/RailCom.cpp similarity index 100% rename from lib/OpenMRNLite/src/dcc/RailCom.cpp rename to components/OpenMRNLite/src/dcc/RailCom.cpp diff --git a/lib/OpenMRNLite/src/dcc/RailCom.hxx b/components/OpenMRNLite/src/dcc/RailCom.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/RailCom.hxx rename to components/OpenMRNLite/src/dcc/RailCom.hxx diff --git a/lib/OpenMRNLite/src/dcc/RailcomBroadcastDecoder.cpp b/components/OpenMRNLite/src/dcc/RailcomBroadcastDecoder.cpp similarity index 100% rename from lib/OpenMRNLite/src/dcc/RailcomBroadcastDecoder.cpp rename to components/OpenMRNLite/src/dcc/RailcomBroadcastDecoder.cpp diff --git a/lib/OpenMRNLite/src/dcc/RailcomBroadcastDecoder.hxx b/components/OpenMRNLite/src/dcc/RailcomBroadcastDecoder.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/RailcomBroadcastDecoder.hxx rename to components/OpenMRNLite/src/dcc/RailcomBroadcastDecoder.hxx diff --git a/lib/OpenMRNLite/src/dcc/RailcomDebug.cpp b/components/OpenMRNLite/src/dcc/RailcomDebug.cpp similarity index 100% rename from lib/OpenMRNLite/src/dcc/RailcomDebug.cpp rename to components/OpenMRNLite/src/dcc/RailcomDebug.cpp diff --git a/lib/OpenMRNLite/src/dcc/RailcomHub.hxx b/components/OpenMRNLite/src/dcc/RailcomHub.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/RailcomHub.hxx rename to components/OpenMRNLite/src/dcc/RailcomHub.hxx diff --git a/lib/OpenMRNLite/src/dcc/RailcomPortDebug.hxx b/components/OpenMRNLite/src/dcc/RailcomPortDebug.hxx similarity index 99% rename from lib/OpenMRNLite/src/dcc/RailcomPortDebug.hxx rename to components/OpenMRNLite/src/dcc/RailcomPortDebug.hxx index d68746da..dc2e61eb 100644 --- a/lib/OpenMRNLite/src/dcc/RailcomPortDebug.hxx +++ b/components/OpenMRNLite/src/dcc/RailcomPortDebug.hxx @@ -181,7 +181,7 @@ public: } return exit(); } - if (message()->data()->channel == 0xfe) + if (message()->data()->channel >= 0xf0) { return release_and_exit(); } diff --git a/lib/OpenMRNLite/src/dcc/Receiver.hxx b/components/OpenMRNLite/src/dcc/Receiver.hxx similarity index 91% rename from lib/OpenMRNLite/src/dcc/Receiver.hxx rename to components/OpenMRNLite/src/dcc/Receiver.hxx index 2d767d6d..f941ef2c 100644 --- a/lib/OpenMRNLite/src/dcc/Receiver.hxx +++ b/components/OpenMRNLite/src/dcc/Receiver.hxx @@ -40,6 +40,12 @@ #include "executor/StateFlow.hxx" +#include "freertos/can_ioctl.h" +#include "freertos_drivers/common/SimpleLog.hxx" + +// If defined, collects samples of timing and state into a ring buffer. +//#define DCC_DECODER_DEBUG + namespace dcc { @@ -48,13 +54,15 @@ namespace dcc class DccDecoder { public: - DccDecoder() + /// @param tick_per_usec specifies how many timer capture ticks happen per + /// usec. The default value assumes the timer does not have a prescaler. + DccDecoder(unsigned tick_per_usec) { - timings_[DCC_ONE].set(52, 64); - timings_[DCC_ZERO].set(95, 9900); - timings_[MM_PREAMBLE].set(1000, -1); - timings_[MM_SHORT].set(20, 32); - timings_[MM_LONG].set(200, 216); + timings_[DCC_ONE].set(tick_per_usec, 52, 64); + timings_[DCC_ZERO].set(tick_per_usec, 95, 9900); + timings_[MM_PREAMBLE].set(tick_per_usec, 1000, -1); + timings_[MM_SHORT].set(tick_per_usec, 20, 32); + timings_[MM_LONG].set(tick_per_usec, 200, 216); } /// Internal states of the decoding state machine. @@ -86,6 +94,10 @@ public: /// change. void process_data(uint32_t value) { +#ifdef DCC_DECODER_DEBUG + debugLog_.add(value); + debugLog_.add(parseState_); +#endif switch (parseState_) { case DCC_PACKET_FINISHED: @@ -304,7 +316,7 @@ private: /// Represents the timing of a half-wave of the digital track signal. struct Timing { - void set(int min_usec, int max_usec) + void set(uint32_t tick_per_usec, int min_usec, int max_usec) { if (min_usec < 0) { @@ -312,7 +324,7 @@ private: } else { - min_value = usec_to_clock(min_usec); + min_value = tick_per_usec * min_usec; } if (max_usec < 0) { @@ -320,7 +332,7 @@ private: } else { - max_value = usec_to_clock(max_usec); + max_value = tick_per_usec * max_usec; } } @@ -329,11 +341,6 @@ private: return min_value <= value_clocks && value_clocks <= max_value; } - static uint32_t usec_to_clock(int usec) - { - return (configCPU_CLOCK_HZ / 1000000) * usec; - } - uint32_t min_value; uint32_t max_value; }; @@ -350,6 +357,9 @@ private: }; /// The various timings by the standards. Timing timings_[MAX_TIMINGS]; +#ifdef DCC_DECODER_DEBUG + LogRing debugLog_; +#endif }; /// User-space DCC decoding flow. This flow receives a sequence of numbers from @@ -387,7 +397,6 @@ private: { return call_immediately(STATE(register_and_sleep)); } - MAP_GPIOPinWrite(GPIO_PORTA_BASE, GPIO_PIN_0, 0xff); debug_data(value); decoder_.process_data(value); if (decoder_.state() == DccDecoder::DCC_PACKET_FINISHED) @@ -417,7 +426,9 @@ private: uint32_t lastValue_ = 0; protected: - DccDecoder decoder_; + /// State machine that does the DCC decoding. We have 1 usec per tick, as + /// these are the numbers we receive from the driver. + DccDecoder decoder_ {1}; }; } // namespace dcc diff --git a/components/OpenMRNLite/src/dcc/SimpleUpdateLoop.cpp b/components/OpenMRNLite/src/dcc/SimpleUpdateLoop.cpp new file mode 100644 index 00000000..70e7bbb4 --- /dev/null +++ b/components/OpenMRNLite/src/dcc/SimpleUpdateLoop.cpp @@ -0,0 +1,86 @@ +/** \copyright + * Copyright (c) 2014, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file SimpleUpdateLoop.cxx + * + * Control flow central to the command station: it round-robins between + * refreshing the individual trains. + * + * @author Balazs Racz + * @date 1 Feb 2015 + */ + +#include "dcc/SimpleUpdateLoop.hxx" +#include "dcc/Packet.hxx" +#include "dcc/PacketSource.hxx" + +namespace dcc +{ + +SimpleUpdateLoop::SimpleUpdateLoop(Service *service, + PacketFlowInterface *track_send) + : StateFlow(service) + , trackSend_(track_send) + , nextRefreshIndex_(0) + , lastCycleStart_(os_get_time_monotonic()) +{ +} + +SimpleUpdateLoop::~SimpleUpdateLoop() +{ +} + +StateFlowBase::Action SimpleUpdateLoop::entry() +{ + long long current_time = os_get_time_monotonic(); + long long prev_cycle_start = lastCycleStart_; + if (nextRefreshIndex_ >= refreshSources_.size()) + { + nextRefreshIndex_ = 0; + lastCycleStart_ = current_time; + } + if (nextRefreshIndex_ == 0 && + (current_time - prev_cycle_start < MSEC_TO_NSEC(5) || + refreshSources_.empty())) + { + // We do not want to send another packet to the same locomotive too + // quick. We send an idle packet instead. OR: We do not have any + // locomotives at all. We will keep sending idle packets. + message()->data()->set_dcc_idle(); + } + else + { + // Send an update to the current loco. + refreshSources_[nextRefreshIndex_] + ->get_next_packet(0, message()->data()); + nextRefreshIndex_++; + } + // We pass on the filled packet to the track processor. + trackSend_->send(transfer_message()); + return exit(); +} + +} // namespace dcc diff --git a/lib/OpenMRNLite/src/dcc/SimpleUpdateLoop.hxx b/components/OpenMRNLite/src/dcc/SimpleUpdateLoop.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/SimpleUpdateLoop.hxx rename to components/OpenMRNLite/src/dcc/SimpleUpdateLoop.hxx diff --git a/components/OpenMRNLite/src/dcc/UpdateLoop.cpp b/components/OpenMRNLite/src/dcc/UpdateLoop.cpp new file mode 100644 index 00000000..0f323fdb --- /dev/null +++ b/components/OpenMRNLite/src/dcc/UpdateLoop.cpp @@ -0,0 +1,61 @@ +/** \copyright + * Copyright (c) 2014, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file UpdateLoop.hxx + * + * Proxy implementation to the global update loop. + * + * @author Balazs Racz + * @date 10 May 2014 + */ + +#include "dcc/UpdateLoop.hxx" +#include "utils/Singleton.hxx" + +namespace dcc { + +void packet_processor_notify_update(PacketSource* source, unsigned code) { + Singleton::instance()->notify_update(source, code); +} + +/** Adds a new refresh source to the background refresh loop. */ +bool packet_processor_add_refresh_source( + PacketSource *source, unsigned priority) +{ + return Singleton::instance()->add_refresh_source( + source, priority); +} + +/** Removes a refresh source from the background refresh loop. */ +void packet_processor_remove_refresh_source(PacketSource* source) { + Singleton::instance()->remove_refresh_source(source); +} + +UpdateLoopBase::~UpdateLoopBase() {} + +} + +//DEFINE_SINGLETON_INSTANCE(dcc::UpdateLoopBase); diff --git a/lib/OpenMRNLite/src/dcc/UpdateLoop.hxx b/components/OpenMRNLite/src/dcc/UpdateLoop.hxx similarity index 100% rename from lib/OpenMRNLite/src/dcc/UpdateLoop.hxx rename to components/OpenMRNLite/src/dcc/UpdateLoop.hxx diff --git a/lib/OpenMRNLite/src/dcc/packet.h b/components/OpenMRNLite/src/dcc/packet.h similarity index 100% rename from lib/OpenMRNLite/src/dcc/packet.h rename to components/OpenMRNLite/src/dcc/packet.h diff --git a/lib/OpenMRNLite/src/dcc/railcom.h b/components/OpenMRNLite/src/dcc/railcom.h similarity index 100% rename from lib/OpenMRNLite/src/dcc/railcom.h rename to components/OpenMRNLite/src/dcc/railcom.h diff --git a/lib/OpenMRNLite/src/endian.h b/components/OpenMRNLite/src/endian.h similarity index 100% rename from lib/OpenMRNLite/src/endian.h rename to components/OpenMRNLite/src/endian.h diff --git a/lib/OpenMRNLite/src/executor/CallableFlow.hxx b/components/OpenMRNLite/src/executor/CallableFlow.hxx similarity index 100% rename from lib/OpenMRNLite/src/executor/CallableFlow.hxx rename to components/OpenMRNLite/src/executor/CallableFlow.hxx diff --git a/lib/OpenMRNLite/src/executor/Dispatcher.hxx b/components/OpenMRNLite/src/executor/Dispatcher.hxx similarity index 100% rename from lib/OpenMRNLite/src/executor/Dispatcher.hxx rename to components/OpenMRNLite/src/executor/Dispatcher.hxx diff --git a/lib/OpenMRNLite/src/executor/Executable.hxx b/components/OpenMRNLite/src/executor/Executable.hxx similarity index 100% rename from lib/OpenMRNLite/src/executor/Executable.hxx rename to components/OpenMRNLite/src/executor/Executable.hxx diff --git a/lib/OpenMRNLite/src/executor/Executor.cpp b/components/OpenMRNLite/src/executor/Executor.cpp similarity index 99% rename from lib/OpenMRNLite/src/executor/Executor.cpp rename to components/OpenMRNLite/src/executor/Executor.cpp index 1d6282df..ed8394dd 100644 --- a/lib/OpenMRNLite/src/executor/Executor.cpp +++ b/components/OpenMRNLite/src/executor/Executor.cpp @@ -223,6 +223,7 @@ void *ExecutorBase::entry() started_ = 1; sequence_ = 0; ExecutorBase* b = this; + unlock_from_thread(); emscripten_set_main_loop_arg(&executor_loop_some, b, 100, true); return nullptr; } diff --git a/lib/OpenMRNLite/src/executor/Executor.hxx b/components/OpenMRNLite/src/executor/Executor.hxx similarity index 95% rename from lib/OpenMRNLite/src/executor/Executor.hxx rename to components/OpenMRNLite/src/executor/Executor.hxx index 4431164b..39cd8c57 100644 --- a/lib/OpenMRNLite/src/executor/Executor.hxx +++ b/components/OpenMRNLite/src/executor/Executor.hxx @@ -37,6 +37,7 @@ #define _EXECUTOR_EXECUTOR_HXX_ #include +#include #include "executor/Executable.hxx" #include "executor/Notifiable.hxx" @@ -87,7 +88,7 @@ public: * the execution is completed. @param fn is the closure to run. */ void sync_run(std::function fn); -#ifdef __FreeRTOS__ +#if OPENMRN_FEATURE_RTOS_FROM_ISR /** Send a message to this Executor's queue. Callable from interrupt * context. * @param action Executable instance to insert into the input queue @@ -95,7 +96,7 @@ public: */ virtual void add_from_isr(Executable *action, unsigned priority = UINT_MAX) = 0; -#endif +#endif // OPENMRN_FEATURE_RTOS_FROM_ISR /** Adds a file descriptor to be watched to the select loop. * @param job Selectable structure that describes the descriptor to watch. @@ -231,9 +232,9 @@ private: /** Set to 1 when the executor thread has exited and it is safe to delete * *this. */ - unsigned done_ : 1; + std::atomic_uint_least8_t done_; /// 1 if the executor is already running - unsigned started_ : 1; + std::atomic_uint_least8_t started_; /// How many executables we schedule blindly before calling a select() in /// order to find more data to read/write in the FDs being waited upon. unsigned selectPrescaler_ : 5; @@ -314,7 +315,7 @@ public: #endif } -#ifdef __FreeRTOS__ +#if OPENMRN_FEATURE_RTOS_FROM_ISR /** Send a message to this Executor's queue. Callable from interrupt * context. * @param msg Executable instance to insert into the input queue @@ -322,11 +323,20 @@ public: */ void add_from_isr(Executable *msg, unsigned priority = UINT_MAX) override { +#ifdef ESP32 + // On the ESP32 we need to call insert instead of insert_locked to + // ensure that all code paths lock the queue for consistency since + // this code path is not guaranteed to be protected by a critical + // section. + queue_.insert( + msg, priority >= NUM_PRIO ? NUM_PRIO - 1 : priority); +#else queue_.insert_locked( msg, priority >= NUM_PRIO ? NUM_PRIO - 1 : priority); +#endif // ESP32 selectHelper_.wakeup_from_isr(); } -#endif +#endif // OPENMRN_FEATURE_RTOS_FROM_ISR /** If the executor was created with NO_THREAD, then this function needs to * be called to run the executor loop. It will exit when the execut gets diff --git a/lib/OpenMRNLite/src/executor/Notifiable.cpp b/components/OpenMRNLite/src/executor/Notifiable.cpp similarity index 100% rename from lib/OpenMRNLite/src/executor/Notifiable.cpp rename to components/OpenMRNLite/src/executor/Notifiable.cpp diff --git a/lib/OpenMRNLite/src/executor/Notifiable.hxx b/components/OpenMRNLite/src/executor/Notifiable.hxx similarity index 98% rename from lib/OpenMRNLite/src/executor/Notifiable.hxx rename to components/OpenMRNLite/src/executor/Notifiable.hxx index 3a0f3f2d..f783de86 100644 --- a/lib/OpenMRNLite/src/executor/Notifiable.hxx +++ b/components/OpenMRNLite/src/executor/Notifiable.hxx @@ -46,12 +46,12 @@ class Notifiable : public Destructable public: /// Generic callback. virtual void notify() = 0; -#ifdef __FreeRTOS__ +#if OPENMRN_FEATURE_RTOS_FROM_ISR virtual void notify_from_isr() { DIE("Unexpected call to notify_from_isr."); } -#endif +#endif // OPENMRN_FEATURE_RTOS_FROM_ISR }; /// A Notifiable for synchronously waiting for a notification. @@ -71,7 +71,7 @@ public: sem_.post(); } -#ifdef __FreeRTOS__ +#if OPENMRN_FEATURE_RTOS_FROM_ISR /// Implementation of notification receive from a FreeRTOS interrupt /// context. void notify_from_isr() OVERRIDE @@ -79,7 +79,7 @@ public: int woken = 0; sem_.post_from_isr(&woken); } -#endif +#endif // OPENMRN_FEATURE_RTOS_FROM_ISR /// Blocks the current thread until the notification is delivered. void wait_for_notification() diff --git a/lib/OpenMRNLite/src/executor/PoolToQueueFlow.hxx b/components/OpenMRNLite/src/executor/PoolToQueueFlow.hxx similarity index 100% rename from lib/OpenMRNLite/src/executor/PoolToQueueFlow.hxx rename to components/OpenMRNLite/src/executor/PoolToQueueFlow.hxx diff --git a/lib/OpenMRNLite/src/executor/Selectable.hxx b/components/OpenMRNLite/src/executor/Selectable.hxx similarity index 100% rename from lib/OpenMRNLite/src/executor/Selectable.hxx rename to components/OpenMRNLite/src/executor/Selectable.hxx diff --git a/lib/OpenMRNLite/src/executor/SemaphoreNotifiableBlock.hxx b/components/OpenMRNLite/src/executor/SemaphoreNotifiableBlock.hxx similarity index 98% rename from lib/OpenMRNLite/src/executor/SemaphoreNotifiableBlock.hxx rename to components/OpenMRNLite/src/executor/SemaphoreNotifiableBlock.hxx index fac640e8..1a2d830c 100644 --- a/lib/OpenMRNLite/src/executor/SemaphoreNotifiableBlock.hxx +++ b/components/OpenMRNLite/src/executor/SemaphoreNotifiableBlock.hxx @@ -78,13 +78,13 @@ public: sem_.post(); } -#ifdef __FreeRTOS__ +#if OPENMRN_FEATURE_RTOS_FROM_ISR void notify_from_isr() OVERRIDE { int woken = 0; sem_.post_from_isr(&woken); } -#endif +#endif // OPENMRN_FEATURE_RTOS_FROM_ISR private: /// How many barriers did we allocate in total? diff --git a/lib/OpenMRNLite/src/executor/Service.cpp b/components/OpenMRNLite/src/executor/Service.cpp similarity index 100% rename from lib/OpenMRNLite/src/executor/Service.cpp rename to components/OpenMRNLite/src/executor/Service.cpp diff --git a/lib/OpenMRNLite/src/executor/Service.hxx b/components/OpenMRNLite/src/executor/Service.hxx similarity index 100% rename from lib/OpenMRNLite/src/executor/Service.hxx rename to components/OpenMRNLite/src/executor/Service.hxx diff --git a/lib/OpenMRNLite/src/executor/StateFlow.cpp b/components/OpenMRNLite/src/executor/StateFlow.cpp similarity index 96% rename from lib/OpenMRNLite/src/executor/StateFlow.cpp rename to components/OpenMRNLite/src/executor/StateFlow.cpp index e9f2e605..29e85c34 100644 --- a/lib/OpenMRNLite/src/executor/StateFlow.cpp +++ b/components/OpenMRNLite/src/executor/StateFlow.cpp @@ -99,24 +99,24 @@ void StateFlowBase::notify() service()->executor()->add(this); } -#ifdef __FreeRTOS__ +#if OPENMRN_FEATURE_RTOS_FROM_ISR void StateFlowBase::notify_from_isr() { service()->executor()->add_from_isr(this, 0); } -#endif +#endif // OPENMRN_FEATURE_RTOS_FROM_ISR void StateFlowWithQueue::notify() { service()->executor()->add(this, currentPriority_); } -#ifdef __FreeRTOS__ +#if OPENMRN_FEATURE_RTOS_FROM_ISR void StateFlowWithQueue::notify_from_isr() { service()->executor()->add_from_isr(this, currentPriority_); } -#endif +#endif // OPENMRN_FEATURE_RTOS_FROM_ISR /** Terminates the current StateFlow activity. This is a sink state, and there * has to be an external call to do anything useful after this state has been diff --git a/lib/OpenMRNLite/src/executor/StateFlow.hxx b/components/OpenMRNLite/src/executor/StateFlow.hxx similarity index 99% rename from lib/OpenMRNLite/src/executor/StateFlow.hxx rename to components/OpenMRNLite/src/executor/StateFlow.hxx index cd37a016..57be225a 100644 --- a/lib/OpenMRNLite/src/executor/StateFlow.hxx +++ b/components/OpenMRNLite/src/executor/StateFlow.hxx @@ -179,11 +179,11 @@ public: * priority. */ void notify() override; -#ifdef __FreeRTOS__ +#if OPENMRN_FEATURE_RTOS_FROM_ISR /** Wakeup call arrived. Schedules *this on the executor. Does not know the * priority. */ virtual void notify_from_isr() OVERRIDE; -#endif +#endif // OPENMRN_FEATURE_RTOS_FROM_ISR /** Return a pointer to the service I am bound to. * @return pointer to service @@ -549,6 +549,7 @@ protected: Buffer *b; mainBufferPool->alloc(&b); b->data()->reset(std::forward(args)...); + b->data()->done.reset(EmptyNotifiable::DefaultInstance()); target_flow->send(b); } @@ -572,6 +573,7 @@ protected: helper->readNonblocking_ = 0; helper->readWithTimeout_ = 0; helper->nextState_ = c; + helper->hasError_ = 0; allocationResult_ = helper; return call_immediately(STATE(internal_try_read)); } @@ -597,6 +599,7 @@ protected: helper->readNonblocking_ = 0; helper->readWithTimeout_ = 0; helper->nextState_ = c; + helper->hasError_ = 0; allocationResult_ = helper; return call_immediately(STATE(internal_try_read)); } @@ -619,6 +622,7 @@ protected: helper->readNonblocking_ = 1; helper->readWithTimeout_ = 0; helper->nextState_ = c; + helper->hasError_ = 0; allocationResult_ = helper; return call_immediately(STATE(internal_try_read)); } @@ -650,6 +654,7 @@ protected: helper->readWithTimeout_ = 1; helper->timer_.set_triggered(); // Needed for the first iteration helper->nextState_ = c; + helper->hasError_ = 0; allocationResult_ = static_cast(helper); return call_immediately(STATE(internal_try_read)); } @@ -720,7 +725,7 @@ protected: } -#ifdef HAVE_BSDSOCKET +#if OPENMRN_FEATURE_BSD_SOCKETS /** Wait for a listen socket to become active and ready to accept an * incoming connection. * @param helper selectable helper for maintaining the select metadata @@ -759,7 +764,7 @@ protected: service()->executor()->select(helper); return wait_and_call(c); } -#endif +#endif // OPENMRN_FEATURE_BSD_SOCKETS /// Writes some data into a file descriptor, repeating the operation as /// necessary until all bytes are written. @@ -786,6 +791,7 @@ protected: helper->readFully_ = 1; helper->readWithTimeout_ = 0; helper->nextState_ = c; + helper->hasError_ = 0; allocationResult_ = helper; return call_immediately(STATE(internal_try_write)); } @@ -960,10 +966,10 @@ public: /// Wakeup call arrived. Schedules *this on the executor. void notify() override; -#ifdef __FreeRTOS__ +#if OPENMRN_FEATURE_RTOS_FROM_ISR /** Wakeup call arrived. Schedules *this on the executor. */ void notify_from_isr() OVERRIDE; -#endif +#endif // OPENMRN_FEATURE_RTOS_FROM_ISR /// @returns true if the flow is waiting for work. bool is_waiting() diff --git a/lib/OpenMRNLite/src/executor/Timer.cpp b/components/OpenMRNLite/src/executor/Timer.cpp similarity index 99% rename from lib/OpenMRNLite/src/executor/Timer.cpp rename to components/OpenMRNLite/src/executor/Timer.cpp index 76712ac5..6ef281e4 100644 --- a/lib/OpenMRNLite/src/executor/Timer.cpp +++ b/components/OpenMRNLite/src/executor/Timer.cpp @@ -68,9 +68,8 @@ ActiveTimers::~ActiveTimers() void ActiveTimers::notify() { - if (!isPending_) + if (isPending_.exchange(1) == 0) { - isPending_ = 1; executor_->add(this); } } diff --git a/lib/OpenMRNLite/src/executor/Timer.hxx b/components/OpenMRNLite/src/executor/Timer.hxx similarity index 97% rename from lib/OpenMRNLite/src/executor/Timer.hxx rename to components/OpenMRNLite/src/executor/Timer.hxx index a3daaef3..61047d2b 100644 --- a/lib/OpenMRNLite/src/executor/Timer.hxx +++ b/components/OpenMRNLite/src/executor/Timer.hxx @@ -120,7 +120,7 @@ private: /// List of timers that are scheduled. QMember activeTimers_; /// 1 if we in the executor's queue. - unsigned isPending_ : 1; + std::atomic_uint_least8_t isPending_; friend class TimerTest; @@ -273,6 +273,13 @@ public: isCancelled_ = 1; } +protected: + /** Updates the period, to be used after the next expiration of the timer + * in order to restart it. */ + void update_period(long long period) + { + period_ = period; + } private: friend class ActiveTimers; // for scheduling an expiring timers diff --git a/lib/OpenMRNLite/src/freertos_drivers/arduino/ArduinoGpio.hxx b/components/OpenMRNLite/src/freertos_drivers/arduino/ArduinoGpio.hxx similarity index 100% rename from lib/OpenMRNLite/src/freertos_drivers/arduino/ArduinoGpio.hxx rename to components/OpenMRNLite/src/freertos_drivers/arduino/ArduinoGpio.hxx diff --git a/lib/OpenMRNLite/src/freertos_drivers/arduino/Can.cpp b/components/OpenMRNLite/src/freertos_drivers/arduino/Can.cpp similarity index 100% rename from lib/OpenMRNLite/src/freertos_drivers/arduino/Can.cpp rename to components/OpenMRNLite/src/freertos_drivers/arduino/Can.cpp diff --git a/lib/OpenMRNLite/src/freertos_drivers/arduino/Can.hxx b/components/OpenMRNLite/src/freertos_drivers/arduino/Can.hxx similarity index 100% rename from lib/OpenMRNLite/src/freertos_drivers/arduino/Can.hxx rename to components/OpenMRNLite/src/freertos_drivers/arduino/Can.hxx diff --git a/lib/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.cpp b/components/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.cpp similarity index 98% rename from lib/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.cpp rename to components/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.cpp index 4de25d15..43780470 100644 --- a/lib/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.cpp +++ b/components/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.cpp @@ -31,8 +31,11 @@ * @date 30 August 2015 */ + #include "CpuLoad.hxx" +#ifdef OPENMRN_FEATURE_THREAD_FREERTOS + #include "os/os.h" #include "freertos_includes.h" @@ -142,3 +145,5 @@ void cpuload_tick(unsigned irq) } DEFINE_SINGLETON_INSTANCE(CpuLoad); + +#endif // OPENMRN_FEATURE_THREAD_FREERTOS diff --git a/lib/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.hxx b/components/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.hxx similarity index 96% rename from lib/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.hxx rename to components/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.hxx index 0498f034..c87e1b26 100644 --- a/lib/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.hxx +++ b/components/OpenMRNLite/src/freertos_drivers/arduino/CpuLoad.hxx @@ -31,6 +31,10 @@ * @date 30 August 2015 */ +#include "openmrn_features.h" + +#ifdef OPENMRN_FEATURE_THREAD_FREERTOS + #ifndef _OS_CPULOAD_HXX_ #define _OS_CPULOAD_HXX_ @@ -229,11 +233,12 @@ private: auto k = l->new_key(); if (k < 300) { - l->set_key_description(k, StringPrintf("irq-%u", k)); + l->set_key_description(k, StringPrintf("irq-%u", (unsigned)k)); } else if (k & 1) { - l->set_key_description(k, StringPrintf("ex 0x%x", k & ~1)); + l->set_key_description( + k, StringPrintf("ex 0x%x", (unsigned)(k & ~1))); } else { @@ -252,3 +257,4 @@ private: }; #endif // _OS_CPULOAD_HXX_ +#endif // OPENMRN_FEATURE_THREAD_FREERTOS diff --git a/lib/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.cpp b/components/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.cpp similarity index 96% rename from lib/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.cpp rename to components/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.cpp index 3516e65d..880cde2a 100644 --- a/lib/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.cpp +++ b/components/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.cpp @@ -34,7 +34,8 @@ #include "DeviceBuffer.hxx" -#ifndef ARDUINO +#include "openmrn_features.h" +#if OPENMRN_FEATURE_DEVTAB #include @@ -61,4 +62,4 @@ void DeviceBufferBase::block_until_condition(File *file, bool read) ::select(fd + 1, read ? &fds : NULL, read ? NULL : &fds, NULL, NULL); } -#endif +#endif // OPENMRN_FEATURE_DEVTAB diff --git a/lib/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.hxx b/components/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.hxx similarity index 96% rename from lib/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.hxx rename to components/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.hxx index 7d4fe8b7..719029f4 100644 --- a/lib/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.hxx +++ b/components/OpenMRNLite/src/freertos_drivers/arduino/DeviceBuffer.hxx @@ -41,7 +41,9 @@ #include #include "utils/macros.h" -#ifndef ARDUINO +#include "openmrn_features.h" + +#if OPENMRN_FEATURE_DEVTAB #include "Devtab.hxx" #endif @@ -50,21 +52,21 @@ class DeviceBufferBase { public: -#ifndef ARDUINO +#if OPENMRN_FEATURE_DEVTAB /** Wait for blocking condition to become true. * @param file file to wait on * @param read true if this is a read operation, false for write operation */ static void block_until_condition(File *file, bool read); -#endif +#endif // OPENMRN_FEATURE_DEVTAB /** Signal the wakeup condition. This will also wakeup select. */ void signal_condition() { -#ifndef ARDUINO +#if OPENMRN_FEATURE_DEVTAB Device::select_wakeup(&selectInfo); -#endif +#endif // OPENMRN_FEATURE_DEVTAB } /** Signal the wakeup condition from an ISR context. This will also @@ -72,10 +74,10 @@ public: */ void signal_condition_from_isr() { -#ifndef ARDUINO +#if OPENMRN_FEATURE_DEVTAB int woken = 0; Device::select_wakeup_from_isr(&selectInfo, &woken); -#endif +#endif // OPENMRN_FEATURE_DEVTAB } /** flush all the data out of the buffer and reset the buffer. It is @@ -110,9 +112,9 @@ public: */ void select_insert() { -#ifndef ARDUINO +#if OPENMRN_FEATURE_DEVTAB return Device::select_insert(&selectInfo); -#endif +#endif // OPENMRN_FEATURE_DEVTAB } /** Remove a number of items from the buffer by advancing the readIndex. @@ -180,10 +182,10 @@ protected: { } -#ifndef ARDUINO +#if OPENMRN_FEATURE_DEVTAB /** Metadata for select() logic */ Device::SelectInfo selectInfo; -#endif +#endif // OPENMRN_FEATURE_DEVTAB /** level of space required in buffer in order to wakeup, 0 if unused */ uint16_t level; diff --git a/components/OpenMRNLite/src/freertos_drivers/arduino/DummyGPIO.hxx b/components/OpenMRNLite/src/freertos_drivers/arduino/DummyGPIO.hxx new file mode 100644 index 00000000..3029726f --- /dev/null +++ b/components/OpenMRNLite/src/freertos_drivers/arduino/DummyGPIO.hxx @@ -0,0 +1,126 @@ +/** \copyright + * Copyright (c) 2015, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file DummyGPIO.hxx + * + * GPIO-abstraction of a nonexistant pin. + * + * @author Balazs Racz + * @date 21 Jun 2015 + */ + +#ifndef _FREERTOS_DRIVERS_COMMON_DUMMYGPIO_HXX_ +#define _FREERTOS_DRIVERS_COMMON_DUMMYGPIO_HXX_ + +#include "GpioWrapper.hxx" + +/// GPIO Pin definition structure with no actual pin behind it. All writes to +/// this pin will be silently ignored. Reads from this pin will not compile. +struct DummyPin +{ + /// Initializes the hardware to the required settings. + static void hw_init() + { + } + /// Resets the hardware to a mode that is safe when there is no software + /// running. This uaully means turning off outputs that consume power. + static void hw_set_to_safe() + { + } + /// Sets the output pin to a given level. + static void set(bool value) + { + } + /// Toggles the output pin level. + static void toggle() + { + } + /// Returns whether this is an output pin or not. + static bool is_output() + { + return true; + } + /// Sets to "hardware" function. + static void set_hw() + { + } + /// Sets to "GPIO out" function. + static void set_output() + { + } + /// Sets to "GPIO in" function. + static void set_input() + { + } +}; + +/// GPIO Pin definition structure with no actual pin behind it. All writes to +/// this pin will be silently ignored. Reads will always return false. +struct DummyPinWithRead : public DummyPin +{ + /// @return the input pin level. + static bool get() + { + return false; + } + + /// @return true if this is an output pin, false if an input pin. + static bool is_output() + { + return false; + } + + /// @return the static Gpio instance. + static constexpr const Gpio *instance() + { + return GpioWrapper::instance(); + } +}; + +/// GPIO Pin definition structure with no actual pin behind it. All writes to +/// this pin will be silently ignored. Reads will always return false. +struct DummyPinWithReadHigh : public DummyPin +{ + /// @return the input pin level. + static bool get() + { + return true; + } + + /// @return true if this is an output pin, false if an input pin. + static bool is_output() + { + return false; + } + + /// @return the static Gpio instance. + static constexpr const Gpio *instance() + { + return GpioWrapper::instance(); + } +}; + +#endif // _FREERTOS_DRIVERS_COMMON_DUMMYGPIO_HXX_ diff --git a/lib/OpenMRNLite/src/freertos_drivers/arduino/GpioWrapper.hxx b/components/OpenMRNLite/src/freertos_drivers/arduino/GpioWrapper.hxx similarity index 100% rename from lib/OpenMRNLite/src/freertos_drivers/arduino/GpioWrapper.hxx rename to components/OpenMRNLite/src/freertos_drivers/arduino/GpioWrapper.hxx diff --git a/components/OpenMRNLite/src/freertos_drivers/arduino/RailcomDriver.hxx b/components/OpenMRNLite/src/freertos_drivers/arduino/RailcomDriver.hxx new file mode 100644 index 00000000..fbda26b6 --- /dev/null +++ b/components/OpenMRNLite/src/freertos_drivers/arduino/RailcomDriver.hxx @@ -0,0 +1,80 @@ +/** \copyright + * Copyright (c) 2014, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file RailcomDriver.hxx + * + * Abstract interface for communicating railcom-related information between + * multiple device drivers. + * + * @author Balazs Racz + * @date 6 Jan 2015 + */ + +#ifndef _FREERTOS_DRIVERS_COMMON_RAILCOMDRIVER_HXX_ +#define _FREERTOS_DRIVERS_COMMON_RAILCOMDRIVER_HXX_ + +/// Abstract base class for railcom drivers. This interface is used to +/// communicate when the railcom cutout happens. The railcom cutout is produced +/// or detected in the DCC generator or DCC parser driver, but the railcom +/// drivers need to adjust the UART configuration in accordance with it. +class RailcomDriver { +public: + /** Call to the driver for sampling the current sensors. This call is + * performed repeatedly, in a configurable interval, on the next positive + * edge. + */ + virtual void feedback_sample() = 0; + /** Instructs the driver that the railcom cutout is starting now. The driver + * will use this information to enable the UART receiver. */ + virtual void start_cutout() = 0; + /** Notifies the driver that the railcom cutout has reached the middle point, + * i.e., the first window is passed and the second window is starting. The + * driver will use this information to separate channel 1 nd channel 2 + * data. */ + virtual void middle_cutout() = 0; + /** Instructs the driver that the railcom cutout is over now. The driver + * will use this information to disable the UART receiver. */ + virtual void end_cutout() = 0; + /** Specifies the feedback key to write into the received railcom data + * packets. This feedback key is used by the application layer to correlate + * the stream of DCC packets to the stream of Railcom packets. This method + * shall be called before start_cutout. The feedback key set here is used + * until this method is called again. @param key is the new feedback key. */ + virtual void set_feedback_key(uint32_t key) = 0; +}; + + +/** Empty implementation of the railcom driver for boards that have no railcom + * hardware. */ +class NoRailcomDriver : public RailcomDriver { + void feedback_sample() OVERRIDE {} + void start_cutout() OVERRIDE {} + void middle_cutout() OVERRIDE {} + void end_cutout() OVERRIDE {} + void set_feedback_key(uint32_t key) OVERRIDE {} +}; + +#endif // _FREERTOS_DRIVERS_COMMON_RAILCOMDRIVER_HXX_ diff --git a/components/OpenMRNLite/src/freertos_drivers/arduino/SimpleLog.hxx b/components/OpenMRNLite/src/freertos_drivers/arduino/SimpleLog.hxx new file mode 100644 index 00000000..79cf4e08 --- /dev/null +++ b/components/OpenMRNLite/src/freertos_drivers/arduino/SimpleLog.hxx @@ -0,0 +1,87 @@ +/** \copyright + * Copyright (c) 2014, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file SimpleLog.hxx + * + * A very simple logging mechanism of driver events that is capable of logging + * a few entries of an 8-byte enum value, in a gdb-friendly way. + * + * @author Balazs Racz + * @date 14 September 2014 + */ + +#ifndef _FREERTOS_DRIVERS_COMMON_SIMPLELOG_HXX_ +#define _FREERTOS_DRIVERS_COMMON_SIMPLELOG_HXX_ + +/// A very simple logging mechanism of driver events that is capable of logging +/// a few entries of an 8-bit enum value, in a gdb-friendly way. +/// +/// C is typically uint64_t. +template class SimpleLog +{ +public: + SimpleLog() + : log_(0) + { + } + + /// Append a byte worth of data to the end of the log buffer. Rotates out + /// some old data. + void log(uint8_t value) + { + log_ <<= 8; + log_ |= value; + } + +private: + /// The raw log buffer. + C log_; +}; + +/// Actual class that keeps 8 log entries of one byte each. +typedef SimpleLog LogBuffer; + + +/// Alternative for hundreds of entries. +template class LogRing { +public: + void add(T data) { + data_[next_] = data; + last_ = data_ + next_; + if (next_) { + --next_; + } else { + next_ = N-1; + } + } + +private: + T data_[N]; + unsigned next_{N}; + T* last_{data_}; +}; + +#endif // _FREERTOS_DRIVERS_COMMON_SIMPLELOG_HXX_ diff --git a/lib/OpenMRNLite/src/freertos_drivers/arduino/WifiDefs.cpp b/components/OpenMRNLite/src/freertos_drivers/arduino/WifiDefs.cpp similarity index 100% rename from lib/OpenMRNLite/src/freertos_drivers/arduino/WifiDefs.cpp rename to components/OpenMRNLite/src/freertos_drivers/arduino/WifiDefs.cpp diff --git a/lib/OpenMRNLite/src/freertos_drivers/arduino/WifiDefs.hxx b/components/OpenMRNLite/src/freertos_drivers/arduino/WifiDefs.hxx similarity index 89% rename from lib/OpenMRNLite/src/freertos_drivers/arduino/WifiDefs.hxx rename to components/OpenMRNLite/src/freertos_drivers/arduino/WifiDefs.hxx index 75ab855d..751bc507 100644 --- a/lib/OpenMRNLite/src/freertos_drivers/arduino/WifiDefs.hxx +++ b/components/OpenMRNLite/src/freertos_drivers/arduino/WifiDefs.hxx @@ -1,6 +1,8 @@ #ifndef _FREERTOS_DRIVERS_COMMON_WIFIDEFS_HXX_ #define _FREERTOS_DRIVERS_COMMON_WIFIDEFS_HXX_ +#include + /// Wifi not associated to access point: continuous short blinks. #define WIFI_BLINK_NOTASSOCIATED 0b1010 /// Waiting for IP address: double short blink, pause, double short blink, ... @@ -22,6 +24,7 @@ enum class WlanState : uint8_t CONNECT_STATIC, CONNECT_FAILED, CONNECTION_LOST, + WRONG_PASSWORD, UPDATE_DISPLAY = 20, }; @@ -42,6 +45,12 @@ enum class CountryCode : uint8_t UNKNOWN, ///< unknown country code }; +enum class WlanConnectResult +{ + CONNECT_OK = 0, ///< success + PASSWORD_INVALID, /// password privided is invalid +}; + extern "C" { /// Name of wifi accesspoint to connect to. extern char WIFI_SSID[]; diff --git a/components/OpenMRNLite/src/freertos_drivers/arduino/libatomic.c b/components/OpenMRNLite/src/freertos_drivers/arduino/libatomic.c new file mode 100644 index 00000000..c40f3024 --- /dev/null +++ b/components/OpenMRNLite/src/freertos_drivers/arduino/libatomic.c @@ -0,0 +1,67 @@ +/** \copyright + * Copyright (c) 2019, Balazs Racz + * All rights reserved. + * + * 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. + * + * 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. + * + * \file libatomic.c + * + * A partial implementation of libatomic for Cortex-M0 for the necessary + * operations in OpenMRN. + * + * @author Balazs Racz + * @date 30 Dec 2019 + */ + +#include + +#if defined(STM32F0xx) || (!defined(ARDUINO) && !defined(ESP32)) +// On Cortex-M0 the only way to do atomic operation is to disable interrupts. + +/// Disables interrupts and saves the interrupt enable flag in a register. +#define ACQ_LOCK() \ + int _pastlock; \ + __asm volatile(" mrs %0, PRIMASK \n cpsid i\n" : "=r"(_pastlock)); + +/// Restores the interrupte enable flag from a register. +#define REL_LOCK() __asm volatile(" msr PRIMASK, %0\n " : : "r"(_pastlock)); + +uint16_t __atomic_fetch_sub_2(uint16_t *ptr, uint16_t val, int memorder) +{ + ACQ_LOCK(); + uint16_t ret = *ptr; + *ptr -= val; + REL_LOCK(); + return ret; +} + +uint8_t __atomic_exchange_1(uint8_t *ptr, uint8_t val, int memorder) +{ + ACQ_LOCK(); + uint8_t ret = *ptr; + *ptr = val; + REL_LOCK(); + return ret; +} + +#endif // guard for arduino compilation diff --git a/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32Gpio.hxx b/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32Gpio.hxx new file mode 100644 index 00000000..940d3d96 --- /dev/null +++ b/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32Gpio.hxx @@ -0,0 +1,432 @@ +/** \copyright + * Copyright (c) 2020, Mike Dunston + * All rights reserved. + * + * 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. + * + * 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. + * + * \file Esp32Gpio.hxx + * + * Helper declarations for using GPIO pins via the ESP-IDF APIs. + * + * @author Mike Dunston + * @date 27 March 2020 + */ + +#ifndef _DRIVERS_ESP32GPIO_HXX_ +#define _DRIVERS_ESP32GPIO_HXX_ + +#include "freertos_drivers/arduino/GpioWrapper.hxx" +#include "os/Gpio.hxx" +#include "utils/macros.h" +#include + +/// Defines a GPIO output pin. Writes to this structure will change the output +/// level of the pin. Reads will return the pin's current level. +/// +/// The pin is set to output at initialization time, with the level defined by +/// `SAFE_VALUE'. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template +class Esp32Gpio +{ +public: +#if defined(CONFIG_IDF_TARGET_ESP32) + static_assert(PIN_NUM >= 0 && PIN_NUM <= 39, "Valid pin range is 0..39."); + static_assert(!(PIN_NUM >= 6 && PIN_NUM <= 11) + , "Pin is reserved for flash usage."); +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + static_assert(PIN_NUM >= 0 && PIN_NUM <= 46, "Valid pin range is 0..46."); + static_assert(!(PIN_NUM >= 26 && PIN_NUM <= 32) + , "Pin is reserved for flash usage."); +#endif // CONFIG_IDF_TARGET_ESP32 + + /// @return the pin number for this GPIO. + static gpio_num_t pin() + { + return (gpio_num_t)PIN_NUM; + } + + /// Sets pin to output. + static void set_output() + { + HASSERT(GPIO_IS_VALID_OUTPUT_GPIO(PIN_NUM)); + ESP_ERROR_CHECK(gpio_set_direction(pin(), GPIO_MODE_OUTPUT)); + } + + /// Sets pin to input. + static void set_input() + { + ESP_ERROR_CHECK(gpio_set_direction(pin(), GPIO_MODE_INPUT)); + } + + /// Turns on pullup. + static void set_pullup_on() + { +#if defined(CONFIG_IDF_TARGET_ESP32) + // these pins have HW PD always. + HASSERT(PIN_NUM != 12); + HASSERT(PIN_NUM != 4); + HASSERT(PIN_NUM != 2); +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + // these pins have HW PD always. + HASSERT(PIN_NUM != 45); + HASSERT(PIN_NUM != 46); +#endif // CONFIG_IDF_TARGET_ESP32 + ESP_ERROR_CHECK(gpio_pullup_en(pin())); + } + + /// Turns off pullup. + static void set_pullup_off() + { +#if defined(CONFIG_IDF_TARGET_ESP32) + // these pins have HW PU always. + HASSERT(PIN_NUM != 0); + HASSERT(PIN_NUM != 15); + HASSERT(PIN_NUM != 5); +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + // these pins have HW PU always. + HASSERT(PIN_NUM != 0); +#endif // CONFIG_IDF_TARGET_ESP32 + + ESP_ERROR_CHECK(gpio_pullup_dis(pin())); + } + + /// Turns on pullup. + static void set_pulldown_on() + { +#if defined(CONFIG_IDF_TARGET_ESP32) + // these pins have HW PU always. + HASSERT(PIN_NUM != 0); + HASSERT(PIN_NUM != 15); + HASSERT(PIN_NUM != 5); +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + // these pins have HW PU always. + HASSERT(PIN_NUM != 0); +#endif // CONFIG_IDF_TARGET_ESP32 + + ESP_ERROR_CHECK(gpio_pulldown_en(pin())); + } + + /// Turns off pullup. + static void set_pulldown_off() + { +#if defined(CONFIG_IDF_TARGET_ESP32) + // these pins have HW PD always. + HASSERT(PIN_NUM != 12); + HASSERT(PIN_NUM != 4); + HASSERT(PIN_NUM != 2); +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + // these pins have HW PD always. + HASSERT(PIN_NUM != 45); + HASSERT(PIN_NUM != 46); +#endif // CONFIG_IDF_TARGET_ESP32 + + ESP_ERROR_CHECK(gpio_pulldown_dis(pin())); + } + + /// Sets output to HIGH. + static void set_on() + { + ESP_ERROR_CHECK(gpio_set_level(pin(), 1)); + } + + /// Sets output to LOW. + static void set_off() + { + ESP_ERROR_CHECK(gpio_set_level(pin(), 0)); + } + + /// @return input pin level. + static bool get() + { + // If the pin is configured as an output we can not use the ESP-IDF API + // gpio_get_level(pin) since it will return LOW for any output pin. To + // work around this API limitation it is necessary to read directly + // from the memory mapped GPIO registers. + if (is_output()) + { + if (PIN_NUM < 32) + { + return GPIO.out & BIT(PIN_NUM & 31); + } + else + { + return GPIO.out1.data & BIT(PIN_NUM & 31); + } + } + return gpio_get_level(pin()); + } + + /// Set output pin level. @param value is the level to set to. + static void set(bool value) + { + if (value) + { + set_on(); + } + else + { + set_off(); + } + } + + /// Toggles output pin value. + static void toggle() + { + set(!get()); + } + + /// @return true if pin is configured as an output pin. + static bool is_output() + { + // If the pin is a valid output, check if it is configured for output. + // Unfortunately, ESP-IDF does not provide gpio_get_direction(pin) but + // does provide gpio_set_direction(pin, direction). To workaround this + // limitation it is necessary to read directly from the memory mapped + // GPIO registers. + if (GPIO_IS_VALID_OUTPUT_GPIO(PIN_NUM)) + { + // pins 32 and below use the first GPIO controller + if (PIN_NUM < 32) + { + return GPIO.enable & BIT(PIN_NUM & 31); + } + else + { + return GPIO.enable1.data & BIT(PIN_NUM & 31); + } + } + return false; + } + + /// Initializes the underlying hardware pin on the ESP32. + static void hw_init() + { + // sanity check that the pin number is valid + HASSERT(GPIO_IS_VALID_GPIO(PIN_NUM)); + + // configure the pad for GPIO function + gpio_pad_select_gpio(pin()); + + // reset the pin configuration to defaults + ESP_ERROR_CHECK(gpio_reset_pin(pin())); + } + + /// @return an os-indepentent Gpio abstraction instance for use in + /// libraries. + static constexpr const Gpio *instance() + { + return GpioWrapper>::instance(); + } +}; + +/// Parametric GPIO output class. +/// @param Base is the GPIO pin's definition base class, supplied by the +/// GPIO_PIN macro. +/// @param SAFE_VALUE is the initial value for the GPIO output pin. +/// @param INVERT inverts the high/low state of the pin when set. +template +struct GpioOutputPin : public Base +{ +public: + /// Initializes the hardware pin. + static void hw_init() + { + HASSERT(GPIO_IS_VALID_OUTPUT_GPIO(Base::pin())); + Base::hw_init(); + Base::set(SAFE_VALUE); + Base::set_output(); + Base::set(SAFE_VALUE); + } + /// Sets the hardware pin to a safe value. + static void hw_set_to_safe() + { + Base::set(SAFE_VALUE); + } + /// Sets the output pinm @param value if true, output is set to HIGH, if + /// false, output is set to LOW. + static void set(bool value) + { + if (INVERT) + { + Base::set(!value); + } + else + { + Base::set(value); + } + } +}; + +/// Defines a GPIO output pin, initialized to be an output pin with low level. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template +struct GpioOutputSafeLow : public GpioOutputPin +{ +}; + +/// Defines a GPIO output pin, initialized to be an output pin with low +/// level. All set() commands are acted upon by inverting the value. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template +struct GpioOutputSafeLowInvert : public GpioOutputPin +{ +}; + +/// Defines a GPIO output pin, initialized to be an output pin with high level. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template +struct GpioOutputSafeHigh : public GpioOutputPin +{ +}; + +/// Defines a GPIO output pin, initialized to be an output pin with high +/// level. All set() commands are acted upon by inverting the value. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template +struct GpioOutputSafeHighInvert : public GpioOutputPin +{ +}; + +/// Parametric GPIO input class. +/// @param Base is the GPIO pin's definition base class, supplied by the +/// GPIO_PIN macro. +/// @param PUEN is true if the pull-up should be enabled. +/// @param PDEN is true if the pull-down should be enabled. +template struct GpioInputPar : public Base +{ +public: + /// Initializes the hardware pin. + static void hw_init() + { + Base::hw_init(); + Base::set_input(); + if (PUEN) + { + Base::set_pullup_on(); + } + else + { + Base::set_pullup_off(); + } + + if (PDEN) + { + Base::set_pulldown_on(); + } + else + { + Base::set_pulldown_off(); + } + } + /// Sets the hardware pin to a safe state. + static void hw_set_to_safe() + { + hw_init(); + } +}; + +/// Defines a GPIO input pin with pull-up and pull-down disabled. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template struct GpioInputNP : public GpioInputPar +{ +}; + +/// Defines a GPIO input pin with pull-up enabled. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template struct GpioInputPU : public GpioInputPar +{ +}; + +/// Defines a GPIO input pin with pull-down enabled. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template struct GpioInputPD : public GpioInputPar +{ +}; + +/// Defines a GPIO input pin with pull-up and pull-down enabled. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template struct GpioInputPUPD : public GpioInputPar +{ +}; + +/// Helper macro for defining GPIO pins on the ESP32. +/// +/// @param NAME is the basename of the declaration. For NAME==FOO the macro +/// declared FOO_Pin as a structure on which the read-write functions will be +/// available. +/// +/// @param BaseClass is the initialization structure, such as @ref LedPin, or +/// @ref GpioOutputSafeHigh or @ref GpioOutputSafeLow. +/// +/// @param NUM is the pin number, such as 3 (see below for usable range). +/// +/// The ESP32 (includes: WROVER, WROVER-B, PICO-D4) and ESP32-S2 modules have +/// some differences in behavior with GPIO pins and usages as documented below. +/// +/// ESP32: Valid pin range is 0..39 with the following restrictions: +/// - 0 : pull-up resistor on most modules. +/// - 2 : pull-down resistor on most modules. +/// - 1, 3 : UART0, serial console. +/// - 4 : pull-down resistor on most modules. +/// - 5 : pull-up resistor on most modules. +/// - 6 - 11 : connected to flash. +/// - 12 : pull-down resistor on most modules. +/// - 15 : pull-up resistor on most modules. +/// - 16, 17 : used for PSRAM on WROVER/WROVER-B modules. +/// - 37, 38 : not exposed on most modules. +/// - 34 - 39 : these pins are INPUT only. +/// +/// ESP32-S2: Valid pin range is 0..46 with the following restrictions: +/// - 0 : pull-up resistor on most modules. +/// - 22 - 25 : does not exist. +/// - 26 - 32 : connected to flash (GPIO 26 is used by PSRAM on S2-WROVER). +/// - 43, 44 : UART0, serial console. +/// - 45 : pull-down resistor on most modules. +/// - 46 : pull-down resistor on most modules, also INPUT only. +/// +/// Data sheet references: +/// ESP32: https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf +/// ESP32-WROVER: https://www.espressif.com/sites/default/files/documentation/esp32-wrover_datasheet_en.pdf +/// ESP32-WROVER-B: https://www.espressif.com/sites/default/files/documentation/esp32-wrover-b_datasheet_en.pdf +/// ESP32-PICO-D4: https://www.espressif.com/sites/default/files/documentation/esp32-pico-d4_datasheet_en.pdf +/// ESP32-S2: https://www.espressif.com/sites/default/files/documentation/esp32-s2_datasheet_en.pdf +/// ESP32-S2-WROVER: https://www.espressif.com/sites/default/files/documentation/esp32-s2-wrover_esp32-s2-wrover-i_datasheet_en.pdf +/// +/// Example: +/// GPIO_PIN(FOO, GpioOutputSafeLow, 3); +/// ... +/// FOO_Pin::set(true); +#define GPIO_PIN(NAME, BaseClass, NUM) \ + typedef BaseClass> NAME##_Pin + +#endif // _DRIVERS_ESP32GPIO_HXX_ diff --git a/lib/OpenMRNLite/src/freertos_drivers/esp32/Esp32HardwareCanAdapter.hxx b/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32HardwareCanAdapter.hxx similarity index 79% rename from lib/OpenMRNLite/src/freertos_drivers/esp32/Esp32HardwareCanAdapter.hxx rename to components/OpenMRNLite/src/freertos_drivers/esp32/Esp32HardwareCanAdapter.hxx index 2b506f44..5564b904 100644 --- a/lib/OpenMRNLite/src/freertos_drivers/esp32/Esp32HardwareCanAdapter.hxx +++ b/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32HardwareCanAdapter.hxx @@ -41,8 +41,11 @@ #include "freertos_drivers/arduino/Can.hxx" #include #include +#include #include +#include "os/OS.hxx" + namespace openmrn_arduino { /// ESP32 CAN bus status strings, used for periodic status reporting @@ -94,10 +97,10 @@ public: ESP_ERROR_CHECK(can_driver_install( &can_general_config, &can_timing_config, &can_filter_config)); - xTaskCreatePinnedToCore(rx_task, "ESP32-CAN RX", OPENMRN_STACK_SIZE, - this, RX_TASK_PRIORITY, &rxTaskHandle_, tskNO_AFFINITY); - xTaskCreatePinnedToCore(tx_task, "ESP32-CAN TX", OPENMRN_STACK_SIZE, - this, TX_TASK_PRIORITY, &txTaskHandle_, tskNO_AFFINITY); + os_thread_create(&rxTaskHandle_, "CAN-RX", RX_TASK_PRIORITY, + RX_TASK_STACK_SIZE, rx_task, this); + os_thread_create(&txTaskHandle_, "CAN-TX", TX_TASK_PRIORITY, + TX_TASK_STACK_SIZE, tx_task, this); } ~Esp32HardwareCan() @@ -138,11 +141,11 @@ private: /// Handle for the tx_task that converts and transmits can_frame to the /// native can driver. - TaskHandle_t txTaskHandle_; + os_thread_t txTaskHandle_; /// Handle for the rx_task that receives and converts the native can driver /// frames to can_frame. - TaskHandle_t rxTaskHandle_; + os_thread_t rxTaskHandle_; /// Interval at which to print the ESP32 CAN bus status. static constexpr TickType_t STATUS_PRINT_INTERVAL = pdMS_TO_TICKS(10000); @@ -151,35 +154,37 @@ private: /// transmit failure or there is nothing to transmit. static constexpr TickType_t TX_DEFAULT_DELAY = pdMS_TO_TICKS(250); + /// Stack size to allocate for the ESP32 CAN RX task. + static constexpr uint32_t RX_TASK_STACK_SIZE = 2048L; + /// Priority to use for the rx_task. This needs to be higher than the /// tx_task and lower than @ref OPENMRN_TASK_PRIORITY. - static constexpr UBaseType_t RX_TASK_PRIORITY = ESP_TASK_TCPIP_PRIO - 1; + static constexpr UBaseType_t RX_TASK_PRIORITY = ESP_TASK_TCPIP_PRIO - 2; + + /// Stack size to allocate for the ESP32 CAN TX task. + static constexpr uint32_t TX_TASK_STACK_SIZE = 2048L; /// Priority to use for the tx_task. This should be lower than /// @ref RX_TASK_PRIORITY and @ref OPENMRN_TASK_PRIORITY. - static constexpr UBaseType_t TX_TASK_PRIORITY = ESP_TASK_TCPIP_PRIO - 2; + static constexpr UBaseType_t TX_TASK_PRIORITY = ESP_TASK_TCPIP_PRIO - 3; /// Background task that takes care of the conversion of the @ref can_frame /// provided by the @ref txBuf into an ESP32 can_message_t which can be /// processed by the native CAN driver. This task also covers the periodic /// status reporting and BUS recovery when necessary. - static void tx_task(void *can) + static void* tx_task(void *can) { /// Get handle to our parent Esp32HardwareCan object to access the /// txBuf. Esp32HardwareCan *parent = reinterpret_cast(can); - // Add this task to the WDT - esp_task_wdt_add(parent->txTaskHandle_); + LOG(VERBOSE, "Esp32Can: TX startup"); /// Tracks the last time that we displayed the CAN driver status. TickType_t next_status_display_tick_count = 0; while (true) { - // Feed the watchdog so it doesn't reset the ESP32 - esp_task_wdt_reset(); - // periodic CAN driver monitoring and reporting, this takes care of // bus recovery when the CAN driver disables the bus due to error // conditions exceeding thresholds. @@ -236,7 +241,8 @@ private: } /// ESP32 native CAN driver frame - can_message_t msg = {0}; + can_message_t msg; + bzero(&msg, sizeof(can_message_t)); msg.flags = CAN_MSG_FLAG_NONE; msg.identifier = can_frame->can_id; @@ -279,26 +285,24 @@ private: vTaskDelay(TX_DEFAULT_DELAY); } } // loop on task + return nullptr; } /// Background task that takes care of receiving can_message_t objects from /// the ESP32 native CAN driver, when they are available, converting them to /// a @ref can_frame and pushing them to the @ref rxBuf. - static void rx_task(void *can) + static void* rx_task(void *can) { /// Get handle to our parent Esp32HardwareCan object to access the rxBuf Esp32HardwareCan *parent = reinterpret_cast(can); - // Add this task to the WDT - esp_task_wdt_add(parent->rxTaskHandle_); + LOG(VERBOSE, "Esp32Can: RX startup"); while (true) { - // Feed the watchdog so it doesn't reset the ESP32 - esp_task_wdt_reset(); - /// ESP32 native CAN driver frame - can_message_t msg = {0}; + can_message_t msg; + bzero(&msg, sizeof(can_message_t)); if (can_receive(&msg, pdMS_TO_TICKS(250)) != ESP_OK) { // native CAN driver did not give us a frame. @@ -355,12 +359,118 @@ private: parent->rxBuf->advance(1); parent->rxBuf->signal_condition(); } + return nullptr; } DISALLOW_COPY_AND_ASSIGN(Esp32HardwareCan); }; +// When not building in an Arduino env we need to have the CanBridge class +// since OpenMRNLite.h is not used. +#ifndef ARUDINO + +/// Bridge class that connects a native CAN controller to the OpenMRN core +/// stack, sending and receiving CAN frames directly. The CAN controller must +/// have a driver matching the Can controller base class defined in +/// OpenMRN/arduino. +class CanBridge : public Executable +{ +public: + /// Constructor. + /// + /// @param port is the CAN hardware driver implementation. + /// @param can_hub is the core CAN frame router of the OpenMRN stack, + /// usually comes from stack()->can_hub(). + CanBridge(Can *port, CanHubFlow *can_hub) + : port_(port) + , canHub_(can_hub) + { + port_->enable(); + can_hub->register_port(&writePort_); + } + + ~CanBridge() + { + port_->disable(); + } + + /// Called by the loop. + void run() override + { + loop_for_write(); + loop_for_read(); + } + +private: + /// Handles data going out of OpenMRN and towards the CAN port. + void loop_for_write() + { + if (!writeBuffer_) + { + return; + } + if (port_->availableForWrite() <= 0) + { + return; + } + port_->write(writeBuffer_->data()); + writeBuffer_ = nullptr; + writePort_.notify(); + } + + /// Handles data coming from the CAN port. + void loop_for_read() + { + while (port_->available()) + { + auto *b = canHub_->alloc(); + port_->read(b->data()); + b->data()->skipMember_ = &writePort_; + canHub_->send(b); + } + } + + friend class WritePort; + class WritePort : public CanHubPort + { + public: + WritePort(CanBridge *parent) + : CanHubPort(parent->canHub_->service()) + , parent_(parent) + { + } + + Action entry() override + { + parent_->writeBuffer_ = message(); + return wait_and_call(STATE(write_done)); + } + + Action write_done() + { + return release_and_exit(); + } + + private: + CanBridge *parent_; + }; + + /// Hardware driver. + Can *port_; + /// Next buffer we are trying to write into the driver's FIFO. + Buffer *writeBuffer_{nullptr}; + /// Connection to the stack. + CanHubFlow *canHub_; + /// State flow with queues for output frames generated by the stack. + WritePort writePort_{this}; +}; +#endif // ARDUINO + } // namespace openmrn_arduino using openmrn_arduino::Esp32HardwareCan; +#ifndef ARUDINO +using openmrn_arduino::CanBridge; +#endif + #endif /* _FREERTOS_DRIVERS_ARDUINO_ESP32HWCAN_HXX_ */ diff --git a/lib/OpenMRNLite/src/freertos_drivers/esp32/Esp32HardwareSerialAdapter.hxx b/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32HardwareSerialAdapter.hxx similarity index 100% rename from lib/OpenMRNLite/src/freertos_drivers/esp32/Esp32HardwareSerialAdapter.hxx rename to components/OpenMRNLite/src/freertos_drivers/esp32/Esp32HardwareSerialAdapter.hxx diff --git a/lib/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiConfiguration.hxx b/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiConfiguration.hxx similarity index 100% rename from lib/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiConfiguration.hxx rename to components/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiConfiguration.hxx diff --git a/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiManager.cpp b/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiManager.cpp new file mode 100644 index 00000000..c910c5c1 --- /dev/null +++ b/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiManager.cpp @@ -0,0 +1,1523 @@ +/** \copyright + * Copyright (c) 2019, Mike Dunston + * All rights reserved. + * + * 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. + * + * 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. + * + * \file Esp32WiFiManager.cxx + * + * ESP32 WiFi Manager + * + * @author Mike Dunston + * @date 4 February 2019 + */ + +// Ensure we only compile this code for the ESP32 +#ifdef ESP32 + +#include "Esp32WiFiManager.hxx" +#include "os/MDNS.hxx" +#include "utils/FdUtils.hxx" + +#include +#include +#include +#include + +#include +#include +#include + +// ESP-IDF v4+ has a slightly different directory structure to previous +// versions. +#ifdef ESP_IDF_VERSION_MAJOR +// ESP-IDF v4+ +#include +#include +#else +// ESP-IDF v3.x +#include +#include +#endif // ESP_IDF_VERSION_MAJOR + +using openlcb::NodeID; +using openlcb::SimpleCanStack; +using openlcb::TcpAutoAddress; +using openlcb::TcpClientConfig; +using openlcb::TcpClientDefaultParams; +using openlcb::TcpDefs; +using openlcb::TcpManualAddress; +using std::string; +using std::unique_ptr; + +#ifndef ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL +/// Allows setting the log level for mDNS related log messages from +/// @ref DefaultSocketClientParams. +#define ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL INFO +#endif + +#ifndef ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL +/// Allows setting the log level for mDNS results in the @ref mdns_lookup +/// method. +#define ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL INFO +#endif + +// Start of global namespace block. + +// These must be declared *OUTSIDE* the openmrn_arduino namespace in order to +// be visible in the MDNS.cxx code. + +/// Advertises an mDNS service name. This is a hook point for the MDNS class +/// and is used as part of the Esp32 WiFi hub support. +void mdns_publish(const char *name, const char *service, uint16_t port); + +/// Removes advertisement of an mDNS service name. This is not currently +/// exposed in the MDNS class but is supported on the ESP32. +void mdns_unpublish(const char *service); + +/// Splits a service name since the ESP32 mDNS library requires the service +/// name and service protocol to be passed in individually. +/// +/// @param service_name is the service name to be split. +/// @param protocol_name is the protocol portion of the service name. +/// +/// Note: service_name *WILL* be modified by this call. +void split_mdns_service_name(string *service_name, string *protocol_name); + +// End of global namespace block. + +namespace openmrn_arduino +{ + +/// Priority to use for the wifi_manager_task. This is currently set to one +/// level higher than the arduino-esp32 loopTask. The task will be in a sleep +/// state until woken up by Esp32WiFiManager::process_wifi_event, +/// Esp32WiFiManager::apply_configuration or shutdown via destructor. +static constexpr UBaseType_t WIFI_TASK_PRIORITY = 2; + +/// Stack size for the wifi_manager_task. +static constexpr uint32_t WIFI_TASK_STACK_SIZE = 2560L; + +/// Interval at which to check the WiFi connection status. +static constexpr TickType_t WIFI_CONNECT_CHECK_INTERVAL = pdMS_TO_TICKS(5000); + +/// Interval at which to check if the GcTcpHub has started or not. +static constexpr uint32_t HUB_STARTUP_DELAY_USEC = MSEC_TO_USEC(50); + +/// Interval at which to check if the WiFi task has shutdown or not. +static constexpr uint32_t TASK_SHUTDOWN_DELAY_USEC = MSEC_TO_USEC(1); + +/// Bit designator for wifi_status_event_group which indicates we are connected +/// to the SSID. +static constexpr int WIFI_CONNECTED_BIT = BIT0; + +/// Bit designator for wifi_status_event_group which indicates we have an IPv4 +/// address assigned. +static constexpr int WIFI_GOTIP_BIT = BIT1; + +/// Allow up to 36 checks to see if we have connected to the SSID and +/// received an IPv4 address. This allows up to ~3 minutes for the entire +/// process to complete, in most cases this should be complete in under 30 +/// seconds. +static constexpr uint8_t MAX_CONNECTION_CHECK_ATTEMPTS = 36; + +/// This is the number of consecutive IP addresses which will be available in +/// the SoftAP DHCP server IP pool. These will be allocated immediately +/// following the SoftAP IP address (default is 192.168.4.1). Default number to +/// reserve is 48 IP addresses. Only four stations can be connected to the +/// ESP32 SoftAP at any single time. +static constexpr uint8_t SOFTAP_IP_RESERVATION_BLOCK_SIZE = 48; + +/// Event handler for the ESP32 WiFi system. This will receive events from the +/// ESP-IDF event loop processor and pass them on to the Esp32WiFiManager for +/// possible processing. This is only used when Esp32WiFiManager is managing +/// both the WiFi and mDNS systems, if these are managed externally the +/// consumer is responsible for calling Esp32WiFiManager::process_wifi_event +/// when WiFi events occur. +static esp_err_t wifi_event_handler(void *context, system_event_t *event) +{ + auto wifi = static_cast(context); + wifi->process_wifi_event(event); + return ESP_OK; +} + +/// Adapter class to load/store configuration via CDI +class Esp32SocketParams : public DefaultSocketClientParams +{ +public: + Esp32SocketParams( + int fd, const TcpClientConfig &cfg) + : configFd_(fd) + , cfg_(cfg) + { + mdnsService_ = cfg_.auto_address().service_name().read(configFd_); + staticHost_ = cfg_.manual_address().ip_address().read(configFd_); + staticPort_ = CDI_READ_TRIMMED(cfg_.manual_address().port, configFd_); + } + + /// @return search mode for how to locate the server. + SearchMode search_mode() override + { + return (SearchMode)CDI_READ_TRIMMED(cfg_.search_mode, configFd_); + } + + /// @return null or empty string if any mdns server is okay to connect + /// to. If nonempty, then only an mdns server will be chosen that has the + /// specific host name. + string mdns_host_name() override + { + return cfg_.auto_address().host_name().read(configFd_); + } + + /// @return true if first attempt should be to connect to + /// last_host_name:last_port. + bool enable_last() override + { + return CDI_READ_TRIMMED(cfg_.reconnect, configFd_); + } + + /// @return the last successfully used IP address, as dotted + /// decimal. Nullptr or empty if no successful connection has ever been + /// made. + string last_host_name() override + { + return cfg_.last_address().ip_address().read(configFd_); + } + + /// @return the last successfully used port number. + int last_port() override + { + return CDI_READ_TRIMMED(cfg_.last_address().port, configFd_); + } + + /// Stores the last connection details for use when reconnect is enabled. + /// + /// @param hostname is the hostname that was connected to. + /// @param port is the port that was connected to. + void set_last(const char *hostname, int port) override + { + cfg_.last_address().ip_address().write(configFd_, hostname); + cfg_.last_address().port().write(configFd_, port); + } + + void log_message(LogMessage id, const string &arg) override + { + switch (id) + { + case CONNECT_RE: + LOG(INFO, "[Uplink] Reconnecting to %s.", arg.c_str()); + break; + case MDNS_SEARCH: + LOG(ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL, + "[Uplink] Starting mDNS searching for %s.", + arg.c_str()); + break; + case MDNS_NOT_FOUND: + LOG(ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL, + "[Uplink] mDNS search failed."); + break; + case MDNS_FOUND: + LOG(ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL, + "[Uplink] mDNS search succeeded."); + break; + case CONNECT_MDNS: + LOG(INFO, "[Uplink] mDNS connecting to %s.", arg.c_str()); + break; + case CONNECT_MANUAL: + LOG(INFO, "[Uplink] Connecting to %s.", arg.c_str()); + break; + case CONNECT_FAILED_SELF: + LOG(ESP32_WIFIMGR_SOCKETPARAMS_LOG_LEVEL, + "[Uplink] Rejecting attempt to connect to localhost."); + break; + case CONNECTION_LOST: + LOG(INFO, "[Uplink] Connection lost."); + break; + default: + // ignore the message + break; + } + } + + /// @return true if we should actively skip connections that happen to + /// match our own IP address. + bool disallow_local() override + { + return true; + } + +private: + const int configFd_; + const TcpClientConfig cfg_; +}; + +// With this constructor being used the Esp32WiFiManager will manage the +// WiFi connection, mDNS system and the hostname of the ESP32. +Esp32WiFiManager::Esp32WiFiManager(const char *ssid + , const char *password + , SimpleCanStack *stack + , const WiFiConfiguration &cfg + , const char *hostname_prefix + , wifi_mode_t wifi_mode + , tcpip_adapter_ip_info_t *station_static_ip + , ip_addr_t primary_dns_server + , uint8_t soft_ap_channel + , wifi_auth_mode_t soft_ap_auth + , const char *soft_ap_password + , tcpip_adapter_ip_info_t *softap_static_ip) + : DefaultConfigUpdateListener() + , hostname_(hostname_prefix) + , ssid_(ssid) + , password_(password) + , cfg_(cfg) + , manageWiFi_(true) + , stack_(stack) + , wifiMode_(wifi_mode) + , stationStaticIP_(station_static_ip) + , primaryDNSAddress_(primary_dns_server) + , softAPChannel_(soft_ap_channel) + , softAPAuthMode_(soft_ap_auth) + , softAPPassword_(soft_ap_password ? soft_ap_password : password) + , softAPStaticIP_(softap_static_ip) +{ + // Extend the capacity of the hostname to make space for the node-id and + // underscore. + hostname_.reserve(TCPIP_HOSTNAME_MAX_SIZE); + + // Generate the hostname for the ESP32 based on the provided node id. + // node_id : 0x050101011425 + // hostname_ : esp32_050101011425 + NodeID node_id = stack_->node()->node_id(); + hostname_.append(uint64_to_string_hex(node_id, 0)); + + // The maximum length hostname for the ESP32 is 32 characters so truncate + // when necessary. Reference to length limitation: + // https://github.com/espressif/esp-idf/blob/master/components/tcpip_adapter/include/tcpip_adapter.h#L611 + if (hostname_.length() > TCPIP_HOSTNAME_MAX_SIZE) + { + LOG(WARNING, "ESP32 hostname is too long, original hostname: %s", + hostname_.c_str()); + hostname_.resize(TCPIP_HOSTNAME_MAX_SIZE); + LOG(WARNING, "truncated hostname: %s", hostname_.c_str()); + } + + // Release any extra capacity allocated for the hostname. + hostname_.shrink_to_fit(); +} + +// With this constructor being used, it will be the responsibility of the +// application to manage the WiFi and mDNS systems. +Esp32WiFiManager::Esp32WiFiManager( + SimpleCanStack *stack, const WiFiConfiguration &cfg) + : DefaultConfigUpdateListener() + , cfg_(cfg) + , manageWiFi_(false) + , stack_(stack) +{ + // Nothing to do here. +} + +// destructor to ensure cleanup of owned resources +Esp32WiFiManager::~Esp32WiFiManager() +{ + // if we are managing the WiFi connection we need to disconnect from the + // event loop to prevent a possible null deref. + if (manageWiFi_) + { + // disconnect from the event loop + esp_event_loop_set_cb(nullptr, nullptr); + } + + // set flag to shutdown WiFi background task and wake it up + shutdownRequested_ = true; + xTaskNotifyGive(wifiTaskHandle_); + + // wait for WiFi background task shutdown to complete + while (shutdownRequested_) + { + usleep(TASK_SHUTDOWN_DELAY_USEC); + } + + // cleanup event group + vEventGroupDelete(wifiStatusEventGroup_); + + // cleanup internal vectors/maps + ssidScanResults_.clear(); + eventCallbacks_.clear(); + mdnsDeferredPublish_.clear(); +} + +ConfigUpdateListener::UpdateAction Esp32WiFiManager::apply_configuration( + int fd, bool initial_load, BarrierNotifiable *done) +{ + AutoNotify n(done); + LOG(VERBOSE, "Esp32WiFiManager::apply_configuration(%d, %d)", fd, + initial_load); + + // Cache the fd for later use by the wifi background task. + configFd_ = fd; + configReloadRequested_ = initial_load; + + // Load the CDI entry into memory to do an CRC-32 check against our last + // loaded configuration so we can avoid reloading configuration when there + // are no interesting changes. + unique_ptr crcbuf(new uint8_t[cfg_.size()]); + + // If we are unable to seek to the right position in the persistent storage + // give up and request a reboot. + if (lseek(fd, cfg_.offset(), SEEK_SET) != cfg_.offset()) + { + LOG_ERROR("lseek failed to reset fd offset, REBOOT_NEEDED"); + return ConfigUpdateListener::UpdateAction::REBOOT_NEEDED; + } + + // Read the full configuration to the buffer for crc check. + FdUtils::repeated_read(fd, crcbuf.get(), cfg_.size()); + + // Calculate CRC-32 from the loaded buffer. + uint32_t configCrc32 = crc32_le(0, crcbuf.get(), cfg_.size()); + LOG(VERBOSE, "existing config CRC-32: \"%s\", new CRC-32: \"%s\"", + integer_to_string(configCrc32_, 0).c_str(), + integer_to_string(configCrc32, 0).c_str()); + + // if this is not the initial loading of the CDI entry check the CRC-32 + // value and trigger a configuration reload if necessary. + if (!initial_load) + { + if (configCrc32 != configCrc32_) + { + configReloadRequested_ = true; + // If a configuration change has been detected, wake up the + // wifi_manager_task so it can consume the change prior to the next + // wake up interval. + xTaskNotifyGive(wifiTaskHandle_); + } + } + else + { + // This is the initial loading of the CDI entry, start the background + // task that will manage the node's WiFi connection(s). + start_wifi_task(); + } + + // Store the calculated CRC-32 for future use when the apply_configuration + // method is called to detect any configuration changes. + configCrc32_ = configCrc32; + + // Inform the caller that the configuration has been updated as the wifi + // task will reload the configuration as part of it's next wake up cycle. + return ConfigUpdateListener::UpdateAction::UPDATED; +} + +// Factory reset handler for the WiFiConfiguration CDI entry. +void Esp32WiFiManager::factory_reset(int fd) +{ + LOG(VERBOSE, "Esp32WiFiManager::factory_reset(%d)", fd); + + // General WiFi configuration settings. + CDI_FACTORY_RESET(cfg_.sleep); + + // Hub specific configuration settings. + CDI_FACTORY_RESET(cfg_.hub().enable); + CDI_FACTORY_RESET(cfg_.hub().port); + cfg_.hub().service_name().write( + fd, TcpDefs::MDNS_SERVICE_NAME_GRIDCONNECT_CAN_TCP); + + // Node link configuration settings. + CDI_FACTORY_RESET(cfg_.uplink().search_mode); + CDI_FACTORY_RESET(cfg_.uplink().reconnect); + + // Node link manual configuration settings. + cfg_.uplink().manual_address().ip_address().write(fd, ""); + CDI_FACTORY_RESET(cfg_.uplink().manual_address().port); + + // Node link automatic configuration settings. + cfg_.uplink().auto_address().service_name().write( + fd, TcpDefs::MDNS_SERVICE_NAME_GRIDCONNECT_CAN_TCP); + cfg_.uplink().auto_address().host_name().write(fd, ""); + + // Node link automatic last connected node address. + cfg_.uplink().last_address().ip_address().write(fd, ""); + CDI_FACTORY_RESET(cfg_.uplink().last_address().port); + + // Reconnect to last connected node. + CDI_FACTORY_RESET(cfg_.uplink().reconnect); +} + +// Processes a WiFi system event +void Esp32WiFiManager::process_wifi_event(system_event_t *event) +{ + LOG(VERBOSE, "Esp32WiFiManager::process_wifi_event(%d)", event->event_id); + + // We only are interested in this event if we are managing the + // WiFi and MDNS systems and our mode includes STATION. + if (event->event_id == SYSTEM_EVENT_STA_START && manageWiFi_ && + (wifiMode_ == WIFI_MODE_APSTA || wifiMode_ == WIFI_MODE_STA)) + { + // Set the generated hostname prior to connecting to the SSID + // so that it shows up with the generated hostname instead of + // the default "Espressif". + LOG(INFO, "[WiFi] Setting ESP32 hostname to \"%s\".", + hostname_.c_str()); + ESP_ERROR_CHECK(tcpip_adapter_set_hostname( + TCPIP_ADAPTER_IF_STA, hostname_.c_str())); + uint8_t mac[6]; + esp_wifi_get_mac(WIFI_IF_STA, mac); + LOG(INFO, "[WiFi] MAC Address: %s", mac_to_string(mac).c_str()); + + if (stationStaticIP_) + { + // Stop the DHCP service before connecting, this allows us to + // specify a static IP address for the WiFi connection + LOG(INFO, "[DHCP] Stopping DHCP Client (if running)."); + ESP_ERROR_CHECK( + tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA)); + + LOG(INFO, + "[WiFi] Configuring Static IP address:\n" + "IP : " IPSTR "\n" + "Gateway: " IPSTR "\n" + "Netmask: " IPSTR, + IP2STR(&stationStaticIP_->ip), + IP2STR(&stationStaticIP_->gw), + IP2STR(&stationStaticIP_->netmask)); + ESP_ERROR_CHECK( + tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, + stationStaticIP_)); + + // if we do not have a primary DNS address configure the default + if (ip_addr_isany(&primaryDNSAddress_)) + { + IP4_ADDR(&primaryDNSAddress_.u_addr.ip4, 8, 8, 8, 8); + } + LOG(INFO, "[WiFi] Configuring primary DNS address to: " IPSTR, + IP2STR(&primaryDNSAddress_.u_addr.ip4)); + // set the primary server (0) + dns_setserver(0, &primaryDNSAddress_); + } + else + { + // Start the DHCP service before connecting so it hooks into + // the flow early and provisions the IP automatically. + LOG(INFO, "[DHCP] Starting DHCP Client."); + ESP_ERROR_CHECK( + tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA)); + } + + LOG(INFO, + "[WiFi] Station started, attempting to connect to SSID: %s.", + ssid_); + // Start the SSID connection process. + esp_wifi_connect(); + } + else if (event->event_id == SYSTEM_EVENT_STA_CONNECTED) + { + LOG(INFO, "[WiFi] Connected to SSID: %s", ssid_); + // Set the flag that indictes we are connected to the SSID. + xEventGroupSetBits(wifiStatusEventGroup_, WIFI_CONNECTED_BIT); + } + else if (event->event_id == SYSTEM_EVENT_STA_GOT_IP) + { + // Retrieve the configured IP address from the TCP/IP stack. + LOG(INFO, + "[WiFi] IP address is " IPSTR ", starting hub (if enabled) and " + "uplink.", + IP2STR(&event->event_info.got_ip.ip_info.ip)); + + // Start the mDNS system since we have an IP address, the mDNS system + // on the ESP32 requires that the IP address be assigned otherwise it + // will not start the UDP listener. + start_mdns_system(); + + // Set the flag that indictes we have an IPv4 address. + xEventGroupSetBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT); + + // Wake up the wifi_manager_task so it can start connections + // creating connections, this will be a no-op for initial startup. + xTaskNotifyGive(wifiTaskHandle_); + } + else if (event->event_id == SYSTEM_EVENT_STA_LOST_IP) + { + // Clear the flag that indicates we are connected and have an + // IPv4 address. + xEventGroupClearBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT); + // Wake up the wifi_manager_task so it can clean up connections. + xTaskNotifyGive(wifiTaskHandle_); + } + else if (event->event_id == SYSTEM_EVENT_STA_DISCONNECTED) + { + // flag to indicate that we should print the reconnecting log message. + bool was_previously_connected = false; + + // Check if we have already connected, this event can be raised + // even before we have successfully connected during the SSID + // connect process. + if (xEventGroupGetBits(wifiStatusEventGroup_) & WIFI_CONNECTED_BIT) + { + // track that we were connected previously. + was_previously_connected = true; + + LOG(INFO, "[WiFi] Lost connection to SSID: %s (reason:%d)", ssid_ + , event->event_info.disconnected.reason); + // Clear the flag that indicates we are connected to the SSID. + xEventGroupClearBits(wifiStatusEventGroup_, WIFI_CONNECTED_BIT); + // Clear the flag that indicates we have an IPv4 address. + xEventGroupClearBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT); + + // Wake up the wifi_manager_task so it can clean up + // connections. + xTaskNotifyGive(wifiTaskHandle_); + } + + // If we are managing the WiFi and MDNS systems we need to + // trigger the reconnection process at this point. + if (manageWiFi_) + { + if (was_previously_connected) + { + LOG(INFO, "[WiFi] Attempting to reconnect to SSID: %s.", + ssid_); + } + else + { + LOG(INFO, + "[WiFi] Connection failed, reconnecting to SSID: %s.", + ssid_); + } + esp_wifi_connect(); + } + } + else if (event->event_id == SYSTEM_EVENT_AP_START && manageWiFi_) + { + // Set the generated hostname prior to connecting to the SSID + // so that it shows up with the generated hostname instead of + // the default "Espressif". + LOG(INFO, "[SoftAP] Setting ESP32 hostname to \"%s\".", + hostname_.c_str()); + ESP_ERROR_CHECK(tcpip_adapter_set_hostname( + TCPIP_ADAPTER_IF_AP, hostname_.c_str())); + + uint8_t mac[6]; + esp_wifi_get_mac(WIFI_IF_AP, mac); + LOG(INFO, "[SoftAP] MAC Address: %s", mac_to_string(mac).c_str()); + + // If the SoftAP is not configured to use a static IP it will default + // to 192.168.4.1. + if (softAPStaticIP_ && wifiMode_ != WIFI_MODE_STA) + { + // Stop the DHCP server so we can reconfigure it. + LOG(INFO, "[SoftAP] Stopping DHCP Server (if running)."); + ESP_ERROR_CHECK(tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP)); + + LOG(INFO, + "[SoftAP] Configuring Static IP address:\n" + "IP : " IPSTR "\n" + "Gateway: " IPSTR "\n" + "Netmask: " IPSTR, + IP2STR(&softAPStaticIP_->ip), + IP2STR(&softAPStaticIP_->gw), + IP2STR(&softAPStaticIP_->netmask)); + ESP_ERROR_CHECK( + tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, + softAPStaticIP_)); + + // Convert the Soft AP Static IP to a uint32 for manipulation + uint32_t apIP = ntohl(ip4_addr_get_u32(&softAPStaticIP_->ip)); + + // Default configuration is for DHCP addresses to follow + // immediately after the static ip address of the Soft AP. + ip4_addr_t first_ip, last_ip; + ip4_addr_set_u32(&first_ip, htonl(apIP + 1)); + ip4_addr_set_u32(&last_ip, + htonl(apIP + SOFTAP_IP_RESERVATION_BLOCK_SIZE)); + + dhcps_lease_t dhcp_lease { + true, // enable dhcp lease functionality + first_ip, // first ip to assign + last_ip, // last ip to assign + }; + + LOG(INFO, + "[SoftAP] Configuring DHCP Server for IPs: " IPSTR " - " IPSTR, + IP2STR(&dhcp_lease.start_ip), IP2STR(&dhcp_lease.end_ip)); + ESP_ERROR_CHECK( + tcpip_adapter_dhcps_option(TCPIP_ADAPTER_OP_SET, + TCPIP_ADAPTER_REQUESTED_IP_ADDRESS, + (void *)&dhcp_lease, + sizeof(dhcps_lease_t))); + + // Start the DHCP server so it can provide IP addresses to stations + // when they connect. + LOG(INFO, "[SoftAP] Starting DHCP Server."); + ESP_ERROR_CHECK( + tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP)); + } + + // If we are operating in SoftAP mode only we can start the mDNS system + // now, otherwise we need to defer it until the station has received + // it's IP address to avoid reinitializing the mDNS system. + if (wifiMode_ == WIFI_MODE_AP) + { + start_mdns_system(); + } + } + else if (event->event_id == SYSTEM_EVENT_AP_STACONNECTED) + { + LOG(INFO, "[SoftAP aid:%d] %s connected.", + event->event_info.sta_connected.aid, + mac_to_string(event->event_info.sta_connected.mac).c_str()); + } + else if (event->event_id == SYSTEM_EVENT_AP_STADISCONNECTED) + { + LOG(INFO, "[SoftAP aid:%d] %s disconnected.", + event->event_info.sta_disconnected.aid, + mac_to_string(event->event_info.sta_connected.mac).c_str()); + } + else if (event->event_id == SYSTEM_EVENT_SCAN_DONE) + { + { + OSMutexLock l(&ssidScanResultsLock_); + uint16_t num_found{0}; + esp_wifi_scan_get_ap_num(&num_found); + LOG(VERBOSE, "[WiFi] %d SSIDs found via scan", num_found); + ssidScanResults_.resize(num_found); + esp_wifi_scan_get_ap_records(&num_found, ssidScanResults_.data()); +#if LOGLEVEL >= VERBOSE + for (int i = 0; i < num_found; i++) + { + LOG(VERBOSE, "SSID: %s, RSSI: %d, channel: %d" + , ssidScanResults_[i].ssid + , ssidScanResults_[i].rssi, ssidScanResults_[i].primary); + } +#endif + } + if (ssidCompleteNotifiable_) + { + ssidCompleteNotifiable_->notify(); + ssidCompleteNotifiable_ = nullptr; + } + } + + { + OSMutexLock l(&eventCallbacksLock_); + // Pass the event received from ESP-IDF to any registered callbacks. + for(auto callback : eventCallbacks_) + { + callback(event); + } + } +} + +// Set configuration flag that enables the verbose logging. +// Note: this should be called as early as possible to ensure proper logging +// from all esp-wifi code paths. +void Esp32WiFiManager::enable_verbose_logging() +{ + verboseLogging_ = true; + enable_esp_wifi_logging(); +} + +// Set configuration flag controlling SSID connection checking behavior. +void Esp32WiFiManager::wait_for_ssid_connect(bool enable) +{ + waitForStationConnect_ = enable; +} + +// If the Esp32WiFiManager is setup to manage the WiFi system, the following +// steps are executed: +// 1) Start the TCP/IP adapter. +// 2) Hook into the ESP event loop so we receive WiFi events. +// 3) Initialize the WiFi system. +// 4) Set the WiFi mode to STATION (WIFI_STA) +// 5) Configure the WiFi system to store parameters only in memory to avoid +// potential corruption of entries in NVS. +// 6) Configure the WiFi system for SSID/PW. +// 7) Set the hostname based on the generated hostname. +// 8) Connect to WiFi and wait for IP assignment. +// 9) Verify that we connected and received a IP address, if not log a FATAL +// message and give up. +void Esp32WiFiManager::start_wifi_system() +{ + // Create the event group used for tracking connected/disconnected status. + // This is used internally regardless of if we manage the rest of the WiFi + // or mDNS systems. + wifiStatusEventGroup_ = xEventGroupCreate(); + + // If we do not need to manage the WiFi and mDNS systems exit early. + if (!manageWiFi_) + { + return; + } + + // Initialize the TCP/IP adapter stack. + LOG(INFO, "[WiFi] Starting TCP/IP stack"); + tcpip_adapter_init(); + + // Install event loop handler. + ESP_ERROR_CHECK(esp_event_loop_init(wifi_event_handler, this)); + + // Start the WiFi adapter. + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + LOG(INFO, "[WiFi] Initializing WiFi stack"); + + // Disable NVS storage for the WiFi driver + cfg.nvs_enable = false; + + // override the defaults coming from arduino-esp32, the ones below improve + // throughput and stability of TCP/IP, for more info on these values, see: + // https://github.com/espressif/arduino-esp32/issues/2899 and + // https://github.com/espressif/arduino-esp32/pull/2912 + // + // Note: these numbers are slightly higher to allow compatibility with the + // WROVER chip and WROOM-32 chip. The increase results in ~2kb less heap + // at runtime. + // + // These do not require recompilation of arduino-esp32 code as these are + // used in the WIFI_INIT_CONFIG_DEFAULT macro, they simply need to be redefined. + cfg.static_rx_buf_num = 16; + cfg.dynamic_rx_buf_num = 32; + cfg.rx_ba_win = 16; + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + if (verboseLogging_) + { + enable_esp_wifi_logging(); + } + + wifi_mode_t requested_wifi_mode = wifiMode_; + if (wifiMode_ == WIFI_MODE_AP) + { + // override the wifi mode from AP only to AP+STA so we can perform wifi + // scans on demand. + requested_wifi_mode = WIFI_MODE_APSTA; + } + // Set the requested WiFi mode. + ESP_ERROR_CHECK(esp_wifi_set_mode(requested_wifi_mode)); + + // This disables storage of SSID details in NVS which has been shown to be + // problematic at times for the ESP32, it is safer to always pass fresh + // config and have the ESP32 resolve the details at runtime rather than + // use a cached set from NVS. + esp_wifi_set_storage(WIFI_STORAGE_RAM); + + // If we want to host a SoftAP configure it now. + if (wifiMode_ == WIFI_MODE_APSTA || wifiMode_ == WIFI_MODE_AP) + { + wifi_config_t conf; + bzero(&conf, sizeof(wifi_config_t)); + conf.ap.authmode = softAPAuthMode_; + conf.ap.beacon_interval = 100; + conf.ap.channel = softAPChannel_; + conf.ap.max_connection = 4; + if (wifiMode_ == WIFI_MODE_AP) + { + // Configure the SSID for the Soft AP based on the SSID passed to + // the Esp32WiFiManager constructor. + strcpy(reinterpret_cast(conf.ap.ssid), ssid_); + } + else + { + // Configure the SSID for the Soft AP based on the generated + // hostname when operating in WIFI_MODE_APSTA mode. + strcpy(reinterpret_cast(conf.ap.ssid), hostname_.c_str()); + } + + if (password_ && softAPAuthMode_ != WIFI_AUTH_OPEN) + { + strcpy(reinterpret_cast(conf.ap.password), password_); + } + + LOG(INFO, "[WiFi] Configuring SoftAP (SSID: %s)", conf.ap.ssid); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &conf)); + } + + // If we need to connect to an SSID, configure it now. + if (wifiMode_ == WIFI_MODE_APSTA || wifiMode_ == WIFI_MODE_STA) + { + // Configure the SSID details for the station based on the SSID and + // password provided to the Esp32WiFiManager constructor. + wifi_config_t conf; + bzero(&conf, sizeof(wifi_config_t)); + strcpy(reinterpret_cast(conf.sta.ssid), ssid_); + if (password_) + { + strcpy(reinterpret_cast(conf.sta.password), password_); + } + + LOG(INFO, "[WiFi] Configuring Station (SSID: %s)", conf.sta.ssid); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &conf)); + } + + // Start the WiFi stack. This will start the SoftAP and/or connect to the + // SSID based on the configuration set above. + LOG(INFO, "[WiFi] Starting WiFi stack"); + ESP_ERROR_CHECK(esp_wifi_start()); + + // If we need the STATION interface *AND* configured to wait until + // successfully connected to the SSID this code block will wait for up to + // approximately three minutes for an IP address to be assigned. In most + // cases this completes in under thirty seconds. If there is a connection + // failure the esp32 will be restarted via a FATAL error being logged. + if (waitForStationConnect_ && + (wifiMode_ == WIFI_MODE_APSTA || wifiMode_ == WIFI_MODE_STA)) + { + uint8_t attempt = 0; + EventBits_t bits = 0; + uint32_t bit_mask = WIFI_CONNECTED_BIT; + while (++attempt <= MAX_CONNECTION_CHECK_ATTEMPTS) + { + // If we have connected to the SSID we then are waiting for IP + // address. + if (bits & WIFI_CONNECTED_BIT) + { + LOG(INFO, "[IPv4] [%d/%d] Waiting for IP address assignment.", + attempt, MAX_CONNECTION_CHECK_ATTEMPTS); + } + else + { + // Waiting for SSID connection + LOG(INFO, "[WiFi] [%d/%d] Waiting for SSID connection.", + attempt, MAX_CONNECTION_CHECK_ATTEMPTS); + } + bits = xEventGroupWaitBits(wifiStatusEventGroup_, + bit_mask, // bits we are interested in + pdFALSE, // clear on exit + pdTRUE, // wait for all bits + WIFI_CONNECT_CHECK_INTERVAL); + // Check if have connected to the SSID + if (bits & WIFI_CONNECTED_BIT) + { + // Since we have connected to the SSID we now need to track + // that we get an IP. + bit_mask |= WIFI_GOTIP_BIT; + } + // Check if we have received an IP. + if (bits & WIFI_GOTIP_BIT) + { + break; + } + } + + // Check if we successfully connected or not. If not, force a reboot. + if ((bits & WIFI_CONNECTED_BIT) != WIFI_CONNECTED_BIT) + { + LOG(FATAL, "[WiFi] Failed to connect to SSID: %s.", ssid_); + } + + // Check if we successfully connected or not. If not, force a reboot. + if ((bits & WIFI_GOTIP_BIT) != WIFI_GOTIP_BIT) + { + LOG(FATAL, "[IPv4] Timeout waiting for an IP."); + } + } +} + +// Starts a background task for the Esp32WiFiManager. +void Esp32WiFiManager::start_wifi_task() +{ + LOG(INFO, "[WiFi] Starting WiFi Manager task"); + os_thread_create(&wifiTaskHandle_, "Esp32WiFiMgr", WIFI_TASK_PRIORITY, + WIFI_TASK_STACK_SIZE, wifi_manager_task, this); +} + +// Background task for the Esp32WiFiManager. This handles all outbound +// connection attempts, configuration loading and making this node as a hub. +void *Esp32WiFiManager::wifi_manager_task(void *param) +{ + Esp32WiFiManager *wifi = static_cast(param); + + // Start the WiFi system before proceeding with remaining tasks. + wifi->start_wifi_system(); + + while (!wifi->shutdownRequested_) + { + EventBits_t bits = xEventGroupGetBits(wifi->wifiStatusEventGroup_); + if (bits & WIFI_GOTIP_BIT) + { + // If we do not have not an uplink connection force a config reload + // to start the connection process. + if (!wifi->uplink_) + { + wifi->configReloadRequested_ = true; + } + } + else + { + // Since we do not have an IP address we need to shutdown any + // active connections since they will be invalid until a new IP + // has been provisioned. + wifi->stop_hub(); + wifi->stop_uplink(); + + // Make sure we don't try and reload configuration since we can't + // create outbound connections at this time. + wifi->configReloadRequested_ = false; + } + + if (wifi->shutdownRequested_) + { + LOG(INFO, "[WiFi] Shutdown requested, stopping background thread."); + break; + } + + // Check if there are configuration changes to pick up. + if (wifi->configReloadRequested_) + { + // Since we are loading configuration data, shutdown the hub and + // uplink if created previously. + wifi->stop_hub(); + wifi->stop_uplink(); + + if (CDI_READ_TRIMMED(wifi->cfg_.sleep, wifi->configFd_)) + { + // When sleep is enabled this will trigger the WiFi system to + // only wake up every DTIM period to receive beacon updates. + // no data loss is expected for this setting but it does delay + // receiption until the DTIM period. + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); + } + else + { + // When sleep is disabled the WiFi radio will always be active. + // This will increase power consumption of the ESP32 but it + // will result in a more reliable behavior when the ESP32 is + // connected to an always-on power supply (ie: not a battery). + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); + } + + if (CDI_READ_TRIMMED(wifi->cfg_.hub().enable, wifi->configFd_)) + { + // Since hub mode is enabled start the hub creation process. + wifi->start_hub(); + } + // Start the uplink connection process in the background. + wifi->start_uplink(); + wifi->configReloadRequested_ = false; + } + + // Sleep until we are woken up again for configuration update or WiFi + // event. + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + } + + // Stop the hub and uplink (if they are active) + wifi->stop_hub(); + wifi->stop_uplink(); + + // reset flag to indicate we have shutdown + wifi->shutdownRequested_ = false; + + return nullptr; +} + +// Shuts down the hub listener (if enabled and running) for this node. +void Esp32WiFiManager::stop_hub() +{ + if (hub_) + { + mdns_unpublish(hubServiceName_); + LOG(INFO, "[Hub] Shutting down TCP/IP listener"); + hub_.reset(nullptr); + } +} + +// Creates a hub listener for this node after loading configuration details. +void Esp32WiFiManager::start_hub() +{ + hubServiceName_ = cfg_.hub().service_name().read(configFd_); + uint16_t hub_port = CDI_READ_TRIMMED(cfg_.hub().port, configFd_); + + LOG(INFO, "[Hub] Starting TCP/IP listener on port %d", hub_port); + hub_.reset(new GcTcpHub(stack_->can_hub(), hub_port)); + + // wait for the hub to complete it's startup tasks + while (!hub_->is_started()) + { + usleep(HUB_STARTUP_DELAY_USEC); + } + mdns_publish(hubServiceName_, hub_port); +} + +// Disconnects and shuts down the uplink connector socket if running. +void Esp32WiFiManager::stop_uplink() +{ + if (uplink_) + { + LOG(INFO, "[Uplink] Disconnecting from uplink."); + uplink_->shutdown(); + uplink_.reset(nullptr); + } +} + +// Creates an uplink connector socket that will automatically add the uplink to +// the node's hub. +void Esp32WiFiManager::start_uplink() +{ + unique_ptr params( + new Esp32SocketParams(configFd_, cfg_.uplink())); + uplink_.reset(new SocketClient(stack_->service(), stack_->executor(), + stack_->executor(), std::move(params), + std::bind(&Esp32WiFiManager::on_uplink_created, this, + std::placeholders::_1, std::placeholders::_2))); +} + +// Converts the passed fd into a GridConnect port and adds it to the stack. +void Esp32WiFiManager::on_uplink_created(int fd, Notifiable *on_exit) +{ + LOG(INFO, "[Uplink] Connected to hub, configuring GridConnect port."); + + const bool use_select = + (config_gridconnect_tcp_use_select() == CONSTANT_TRUE); + + // create the GridConnect port from the provided socket fd. + create_gc_port_for_can_hub(stack_->can_hub(), fd, on_exit, use_select); + + // restart the stack to kick off alias allocation and send node init + // packets. + stack_->restart_stack(); +} + +// Enables the ESP-IDF wifi module logging at verbose level, will also set the +// sub-modules to verbose if they are available. +void Esp32WiFiManager::enable_esp_wifi_logging() +{ + esp_log_level_set("wifi", ESP_LOG_VERBOSE); + +// arduino-esp32 1.0.2 uses ESP-IDF 3.2 which does not have these two methods +// in the headers, they are only available in ESP-IDF 3.3. +#if defined(WIFI_LOG_SUBMODULE_ALL) + esp_wifi_internal_set_log_level(WIFI_LOG_VERBOSE); + esp_wifi_internal_set_log_mod( + WIFI_LOG_MODULE_ALL, WIFI_LOG_SUBMODULE_ALL, true); +#endif // WIFI_LOG_SUBMODULE_ALL +} + +// Starts a background scan of SSIDs that can be seen by the ESP32. +void Esp32WiFiManager::start_ssid_scan(Notifiable *n) +{ + clear_ssid_scan_results(); + std::swap(ssidCompleteNotifiable_, n); + // If there was a previous notifiable notify it now, there will be no + // results but that should be fine since a new scan will be started. + if (n) + { + n->notify(); + } + // Start an active scan all channels, 120ms per channel (defaults) + wifi_scan_config_t cfg; + bzero(&cfg, sizeof(wifi_scan_config_t)); + // The boolean flag when set to false triggers an async scan. + ESP_ERROR_CHECK(esp_wifi_scan_start(&cfg, false)); +} + +// Returns the number of SSIDs found in the last scan. +size_t Esp32WiFiManager::get_ssid_scan_result_count() +{ + OSMutexLock l(&ssidScanResultsLock_); + return ssidScanResults_.size(); +} + +// Returns one SSID record from the last scan. +wifi_ap_record_t Esp32WiFiManager::get_ssid_scan_result(size_t index) +{ + OSMutexLock l(&ssidScanResultsLock_); + wifi_ap_record_t record = wifi_ap_record_t(); + if (index < ssidScanResults_.size()) + { + record = ssidScanResults_[index]; + } + return record; +} + +// Clears all cached SSID scan results. +void Esp32WiFiManager::clear_ssid_scan_results() +{ + OSMutexLock l(&ssidScanResultsLock_); + ssidScanResults_.clear(); +} + +// Advertises a service via mDNS. +// +// If mDNS has not yet been initialized the data will be cached and replayed +// after mDNS has been initialized. +void Esp32WiFiManager::mdns_publish(string service, const uint16_t port) +{ + { + OSMutexLock l(&mdnsInitLock_); + if (!mdnsInitialized_) + { + // since mDNS has not been initialized, store this publish until + // it has been initialized. + mdnsDeferredPublish_[service] = port; + return; + } + } + + // Schedule the publish to be done through the Executor since we may need + // to retry it. + stack_->executor()->add(new CallbackExecutable([service, port]() + { + string service_name = service; + string protocol_name; + split_mdns_service_name(&service_name, &protocol_name); + esp_err_t res = mdns_service_add( + NULL, service_name.c_str(), protocol_name.c_str(), port, NULL, 0); + LOG(VERBOSE, "[mDNS] mdns_service_add(%s.%s:%d): %s." + , service_name.c_str(), protocol_name.c_str(), port + , esp_err_to_name(res)); + // ESP_FAIL will be triggered if there is a timeout during publish of + // the new mDNS entry. The mDNS task runs at a very low priority on the + // PRO_CPU which is also where the OpenMRN Executor runs from which can + // cause a race condition. + if (res == ESP_FAIL) + { + // Send it back onto the scheduler to be retried + Singleton::instance()->mdns_publish(service + , port); + } + else + { + LOG(INFO, "[mDNS] Advertising %s.%s:%d.", service_name.c_str() + , protocol_name.c_str(), port); + } + })); +} + +// Removes advertisement of a service from mDNS. +void Esp32WiFiManager::mdns_unpublish(string service) +{ + { + OSMutexLock l(&mdnsInitLock_); + if (!mdnsInitialized_) + { + // Since mDNS is not in an initialized state we can discard the + // unpublish event. + return; + } + } + string service_name = service; + string protocol_name; + split_mdns_service_name(&service_name, &protocol_name); + LOG(INFO, "[mDNS] Removing advertisement of %s.%s." + , service_name.c_str(), protocol_name.c_str()); + esp_err_t res = + mdns_service_remove(service_name.c_str(), protocol_name.c_str()); + LOG(VERBOSE, "[mDNS] mdns_service_remove: %s.", esp_err_to_name(res)); +} + +// Initializes the mDNS system on the ESP32. +// +// After initialization, if any services are pending publish they will be +// published at this time. +void Esp32WiFiManager::start_mdns_system() +{ + // if we are managing the WiFi stack start mDNS if it hasn't already been + // started previously. + if (manageWiFi_) + { + OSMutexLock l(&mdnsInitLock_); + // If we have already initialized mDNS we can exit early. + if (mdnsInitialized_) + { + return; + } + + // Initialize the mDNS system. + LOG(INFO, "[mDNS] Initializing mDNS system"); + ESP_ERROR_CHECK(mdns_init()); + + // Set the mDNS hostname based on our generated hostname so it can be + // found by other nodes. + LOG(INFO, "[mDNS] Setting mDNS hostname to \"%s\"", hostname_.c_str()); + ESP_ERROR_CHECK(mdns_hostname_set(hostname_.c_str())); + + // Set the default mDNS instance name to the generated hostname. + ESP_ERROR_CHECK(mdns_instance_name_set(hostname_.c_str())); + + // Set flag to indicate we have initialized mDNS. + mdnsInitialized_ = true; + } + + // Publish any deferred mDNS entries + for (auto & entry : mdnsDeferredPublish_) + { + mdns_publish(entry.first, entry.second); + } + mdnsDeferredPublish_.clear(); +} + +} // namespace openmrn_arduino + +/// Maximum number of milliseconds to wait for mDNS query responses. +static constexpr uint32_t MDNS_QUERY_TIMEOUT = 2000; + +/// Maximum number of results to capture for mDNS query requests. +static constexpr size_t MDNS_MAX_RESULTS = 10; + +// Advertises an mDNS service name. +void mdns_publish(const char *name, const char *service, uint16_t port) +{ + if (Singleton::exists()) + { + // The name parameter is unused today. + Singleton::instance()->mdns_publish(service, port); + } +} + +// Removes advertisement of an mDNS service name. +void mdns_unpublish(const char *service) +{ + if (Singleton::exists()) + { + Singleton::instance()->mdns_unpublish(service); + } +} + +// Splits an mDNS service name. +void split_mdns_service_name(string *service_name, string *protocol_name) +{ + HASSERT(service_name != nullptr); + HASSERT(protocol_name != nullptr); + + // if the string is not blank and contains a period split it on the period. + if (service_name->length() && service_name->find('.', 0) != string::npos) + { + string::size_type split_loc = service_name->find('.', 0); + protocol_name->assign(service_name->substr(split_loc + 1)); + service_name->resize(split_loc); + } +} + +// EAI_AGAIN may not be defined on the ESP32 +#ifndef EAI_AGAIN +#ifdef TRY_AGAIN +#define EAI_AGAIN TRY_AGAIN +#else +#define EAI_AGAIN -3 +#endif +#endif // EAI_AGAIN + +// Looks for an mDNS service name and converts the results of the query to an +// addrinfo struct. +int mdns_lookup( + const char *service, struct addrinfo *hints, struct addrinfo **addr) +{ + unique_ptr ai(new struct addrinfo); + if (ai.get() == nullptr) + { + LOG_ERROR("[mDNS] Allocation failed for addrinfo."); + return EAI_MEMORY; + } + bzero(ai.get(), sizeof(struct addrinfo)); + + unique_ptr sa(new struct sockaddr); + if (sa.get() == nullptr) + { + LOG_ERROR("[mDNS] Allocation failed for sockaddr."); + return EAI_MEMORY; + } + bzero(sa.get(), sizeof(struct sockaddr)); + + struct sockaddr_in *sa_in = (struct sockaddr_in *)sa.get(); + ai->ai_flags = 0; + ai->ai_family = hints->ai_family; + ai->ai_socktype = hints->ai_socktype; + ai->ai_protocol = hints->ai_protocol; + ai->ai_addrlen = sizeof(struct sockaddr_in); + sa_in->sin_len = sizeof(struct sockaddr_in); + sa_in->sin_family = hints->ai_family; + + string service_name = service; + string protocol_name; + split_mdns_service_name(&service_name, &protocol_name); + + mdns_result_t *results = NULL; + if (ESP_ERROR_CHECK_WITHOUT_ABORT( + mdns_query_ptr(service_name.c_str(), + protocol_name.c_str(), + MDNS_QUERY_TIMEOUT, + MDNS_MAX_RESULTS, + &results))) + { + // failed to find any matches + return EAI_FAIL; + } + + if (!results) + { + // failed to find any matches + LOG(ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL, + "[mDNS] No matches found for service: %s.", + service); + return EAI_AGAIN; + } + + // make a copy of the results to preserve the original list for cleanup. + mdns_result_t *res = results; + // scan the mdns query results linked list, the first match with an IPv4 + // address will be returned. + bool match_found = false; + while (res && !match_found) + { + mdns_ip_addr_t *ipaddr = res->addr; + while (ipaddr && !match_found) + { + // if this result has an IPv4 address process it + if (ipaddr->addr.type == IPADDR_TYPE_V4) + { + LOG(ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL, + "[mDNS] Found %s as providing service: %s on port %d.", + res->hostname, service, res->port); + inet_addr_from_ip4addr( + &sa_in->sin_addr, &ipaddr->addr.u_addr.ip4); + sa_in->sin_port = htons(res->port); + match_found = true; + } + ipaddr = ipaddr->next; + } + res = res->next; + } + + // free up the query results linked list. + mdns_query_results_free(results); + + if (!match_found) + { + LOG(ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL, + "[mDNS] No matches found for service: %s.", + service); + return EAI_AGAIN; + } + + // return the resolved data to the caller + *addr = ai.release(); + (*addr)->ai_addr = sa.release(); + + // successfully resolved an address, inform the caller + return 0; +} + +// The functions below are not available via the standard ESP-IDF provided +// API. + +/// Retrieves the IPv4 address from the ESP32 station interface. +/// +/// @param ifap will hold the IPv4 address for the ESP32 station interface when +/// successfully retrieved. +/// @return zero for success, -1 for failure. +int getifaddrs(struct ifaddrs **ifap) +{ + tcpip_adapter_ip_info_t ip_info; + + /* start with something "safe" in case we bail out early */ + *ifap = nullptr; + + if (!tcpip_adapter_is_netif_up(TCPIP_ADAPTER_IF_STA)) + { + // Station TCP/IP interface is not up + errno = ENODEV; + return -1; + } + + // allocate memory for various pieces of ifaddrs + std::unique_ptr ia(new struct ifaddrs); + if (ia.get() == nullptr) + { + errno = ENOMEM; + return -1; + } + bzero(ia.get(), sizeof(struct ifaddrs)); + std::unique_ptr ifa_name(new char[6]); + if (ifa_name.get() == nullptr) + { + errno = ENOMEM; + return -1; + } + strcpy(ifa_name.get(), "wlan0"); + std::unique_ptr ifa_addr(new struct sockaddr); + if (ifa_addr == nullptr) + { + errno = ENOMEM; + return -1; + } + bzero(ifa_addr.get(), sizeof(struct sockaddr)); + + // retrieve TCP/IP address from the interface + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info); + + // copy address into ifaddrs structure + struct sockaddr_in *addr_in = (struct sockaddr_in *)ifa_addr.get(); + addr_in->sin_family = AF_INET; + addr_in->sin_addr.s_addr = ip_info.ip.addr; + ia.get()->ifa_next = nullptr; + ia.get()->ifa_name = ifa_name.release(); + ia.get()->ifa_flags = 0; + ia.get()->ifa_addr = ifa_addr.release(); + ia.get()->ifa_netmask = nullptr; + ia.get()->ifa_ifu.ifu_broadaddr = nullptr; + ia.get()->ifa_data = nullptr; + + // report results + *ifap = ia.release(); + return 0; +} + +/// Frees memory allocated as part of the call to @ref getifaddrs. +/// +/// @param ifa is the ifaddrs struct to be freed. +void freeifaddrs(struct ifaddrs *ifa) +{ + while (ifa) + { + struct ifaddrs *next = ifa->ifa_next; + + HASSERT(ifa->ifa_data == nullptr); + HASSERT(ifa->ifa_ifu.ifu_broadaddr == nullptr); + HASSERT(ifa->ifa_netmask == nullptr); + + delete ifa->ifa_addr; + delete[] ifa->ifa_name; + delete ifa; + + ifa = next; + } +} + +/// @return the string equivalant of the passed error code. +const char *gai_strerror(int __ecode) +{ + switch (__ecode) + { + default: + return "gai_strerror unknown"; + case EAI_AGAIN: + return "temporary failure"; + case EAI_FAIL: + return "non-recoverable failure"; + case EAI_MEMORY: + return "memory allocation failure"; + } +} + +#endif // ESP32 diff --git a/lib/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiManager.hxx b/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiManager.hxx similarity index 54% rename from lib/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiManager.hxx rename to components/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiManager.hxx index 854f0eb6..226d3a0c 100644 --- a/lib/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiManager.hxx +++ b/components/OpenMRNLite/src/freertos_drivers/esp32/Esp32WiFiManager.hxx @@ -42,11 +42,14 @@ #include "openlcb/TcpDefs.hxx" #include "utils/ConfigUpdateListener.hxx" #include "utils/GcTcpHub.hxx" +#include "utils/Singleton.hxx" #include "utils/SocketClient.hxx" #include "utils/SocketClientParams.hxx" #include "utils/macros.h" #include +#include +#include namespace openmrn_arduino { @@ -67,6 +70,7 @@ namespace openmrn_arduino /// OpenMRN::begin() which will trigger the loading of the node configuration /// which will trigger the management of the hub and uplink functionality. class Esp32WiFiManager : public DefaultConfigUpdateListener + , public Singleton { public: /// Constructor. @@ -85,11 +89,46 @@ public: /// @param cfg is the WiFiConfiguration instance used for this node. This /// will be monitored for changes and the WiFi behavior altered /// accordingly. + /// @param hostname_prefix is the hostname prefix to use for this node. + /// The @ref NodeID will be appended to this value. The maximum length for + /// final hostname is 32 bytes. + /// @param wifi_mode is the WiFi operating mode. When set to WIFI_MODE_STA + /// the Esp32WiFiManager will attempt to connect to the provided WiFi SSID. + /// When the wifi_mode is WIFI_MODE_AP the Esp32WiFiManager will create an + /// AP with the provided SSID and PASSWORD. When the wifi_mode is + /// WIFI_MODE_APSTA the Esp32WiFiManager will connect to the provided WiFi + /// AP and create an AP with the SSID of "" and the provided + /// password. Note, the password for the AP will not be used if + /// soft_ap_auth is set to WIFI_AUTH_OPEN (default). + /// @param station_static_ip is the static IP configuration to use for the + /// Station WiFi connection. If not specified DHCP will be used instead. + /// @param primary_dns_server is the primary DNS server to use when a + /// static IP address is being used. If left as the default (ip_addr_any) + /// the Esp32WiFiManager will use 8.8.8.8 if using a static IP address. + /// @param soft_ap_channel is the WiFi channel to use for the SoftAP. + /// @param soft_ap_auth is the authentication mode for the AP when + /// wifi_mode is set to WIFI_MODE_AP or WIFI_MODE_APSTA. + /// @param soft_ap_password will be used as the password for the SoftAP, + /// if null and soft_ap_auth is not WIFI_AUTH_OPEN password will be used. + /// If provided, this must stay alive forever. + /// @param softap_static_ip is the static IP configuration for the SoftAP, + /// when not specified the SoftAP will have an IP address of 192.168.4.1. /// /// Note: Both ssid and password must remain in memory for the duration of /// node uptime. - Esp32WiFiManager(const char *ssid, const char *password, - openlcb::SimpleCanStack *stack, const WiFiConfiguration &cfg); + Esp32WiFiManager(const char *ssid + , const char *password + , openlcb::SimpleCanStack *stack + , const WiFiConfiguration &cfg + , const char *hostname_prefix = "esp32_" + , wifi_mode_t wifi_mode = WIFI_MODE_STA + , tcpip_adapter_ip_info_t *station_static_ip = nullptr + , ip_addr_t primary_dns_server = ip_addr_any + , uint8_t soft_ap_channel = 1 + , wifi_auth_mode_t soft_ap_auth = WIFI_AUTH_OPEN + , const char *soft_ap_password = nullptr + , tcpip_adapter_ip_info_t *softap_static_ip = nullptr + ); /// Constructor. /// @@ -106,6 +145,9 @@ public: Esp32WiFiManager( openlcb::SimpleCanStack *stack, const WiFiConfiguration &cfg); + /// Destructor. + ~Esp32WiFiManager(); + /// Updates the WiFiConfiguration settings used by this node. /// /// @param fd is the file descriptor used for the configuration settings. @@ -126,19 +168,73 @@ public: /// @param fd is the file descriptor used for the configuration settings. void factory_reset(int fd) override; - /// Processes an Esp32 WiFi event based on the event_id raised by the + /// Processes an ESP-IDF WiFi event based on the event raised by the /// ESP-IDF event loop processor. This should be used when the - /// Esp32WiFiManager is not managing the WiFi or MDNS systems so that it - /// can react to WiFi events to cleanup or recreate the hub or uplink - /// connections as required. + /// Esp32WiFiManager is not managing the WiFi or MDNS systems so that + /// it can react to WiFi events to cleanup or recreate the hub or uplink + /// connections as required. When Esp32WiFiManager is managing the WiFi + /// connection this method will be called automatically from the + /// esp_event_loop. Note that ESP-IDF only supports one callback being + /// registered. /// - /// @param event_id is the system_event_t.event_id value. - void process_wifi_event(int event_id); + /// @param event is the system_event_t raised by ESP-IDF. + void process_wifi_event(system_event_t *event); - /// If called, setsthe ESP32 wifi stack to log verbose information to the + /// Adds a callback to receive WiFi events as they are received/processed + /// by the Esp32WiFiManager. + /// + /// @param callback is the callback to invoke when events are received, + /// the only parameter is the system_event_t that was received. + void add_event_callback(std::function callback) + { + OSMutexLock l(&eventCallbacksLock_); + eventCallbacks_.emplace_back(std::move(callback)); + } + + /// If called, sets the ESP32 wifi stack to log verbose information to the /// ESP32 serial port. void enable_verbose_logging(); + /// Starts a scan for available SSIDs. + /// + /// @param n is the @ref Notifiable to notify when the SSID scan completes. + void start_ssid_scan(Notifiable *n); + + /// @return the number of SSIDs that were found via the scan. + size_t get_ssid_scan_result_count(); + + /// Returns one entry from the SSID scan. + /// + /// @param index is the index of the SSID to retrieve. If the index is + /// invalid or no records exist a blank wifi_ap_record_t will be returned. + wifi_ap_record_t get_ssid_scan_result(size_t index); + + /// Clears the SSID scan results. + void clear_ssid_scan_results(); + + /// Advertises a service via mDNS. + /// + /// @param service is the service name to publish. + /// @param port is the port for the service to be published. + /// + /// Note: This will schedule a @ref CallbackExecutable on the @ref Executor + /// used by the @ref SimpleCanStack. + void mdns_publish(std::string service, uint16_t port); + + /// Removes the advertisement of a service via mDNS. + /// + /// @param service is the service name to remove from advertising. + void mdns_unpublish(std::string service); + + /// Forces the Esp32WiFiManager to wait until SSID connection completes. + /// + /// By default the ESP32 will be restarted if the SSID connection attempt + /// has not completed after approximately three minutes. When the SoftAP + /// interface is also active an application may opt to disable this + /// functionality to allow a configuration portal to be displayed instead + /// of requiring the firmware to be rebuilt with a new SSID/PW. + void wait_for_ssid_connect(bool enable); + private: /// Default constructor. Esp32WiFiManager(); @@ -185,12 +281,16 @@ private: /// available. void enable_esp_wifi_logging(); + /// Initializes the mDNS system if it hasn't already been initialized. + void start_mdns_system(); + /// Handle for the wifi_manager_task that manages the WiFi stack, including /// periodic health checks of the connected hubs or clients. os_thread_t wifiTaskHandle_; - /// Dynamically generated hostname for this node, esp32_{node-id}. - std::string hostname_{"esp32_"}; + /// Dynamically generated hostname for this node, esp32_{node-id}. This is + /// also used for the SoftAP SSID name (if enabled). + std::string hostname_; /// User provided SSID to connect to. const char *ssid_; @@ -205,9 +305,33 @@ private: /// some environments this may be managed externally. const bool manageWiFi_; - /// OpenMRN stack for the Arduino system + /// OpenMRN stack for the Arduino system. openlcb::SimpleCanStack *stack_; + /// WiFi operating mode. + wifi_mode_t wifiMode_{WIFI_MODE_STA}; + + /// Static IP Address configuration for the Station connection. + tcpip_adapter_ip_info_t *stationStaticIP_{nullptr}; + + /// Primary DNS Address to use when configured for Static IP. + ip_addr_t primaryDNSAddress_{ip_addr_any}; + + /// Channel to use for the SoftAP interface. + uint8_t softAPChannel_{1}; + + /// Authentication mode to use for the SoftAP. If not set to WIFI_AUTH_OPEN + /// @ref softAPPassword_ will be used. + wifi_auth_mode_t softAPAuthMode_{WIFI_AUTH_OPEN}; + + /// User provided password for the SoftAP when active, defaults to + /// @ref password when null and softAPAuthMode_ is not WIFI_AUTH_OPEN. + const char *softAPPassword_; + + /// Static IP Address configuration for the SoftAP. + /// Default static IP provided by ESP-IDF is 192.168.4.1. + tcpip_adapter_ip_info_t *softAPStaticIP_{nullptr}; + /// Cached copy of the file descriptor passed into apply_configuration. /// This is internally used by the wifi_manager_task to processed deferred /// configuration load. @@ -220,21 +344,54 @@ private: /// Internal flag to request the wifi_manager_task reload configuration. bool configReloadRequested_{true}; - /// if true, request esp32 wifi to do verbose logging. - bool esp32VerboseLogging_{false}; + /// Internal flag to request the wifi_manager_task to shutdown. + bool shutdownRequested_{false}; + + /// If true, request esp32 wifi to do verbose logging. + bool verboseLogging_{false}; + + /// If true, the esp32 will block startup until the SSID connection has + /// successfully completed and upon failure (or timeout) the esp32 will be + /// restarted. + bool waitForStationConnect_{true}; /// @ref GcTcpHub for this node's hub if enabled. std::unique_ptr hub_; /// mDNS service name being advertised by the hub, if enabled. - std::string hubServiceName_{""}; + std::string hubServiceName_; /// @ref SocketClient for this node's uplink. std::unique_ptr uplink_; + /// Collection of registered WiFi event callback handlers. + std::vector> eventCallbacks_; + + /// Protects eventCallbacks_ vector. + OSMutex eventCallbacksLock_; + /// Internal event group used to track the IP assignment events. EventGroupHandle_t wifiStatusEventGroup_; + /// WiFi SSID scan results holder. + std::vector ssidScanResults_; + + /// Protects ssidScanResults_ vector. + OSMutex ssidScanResultsLock_; + + /// Notifiable to be called when SSID scan completes. + Notifiable *ssidCompleteNotifiable_{nullptr}; + + /// Protects the mdnsInitialized_ flag and mdnsDeferredPublish_ map. + OSMutex mdnsInitLock_; + + /// Internal flag for tracking that the mDNS system has been initialized. + bool mdnsInitialized_{false}; + + /// Internal holder for mDNS entries which could not be published due to + /// mDNS not being initialized yet. + std::map mdnsDeferredPublish_; + DISALLOW_COPY_AND_ASSIGN(Esp32WiFiManager); }; diff --git a/lib/OpenMRNLite/src/freertos_includes.h b/components/OpenMRNLite/src/freertos_includes.h similarity index 100% rename from lib/OpenMRNLite/src/freertos_includes.h rename to components/OpenMRNLite/src/freertos_includes.h diff --git a/lib/OpenMRNLite/src/ifaddrs.h b/components/OpenMRNLite/src/ifaddrs.h similarity index 100% rename from lib/OpenMRNLite/src/ifaddrs.h rename to components/OpenMRNLite/src/ifaddrs.h diff --git a/lib/OpenMRNLite/src/nmranet_config.h b/components/OpenMRNLite/src/nmranet_config.h similarity index 96% rename from lib/OpenMRNLite/src/nmranet_config.h rename to components/OpenMRNLite/src/nmranet_config.h index e23a20ad..116ea427 100644 --- a/lib/OpenMRNLite/src/nmranet_config.h +++ b/components/OpenMRNLite/src/nmranet_config.h @@ -146,5 +146,9 @@ DECLARE_CONST(enable_all_memory_space); * standard. */ DECLARE_CONST(node_init_identify); +/** Stack size for @ref SocketListener threads. */ +DECLARE_CONST(socket_listener_stack_size); +/** Number of sockets to allow for @ref SocketListener backlog. */ +DECLARE_CONST(socket_listener_backlog); #endif /* _nmranet_config_h_ */ diff --git a/lib/OpenMRNLite/src/openlcb/AliasAllocator.cpp b/components/OpenMRNLite/src/openlcb/AliasAllocator.cpp similarity index 100% rename from lib/OpenMRNLite/src/openlcb/AliasAllocator.cpp rename to components/OpenMRNLite/src/openlcb/AliasAllocator.cpp diff --git a/lib/OpenMRNLite/src/openlcb/AliasAllocator.hxx b/components/OpenMRNLite/src/openlcb/AliasAllocator.hxx similarity index 100% rename from lib/OpenMRNLite/src/openlcb/AliasAllocator.hxx rename to components/OpenMRNLite/src/openlcb/AliasAllocator.hxx diff --git a/lib/OpenMRNLite/src/openlcb/AliasCache.cpp b/components/OpenMRNLite/src/openlcb/AliasCache.cpp similarity index 100% rename from lib/OpenMRNLite/src/openlcb/AliasCache.cpp rename to components/OpenMRNLite/src/openlcb/AliasCache.cpp diff --git a/lib/OpenMRNLite/src/openlcb/AliasCache.hxx b/components/OpenMRNLite/src/openlcb/AliasCache.hxx similarity index 100% rename from lib/OpenMRNLite/src/openlcb/AliasCache.hxx rename to components/OpenMRNLite/src/openlcb/AliasCache.hxx diff --git a/lib/OpenMRNLite/src/openlcb/ApplicationChecksum.hxx b/components/OpenMRNLite/src/openlcb/ApplicationChecksum.hxx similarity index 100% rename from lib/OpenMRNLite/src/openlcb/ApplicationChecksum.hxx rename to components/OpenMRNLite/src/openlcb/ApplicationChecksum.hxx diff --git a/lib/OpenMRNLite/src/openlcb/BlinkerFlow.hxx b/components/OpenMRNLite/src/openlcb/BlinkerFlow.hxx similarity index 100% rename from lib/OpenMRNLite/src/openlcb/BlinkerFlow.hxx rename to components/OpenMRNLite/src/openlcb/BlinkerFlow.hxx diff --git a/lib/OpenMRNLite/src/openlcb/Bootloader.hxx b/components/OpenMRNLite/src/openlcb/Bootloader.hxx similarity index 100% rename from lib/OpenMRNLite/src/openlcb/Bootloader.hxx rename to components/OpenMRNLite/src/openlcb/Bootloader.hxx diff --git a/lib/OpenMRNLite/src/openlcb/BootloaderClient.hxx b/components/OpenMRNLite/src/openlcb/BootloaderClient.hxx similarity index 95% rename from lib/OpenMRNLite/src/openlcb/BootloaderClient.hxx rename to components/OpenMRNLite/src/openlcb/BootloaderClient.hxx index 87ce36af..f66eefb0 100644 --- a/lib/OpenMRNLite/src/openlcb/BootloaderClient.hxx +++ b/components/OpenMRNLite/src/openlcb/BootloaderClient.hxx @@ -36,6 +36,7 @@ #include #include "openlcb/DatagramDefs.hxx" +#include "openlcb/FirmwareUpgradeDefs.hxx" #include "openlcb/StreamDefs.hxx" #include "openlcb/PIPClient.hxx" #include "openlcb/CanDefs.hxx" @@ -731,6 +732,35 @@ private: Action finish() { + auto result = dgClient_->result(); + result &= DatagramClient::RESPONSE_CODE_MASK; + if (result == DatagramClient::DST_REBOOT || + result == DatagramClient::OPERATION_SUCCESS) + { + // this is fine + result = 0; + } + uint16_t olcb_error = result & 0xffff; + if (olcb_error == FirmwareUpgradeDefs::ERROR_INCOMPATIBLE_FIRMWARE) + { + return return_error(olcb_error, + "The firmware data is incompatible with this hardware."); + } + if (olcb_error == FirmwareUpgradeDefs::ERROR_CORRUPTED_DATA) + { + return return_error( + olcb_error, "The firmware data is invalid or corrupted."); + } + if (olcb_error == FirmwareUpgradeDefs::ERROR_WRITE_CHECKSUM_FAILED) + { + return return_error(olcb_error, + "The firmware written has failed checksum. Try again."); + } + if (olcb_error & DatagramClient::PERMANENT_ERROR) + { + return return_error(result & 0xffff, ""); + } + // Not sure what this is. datagramService_->client_allocator()->typed_insert(dgClient_); return return_error(0, ""); } diff --git a/lib/OpenMRNLite/src/openlcb/BootloaderPort.hxx b/components/OpenMRNLite/src/openlcb/BootloaderPort.hxx similarity index 99% rename from lib/OpenMRNLite/src/openlcb/BootloaderPort.hxx rename to components/OpenMRNLite/src/openlcb/BootloaderPort.hxx index d36da2f5..b8aa7659 100644 --- a/lib/OpenMRNLite/src/openlcb/BootloaderPort.hxx +++ b/components/OpenMRNLite/src/openlcb/BootloaderPort.hxx @@ -55,7 +55,7 @@ public: return is_waiting_; } - virtual Action entry() + Action entry() override { AtomicHolder h(this); is_waiting_ = true; diff --git a/components/OpenMRNLite/src/openlcb/BroadcastTime.cpp b/components/OpenMRNLite/src/openlcb/BroadcastTime.cpp new file mode 100644 index 00000000..fb5a91be --- /dev/null +++ b/components/OpenMRNLite/src/openlcb/BroadcastTime.cpp @@ -0,0 +1,53 @@ +/** @copyright + * Copyright (c) 2018, Stuart W. Baker + * All rights reserved. + * + * 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. + * + * 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. + * + * @file BroadcastTime.cxx + * + * Implementation of a Broadcast Time Protocol Interface. + * + * @author Stuart W. Baker + * @date 4 November 2018 + */ + +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200112L +#endif + +#include "openlcb/BroadcastTime.hxx" + +namespace openlcb +{ + +// +// BroadcastTimeClient::clear_timezone +// +void BroadcastTime::clear_timezone() +{ + setenv("TZ", "GMT0", 1); + tzset(); +} + +} // namespace openlcb diff --git a/components/OpenMRNLite/src/openlcb/BroadcastTime.hxx b/components/OpenMRNLite/src/openlcb/BroadcastTime.hxx new file mode 100644 index 00000000..4aeb591a --- /dev/null +++ b/components/OpenMRNLite/src/openlcb/BroadcastTime.hxx @@ -0,0 +1,523 @@ +/** @copyright + * Copyright (c) 2018, Stuart W. Baker + * All rights reserved. + * + * 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. + * + * 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. + * + * @file BroadcastTime.hxx + * + * Implementation of Broadcast Time Protocol. + * + * @author Stuart W. Baker + * @date 4 November 2018 + */ + +#ifndef _OPENLCB_BROADCASTTIME_HXX_ +#define _OPENLCB_BROADCASTTIME_HXX_ + +#include + +#include "openlcb/BroadcastTimeDefs.hxx" +#include "openlcb/EventHandlerTemplates.hxx" + +namespace openlcb +{ + +/// Implementation of Broadcast Time Protocol. +class BroadcastTime : public SimpleEventHandler + , public StateFlowBase + , protected Atomic +{ +public: + typedef std::vector>::size_type UpdateSubscribeHandle; + + /// Set the time in seconds since the system Epoch. The new time does not + /// become valid until the update callbacks are called. + /// @param hour hour (0 to 23) + /// @param minutes minutes (0 to 59) + void set_time(int hours, int minutes) + { + new SetFlow(this, SetFlow::Command::SET_TIME, hours, minutes); + } + + /// Set the time in seconds since the system Epoch. The new date does not + /// become valid until the update callbacks are called. + /// @param month month (1 to 12) + /// @param day day of month (1 to 31) + void set_date(int month, int day) + { + new SetFlow(this, SetFlow::Command::SET_DATE, month, day); + } + + /// Set the time in seconds since the system Epoch. The new year does not + /// become valid until the update callbacks are called. + /// @param year (0AD to 4095AD) + void set_year(int year) + { + new SetFlow(this, SetFlow::Command::SET_YEAR, year); + } + + /// Set Rate. The new rate does not become valid until the update callbacks + /// are called. + /// @param rate clock rate ratio as 12 bit sign extended fixed point + /// rrrrrrrrrr.rr + void set_rate_quarters(int16_t rate) + { + new SetFlow(this, SetFlow::Command::SET_RATE, rate); + } + + /// Start clock + void start() + { + new SetFlow(this, SetFlow::Command::START); + } + + /// Stop clock + void stop() + { + new SetFlow(this, SetFlow::Command::STOP); + } + + /// Get the time as a value of seconds relative to the system epoch. At the + /// same time get an atomic matching pair of the rate + /// @return pair