commit 6489e78cdc349cbc35b1f34ccae028fc82c42c7c Author: vuko Date: Tue Mar 20 10:40:47 2018 +0100 adding project files current status: power and energy measuring works (not very well in (0;10W) range) not well defined behavior on network/mqtt disconnects current and voltage measurements TBD diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d043384 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.d +*.o +*.elf +*.hex +*.map +*.swp +*.swo +*.*.sw* +*.obj +*.gdb_history +*__pycache__* +**/debug/ +**/build diff --git a/README.mk b/README.mk new file mode 100644 index 0000000..c1374ed --- /dev/null +++ b/README.mk @@ -0,0 +1,45 @@ +spejsiot Sonoff® POW +==================== +"Spejsiot Building Automation Framework" implementation of Sonoff® POW + +dependencies +------------ +copy/link those dependencies to following directories: + + * [esp-open-sdk](https://github.com/pfalcon/esp-open-sdk) to ext/esp/esp-opensdk + * [Sming](https://github.com/SmingHub/Sming) to ext/esp/Sming + * [rboot](https://github.com/raburton/rboot) to ext/esp/rboot + +preparation +----------- + $ source cfg/env_noproject + +or if you are feeling brave: + + $ ./init.sh + +credits +------- +created by: Jan Wiśniewski + +This project uses [spejsiot](https://wiki.hackerspace.pl/projects:spejsiot). + + +licence +------- +All code except for spejsiot part is licensed under _zlib license_: + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. diff --git a/cfg/env b/cfg/env new file mode 100644 index 0000000..2e957ed --- /dev/null +++ b/cfg/env @@ -0,0 +1,3 @@ +#!/bin/bash +source "${PROJECT_DIR}/cfg/env_spacenode" +alias pd="cd $(realpath ${PROJECT_DIR})" diff --git a/cfg/env_spacenode b/cfg/env_spacenode new file mode 100644 index 0000000..9fd47c8 --- /dev/null +++ b/cfg/env_spacenode @@ -0,0 +1,9 @@ +#!/bin/bash +export ESP_HOME=$(realpath ${PROJECT_DIR}/ext/esp/esp-open-sdk/) +export SMING_HOME=$(realpath ${PROJECT_DIR}/ext/esp/Sming/Sming/) +export PATH="$(realpath ${PROJECT_DIR}/ext/esp/esp-open-sdk/esptool2):${PATH}" +export PATH="$(realpath ${PROJECT_DIR}/ext/esp/esp-open-sdk/xtensa-lx106-elf/bin/):${PATH}" + +# Make flashing slightly (4x) faster. +export ESPBAUD=460800 +export COM_SPEED_ESPTOOL="$ESPBAUD" diff --git a/cfg/noproject_env b/cfg/noproject_env new file mode 100644 index 0000000..6ffe7fd --- /dev/null +++ b/cfg/noproject_env @@ -0,0 +1,3 @@ +#!/usr/bin/bash +export PROJECT_DIR=$(realpath ./) +source cfg/env diff --git a/cfg/vim b/cfg/vim new file mode 100644 index 0000000..122563f --- /dev/null +++ b/cfg/vim @@ -0,0 +1 @@ +Project cfg/vimproject diff --git a/cfg/vimproject b/cfg/vimproject new file mode 100644 index 0000000..4866c48 --- /dev/null +++ b/cfg/vimproject @@ -0,0 +1,45 @@ +sonoff_pow="$PWD" CD=. filter="*.c *.h *.py" { + init.sh + README.mk + doc/hw.txt + doc/cse7759.py + + cfg/env + cfg/env_spacenode + cfg/env_noproject + cfg/vim + cfg/vimproject + + soft="soft" CD=. { + CMakeLists.txt + + Makefile + platform/standalone.rom.ld + platform/common.ld + platform/rboot.rom0.ld + + src="src" { + TcpPublish.cpp + TcpPublish.h + application.cpp + common/common_config.h + common/user_config.h + impuls_counter.c + impuls_counter.h + sonoff_pow.cpp + sonoff_pow.h + spejsiot="spejsiot" { + SpejsNode.h + Endpoint.h + Endpoint.cpp + endpoints + endpoints/OutputEndpoint.cpp + endpoints/OutputEndpoint.h + endpoints/ImplementationEndpoint.cpp + endpoints/ImplementationEndpoint.h + endpoints/ImplementationEndpoint.h + SpejsNode.cpp + ver.h + } + } +} diff --git a/doc/cse7759.py b/doc/cse7759.py new file mode 100644 index 0000000..86ab929 --- /dev/null +++ b/doc/cse7759.py @@ -0,0 +1,30 @@ +#!/usr/bin/python3 +import math + +f_osc = 3.549e+6 +v_ref = 2.43 + +r_i = 1e-3 + +x_c = 1 / (2 * math.pi * 50 * 100e-9) # excess +r1 = 5*470e+3 +r2 = 1/(1/1e+3 + 1/x_c) +v_2 = r2 / (r1 + r2) * 230 + +f_cf_max = v_2 * (16*r_i) * 48 / (v_ref**2) * f_osc / 128 +f_cfi_max = (16*r_i) * 24 / v_ref * f_osc / 512 +f_cfv_max = v_2 * 2 / v_ref * f_osc / 512 + +joul_to_period = 128 * (v_ref**2) * (r1+r2)/r2 / r_i / (48 * f_osc) +joul_to_input_change = joul_to_period/2 +wh_per_input_change = joul_to_input_change / (60 * 60) +w_per_changes_per_second = joul_to_input_change +samples_per_second = 4000 +w_per_changes_per_sample = joul_to_input_change * 4000 +print("power_factor: {:0.15f}".format(w_per_changes_per_sample)) +print("energy_factor: {:0.15f}".format(wh_per_input_change)) + +f_max = max([f_cf_max, f_cfi_max, f_cfv_max]) + +f_sample_min = f_max * 2 * 1.2 +t_sample = 1/f_sample_min diff --git a/doc/hw.txt b/doc/hw.txt new file mode 100644 index 0000000..e4da07c --- /dev/null +++ b/doc/hw.txt @@ -0,0 +1,22 @@ +sonoff ver 2.0 + esp to cse7795 + (12 - GPIO13 - D7) pin esp - CF1 + ( 9 - GPIO14 - D5) pin esp - CF + (24 - GPIO5 - D1) pin esp - SEL + esp to relay + (10 - GPIO12 - D6) pin esp - relay transistor and LED + flash memory: winbond 25q32fvsig 32M(i)b (4MiB) + +uart pinout + 1 GND + 2 TXD (esp 26 pin) + 3 RXD (esp 25 pin) + 4 VCC + +programing sequence: + 1. make sure device *is disconnected* from mains + 2. if device is connected to maing go to point 1 + 3. hold sonoff button + 4. power device from VCC pin (3.3V) + 5. release sonoff button + 6. make flash diff --git a/init.sh b/init.sh new file mode 100755 index 0000000..bb42276 --- /dev/null +++ b/init.sh @@ -0,0 +1,27 @@ +#!/bin/bash +BASEDIR=$(dirname ${BASH_SOURCE}) +cd "${BASEDIR}" +PROJECT_NAME=${PWD##*/} + +tmux attach-session -t "${PROJECT_NAME}" && exit 0 + +PROJECT_DIR="$PWD" +ENV="$(realpath ./cfg/env)" +tmux new-session -d -P -s "${PROJECT_NAME}" cat || exit 1 +tmux set-environment -t "${PROJECT_NAME}" "ENV" "${ENV}" +tmux set-environment -t "${PROJECT_NAME}" "BASH_ENV" "${ENV}" +tmux set-environment -t "${PROJECT_NAME}" "BASH_ENV_INT" "${ENV}" +tmux set-environment -t "${PROJECT_NAME}" "PROJECT_NAME" "${PROJECT_NAME}" +tmux set-environment -t "${PROJECT_NAME}" "PROJECT_DIR" "${PROJECT_DIR}" + +tmux new-window -t "${PROJECT_NAME}:1" cat +tmux kill-window -t "${PROJECT_NAME}:0" +tmux new-window -t "${PROJECT_NAME}:0" "bash -i -c \"NVIM_TUI_ENABLE_TRUE_COLOR=1 nvim -S cfg/vim\"" +tmux split-window -t "${PROJECT_NAME}:0" -c "${PROJECT_DIR}/soft/" +tmux resize-pane -t "${PROJECT_NAME}:0.0" -Z +tmux kill-window -t "${PROJECT_NAME}:1" +tmux set-option -t "${PROJECT_NAME}" allow-rename off +tmux set-window-option -t "${PROJECT_NAME}" aggressive-resize on +tmux rename-window -t "${PROJECT_NAME}:0" vim +tmux attach-session -t "${PROJECT_NAME}" + diff --git a/soft/CMakeLists.txt b/soft/CMakeLists.txt new file mode 100644 index 0000000..bbed92f --- /dev/null +++ b/soft/CMakeLists.txt @@ -0,0 +1,123 @@ +cmake_minimum_required (VERSION 3.0) +set(CMAKE_SYSTEM_NAME Generic) +set(CMAKE_SYSTEM_VERSION 1) + +project (comm_mcu) + +# specify the cross compiler +set(GCC_PREFIX "xtensa-lx106-elf") +set(CMAKE_C_COMPILER "${GCC_PREFIX}-gcc") +set(CMAKE_CXX_COMPILER "${GCC_PREFIX}-g++") + +set(CPP_FLAGS "${CPP_FLAGS} -Os -g") + +#set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wundef -Wno-unused-function -Wcast-align -Wextra -Wshadow -Wimplicit-function-declaration -Wredundant-decls -Wmissing-prototypes -Wstrict-prototypes") + +set(CPP_FLAGS "${CPP_FLAGS} -Wpointer-arith -Wundef -Werror -Wl,-EL -nostdlib -mlongcalls -finline-functions -mtext-section-literals") +set(CPP_FLAGS "${CPP_FLAGS} -D__ets__ -DICACHE_FLASH -DARDUINO=106 -DCOM_SPEED_SERIAL=115200 -DENABLE_CMD_EXECUTOR=1 -DCUST_FILE_BASE=application -DDEBUG_VERBOSE_LEVEL=2 -DDEBUG_PRINT_FILENAME_AND_LINE=0 -DDISABLE_SPIFFS=1 -DBOOT_RTC_ENABLED=1") +set(CPP_FLAGS "${CPP_FLAGS} -DRBOOT_SPIFFS_0=0x100000 -DRBOOT_SPIFFS_1=0x300000 -DRBOOT_INTEGRATION -DBOOT_BIG_FLASH -DDISABLE_SPIFFS=1 -DSPIFF_SIZE=196608") + +# disable exceptions and Run-Time Type Information +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -fno-rtti") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -felide-constructors") + +# remove unused objects +set(CPP_FLAGS "${CPP_FLAGS} -ffunction-sections -fdata-sections") + + +# CPP FLAGS +set(CMAKE_C_FLAGS "${CPP_FLAGS} ${CMAKE_C_FLAGS}") +set(CMAKE_CXX_FLAGS "${CPP_FLAGS} ${CMAKE_CXX_FLAGS}") + +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L.") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Trboot.rom0.ld -nostdlib -u Cache_Read_Enable_New -u spiffs_get_storage_config -u call_user_start -Wl,--gc-sections -Wl,-wrap,system_restart_local") +get_filename_component(EXT_PATH "../../ext/" ABSOLUTE) + +get_filename_component(PLATFORM_PATH "platform/" ABSOLUTE) +link_directories(${PLATFORM_PATH}) + +# esp-open-sdk +get_filename_component(SDK_PATH "${EXT_PATH}/esp/esp-open-sdk" ABSOLUTE) +include_directories("${SDK_PATH}/include/") +include_directories("${SDK_PATH}/sdk/include/") +link_directories("${SDK_PATH}/lib/") +link_directories("${SDK_PATH}/sdk/ld") +set(BLANK_BIN "${SDK_PATH}/sdk/bin/blank.bin") +set(ESPTOOL "${SDK_PATH}/esptool/esptool.py") +set(TLIBS ${TLIBS} wpa pp phy smartconfig net80211 crypto hal) + +# rboot +get_filename_component(RBOOT_PATH "${EXT_PATH}/esp/rboot" ABSOLUTE) +include_directories("${RBOOT_PATH}/") +include_directories("${RBOOT_PATH}/appcode/") +link_directories("${RBOOT_PATH}/main/lib") +set(RBOOT_BIN "${RBOOT_PATH}/firmware/rboot.bin") +set(TLIBS ${TLIBS} main2 rboot) + +# Sming +get_filename_component(SMING_PATH "${EXT_PATH}/esp/Sming/Sming" ABSOLUTE) +include_directories("${SMING_PATH}/include/") +include_directories("${SMING_PATH}/") +include_directories("${SMING_PATH}/third-party/esp-open-lwip/include") +include_directories("${SMING_PATH}/system/include") +include_directories("${SMING_PATH}/Wiring") +include_directories("${SMING_PATH}/Libraries") +include_directories("${SMING_PATH}/Libraries/Adafruit_GFX") +include_directories("${SMING_PATH}/SmingCore") +include_directories("${SMING_PATH}/Services/SpifFS") +include_directories("${SMING_PATH}/third-party/http-parser") +include_directories("${SMING_PATH}/third-party/spiffs/src") +link_directories("${SMING_PATH}/compiler/lib/") # Sming libraries +set(TLIBS ${TLIBS} sming microc microgcc lwip_open pwm_open) + +add_library (rboot STATIC + ${RBOOT_PATH}/appcode/rboot-api.c + ${RBOOT_PATH}/appcode/rboot-bigflash.c + ${SMING_PATH}/appspecific/rboot/overrides.c + ) + + +include_directories("src") +include_directories("src/spejsiot") +include_directories("src/spejsiot/include") +include_directories("src/common") + +add_library (app STATIC + src/application.cpp + + src/sonoff_pow.cpp + src/impuls_counter.c + + src/spejsiot/Endpoint.cpp + src/spejsiot/endpoints/OutputEndpoint.cpp + src/spejsiot/endpoints/ImplementationEndpoint.cpp + src/spejsiot/SpejsNode.cpp + src/TcpPublish.cpp + ${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/ver.c + ) + +add_executable(main.elf + ${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/empty.c) + +target_link_libraries(main.elf -Wl,--start-group app ${TLIBS} -Wl,--end-group) + +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/ver.c + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY} + COMMAND echo \"char* BUILD_ID = \\\"$$\(git rev-parse --short HEAD\)-$$\(TZ=UTC date +%Y%m%d-%H%M%S\)\\\"\;\" > ver.c + ) + +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/empty.c + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY} + COMMAND touch empty.c + ) + +add_custom_command(OUTPUT firmware/rom0.bin + COMMAND mkdir -p firmware/ + COMMAND esptool2 -quiet -bin -boot2 main.elf firmware/rom0.bin .text .data .rodata + DEPENDS main.elf) + +add_custom_target(flash + DEPENDS firmware/rom0.bin + DEPENDS ${RBOOT_BIN} + COMMAND python2 "${ESPTOOL}" -p /dev/ttyUSB0 -b 460800 write_flash -ff 40m -fm qio -fs 4MB 0x00000 ${RBOOT_BIN} 0x01000 ${BLANK_BIN} 0x02000 firmware/rom0.bin) diff --git a/soft/Makefile b/soft/Makefile new file mode 100644 index 0000000..5a48abc --- /dev/null +++ b/soft/Makefile @@ -0,0 +1,12 @@ +all: + mkdir -p build + cd build; cmake .. + cd build; make --no-print-directory + +flash: + mkdir -p build + cd build; cmake .. + @- cd build; make --no-print-directory flash + +clean: + rm -r build diff --git a/soft/platform/common.ld b/soft/platform/common.ld new file mode 100644 index 0000000..bf4f880 --- /dev/null +++ b/soft/platform/common.ld @@ -0,0 +1,242 @@ +/* + common.ld +*/ + +PHDRS +{ + dport0_0_phdr PT_LOAD; + dram0_0_phdr PT_LOAD; + dram0_0_bss_phdr PT_LOAD; + iram1_0_phdr PT_LOAD; + irom0_0_phdr PT_LOAD; +} + + +/* Default entry point: */ +ENTRY(call_user_start) +EXTERN(_DebugExceptionVector) +EXTERN(_DoubleExceptionVector) +EXTERN(_KernelExceptionVector) +EXTERN(_NMIExceptionVector) +EXTERN(_UserExceptionVector) +PROVIDE(_memmap_vecbase_reset = 0x40000000); +/* Various memory-map dependent cache attribute settings: */ +_memmap_cacheattr_wb_base = 0x00000110; +_memmap_cacheattr_wt_base = 0x00000110; +_memmap_cacheattr_bp_base = 0x00000220; +_memmap_cacheattr_unused_mask = 0xFFFFF00F; +_memmap_cacheattr_wb_trapnull = 0x2222211F; +_memmap_cacheattr_wba_trapnull = 0x2222211F; +_memmap_cacheattr_wbna_trapnull = 0x2222211F; +_memmap_cacheattr_wt_trapnull = 0x2222211F; +_memmap_cacheattr_bp_trapnull = 0x2222222F; +_memmap_cacheattr_wb_strict = 0xFFFFF11F; +_memmap_cacheattr_wt_strict = 0xFFFFF11F; +_memmap_cacheattr_bp_strict = 0xFFFFF22F; +_memmap_cacheattr_wb_allvalid = 0x22222112; +_memmap_cacheattr_wt_allvalid = 0x22222112; +_memmap_cacheattr_bp_allvalid = 0x22222222; +PROVIDE(_memmap_cacheattr_reset = _memmap_cacheattr_wb_trapnull); + +SECTIONS +{ + + .dport0.rodata : ALIGN(4) + { + _dport0_rodata_start = ABSOLUTE(.); + *(.dport0.rodata) + *(.dport.rodata) + _dport0_rodata_end = ABSOLUTE(.); + } >dport0_0_seg :dport0_0_phdr + + .dport0.literal : ALIGN(4) + { + _dport0_literal_start = ABSOLUTE(.); + *(.dport0.literal) + *(.dport.literal) + _dport0_literal_end = ABSOLUTE(.); + } >dport0_0_seg :dport0_0_phdr + + .dport0.data : ALIGN(4) + { + _dport0_data_start = ABSOLUTE(.); + *(.dport0.data) + *(.dport.data) + _dport0_data_end = ABSOLUTE(.); + } >dport0_0_seg :dport0_0_phdr + + .data : ALIGN(4) + { + _data_start = ABSOLUTE(.); + *(.data) + *(.data.*) + *(.gnu.linkonce.d.*) + *(.data1) + *(.sdata) + *(.sdata.*) + *(.gnu.linkonce.s.*) + *(.sdata2) + *(.sdata2.*) + *(.gnu.linkonce.s2.*) + *(.jcr) + _data_end = ABSOLUTE(.); + } >dram0_0_seg :dram0_0_phdr + + .rodata : ALIGN(4) + { + _rodata_start = ABSOLUTE(.); + *(.sdk.version) + *(.rodata) + *(.rodata.*) + *(.gnu.linkonce.r.*) + *(.rodata1) + __XT_EXCEPTION_TABLE__ = ABSOLUTE(.); + *(.xt_except_table) + *(.gcc_except_table) + *(.gnu.linkonce.e.*) + *(.gnu.version_r) + *(.eh_frame) + . = (. + 3) & ~ 3; + /* C++ constructor and destructor tables, properly ordered: */ + __dso_handle = ABSOLUTE(.); + __init_array_start = ABSOLUTE(.); + KEEP (*crtbegin.o(.ctors)) + KEEP (*(EXCLUDE_FILE (*crtend.o) .ctors)) + KEEP (*(SORT(.ctors.*))) + KEEP (*(.ctors)) + __init_array_end = ABSOLUTE(.); + KEEP (*crtbegin.o(.dtors)) + KEEP (*(EXCLUDE_FILE (*crtend.o) .dtors)) + KEEP (*(SORT(.dtors.*))) + KEEP (*(.dtors)) + /* C++ exception handlers table: */ + __XT_EXCEPTION_DESCS__ = ABSOLUTE(.); + *(.xt_except_desc) + *(.gnu.linkonce.h.*) + __XT_EXCEPTION_DESCS_END__ = ABSOLUTE(.); + *(.xt_except_desc_end) + *(.dynamic) + *(.gnu.version_d) + . = ALIGN(4); /* this table MUST be 4-byte aligned */ + _bss_table_start = ABSOLUTE(.); + LONG(_bss_start) + LONG(_bss_end) + _bss_table_end = ABSOLUTE(.); + _rodata_end = ABSOLUTE(.); + } >dram0_0_seg :dram0_0_phdr + + .bss ALIGN(8) (NOLOAD) : ALIGN(4) + { + . = ALIGN (8); + _bss_start = ABSOLUTE(.); + *(.dynsbss) + *(.sbss) + *(.sbss.*) + *(.gnu.linkonce.sb.*) + *(.scommon) + *(.sbss2) + *(.sbss2.*) + *(.gnu.linkonce.sb2.*) + *(.dynbss) + *(.bss) + *(.bss.*) + *(.gnu.linkonce.b.*) + *(COMMON) + . = ALIGN (8); + _bss_end = ABSOLUTE(.); + _heap_start = ABSOLUTE(.); +/* _stack_sentry = ALIGN(0x8); */ + } >dram0_0_seg :dram0_0_bss_phdr +/* __stack = 0x3ffc8000; */ + + .irom0.text : ALIGN(4) + { + _irom0_text_start = ABSOLUTE(.); + + *libsmartconfig.a:(.literal .text .literal.* .text.*) + *libstdc++.a:(.literal .text .literal.* .text.*) + *liblwip_open.a:(.literal .text .literal.* .text.*) + *liblwip_full.a:(.literal .text .literal.* .text.*) + *liblwip2.a:(.literal .text .literal.* .text.*) + *libaxtls.a:(.literal .text .literal.* .text.*) + *libat.a:(.literal.* .text.*) + *libcrypto.a:(.literal.* .text.*) + *libespnow.a:(.literal.* .text.*) + *libjson.a:(.literal.* .text.*) + *liblwip.a:(.literal.* .text.*) + *libmesh.a:(.literal.* .text.*) + *libnet80211.a:(.literal.* .text.*) + *libsmartconfig.a:(.literal.* .text.*) + *libssl.a:(.literal.* .text.*) + *libupgrade.a:(.literal.* .text.*) + *libwpa.a:(.literal.* .text.*) + *libwpa2.a:(.literal.* .text.*) + *libwps.a:(.literal.* .text.*) + + *libmbedtls.a:(.literal.* .text.*) + + *libm.a:(.literal .text .literal.* .text.*) + + *(.irom0.literal .irom.literal .irom.text.literal .irom0.text .irom.text .irom.debug.*) + *libapp.a:*(.literal .text .literal.* .text.* .stub .gnu.warning .gnu.linkonce.literal.* .gnu.linkonce.t.*.literal .gnu.linkonce.t.* .irom.debug.*) + *librboot.a:*(.literal .text .literal.* .text.* .stub .gnu.warning .gnu.linkonce.literal.* .gnu.linkonce.t.*.literal .gnu.linkonce.t.* .irom.debug.*) + *libcapnp.a:*(.literal .text .literal.* .text.* .stub .gnu.warning .gnu.linkonce.literal.* .gnu.linkonce.t.*.literal .gnu.linkonce.t.* .irom.debug.*) + *libsming.a:*(.literal .text .literal.* .text.* .stub .gnu.warning .gnu.linkonce.literal.* .gnu.linkonce.t.*.literal .gnu.linkonce.t.* .irom.debug.*) + *libsmingssl.a:*(.literal .text .literal.* .text.* .stub .gnu.warning .gnu.linkonce.literal.* .gnu.linkonce.t.*.literal .gnu.linkonce.t.* .irom.debug.*) + + _irom0_text_end = ABSOLUTE(.); + _flash_code_end = ABSOLUTE(.); + } >irom0_0_seg :irom0_0_phdr + + .text : ALIGN(4) + { + _stext = .; + _text_start = ABSOLUTE(.); + *(.UserEnter.text) + . = ALIGN(16); + *(.DebugExceptionVector.text) + . = ALIGN(16); + *(.NMIExceptionVector.text) + . = ALIGN(16); + *(.KernelExceptionVector.text) + LONG(0) + LONG(0) + LONG(0) + LONG(0) + . = ALIGN(16); + *(.UserExceptionVector.text) + LONG(0) + LONG(0) + LONG(0) + LONG(0) + . = ALIGN(16); + *(.DoubleExceptionVector.text) + LONG(0) + LONG(0) + LONG(0) + LONG(0) + . = ALIGN (16); + *(.entry.text) + *(.init.literal) + *(.init) + *(.literal .text .literal.* .text.* .stub .gnu.warning .gnu.linkonce.literal.* .gnu.linkonce.t.*.literal .gnu.linkonce.t.*) + *(.iram.literal .iram.text.literal .iram.text) + *(.fini.literal) + *(.fini) + *(.gnu.version) + _text_end = ABSOLUTE(.); + _etext = .; + } >iram1_0_seg :iram1_0_phdr + + .lit4 : ALIGN(4) + { + _lit4_start = ABSOLUTE(.); + *(*.lit4) + *(.lit4.*) + *(.gnu.linkonce.lit4.*) + _lit4_end = ABSOLUTE(.); + } >iram1_0_seg :iram1_0_phdr +} + +/* get ROM code address */ +INCLUDE "../ld/eagle.rom.addr.v6.ld" diff --git a/soft/platform/rboot.rom0.ld b/soft/platform/rboot.rom0.ld new file mode 100644 index 0000000..8f1f0ee --- /dev/null +++ b/soft/platform/rboot.rom0.ld @@ -0,0 +1,11 @@ +/* Linker Script for rboot */ + +MEMORY +{ + dport0_0_seg : org = 0x3FF00000, len = 0x10 + dram0_0_seg : org = 0x3FFE8000, len = 0x14000 + iram1_0_seg : org = 0x40100000, len = 0x8000 + irom0_0_seg : org = 0x40202010, len = (1M - 0x2010) +} + +INCLUDE "common.ld" diff --git a/soft/platform/standalone.rom.ld b/soft/platform/standalone.rom.ld new file mode 100644 index 0000000..13fdd17 --- /dev/null +++ b/soft/platform/standalone.rom.ld @@ -0,0 +1,11 @@ +/* linker script for espressif bootloader */ + +MEMORY +{ + dport0_0_seg : org = 0x3FF00000, len = 0x10 + dram0_0_seg : org = 0x3FFE8000, len = 0x14000 + iram1_0_seg : org = 0x40100000, len = 0x8000 + irom0_0_seg : org = 0x4020a000, len = (1M - 0x0a000) +} + +INCLUDE "common.ld" diff --git a/soft/src/TcpPublish.cpp b/soft/src/TcpPublish.cpp new file mode 100644 index 0000000..62fc641 --- /dev/null +++ b/soft/src/TcpPublish.cpp @@ -0,0 +1,28 @@ +#include +#include +#include + +TcpPublish::TcpPublish() : TcpServer() { +}; + +int TcpPublish::write(const char * str, int len) { + int i = 0; + for ( i=0; i < clients.count(); i++ ) { + clients[i]->write(str, len); + } + return i; +}; + +void TcpPublish::onClient(TcpClient* client) { + clients.addElement(client); +}; + +void TcpPublish::onClientComplete(TcpClient* client) { + for ( int i=0; i < clients.count(); i++ ) { + if(client == clients[i]){ + clients.remove(i); + return; + } + } +} + diff --git a/soft/src/TcpPublish.h b/soft/src/TcpPublish.h new file mode 100644 index 0000000..c8061d5 --- /dev/null +++ b/soft/src/TcpPublish.h @@ -0,0 +1,12 @@ +#include + +class TcpPublish: public TcpServer { +public: + TcpPublish(); + int write(const char * str, int len); + Vector clients; + +protected: + void onClient(TcpClient* client); + void onClientComplete(TcpClient* client); +}; diff --git a/soft/src/application.cpp b/soft/src/application.cpp new file mode 100644 index 0000000..0fe2ba9 --- /dev/null +++ b/soft/src/application.cpp @@ -0,0 +1,74 @@ +#include + +#include +#include + +#include +#include +#include +#include + +extern "C" { + #include "sonoff_pow.h" +}; + +SpejsNode node("sonoff pow"); +OutputEndpoint *relay; +TcpPublish tcpPublish; + +Hardware_Timer cseMeasureTimer; +Timer cseUpdateTimer; +Timer printTimer; +bool started; + +void startServers() { + tcpPublish.listen(123); + // TODO change to false when disconnected + started = true; +} + +void print() { + char buf[30]; + memset(buf, 0, 30); + + int len = 0; + float power_factor = 21517.217583641264; + float power = + (float)cse.power.changes * power_factor / (float)cse.power.samples; + double energy_factor = 0.001494251221086199; + double energy = (double)cse.energy * energy_factor; + len = m_snprintf(buf, 30, "%.2f W\n", power); + tcpPublish.write("power: ", 7); + tcpPublish.write(buf, len); + if ( started == true ) { + relay->notify("power", String(buf, len)); + } + + len = m_snprintf(buf, 30, "%.4f Wh\n", energy); + tcpPublish.write("energy: ", 8); + tcpPublish.write(buf, len); + if ( started == true ) { + // TODO ignore notify if not connected to mqtt broker + relay->notify("energy", String(buf, len)); + } +} + + +void init() { + Serial.begin(SERIAL_BAUD_RATE); // 115200 by default + Serial.systemDebugOutput(true); + + relay = new OutputEndpoint(RELAY_PIN); + node.registerEndpoint("relay", relay); + node.onConnectedCallback = startServers; + node.init(); + + cse_init( 4000 ); + cseMeasureTimer.initializeUs(250, cse_measure_isr); + cseMeasureTimer.start(); + cseUpdateTimer.initializeMs(100, cse_update); + cseUpdateTimer.start(); + + printTimer.initializeMs(1000, print).start(); + printTimer.start(); +} diff --git a/soft/src/common/common_config.h b/soft/src/common/common_config.h new file mode 100644 index 0000000..ecca32d --- /dev/null +++ b/soft/src/common/common_config.h @@ -0,0 +1,29 @@ +#ifndef __COMMON_CONFIG_H__ +#define __COMMON_CONFIG_H__ + +#ifndef WIFI_SSID + #define WIFI_SSID "╳╳╳" + #define WIFI_PWD "bs7RFiQpgmMycfmt" + //#define WIFI_SSID "hackerspace.pl-guests" + //#define WIFI_PWD "HackThePlanet!" +#endif + +#ifndef MQTT_BROKER + //#define MQTT_BROKER "10.8.1.16" + #define MQTT_BROKER "10.1.10.50" + #ifdef ENABLE_SSL + #define MQTT_PORT 8883 + #define SSL_FINGERPRINT { } // TODO + #else + #define MQTT_PORT 1883 + #endif +#endif + +#define TOPIC_PREFIX "iot/" + +#define OTA_URL "http://" MQTT_BROKER "/api/1/ota/" + +#define BTN_PIN 0 +#define LED_PIN 2 + +#endif diff --git a/soft/src/common/user_config.h b/soft/src/common/user_config.h new file mode 100644 index 0000000..1c6ac36 --- /dev/null +++ b/soft/src/common/user_config.h @@ -0,0 +1,45 @@ +#ifndef __USER_CONFIG_H__ +#define __USER_CONFIG_H__ + +#ifdef __cplusplus +extern "C" { +#endif + + // UART config + #define SERIAL_BAUD_RATE 115200 + + // ESP SDK config + #define LWIP_OPEN_SRC + #define USE_US_TIMER + + // Default types + #define __CORRECT_ISO_CPP_STDLIB_H_PROTO + #include + #include + + // Override c_types.h include and remove buggy espconn + #define _C_TYPES_H_ + #define _NO_ESPCON_ + + // Updated, compatible version of c_types.h + // Just removed types declared in + #include + + // System API declarations + #include + + // C++ Support + #include + // Extended string conversion for compatibility + #include + // Network base API + #include + + // Beta boards + #define BOARD_ESP01 + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/soft/src/freq_counter.c b/soft/src/freq_counter.c new file mode 100644 index 0000000..0351ea1 --- /dev/null +++ b/soft/src/freq_counter.c @@ -0,0 +1,43 @@ +#include + +void freq_counter_init ( struct freq_counter *f, uint32_t cycle_len ) { + f->input_initialized = false; + f->input_state = false; + + f->counter = 0; + f->samples = 0; + f->cycle_len = cycle_len; + + f->new_measurement = false; + f->measurement = 0; +} + +void IRAM_ATTR freq_counter_update ( struct freq_counter *f, bool input ) { + if ( f->input_initialized == false ) { + // ignore first input value + f->input_state = input; + f->input_initialized = true; + } + if ( f->input_state != input) { + f->counter += 1; + f->input_state = input; + } + f->samples += 1; + if ( f->samples >= f->cycle_len ) { + /* cycle complete */ + f->measurement = f->counter; + f->new_measurement = true; + f->samples = 0; + f->counter = 0; + } +} + +bool freq_counter_get_measurement( struct freq_counter *f, uint32_t *measurement) { + if ( f->new_measurement == true ) { + *measurement = f->measurement; + f->new_measurement = false; + return true; + } else { + return false; + } +} diff --git a/soft/src/freq_counter.h b/soft/src/freq_counter.h new file mode 100644 index 0000000..f0b1473 --- /dev/null +++ b/soft/src/freq_counter.h @@ -0,0 +1,42 @@ +#include +#include + +#ifndef FREQ_COUNTER_H +#define FREQ_COUNTER_H + +#ifndef IRAM_ATTR + #define IRAM_ATTR __attribute__((section(".iram.text"))) +#endif + +struct freq_counter { + bool input_initialized; /* false before first update call */ + bool input_state; /* last update input state */ + + uint32_t counter; /* input state changes counter */ + uint32_t samples; /* input samples counter */ + uint32_t cycle_len; /* number of samples in one cycle */ + + bool new_measurement; /* set to true after updating measurement */ + uint32_t measurement; /* number of state changes in last complete cycle */ +}; + +/** + * @brief initialize frequency counter structure + * + * @param cycle_len number of samples in one measurement cycle + */ +void freq_counter_init ( struct freq_counter *f, uint32_t cycle_len ); +void IRAM_ATTR freq_counter_update ( struct freq_counter *f, bool input ); + + +/** + * @brief get new measurement from freq_counter + * + * this function should be called at least twice per cycle in order to avoid + * concurrent rw acces to measurement variable + * @param measurement is set to new measurement value if returned true + * @return true if new measurement is available + */ +bool freq_counter_get_measurement( struct freq_counter *f, uint32_t *measurement); + +#endif /* end of include guard: FREQ_COUNTER_H */ diff --git a/soft/src/impuls_counter.c b/soft/src/impuls_counter.c new file mode 100644 index 0000000..4bde701 --- /dev/null +++ b/soft/src/impuls_counter.c @@ -0,0 +1,51 @@ +#include + +void impuls_counter_init ( struct impuls_counter *f, uint32_t cycle_len ) { + f->input_initialized = false; + f->input_state = false; + + f->counter = 0; + f->samples = 0; + f->cycle_len = cycle_len; + + f->new_measurement = false; + f->measurement.samples = 0; + f->measurement.changes = 0; +} + +void IRAM_ATTR impuls_counter_update ( struct impuls_counter *f, bool input ) { + if ( f->input_initialized == false ) { + // ignore first input value + f->input_state = input; + f->input_initialized = true; + } + + bool changed = false; + f->samples += 1; + if ( f->input_state != input) { + changed = true; + f->counter += 1; + f->input_state = input; + } + + if ( f->samples >= f->cycle_len && (changed || f->counter == 0 \ + || f->samples >= 2 * f->cycle_len)) { + /* cycle complete */ + f->measurement.changes = f->counter; + f->measurement.samples = f->samples; + f->new_measurement = true; + f->samples = 0; + f->counter = 0; + } +} + +bool impuls_counter_get_measurement( struct impuls_counter *f, + struct impuls_measurement *measurement) { + if ( f->new_measurement == true ) { + *measurement = f->measurement; + f->new_measurement = false; + return true; + } else { + return false; + } +} diff --git a/soft/src/impuls_counter.h b/soft/src/impuls_counter.h new file mode 100644 index 0000000..82a0121 --- /dev/null +++ b/soft/src/impuls_counter.h @@ -0,0 +1,48 @@ +#include +#include + +#ifndef IMPULS_COUNTER_H +#define IMPULS_COUNTER_H + +#ifndef IRAM_ATTR + #define IRAM_ATTR __attribute__((section(".iram.text"))) +#endif + +struct impuls_measurement { + uint32_t samples; /* number of samples */ + uint32_t changes; /* input state changes */ +}; + +struct impuls_counter { + bool input_initialized; /* false before first update call */ + bool input_state; /* last update input state */ + + uint32_t counter; /* input state changes counter */ + uint32_t samples; /* input samples counter */ + uint32_t cycle_len; /* minimum number of samples in one cycle */ + + bool new_measurement; /* set to true after updating measurement */ + struct impuls_measurement measurement; /* one measurement */ +}; + +/** + * @brief initialize impuls counter structure + * + * @param cycle_len number of samples in one measurement cycle + */ +void impuls_counter_init ( struct impuls_counter *f, uint32_t cycle_len ); + +void IRAM_ATTR impuls_counter_update ( struct impuls_counter *f, bool input ); + +/** + * @brief get new measurement from impuls_counter + * + * this function should be called at least twice per cycle in order to avoid + * concurrent rw acces to measurement variable + * @param measurement is set to new measurement value if returned true + * @return true if new measurement is available + */ +bool impuls_counter_get_measurement( struct impuls_counter *f, + struct impuls_measurement *measurement); + +#endif /* end of include guard: IMPULS_COUNTER_H */ diff --git a/soft/src/sonoff_pow.cpp b/soft/src/sonoff_pow.cpp new file mode 100644 index 0000000..6c0657d --- /dev/null +++ b/soft/src/sonoff_pow.cpp @@ -0,0 +1,45 @@ +#include + + +extern "C" { + #include +} + +struct cse cse; + +static struct impuls_counter power_counter; +static struct impuls_counter current_counter; +static struct impuls_counter voltage_counter; + +void IRAM_ATTR cse_measure_isr ( void ) { + bool cf, cf1; + cf = digitalRead( CF_PIN ); + cf1 = digitalRead( CF1_PIN ); + impuls_counter_update( &power_counter, cf ); + impuls_counter_update( ¤t_counter, cf1 ); +} + +void cse_init( uint32_t cycle_len ) { + pinMode(CF1_PIN, INPUT); + pinMode(CF_PIN, INPUT); + pinMode(SEL_PIN, OUTPUT); + impuls_counter_init( &power_counter, cycle_len); + impuls_counter_init( ¤t_counter , cycle_len); + + cse.power.samples = 0; + cse.power.changes = 0; + cse.current.samples = 0; + cse.current.changes = 0; + cse.voltage.samples = 0; + cse.voltage.changes = 0; + cse.energy = 0; +} + +void cse_update() { + bool pm, cm; + pm = impuls_counter_get_measurement( &power_counter, &cse.power ); + cm = impuls_counter_get_measurement( ¤t_counter, &cse.current ); + if ( pm == true ) { + cse.energy += cse.power.changes; + } +} diff --git a/soft/src/sonoff_pow.h b/soft/src/sonoff_pow.h new file mode 100644 index 0000000..948402d --- /dev/null +++ b/soft/src/sonoff_pow.h @@ -0,0 +1,28 @@ +#include + +#ifndef SONOFF_POW_H +#define SONOFF_POW_H + +#ifndef IRAM_ATTR + #define IRAM_ATTR __attribute__((section(".iram.text"))) +#endif + +#define CF_PIN 14 +#define CF1_PIN 13 +#define SEL_PIN 5 +#define RELAY_PIN 12 + +struct cse { + struct impuls_measurement power; + struct impuls_measurement current; + struct impuls_measurement voltage; + uint64_t energy; +}; + +extern struct cse cse; + +void IRAM_ATTR cse_measure_isr ( void ); +void cse_init( uint32_t cycle_len ); +void cse_update(); + +#endif /* end of include guard: SONOFF_POW_H */ diff --git a/soft/src/spejsiot/Endpoint.cpp b/soft/src/spejsiot/Endpoint.cpp new file mode 100644 index 0000000..2212992 --- /dev/null +++ b/soft/src/spejsiot/Endpoint.cpp @@ -0,0 +1,33 @@ +#include +#include + +void Endpoint::bind(String _name, SpejsNode* _parent) { + parent = _parent; + name = _name; +} + +void Endpoint::notify(String property, String value) { + if(parent) + parent->notify(name + "/" + property, value); +} + +void Endpoint::onConnected() { + parent->subscribe(name + "/+/set"); + parent->notify(name + "/$type", type); +} + +void Endpoint::onMessage(String topic, String payload) { + String devicePrefix = parent->DEV_TOPIC(""); + + if(!topic.startsWith(devicePrefix)) { + return; + } + + int propPos = topic.indexOf("/", devicePrefix.length()); + String endpoint = topic.substring(devicePrefix.length(), propPos); + String property = topic.substring(propPos+1, topic.indexOf("/", propPos+1)); + + if(name.equals(endpoint)) { + debugf("%s - %s response: %d\n", endpoint.c_str(), property.c_str(), onValue(property, payload).status); + } +} diff --git a/soft/src/spejsiot/Endpoint.h b/soft/src/spejsiot/Endpoint.h new file mode 100644 index 0000000..d145f2d --- /dev/null +++ b/soft/src/spejsiot/Endpoint.h @@ -0,0 +1,54 @@ +#ifndef ENDPOINT_H +#define ENDPOINT_H + +#include + +class SpejsNode; + +class EndpointResult { +public: + int status; + String description; + + EndpointResult(int _status) : status(_status) {} + EndpointResult(int _status, String _description) : + status(_status), description(_description) {} +}; + +class Endpoint { +protected: + SpejsNode* parent; + String name; + +public: + String type; + + Endpoint(String _type = "unknown") : type(_type) { } + + virtual void bind(String _name, SpejsNode* _parent); + void notify(String property, String value); + + virtual void onMessage(String topic, String payload); + virtual EndpointResult onValue(String property, String value) { + return 400; + } + + virtual void onConnected(); + + static Endpoint* fromJSON(JsonObject& obj) { + return NULL; + } +}; + +template class ValueEndpoint : public Endpoint { +protected: + T value; + void updateValue(T newValue); + +public: + ValueEndpoint(String _type) : Endpoint(_type) {} +}; + +typedef Endpoint* (*EndpointInitializer) (JsonObject&); + +#endif diff --git a/soft/src/spejsiot/README b/soft/src/spejsiot/README new file mode 100644 index 0000000..06711a3 --- /dev/null +++ b/soft/src/spejsiot/README @@ -0,0 +1,2 @@ +spejsiot provided by: +http://wiki.hackerspace.pl/projects:spejsiot diff --git a/soft/src/spejsiot/SpejsNode.cpp b/soft/src/spejsiot/SpejsNode.cpp new file mode 100644 index 0000000..aac941e --- /dev/null +++ b/soft/src/spejsiot/SpejsNode.cpp @@ -0,0 +1,221 @@ +#include +#include +#include + +#include + +#define CONFIG_FILE "config.json" + +void SpejsNode::init() { + deviceID = WifiStation.getMAC().substring(6, 12); + + currentSlot = 0; + if(!rboot_get_last_boot_rom(¤tSlot)) { + currentSlot = rboot_get_current_rom(); + } + + Serial.begin(115200); + Serial.systemDebugOutput(false); // Debug output to serial + + debugf("*** SpejsNode init, runnning on: %s, current rom: %d", deviceID.c_str(), currentSlot); + + WifiAccessPoint.enable(false); + WifiStation.enable(true); + WifiStation.config(WIFI_SSID, WIFI_PWD); + + WifiEvents.onStationGotIP(StationGotIPDelegate(&SpejsNode::gotIP, this)); + + //registerEndpoint("$implementation", new ImplementationEndpoint()); + + // Keepalive Timer initialization + keepaliveTimer.initializeMs(10000, TimerDelegate(&SpejsNode::keepAliveHandler, this)).start(); + + statusLED.high(); +} + +void SpejsNode::loadJSON(std::vector initializers) { +#ifdef RBOOT_SPIFFS_0 + debugf("trying to mount spiffs at 0x%08x, length %d", RBOOT_SPIFFS_0, SPIFF_SIZE); + spiffs_mount_manual(RBOOT_SPIFFS_0, SPIFF_SIZE); +#else + debugf("trying to mount spiffs at 0x%08x, length %d", 0x100000, SPIFF_SIZE); + spiffs_mount_manual(0x100000, SPIFF_SIZE); +#endif + + DynamicJsonBuffer jsonBuffer; + if (fileExist(CONFIG_FILE)) { + int size = fileGetSize(CONFIG_FILE); + debugf("Found config file, %d bytes", size); + char* jsonString = new char[size + 1]; + fileGetContent(CONFIG_FILE, jsonString, size + 1); + JsonObject& root = jsonBuffer.parseObject(jsonString); + + if (root.containsKey("name")) + deviceType = (root["name"]).asString(); + + JsonObject& data = root["endpoints"].asObject(); + for (auto it: data) { + bool found = false; + + for(auto init: initializers) { + Endpoint* ep = init(it.value); + if (ep != NULL) { + debugf("%s: got object", it.key); + registerEndpoint(it.key, ep); + found = true; + break; + } + } + + if (!found) { + debugf("%s: nothing found", it.key); + } + } + } else { + debugf("No configuration"); + } +} + +void SpejsNode::keepAliveHandler() { + static int failureCounter = 0; + if(!WifiStation.isConnected()) { + statusLED.high(); + debugf("keepalive: Network reconnect"); + if(failureCounter++ < 5) + WifiStation.connect(); + else + System.restart(); + } else if(mqtt.getConnectionState() != eTCS_Connected) { + statusLED.high(); + debugf("keepalive: MQTT reconnect"); + if(failureCounter++ < 5) + onConnected(); + else + System.restart(); + } else { + statusLED.idle(); + failureCounter = 0; + + uint8_t mode; + if(rboot_get_last_boot_mode(&mode) && mode == MODE_TEMP_ROM) { + rboot_set_current_rom(currentSlot); + debugf("Successfuly connected, accepting temp rom"); + } + } +} + +inline String SpejsNode::DEV_TOPIC(String t) { + return TOPIC_PREFIX + deviceID + "/" + t; +} + +void SpejsNode::httpIndex(HttpRequest &request, HttpResponse &response) +{ + response.sendString("This is spejsiot device, take a look at: https://wiki.hackerspace.pl/projects:spejsiot\n" + "\nDevice type: " + deviceType + + "\nFirmware version: " + String(BUILD_ID) + + "\nMAC: " + WifiStation.getMAC()); +} + +/* + * Successful network connection handler + */ +void SpejsNode::gotIP(IPAddress ip, IPAddress netmask, IPAddress gateway) { + onConnected(); +} + +void SpejsNode::onConnected() { + statusLED.idle(); + + debugf("Connection successful"); + + // MQTT initialization + mqtt.setWill(DEV_TOPIC("$online"), "false", 1, true); + +#ifdef ENABLE_SSL + const uint8_t sha1Fingerprint[] = SSL_FINGERPRINT; + mqtt.connect("iot-" + deviceID, "", "", true); + mqtt.addSslOptions(SSL_SERVER_VERIFY_LATER); + mqtt.setSslFingerprint(sha1Fingerprint, 20); +#else + mqtt.connect("iot-" + deviceID); +#endif + + for(unsigned int i = 0 ; i < endpoints.count() ; i++) { + endpoints.valueAt(i)->onConnected(); + } + if ( onConnectedCallback != NULL ) { + (*onConnectedCallback)(); + } + + subscribe("$implementation/+"); + + // Say hello + notify("$online", "true"); + notify("$homie", "2"); + notify("$name", deviceType); + notify("$localip", WifiStation.getIP().toString()); + notify("$mac", WifiStation.getMAC()); + notify("$fw/name", "spejsiot"); + notify("$fw/version", BUILD_ID); + notify("$implementation/slot", String(currentSlot)); + + // HTTP initialization + http.listen(80); + http.addPath("/", HttpPathDelegate(&SpejsNode::httpIndex, this)); + http.setDefaultHandler(HttpPathDelegate(&SpejsNode::httpFile, this)); + +} + +void SpejsNode::httpFile(HttpRequest &request, HttpResponse &response) +{ + String file = request.getPath(); + + if (file[0] == '/') + file = file.substring(1); + + if (file.startsWith("api/1/")) { + String req = file.substring(6); + String key = req.substring(0, req.indexOf("/")); + String value = req.substring(req.indexOf("/") + 1); + + if(key.length() == 0 || value.length() == 0 || !endpoints.contains(key)) { + response.code = 400; + } else { + EndpointResult result = endpoints[key]->onValue(key, value); + JsonObjectStream* stream = new JsonObjectStream(); + JsonObject& json = stream->getRoot(); + json["status"] = result.status; + response.sendJsonObject(stream); + } + } +} + +/* + * Publish on device-specific topic + */ +bool SpejsNode::notify(String key, String value) { + mqtt.publish(DEV_TOPIC(key), value, true); + return mqtt.getConnectionState() == eTCS_Connected; +} + +/* + * Subsribe to device-specific topic + */ +bool SpejsNode::subscribe(String topic) { + mqtt.subscribe(DEV_TOPIC(topic)); + return mqtt.getConnectionState() == eTCS_Connected; +} + +/* + * Register new endpoint + */ +void SpejsNode::registerEndpoint(String key, Endpoint* endpoint) { + endpoints[key] = endpoint; + endpoint->bind(key, this); +} + +void SpejsNode::mqttCallback(String origtopic, String value) { + for(unsigned int i = 0 ; i < endpoints.count() ; i++) { + endpoints.valueAt(i)->onMessage(origtopic, value); + } +} diff --git a/soft/src/spejsiot/SpejsNode.h b/soft/src/spejsiot/SpejsNode.h new file mode 100644 index 0000000..0d7193a --- /dev/null +++ b/soft/src/spejsiot/SpejsNode.h @@ -0,0 +1,104 @@ +#ifndef SPEJSNODE_H +#define SPEJSNODE_H + +#include +#include +#include +#include + +#include + +#define D0 16 +#define D1 5 +#define D2 4 +#define D3 0 +#define D4 2 +#define D5 14 +#define D6 12 +#define D7 13 +#define D8 15 + +class LED { + int highState = HIGH; + int pin = 2; + Timer animateTimer; + + void animate() { + if (millis() % blinkRate > blinkOn) { + digitalWrite(pin, !highState); + } else { + digitalWrite(pin, highState); + } + } + +public: + int blinkRate = 4000; + int blinkOn = 100; + + LED() { } + + void config(int pin_, bool highState_ = HIGH) { + pin = pin_; + highState = highState_; + + pinMode(pin, OUTPUT); + animateTimer.initializeMs(50, TimerDelegate(&LED::animate, this)).start(); + } + + void idle() { + blinkRate = 4000; + } + + void high() { + blinkRate = 300; + } +}; + +class SpejsNode { +protected: + Timer keepaliveTimer; + + HashMap endpoints; + + void keepAliveHandler(); + void initializeMDNS(); + + void buildMetadata(JsonObjectStream* stream); + + void mqttCallback(String, String); + void otaUpdateCallback(bool result); + void httpFile(HttpRequest &request, HttpResponse &response); + void httpIndex(HttpRequest &request, HttpResponse &response); +public: + MqttClient mqtt; + HttpServer http; + + String deviceID; + String deviceType; + + LED statusLED; + + void (*onConnectedCallback)(void); + + uint8_t currentSlot; + + SpejsNode(String _deviceType) : + mqtt(MQTT_BROKER, MQTT_PORT, MqttStringSubscriptionCallback(&SpejsNode::mqttCallback, this)), + deviceType(_deviceType), onConnectedCallback(NULL) {}; + + void onConnected(); + void gotIP(IPAddress ip, IPAddress netmask, IPAddress gateway); + + void init(); + + bool notify(String key, String value); + bool subscribe(String topic); + + void registerEndpoint(String key, Endpoint* cb); + + String DEV_TOPIC(String t); + + void loadJSON(std::vector); +}; + +#endif diff --git a/soft/src/spejsiot/endpoints/ImplementationEndpoint.cpp b/soft/src/spejsiot/endpoints/ImplementationEndpoint.cpp new file mode 100644 index 0000000..4723248 --- /dev/null +++ b/soft/src/spejsiot/endpoints/ImplementationEndpoint.cpp @@ -0,0 +1,78 @@ +#include +#include + +EndpointResult ImplementationEndpoint::onValue(String property, String value) { + if (property == "ota" and value == "true") { + startOTA(); + return 200; + } + return 400; +} + +void ImplementationEndpoint::startOTA() { + uint8_t slot; + rboot_config bootconf; + +#ifdef RBOOT_TWO_ROMS + String romURL = OTA_URL + parent->deviceID + (parent->currentSlot ? "/rom1.bin" : "/rom0.bin"); +#else + String romURL = OTA_URL + parent->deviceID + "/rom0.bin"; +#endif + String spiffsURL = OTA_URL + parent->deviceID + "/spiff_rom.bin"; + + debugf("Updating..."); + + // need a clean object, otherwise if run before and failed will not run again + if (otaUpdater) delete otaUpdater; + otaUpdater = new rBootHttpUpdate(); + + bootconf = rboot_get_config(); + + slot = !parent->currentSlot; + + debugf("Updating to rom %d.", slot); + + // flash rom to position indicated in the rBoot config rom table + otaUpdater->addItem(bootconf.roms[slot], romURL); + +#ifndef DISABLE_SPIFFS + // use user supplied values (defaults for 4mb flash in makefile) + if (slot == 0) { + otaUpdater->addItem(RBOOT_SPIFFS_0, spiffsURL); + } else { + otaUpdater->addItem(RBOOT_SPIFFS_1, spiffsURL); + } +#endif + + otaUpdater->setCallback(OtaUpdateDelegate(&ImplementationEndpoint::otaUpdateCallback, this)); + otaUpdater->start(); + + notify("ota", "started"); + + parent->statusLED.high(); +} + +void ImplementationEndpoint::otaUpdateCallback(rBootHttpUpdate& updater, bool result) { + parent->statusLED.idle(); + + if(result == true) { + // success + notify("ota", "finished"); + + uint8 slot; + + if (parent->currentSlot == 0) + slot = 1; + else + slot = 0; + + // set to boot new rom and then reboot + debugf("Firmware updated, rebooting to rom %d...", slot); + + rboot_set_temp_rom(slot); + + System.restart(); + } else { + notify("ota", "failed"); + } +} diff --git a/soft/src/spejsiot/endpoints/ImplementationEndpoint.h b/soft/src/spejsiot/endpoints/ImplementationEndpoint.h new file mode 100644 index 0000000..55de752 --- /dev/null +++ b/soft/src/spejsiot/endpoints/ImplementationEndpoint.h @@ -0,0 +1,19 @@ +#ifndef IMPLEMENTATIONENDPOINT_H +#define IMPLEMENTATIONENDPOINT_H + +#include +#include + +class ImplementationEndpoint : public Endpoint { +protected: + rBootHttpUpdate* otaUpdater = 0; + +public: + // Use empty endpoint type to just keep it unpublished + ImplementationEndpoint() : Endpoint("") {} + + EndpointResult onValue(String property, String value); + void otaUpdateCallback(rBootHttpUpdate& updater, bool result); + void startOTA(); +}; +#endif diff --git a/soft/src/spejsiot/endpoints/OutputEndpoint.cpp b/soft/src/spejsiot/endpoints/OutputEndpoint.cpp new file mode 100644 index 0000000..72fdcec --- /dev/null +++ b/soft/src/spejsiot/endpoints/OutputEndpoint.cpp @@ -0,0 +1,20 @@ +#include +#include + +EndpointResult OutputEndpoint::onValue(String property, String value) { + if (value == "1" or value == "on" or value == "true") { + currentValue = 1; + } else if (value == "0" or value == "off" or value == "false") { + currentValue = 0; + } else { + return 400; + } + + if (currentValue != (parent->statusLED.blinkOn > parent->statusLED.blinkRate/2)) { + parent->statusLED.blinkOn = parent->statusLED.blinkRate - parent->statusLED.blinkOn; + } + + digitalWrite(pin, inverted ^ currentValue); + notify("on", currentValue ? "true" : "false"); + return 200; +} diff --git a/soft/src/spejsiot/endpoints/OutputEndpoint.h b/soft/src/spejsiot/endpoints/OutputEndpoint.h new file mode 100644 index 0000000..78b7ddc --- /dev/null +++ b/soft/src/spejsiot/endpoints/OutputEndpoint.h @@ -0,0 +1,30 @@ +#ifndef OUTPUTENDPOINT_H +#define OUTPUTENDPOINT_H value + +#include + +class OutputEndpoint : public Endpoint { +private: + int pin; + bool inverted; + bool currentValue; + +public: + OutputEndpoint(int _pin, bool _inverted = false) : Endpoint("output"), + pin(_pin), inverted(_inverted), currentValue(inverted) { + pinMode(pin, OUTPUT); + digitalWrite(pin, currentValue); + } + + EndpointResult onValue(String property, String value); + + static Endpoint* fromJSON(JsonObject& obj) { + if (String(obj["type"].asString()) == "output" && obj.containsKey("gpio")) { + return new OutputEndpoint(obj["gpio"], obj["inverted"]); + } + + return NULL; + } +}; + +#endif /* ifndef OUTPUTENDPOINT_H */ diff --git a/soft/src/spejsiot/ver.h b/soft/src/spejsiot/ver.h new file mode 100644 index 0000000..bf32d9f --- /dev/null +++ b/soft/src/spejsiot/ver.h @@ -0,0 +1 @@ +extern char* BUILD_ID;