diff --git a/.gitignore b/.gitignore index 89cc49c..3b1fbc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .pio +include/homenet.h .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json diff --git a/Makefile b/Makefile index d203656..889aaa0 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ deploy: $(FIRMWARE) .PHONY: clean clean: $(PIO) -t clean + rm -rf *.bin unpacked_fs .PHONY: cloc cloc: @@ -35,3 +36,7 @@ cloc: test: $(PIO) -t test +.PHONY: downloadfs +downloadfs: + $(PIO) -t downloadfs + diff --git a/include/Arena.h b/include/Arena.h new file mode 100644 index 0000000..10fbae0 --- /dev/null +++ b/include/Arena.h @@ -0,0 +1,44 @@ +#ifndef KIMODEM_ARENA_H +#define KIMODEM_ARENA_H + + +#include +#include +#include + + +typedef struct { + uint8_t *Store; + size_t Size; + int fd; + uint8_t Type; +} Arena; + + +/* + * InitializeArena is intended for use only with systems that + * do not initialize new variables to zero. It should be called + * exactly once, at the start of the program. Any other time the + * arena needs to be reset, it should be called with clear_arena + * or destroy_arena. + */ +void InitializeArena(Arena &arena); +int NewStaticArena(Arena &, uint8_t *, size_t); +int AllocNewArena(Arena &, size_t); +#if defined(__linux__) +int MMapArena(Arena &, int); /* arena will own fd */ +int CreateArena(Arena &arena, const char *path, size_t size, mode_t mode); +int OpenArena(Arena &, const char *, size_t); +#endif + +void ClearArena(Arena &); +int DestroyArena(Arena &); /* dispose of any memory used by arena */ + +/* DANGER: if arena is file backed (mmap or open), DO NOT WRITE TO THE + * BACKING FILE! */ +int WriteArena(const Arena &arena, const char *path); + +void DisplayArena(const Arena &arena); + + +#endif diff --git a/include/Dictionary.h b/include/Dictionary.h new file mode 100644 index 0000000..5dec825 --- /dev/null +++ b/include/Dictionary.h @@ -0,0 +1,44 @@ +#ifndef KLIB_DICTIONARY_H +#define KLIB_DICTIONARY_H + + +#include "Arena.h" +#include "TLV.h" + + +#define DICTIONARY_TAG_KEY 1 +#define DICTIONARY_TAG_VAL 2 + + +/* + * A Dictionary is a collection of key-value pairs, similar to how + * a dictionary is a mapping of names to definitions. + */ +class Dictionary { +public: + Dictionary(Arena &arena) : + arena(arena), + kTag(DICTIONARY_TAG_KEY), + vTag(DICTIONARY_TAG_VAL) {} ; + Dictionary(Arena &arena, uint8_t kt, uint8_t vt) : + arena(arena), + kTag(kt), + vTag(vt) {}; + + bool Lookup(const char *key, uint8_t klen, TLV::Record &res); + int Set(const char *key, uint8_t klen, const char *val, + uint8_t vlen); + bool Has(const char *key, uint8_t klen); + void DumpKVPairs(); + void DumpToFile(const char *path); +private: + uint8_t *seek(const char *key, uint8_t klen); + bool spaceAvailable(uint8_t klen, uint8_t vlen); + + Arena &arena; + uint8_t kTag; + uint8_t vTag; +}; + + +#endif diff --git a/include/TLV.h b/include/TLV.h new file mode 100644 index 0000000..e1c7e84 --- /dev/null +++ b/include/TLV.h @@ -0,0 +1,48 @@ +#ifndef KIMODEM_TLV_H +#define KIMODEM_TLV_H + +#include + +#include "Arena.h" + + +#ifndef TLV_MAX_LEN +#define TLV_MAX_LEN 253 +#endif + + +#define TAG_EMPTY 0 + + +namespace TLV { + + +struct Record { + uint8_t Tag; + uint8_t Len; + char Val[TLV_MAX_LEN]; +}; + + +uint8_t *WriteToMemory(Arena &, uint8_t *, Record &); +void ReadFromMemory(Record &, uint8_t *); +void SetRecord(Record &, uint8_t, uint8_t, const char *); +void DeleteRecord(Arena &, uint8_t *); + +/* + * returns a pointer to memory where the record was found, + * e.g. LocateTag(...)[0] is the tag of the found record. + * FindTag will call LocateTag and then SkipRecord if the + * tag was found. + */ +uint8_t *FindTag(Arena &, uint8_t *, Record &); +uint8_t *LocateTag(Arena &, uint8_t *, Record &); + +uint8_t *FindEmpty(Arena &, uint8_t *); +uint8_t *SkipRecord(Record &, uint8_t *); + + +} // namespace TLV + + +#endif diff --git a/include/WiFiMgr.h b/include/WiFiMgr.h new file mode 100644 index 0000000..b2138c5 --- /dev/null +++ b/include/WiFiMgr.h @@ -0,0 +1,15 @@ +#ifndef KIMODEM_WIFI_H +#define KIMODEM_WIFI_H + + +#include "Dictionary.h" +#include "WiFiMgr.h" + + +bool SetupWiFi(); +bool Autoconnect(Dictionary &pb); +bool Autoconnect(Dictionary &pb, bool reset); + + +#endif + diff --git a/platformio.ini b/platformio.ini index da5261c..0d37b26 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,7 +8,11 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html +[platformio] + [env:sparkfun_esp32micromod] platform = espressif32 board = sparkfun_esp32micromod framework = arduino +monitor_speed = 115200 +extra_scripts = scripts/download_fs.py diff --git a/scripts/download_fs.py b/scripts/download_fs.py new file mode 100644 index 0000000..b888290 --- /dev/null +++ b/scripts/download_fs.py @@ -0,0 +1,341 @@ +# Written by Maximilian Gerhardt +# 29th December 2020 +# License: Apache +# Expanded from functionality provided by PlatformIO's espressif32 and espressif8266 platforms, credited below. +# This script provides functions to download the filesystem (SPIFFS or LittleFS) from a running ESP32 / ESP8266 +# over the serial bootloader using esptool.py, and mklittlefs / mkspiffs for extracting. +# run by either using the VSCode task "Custom" -> "Download Filesystem" +# or by doing 'pio run -t downloadfs' (with optional '-e ') from the commandline. +# output will be saved, by default, in the "unpacked_fs" of the project. +# this folder can be changed by writing 'custom_unpack_dir = some_other_dir' in the corresponding platformio.ini +# environment. +import re +import sys +from os.path import isfile, join +from enum import Enum +import typing +from platformio.builder.tools.pioupload import AutodetectUploadPort +import os +import subprocess +import shutil +import shlex + +Import("env") +platform = env.PioPlatform() +board = env.BoardConfig() +mcu = board.get("build.mcu", "esp32") +# needed for later +AutodetectUploadPort(env) + +class FSType(Enum): + SPIFFS="spiffs" + LITTLEFS="littlefs" + FATFS="fatfs" + +class FSInfo: + def __init__(self, fs_type, start, length, page_size, block_size): + self.fs_type = fs_type + self.start = start + self.length = length + self.page_size = page_size + self.block_size = block_size + def __repr__(self): + return f"FS type {self.fs_type} Start {hex(self.start)} Len {self.length} Page size {self.page_size} Block size {self.block_size}" + # extract command supposed to be implemented by subclasses + def get_extract_cmd(self): + raise NotImplementedError() + +class LittleFSInfo(FSInfo): + def __init__(self, start, length, page_size, block_size): + if env["PIOPLATFORM"] == "espressif32": + #for ESP32: retrieve and evaluate, e.g. to mkspiffs_espressif32_arduino + #Espressif32 Framework 3.X.X: MKSPIFFSTOOL + #Espressif32 Framework 4.X.X and 5.X.X: MKFSTOOL + if "MKSPIFFSTOOL" in env: + self.tool = env.subst(env["MKSPIFFSTOOL"]) + else: + self.tool = env.subst(env["MKFSTOOL"]) + else: + self.tool = env["MKFSTOOL"] # from mkspiffs package + self.tool = join(platform.get_package_dir("tool-mklittlefs"), self.tool) + super().__init__(FSType.LITTLEFS, start, length, page_size, block_size) + def __repr__(self): + return f"FS type {self.fs_type} Start {hex(self.start)} Len {self.length} Page size {self.page_size} Block size {self.block_size} Tool: {self.tool}" + def get_extract_cmd(self, input_file, output_dir): + return [self.tool, "-b", str(self.block_size), "-p", str(self.page_size), "--unpack", output_dir, input_file] + + +class SPIFFSInfo(FSInfo): + def __init__(self, start, length, page_size, block_size): + if env["PIOPLATFORM"] == "espressif32": + #for ESP32: retrieve and evaluate, e.g. to mkspiffs_espressif32_arduino + #Espressif32 Framework 3.X.X: MKSPIFFSTOOL + #Espressif32 Framework 4.X.X and 5.X.X: MKFSTOOL + if "MKSPIFFSTOOL" in env: + self.tool = env.subst(env["MKSPIFFSTOOL"]) + else: + self.tool = env.subst(env["MKFSTOOL"]) + else: + self.tool = env["MKFSTOOL"] # from mkspiffs package + self.tool = join(platform.get_package_dir("tool-mkspiffs"), self.tool) + super().__init__(FSType.SPIFFS, start, length, page_size, block_size) + def __repr__(self): + return f"FS type {self.fs_type} Start {hex(self.start)} Len {self.length} Page size {self.page_size} Block size {self.block_size} Tool: {self.tool}" + def get_extract_cmd(self, input_file, output_dir): + return [self.tool, "-b", str(self.block_size), "-p", str(self.page_size), "--unpack", output_dir, input_file] + +# SPIFFS helpers copied from ESP32, https://github.com/platformio/platform-espressif32/blob/develop/builder/main.py +# Copyright 2014-present PlatformIO +# Licensed under the Apache License, Version 2.0 (the "License"); + +def _parse_size(value): + if isinstance(value, int): + return value + elif value.isdigit(): + return int(value) + elif value.startswith("0x"): + return int(value, 16) + elif value[-1].upper() in ("K", "M"): + base = 1024 if value[-1].upper() == "K" else 1024 * 1024 + return int(value[:-1]) * base + return value + +def _parse_partitions(env): + partitions_csv = env.subst("$PARTITIONS_TABLE_CSV") + if not isfile(partitions_csv): + sys.stderr.write("Could not find the file %s with partitions " + "table.\n" % partitions_csv) + env.Exit(1) + return + + result = [] + next_offset = 0 + with open(partitions_csv) as fp: + for line in fp.readlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + tokens = [t.strip() for t in line.split(",")] + if len(tokens) < 5: + continue + partition = { + "name": tokens[0], + "type": tokens[1], + "subtype": tokens[2], + "offset": tokens[3] or next_offset, + "size": tokens[4], + "flags": tokens[5] if len(tokens) > 5 else None + } + result.append(partition) + next_offset = (_parse_size(partition['offset']) + + _parse_size(partition['size'])) + return result + +def esp32_fetch_spiffs_size(env): + spiffs = None + for p in _parse_partitions(env): + if p['type'] == "data" and p['subtype'] == "spiffs": + spiffs = p + if not spiffs: + sys.stderr.write( + env.subst("Could not find the `spiffs` section in the partitions " + "table $PARTITIONS_TABLE_CSV\n")) + env.Exit(1) + return + env["SPIFFS_START"] = _parse_size(spiffs['offset']) + env["SPIFFS_SIZE"] = _parse_size(spiffs['size']) + env["SPIFFS_PAGE"] = int("0x100", 16) + env["SPIFFS_BLOCK"] = int("0x1000", 16) + +## FS helpers for ESP8266 +# copied from https://github.com/platformio/platform-espressif8266/blob/develop/builder/main.py +# Copyright 2014-present PlatformIO +# Licensed under the Apache License, Version 2.0 (the "License"); + +def _get_board_f_flash(env): + frequency = env.subst("$BOARD_F_FLASH") + frequency = str(frequency).replace("L", "") + return int(int(frequency) / 1000000) + +def _parse_ld_sizes(ldscript_path): + assert ldscript_path + result = {} + # get flash size from board's manifest + result['flash_size'] = int(env.BoardConfig().get("upload.maximum_size", 0)) + # get flash size from LD script path + match = re.search(r"\.flash\.(\d+[mk]).*\.ld", ldscript_path) + if match: + result['flash_size'] = _parse_size(match.group(1)) + + appsize_re = re.compile( + r"irom0_0_seg\s*:.+len\s*=\s*(0x[\da-f]+)", flags=re.I) + filesystem_re = re.compile( + r"PROVIDE\s*\(\s*_%s_(\w+)\s*=\s*(0x[\da-f]+)\s*\)" % "FS" + if "arduino" in env.subst("$PIOFRAMEWORK") + else "SPIFFS", + flags=re.I, + ) + with open(ldscript_path) as fp: + for line in fp.readlines(): + line = line.strip() + if not line or line.startswith("/*"): + continue + match = appsize_re.search(line) + if match: + result['app_size'] = _parse_size(match.group(1)) + continue + match = filesystem_re.search(line) + if match: + result['fs_%s' % match.group(1)] = _parse_size( + match.group(2)) + return result + +def _get_flash_size(env): + ldsizes = _parse_ld_sizes(env.GetActualLDScript()) + if ldsizes['flash_size'] < 1048576: + return "%dK" % (ldsizes['flash_size'] / 1024) + return "%dM" % (ldsizes['flash_size'] / 1048576) + +def esp8266_fetch_fs_size(env): + ldsizes = _parse_ld_sizes(env.GetActualLDScript()) + for key in ldsizes: + if key.startswith("fs_"): + env[key.upper()] = ldsizes[key] + + assert all([ + k in env + for k in ["FS_START", "FS_END", "FS_PAGE", "FS_BLOCK"] + ]) + + # esptool flash starts from 0 + for k in ("FS_START", "FS_END"): + _value = 0 + if env[k] < 0x40300000: + _value = env[k] & 0xFFFFF + elif env[k] < 0x411FB000: + _value = env[k] & 0xFFFFFF + _value -= 0x200000 # correction + else: + _value = env[k] & 0xFFFFFF + _value += 0xE00000 # correction + + env[k] = _value + +def esp8266_get_esptoolpy_reset_flags(resetmethod): + # no dtr, no_sync + resets = ("no_reset_no_sync", "soft_reset") + if resetmethod == "nodemcu": + # dtr + resets = ("default_reset", "hard_reset") + elif resetmethod == "ck": + # no dtr + resets = ("no_reset", "soft_reset") + + return ["--before", resets[0], "--after", resets[1]] + +## Script interface functions + +def get_fs_type_start_and_length(): + platform = env["PIOPLATFORM"] + if platform == "espressif32": + print("Retrieving filesystem info for ESP32. Assuming SPIFFS.") + print("Partition file: " + str(env.subst("$PARTITIONS_TABLE_CSV"))) + esp32_fetch_spiffs_size(env) + return SPIFFSInfo(env["SPIFFS_START"], env["SPIFFS_SIZE"], env["SPIFFS_PAGE"], env["SPIFFS_BLOCK"]) + elif platform == "espressif8266": + print("Retrieving filesystem info for ESP8266.") + filesystem = board.get("build.filesystem", "spiffs") + if filesystem not in ("spiffs", "littlefs"): + print("Unrecognized board_build.filesystem option '" + str(filesystem) + "'.") + env.Exit(1) + # fetching sizes is the same for all filesystems + esp8266_fetch_fs_size(env) + print("FS_START: " + hex(env["FS_START"])) + print("FS_END: " + hex(env["FS_END"])) + print("FS_PAGE: " + hex(env["FS_PAGE"])) + print("FS_BLOCK: " + hex(env["FS_BLOCK"])) + if filesystem == "spiffs": + print("Recognized SPIFFS filesystem.") + return SPIFFSInfo(env["FS_START"], env["FS_END"] - env["FS_START"], env["FS_PAGE"], env["FS_BLOCK"]) + elif filesystem == "littlefs": + print("Recognized LittleFS filesystem.") + return LittleFSInfo(env["FS_START"], env["FS_END"] - env["FS_START"], env["FS_PAGE"], env["FS_BLOCK"]) + else: + print("Unrecongized configuration.") + pass + +def download_fs(fs_info: FSInfo): + esptoolpy = join(platform.get_package_dir("tool-esptoolpy") or "", "esptool.py") + fs_file = join(env["PROJECT_DIR"], f"downloaded_fs_{hex(fs_info.start)}_{hex(fs_info.length)}.bin") + esptoolpy_cmd = [ + env["PYTHONEXE"], + esptoolpy, + "--chip", mcu, + "--port", env.subst("$UPLOAD_PORT"), + "--baud", env.subst("$UPLOAD_SPEED"), + "--before", "default_reset", + "--after", "hard_reset", + "read_flash", + hex(fs_info.start), + hex(fs_info.length), + fs_file + ] + print("Executing flash download command.") + print(shlex.join(esptoolpy_cmd)) + try: + subprocess.call(esptoolpy_cmd) + print("Downloaded filesystem binary.") + return (True, fs_file) + except subprocess.CalledProcessError as exc: + print("Downloading failed with " + str(exc)) + return (False, "") + +def unpack_fs(fs_info: FSInfo, downloaded_file: str): + # by writing custom_unpack_dir = some_dir in the platformio.ini, one can + # control the unpack directory + unpack_dir = env.GetProjectOption("custom_unpack_dir", "unpacked_fs") + #unpack_dir = "unpacked_fs" + try: + if os.path.exists(unpack_dir): + shutil.rmtree(unpack_dir) + except Exception as exc: + print("Exception while attempting to remove the folder '" + str(unpack_dir) + "': " + str(exc)) + if not os.path.exists(unpack_dir): + os.makedirs(unpack_dir) + + cmd = fs_info.get_extract_cmd(downloaded_file, unpack_dir) + print("Executing extraction command:", shlex.join(cmd)) + try: + subprocess.call(cmd) + print("Unpacked filesystem.") + return (True, unpack_dir) + except subprocess.CalledProcessError as exc: + print("Unpacking filesystem failed with " + str(exc)) + return (False, "") + +def display_fs(extracted_dir): + # extract command already nicely lists all extracted files. + # no need to display that ourselves. just display a summary + file_count = sum([len(files) for r, d, files in os.walk(extracted_dir)]) + print("Extracted " + str(file_count) + " file(s) from filesystem.") + +def command_download_fs(*args, **kwargs): + print("Entrypoint") + #print(env.Dump()) + info = get_fs_type_start_and_length() + print("Parsed FS info: " + str(info)) + download_ok, downloaded_file = download_fs(info) + print("Download was okay: " + str(download_ok) + ". File at: "+ str(downloaded_file)) + unpack_ok, unpacked_dir = unpack_fs(info, downloaded_file) + if unpack_ok is True: + display_fs(unpacked_dir) + +env.AddCustomTarget( + name="downloadfs", + dependencies=None, + actions=[ + command_download_fs + ], + title="Download Filesystem", + description="Downloads and displays files stored in the target ESP32/ESP8266" +) diff --git a/src/Arena.cc b/src/Arena.cc new file mode 100644 index 0000000..e2fa5b0 --- /dev/null +++ b/src/Arena.cc @@ -0,0 +1,291 @@ +#if defined(__linux__) || defined(MSVC) +#include +#endif +#include +#include + +#if defined(__linux__) +#include +#include +#include +#include +#include +#endif + +#if defined(DESKTOP_BUILD) +#include +#include +#endif + +#if defined(ESP_PLATFORM) +#include +#include +#endif + +#include "Arena.h" + + +#define ARENA_UNINIT 0 +#define ARENA_STATIC 1 +#define ARENA_ALLOC 2 +#if defined(__linux__) +#define ARENA_MMAP 3 +#define PROT_RW PROT_READ|PROT_WRITE +#endif + + +void +InitializeArena(Arena &arena) +{ + arena.Store = NULL; + arena.Size = 0; + arena.Type = ARENA_UNINIT; + arena.fd = 0; +} + + +int +NewStaticArena(Arena &arena, uint8_t *mem, size_t size) +{ + arena.Store = mem; + arena.Size = size; + arena.Type = ARENA_STATIC; + return 0; +} + + +int +AllocNewArena(Arena & arena, size_t size) +{ + if (arena.Size > 0) { + if (DestroyArena(arena) != 0) { + return -1; + } + } + + arena.Type = ARENA_ALLOC; + arena.Size = size; + arena.Store = (uint8_t *)calloc(sizeof(uint8_t), size); + if (arena.Store == NULL) { + return -1; + } + + return 0; +} + + +#if defined(__linux__) +int +MMapArena(Arena &arena, int fd, size_t size) +{ + if (arena.Size > 0) { + if (DestroyArena(arena) != 0) { + return -1; + } + } + + arena.Type = ARENA_MMAP; + arena.Size = size; + arena.Store = (uint8_t *)mmap(NULL, size, PROT_RW, MAP_SHARED, fd, 0); + if ((void *)arena.Store == MAP_FAILED) { + return -1; + } + arena.fd = fd; + return 0; +} + + +int +OpenArena(Arena &arena, const char *path) +{ + struct stat st; + + if (arena.Size > 0) { + if (DestroyArena(arena) != 0) { + return -1; + } + } + + if (stat(path, &st) != 0) { + return -1; + } + + arena.fd = open(path, O_RDWR); + if (arena.fd == -1) { + return -1; + } + + return MMapArena(arena, arena.fd, (size_t)st.st_size); +} + + +int +CreateArena(Arena &arena, const char *path, size_t size, mode_t mode) +{ + int fd = 0; + + if (arena.Size > 0) { + if (DestroyArena(arena) != 0) { + return -1; + } + } + + fd = open(path, O_WRONLY|O_CREAT|O_TRUNC, mode); + if (fd == -1) { + return -1; + } + + if (ftruncate(fd, size) == -1) { + return -1; + } + + close(fd); + + return OpenArena(arena, path); +} +#endif + + +/* + * ClearArena clears the memory being used, removing any data + * present. It does not free the memory; it is effectively a + * wrapper around memset. + */ +void +ClearArena(Arena &arena) +{ + if (arena.Size == 0) { + return; + } + + memset(arena.Store, 0, arena.Size); +} + + +int +DestroyArena(Arena &arena) +{ + if (arena.Type == ARENA_UNINIT) { + return 0; + } + + switch (arena.Type) { + case ARENA_STATIC: + break; + case ARENA_ALLOC: + free(arena.Store); + break; + #if defined(__linux__) + case ARENA_MMAP: + if (munmap(arena.Store, arena.Size) == -1) { + return -1; + } + + if (close(arena.fd) == -1) { + return -1; + } + + arena.fd = 0; + break; + #endif + default: + #if defined(NDEBUG) + return -1; + #else + abort(); + #endif + + } + + arena.Type = ARENA_UNINIT; + arena.Size = 0; + arena.Store = NULL; + return 0; +} + +#if defined(DESKTOP_BUILD) +void +DisplayArena(const Arena &arena) +{ + std::cout << "Arena @ 0x"; + std::cout << std::hex << (uintptr_t)&arena << std::endl; + std::cout << std::dec; + std::cout << "\tStore is " << arena.Size << " bytes at address 0x"; + std::cout << std::hex << (uintptr_t)&(arena.Store) << std::endl; + std::cout << "\tType: "; + + switch (arena.Type) { + case ARENA_UNINIT: + std::cout << "uninitialized"; + break; + case ARENA_STATIC: + std::cout << "static"; + break; + case ARENA_ALLOC: + std::cout << "allocated"; + break; +#if defined(__linux__) + case ARENA_MMAP: + std::cout << "mmap/file"; + break; +#endif + default: + std::cout << "unknown (this is a bug)"; + } + std::cout << std::endl; +} +#else +void +DisplayArena(const Arena &arena) +{ + +} +#endif + +#if defined(__linux__) || defined(__MSVC__) +int +WriteArena(const Arena &arena, const char *path) +{ + FILE *arenaFile = NULL; + int retc = -1; + + arenaFile = fopen(path, "w"); + if (arenaFile == NULL) { + return -1; + } + + if (fwrite(arena.Store, sizeof(*arena.Store), arena.Size, + arenaFile) == arena.Size) { + retc = 0; + } + + if (fclose(arenaFile) != 0) { + return -1; + } + + return retc; +} +#elif defined(ESP_PLATFORM) +int +WriteArena(const Arena &arena, const char *path) +{ + File arenaDataFile = SPIFFS.open(path, FILE_WRITE); + size_t offset = 0; + size_t written = 0; + + while (written < arena.Size) { + offset = arenaDataFile.write(arena.Store + written, arena.Size - written); + if (offset == 0) { + break; + } + + written += offset; + } + + if (written != arena.Size) { + return -1; + } + + return 0; +} +#endif diff --git a/src/Dictionary.cc b/src/Dictionary.cc new file mode 100644 index 0000000..42e451b --- /dev/null +++ b/src/Dictionary.cc @@ -0,0 +1,186 @@ +#include +#include +#include +#include "Dictionary.h" + +#if defined(DESKTOP_BUILD) +#include +#endif + +bool +Dictionary::Lookup(const char *key, uint8_t klen, TLV::Record &res) +{ + res.Tag = this->kTag; + + uint8_t *cursor = TLV::FindTag(this->arena, NULL, res); + + while ((cursor != NULL) && (res.Tag != TAG_EMPTY)) { + if (klen != res.Len) { + cursor = TLV::FindTag(this->arena, cursor, res); + continue; + } + + if (memcmp(res.Val, key, klen) == 0) { + TLV::ReadFromMemory(res, cursor); + if (res.Tag != this->vTag) { + abort(); + } + return true; + } + + cursor = TLV::FindTag(this->arena, cursor, res); + } + + return false; + +} + + +int +Dictionary::Set(const char *key, uint8_t klen, const char *val, uint8_t vlen) +{ + TLV::Record rec; + uint8_t *cursor = NULL; + + SetRecord(rec, this->kTag, klen, key); + cursor = this->seek(key, klen); + if (cursor != NULL) { + TLV::DeleteRecord(this->arena, cursor); + TLV::DeleteRecord(this->arena, cursor); + } + + if (!spaceAvailable(klen, vlen)) { + return -1; + } + + cursor = TLV::WriteToMemory(this->arena, NULL, rec); + if (cursor == NULL) { + return -1; + } + + SetRecord(rec, this->vTag, vlen, val); + if (TLV::WriteToMemory(this->arena, NULL, rec) == NULL) { + return -1; + } + + return 0; +} + + + +uint8_t * +Dictionary::seek(const char *key, uint8_t klen) +{ + TLV::Record rec; + + rec.Tag = this->kTag; + uint8_t *cursor = TLV::LocateTag(this->arena, NULL, rec); + + while (cursor != NULL) { + if ((klen == rec.Len) && (this->kTag == rec.Tag)) { + if (memcmp(rec.Val, key, klen) == 0) { + return cursor; + } + } + cursor = TLV::SkipRecord(rec, cursor); + cursor = TLV::LocateTag(this->arena, cursor, rec); + } + + return NULL; +} + + +bool +Dictionary::Has(const char *key, uint8_t klen) +{ + return this->seek(key, klen) != NULL; +} + + +bool +Dictionary::spaceAvailable(uint8_t klen, uint8_t vlen) +{ + size_t required = 0; + uintptr_t remaining = 0; + uint8_t *cursor = NULL; + + cursor = TLV::FindEmpty(this->arena, NULL); + if (cursor == NULL) { + return false; + } + + required += klen + 2; + required += vlen + 2; + + remaining = (uintptr_t)cursor - (uintptr_t)arena.Store; + remaining = arena.Size - remaining; + return ((size_t)remaining >= required); +} + + +#if defined(DESKTOP_BUILD) +void +Dictionary::DumpKVPairs() +{ + uint8_t *cursor = (this->arena).Store; + TLV::Record rec; + + TLV::ReadFromMemory(rec, cursor); + std::cout << "Dictionary KV pairs" << std::endl; + if (rec.Tag == TAG_EMPTY) { + std::cout << "\t(NONE)" << std::endl; + return; + } + + while ((cursor != NULL) && (rec.Tag != TAG_EMPTY)) { + std::cout << "\t" << rec.Val << "->"; + cursor = TLV::SkipRecord(rec, cursor); + TLV::ReadFromMemory(rec, cursor); + std::cout << rec.Val << std::endl; + cursor = TLV::SkipRecord(rec, cursor); + TLV::ReadFromMemory(rec, cursor); + } + +} +#elif defined(ESP_PLATFORM) +#include +void +Dictionary::DumpKVPairs() +{ + uint8_t *cursor = (this->arena).Store; + TLV::Record rec; + + TLV::ReadFromMemory(rec, cursor); + + Serial.println("dictionary entries: {"); + if (rec.Tag == TAG_EMPTY) { + Serial.println("\tNONE"); + Serial.println("}"); + return; + } + + while ((cursor != NULL) && (rec.Tag != TAG_EMPTY)) { + Serial.print("\t"); + Serial.print(rec.Val); + Serial.print("->"); + cursor = TLV::SkipRecord(rec, cursor); + TLV::ReadFromMemory(rec, cursor); + Serial.println(rec.Val); + cursor = TLV::SkipRecord(rec, cursor); + TLV::ReadFromMemory(rec, cursor); + } + Serial.println("}"); +} +#else +void +Dictionary::DumpKVPairs(const char *label) +{ +} +#endif + + +void +Dictionary::DumpToFile(const char *path) +{ + WriteArena(this->arena, path); +} diff --git a/src/TLV.cc b/src/TLV.cc new file mode 100644 index 0000000..d647411 --- /dev/null +++ b/src/TLV.cc @@ -0,0 +1,219 @@ +#include +#include "TLV.h" + +#if defined(ESP_PLATFORM) +#include +#endif + + +#define REC_SIZE(x) ((std::size_t)x.Len + 2) + + +namespace TLV { + + +static inline size_t +spaceRemaining(const Arena &arena, const uint8_t *cursor) +{ + uintptr_t remaining = 0; + + if (cursor == NULL) { + cursor = arena.Store; + } + + remaining = (uintptr_t)cursor - (uintptr_t)arena.Store; + remaining = arena.Size - remaining; + return remaining; +} + + +static bool +cursorInArena(const Arena &arena, const uint8_t *cursor) +{ + if (cursor == NULL) { + return false; + } + + uint8_t *end = arena.Store + spaceRemaining(arena, NULL); + if (cursor < arena.Store) { + return false; + } + + return cursor < end; +} + + +static bool +spaceAvailable(const Arena &arena, const uint8_t *cursor, const uint8_t len) +{ + uintptr_t remaining = 0; + + if (cursor == NULL) { + return false; + } + + remaining = (uintptr_t)cursor - (uintptr_t)arena.Store; + remaining = arena.Size - remaining; + + return ((size_t)remaining > ((size_t)len+2)); +} + +static inline void +clearUnused(Record &rec) +{ + uint8_t trail = TLV_MAX_LEN-rec.Len; + + memset(rec.Val+rec.Len, 0, trail); +} + + +uint8_t * +WriteToMemory(Arena &arena, uint8_t *cursor, Record &rec) +{ + // If cursor is NULL, the user needs us to select an empty + // slot for the record. If we can't find one, that's an + // error. + // + // If, however, the user gives us a cursor, we'll trust it + // (though spaceAvailable will sanity check that cursor). + if (cursor == NULL) { + cursor = FindEmpty(arena, cursor); + if (cursor == NULL) { + return NULL; + } + } + + if (!spaceAvailable(arena, cursor, rec.Len)) { + return NULL; + } + + memcpy(cursor, &rec, REC_SIZE(rec)); + cursor = SkipRecord(rec, cursor); + + return cursor; +} + + +void +SetRecord(Record &rec, uint8_t tag, uint8_t len, const char *val) +{ + rec.Tag = tag; + rec.Len = len; + memcpy(rec.Val, val, len); + clearUnused(rec); +} + + +void +ReadFromMemory(Record &rec, uint8_t *cursor) +{ + rec.Tag = cursor[0]; + rec.Len = cursor[1]; + memcpy(rec.Val, cursor+2, rec.Len); + clearUnused(rec); +} + + +/* + * returns a pointer to memory where the record was found, + * e.g. FindTag(...)[0] is the tag of the found record. + */ +uint8_t * +FindTag(Arena &arena, uint8_t *cursor, Record &rec) +{ + cursor = LocateTag(arena, cursor, rec); + if (cursor == NULL) { + return NULL; + } + + if (rec.Tag != TAG_EMPTY) { + cursor = SkipRecord(rec, cursor); + } + + return cursor; +} + + +uint8_t * +LocateTag(Arena &arena, uint8_t *cursor, Record &rec) +{ + uint8_t tag, len; + + if (cursor == NULL) { + cursor = arena.Store; + } + + if (!cursorInArena(arena, cursor)) { + return NULL; + } + + while ((tag = cursor[0]) != rec.Tag) { + // We could call SkipRecord, but we already need + // to pull the length to figure out if we're at + // the end or not. + len = cursor[1]; + if ((tag == TAG_EMPTY) && (len == 0)) { + return NULL; + } + + if (!spaceAvailable(arena, cursor, len)) { + return NULL; + } + cursor += len; + cursor += 2; + } + + if (tag != rec.Tag) { + return NULL; + } + + if (tag != TAG_EMPTY) { + ReadFromMemory(rec, cursor); + } + return cursor; +} + + +uint8_t * +FindEmpty(Arena &arena, uint8_t *cursor) { + Record rec; + + rec.Tag = TAG_EMPTY; + return FindTag(arena, cursor, rec); +} + + + +uint8_t * +SkipRecord(Record &rec, uint8_t *cursor) +{ + return (uint8_t *)((uintptr_t)cursor + rec.Len + 2); +} + + +void +DeleteRecord(Arena &arena, uint8_t *cursor) +{ + if (cursor == NULL) { + return; + } + + uint8_t len = cursor[1] + 2; + uint8_t *stop = arena.Store + arena.Size; + + stop -= len; + + while (cursor != stop) { + cursor[0] = cursor[len]; + cursor++; + } + + stop += len; + while (cursor != stop) { + cursor[0] = 0; + cursor++; + } +} + + +} // namespace TLV diff --git a/src/WiFiMgr.cc b/src/WiFiMgr.cc new file mode 100644 index 0000000..2399b45 --- /dev/null +++ b/src/WiFiMgr.cc @@ -0,0 +1,95 @@ +#include +#include +#include + +#include "Dictionary.h" +#include "TLV.h" +#include "WiFiMgr.h" + +#define MAX_WAIT 10000 +#define CONN_WAIT 250 + + +bool +SetupWiFi() +{ + WiFi.mode(WIFI_STA); + return true; +} + + +static bool +tryConnect(Dictionary &pb, int network) +{ + const char *ssid = WiFi.SSID(network).c_str(); + size_t ssidLen = strnlen(ssid, TLV_MAX_LEN); + TLV::Record password; + size_t waitedFor = 0; + + if (ssidLen == 0) { + return false; + } + + Serial.print("MODEM: CHECK "); + Serial.println(ssid); + + if (!pb.Lookup(ssid, uint8_t(ssidLen), password)) { + Serial.println("SSID NOT IN PHONEBOOK"); + return false; + } + + Serial.println("CONNECTING"); + WiFi.begin(ssid, password.Val); + while ((WiFi.status() != WL_CONNECTED) && (waitedFor < MAX_WAIT)) { + waitedFor += CONN_WAIT; + if ((waitedFor % 1000) == 0) { + Serial.print("."); + } + delay(250); + } + + if (WiFi.status() == WL_CONNECTED) { + Serial.print("MODEM ADDR "); + Serial.println(WiFi.localIP()); + return true; + } + + Serial.println("CARRIER SIGNAL LOST"); + return false; +} + +bool +Autoconnect(Dictionary &pb) +{ + Autoconnect(pb, true); +} + + +bool +Autoconnect(Dictionary &pb, bool reset) +{ + int networkCount = 0; + int network = 0; + + Serial.println("MODEM: TRY AUTOCONNECT"); + + if (reset) { + Serial.println("RADIO RESET"); + SetupWiFi(); + WiFi.disconnect(); + delay(1000); + } else { + Serial.println("NO RESET"); + } + + networkCount = WiFi.scanNetworks(); + for (network = 0; network < networkCount; network++) { + if (tryConnect(pb, network)) { + return true; + } + } + + Serial.println("NO CARRIER"); + return false; +} + diff --git a/src/main.cc b/src/main.cc index cb9fbba..85a8131 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,18 +1,78 @@ #include +#include + +#include "Arena.h" +#include "Dictionary.h" +#include "WiFiMgr.h" +#include "homenet.h" + + +constexpr size_t PHONEBOOK_SIZE = 512; +constexpr char pbFilePath[] = "/pb.dat"; +uint8_t phonebookBuffer[PHONEBOOK_SIZE]; +Arena arena; +Dictionary phonebook(arena); + + +static bool +setupPhonebook() +{ + File pbFile = SPIFFS.open(pbFilePath, FILE_READ); + size_t fileSize = pbFile.size(); + bool ok = false; + + Serial.print("DAT FILE "); + Serial.print(fileSize); + Serial.println("B"); + if (fileSize == 0) { + Serial.println("INIT PHONEBOOK"); + phonebook.Set(HOME_SSID, HOME_SSIDLEN, HOME_WPA, HOME_WPALEN); + if (WriteArena(arena, pbFilePath) == 0) { + ok = true; + } + + pbFile.close(); + return ok; + } + fileSize = fileSize > PHONEBOOK_SIZE ? PHONEBOOK_SIZE : fileSize; + + Serial.print("LOAD PHONEBOOK "); + if (fileSize != pbFile.read(phonebookBuffer, fileSize)) { + Serial.println("FAILED"); + pbFile.close(); + return false; + } + + Serial.println("OK"); + pbFile.close(); + phonebook.DumpKVPairs(); + return true; +} -// put function declarations here: -int myFunction(int, int); void setup() { - // put your setup code here, to run once: - int result = myFunction(2, 3); + Serial.begin(115200); + while (!Serial) ; + + Serial.println("MODEM BOOT"); + InitializeArena(arena); + NewStaticArena(arena, phonebookBuffer, PHONEBOOK_SIZE); + + if (!SPIFFS.begin(true) && !SPIFFS.begin(false)) { + Serial.println("SPIFFS BEGIN FAIL"); + while (true) ; + } + setupPhonebook(); + + while (!Autoconnect(phonebook)) { + Serial.println("STANDBY"); + delay(1000); + } + + Serial.println("MODEM READY"); } void loop() { // put your main code here, to run repeatedly: } -// put function definitions here: -int myFunction(int x, int y) { - return x + y; -} \ No newline at end of file