diff --git a/.github/actions/setup-build-environment/action.yml b/.github/actions/setup-build-environment/action.yml new file mode 100644 index 00000000..2ba7617e --- /dev/null +++ b/.github/actions/setup-build-environment/action.yml @@ -0,0 +1,29 @@ +name: Setup Build Environment +runs: + using: "composite" + steps: + + - name: Init Cache + uses: actions/cache@v4 + with: + path: | + ~/.cache/pip + ~/.platformio/.cache + key: ${{ runner.os }}-pio + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install PlatformIO + shell: bash + run: | + pip install --upgrade platformio + + # a git tag of "room-server-v1.2.3" should set "v1.2.3" as GIT_TAG_VERSION + - name: Extract Version from Git Tag + shell: bash + run: | + GIT_TAG_NAME="${GITHUB_REF#refs/tags/}" + echo "GIT_TAG_VERSION=${GIT_TAG_NAME##*-}" >> $GITHUB_ENV diff --git a/.github/workflows/build-companion-firmwares.yml b/.github/workflows/build-companion-firmwares.yml new file mode 100644 index 00000000..25ca9458 --- /dev/null +++ b/.github/workflows/build-companion-firmwares.yml @@ -0,0 +1,39 @@ +name: Build Companion Firmwares + +on: + workflow_dispatch: + push: + tags: + - 'companion-*' + +jobs: + + build: + runs-on: ubuntu-latest + steps: + + - name: Clone Repo + uses: actions/checkout@v4 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Build Firmwares + env: + FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }} + run: /usr/bin/env bash build.sh build-companion-firmwares + + - name: Upload Workflow Artifacts + uses: actions/upload-artifact@v4 + with: + name: companion-firmwares + path: out + + - name: Create Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + name: Companion Firmware ${{ env.GIT_TAG_VERSION }} + body: "" + draft: true + files: out/* \ No newline at end of file diff --git a/.github/workflows/build-repeater-firmwares.yml b/.github/workflows/build-repeater-firmwares.yml new file mode 100644 index 00000000..8b36f829 --- /dev/null +++ b/.github/workflows/build-repeater-firmwares.yml @@ -0,0 +1,39 @@ +name: Build Repeater Firmwares + +on: + workflow_dispatch: + push: + tags: + - 'repeater-*' + +jobs: + + build: + runs-on: ubuntu-latest + steps: + + - name: Clone Repo + uses: actions/checkout@v4 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Build Firmwares + env: + FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }} + run: /usr/bin/env bash build.sh build-repeater-firmwares + + - name: Upload Workflow Artifacts + uses: actions/upload-artifact@v4 + with: + name: repeater-firmwares + path: out + + - name: Create Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + name: Repeater Firmware ${{ env.GIT_TAG_VERSION }} + body: "" + draft: true + files: out/* \ No newline at end of file diff --git a/.github/workflows/build-room-server-firmwares.yml b/.github/workflows/build-room-server-firmwares.yml new file mode 100644 index 00000000..d577ba77 --- /dev/null +++ b/.github/workflows/build-room-server-firmwares.yml @@ -0,0 +1,39 @@ +name: Build Room Server Firmwares + +on: + workflow_dispatch: + push: + tags: + - 'room-server-*' + +jobs: + + build: + runs-on: ubuntu-latest + steps: + + - name: Clone Repo + uses: actions/checkout@v4 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Build Firmwares + env: + FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }} + run: /usr/bin/env bash build.sh build-room-server-firmwares + + - name: Upload Workflow Artifacts + uses: actions/upload-artifact@v4 + with: + name: room-server-firmwares + path: out + + - name: Create Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + name: Room Server Firmware ${{ env.GIT_TAG_VERSION }} + body: "" + draft: true + files: out/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 89cc49cb..a66b3e93 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch +out/ diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..a4a19495 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,15 @@ +# Releasing Firmware + +GitHub Actions is set up to automatically build and release firmware. + +It will automatically build firmware when one of the following tag formats are pushed. + +- `companion-v1.0.0` +- `repeater-v1.0.0` +- `room-server-v1.0.0` + +> NOTE: replace `v1.0.0` with the version you want to release as. + +- You can push one, or more tags on the same commit, and they will all build separately. +- Once the firmware has been built, a new (draft) GitHub Release will be created. +- You will need to update the release notes, and publish it. diff --git a/bin/uf2conv/uf2conv.py b/bin/uf2conv/uf2conv.py new file mode 100644 index 00000000..cb1987a1 --- /dev/null +++ b/bin/uf2conv/uf2conv.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +import sys +import struct +import subprocess +import re +import os +import os.path +import argparse +import json +from time import sleep + + +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected +UF2_MAGIC_END = 0x0AB16F30 # Ditto + +INFO_FILE = "/INFO_UF2.TXT" + +appstartaddr = 0x2000 +familyid = 0x0 + + +def is_uf2(buf): + w = struct.unpack(" 476: + assert False, "Invalid UF2 data size at " + ptr + newaddr = hd[3] + if (hd[2] & 0x2000) and (currfamilyid == None): + currfamilyid = hd[7] + if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid): + currfamilyid = hd[7] + curraddr = newaddr + if familyid == 0x0 or familyid == hd[7]: + appstartaddr = newaddr + padding = newaddr - curraddr + if padding < 0: + assert False, "Block out of order at " + ptr + if padding > 10*1024*1024: + assert False, "More than 10M of padding needed at " + ptr + if padding % 4 != 0: + assert False, "Non-word padding size at " + ptr + while padding > 0: + padding -= 4 + outp.append(b"\x00\x00\x00\x00") + if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]): + outp.append(block[32 : 32 + datalen]) + curraddr = newaddr + datalen + if hd[2] & 0x2000: + if hd[7] in families_found.keys(): + if families_found[hd[7]] > newaddr: + families_found[hd[7]] = newaddr + else: + families_found[hd[7]] = newaddr + if prev_flag == None: + prev_flag = hd[2] + if prev_flag != hd[2]: + all_flags_same = False + if blockno == (numblocks - 1): + print("--- UF2 File Header Info ---") + families = load_families() + for family_hex in families_found.keys(): + family_short_name = "" + for name, value in families.items(): + if value == family_hex: + family_short_name = name + print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex)) + print("Target Address is 0x{:08x}".format(families_found[family_hex])) + if all_flags_same: + print("All block flag values consistent, 0x{:04x}".format(hd[2])) + else: + print("Flags were not all the same") + print("----------------------------") + if len(families_found) > 1 and familyid == 0x0: + outp = [] + appstartaddr = 0x0 + return b"".join(outp) + +def convert_to_carray(file_content): + outp = "const unsigned long bindata_len = %d;\n" % len(file_content) + outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {" + for i in range(len(file_content)): + if i % 16 == 0: + outp += "\n" + outp += "0x%02x, " % file_content[i] + outp += "\n};\n" + return bytes(outp, "utf-8") + +def convert_to_uf2(file_content): + global familyid + datapadding = b"" + while len(datapadding) < 512 - 256 - 32 - 4: + datapadding += b"\x00\x00\x00\x00" + numblocks = (len(file_content) + 255) // 256 + outp = [] + for blockno in range(numblocks): + ptr = 256 * blockno + chunk = file_content[ptr:ptr + 256] + flags = 0x0 + if familyid: + flags |= 0x2000 + hd = struct.pack(b"= 3 and words[1] == "2" and words[2] == "FAT": + drives.append(words[0]) + else: + searchpaths = ["/media"] + if sys.platform == "darwin": + searchpaths = ["/Volumes"] + elif sys.platform == "linux": + searchpaths += ["/media/" + os.environ["USER"], '/run/media/' + os.environ["USER"]] + + for rootpath in searchpaths: + if os.path.isdir(rootpath): + for d in os.listdir(rootpath): + if os.path.isdir(rootpath): + drives.append(os.path.join(rootpath, d)) + + + def has_info(d): + try: + return os.path.isfile(d + INFO_FILE) + except: + return False + + return list(filter(has_info, drives)) + + +def board_id(path): + with open(path + INFO_FILE, mode='r') as file: + file_content = file.read() + return re.search(r"Board-ID: ([^\r\n]*)", file_content).group(1) + + +def list_drives(): + for d in get_drives(): + print(d, board_id(d)) + + +def write_file(name, buf): + with open(name, "wb") as f: + f.write(buf) + print("Wrote %d bytes to %s" % (len(buf), name)) + + +def load_families(): + # The expectation is that the `uf2families.json` file is in the same + # directory as this script. Make a path that works using `__file__` + # which contains the full path to this script. + filename = "uf2families.json" + pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) + with open(pathname) as f: + raw_families = json.load(f) + + families = {} + for family in raw_families: + families[family["short_name"]] = int(family["id"], 0) + + return families + + +def main(): + global appstartaddr, familyid + def error(msg): + print(msg, file=sys.stderr) + sys.exit(1) + parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.') + parser.add_argument('input', metavar='INPUT', type=str, nargs='?', + help='input file (HEX, BIN or UF2)') + parser.add_argument('-b', '--base', dest='base', type=str, + default="0x2000", + help='set base address of application for BIN format (default: 0x2000)') + parser.add_argument('-f', '--family', dest='family', type=str, + default="0x0", + help='specify familyID - number or name (default: 0x0)') + parser.add_argument('-o', '--output', metavar="FILE", dest='output', type=str, + help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible') + parser.add_argument('-d', '--device', dest="device_path", + help='select a device path to flash') + parser.add_argument('-l', '--list', action='store_true', + help='list connected devices') + parser.add_argument('-c', '--convert', action='store_true', + help='do not flash, just convert') + parser.add_argument('-D', '--deploy', action='store_true', + help='just flash, do not convert') + parser.add_argument('-w', '--wait', action='store_true', + help='wait for device to flash') + parser.add_argument('-C', '--carray', action='store_true', + help='convert binary file to a C array, not UF2') + parser.add_argument('-i', '--info', action='store_true', + help='display header information from UF2, do not convert') + args = parser.parse_args() + appstartaddr = int(args.base, 0) + + families = load_families() + + if args.family.upper() in families: + familyid = families[args.family.upper()] + else: + try: + familyid = int(args.family, 0) + except ValueError: + error("Family ID needs to be a number or one of: " + ", ".join(families.keys())) + + if args.list: + list_drives() + else: + if not args.input: + error("Need input file") + with open(args.input, mode='rb') as f: + inpbuf = f.read() + from_uf2 = is_uf2(inpbuf) + ext = "uf2" + if args.deploy: + outbuf = inpbuf + elif from_uf2 and not args.info: + outbuf = convert_from_uf2(inpbuf) + ext = "bin" + elif from_uf2 and args.info: + outbuf = "" + convert_from_uf2(inpbuf) + elif is_hex(inpbuf): + outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8")) + elif args.carray: + outbuf = convert_to_carray(inpbuf) + ext = "h" + else: + outbuf = convert_to_uf2(inpbuf) + if not args.deploy and not args.info: + print("Converted to %s, output size: %d, start address: 0x%x" % + (ext, len(outbuf), appstartaddr)) + if args.convert or ext != "uf2": + if args.output == None: + args.output = "flash." + ext + if args.output: + write_file(args.output, outbuf) + if ext == "uf2" and not args.convert and not args.info: + drives = get_drives() + if len(drives) == 0: + if args.wait: + print("Waiting for drive to deploy...") + while len(drives) == 0: + sleep(0.1) + drives = get_drives() + elif not args.output: + error("No drive to deploy.") + for d in drives: + print("Flashing %s (%s)" % (d, board_id(d))) + write_file(d + "/NEW.UF2", outbuf) + + +if __name__ == "__main__": + main() diff --git a/bin/uf2conv/uf2families.json b/bin/uf2conv/uf2families.json new file mode 100644 index 00000000..25943b52 --- /dev/null +++ b/bin/uf2conv/uf2families.json @@ -0,0 +1,362 @@ +[ + { + "id": "0x16573617", + "short_name": "ATMEGA32", + "description": "Microchip (Atmel) ATmega32" + }, + { + "id": "0x1851780a", + "short_name": "SAML21", + "description": "Microchip (Atmel) SAML21" + }, + { + "id": "0x1b57745f", + "short_name": "NRF52", + "description": "Nordic NRF52" + }, + { + "id": "0x1c5f21b0", + "short_name": "ESP32", + "description": "ESP32" + }, + { + "id": "0x1e1f432d", + "short_name": "STM32L1", + "description": "ST STM32L1xx" + }, + { + "id": "0x202e3a91", + "short_name": "STM32L0", + "description": "ST STM32L0xx" + }, + { + "id": "0x21460ff0", + "short_name": "STM32WL", + "description": "ST STM32WLxx" + }, + { + "id": "0x22e0d6fc", + "short_name": "RTL8710B", + "description": "Realtek AmebaZ RTL8710B" + }, + { + "id": "0x2abc77ec", + "short_name": "LPC55", + "description": "NXP LPC55xx" + }, + { + "id": "0x300f5633", + "short_name": "STM32G0", + "description": "ST STM32G0xx" + }, + { + "id": "0x31d228c6", + "short_name": "GD32F350", + "description": "GD32F350" + }, + { + "id": "0x3379CFE2", + "short_name": "RTL8720D", + "description": "Realtek AmebaD RTL8720D" + }, + { + "id": "0x04240bdf", + "short_name": "STM32L5", + "description": "ST STM32L5xx" + }, + { + "id": "0x4c71240a", + "short_name": "STM32G4", + "description": "ST STM32G4xx" + }, + { + "id": "0x4fb2d5bd", + "short_name": "MIMXRT10XX", + "description": "NXP i.MX RT10XX" + }, + { + "id": "0x51e903a8", + "short_name": "XR809", + "description": "Xradiotech 809" + }, + { + "id": "0x53b80f00", + "short_name": "STM32F7", + "description": "ST STM32F7xx" + }, + { + "id": "0x55114460", + "short_name": "SAMD51", + "description": "Microchip (Atmel) SAMD51" + }, + { + "id": "0x57755a57", + "short_name": "STM32F4", + "description": "ST STM32F4xx" + }, + { + "id": "0x5a18069b", + "short_name": "FX2", + "description": "Cypress FX2" + }, + { + "id": "0x5d1a0a2e", + "short_name": "STM32F2", + "description": "ST STM32F2xx" + }, + { + "id": "0x5ee21072", + "short_name": "STM32F1", + "description": "ST STM32F103" + }, + { + "id": "0x621e937a", + "short_name": "NRF52833", + "description": "Nordic NRF52833" + }, + { + "id": "0x647824b6", + "short_name": "STM32F0", + "description": "ST STM32F0xx" + }, + { + "id": "0x675a40b0", + "short_name": "BK7231U", + "description": "Beken 7231U/7231T" + }, + { + "id": "0x68ed2b88", + "short_name": "SAMD21", + "description": "Microchip (Atmel) SAMD21" + }, + { + "id": "0x6a82cc42", + "short_name": "BK7251", + "description": "Beken 7251/7252" + }, + { + "id": "0x6b846188", + "short_name": "STM32F3", + "description": "ST STM32F3xx" + }, + { + "id": "0x6d0922fa", + "short_name": "STM32F407", + "description": "ST STM32F407" + }, + { + "id": "0x4e8f1c5d", + "short_name": "STM32H5", + "description": "ST STM32H5xx" + }, + { + "id": "0x6db66082", + "short_name": "STM32H7", + "description": "ST STM32H7xx" + }, + { + "id": "0x70d16653", + "short_name": "STM32WB", + "description": "ST STM32WBxx" + }, + { + "id": "0x7b3ef230", + "short_name": "BK7231N", + "description": "Beken 7231N" + }, + { + "id": "0x7eab61ed", + "short_name": "ESP8266", + "description": "ESP8266" + }, + { + "id": "0x7f83e793", + "short_name": "KL32L2", + "description": "NXP KL32L2x" + }, + { + "id": "0x8fb060fe", + "short_name": "STM32F407VG", + "description": "ST STM32F407VG" + }, + { + "id": "0x9fffd543", + "short_name": "RTL8710A", + "description": "Realtek Ameba1 RTL8710A" + }, + { + "id": "0xada52840", + "short_name": "NRF52840", + "description": "Nordic NRF52840" + }, + { + "id": "0x820d9a5f", + "short_name": "NRF52820", + "description": "Nordic NRF52820_xxAA" + }, + { + "id": "0xbfdd4eee", + "short_name": "ESP32S2", + "description": "ESP32-S2" + }, + { + "id": "0xc47e5767", + "short_name": "ESP32S3", + "description": "ESP32-S3" + }, + { + "id": "0xd42ba06c", + "short_name": "ESP32C3", + "description": "ESP32-C3" + }, + { + "id": "0x2b88d29c", + "short_name": "ESP32C2", + "description": "ESP32-C2" + }, + { + "id": "0x332726f6", + "short_name": "ESP32H2", + "description": "ESP32-H2" + }, + { + "id": "0x540ddf62", + "short_name": "ESP32C6", + "description": "ESP32-C6" + }, + { + "id": "0x3d308e94", + "short_name": "ESP32P4", + "description": "ESP32-P4" + }, + { + "id": "0xf71c0343", + "short_name": "ESP32C5", + "description": "ESP32-C5" + }, + { + "id": "0x77d850c4", + "short_name": "ESP32C61", + "description": "ESP32-C61" + }, + { + "id": "0xb6dd00af", + "short_name": "ESP32H21", + "description": "ESP32-H21" + }, + { + "id": "0x9e0baa8a", + "short_name": "ESP32H4", + "description": "ESP32-H4" + }, + { + "id": "0xde1270b7", + "short_name": "BL602", + "description": "Boufallo 602" + }, + { + "id": "0xe08f7564", + "short_name": "RTL8720C", + "description": "Realtek AmebaZ2 RTL8720C" + }, + { + "id": "0xe48bff56", + "short_name": "RP2040", + "description": "Raspberry Pi RP2040" + }, + { + "id": "0xe48bff57", + "short_name": "RP2XXX_ABSOLUTE", + "description": "Raspberry Pi Microcontrollers: Absolute (unpartitioned) download" + }, + { + "id": "0xe48bff58", + "short_name": "RP2XXX_DATA", + "description": "Raspberry Pi Microcontrollers: Data partition download" + }, + { + "id": "0xe48bff59", + "short_name": "RP2350_ARM_S", + "description": "Raspberry Pi RP2350, Secure Arm image" + }, + { + "id": "0xe48bff5a", + "short_name": "RP2350_RISCV", + "description": "Raspberry Pi RP2350, RISC-V image" + }, + { + "id": "0xe48bff5b", + "short_name": "RP2350_ARM_NS", + "description": "Raspberry Pi RP2350, Non-secure Arm image" + }, + { + "id": "0x00ff6919", + "short_name": "STM32L4", + "description": "ST STM32L4xx" + }, + { + "id": "0x9af03e33", + "short_name": "GD32VF103", + "description": "GigaDevice GD32VF103" + }, + { + "id": "0x4f6ace52", + "short_name": "CSK4", + "description": "LISTENAI CSK300x/400x" + }, + { + "id": "0x6e7348a8", + "short_name": "CSK6", + "description": "LISTENAI CSK60xx" + }, + { + "id": "0x11de784a", + "short_name": "M0SENSE", + "description": "M0SENSE BL702" + }, + { + "id": "0x4b684d71", + "short_name": "MaixPlay-U4", + "description": "Sipeed MaixPlay-U4(BL618)" + }, + { + "id": "0x9517422f", + "short_name": "RZA1LU", + "description": "Renesas RZ/A1LU (R7S7210xx)" + }, + { + "id": "0x2dc309c5", + "short_name": "STM32F411xE", + "description": "ST STM32F411xE" + }, + { + "id": "0x06d1097b", + "short_name": "STM32F411xC", + "description": "ST STM32F411xC" + }, + { + "id": "0x72721d4e", + "short_name": "NRF52832xxAA", + "description": "Nordic NRF52832xxAA" + }, + { + "id": "0x6f752678", + "short_name": "NRF52832xxAB", + "description": "Nordic NRF52832xxAB" + }, + { + "id": "0xa0c97b8e", + "short_name": "AT32F415", + "description": "ArteryTek AT32F415" + }, + { + "id": "0x699b62ec", + "short_name": "CH32V", + "description": "WCH CH32V2xx and CH32V3xx" + }, + { + "id": "0x7be8976d", + "short_name": "RA4M1", + "description": "Renesas RA4M1" + } +] diff --git a/build.sh b/build.sh new file mode 100644 index 00000000..10782e00 --- /dev/null +++ b/build.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash + +# usage +# sh build.sh build-firmware RAK_4631_Repeater +# sh build.sh build-firmwares +# sh build.sh build-companion-firmwares +# sh build.sh build-repeater-firmwares +# sh build.sh build-room-server-firmwares + +# get a list of pio env names that start with "env:" +get_pio_envs() { + echo $(pio project config | grep 'env:' | sed 's/env://') +} + +# $1 should be the string to find (case insensitive) +get_pio_envs_containing_string() { + shopt -s nocasematch + envs=($(get_pio_envs)) + for env in "${envs[@]}"; do + if [[ "$env" == *${1}* ]]; then + echo $env + fi + done +} + +# build firmware for the provided pio env in $1 +build_firmware() { + + # get git commit sha + COMMIT_HASH=$(git rev-parse --short HEAD) + + # set firmware build date + FIRMWARE_BUILD_DATE=$(date '+%d-%b-%Y') + + # get FIRMWARE_VERSION, which should be provided by the environment + if [ -z "$FIRMWARE_VERSION" ]; then + echo "FIRMWARE_VERSION must be set in environment" + exit 1 + fi + + # set firmware version string + # e.g: v1.0.0-abcdef + FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" + + # craft filename + # e.g: RAK_4631_Repeater-v1.0.0-SHA + FIRMWARE_FILENAME="$1-${FIRMWARE_VERSION_STRING}" + + # export build flags for pio so we can inject firmware version info + export PLATFORMIO_BUILD_FLAGS="-DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'" + + # build firmware target + pio run -e $1 + + # build merge-bin for esp32 fresh install + if [ -f .pio/build/$1/firmware.bin ]; then + pio run -t mergebin -e $1 + fi + + # build .uf2 for RAK_4631 and t1000e + if [[ $1 == *"RAK_4631"* || $1 == *"t1000e"* ]]; then + python bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840 + fi + + # copy .bin, .uf2, and .zip to out folder + # e.g: Heltec_v3_room_server-v1.0.0-SHA.bin + # e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2 + + # copy .bin for esp32 boards + cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null + cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null + + # copy .zip and .uf2 of nrf52 boards + cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null + cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null + +} + +# firmwares containing $1 will be built +build_all_firmwares_matching() { + envs=($(get_pio_envs_containing_string "$1")) + for env in "${envs[@]}"; do + build_firmware $env + done +} + +build_repeater_firmwares() { + +# # build specific repeater firmwares +# build_firmware "Heltec_v2_repeater" +# build_firmware "Heltec_v3_repeater" +# build_firmware "Xiao_C3_Repeater_sx1262" +# build_firmware "Xiao_S3_WIO_Repeater" +# build_firmware "LilyGo_T3S3_sx1262_Repeater" +# build_firmware "RAK_4631_Repeater" + + # build all repeater firmwares + build_all_firmwares_matching "repeater" + +} + +build_companion_firmwares() { + +# # build specific companion firmwares +# build_firmware "Heltec_v2_companion_radio_usb" +# build_firmware "Heltec_v2_companion_radio_ble" +# build_firmware "Heltec_v3_companion_radio_usb" +# build_firmware "Heltec_v3_companion_radio_ble" +# build_firmware "Xiao_S3_WIO_companion_radio_ble" +# build_firmware "LilyGo_T3S3_sx1262_companion_radio_usb" +# build_firmware "LilyGo_T3S3_sx1262_companion_radio_ble" +# build_firmware "RAK_4631_companion_radio_usb" +# build_firmware "RAK_4631_companion_radio_ble" +# build_firmware "t1000e_companion_radio_ble" + + # build all companion firmwares + build_all_firmwares_matching "companion_radio_usb" + build_all_firmwares_matching "companion_radio_ble" + +} + +build_room_server_firmwares() { + +# # build specific room server firmwares +# build_firmware "Heltec_v3_room_server" +# build_firmware "RAK_4631_room_server" + + # build all room server firmwares + build_all_firmwares_matching "room_server" + +} + +build_firmwares() { + build_companion_firmwares + build_repeater_firmwares + build_room_server_firmwares +} + +# clean build dir +rm -rf out +mkdir -p out + +# handle script args +if [[ $1 == "build-firmware" ]]; then + if [ "$2" ]; then + build_firmware $2 + fi +elif [[ $1 == "build-firmwares" ]]; then + build_firmwares +elif [[ $1 == "build-companion-firmwares" ]]; then + build_companion_firmwares +elif [[ $1 == "build-repeater-firmwares" ]]; then + build_repeater_firmwares +elif [[ $1 == "build-room-server-firmwares" ]]; then + build_room_server_firmwares +fi diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..a7169a0e --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,280 @@ +# MeshCore-FAQ +A list of frequently-asked questions and answers for MeshCore + +author: https://github.com/LitBomb +--- + +## Q: What is MeshCore? + +**A:** MeshCore is free and open source +* MeshCore is the routing and firmware etc, available on GitHub under MIT license +* There are clients made by the community, such as the web clients, these are free to use, and some are open source too +* The cross platform mobile app developed by [Liam Cottle](https://liamcottle.net) for Android/iOS/PC etc is free to download and use +* The T-Deck firmware is developed by Scott at Ripple Radios, the creator of MeshCore, is also free to flash on your devices and use + + +Some more advanced, but optional features are available on T-Deck if you register your device for a key to unlock. On the MeshCore smartphone clients for Android and iOS/iPadOS, you can unlock the wait timer for repeater and room server remote management over RF feature. + +These features are completely optional and aren't needed for the core messaging experience. They're like super bonus features and to help the developers continue to work on these amazing features, they may charge a small fee for an unlock code to utilise the advanced features. + +Anyone is able to build anything they like on top of MeshCore without paying anything. + +## Q: What do you need to start using MeshCore? +**A:** Everything you need for MeshCore is available at: + Main web site: [https://meshcore.co.uk/](https://meshcore.co.uk/) + Firmware Flasher: https://flasher.meshcore.co.uk/ + Phone Client Applications: https://meshcore.co.uk/apps.html + MeshCore Fimrware Github: https://github.com/ripplebiz/MeshCore + + + You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server). + +### Hardware +To use MeshCore without using a phone as the client interface, you can run MeshCore on a T-Deck or T-Deck Plus. It is a complete off-grid secure communication solution. + +MeshCore is also available on a variety of 868MHz and 915MHz LoRa devices. For example, RAK4631 devices (19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Seeed Studio T1000-E. More devices will be supported later. + +### Firmware +MeshCore has four firmware types that are not available on other LoRa systems. MeshCore has the following: + +#### Companion Radio Firmware +Companion radios are for connecting to the Android app or web app as a messenger client. There are two different companion radio firmware versions: + +1. **BLE Companion** + BLE Companion firmware runs on a supported LoRa device and connects to a smart device running the Android MeshCore client over BLE (iOS MeshCore client will be available soon) + + +2. **USB Serial Companion** + USB Serial Companion firmware runs on a supported LoRa device and connects to a smart device or a computer over USB Serial running the MeshCore web client + + + +#### Repeater +Repeaters are used to extend the range of a MeshCore network. Repeater firmware runs on the same devices that run client firmware. A repeater's job is to forward MeshCore packets to the destination device. It does **not** forward or retransmit every packet it receives, unlike other LoRa mesh systems. + +A repeater can be remotely administered using a T-Deck running the MeshCore firwmware with remote admistration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app. + +#### Room Server +A room server is a simple BBS server for sharing posts. T-Deck devices running MeshCore firmware or a BLE Companion client connected to a smartphone running the MeshCore app can connect to a room server. + +room servers store message history on them, and push the stored messages to users. Room servers allow roaming users to come back later and retrieve message history. Contrast to channels, messages are either received when it's sent, or not received and missed if the a room user is out of range. You can think of room servers like email servers where you can come back later and get your emails from your mail server + +A room server can be remotely administered using a T-Deck running the MeshCore firwmware with remote admistration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app. + +When a client logs into a room server, the client will receive the previously 16 unseen messages. + +A room server can also take on the repeater role. To enable repeater role on a room server, use this command: + +`set repeat {on|off}` + +--- + +## Initial Setup + +### Q: How many devices do I need to start using meshcore? +**A:** If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android client via bluetooth (iOS client will be available later). You can start communiating with other MeshCore users near you. + +If you have two supported devices, and there are not many MeshCore users near you, flash both of them to BLE Companion firmware so you can use your devices to communiate with your near-by friends and family. + +If you have two supported devices, and there are other MeshcCore users near by, you can flash one of your devices with BLE Companion firmware, and flash another supported device to repeater firmware. Place the repeater high above ground o extend your MeshCore network's reach. + +If you have more supported devices, you can use your additional deivces with the room server firmware. + +### Q: Does MeshCore cost any money? + +**A:** All radio firmware versions (e.g. for Heltec V3, RAK, T-1000E, etc) are free and open source developed by Scott at Ripple Radios. + +The native Android and iOS client uses the freemium model and is developed by Liam Cottle, developer of meshtastic map at [meshtastic.liamcottle.net](https://meshtastic.liamcottle.net) on [github ](https://github.com/liamcottle/meshtastic-map)and [reticulum-meshchat on github](https://github.com/liamcottle/reticulum-meshchat). + +The T-Deck firmware is free to download and most features are available without cost. To support the firmware developer, you can pay for a registration key to unlock your T-Deck for deeper map zoom and remote server administration over RF using the T-Deck. You do not need to pay for the registration to use your T-Deck for direct messaging and connecting to repeaters and room servers. + + +### Q: What frequencies are supported by MeshCore? +**A:** It supports the 868MHz range in the UK/EU and the 915MHz range in New Zealand, Australia, and the USA. Countries and regions in these two frequency ranges are also supported. The firmware and client allow users to set their preferred frequency. +- Australia and New Zealand are using **915.8MHz** +- UK and EU are gravitating toward **867.5MHz** +- There are discussions on discord for UK to move to 869.525MHz (https://discord.com/channels/826570251612323860/1330643963501351004/1342554454498742374) +- USA is gravitating toward **910.525MHz** + +the rest of the radio settings are the same for all frequencies: +- Spread Factor (SF): 10 +- Coding Rate (CR): 5 +- Bandwidth (BW): 250.00 + +### Q: What is an "advert" in MeshCore? +**A:** +Advert means to advertise yourself on the network. In Reticulum terms it would be to announce. In Meshtastic terms it would be the node sending it's node info. + +MeshCore allows you to manually broadcast your name, position and public encryption key, which is also signed to prevent spoofing. When you click the advert button, it broadcasts that data over LoRa. MeshCore calls that an Advert. There's two ways to advert, "zero hop" and "flood". + +* Zero hop means your advert is broadcasted out to anyone that can hear it, and that's it. +* Flooded means it's broadcasted out, and then repeated by all the repeaters that hear it. + +MeshCore clients only advertise themselves when the user initiates it. A repeater (and room server?) advertises its presence once every 240 minutes. This interval can be configured using the following command: + +`set advert.interval {minutes}` + +### Q: Is there a hop limit? + +**A:** Internally the firmware has maximum limit of 64 hops. In real world settings it will be difficult to get close to the limit due to the environments and timing as packets travel further and further. We want to hear how far your MeshCore conversations go. + + +--- + + +## Server Administration + +### Q: How do you configure a repeater or a room server? +**A:** One of these servers can be administered with one of the options below: +- Connect the server device through a USB serial connection to a computer running Chrome on this site: + +- A T-Deck running unlocked/registered MeshCore firmware. Remote server administration is enabled through registering your T-Deck with Ripple Radios. It is one of the ways to support MeshCore development. You can register your T-Deck at: + +- MeshCore smart device clients may have the ability to remotely administer servers in the future. + +### Q: Do I need to set the location for a repeater? +**A:** With location set for a repeater, it can show up on a MeshCore map in the future. Set location with the following commands: + +`set lat set long ` + +You can get the latitude and longitude from Google Maps by right-clicking the location you are at on the map. + +### Q: What is the password to administer a repeater or a room server? +**A:** The default admin password to a repeater and room server is `password`. Use the following command to change the admin password: + +`password {new-password}` + + +### Q: What is the password to join a room server? +**A:** The default guest password to a room server is `hello`. Use the following command to change the guest password: + +`set guest.password {guest-password}` + + +--- + +## T-Deck Related + +### Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode? +**A:** +1. Device off +2. Connect USB cable to device +3. Hold down trackball (keep holding) +4. Turn on device +5. Hear USB connection sound +6. Release trackball +7. T-Deck in DFU mode now +8. At this point you can begin flashing using + +### Q: Why is my T-Deck Plus not getting any satellite lock? +**A:** For T-Deck Plus, the GPS baud rate should be set to **38400**. Also, a number of T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open up the device to check the GPS orientation. + +### Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock? +**A:** The OG (non-Plus) T-Deck doesn't come with a GPS. If you added a GPS to your OG T-Deck, please refer to the manual of your GPS to see what baud rate it requires. Alternatively, you can try to set the baud rate from 9600, 19200, etc., and up to 115200 to see which one works. + +### Q: What size of SD card does the T-Deck support? +**A:** Users have had no issues using 16GB or 32GB SD cards. Format the SD card to **FAT32**. + +### Q: How do I get maps on T-Deck? +**A:** You need map tiles. You can get pre-downloaded map tiles here (a good way to support development): +- (Europe) +- (US) + +Another way to download map tiles is to use this Python script to get the tiles in the areas you want: + + +There is also a modified script that adds additional error handling and parallel downloads: + + +UK map tiles are available separately from Andy Kirby on his discord server: + + +### Q: Where do the map tiles go? +Once you have the tiles downloaded, copy the `\tiles` folder to the root of your T-Deck's SD card. + +### Q: How to unlock deeper map zoom and server management features on T-Deck? +**A:** You can download, install, and use the T-Deck firmware for free, but it has some features (map zoom, server administration) that are enabled if you purchase an unlock code for \$10 per T-Deck device. +Unlock page: + + +### Q: The T-Deck sound is too loud? +### Q: Can you customize the sound? + +**A:** You can customise the sounds on the T-Deck, just by placing `.mp3` files onto the `root` dir of the SD card. `startup.mp3`, `alert.mp3` and `new-advert.mp3` + +### Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts? + +**A:** 'Import from Clipboard' is for importing a contact via a file named 'clipboard.txt' on the SD card. The opposite, is in the Identity screen, the 'Card to Clipboard' menu, which writes to 'clipboard.txt' so you can share yourself (call these 'biz cards', that start with "meshcore://...") + +--- + +## General + +### Q: What are BW, SF, and CR? + +**A:** + +**BW is bandwidth** - width of frequency spectrum that is used for transmission + +**SF is spreading factor** - how much should the communication spread in time + +**CR is coding rate** - https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate/ +Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more bytes in the same time. Making the spreading factor 1 step lower (from SF10 to SF9) allows you to send 2x more bytes in the same time. + +Lowering the spreading factor makes it more difficult for the gateway to receive a transmission, as it will be more sensitive to noise. You could compare this to two people taking in a noisy place (a bar for example). If you’re far from each other, you have to talk slow (SF10), but if you’re close, you can talk faster (SF7) + +So it's balancing act between speed of the transmission and resistance to noise. +things network is mainly focused on LoRaWAN, but the LoRa low-level stuff still checks out for any LoRa project + +### Q: Is MeshCore open source? +**A:** Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps. +- Firmware repo: + +### Q: How can I support MeshCore? +**A:** Provide your honest feedback on GitHub and on AndyKirby's Discord server . Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support MeshCore development at . + +### Q: How do I build MeshCore firmware from source? +**A:** See instructions here: + + +Andy also has a video on how to build using VS Code: +*How to build and flash Meshcore repeater firmware | Heltec V3* + *(Link referenced in the Discord post)* + +### Q: Are there other MeshCore related open source projects? + +**A:** [Liam Cottle](https://liamcottle.net)'s MeshCore web client and MeshCore Javascript libary are open source under MIT license. + +Web client: https://github.com/liamcottle/meshcore-web +Javascript: https://github.com/liamcottle/meshcore.js + +### Q: Does MeshCore support ATAK +**A:** ATAK is not currently on MeshCore's roadmap. + +--- + +## Troubleshooting + +### Q: My client says another client or a repeater or a room server was last seen many, many days ago. +### Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed. +**A:** +- If your client is a T-Deck, it may not have its time set (no GPS installed, no GPS lock, or wrong GPS baud rate). +- If you are using the Android or iOS client, the other client, repeater, or room server may have the wrong time. + +You can get the epoch time on and use it to set your T-Deck clock. For a repeater and room server, the admin can use a T-Deck to remotely set their clock (clock sync), or use the `time` command in the USB serial console with the server device connected. + +### Q: How to connect to a repeater via BLE (bluetooth)? +**A:** You can't connect to a device running repeater firmware via bluetooth. Devices running the BLE companion firmware you can connect to it via bluetooth using the android app + +### Q: I can't connect via bluetooth, what is the bluetooth pairing code? + +**A:** the default bluetooth pairing code is `123456` + +### Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection. + +**A:** Heltec V3 has a very small coil antenna on its PCB for WiFi and Bluetooth connectivty. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification. + + +--- + + diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index a3168a44..f81b9ffc 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1081,7 +1081,13 @@ public: }; #ifdef ESP32 - #ifdef BLE_PIN_CODE + #ifdef WIFI_SSID + #include + SerialWifiInterface serial_interface; + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif + #elif defined(BLE_PIN_CODE) #include SerialBLEInterface serial_interface; #else @@ -1170,7 +1176,10 @@ void setup() { SPIFFS.begin(true); the_mesh.begin(SPIFFS, trng); -#ifdef BLE_PIN_CODE +#ifdef WIFI_SSID + WiFi.begin(WIFI_SSID, WIFI_PWD); + serial_interface.begin(TCP_PORT); +#elif defined(BLE_PIN_CODE) char dev_name[32+10]; sprintf(dev_name, "MeshCore-%s", the_mesh.getNodeName()); serial_interface.begin(dev_name, BLE_PIN_CODE); diff --git a/platformio.ini b/platformio.ini index 48b83dc6..df6d2093 100644 --- a/platformio.ini +++ b/platformio.ini @@ -111,6 +111,8 @@ build_flags = -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 -D P_LORA_TX_LED=35 + -D PIN_BOARD_SDA=17 + -D PIN_BOARD_SCL=18 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=130.0f ; for best TX power! @@ -186,6 +188,24 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_v3_companion_radio_wifi] +extends = Heltec_lora32_v3 +build_flags = + ${Heltec_lora32_v3.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=1 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID="\"myssid\"" + -D WIFI_PWD="\"mypwd\"" +; -D ENABLE_PRIVATE_KEY_IMPORT=1 +; -D ENABLE_PRIVATE_KEY_EXPORT=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/companion_radio/main.cpp> +lib_deps = + ${Heltec_lora32_v3.lib_deps} + densaugeo/base64 @ ~1.4.0 + ; ================ [Xiao_esp32_C3] extends = esp32_base diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/esp32/SerialWifiInterface.cpp new file mode 100644 index 00000000..e7dc055e --- /dev/null +++ b/src/helpers/esp32/SerialWifiInterface.cpp @@ -0,0 +1,93 @@ +#include "SerialWifiInterface.h" +#include + +void SerialWifiInterface::begin(int port) { + // wifi setup is handled outside of this class, only starts the server + server.begin(port); +} + +// ---------- public methods +void SerialWifiInterface::enable() { + if (_isEnabled) return; + + _isEnabled = true; + clearBuffers(); +} + +void SerialWifiInterface::disable() { + _isEnabled = false; +} + +size_t SerialWifiInterface::writeFrame(const uint8_t src[], size_t len) { + if (len > MAX_FRAME_SIZE) { + WIFI_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); + return 0; + } + + if (deviceConnected && len > 0) { + if (send_queue_len >= FRAME_QUEUE_SIZE) { + WIFI_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + return 0; + } + + send_queue[send_queue_len].len = len; // add to send queue + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + + return len; + } + return 0; +} + +bool SerialWifiInterface::isWriteBusy() const { + return false; +} + +size_t SerialWifiInterface::checkRecvFrame(uint8_t dest[]) { + if (!client) client = server.available(); + + if (client.connected()) { + if (!deviceConnected) { + WIFI_DEBUG_PRINTLN("Got connection"); + deviceConnected = true; + } + } else { + if (deviceConnected) { + deviceConnected = false; + WIFI_DEBUG_PRINTLN("Disconnected"); + } + } + + if (deviceConnected) { + if (send_queue_len > 0) { // first, check send queue + + _last_write = millis(); + int len = send_queue[0].len; + + uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames + pkt[0] = '>'; + pkt[1] = (len & 0xFF); // LSB + pkt[2] = (len >> 8); // MSB + memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); + client.write(pkt, 3 + len); + send_queue_len--; + for (int i = 0; i < send_queue_len; i++) { // delete top item from queue + send_queue[i] = send_queue[i + 1]; + } + } else { + int len = client.available(); + if (len > 0) { + uint8_t buf[MAX_FRAME_SIZE + 4]; + client.readBytes(buf, len); + memcpy(dest, buf+3, len-3); // remove header (don't even check ... problems are on the other dir) + return len-3; + } + } + } + + return 0; +} + +bool SerialWifiInterface::isConnected() const { + return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0; +} \ No newline at end of file diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/esp32/SerialWifiInterface.h new file mode 100644 index 00000000..2b6c6edd --- /dev/null +++ b/src/helpers/esp32/SerialWifiInterface.h @@ -0,0 +1,59 @@ +#pragma once + +#include "../BaseSerialInterface.h" +#include + +class SerialWifiInterface : public BaseSerialInterface { + bool deviceConnected; + bool _isEnabled; + unsigned long _last_write; + unsigned long adv_restart_time; + + WiFiServer server; + WiFiClient client; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 4 + int recv_queue_len; + Frame recv_queue[FRAME_QUEUE_SIZE]; + int send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; } + +protected: + +public: + SerialWifiInterface() : server(WiFiServer()), client(WiFiClient()) { + deviceConnected = false; + _isEnabled = false; + _last_write = 0; + send_queue_len = recv_queue_len = 0; + } + + void begin(int port); + + // BaseSerialInterface methods + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override; + bool isWriteBusy() const override; + + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; +}; + +#if WIFI_DEBUG_LOGGING && ARDUINO + #include + #define WIFI_DEBUG_PRINT(F, ...) Serial.printf("WiFi: " F, ##__VA_ARGS__) + #define WIFI_DEBUG_PRINTLN(F, ...) Serial.printf("WiFi: " F "\n", ##__VA_ARGS__) +#else + #define WIFI_DEBUG_PRINT(...) {} + #define WIFI_DEBUG_PRINTLN(...) {} +#endif