From 1ec0a5e33683b3dd3fd978f8ba82f59866bb7c1a Mon Sep 17 00:00:00 2001 From: wenbo13579 Date: Sat, 24 Feb 2024 10:39:51 +0800 Subject: [PATCH] first commit --- .clang-format | 101 +++++ .gitignore | 1 + .vscode/c_cpp_properties.json | 21 + .vscode/launch.json | 19 + .vscode/settings.json | 11 + .vscode/tasks.json | 84 ++++ LICENSE | 202 +++++++++ Makefile | 201 +++++++++ README.md | 751 +++++++++++++++++++++++++++++++++ README.vsdx | Bin 0 -> 50488 bytes build.mk | 5 + code_format.py | 27 ++ ebtn/bit_array.h | 597 +++++++++++++++++++++++++++ ebtn/ebtn.c | 550 +++++++++++++++++++++++++ ebtn/ebtn.h | 463 +++++++++++++++++++++ example_test.c | 755 ++++++++++++++++++++++++++++++++++ example_user.c | 290 +++++++++++++ main.c | 14 + 18 files changed, 4092 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 README.vsdx create mode 100644 build.mk create mode 100644 code_format.py create mode 100644 ebtn/bit_array.h create mode 100644 ebtn/ebtn.c create mode 100644 ebtn/ebtn.h create mode 100644 example_test.c create mode 100644 example_user.c create mode 100644 main.c diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ae691d2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Note: The list of ForEachMacros can be obtained using: +# +# git grep -h '^#define [^[:space:]]*FOR_EACH[^[:space:]]*(' include/ \ +# | sed "s,^#define \([^[:space:]]*FOR_EACH[^[:space:]]*\)(.*$, - '\1'," \ +# | sort | uniq +# +# References: +# - https://clang.llvm.org/docs/ClangFormatStyleOptions.html + +--- +BasedOnStyle: LLVM +AlignConsecutiveMacros: AcrossComments +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AttributeMacros: + - __aligned + - __deprecated + - __packed + - __printf_like + - __syscall + - __subsystem +ColumnLimit: 160 +ConstructorInitializerIndentWidth: 8 +ContinuationIndentWidth: 8 +ForEachMacros: + - 'FOR_EACH' + - 'FOR_EACH_FIXED_ARG' + - 'FOR_EACH_IDX' + - 'FOR_EACH_IDX_FIXED_ARG' + - 'FOR_EACH_NONEMPTY_TERM' + - 'RB_FOR_EACH' + - 'RB_FOR_EACH_CONTAINER' + - 'SYS_DLIST_FOR_EACH_CONTAINER' + - 'SYS_DLIST_FOR_EACH_CONTAINER_SAFE' + - 'SYS_DLIST_FOR_EACH_NODE' + - 'SYS_DLIST_FOR_EACH_NODE_SAFE' + - 'SYS_SFLIST_FOR_EACH_CONTAINER' + - 'SYS_SFLIST_FOR_EACH_CONTAINER_SAFE' + - 'SYS_SFLIST_FOR_EACH_NODE' + - 'SYS_SFLIST_FOR_EACH_NODE_SAFE' + - 'SYS_SLIST_FOR_EACH_CONTAINER' + - 'SYS_SLIST_FOR_EACH_CONTAINER_SAFE' + - 'SYS_SLIST_FOR_EACH_NODE' + - 'SYS_SLIST_FOR_EACH_NODE_SAFE' + - '_WAIT_Q_FOR_EACH' + - 'Z_FOR_EACH' + - 'Z_FOR_EACH_ENGINE' + - 'Z_FOR_EACH_EXEC' + - 'Z_FOR_EACH_FIXED_ARG' + - 'Z_FOR_EACH_FIXED_ARG_EXEC' + - 'Z_FOR_EACH_IDX' + - 'Z_FOR_EACH_IDX_EXEC' + - 'Z_FOR_EACH_IDX_FIXED_ARG' + - 'Z_FOR_EACH_IDX_FIXED_ARG_EXEC' + - 'Z_GENLIST_FOR_EACH_CONTAINER' + - 'Z_GENLIST_FOR_EACH_CONTAINER_SAFE' + - 'Z_GENLIST_FOR_EACH_NODE' + - 'Z_GENLIST_FOR_EACH_NODE_SAFE' +SortIncludes: Never +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<(assert|complex|ctype|errno|fenv|float|inttypes|limits|locale|math|setjmp|signal|stdarg|stdbool|stddef|stdint|stdio|stdlib|string|tgmath|time|wchar|wctype)\.h>$' + Priority: 0 + - Regex: '.*' + Priority: 1 +IndentCaseLabels: false +IndentWidth: 4 +# SpaceBeforeParens: ControlStatementsExceptControlMacros # clang-format >= 13.0 +UseTab: Never +WhitespaceSensitiveMacros: + - STRINGIFY + - Z_STRINGIFY + +AlignAfterOpenBracket: Align +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterCaseLabel: true + AfterExternBlock: false # Fix "extern "C"" + BeforeCatch: true + BeforeElse: true + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBraces: Custom # Fix "extern "C"" +IndentExternBlock: AfterExternBlock # Not useful for "extern "C"" + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f57b97 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/output diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..9f46445 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,21 @@ +{ + "configurations": [ + { + "name": "Win32", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [ + "_DEBUG", + "UNICODE", + "_UNICODE" + ], + "compilerPath": "C:\\msys64\\mingw64\\bin\\gcc.exe", + "cStandard": "c99", + "cppStandard": "gnu++03", + "intelliSenseMode": "windows-gcc-x64", + "configurationProvider": "ms-vscode.makefile-tools" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ef2bc56 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug", + "type": "cppdbg", + "request": "launch", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "miDebuggerPath": "gdb.exe", + "program": "${workspaceFolder}/output/main.exe", + "preLaunchTask": "build" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b9e1699 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.associations": { + "bare_heap.h": "c", + "bare_task.h": "c", + "bare_list.h": "c", + "stddef.h": "c", + "bare_common.h": "c", + "stdlib.h": "c", + "windows.h": "c" + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f12924e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,84 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "group": { + "kind": "build", + "isDefault": true + }, + "windows": { + "command": "powershell", + "args": [ + "-c", + "make" + ] + }, + "linux": { + "command": "bash", + "args": [ + "-c", + "make" + ] + }, + "osx": { + "command": "bash", + "args": [ + "-c", + "make" + ] + } + }, + { + "label": "build & run", + "type": "shell", + "windows": { + "command": "powershell", + "args": [ + "-c", + "make run'" + ] + }, + "linux": { + "command": "bash", + "args": [ + "-c", + "'make run'" + ] + }, + "osx": { + "command": "bash", + "args": [ + "-c", + "'make run'" + ] + } + }, + { + "label": "clean", + "type": "shell", + "windows": { + "command": "powershell", + "args": [ + "-c", + "'make clean'" + ] + }, + "linux": { + "command": "bash", + "args": [ + "-c", + "'make clean'" + ] + }, + "osx": { + "command": "bash", + "args": [ + "-c", + "'make clean'" + ] + } + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..40b5787 --- /dev/null +++ b/Makefile @@ -0,0 +1,201 @@ +# +# 'make' build executable file 'main' +# 'make clean' removes all .o and executable files +# +.DEFAULT_GOAL := all + +# CROSS_COMPILE ?= arm-linux-gnueabihf- +CROSS_COMPILE ?= +TARGET ?= main + +# define the C compiler to use +CC := $(CROSS_COMPILE)gcc +LD := $(CROSS_COMPILE)ld +OBJCOPY := $(CROSS_COMPILE)objcopy +OBJDUMP := $(CROSS_COMPILE)objdump +SIZE := $(CROSS_COMPILE)size + +# define any compile-time flags +CFLAGS ?= +CFLAGS += -O0 +CFLAGS += -g + +# warning param setting +CFLAGS += -Wall +# think use -Os. +CFLAGS += -Wno-unused-function +CFLAGS += -Wno-unused-variable + +CFLAGS += -Wstrict-prototypes +CFLAGS += -Wshadow +# CFLAGS += -Werror + +# spec c version +CFLAGS += -std=c99 +# CFLAGS += -Wno-format + +# for makefile depend tree create +CFLAGS += -MMD -MP + +# for weak. +CFLAGS += -fno-common + +## MAKEFILE COMPILE MESSAGE CONTROL ## +ifeq ($(V),1) + Q= +else + Q=@ +endif + +# Put functions and data in their own binary sections so that ld can +# garbage collect them +ifeq ($(NOGC),1) +GC_CFLAGS = +GC_LDFLAGS = +else +GC_CFLAGS = -ffunction-sections -fdata-sections +GC_LDFLAGS = -Wl,--gc-sections -Wl,--check-sections +endif + + +CFLAGS += $(GC_CFLAGS) + +# define ld params +LDFLAGS := +LDFLAGS += $(GC_LDFLAGS) + + +# define library paths in addition to /usr/lib +# if I wanted to include libraries not in /usr/lib I'd specify +# their path using -Lpath, something like: +LFLAGS := + +# define output directory +OUTPUT_PATH := output + +# define source directory +SRC := + +# define include directory +INCLUDE := + +# define lib directory +LIB := + + +OUTPUT_TARGET := $(OUTPUT_PATH)/$(TARGET) + +OBJDIR = $(OUTPUT_PATH)/obj + + +# include user build.mk +include build.mk + + +ifeq ($(OS),Windows_NT) +ifdef ComSpec + WINCMD:=$(ComSpec) +endif +ifdef COMSPEC + WINCMD:=$(COMSPEC) +endif + +SHELL:=$(WINCMD) + +MAIN := $(TARGET).exe +ECHO=echo +SOURCEDIRS := $(SRC) +INCLUDEDIRS := $(INCLUDE) +LIBDIRS := $(LIB) +FIXPATH = $(subst /,\,$1) +RM := del /q /s +MD := mkdir +else +MAIN := $(TARGET) +ECHO=echo +SOURCEDIRS := $(shell find $(SRC) -type d) +INCLUDEDIRS := $(shell find $(INCLUDE) -type d) +LIBDIRS := $(shell find $(LIB) -type d) +FIXPATH = $1 +RM = rm -rf +MD := mkdir -p +endif + +# define any directories containing header files other than /usr/include +INCLUDES := $(patsubst %,-I%, $(INCLUDEDIRS:%/=%)) +@echo INCLUDES: $(INCLUDES) + +# define the C libs +LIBS := $(patsubst %,-L%, $(LIBDIRS:%/=%)) + +# define the C source files +SOURCES := $(wildcard $(patsubst %,%/*.c, $(SOURCEDIRS))) + +# define the C object files +OBJECTS := $(patsubst %, $(OBJDIR)/%, $(SOURCES:.c=.o)) +OBJ_MD := $(addprefix $(OBJDIR)/, $(SOURCEDIRS)) + + + +ALL_DEPS := $(OBJECTS:.o=.d) + +# include dependency files of application +ifneq ($(MAKECMDGOALS),clean) +-include $(ALL_DEPS) +endif + +# +# The following part of the makefile is generic; it can be used to +# build any executable just by changing the definitions above and by +# deleting dependencies appended to the file from 'make depend' +# + +OUTPUT_MAIN := $(OUTPUT_PATH)/$(MAIN) + +# Fix path error. +#OUTPUT_MAIN := $(call FIXPATH,$(OUTPUT_MAIN)) + +.PHONY: all clean + +all: main + @$(ECHO) Start Build Image. + $(OBJCOPY) -v -O binary $(OUTPUT_MAIN) $(OUTPUT_TARGET).bin + $(OBJDUMP) --source --all-headers --demangle --line-numbers --wide $(OUTPUT_MAIN) > $(OUTPUT_TARGET).lst + @$(ECHO) Print Size + $(Q)@$(SIZE) --format=berkeley $(OUTPUT_MAIN) +# @$(ECHO) Executing 'all' complete! + +# mk path for object. +$(OBJ_MD): + $(Q)if not exist "$@" $(Q)$(MD) $(call FIXPATH, $@) + +# mk output path. +$(OUTPUT_PATH): + $(Q)if not exist "$@" $(Q)$(MD) $(call FIXPATH, $@) + +$(OBJDIR): + $(Q)if not exist "$@" $(Q)$(MD) $(call FIXPATH, $@) + +$(OUTPUT_MAIN): $(OBJECTS) + @$(ECHO) Linking : "$@" + $(Q)$(CC) $(CFLAGS) $(LDFLAGS) $(INCLUDES) -Wl,-Map,$(OUTPUT_TARGET).map -o $(OUTPUT_MAIN) $(OBJECTS) $(LFLAGS) $(LIBS) $(OUTPUT_BT_LIB) + +main: | $(OUTPUT_PATH) $(OBJDIR) $(OBJ_MD) $(OUTPUT_MAIN) + @$(ECHO) Building : "$(OUTPUT_MAIN)" + +# Static Pattern Rules [targets ...: target-pattern: prereq-patterns ...] +$(OBJECTS): $(OBJDIR)/%.o : %.c + @$(ECHO) Compiling : "$<" + $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + + +clean: +# $(RM) $(OUTPUT_MAIN) +# $(RM) $(OBJECTS) +# $(RM) $(OBJDIR) + $(Q)$(RM) $(call FIXPATH, $(OUTPUT_PATH)) + @$(ECHO) Cleanup complete! + +run: all + ./$(OUTPUT_MAIN) + @$(ECHO) Executing 'run: all' complete! diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a0827b --- /dev/null +++ b/README.md @@ -0,0 +1,751 @@ +# 简介 + +在嵌入式裸机开发中,经常有按键的管理需求,GitHub上已经有蛮多成熟的按键驱动了,但是由于这样那样的问题,最终还是自己实现了一套。本项目地址:[bobwenstudy/easy_button (github.com)](https://github.com/bobwenstudy/easy_button)。 + +项目开发过程中参考了如下几个项目[murphyzhao/FlexibleButton: 灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键 (github.com)](https://github.com/murphyzhao/FlexibleButton),[0x1abin/MultiButton: Button driver for embedded system (github.com)](https://github.com/0x1abin/MultiButton)和[MaJerle/lwbtn: Lightweight button handler for embedded systems (github.com)](https://github.com/MaJerle/lwbtn)。 + +其中核心的按键管理机制借鉴的是[lwbtn](https://github.com/MaJerle/lwbtn),并在其基础上做了比较多的改动,部分事件上报行为和原本处理有些不同。 + +## 对比分析 + +下面从几个维度来对比几个开源库的差异。 + +注意:分析纯属个人观点,如有异议请随时与我沟通。 + +| | [easy_button](https://github.com/bobwenstudy/easy_button) | [FlexibleButton](https://github.com/murphyzhao/FlexibleButton) | [MultiButton](https://github.com/0x1abin/MultiButton) | [lwbtn](https://github.com/MaJerle/lwbtn) | +| ------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------- | ----------------------------------------- | +| 最大支持按键数 | 无限 | 32 | 无限 | 无限 | +| 按键时间参数独立配置 | 支持 | 支持 | 部分支持 | 支持 | +| 单个按键RAM Size(Bytes) | 20(ebtn_btn_t) | 28(flex_button_t) | 44(Button) | 48(lwbtn_btn_t) | +| 支持组合按键 | 支持 | 不支持 | 不支持 | 不支持 | +| 支持静态注册(可以省Code Size) | 支持 | 不支持 | 不支持 | 支持 | +| 支持动态注册 | 支持 | 支持 | 支持 | 不支持 | +| 单击最大次数 | 无限 | 无限 | 2 | 无限 | +| 长按种类 | 无限 | 1 | 1 | 无限 | +| 批量扫描支持 | 支持 | 不支持 | 不支持 | 不支持 | + +可以看出easy_button功能是最全的,并且使用的RAM Size也是最小的,这个在键盘之类有很多按键场景下非常有意义。 + + + +## 组合按键支持 + +现有的项目基本都不支持组合按键,基本都是要求用户根据ID在应用层将多个按键作为一个ID来实现,虽然这样也能实现组合按键的功能需要。 + +但是这样的实现逻辑不够优雅,并且扫描按键行为的逻辑不可避免有重复的部分,增加了mips。 + +本项目基于[bit_array_static](https://github.com/bobwenstudy/bit_array_static)实现了优雅的组合按键处理机制,无需重复定义按键扫描逻辑,驱动会利用已经读取到的按键状态来实现组合按键的功能逻辑。 + + + +## 长按支持 + +实际项目中会遇到各种功能需求,如长按3s是功能A,长按5s是功能B,长按30s是功能C。通过`keepalive_cnt`和`time_keepalive_period`的设计,能够支持各种场景的长按功能需要。 + +如定义`time_keepalive_period=1000`,那么每隔1s会上报一个`KEEPALIVE(EBTN_EVT_KEEPALIVE)`事件,应用层在收到上报事件后,当`keepalive_cnt==3`时,执行功能A;当`keepalive_cnt==5`时,执行功能B;当`keepalive_cnt==30`时,执行功能C。 + + + +## 批量扫描支持 + +现有的按键库都是一个个按键扫描再单独处理,这个在按键比较少的时候,比较好管理,但是在多按键场景下,尤其是矩阵键盘下,这个会大大增加扫描延迟,通过批量扫描支持,可以先在用户层将所有按键状态记录好(用户层根据具体应用优化获取速度),而后一次性将当前状态传给(`ebtn_process_with_curr_state`)驱动。 + +嵌入式按键处理驱动,支持单击、双击、多击、自动消抖、长按、长长按、超长按 | 低功耗支持 | 组合按键支持 | 静态/动态注册支持 + + + +## 简易但灵活的事件类型 + +参考[lwbtn](https://github.com/MaJerle/lwbtn)实现,当有按键事件发生时,所上报的事件类型只有4种,通过`click_cnt`和`keepalive_cnt`来支持灵活的按键点击和长按功能需要,这样的设计大大简化了代码行为,也大大降低了后续维护成本。 + +如果用户觉得不好用,也可以在该驱动基础上再封装出自己所需的驱动。 + +```c +typedef enum +{ + EBTN_EVT_ONPRESS = 0x00, /*!< On press event - sent when valid press is detected */ + EBTN_EVT_ONRELEASE, /*!< On release event - sent when valid release event is detected (from + active to inactive) */ + EBTN_EVT_ONCLICK, /*!< On Click event - sent when valid sequence of on-press and on-release + events occurs */ + EBTN_EVT_KEEPALIVE, /*!< Keep alive event - sent periodically when button is active */ +} ebtn_evt_t; +``` + + + +## 关于低功耗 + +参考[Flexible Button](https://github.com/murphyzhao/FlexibleButton),本按键库是通过不间断扫描的方式来检查按键状态,因此会一直占用 CPU 资源,这对低功耗应用场景是不友好的。为了降低正常工作模式下的功耗,建议合理配置扫描周期(5ms - 20ms),扫描间隙里 CPU 可以进入轻度睡眠。 + +一般MCU都有深度睡眠模式,这是CPU只能被IO切换唤醒,所以驱动为大家提供了`int ebtn_is_in_process(void)`接口来判断是否可以进入深度睡眠模式。 + + + + + + +# 代码结构 + +代码结构如下所示: + +- **ebtn**:驱动库,主要包含BitArray管理和EasyButton管理。 +- **example_user.c**:捕获windows的0-9作为按键输入,测试用户交互的例程。 +- **example_test.c**:模拟一些场景的按键事件,对驱动进行测试。 +- **main.c**:程序主入口,配置进行测试模式函数用户交互模式。 +- **build.mk**和**Makefile**:Makefile编译环境。 +- **README.md**:说明文档 + +```shell +bare_task_msg + ├── ebtn + │ ├── bit_array.h + │ ├── ebtn.c + │ └── ebtn.h + ├── build.mk + ├── example_user.c + └── example_test.c + ├── main.c + ├── Makefile + └── README.md +``` + + + + + + + +# 使用说明 + +## 使用简易步骤 + +Step1:定义KEY_ID、按键参数和按键数组和组合按键数组。 + +```c + +typedef enum +{ + USER_BUTTON_0 = 0, + USER_BUTTON_1, + USER_BUTTON_2, + USER_BUTTON_3, + USER_BUTTON_4, + USER_BUTTON_5, + USER_BUTTON_6, + USER_BUTTON_7, + USER_BUTTON_8, + USER_BUTTON_9, + USER_BUTTON_MAX, + + USER_BUTTON_COMBO_0 = 0x100, + USER_BUTTON_COMBO_1, + USER_BUTTON_COMBO_2, + USER_BUTTON_COMBO_3, + USER_BUTTON_COMBO_MAX, +} user_button_t; + +/* User defined settings */ +static const ebtn_btn_param_t defaul_ebtn_param = EBTN_PARAMS_INIT(20, 0, 20, 300, 200, 500, 10); + +static ebtn_btn_t btns[] = { + EBTN_BUTTON_INIT(USER_BUTTON_0, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_1, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_2, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_3, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_4, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_5, &defaul_ebtn_param), +}; + + +static ebtn_btn_combo_t btns_combo[] = { + EBTN_BUTTON_COMBO_INIT(USER_BUTTON_COMBO_0, &defaul_ebtn_param), + EBTN_BUTTON_COMBO_INIT(USER_BUTTON_COMBO_1, &defaul_ebtn_param), +}; +``` + +Step2:初始化按键驱动 + +```c +ebtn_init(btns, EBTN_ARRAY_SIZE(btns), btns_combo, EBTN_ARRAY_SIZE(btns_combo), + prv_btn_get_state, prv_btn_event); +``` + +Step3:配置组合按键comb_key,必须在按键注册完毕后再配置,不然需要`ebtn_combo_btn_add_btn_by_idx`用这个接口。 + +```c +ebtn_combo_btn_add_btn(&btns_combo[0], USER_BUTTON_0); +ebtn_combo_btn_add_btn(&btns_combo[0], USER_BUTTON_1); + +ebtn_combo_btn_add_btn(&btns_combo[1], USER_BUTTON_2); +ebtn_combo_btn_add_btn(&btns_combo[1], USER_BUTTON_3); +``` + +Step4:动态注册所需按键,并配置comb_key。 + +```c +// dynamic register +for (int i = 0; i < (EBTN_ARRAY_SIZE(btns_dyn)); i++) +{ + ebtn_register(&btns_dyn[i]); +} + +ebtn_combo_btn_add_btn(&btns_combo_dyn[0].btn, USER_BUTTON_4); +ebtn_combo_btn_add_btn(&btns_combo_dyn[0].btn, USER_BUTTON_5); + +ebtn_combo_btn_add_btn(&btns_combo_dyn[1].btn, USER_BUTTON_6); +ebtn_combo_btn_add_btn(&btns_combo_dyn[1].btn, USER_BUTTON_7); + +for (int i = 0; i < (EBTN_ARRAY_SIZE(btns_combo_dyn)); i++) +{ + ebtn_combo_register(&btns_combo_dyn[i]); +} +``` + +Step5:启动按键扫描,具体实现可以用定时器做,也可以启任务或者轮询处理。需要注意需要将当前系统时钟`get_tick()`传给驱动接口`ebtn_process`。 + +```c +while (1) +{ + /* Process forever */ + ebtn_process(get_tick()); + + /* Artificial sleep to offload win process */ + Sleep(5); +} +``` + +具体可以参考`example_win32.c`和`test.c`的实现。 + + + + + +## key_id和key_idx的说明 + +为了更好的实现**组合按键**以及**批量扫描**的支持,驱动引入了BitArray来管理按键的历史状态和组合按键信息。这样就间接引入了key_index的概念,其代表独立按键在驱动的位置,该值不可直接设置,是按照一定规则隐式定义的。 + +key_id是用户定义的,用于标识按键的,该值可以随意更改,但是尽量保证该值独立。 + +如下图所示,驱动有2个静态注册的按键,还有3个动态注册的按键。每个按键的key_id是随意定义的,但是key_idx却是驱动内部隐式定义的,先是静态数组,而后按照动态数组顺先依次定义。 + +**注意**:由于组合按键也会用到key_idx的信息,所以动态按键目前并不提供删除按键的行为,这个可能引发一些风险。 + +![image-20240223103317921](https://markdown-1306347444.cos.ap-shanghai.myqcloud.com/img/image-20240223103317921.png) + + + +## 结构体说明 + +### 按键配置参数结构体说明-ebtn_btn_param_t + +按键根据不同的时间触发不同的事件,目前每个按键可以配置的参数如下。 + +| 名称 | 说明 | +| ---------------------- | ------------------------------------------------------------ | +| time_debounce | 防抖处理,按下防抖超时,配置为0,代表不启动 | +| time_debounce_release | 防抖处理,松开防抖超时,配置为0,代表不启动 | +| time_click_pressed_min | 按键超时处理,按键最短时间,配置为0,代表不检查最小值 | +| time_click_pressed_max | 按键超时处理,按键最长时间,配置为0xFFFF,代表不检查最大值,用于区分长按和按键事件。 | +| time_click_multi_max | 多击处理,两个按键之间认为是连击的超时时间 | +| time_keepalive_period | 长按处理,长按周期,每个周期增加keepalive_cnt计数 | +| max_consecutive | 最大连击次数,配置为0,代表不进行连击检查。 | + + + +```c +typedef struct ebtn_btn_param +{ + /** + * \brief Minimum debounce time for press event in units of milliseconds + * + * This is the time when the input shall have stable active level to detect valid *onpress* + * event. + * + * When value is set to `> 0`, input must be in active state for at least + * minimum milliseconds time, before valid *onpress* event is detected. + * + * \note If value is set to `0`, debounce is not used and *press* event will be + * triggered immediately when input states goes to *inactive* state. + * + * To be safe not using this feature, external logic must ensure stable + * transition at input level. + * + */ + uint16_t time_debounce; /*!< Debounce time in milliseconds */ + + /** + * \brief Minimum debounce time for release event in units of milliseconds + * + * This is the time when the input shall have minimum stable released level to detect valid + * *onrelease* event. + * + * This setting can be useful if application wants to protect against + * unwanted glitches on the line when input is considered "active". + * + * When value is set to `> 0`, input must be in inactive low for at least + * minimum milliseconds time, before valid *onrelease* event is detected + * + * \note If value is set to `0`, debounce is not used and *release* event will be + * triggered immediately when input states goes to *inactive* state + * + */ + uint16_t time_debounce_release; /*!< Debounce time in milliseconds for release event */ + + /** + * \brief Minimum active input time for valid click event, in milliseconds + * + * Input shall be in active state (after debounce) at least this amount of time to even consider + * the potential valid click event. Set the value to `0` to disable this feature + * + */ + uint16_t time_click_pressed_min; /*!< Minimum pressed time for valid click event */ + + /** + * \brief Maximum active input time for valid click event, in milliseconds + * + * Input shall be pressed at most this amount of time to still trigger valid click. + * Set to `-1` to allow any time triggering click event. + * + * When input is active for more than the configured time, click even is not detected and is + * ignored. + * + */ + uint16_t time_click_pressed_max; /*!< Maximum pressed time for valid click event*/ + + /** + * \brief Maximum allowed time between last on-release and next valid on-press, + * to still allow multi-click events, in milliseconds + * + * This value is also used as a timeout length to send the *onclick* event to application from + * previously detected valid click events. + * + * If application relies on multi consecutive clicks, this is the max time to allow user + * to trigger potential new click, or structure will get reset (before sent to user if any + * clicks have been detected so far) + * + */ + uint16_t time_click_multi_max; /*!< Maximum time between 2 clicks to be considered consecutive + click */ + + /** + * \brief Keep-alive event period, in milliseconds + * + * When input is active, keep alive events will be sent through this period of time. + * First keep alive will be sent after input being considered + * active. + * + */ + uint16_t time_keepalive_period; /*!< Time in ms for periodic keep alive event */ + + /** + * \brief Maximum number of allowed consecutive click events, + * before structure gets reset to default value. + * + * \note When consecutive value is reached, application will get notification of + * clicks. This can be executed immediately after last click has been detected, or after + * standard timeout (unless next on-press has already been detected, then it is send to + * application just before valid next press event). + * + */ + uint16_t max_consecutive; /*!< Max number of consecutive clicks */ +} ebtn_btn_param_t; +``` + + + + + +### 按键控制结构体说明-ebtn_btn_t + +每个按键有一个管理结构体,用于记录按键当前状态,按键参数等信息。 + +| 名称 | 说明 | +| ------------------- | ------------------------------------------------------------ | +| key_id | 用户定义的key_id信息,该值建议唯一 | +| flags | 用于记录一些状态,目前只支持`EBTN_FLAG_ONPRESS_SENT`和`EBTN_FLAG_IN_PROCESS` | +| time_change | 记录按键按下或者松开状态的时间点 | +| time_state_change | 记录按键状态切换时间点(并不考虑防抖,单纯记录状态切换时间点) | +| keepalive_last_time | 长按最后一次上报长按时间的时间点,用于管理keepalive_cnt | +| click_last_time | 点击最后一次松开状态的时间点,用于管理click_cnt | +| keepalive_cnt | 长按的KEEP_ALIVE次数 | +| click_cnt | 多击的次数 | +| param | 按键时间参数,指向ebtn_btn_param_t,方便节省RAM,并且多个按键可公用一组参数 | + + + + + +```c +typedef struct ebtn_btn +{ + uint16_t key_id; /*!< User defined custom argument for callback function purpose */ + uint16_t flags; /*!< Private button flags management */ + ebtn_time_t time_change; /*!< Time in ms when button state got changed last time after valid + debounce */ + ebtn_time_t time_state_change; /*!< Time in ms when button state got changed last time */ + + ebtn_time_t keepalive_last_time; /*!< Time in ms of last send keep alive event */ + ebtn_time_t + click_last_time; /*!< Time in ms of last successfully detected (not sent!) click event + */ + + uint16_t keepalive_cnt; /*!< Number of keep alive events sent after successful on-press + detection. Value is reset after on-release */ + uint16_t click_cnt; /*!< Number of consecutive clicks detected, respecting maximum timeout + between clicks */ + + const ebtn_btn_param_t *param; +} ebtn_btn_t; +``` + + + + + +### 组合按键控制结构体说明-ebtn_btn_combo_t + +每个组合按键有一个管理结构体,用于记录组合按键组合配置参数,以及按键信息。 + +| 名称 | 说明 | +| -------- | --------------------------------- | +| comb_key | 用独立按键的key_idx设置的BitArray | +| btn | ebtn_btn_t管理对象,管理按键状态 | + + + +```c +typedef struct ebtn_btn_combo +{ + BIT_ARRAY_DEFINE( + comb_key, + EBTN_MAX_KEYNUM); /*!< select key index - `1` means active, `0` means inactive */ + + ebtn_btn_t btn; +} ebtn_btn_combo_t; +``` + + + + + +### 动态注册按键控制结构体说明-ebtn_btn_dyn_t + +动态注册需要维护一个列表,所以需要一个next指针。 + +| 名称 | 说明 | +| ---- | -------------------------------- | +| next | 用于链表链接每个节点 | +| btn | ebtn_btn_t管理对象,管理按键状态 | + + + +```c +typedef struct ebtn_btn_dyn +{ + struct ebtn_btn_dyn *next; + + ebtn_btn_t btn; +} ebtn_btn_dyn_t; +``` + + + + + +### 动态注册组合按键控制结构体说明-ebtn_btn_combo_dyn_t + +动态注册需要维护一个列表,所以需要一个next指针。 + +| 名称 | 说明 | +| ---- | ------------------------------------------ | +| next | 用于链表链接每个节点 | +| btn | ebtn_btn_combo_t管理对象,管理组合按键状态 | + + + +```c +typedef struct ebtn_btn_combo_dyn +{ + struct ebtn_btn_combo_dyn *next; /*!< point to next combo-button */ + + ebtn_btn_combo_t btn; +} ebtn_btn_combo_dyn_t; +``` + + + + + + + +### 按键驱动管理结构体-ebtn_t + +按键驱动需要管理所有静态注册和动态注册的按键和组合按键信息,并且记录接口以及最后的按键状态。 + +| 名称 | 说明 | +| ------------------ | ------------------------------ | +| btns | 管理静态注册按键的指针 | +| btns_cnt | 记录静态注册按键的个数 | +| btns_combo | 管理静态注册组合按键的指针 | +| btns_combo_cnt | 记录静态注册组合按键的个数 | +| btn_dyn_head | 管理动态注册按键的列表指针 | +| btn_combo_dyn_head | 管理动态注册组合按键的列表指针 | +| evt_fn | 事件上报的回调接口 | +| get_state_fn | 按键状态获取的回调接口 | +| old_state | 记录按键上一次状态 | + + + +```c +typedef struct ebtn +{ + ebtn_btn_t *btns; /*!< Pointer to buttons array */ + uint16_t btns_cnt; /*!< Number of buttons in array */ + ebtn_btn_combo_t *btns_combo; /*!< Pointer to comb-buttons array */ + uint16_t btns_combo_cnt; /*!< Number of comb-buttons in array */ + + ebtn_btn_dyn_t *btn_dyn_head; /*!< Pointer to btn-dynamic list */ + ebtn_btn_combo_dyn_t *btn_combo_dyn_head; /*!< Pointer to btn-combo-dynamic list */ + + ebtn_evt_fn evt_fn; /*!< Pointer to event function */ + ebtn_get_state_fn get_state_fn; /*!< Pointer to get state function */ + + BIT_ARRAY_DEFINE( + old_state, + EBTN_MAX_KEYNUM); /*!< Old button state - `1` means active, `0` means inactive */ +} ebtn_t; +``` + + + +## 操作API + + + +### 核心API + +主要的就是初始化和运行接口,加上动态注册接口。 + +```c +void ebtn_process(ebtn_time_t mstime); +int ebtn_init(ebtn_btn_t *btns, uint16_t btns_cnt, ebtn_btn_combo_t *btns_combo, + uint16_t btns_combo_cnt, ebtn_get_state_fn get_state_fn, ebtn_evt_fn evt_fn); +int ebtn_register(ebtn_btn_dyn_t *button); +int ebtn_combo_register(ebtn_btn_combo_dyn_t *button); +``` + + + +### 组合按键注册key的API + +用于给组合按键绑定btn使用,最终都是关联到`key_idx`上。 + +**注意**,`key_id`注册接口必需先确保对应的Button已经注册到驱动中。 + +```c +void ebtn_combo_btn_add_btn_by_idx(ebtn_btn_combo_t *btn, int idx); +void ebtn_combo_btn_remove_btn_by_idx(ebtn_btn_combo_t *btn, int idx); +void ebtn_combo_btn_add_btn(ebtn_btn_combo_t *btn, uint16_t key_id); +void ebtn_combo_btn_remove_btn(ebtn_btn_combo_t *btn, uint16_t key_id); +``` + + + +### 其他API + +一些工具函数,按需使用。 + +```c +void ebtn_process_with_curr_state(bit_array_t *curr_state, ebtn_time_t mstime); + +int ebtn_get_total_btn_cnt(void); +int ebtn_get_btn_index_by_key_id(uint16_t key_id); +ebtn_btn_t *ebtn_get_btn_by_key_id(uint16_t key_id); +int ebtn_get_btn_index_by_btn(ebtn_btn_t *btn); +int ebtn_get_btn_index_by_btn_dyn(ebtn_btn_dyn_t *btn); + +int ebtn_is_btn_active(const ebtn_btn_t *btn); +int ebtn_is_btn_in_process(const ebtn_btn_t *btn); +int ebtn_is_in_process(void); +``` + + + +其中`ebtn_is_in_process()`可以用于超低功耗业务场景,这时候MCU只有靠IO翻转唤醒。 + + + + + +# 按键核心处理逻辑说明 + +这里参考[用户手册 — LwBTN 文档 (majerle.eu)](https://docs.majerle.eu/projects/lwbtn/en/latest/user-manual/index.html#how-it-works)对本驱动的按键实现机制进行说明。 + +在驱动运行中,应用程序可以会接收到如下事件: + +- `EBTN_EVT_ONPRESS`(简称:`ONPRESS`),每当输入从非活动状态变为活动状态并且最短去抖动时间过去时,都会将事件发送到应用程序 +- `EBTN_EVT_ONRELEASE`(简称:`ONRELEASE`),每当输入发送 `ONPRESS`事件时,以及当输入从活动状态变为非活动状态时,都会将事件发送到应用程序 +- `EBTN_EVT_KEEPALIVE`(简称:`KEEPALIVE`),事件在 `ONPRESS` 和`ONRELEASE`事件之间定期发送 +- `EBTN_EVT_ONCLICK`(简称:`ONCLICK`),事件在`ONRELEASE`后发送,并且仅当活动按钮状态在有效单击事件的允许窗口内时发送。 + +## ONPRESS事件 + +`ONPRESS` 事件是检测到按键处于活动状态时的第一个事件。 由于嵌入式系统的性质和连接到设备的各种按钮,有必要过滤掉潜在的噪音,以忽略无意的多次按下。 这是通过检查输入至少在一些最短时间内处于稳定水平来完成的,通常称为*消抖时间*,通常需要大约`20ms` 。 + +按键*消抖时间*分为按下消抖时间`time_debounce`和松开消抖时间`time_debounce_release`。 + +![image-20240223135908798](https://markdown-1306347444.cos.ap-shanghai.myqcloud.com/img/image-20240223135908798.png) + +## ONRELEASE事件 + +当按键从活动状态变为非活动状态时,才会立即触发 `ONRELEASE`事件,前提是在此之前检测到`ONPRESS` 事件。也就是 `ONRELEASE`事件是伴随着`ONPRESS` 事件发生的。 + +![image-20240223143215840](https://markdown-1306347444.cos.ap-shanghai.myqcloud.com/img/image-20240223143215840.png) + +## ONCLICK事件 + +`ONCLICK`事件在多个事件组合后触发: + +- 应正确检测到`ONPRESS` 事件,表示按钮已按下 +- 应检测到`ONRELEASE`事件,表示按钮已松开 +- `ONPRESS`和`ONRELEASE`事件之间的时间必须在时间窗口内,也就是在`time_click_pressed_min`和`time_click_pressed_max`之间时。 + +当满足条件时,在`ONRELEASE`事件之后的`time_click_multi_max`时间,发送`ONCLICK`事件。 + +![image-20240223143426179](https://markdown-1306347444.cos.ap-shanghai.myqcloud.com/img/image-20240223143426179.png) + +下面显示了在 Windows 测试下的单击事件演示。 + +![image-20240223173405665](https://markdown-1306347444.cos.ap-shanghai.myqcloud.com/img/image-20240223173405665.png) + + + + + +## Multi-Click事件 + +实际需求除了单击需求外,还需要满足多击需求。本驱动是靠`time_click_multi_max`来满足此功能,虽然有多次点击,但是只发送**一次 `ONCLICK`事件**。 + +注意:想象一下,有一个按钮可以在单击时切换一盏灯,并在双击时关闭房间中的所有灯。 通过超时功能和单次点击通知,用户将只收到**一次点击**,并且会根据连续按压次数值,来执行适当的操作。 + +下面是**Multi-Click**的简化图,忽略了消抖时间。`click_cnt`表示检测到的**Multi-Click** 事件数,将在最终的`ONCLICK`事件中上报。 + +需要注意前一个按键的`ONRELEASE`事件和下次的`ONPRESS`事件间隔时间应小于`time_click_multi_max`,`ONCLICK`事件会在最后一次按键的`ONRELEASE`事件之后`time_click_multi_max`时间上报。 + +![image-20240223144701688](https://markdown-1306347444.cos.ap-shanghai.myqcloud.com/img/image-20240223144701688.png) + +下面显示了在 Windows 测试下的三击事件演示。 + +![image-20240223173435824](https://markdown-1306347444.cos.ap-shanghai.myqcloud.com/img/image-20240223173435824.png) + + + +## KEEPALIVE事件 + +`KEEPALIVE`事件在 `ONPRESS`事件和`ONRELEASE`事件之间定期发送,它可用于长按处理,根据过程中有多少`KEEPALIVE`事件以及`time_keepalive_period`可以实现各种复杂的长按功能需求。 + +需要注意这里根据配置的时间参数的不同,可能会出现`KEEPALIVE`事件和`ONCLICK`事件在一次按键事件都上报的情况。这个情况一般发生在按下保持时间(`ONPRESS`事件和`ONRELEASE`事件之间)大于`time_keepalive_period`却小于`time_click_pressed_max`的场景下。 + +![image-20240223173042135](https://markdown-1306347444.cos.ap-shanghai.myqcloud.com/img/image-20240223173042135.png) + +下面显示了在 Windows 测试下的`KEEPALIVE`事件和`ONCLICK`事件在一次按键事件出现的演示。 + +![image-20240223173002558](https://markdown-1306347444.cos.ap-shanghai.myqcloud.com/img/image-20240223173002558.png) + + + + + +而当按下保持时间大于`time_click_pressed_max`时,就不会上报`ONCLICK`事件,如下图所示。 + +![image-20240223145802203](https://markdown-1306347444.cos.ap-shanghai.myqcloud.com/img/image-20240223145802203.png) + + + +## 其他边界场景 + +在`example_test.c`中对一些场景进行了覆盖性测试,具体可以看代码实现,测试都符合预期。 + +**注意**:time_overflow相关的case需要`EBTN_CONFIG_TIMER_16`宏,不然测试时间太长了。 + + + + + + + +# 测试说明 + +## 环境搭建 + +目前测试暂时只支持Windows编译,最终生成exe,可以直接在PC上跑。 + +目前需要安装如下环境: +- GCC环境,笔者用的msys64+mingw,用于编译生成exe,参考这个文章安装即可。[Win7下msys64安装mingw工具链 - Milton - 博客园 (cnblogs.com)](https://www.cnblogs.com/milton/p/11808091.html)。 + + +## 编译说明 + +本项目都是由makefile组织编译的,编译整个项目只需要执行`make all`即可。 + + +也就是可以通过如下指令来编译工程: + +```shell +make all +``` + +而后运行执行`make run`即可运行例程,例程中实现了2个Task的消息管理,并实现了Task Save的相关逻辑。 + +```shell +PS D:\workspace\github\bare_task_msg> make run +Building : "output/main.exe" +Start Build Image. +objcopy -v -O binary output/main.exe output/main.bin +copy from `output/main.exe' [pei-i386] to `output/main.bin' [binary] +objdump --source --all-headers --demangle --line-numbers --wide output/main.exe > output/main.lst +Print Size + text data bss dec hex filename + 41452 7040 2644 51136 c7c0 output/main.exe +./output/main.exe +Heap Remain Size: 0xff8 +Task Start Work! +user_task1(), id: 0x1, len: 0 +BARE_TASK_HDL_SAVED +user_task2(), id: 0x8, len: 0 +user_task2(), id: 0x9, len: 0 +user_task2(), id: 0x50, len: 20 +0x0:0x1:0x2:0x3:0x4:0x5:0x6:0x7:0x8:0x9:0xa:0xb:0xc:0xd:0xe:0xf:0x10:0x11:0x12:0x13: +BARE_TASK_HDL_SAVED +user_task1(), id: 0x1, len: 0 +user_task1(), id: 0x2, len: 0 +user_task1(), id: 0x10, len: 10 +0x0:0x1:0x2:0x3:0x4:0x5:0x6:0x7:0x8:0x9: +user_task1(), id: 0x11, len: 10 +0x10:0x11:0x12:0x13:0x14:0x15:0x16:0x17:0x18:0x19: +user_task2(), id: 0x50, len: 20 +0x0:0x1:0x2:0x3:0x4:0x5:0x6:0x7:0x8:0x9:0xa:0xb:0xc:0xd:0xe:0xf:0x10:0x11:0x12:0x13: +user_task2(), id: 0x51, len: 20 +0x10:0x11:0x12:0x13:0x14:0x15:0x16:0x17:0x18:0x19:0x1a:0x1b:0x1c:0x1d:0x1e:0x1f:0x20:0x21:0x22:0x23: +user_task2(), id: 0x51, len: 100 +0x50:0x51:0x52:0x53:0x54:0x55:0x56:0x57:0x58:0x59:0x5a:0x5b:0x5c:0x5d:0x5e:0x5f:0x60:0x61:0x62:0x63:0x64:0x65:0x66:0x67:0x68:0x69:0x6a:0x6b:0x6c:0x6d:0x6e:0x6f:0x70:0x71:0x72:0x73:0x74:0x75:0x76:0x77:0x78:0x79:0x7a:0x7b:0x7c:0x7d:0x7e:0x7f:0x80:0x81:0x82:0x83:0x84:0x85:0x86:0x87:0x88:0x89:0x8a:0x8b:0x8c:0x8d:0x8e:0x8f:0x90:0x91:0x92:0x93:0x94:0x95:0x96:0x97:0x98:0x99:0x9a:0x9b:0x9c:0x9d:0x9e:0x9f:0xa0:0xa1:0xa2:0xa3:0xa4:0xa5:0xa6:0xa7:0xa8:0xa9:0xaa:0xab:0xac:0xad:0xae:0xaf:0xb0:0xb1:0xb2:0xb3: +Task End Work! +Heap Remain Size: 0xff8 +Executing 'run: all' complete! +``` + +可以看到,2个task同时支持消息处理,当有Save的场景会pending消息,先执行另外一个task,task执行完所有消息后,heap保持不变。 + + + + + + + + + + + + + diff --git a/README.vsdx b/README.vsdx new file mode 100644 index 0000000000000000000000000000000000000000..e3c6740e9789817530291f170d6d06ab5987c771 GIT binary patch literal 50488 zcmeEsQfOo;t11iNaw4uO(xN(*NAm3Jk#z^lf(&?AkBY*ompwpPp zBf;W0w33x}=%sUH?~4S#GKuH*Y-Cx2IO@{|cg$QP7I^bbcv^GwA+?@!(|kcH^^GEG znbpZ_a?>|l;g3KVacQ{^Fy|IHZD}G<1Bc^YHszEocDS*E{;P)m-mq~lhSFQtGRiIFS6YeunpPOHf5e{!URSsQwSnj>-s@@ziEUwmAJhe6#iD|w0&dH8p-fA z$>26@Ww9B&#YDwtSqLN`Hip?pmx;A3HV>OJDF=?6yQO1zIMd0DS}b}%AeH3#9^4l8 za=Qqzoxni7@2vB9e3*A8+~qR(T?Z0@;+of0pu*3{jrB~QxhI=k?_aL|`~U;U|6eRF zRU2p~{p-;I1^|Hm%VJ#zV=G5G+JD&p&))yRl>INzs}d(=2mird@z1~`?}H-lAo)Za z1VvYfV0es62?InvBe{y_n|dfvB2wddS{}R|Pm`K1jT`CN7K{RymP3&g#}F0HVr$L& z)_3>2K6R2hC=b*6L(eELZEf9ZoqBM|Sc6h;BZ=XW1Xtm2g)j;(W(1~_`I5{LP~fDP zd{n$c@Ft2BcOSX|U09uH<~7k|ZmT6&)2D6n6UU}t0U%Hi!45xYd?a;(Y!iK!Ahhzs ziSbmWn9vw%1xE(hrY>p(=Ro(?ybs8v+)fx7rqSP1w-_{mku~|!oWw1hq**OGhX>V> z%rAO#3$uY8P#{(QY=F>0^I4;tNkHq5zo9Pi{OX`xKU3p&P;@phqcwYlJ+wb>c@}Y+Tj1A^h zx0krrzuxza8Ef)IhZC>^^`rl0FAl z=|Z#FB6zyyz@EL6r;K(n^P_WlVixY_HTD@l6yR)hWs)+p7?pxW6Gw^Mob$)P)6?zpfP*O9GcP_fd-@c*53Aa%hSWR z8QK(TbN$NpPK(drS)eLq<{m#aTqX@5z0WY=)|&MY+QwYpN+UqWhX7Ixc;l}6&`9Xh zm-T+@2f9R7gXR3fu)ekL#&>XmTmVPo={|ab4;Hmz8W|Yy8+JImhYuHarr#wxfeNd& z68PoIJ_VS`{ttK~v%5BjkB7Ws6+BaXqSyq3}p95@fW@Y8kPpRL07vhAy@>!)+;3ZmuC!Tj34wlgrBxa04ct`e0yXXBsZj+$bR)^=d&vhg{^?v1Ke>`pI6t4ViW z5Kw0{X?_2zNne%QlfO6=*l}qDXm~S%^IX@A={t?{G;Uhgqq}7dTUBZ|Ygag@WxIdd zy8f#60A1hA1c9U`^J~uJ^1SVR)+yfFw7g=;ur`|#p^rq*`-7G2(cSRdTp8<}4uT^p zK$Bw4>NbBP<=)RsYc!e+hCP!{Ddt1V)&wYb7!|HcB?i6wh$y-g6VLt5927pXQ>OF8L_%9Vvl4%ItEN-qsN_73{ATS5Bh+H z5)R3!85lM|?K+ki$$fQSas+%$?TKjOom67EGBMILZ);cxd(JUMI=54y4IR+4RM0#R zd3_z4>ubPBKYMvjn|c90uduy2o%*8`njbWLDbzk}>SP+^sV{4%bHlt4>|bi7#N zYq91iWT%8&AV^7(ROb-sA#uRg5i#yC)U!M}}vg9@jBs|PYQSE*4 zE?B*KLrZZa2C@X@ooRtpPS)ON^+{^^YB3i89T=xX8FrYZ{;|67kF46~_U{(-a6Ci? zzVGFA`~9D#YrNw7#ib)U2-{`G6@an)>>O~`d> zu^(Cswq!4!rX_7&nr{i+KA7Z&gs~FCC0Vn#S~IP*GXv{QdfJbH%wr?-I|qJDWP$i> zZh+qFzGzPc^3yS>r=^37Ze5B1{c0ESsM2!%UkC;ls*FBk1ss?8dn{rZ*(;GJwfcf4 ze<~$7AaxoKlVoy@9hZxEy0t{9&kA!1QCaO5*P(lL1kjIW^9?3Y5Q@E*N>8ilE73-; zr~1A-;wC0z`eub>Wp(~Y9T|HI-WBlf2kLR6HqK_G@W>B9z6uSUT89(c{fQ#xC1YwC zOUac9ev0$Uq{4PFM4VT_bW*{g4NIPoiFfzwh+tm}gM~k?rZ6AhhDWc1?NSl8d<=GJ z2NH@kj{ZA}-T`^5fGrz%b;V(K!q11=2t}5%K^8KET|Wb7sSk=&Q!|2xA%emxB^@i=If*P?u7Y}fqT9QVjWEVaF^?m*-!z**s_*gu0 zl7-d-_(T;)(|bkwvZ@2{Yt5~P9i&LIm`a<)1m1=Pw#YmNX2}psN-i^}oB?hHZMhhkcO-F{fjqnIdQtWdVSlba=zx~ z>axXB=20y@LS&FhXZW@dLfXX1Q2aQ;x>V)j(u9|{JeE|gffmTMpBI3xryon}DOy%D z7f+_1MdO}UFj92iw|JHG9b_-zO_JRL#Qrmt#Y`9NbJE__ysN4JV+Z#KM^?&>%pnyV z>es1iz0}cSnhJ{ATrH_xg}F69{m6L6gNzmAXo(XsQRHM-WI*2`H!8@0v)Up5W(2CY zZ8iJ1=ltlO8-*rN|C3(wTb?={%<>uzLTnMo2&jvUQ#S5~d5yT-V|gI@2)}Axlw;xC z#=}W^M{KpqHz01^aC^b}<1JOGx~}%)e*YD?We*q7xM~epAvtYG4Cw?0o3Y*g!|JC8 zVB#VWm|`y21ro330NgNDuvFZiN&dSqT`)0x9Oqe7oU?jo_yoNuU>MHrYq5K%9s*}HPlk7((z?zBwlLDtwj24yJRA;KUPD9ppQ^( zX(UV4HxM*%b0|(bQWc}v6yvwE466!hds&@ax&~wwR!M?NRyW^cSfz2J{ol!(l6_hNJFM)qz z6gAaw#G;H7V=b8Hiy{azVhnDd67rdzJsE+>@~UnlmbxqZ6S$VzC6i)VW^aL_qQ-jD znQcJ@M1!O@gD*!?HP&~S*&_A9*2Gy=(q3maYRmY&#+dj^i+YJf)8~b_d$k)-Q3my$ zqtqMal!GcHFXd1C16c^=I7l zXYMnyzi`amK({9QO@mHM^K=>7p&Djfw}A9$sGxS`RDFl( z?V&N(;e*r#!t<$OyvuEDCe|x&f?EuQGfTGDNH5Z=R08!mRX@z`dubV$a1a6dA$6;w zb+J*0B!Z7?xVqVh(1la03YFF={oEQf3;811)&_vje&B!+vdNX$Xpi8NG~kaBJ`H}r z!GA+)_X5LvGWa$5N*a4ofq92lX6=S=1V}g1@Ti7}2QEUF$6bJ4uT=qNn7_As9H452 ztM~Y)!Fa^$aCY2I{T8BTDFK2t*a`&p`vieQ;Smm8!GO>kIhD>Awh zP<*5rR~(y5*C6+Xg)1Uhh4zG5j3@ig*VlW>(H`D(>?*8q zg?w-a8@ne%!035&sQo=`Z`dJ8C2~v(qE>6C$k2^`5XboDHfZp*i_F6#hm8gOYHzY=BCq zlX&|0M|VW~v@<4rwIk6*LmdY_y`2};2@{OUL-3!k*NY#wht{2*w@}|kLJUsTe*66= zemi_KSN8ae+_>g<3D+?Cr20JaAX$JoRYJd6>9$!E&Q+|J=78I_VUA->Pp>q*>nx5tCok=mXNai7L$O;?la?r5BJI=#6*vep5W14U|vA-T&vG^eD zHRm80sA18?f*5bRiq#9Gw^ykZgxk?2^`bFE4APHofTd~nwLsE_HevlFkZ7Cl>myPC z{U;P^fg+V?RpQM^?m=d&>FYkBM3k$^bLl@F&!JIcH*t!ER{0RfG(3kt-Lx2v+A~iX zH)YJFN98!&Cz5w%g_ng|px1Qa&*WV8Y;DPqkq22*&Hc<-k;A&)qy?#&GKABPnE~sj zEi9GQT4)rM=91m7L@-QT2t}<6?S=^bHrw*2Y=A5?cEC7nThP`5>ryMFp}B`&r3JcX*!<+tAoBO3g7I%x$u$eG2T0oo87d+=ky4XJoU z$NK)9;P?D&mz-R|w~-I`&u+@zB4a6@m&T4mCdU#4YB7Ak+aFxukhV@FO=L4jCELU2 z7Dp&xIt?kQiV6Qv-V5a_MwVDf$*6w|fO(1r$gw=s;3@gOF4Osbp!>cZ)cP6<-6uZR zc*qJ&vI{`AlzbOC2Kvf?CbcGF%OrL%Bu#IxVoWIag2s6G93U6N5E@7Ns^y-J$cG>V zzZg)QY|VamXRJo8(Ekcs;5Zmq(y4*fSJeL9-@)7qOcucHiZDJQES5d&ve4kH?~cYL zF-ZcAhn$y~3JH@wB)#h}J_Rx$V?oB>erN(DK*oXG_7Z<7tpu%ECF?~A+<3<0zA>(a;^NQ7JlnSX{hRoYy}37eox7!)8|+T6d) zNHUlCtx6Q1Hg)|t(BI#JT>afo_8k{6C~XaC-`G96{ZJ|cS7Qa@P)z6NY(mfca09gw zBoBjb;f)B4H8tzZbga`pFM%17z|@S)63t+$NCrrp#1@OCd)$8nM*f1jGd%nOau=J0#&!|QI5x#gs6Wm zTAYNxYfx2-bptU%B+xX7u%fdo!EL$!Fr#adAOkbdI{Rv7#+2p&dl8I-A%Ci z6VxedQZoGYk(u|qrGdXcn1DvUzneh2&+HycJF@t52EK!=kj8R}{O9)O=T;o+A5nm-?2>+ogtxpA{p@-gB06**wGzC zKC%hQTzo&2bFDXsaMW_v6!62&E~viIfAUFCMcUfsNRmT+@F;>Kedq#J)6n}YYmG&z zlUAvR%Ly~NtFu9&L)zWDoYDP3jLhSV@)c{kHNjt%bwCC|N1G&5LKX~5Zfn>%K^(+MI=j?O+9X@fQ8LA;hWP{wo9kEJL~D89XMD+X(1N@FCyVR`^RIMkm~e7GzIV>ZzE71xw+QzCs#+j*>K6p;d!!{dm6WvGcQ$s z;&yT2_$wAto*pdWwT!GDV5Y+nuz=6>%6Jek{zc&(ymtmcM^sSE4l%j!pF_M6c2w~6U@TzBtUI-k+a z&Rcdng*QkRf%@kN0LWL^#*>=SLME1OlRV!k!OLWX0ry< zi&#lYUaxpN*R?tNbN8&JX7rz7Wr3+t^3QtNz11f5Tjcld*}eEFIm_I^omm{(9Qin{ zg-kjy67WPq>l`5@OLYPo}EZdm}8Q@m5L&O0?r< zl_~{MnchkjX_+a)BZ@ouk0hTlw-I`HK~)j^0#R=+6!nDS;sO}}f>o`;JXTd~B0Ly2 zLv~lfSYT?XR<6SUh@>h}4BI$Q;s(7u=YV@j4BG@xd3w-k6}8u%^v9V+co0V5XRQ)E zY4LBO<=}~)l}mvMv5-@f;`;3j}b!^BDTQ{Berh$(@0D$UYQoH%_{ zCf5hs*RGuQL=|QrzCuHGWn%Z@&51|#6bSBd1UgH^9RY6YvBKl#mfdw?1Uz(Ch~f{y zLIF{T9)F}lK|egX}&-z9`{%+gl+qosu)@4~P>ZS%)c*4v^z=^Dk9BG<}hammd>tgzK~8aaUG z`nW-O1l_uaDNs)RuLaJQf9GPVxmo*mF~O!487hT@JP5oMs8v|M6RA~5xtFL__^kF? z%-y`@druJeLRGowEZlv6vkrh?45K-1Fe>;^evzkAA_QBaHs}AYa z^P2dsS8;Ew3KMwlTWcP5+?`_sJj2Qq9xsYW8O4ks*IK#O9r!3htY7{@+k0}Tn~LB1 zGu-C8#jbPca$nD`Qzb5N5IVf)!`Gv;KMMJV?Q_o!mx$nV!x#1YvGwb8@r^N?Zm(bV zKyo2X%B|Kpz7CzxbG$kAiJ#wnNqdxw|iED`B7VJZGsAWHG1rt0BX#H_V+zkuKb!9^lR_%)vmx( zC8?%hfY*H%X%~){9JRQO`nrq_I{2~(&NN(1N8#SMyYYtyCocel2V~$&^yn&XS5O6( z7p~~??IKt0yVs0K!LhYY;;Emt7avCouG8z!e_TXpUWSM#fB*ncKm!0^{(BK&t?%e$ z?BGcEzt4Z{BbrpSV>g6Rymifg0(U<5hw#G>j8IE6NHT4fz92HU6!E&BNig?TM{=yN z5ReFx3#xZvgUm=NfXd;i*Ha53jL%~A6KQ?|xC~tl$v4nC42ZZ~O=fr;ud~xLm25Y0 zI|c~hi1$59%8pSL`f{$ghAuMq!=4?C71|UuwaabP5kb=c+~^pG0gL-HsKuTE;HxS4 zNL)_Ed}5ESeJMV=@#^cn#Ql{2peQGXqTLOxy3%OfKLcL^+Rnmpg4&FBME0&(?C&UXBr^vLxhH^RgrQAyDXxxD6F=Gxb2{D<=Y-Wc&#fK zqA2*LMO88kpqR?>b-UdYaKWD;fZpPOEuoZ>=g!9XX5@Ui**|Vv_OmV0$FZ2=Hh-mjhpUTe(0onK{)imw-?>iE4M|_M6L(ElUxf$U^?;HQDX(Z@Z9%9FpcAz z8y70dI}w*sM2zT2)^zD8_5Gqe5```~Umq=yZ!N&6Jr`G%6d^V}l_XSMii@XOVPP>% z!kUiASNT->Z7EsnRG&Z2#o9t(3`Y~%Wj0L zTPNrYH%8&ULPJGager+yh7xOCxy!NirR}%uG&)RSfn`XIDvGx19=jH0iKQm3>>mZF z{iR5W$KApdXG_i9fE)x(7wQ64Y&_$P zy=NOYH~W3TQt<$3pYJm>$I4^J6n5dTp|>onU(eQU!{9@tP7PgTlW)t4Z#Fn>ms-TN zKa9QRs4)GGs|{A?p;=lozzJTL`xMy?Fh$5nu?>B$f3oxCa=k|tUSf(9n7ds+_fJk5@xR+%&+~;lxD-Kam?Ik^tyLyhv)7u)*n<|D zFJgnAaa~r9TZ5CMl4#XvvI|0P$kG>`czEW6wW9X@7|FwIO&YK#(+{DILlDD)8YYku z_w>Q7-5>AuOxSHY3eyOgCaCJf;5eivpGKeipv*bJ@HTbK-02PwG)mg8plM#yG1}1r zC*m00$lF0K8eEnuszwIj(FMz5<~S|b=D75PEZ@sw<)QJG^yvNwjURFXKl~B98j|$3 z?{M!0B=htG>dOl8Woz3CC7oA6j>gRU9fQA=bY%bAMCjhl8MG<}h6f`V(7wAR@7lu% zqWJ6_)btKm-~TjN1+&o&EpvKbQ(GN}_ERh*im$X24R~()i|JekSwvJQgplR8f3~+V zo+7)=3`>y-zd%QED-q9k%JYxFsO)Vfb}un&Vt2oeGv2Wv{t`{pXXm%fOj{9QV`FNg z;bPT9m#~s+Jynq95W{fY)h!n3sBd^)4#|&)<$o?@kn-c3?|(H4jDHIm>c0z_oxZ8D zBi%pkKZ@9rO1I5{D8e=HA?|8DbN}lv- zR#K$6?)@XdTr*oBT`zk_1iHK>h4!EL9D5zy43lgl`JrZoxS;url1*_l{RL)H$TX?| zPMOgL+{Q48a<)~Wme3N^q%yYRsxWtJ`Igl(jCmt<{&9uyqfD6lw8B(p(FzR2w{eM^GKD5dvhE_xd&ix0S?4j(ZeB&R9?a5_OqZz<(l7!4l!wYKVCc{ds@ zE3WbKo`}(EU7{o1XrkMLm-zuBc^Q<-wyi^LQ)9S$i;OkTdqhc08D4Duzr;$cL4rQW zQ&UYV@>Lv~svHuAJ}K8O?qFL+aK|BiS6eB6Enihs?P<&Q=nmMok7*)9F$4uo?U`rU zO%yq3Iy)xDLM~ZM6pcm24+Ev?9k!-g!C9pZ9|0YU-#`|T!>?E6SM-?EFVSvh9bvT)sdF94I#iva9M~@N z&@BJDNaEBHXa`YFN7u+HeN+i8jLlPdf##QS+TZBd!rdy=s_fE~bumX%r571sbui(p;a9&rvZJUWq+2 ziJVeiX$D`Z&tL>*g_|R_?WyH#lQyKa)O)U#(KM{ji`7M9m67i&m*LA(w+$1^6P53d zBqP#hD^Kw!WiU2F`|_cABZVa~l8I}{rBZl4ukWwXUY~DAxm4z(&st>Xj78^!;iwzX zmGm;Pj`V5fA$<1wONVIO1oxuGJ1sPk9wM$_d7fAF|0KT{Gqo~7`73Jr`l~#|{dckW zm$d1>iZK2ub1F)hwD_mICHOlq&6|eHTVC+cN|JCR!N)Idw59BcUordmhO3P>CK4G+ zJ-l7;W$JXs^hf2Z^R43Ymz>wg&~|Cv>-*;%Lo zdKj?V&RwpgO>I_qqK28wUFAnWN%YqJ_s|lUmuIjp_E>zv-b2e-TfXvi#!d`oW@Xrj zfaq@Mz>v|e2Cjg813}vo4Dmrhm=l{$b6e(gA}Q{UA<3jZtP#Zp$oMPqZ97zC&~$FY z=MwQLU@}8p6N1f^C%}hVz^w_T6&<0TXV|ijgv0yXtL*9cgGMD*kF0DIgr@QpiV5uC z1`INIC1x+j-{YEo+L4u5Tj2lcONt##zij&p8Tv1Df`3E*SKIj)@P9X-{{)-uFKib! zebsqz3hm2?3;YbC_vew!2Mj?U-5wveU=i;6I5`D@l_moEb>_EIl+BuxEkqDyWb($M zFsN;D`qQhD-d%%V`16-ckSau!X_nLEl;blPFf5dYhtoXCdcR_a$E_|0*^@q6c*gE6 z{dCd|vQU!j&-t}>(i49!cdaFouAA#A$4XCtqFk=9j<`dq)As9ry~=72()llSYbnp+ z|Hbs*4X zHDkX~M>pn78y997~Sd{(ze&T`bIj=VXvdY zSsmal>Dl9%)4B8DsdL?4I6C{s$N<=^10w^C7yB zLx9aMB_sk6MTmTkQA935c~Cw!Xs|;Kltlo3nM5IwlNy^4@y|e=R*Epfc3j-89kZaw zAd7&axak#QwJD2|f52f%z6vr74@h$aRX~VAcgPYEVSIWWm{C9f#&Tss#l}L+X$3(5 zNx20-_&OLtEG~@_DC+4oU3_ukA&kUr=C2H6U!rhQ2|!T8-=f6!31i|7n{Tn;rqMIL zR1uH*xN3~N5puJ0_36T14iOPEva(_)C*-E-V^c;%@o5E&_93z)jQGS)<^+=Cr78k_ ziab}BgW&$BhMmIL24hq$WC&(^*wtJwM%P+_7+PG*ifFhns1;+pQ zBWdjz(m+a*N^LC8D3dQz*I?G;QIG;Up3n={!4eA!gLKUNu}-}idh{M>e7nrt>P&UT z-!aqj_Pc=+LdC~}svsZ2QImf#MM2?v!IgekNNM*jZ+8X^wW<~OXminvpISoY0(%WG zCtn4cUCW7|rpHM=0XpNJEN!43L`B04IZ2cDgNjM-{d(U2Y_7i(a#63(5ZO_5O3|)! z(6h6eN(2fW2^$$EKYseCjfI5@$-|_;Qa>`U8JR&<(p2+J*WI+{?c^}s{;03_c753{9$AE*E?vPw zw>dxxB^xmJdhR#$Sgqcg6KVN9RD3CKqdrl+H#bWO#iRvC;oKSF-g!6adi*E5k4sP{ z`+)n{K1h!gv2~!tjB^%jL@h*j(vduDz`!D%FonZua_^P;_wC4w6KYr?igHsCbeSHu zj<-EO#Sv<|^1u__V#oB7?=g3o_(TBM#V;56p9EH7+kuYw8p-?H?0dmrR14X3?!2t5 zT5K>S#xfRtOMLo8p~dY3w_nph^qU`EfZC&9Rok2H9Un#~Z-c!$-0vOjj+OQovIpPF zp};rr$Nc_#O#YyHc6m5;wh{;ZIbZ{?`d%rHnD?@dqjt0R380X*yQe^nv=lcc`2g`O$w0&UMztK)><_le4{;1!m;5I39$3 z#uhIF5LvH_c$%}-8Cu#`;pefB)Z$%~JR_DPaz@lbr1jj*P0-np2=j<74GL{>UW`Ku zwK<}eaIeU5TRD|V&JKp$RC~y$g^f70LtPy#k?S=(qNQIB5aI7u_EiW5ROdXs?upZ)7me^ptfEus z;!y*r0f&TGd|tzh30^?X$PlTy_X}R++n71%>*qtwMls4DhUkK+z3Sk0Kh9;ypHVGz zc;+i52V(n$&bWImpnSU-i_JsI4c27`WRLPw(Qqa?l(;7p1Pe(e>LT25x2?@ab!YI< zp?NM}qNBVNxd(-@>)*7|K`dfCY=FvvkA2(qcAc;4Ez@ND&flk>O4ib<>@lJM?H>UbClkJJM-SrHDNYk6+K6Jlz$#ooxGZ ze_iP2bY;cc|%Z4~C+pc5;K?1T*IRO3A3U6S)f4l5#3~;xPDVQUJIAJ0KAgQTXw=9ykmk^t=FZxyQ5%HD zs-CSmMbmk@bN05ORDK=Y8*oawy3(qg8F#!VTyuLfa%AfirLO(-zMON+Se{n_UUpC9Y6c?q;n7){xmN3aL@Ns_j>2-W#{mwckc!NP4&%` zlaE6?64kor>&ZbSE7!1HAWc~I@I(I;B!>!)v}a<+imJ>|F?ZsuhdTB?u)cR3nxrSlkfZ)! zwdUmA-!L?OG?wCd!>M=G<1u>JA_tYs{|dbRU^oZe9Z@&>I(T)mdegH7jmLrS%cp@I zsq@>8+GpsA`}5>G^DFh{Wt+#B;QJx9dj3{@j=C1uF=W1 zXO~sz{-rLB>>FDNd?Zs!>Nq^HE152cwV2jZ_p-&C^5w|`>8a9Mat$p*KL?QRX|%5k zr?!mgg%hZ_d+Vpi&2s1#F7{70G0?}aZG}@g(@!qG5%L^f_beK4>R&6~>9n(pPv^7Q z6A|E2?>@_#&UHn<(KnJ;cJ(+ore_p)deU^3uIOyuAC@1BK8dr}_pWC$k5V7)D0>qJ zXBd4yPP?c1{dDEuoFWS+f6t^8(q++|sH8YnbFwE+az#(3sy^2DImF!aSM@QfzfF5nKkU&Sz3(c0kwT)VX~;7J=KwMh)*{*HObTw!F_<^LjJx1GZY`F@}G z0=BY-J^?5K+{5u{b$S2#U0eXA3g}xcfgaNw=BQKzj$RdE;oe1f9 zH0C-onn352jhnjb806xGq?>czAf`L8q_X zq(d!<%!;}Z4l9l$y2bs(4?W|gpS|YMob+o4e9(kAe#&d6Ng?K9#7Oy_Rn2KoJh+wM zZXe$iZVfMCxzoHQZX#ENYUaGmv-5B{7Awz_7q2n6ka$U~~n^s6g1U4I3x{Y=5}8rMZA=E3Z( zc`zo)>81|5*qH6;c$sdcXC;~nB9B*pVf8uIrXT%MCt7Pgma5WnBFbVm))!Wy})tvj2fEJ4}3ihYNPJkh4)>as;%$L zmqNwIBL$ftpoE*CoEwrH@|$^rQF_tDs8A$ntU`&@LvTap_Dt8hE<$G)J%?=j)jwth zE%a&gPShwJ3h|JM?4ploj*BZc%#|xq^ zjHvz&J*)il+cM)@?RYrANo`hkq=La;CF|mQ1E_3K=?g?Hl^NpBwL3w;H%QR2JDx3x z^@^_JlHn(oz#Gu;%OSmKY49LybE7t_GE*%U_8yuKM*u8rYjh1Dq_`Nph#AcD{=r`j z{fNLK08Wa!fP8Tl&|UXdB+WpxE|(a0$gb!^Z{mVEs9^CYr13IZic0h-_e7>QNS={D z6ta}14@5R0_Cd}fI7eAQjy?OnUpA>D4%(RiGj4$ zxY34Ff7ckN6MXGekPFHH9*3CwuB}bP?X5i=Car zz-v9R^A#=^e$ye_4f)Eglvd6z%kk*?i~wt1)_;0AXl$K1ORg!;M=E?3wq1u-o)sK# zzR;d5isWj#`Pip?Go)7^qhC<@RJE1FCP_`_bogfTdbr5*I+LEIb+crfZDP@6+U0q; zhZNcOO3kjPZC;8bj+9SqkNkR6WC@pzsd)h0)0T1MCghB*T;9sak#xr+1_0k%RPD zoznP-WDEAy)A`{0GyC)H`}3un5qNLn5#9>jEp};%4$Bpdi2TBD(4-%%598Yh9-I!0 z@WKx?D*$t{y4&35Q_#{_h6k~HCa)6N7Z8pp_W-I44?-MozJv*22_=cpxN{$Q7(QRHF0xQIq)p$$P#AC#GUwCD1 zL~Bg@UiJpR$JU`%HJ|T7URx}T(wVsQ(2u>onKrQl8d~4;UY4{F2zNy(s=4ISqmub< zfARVOtJw>9Ua~>tQ&nUV$IlFs&SkqW7 zXed2h4&Wryw_&2{pQ!~Ap(fzk^2x51gVe|P)kG^#g!vJ`bDlCv>0DYa`{!#KPw%Z7 z+Zy2{5az=!$oL&L*?)>#%Has_t$bDrw=<`S=p{pgFm?`O-Qlw7*4JQ&; zUoO;HN*#LB1gI)FdX7fycSDr6AMBTtyvUvtW<3`bAN%TKtQWCuJr0t^AO19NEET2) zVH`~{X$sVq+VEQT&xResjRtF&*y$$!JCHP~5-R@?i)ut3(l>T#npbFK1vsuoT zS4)b3W8iTs!t{lLMI#g4UoY`-p5X=BH_LU5JnN03^S(NYj7LIgD zl#w|V5jQ)hJOs)^Y(kg52SLz*6>btM4$%GVRY#7TV7sgND1=wp_^i5c3h2eOhl;=Rl5n= zuX}{3%*Q#%BwKbsI%k)PhLSrJ-RSOtj_b+5MdEEQA5!0<6y&iUsb-z*m9U$rwptUC zkn<&1><(56qW<41C?%ScOEI7{LS=276m|tO;Y6nJ7vb0FVT9q3YGS>cpH}XB&xxM!?sgI3AF{? zw+rD8s$D@5wq1r0maQ6@Om^1@Y?m-gj93IFhpvGI(7goKuS*X89yvlbiznoKS*7ao z!mI4`6W#zkV&dBB_JO_@4Gr{CI+a1l>m@CTKa);wT`!>pqQpa z(RHK&2q8ge2ZCun`YeEZXBdoFl5wWiZ9J8Qj%c$iUFkXQ+8jlTUVnh# z5$p(CWVgZW53rsH245haFI~jbHm5zWI||L4U8!< zdYp6iq`7uQhhIGO95!{N4W6d2oyRXAr%SBz@c~Nr4_g6Z*6y|7()=bYbKGsiClj_O z!^DK*^;qkp%=;9pdVzZ2XzjJ=9COG=NX2{M0vAE)bOFBs`*dLMV(9R<_Fr?EHJ4ks z_bbw>E~R81i^{%y+D0soR0#5H(wbzEU1n5#q_5=alQx@kkC$K6vwH>EQsJIg0h~#SLiA7>G?pVu7 zl+fBfk~FNu)jSzHNhW3CZ$9BpY_R~!n&CbD+gk7Lexj0ycS z#Amr^wNIIqP4vet6b~$*ep6ds(Hrf)jh}H!4MsB#Ye{i34G+NNVG_I`+%hyy+~rX8 z01{r9ly}1NSZJCihbnFH&$fa0!{Q5*NqhIn-|)WHiJ($!~9#eT{xo~&wSQ7~$*a0ApM`gsnv!_U$R zrsFV;cs1<~OX`FVw!1u}Lt-}5uFLikK3eWvV2N{T49E5bDkxV_WSi!|X6=OOA3c>d zvt+W$koBhAn4K)Ts=+ z#wD2rdG70HV3>?FenD+Z9a!M(FvJf^@vDLzg-|nVS}J+bAaECICQ+o7>=%C zhWElzzI;W$u@pw>2_M|qrPJ;-@T`b=av^C$iqbzv1Pvenm)$ZnjR~~}49i!i)7mL2 z^uijB!5c_=QVc_jHDp7yAvIO$)wrV=&gEVnq{2m^?LWc?&#fWupSP#opAyTYmG@~m z{(Up31r#J%xE?f%LrMC=jG5^@X?@N!NJ4wbHiBDM8Y%cCjACSV9?)rM*^gn(E z_82--05z;GW|pXic+FSu3{lIp>?H1b$}Lu#t7(6M)FC;ZVP}lMIVVI*4@+UL!pyag z<|y-+|1SW`Ks3Koul!}LaoRfKMN~RqeJX%xAk-uyjHbYV;a*ld#@yr>h9N*Vr!9Lb zJ4U>Tk01+e22{hzbq^g*1=o8qUgVbj1sIy6U)Qd+V zMqgzFJ!uRl;cFTD`90dt=A5hP_F&fa4#^DE!uQs84amI7uJ!Kr@DNS`fWS?_EFV4#{qX>GsGeWO;~Yx;<(exY;wa$+y>fBdUGd3RrYpr8?%MW)-nXi8HkC z!snLckxS)_T2~pX+vDRZJxejmsflCfA=J{kKJraP*9UD->_KDKMywAV?T?4XUkeNu2lcdl;9AU;-!7wH$-mse(D*dV^4s(*4{0&ZxqiJqQEX=6Sa7Bh61-{b%|=d^nJjwC;9B zl=m}p_+CF~ox^v7$@g6z!_2%jKNO0|(rN!y`w8Ad-F);66dM<-n$hOP^0z5Jjc-l9 zZ@R?FiE=_@-AOhr)c+)FS_immah0n@SsrCb@6QMC_FwPsO(u=*z>s1!u0>RP8|um! zqlL-0yqwrVx%aJv2pbeP68ndLvZWA-aa+iaAvYGFbO#>jh?God&v?3F)?Jt-=^BXm%lO z7kNH%c&P0#-bf432NnkIU@$re+9An6vQM@EJt>8u-V=cT0h&fg6dIuKNTn;t(AcQX zIT#Rx>RiSNxtmB|xnp%mZ(A`@aACSp&ID&AQ4B{3^-YTzs5g@pZsBAF9k;Sh)U6R6 zB)9<)9f<6JxfOs_fI^GJ)=q- zi0Ox@?jrqynV6h(+?sxY5IwAi)S^dYrA*z70_T*5Ml_OOM5B!squgE%lz+LAqg-lk zDg#>;;8B7=I}hX`0^+f87rOxXn&l{4ruFX&*uh&k+PFpz9n)c&Nm4 zILI+ZwJv2hhUwHbow}w|mm|n`+^Jh@pSHH#FmpOzF56V{<*-4q?u~r88fa!nUoe}T z&~lMLf!vS{WrReXP$|x;UJqYMyekYQe=eNB-ZAs-UQGu7_oE^{g zVmE7M(rc8{KhxEl^!!u05?@^*U{XEm*Ocpv*{CnM{AcIhdswhTVPy2B)$->orhj!i zQa$&!sU$a%d2(us%9-nOw+5ihRUWv}D05v-$S~&K?EkX&tr>GocSa##>9n;vwc1`J ztOi7CJn)U;%Qd}1@+@lkjW5&XdAeTxxg&&sNk+JaaaMPtO;=dCYD4nn+E23P+t=~s z+7~LX7DX5uU+(R@ePYYa&Y;@$akg0R+D+6sUuVg0L#Zeda?sL%3KoW26IS*t!1>St z={MTEHgi2vNPp+lYOD8@`FUDIpK)TM5ZW1v2UkS&jjNR*c>oTkEWnwRE%zpyCWkjW zlT{*I?o^RS!KQpycQlN-Z^4)wNv)RBSxQv54ZeF8#@+Z}O1LiOXM34~QAd2w8FLvr zl$N0-W$Re@M1+V8X2JHoapeugY;Y*nLP&~K8HD{BC}DdesL_r1+Z)Y&i{uVbdW(_3 zPbk67Yy(Gg?cTG!Mop34L1wf;^BW>oTL)U(rt$nC*>X^L0lx_|m4WFF$gWa$rX!eR z{-uwK2?^RkubR2}Entqcp(3~!%i7&Y&blhNm)w5AJ-VleyO3`Ud?*U38nnIP9jgTn z9uMdhLe@B&y{#6;{II_GrfqL3zS#*<-VySo9)>Z9CuxvFjA~d+?G19MOulcqSZ}hm z)s%Qs5L0$}m!G@;(8jFAw}(G+?}GkZ-i05Sl90ND*ysMkcQ1c>U_ZpaA1kZ8ovzpE zY9W&ON;N?z&AVoslRNfY+4QSG%^2X97Ok<+NpxY=`(Nqn|QNR@5M6m*9a+0o} zr^nOJXKNv*^z=!7iC!(f%rEDklvy@e60a;$V1ak(<@{giuk-aO`7}&1u62R3PH?px zUeEEM<+pyNWz5WQ(&DZ^|8(|#ag?sk;J`@k0|%m?zij%88BV&p^Ln~CdG%aewSS#H zdUilpPJQTjHPAeC=Alc^(<2d678Y6Y|DP?-FV50GCbQN2Lc%kNtUgjXJsTgodU#zf z7U^s~UoOgewayBtn~2+FRLf_)X1uPU7|=Z2K1Fk83=PFVlIR$xNj_}lvt2_mC?$E= zOk74dc#>_Vn4^(=Fr*Z@V?hTMWJ3%!M1d+-R_K_|^ua{8N!eF*z?E>4L?i_o_n z7a9x5h02ePlFk&}VK_Yq3_+M6u_ZHbsNxiK$WhwE@DN-<4@Di27R6P#b}1i-6!{p( zfv#?vB8j$h7VN4SO=LbJsL?|c7Yq`mXSbjqP<}LWFr5osCw3GgbIn%c1uOI=fVJxT z;DKUd3*XD+6^#zL?03iW(oKYjWezox#^iK*k&^209|Z zR>H^<%GtVA!{|?A$@}R(eXf>xrz<#U^aP z;b~jg3VG~m?&$6EvbvFyAd}fNTUmLNdtNz{xEdpekUw=>C$7v>XLEaI(xpneG+jJ; zKcVYWrjiKjxJK}m(xv99Gf&-i`O;2-h;oq+mgG`bP2G^h15C_lo6yUg=ikaNQ@&qF z)oOD$IL2rGJ%_B0tMGSr<0`_?H+>i0cqCU*uCe?st*Z<@10+uz(t%|@@nV%;UVc2E zFaGUp{qMVn^Yc{vN4_J4I%{nFm~Wu@bIK>De>7zvgCP}VeT&8sA`){G(D1;GWO>AP zMHAv^_?UZ#D9`W7sDzFz=Inw-=VHuMAdWgp3X&8}AS8?Lhb$VttuJ$g?E#OlrZ*h`4r(%RL@_Y_D$B@Ch5eZ1#Q>aOTpNJ$cxHO593sm!uPGBPeozMhoXq)gi zyU0J%69Uy5%r``wj%z5N5S8y3c=eP@ZTmKqN^PlPgt4|=KZHqrevhfoB5Ju$W>cyY z7)s@i8DablWZ;SJUt^rM&9MgHiEEU~d{aTGqz&CEl~rJ7?p>K^g_?=sRF1A@a(q0) zRAk{Zuqe>I~Y9>x&I1dp~ z#ptu*E5+1h@fE+d6<<9IQ6H!_U$xfwe30iO>m7y3jt@sCF3oGMC+Mk_@595Q-!zlc6xHO=~FJllj8hjTJX-jZGsoq!dL~+<MFN{`P-kzyx&))~ z_h=9BjzWJr6E8saouzMkbsMHQeBXkV^G)6PdNkYGp9c^R2J+Cw!Kir%%sUX@iz3_a zk)m>}&_|aSq5$NZ;Bhni(2}CkM}bQ09Vth32Sylo$9e6J)d4*yIstX?MKBpq0z)Ek z6Vh*9%s{;vigpW!qUpFb&($#LJHJvsKjtXU?AQ= z{H=ue+F20WI_)7c0s}&Pgvu`1-ssm&F#Hf6TIafQ`AQ?t0y;ALWUJFo)O6^=g3Bys zC(DG;NDz;R0)ri+0PV<_?1^jxE3{ZjJwX}3C3gExvNK&Fy>iPK^cRp$+8z-l+rTp* z33?9Ib%37TeYTVVbOY$O63}a5YimG9>2yFq4&_W zMn%$7kdJa4#kdvtj-IEwM?`J>3Eld}A8otdmBZ}=$yXuugMxwWlBO{DYJ(hORO?cf zY|S8JGsxHsGUm?TcT0w%&Yv3lw6)Z;LX2iRsv=8WM&l}RR3pPbw}h|FAmfw8(Zkp8 z-#mZy;>Clqy{MS!aR0}LHCtV@9H~!sq|@Cptv{yqhu8g$%5V|~ zP6DzhsP)3pRY(ols9n8*sme4%QOJ#{ufKM&94qrTwSw8?VJipj$fl?n1zaT*+g8Px zRE}4(P~Ay3E!6)cYYsX>>rcNy#-ZNtyA6A;$j4#kWC$?|-oVqMyc8lcdiOoy{F9K(jxkd83QgO+&%7(7ZH8qYCI&*Yj7rIYX%vgorxepsl?to z-rNQKOrI|Gb^L!O*iqEZ!u|_LUS{#elEptaFgyrb=z_sW3H)y#h=*JN;4pv=9QwgQ@DQIJ3*tfi_A&0nfyNR} zRGJfc6Eu-MB|9j-QX9*{5n_BGl}8b}JW=`w$Q*PmVYWzunaT1LwQ|{p@y4OLy}2Y8#WpC}4xWgNlR3ySMl~&_B!|K> zv!jD`3zhlgvdhSb@fk5bZC+;1XCTnc0G(qU<2CfqC!msCjd9xAYJ+E>m*lcdWl0Vj z6wBUNl5;TbTSaE(QmYj?@ZG_nq(}_o>DgZ5q)bJw7yzlYC7J=pTHB^6$n8*U^>Q^m znx~8P{$h%`#7AY6!AehiVz}jHHr*0-(hN8T2@M{CxLWPATZrHycKxB}c;&oKBL+rw zJlN2KV!$C+es=D?2gU4=L$8?=Q#g4)^6RqQtXT^4X<`Sj5}Jpuuy5v}8{?s?EoQTt zx=|f9)B{BT^tMnBh8l28`9CG@*`{a(4HpLu29R3|F_RXfLRen1g_MAdrh`1F7em)4 zO8OP8R0a2}kThCGlaCk4L@5T%Xx^~#BDkkEveUMM(aX>WjYIA+Xemu-$)fHUAX|w{ zBHyndCD0S%d2v93NXUF5K}rb(HQgB5^<4(q_U4o(zHNZP$e6b5NrKBD$1o|)?@k=>m#ac<^|T*G`3h325pY$_?!(zksqlTvrx5t4Ywtr;L%0^IR12 zrp-+)^`GhA-+}`vMeI&Z6DFnkX1`_nbb|(<3G_F$1t>)~c9}$mt}X^PJqD?iXU@#9 z)!s<)ZF&qU<3+rt@gg+HL$UeWg!2$!6m*slax9p58KG+t!CEJ!FV_Oc2rV6m1`{x1 zdi5AU8!U1$(FB^09(XiJP~fN|h4UUV4k46oK$Ru^D|xJe!g(4n0&(0pjD1KId!W?1 z=m5dF+!T@$PCdo|n2ixVZeo-mjT1tl88<~4ELueLP>-=AXfY!-;Q3$=>wC%f$)N`V zb5?vzbct;UH*-hHMAhG8RIRyYnzsv;<9tG%wxh@y?X6bJucAVzkRtF@ZTO4EtKGc3 zh><3zM_(tW*%3sL!pg%v6p2%Nnq<@Yva@LLMmnkLDCox zUhKV|>{q_n6+c?iIe(e1=F|0jDKCXcrC$xem^6Et1*JDcqyg_geE0IF2lhk!Q}qHm zjvpzb3@ZHd*^5^y{9pCUuAxbDO>7hN7pXsnZqkEibbfJ`{xO-Y<`?VYiF>4&2{YKH zU@&MSX*kU$Gi=;MRqSgPt!k@OQvu55&z$$tO4qN#TkGX_QLIYGRRU(MH?ATU);$IS zw5LyI7uh`7$4?LE=jmVn@^5GBe@C#KZKo4kWBbP*18<^XTRQb-z0Tn@wd}hzXn=a+ zp_|VF@m5t;)3PrUJHz=lB7(`CN&TKOH&L&S$Mr3kMJSEC+@+gS=(W;tPz2?P=P5l# zqlVI^os)SeSv+_(x6DksDdN7GENa!XjnuIpp~y5?wiFB|G*_da9sQUghEDRP%vSOf zG&sZ?jVvURpMBt6Q=gTRnqy^4Z@XxABLZ_cFEKc~V+{5SJa?@CgO^WZmnp$rI{Ld@N5$S+p`wh~MbdYRVH*VFNrx+Yh0$|EKq78pBA8E$iYN0J!&3NeaR5lUo`j$!*Te6M>#G@@y=fy}CIZzi*1 zWD?YX;9l5)^bH0%#;CTXq!}8?Y$KU%B(phy7|HC5R3Wp%KJfCIbFa*g1 znn?uf#AYqaZ#IZSR20NZVHC4*5w1O+*Y7EFxU_g4IS5if^%5_N6v``$=Nnct`C1Ts z*wv+1@z>NSW*fz9<*~@@1U)Go^0mpJ_@1Ck-AIe&1RzyAH@? zE$He1AL}jX+d1%P(Q?3T2j>>Etx--d-SAd|1@yAx5L%qjCjncZeX>JAj!Tke#k{c) zUeU|;r41XdAct#qv-GlE3+Bv8JjaXBw*cY`b1*gc9Sw9K)gW|(w#+8?9hFAps4#cl z4`GZEJi->MrLx$mFUM)yo6B(+xPWYhpOAF>aK{+cvY2w5k;@i+_LQO(4}*LEp%W;b zE0f>T_3X4sSoLb1bG;f*ie8RdqBqNynr^!tT`oJbOY>BF3vU;7oS&P`vzYCT28>4)(E^a8C-jr%wAtEbV;%1BWZeu+z9&&=MXF=cvx-!)1j^Ga+ z_C3ORsLLb?R7WBksb1Oh&@o}F3KYchs3V2*-%?$MikjjIUVFwC(gPvx3R1~PqQX&R zz8;V1NfE}TX_Eo+GImlQ(|Z;zgogufRX_?de6=!TE7WCT8$FW90BGes6IUB`8QB4f z=f$YYP&Y{-jia<_WBX!yk%@1xm-b*OgejiifT4~mcxTdJTgSVw)k-YOlGFsL2xYQ9 z5ej-+x<-Umi&05KM<2xbRKVh)E-H;kN7EQ@$W$xf$3tDCF0(w6+8)ZIYSd*aBap~> zVqthyn8v8fMDC2GD_Uen^Uzzo`bJ%*Y8GCnB-V6!F`d=CpdutcTum33)er7n&Zmn$ z0zphB=87LP>N5G`oF$ORjaGP{nl4P0p{w}u9apI)18PXC0v<7zyX#*+a3K_`r3{FlO!8a0+S>?c;5G4e8BiarD`ESMVr0yi1>DS^Qz!$cun z-qujHZ6nFcU{=zQ_zp4dIEXN-omiTXE$xi&LSRI^(V(d!BPGRpHsB4X?r z%e|_1>E--i>6_YERp4|71m<{NxnnFH!|*)ukZ2&$AxJIwib8E2oM$(mEoI=`sLL32 znXc+GM6VgpUH35)AcV3s>Kfp@a7t*Y$hKu$IF+ zsmnx;8^NGMz)`Ph<+eg!jO@L}W{o$nS=H@LY}ODe=Wgiv;MPJ7NO%T=9Ai||QW8Fl zxQr2(G2${j9rhhp_iByP*3~@(N;;c0+f=eyu|cuyjp8!INl;BUgLz)!L#Mb)HrmIi z#6X#c%S(I}#RayS5tq>zRE$?D3JX6(_Vmb?&J(MC)v{T``;Bx~?ATEfYzZzTpR6%7&`0xQ#O4<4mioKPZP8Bbh?_(P&)&KmDG<(ip9`b^HfTfU1?-E zE7c;Mlv|hHYvNcpE>^8z8F88KM_k5j5|@dAXNu8hv>!4kJ*#h7E)K!G4}mP6V{HlN z%jz3GiC##7vK`5+rusH&IIrJR=9y{Xe5MxwfmRGu#u8B%+|x6o+-<&Xq(8UmDQdCNg7zUOKp89J18yOwkAqHFP+3dz)B*49u?#CtCOyp5~wTaHcI z{+R9xkq}oRT_>F|8tV|cDA;+43mq0e2L>batu;?Q@Wo!3`Hqq0e80j|r`6&x3=*6uETSzr<7!nb zaqBB@+V-Z(8zusw_6OT;2T8`c9OM|I8WvM|GdeOxM~2K#wXfm3uDo%Lv&=eeZM8A0 zMytHhOmf;(RNiQVV%ZxjZ&hSwIImXT+`vY~0$Q0gEtTyhPDI3}wN8PhhAm%3x|#>d6yD(4 zRaNCW&Zn)|5_W}^3M21CsH-kEEu_~-Z?KL`W9f~GT#D7-E}loHeH=kF4zh6$de~S2 zttB-J*^q>06e6SRgNoUMN$WsK+Cu^`NstjB3Mbv}k-~YJ&1lCEqxKw(fKOx^c%X2e zWWU`g5bh6z_eeHXrZsLrL~lbBHV$xaaQ;ej1CQuQ`6W&uSRMuec2+XgMMRHmt>bwy zrg=gfCPJqp_Xx{$Ej3p0reR>H11ZjUF53UtT~xvjSQq}I?5IvJ z^xen_1J6hEfE|X}D5Z9jWRY|``^i$P?e#g={F)x(n&u_uJfDT7ny^g&M zS*r{dHPWbV4iV0*oPf?f8&E3>6SBp&m91naVgpixxTwRINk~^_Xje(HI_tH8CawE-!yQ zt_bxGjT~~rfa+@jz+OZ;2I_4^I-hR(+bQWGEi%(Y@gPf!s=H54kG}TUoucFVv8vip zs-j=?#jg0#arMHk2KD;O%=DTKoag4Nqfw|PpA?5rt|B%m`76z|+)#0*Cyyo0Kc1XU z)nVfR&*9+Pw~(E1z?TEs0wM!tfK3VCzJ-2m5D6kA6)=QyquQf^?|KOhtZo6YV?mb|MSq(bxg$!-o zE8Wt2o7K+v>%u%TCD+cmqSdTX9hS+(mGiDF{y4imsE${%As#{ojt|PNi+L4F!i~Xv!&(OBjb`<&)T~xfE0lfd+v7Qr zP=;!qr`H!Gbif^&qmeV`sN77ok1TXMColrwGrE50_MqBt7|t*fpc6$B?re-^0t}dS z5%82Idw|~gh}sQ2((Xp&BQ4Xl`|>zgsoNpd~9L#p##?-#8p58 zyb?PYfHwetD*=8Kl`9Be3<&V9g{%l>F~>w9Owb1-;IE6rUcS}tr#H*4qbYJz-|x^Q4JfDLXfhLkw%xvj!qCy`d2Out3l2kX_DML zjDqoq7b1t|W+UxRt)rPe87L_O{i~bk!|V5ydEE@(DXT`^Ujkj%KWtRMiY=&=1@MW`y%sq5{^B_t=qRJw7o&k?G_Z^Y7S$%IZJ6I}SMUZ4 zwY}|zmDEJAgymFJu*4Q6OV=oaRRw4U;|1?2wvc0iBq9V4F&q-fb`JL9yyWexio>wg zl%_GNFD)};fE!#%n-Q_=d&nP)O`qKDxiR(YiV)|G29V#+dcX9wtm%9al zs>Y|R<}Trz)#A(ivPs5@sQ*|%((`_uuTROb!NU1F)Ve@f>t$cCM@{}XJ;slC5lUq@ z`V!)0x&QFp%byJ{kv*yL}CnLXnO|MYH-Kus|-eeuD_LHpn_PlfPDp!l%0;Q4s=YzNJ_9v6S{zW#E z&lc-l`>%ibeflx`ugyJ)5TqV3iCfLjb&c@LDjh_UP@n3N#5GmA{LLFqphQIOAA~zo zy{F7c(;|Gyo1q*$k|5maDIx^} zn{w(lz}WU-U_;kA71>JB5d22()?|&rzPmdSbM_F~Nigs&j4kkqc8Ack)9aFd^ ztuBX$@{=vJIs~1d<$4f1B#AsoEsBGVl);T5_YF-$OYBF`QLw)JqRBt}BAqLM)MeY_>Tw<#u{<;^k)~v3-v>I%sMe*-+q&=+m`y7ctjNFWup8PTj+S3L z?D%52kQp%Ht#3Rm<)pvkSfOqd%8d?T6!ZW>X&t@I+lg>{Ce^ z=7BS5HJ3o5F zo6k?U^s+cXPie30TYimLKdwT%x^>M>8|>5emK)}wQjg2F6;o;8No)4Ku^v|iXa+*O z9tYzGp#sb(b3BNZbEp#MC7+Ex^*D4lLA)JS29O;o4@Cc(h+dD2kw8MohbcM;`Xo_O zFPh!NdK}A~F#W|6hxTSOsIka5RTeGscT0nq?`SNI>$l&mLCSI3^F|1-JrhM`?L+o7 zcPRxIAIkIaT~3H@SG}|)-aZEGrW}`D4CSh@YFUwKW7`_aaho5t#zNNi<+v*z_=c6^ zHorm*4#C^$YI?GoUYuq{Ws;HO7xdNQ=*4_>nNc6OVZmQsr^oewcvrF|_(vue z^2K}K``-87LjJaLZwjmW?R@du?sRf0g>jWN>gx~FY1Z)74 zvikk`vzPma`@57oEq0+N*OW(fyQH~Y2gt(4a2b;DfItb=E?^PMn;e)CrICUILXKh< zfb^+CvF?Foh(V5?3gB5m8wKe*>33}fl@YZtPKa;lBsf=|?A;J3Lw$uD^j5@uz!^X_ zF8k&*D17{AS+p0PYuqIr7sC zS`G^b;Lbo6&9-}O#DYo)+txHnXH=7-!a$Etj^{+{Ke;C_Xh&@)@ zZGyqj^n&!jw3FZR_(q>q1G^59N392HW$5ziEPGE#G{?vl#9DkwVZu2XsKT)fE`%r~ zVub#ajV~HvIs{8{E#&VtUBS`uI3HOFu*$g0tY6dKPt(`4lWZ|9qD`gQ(({jPpCB-t z0M0*Ke5ilBz4-?q{N*0=52t7KoeneV)N*k?K5n>Zao=Ap#vpLlG>dmHCga(VNBI7H zn*DNgyqKIXS3In|r)n(AdR_f?_VDpDdO{WaqN~ggIrnrf!)`n4WdEqbjVb4poptnH zXdTuHTgU0@cGgve8rIg8E0?y)F%vo1)>Tjwi*V||T=5FCNJLp`E+ctr`}&n(ezi`$ zqSB@*1<$YMQhPdNeAhgK#GQ{kKUA%D)eIniNC6u~U%xMpCCXUAiRX*#;^NKw$?Q+l z$RTXNXWrwlc%Zqblnopt`IR=1)4jS`n%hJSO(RXU9;)cQ~>)>oYz2AKn~ zts;lf76p*)B!|@vG=t_!#1&!O}8s5HDnW_5P!pLF4y-s#ml0`pp z4yYXI=-b`B{rj|_9DpT(YQ6`Dub=KcIXGbJSGnBW`|LqwAP#{B#0WCYWW1$$ASV1)7s(*liL>KS#gv=yhbme=OeAu2-nzY1I*r1~N|*4K9KRDo7fR zPb3#KQ4_I4(}nU7z3C#m1}aR9`9(H-`)09*c7vneEqV(SbfDlF32#fGB0WBQ^7uP0 z;sDA~F5=KlK5`LbD}DjpwX%+PVB{a5q%%QlBv6_>WNT?`#KmTjKZq?3wWUarngxr6 z9oZB4Q%@hCZQ*izh2(v09aLH8F8u|bj&-*Q87Ke3O{c}dJ^l+*!L$jjTV zIP^!M0O=b;-wKZvm0WG1e*y{17>^zFBPb(+L&YHi&_t@x9~ze!62*g%*H?1+*h{`} z7%Ra(gt7ogK+d0!Wkc^0l`PCu1s{+L(Z&|4_Oj%o;?7y}(Ic5M%-63=4T&|=?WfG1 zDqujq43ibm7s-ZpP%eCewl!@q`ADp3pbAAK8tx=0WNi+GeGq3M8V(99$VDpd$%7P9 zvh^Lb@5U+#lv=q;B{DH*9|a!t6JU3Vg9|X)bWzfYB9?x{9;~Gw=`~Vip3{sc!?+$= z)_rw+(B?m@oMSzpQA~+6m|~MOn7Mh^GM79;$zwlm-V8_CbpBIjF5g3Xa`NVVU*;0# zZezAS0)`|t4H*4!xwDr0xHZ@=R{TMEPiv7 zcKbb;d4l3M7Y*=Yw0L$Q5~N6h_K6aqq702!Q5ry!WJ9XoV0XP3y`bRx0~Z1UB8~Ev z5Gb^sKr<**ZUJS%8FBYE%RE6hYb;rw6GKx6=`0L>#qg7OrT}}fenNyIP+KxF=1JAM z$p=7;IuX7qm+%ut^B`6gAzBk%MJOD6w4*am(l+La`hm^~Vel2EAZ}#pBo6vU<~*hi zoyEdmHxd4-lBSkp;N^A-(ZV?i zm?!x*#yr6mB|__Dp41IALuJYkjM{|2tol(5rJoQ#{3h`btd`?rqbVU+EsM!^9G?9j zp8CJ0Z9;r!%rwJf^hE6wK#zP7BoCm%sM0j18yZn+#!C?eLn?udOTmJil~@$e6?SxcFj&Y?mO6m$wxcP(rIP7jj0>PC$qG>YfrngqHNPC zbtkKY*XkAX)n{eWQ!QjCZ($`-wkE4>W$^gZ*z_H5G+CB=Dy4T>?4jp!Xo?$gPruvW zf3bV`|2P1ekMuJ{^5xD zG_YVX3(pt0=tvtN^&qlJ3i*}c{dA{^@QhZlpuj3{l*Q|BB3`dSGGu2MYTYhoCuUfS zA|B>T%O@Ia);_$Yt6nKxwaIp`4WoMh`@#r#N$?t!RLO6_MK#` z(6J0c1AH$IpHs_hQbUFMe+mPr*hmFFAHY^c4FwhML@osLijHjkYm#L!Sq77g+?W~G z+a?%7xn+#8E|s+i=eVl*N+8^-7h=Wo?x0wnVMtq~2dsb;@-ubOrrS)QewStEEs2CkK$)ht4{@8R!$6*4-FI*U-5Kl@P_S2^eo6LbGGxwf+p1khl!5(E~Pe5jjI@Ka9bIAlHTB0TTO0wo0ygTXEGv2f+Ffq7Dyb_|T@7RE-UJ>JPn9iijgcuRyS%6CWUr ztd_Vb=DmD^DIP)_MB}8WrUe!SH7>{_G&3No$pG0h5Ka^i-`aO;-*2IPZ?C;$>)m@ zV zyaO!@c}>gV$0zyPCCrzsX0I$6Ys3(BH)8Nl!YmJ%*+|#45kvpeLhX6yEF(Bod%!rP zd%`szGkoqf)W!E-5UuI95yRy-VrYLdii#yGIjvRROTX5v8^}N9X3+~)JTb4nE>7Il zJE8L`>WnnA#wo>B)ND3;h4k_+mfrg&o4?PNi(h3gLkGPKiPj3j964r1i^HZ1t!b;& z&et$d(?xa-R3t3TFS6mJT;|k#po)M(A%;>6_x#z5m-|OYyy1dVHeA4OUw$qRX%Ni` zHd(?Jj&yL2o`FetACB_eoD2ONBxYntW*FYv+?=GMA516b`D@P|B-)%$Grsv{c7ZPK zPv<9DL&d%zW@A_*O7qV>FzJ(QY;fQ-;CzCFhFFy)+KI6HiZaj=I}| zb%BNgAW$Pb^S27$dc+Wkp=pg($no8c;3{L7^fdz7ZUZRO?()~Xaa#o zx=7Q3ro$H&dgQEk&n((3PS? zzd0=UXz}Kf$~HZu4V`4GtBXPE&1UFixLRe}a@-3|Ny>GPet~~kQOhe_yE@MHO1Hhz zFNYW3v_7rBZo{nAGIaSimZ8HI^9&ssd(}KMW6v{m=+B1r7EU^v55x*TPFz?1O4hJY zHky_&(HhCJ9fxO06kfZLbnvyz(6KremRGAR8OzXhC+JWP0Oe}22(p2$X@bsnO8;q^ zom6lVOK$8QFDLWaMV{1v?&DFm+{@mM(OXQYfK-&ZC$o=}iwOqG$7xJvU;v5Y{xDgd zkz0d-iws5Y098JLeZc{RvICWRJ5oL@e^zFD*fx<~gdq(FgE*0z3#+z#by@sSd%ewa?PWvOVS^lRImNN%H@ylGkL zzBk=COWik6b+>3eQnJ*A)G9r<=g(g5AMWo`&bru4lcP_X)$7h>1Fly*Wr`F_I{HAW zI_UIpjsf9-JC5Z=LQ-a_>;(83LEmWpMjbcBvSgIX78HIYRy?G@6G#@wL$`v1i9#f# zBhV!d`3S&6;#+PA1|u<6Xc0uj(+AZ*^dZ+L#=l}DjOdHFs}8t+xV>ms%7PRH2m@0c zO-&PM_Rzy`=BiVqg|2EYU^UcoLF0#y#_}8q`fGJEZQ)jh_MxnM<*vq7wx|<+z3m zl2u~0O03d`R;iayb>MBYN~{%ab|ZbZR{gS8ZS^jwt!A@_uaC&Osm|-=h|V^klRzrgcNGkhxxF5!M<6XagoZ%(j3EP+*XxWtpAa3i z+Cc|+&=r)t>56VIlv=6v&$jAbeAQ-+Mh_QaJ^?SSgUai2M(C#Hqmt5#p@|je2)7Xq4<4uo z>?4J2FO(928A~qfR)Bb{DbR*@QY=kKXXA{L=Fz_Zh;>4)IqhxHpcxaz&{GQ`f))E< zy^5o*7>1{33ccbOIzDK3200(T+A27FDF}wvZeTU>{eYOt)V-J?klYC&(o6!+;tDmY zXc)BcQMC`c_^64Wf*6hD0k~lDHg4!7HVbeQvj7!~%kojJV$5YL#x$-^a?MZzoSLgr z<>woU!KvCqC!`kJDncr4QNpuM$(WjXW|37Hevx7fWm6+Jg77h_C%C2O@t5VJN+AS> zbss16Ppo>H&rj*0gZUymok=$0rx0Fq#!?cL(MVr@^Ywk#^&SMt161F9_8GtTT6M4= zfrg5~5TUXGEDsg#RRV7j&P>yDC^cWC!(t|*Q66N%b}S+jdR$g0rc)@UxK)Sa+3Ax# zky37f*f~pdC5U|>L$>&4g0UpNSU;XWz9Zx+Nf}`VcPAFCu35E@zf=~G= zDsb57dp}KI&rY(%6y&P$-8WMw)jv#YH$7lROfP!JBY9o0gtkYYv*8Rqae#9t;5LjmW?lgACsDbCMBsZYJXwvi{%|nD8R{Sb|gAbSE#DhM?w zVI9UkI-sV|hK22Y1A8S~*KOQ!(y=DcVp?%1|%+vp^nr5SRwJbQm+)Kr(QNO zWI`&C*g8RC#fCx&{}x}A`}@^+Kwp|#sp{E6G6ntwk2w6)b~q(&&X0%*%$+VOX*5I= zUg7UNhSyq#2fK}iz%$%XVC&PnpZg(1m&viZ#cP2Ay?BFJ7#s^cNv31k_sk^?`1?S` z@8`m)WU;~2o8Cx!?y;DJorCpEZR!kAfyjkaif=(n`Bf+ zp59T!Z+)1s4TDZjo(jgqBBi~BQ?1T;Nk#Ak!5JauA(63xXrTQ=Obr`Y*_&5eav7d2orv^vR5e@2 zc$P;^^C54(cG68ZLT<>gp{$w8J7k;O38+#JrKTjJez0Nr*71vu#J01D=^dgjzUy*)qD#g~e4?BWXoxNF?kag$ zJP%v6hwTAYzBDR_=~%h}IRtkOvMA7%MCS8Po+g%DbwJpFLd5s`X>f$EBepE4KJySu zqF2Dl`lPk?c|(L7>aR}^kV_Ct1(owg)LULHF7gy#8b5TZ9I35(do9jh`P1)x*zx#*8IwMQ~*ZmcB9anbvaq)V!a~zr|E} zyWX62Af?VQenA9{r()D~!Pb+2*fgfvjmU3|s+RaZF{A|lRboU2ltph@iKIrm1|sJ0 zap9PQYkFe8w3VZksk_j$s`Xw8|SL5Bs(X~Lp{HK~`Jx-~enN2Ys_y-coR zKC3Hg7C&!fQ{8yqIZE^=mwT(MBwK6)AevGrFLv9xsTxA z{YQ(Rv2jK>lqxa^_59Q?->%{j!5t_MjVoiAjh;Nb$^j{XFje+ke4?q`U>&w+wmb*> zX-Xa5Fha}Oeg%^v%#GV#w6>@iF+nXfEm<1GOShbF^BX%Nu7>k@lG2GwhZ~@!hmN>g z+3F+1c?KF+$~S-nbJkya1ekMB(5{W0KW%TnMOJVV?rR~TE?RZ;sOkO?yLve7id2iT zi++i0v22l+)kQ(o@G!K90qUV7CSHZw8x21BqCKw>3N**%ao|Z|!#P>Q(tfN3ii(^1 zl)iCw&Q3P+5wFiH?=qp%@b--@Tb|3ggnWZMZABWX>ATjgX2D?JYGNa%hH?|DA2s@B zp;M-Lw*aRlUnU+()*EwX6|#{czlo&k;dU?lF%vOVTSisMn)h=(-~wYo&+*)r-`?Pw zGr)pB#NQoL!1dGmgqBL;EZz1BP}<8J)Bu?i;;)`hyO!kS_5DX;NCK7cSX7>L5lI_D z^@=cg!(-&1GiX>Tk@qC2)l+{a!2yiY0=D$6#(lItv+*BljcdW@3@Jp;eOI=^gITpCCs3MQbOABY5ufh{C(aN62 z=knc@$L8KxMZyNrjDT#41ru6SEtv!te5EIVpkv7=#p9XQK~7)MFEVx)3 z6DMOTT;-%+6-rqHqCCnWsnr2-Y3j?kOj-s%hV658M<+WcHu4TdUI9(xV>pBrM<-7* z#$=!3`-6xXDM^8|ag1%!d0oYvXrx$UaOi4Yb2;Vf0hb3~Y&4rrr`15yhwO7vyo$

z&JTEt6NgALK{$9VUgyXpkTb`c6DQ|bD8}*qnt?$S8$g1dGsC#uy(#1HJwVE@CU8YT z>IltJuneQMW$AGRX1QF|`+*D<0kp-U_WeT(X6+6M7`F;xYa? z+aES3+kWb<+GHoOOF$xP!L&H$gte#VyuK*C8R?=JH(PHFg=H)V++OavQAqN!%=A%P zC@Hk{qHYicMP;7x%o6G-GAmCx>f*u#cDW`QJ6jN;!7CQ$Af|U1oHsx6sOwOBsnJ&- z^$FUy<)>ucw{5M5)AZqCqC@n>V-yKuTx&Ss@gEFeC>mp*W$T<~kqIBaZOYWR6WWw~ zlWsku0cB~8qF%LYGGb;e>z8bzg)G)$eQ76bwZ8BiX(?QGQh;uiw%DBK0ZrK*4k+uL8RE>KZLG8<#E137>^Y94C_Zm%m0@H{Dqg}b*SeIs!saxo!2S`MSp zf>5X&9h*|c)NQTN8}Q~-gbRXlfmW6)`)y*hIJY|P$|zh%?Vahm4Nkmg)b$`)2G?um z%cLx;If?voht>ipX8(q=Uu45*r%@xl(C43)CUjFb?m7#nJf`hOKL{B$d|5 zG?u=}Z0#!1gb){4L7>8JjR%n(oBY0I(y8F01&K2b;1LW&96v;2Aew&E#eTfqhqQrWZhff=T%`+SmK z>9#a`XS{T}{5eGl&tNn#@61~*vX!)F>YIbiAq+pnPS^Ff#oSWYY9^VB`Y*5NytdCVsS`Z(O2o{g)qEVQEG zW*=sZPj*r=-f1J`earx>?ov)a%E-FFZrrGiRZxpjMqBW0D|3pH(NpW>;dIlx|5UN81QeTp22RZaaF$^d#SUu&%2BPJvd!0HBi^kXcj4(Ug zoIB%D9o6=%Lzmm)JLre|pA|K`eYTRj81aPmD3|r+SdYS#ex(JnlQ$YPo%g0X{Oc>9 zZQP>*C$Gj&*ONUS=`?<>{q|FXoO7LVZ&^zKZYZ$TDe9GOfODz=#297Suo{3gANsjm zsZn`Zec!$M-qPEV<-OVN#nN-5UaNj*Nh4CG48ehIQ8_?Mi&-`12dwaOW4V3Ak8+}p zHCK%~>L7~(=TaJox`~^|H2V_fQNqx zPG(9^MhGi3e#5DH9vGjk6B)+JJG#yQCufVYD&CzkFI^nRrpyVv(=5tj>JI*+Uik6y z{dTkS{j~G-&By0;wex*H#^>iN-TU?9`}3yt3oLR2!;j@To-$kEkqXxkFXe~22e)Te z4tDQB$(>vDmr3pO_n#*rWRn+*k~=^!;<6XM>oF4-g z=e+Ocx?M);doblUx69O=+RB|?_SGq!=QY{$UR{FUlpkS zBkk}%3RL6JAHrKeTmKQ>vigVcmY{;r<^~QIn+yTeQ=H?VO3HjZ^a~1HeG4#Px}bY- zy6fD1<&591!LHgN+;B?@cF?>1gqCTQQjqIuOgv!-qEP)7ICfpAyH+|i&+(!JOoN1m z$oLf3nRtz59DcM4__aszu$zw^fu{JBbM%0h(^Unj$YY)s+|%8SZ>mu(kK9C&Qh({I zJaf+dkG{<%^Jc{AOWSL?_Ydd(x9ev9)-`?~5^hc4KtP{AlEB%_!OVuv#mw5!#>L?` z`Kd7lIh%BPMDOhS_pbD8=oBD7$l{g!anc40#g>ZrZ=}g=Va(OB=fvloo0uR{3+6H& zOE5QHE(nkoy?;{9MUpBuFd<+Q^|c27~P?oX%%7Tg}L1#dF13zn!X)EP$Z zT^i@oQQY_;(K(KgjZUM2akE&3(dzdp0oWDkVY|ilf);saX^HaWH8ai><|5_F<@poQ z-Mi6;`jaJ4yCJQ}PX~>rj$td7JLp%5JAy;$?k>BYeb zOn|8T@R2WoWxwcqwVcexh-O4Zsl-AwprNP+4$1v;`kVX@X$F2ZV z{D%M@T&D0gT=#i$EI(_13w%kV$1DGGirTRsna9}QU)qQh-PYg*4Ka10sY^U1V{KZy zKLlv?Fof(J<6dL)rUtxmTsyG0*)TqJv2KRGzBc;fWp4Mehp&uj)*2l+yLwp5F^&}E z?=9LMLLOgOEyuwxyP-E=ZVi~3dG`v?SB9jqx_+%{WGYe*IG~I`J|vd5mrf%%?FSxn zjjVqvGm&a$s2&maRC+P}_0rW-J}gBUEJCr&Jg!UHS%O_Aof_l81XVG0v7DMdm`9 z7MXHTpvjk$lvG$ict)&xZSHx5@{k1^QBvD9MLeL=-I(wYxOJ$C+BY`gjtZuNeX{K570 ze)Mh=>flwXI$%@r0sG(bYhWPs9{7hoX#fup5ZZt9tD~url@Z;4_P-0TU=LgG4&CG8 zzw;eUpgs1RKKhoOTiaaC@Z*glk=RMbLW?!8B9DoHHSc9qLDCOxjv2iXR)t5D4#gET z&Ph-9S?BaqcDUa^JeqH>;&qH6Zxf6}ut^|%tQu~HFLX?fJ$QU)YwLj=zQ9K=o)9e; zS1`T3#Xt$c+xBvsy^$ASk2X#OJW$jR%z-mDVsA%}gGO{D+@Egwx!}q=Zr?#sfH50> zko_6f^>A1=me_zz#Et_|p6OFPdb}7{c%aVv>!{89^Wm0O0Jo@acs;f4Ao*|=!-eVj zx9j#u;0RCmLz3nz-?&~sp*M(L7n_iy90<`79!-qEE0x}Ppk0rG5SdxWaa(myFno#^ zTR%(;zv&$!AX6p~<7Wqp;1XALI0;~-7@W0^Uv{5XoNU|uELG>N&|oXrRWWKQCT7ZUY-B3xpD^eoYJZ6pRoF7yDwbWg6k3O5iJ%Pi$W?TJTGUdk zw#xyDM4hzOoW96^DAs_mnJaFl1}NF;Z5J=Z(~kGZ(bU<{@nf5+HtEfuk$2>JO1XUZ zm4?X4!()abPcXkn-8V1Q9^^xd{_QZ3Bfqh7h?QdGT2kSr^?OHDUh$H&9SC%Rx)(qW zGdyUuD358KIg%1iKHgG3p|Hhowa#9aJm`BBxs-(hEsRLRGcJIH_BSlC~?JM)49ZVZyLnpgqM^V=QPu~ zMwHyH_n`pSsG^iu#g<~h72fHUjoJfxE^!6>!6$Cf-eWh2TlAhF6s)Ktg8(LeqT)&J zggzIyNCOnfk4i*ovG~Yon<959VW$XS^J}LWTOa!Ca^g% zNykPB6Krg}887hT)8k(=Zw@G^|=H_k;dJ~Z8GW<)VGR?GpO*ZTaO6=sgk)1?Z#0{aFuoPLKsJ zK83=1He_ddb9}Gy;d8^>ytU2ZKu2fF&tCUnWYW3Pt&*9DFy_bvaL&C@|c ze?otpJT~O;L%V0J=;DnlPr7S|lGKd%V4Up#JDDdZ_ z1&{iAQ@z~G%Mdr{btMBfMs;|N(}=!C(TnZ$Z{r8iwE2BA+1J^zcjgv^QSIJ><94K{ zF&`-msvd%7!^2;n&$9F|txoACasw{QspSy$^~!XXX#*!~O`px{(Iil0f))8mg554d znD|4ev`P?~47I5Rs6vl&NM;;g(FkPDJ_K~WE555)nn6H}X@ELjNfVQ7T8WABbh`lz zYC96bRa4O*OBUM(k)K;|w-~7y>cwK50F+7=;*wRtss@gt+?p9C{#U+rw%>eDVFT_d z#QlhBBu01RGdR(4i&@-^bb5ZzVXt2W)iWOkfo;)F)fK&t?=i}8J)z_GAYB5hU9QMV zbT_%@;uIwa0qyyP%zTV(ZSvivi-@rouG`IMCMA~aB5E97j=+n-Jb;HQz#?-=a z2BpHoz`*f^ZJ$TjDa3F6k{{w9;;?@)?Im0 zw(UE1_z9uH51lRD%d6vt)0+4tnu)6C4I{hg`{9OB%*wQr>Vx^KgOnSrxC6`bN-2qg z7Ldh~5ZluxOsAap9vnYIGS%N3X?VxZR3v9Hm}=PZf469uVm2$alEk*DAt=K_Np~lw zv@M2>fYU&ynM42#KD6^)GcVgGxYqzTwNXnEBpHsAq@8R`+*UD^xBi%|pkd*hscW<0 z4X94xoc4$7QtBt@<^D`wEy|Ekv|Cjb%r{n*Pn zFZj!!_~Y4ZA3id`pvBH{Rgw#<$3o&VHj4y<&<|Qz}~%X$4C)$9j51 zBb8~UQTc7@Of@om`^WP%MR1SiR;7LSrYIHeGtYGRrb5xZy0Vn4Dvb`(v^em^)2*+W zN0iZ2Y0_H|;pJ-4`xQ4<{gzkz9!5WC==|wCow-4+5wXxT@n9`s>akt(c3X4F)kv>O z%WUmxo4+rzrd+07N>kKJ1GVIumO{6U7D6Qpa?b~!CgeCSO(&uZJ{XQ$f={7*?$@4+ zvWOey(%^&TH^SWiY0M%$wZz&Ppz@PaT!TevHM)IC*EQN=b?ZxHHFfNDF%{oe7*J6QT4bO$QJ zi_R6U_bQJvrz*AA+WkqZ_m6wYm3L3l^n}38a5A^4n)B$p&?u`JL+oDf7%Xpg4fo8j z-DC}qokxoH72Z?2eWw?@q~lJ=;yicmy6I;1@%>j2}#2k5o1UYcXPZS0?hA;#;CGiXJ$h(qRFLXP8# z?=i>*z+43ZnM(dYI3MUPZ2}tVZhu@T+Gn3Jg*0}0WJHlML-aO^b!}DAFlpR^kY@vQ z7IN!ZykzR{yO3zsrxLqraC%`+PO4wULnP$~TfKaMg|^Z1p0pB(?HGD>aCc^60wM7O zfS{VLxG{a$arlCQ%i0f`R}jtB*(?_(mDV@#s^oXKQ?U>#oZHkFUth0nw-zT5Cg)sD z8+M0H+;mM}uXXr&7pU-yJ6dxM=S2%2^Qijf7Ztw{2jr$%^J}W&w@kN0;?=t$5oR!yyPFNn&bzcSODRVCj8UcLODDZ@$0Qeg3th z7l?EF2^t;<=uGDK!1+U?Th`vj*5M;+cCym9)-$uDHL@~3)6}p#2n)A&&ir~ ziCnwO>1CC4x1GB&XAiG3Ny+qL+rGhRk1;n^>76foD^qxLYlU^JpuO_@r?tD+H-bXJ#B`ovAoQgX2i5+DbkUDXY!Kj#C#r+VmdG(Lksl_ z0#@&^Jpp`@f#>&1`0q6#A;gTSuJrNg;3jqn&TZ2q-S~_+R(~V3AJnKQau(1z4C0PY zO;AKOqqT$?^5cQsbzCiWHODJx$nVT#YK+-Av%v-+EpWNj_MnG}%WYjfH4(2FI~;rX zn0;}5>aNDOa-%QjQ7n+Fs90~vWUB^#J4hA8o;X{+x&alJy)9VT@aO{@m~C=vEeVP*w`Jh2`2INdwSM+xBK!9$;moMuuR|GBOC@yz*a zKer}Y@Pwv_@+9G>1U@dbU&DI_1y{4bQsfsvnsSa`XrCJ{Jk>Ar7ng3-P$<@$JC_gf z)7RMFQbrg0PqFs~`G?@$4)KQ`IM;r0Oq(kDVz~9A#rw&~>W-ewoI2xJaTGG??icBM z%rY2iv51u2sJ>2wpKPkkQN|FgT2-4aB-IR^)0Kkw`2roEm>LO`9^)g|gYDkd)oOBC zi*Q*&hTNSPT6@f*+G5tT<0?S>P_D9ysnOJgkQfoIh8zX>`odT#Aw2a|CKVdlhCMt+ z!G|Dbb7zh?4St{K*eb5Gxw$sLM~;u5*q-JY)Sc5!m1Gb#=Q6tO2l1;0x^`YRXS-5ivqWM&`SDy0*hVcEq~v`BIF}M;I%jX+}u!NI^6T=z4jX zc%yIH^1#wDqB^1+d0`+o@i`IBGD`Sw_jg)ad=>1~BWNmnFYa* zjmspNk<+vQxN#6@P3NGdpUlaYk8?q^z}Eo1{n z(YFq35C(2fx@Z#b4)Cx)?GU^!T-j?>#Bk`UdexL_@8fQ&+Uv1l4>=9rqnid++t@oM zfv|Z3jtZQ^-M+8Lxzmq2k(v$JcZ;xG!erO0Qrv-7`6BdK0hxZM#xcd`R4Bpxgsp>t z1OL5fGGatNgc==IW796;9`gzk3PExTu0yk4NAY?c!dEq>(7Dpc3)H}h8LJkwhfT&^ zr)D>W0MB++$)o^~vDfzTsF(xgG-^3}+FSxB-q^uvvW{wTT|+ zJfFv+3T`z`bAdh^#@+N5_9gFbz|=aEq#c&F#FXhAJEy2N-jz635|bJiHXRy_6mM60 ztQ8KHAL-G~o-*BoKmh_K!;~qeNKs-Y1w_cMOkKO_x-*=x#-DFpgy?ow5iKZApH=v9Xg25oT79U9 zA`y=fk+hH+`8#Q--L29-D<$d%yscsxz0@4sA`Dd1F84WQEld~q%S{uTp8Z}0IQZ$B z$)ioECK#0xFU^D)0dR*)%BC0PwEO7S=YA_AgWYe-et-=%T9z;V;TM7Wke`mU3&-Y2usnX4ZBEp1s~h5D5UbF@ZJ@^v@RA zxrsDgbJgcNOkd2IAgmKLDiiAfmXFoTd7=&l=XWBOpVM-3O+9^~Z)uHgnwBOn=oK$b zLIe!CrYx`wotf#cOKz_;zKAGVeil0Jw2P#8HpdR9=3Gb-U#tO?3(ITPGIrpOYV3{e z#@)15NU1Dodr58PNl*d|!rC+EegNb%qZk392z6n4eVmMd@AU=StEtM5=19)HvPo(I zCW{9R`B`Q>?euj0u?GFSAdxy>9!}RCQx|-smEI64p5x!xPa9M_)>ot!OA^QhREKzZ z@N~oR)z&wVcABsTn%haLTwZ>79Nu-=^sG2EUll1L9$!aHDGJ;2_x3Opd{d)^h@TdZ zQ$}$3dIUKkTy1i3v19VoIIRDleTt4U%fr%7fIL-bou4vHc0JEo&B=OQ>j(d1mjNVO`uwo%muaNA2E77EKqVS8 zt$(X`QK~s^Y71wdkmVtF?=<-v7$91W78f)X7N2@dZATj+cM@Li&cGP3aDa2?m@3cz z!DGBPlDDoU+pS{lZ8d#6u-ow5=t>u{}H)q|5mvCMxH%^vOob3WU?Xe1Hz_96Em zW7;sU9xH-*n6=fzBy^Y%Mhr%b%HCgHwoLliTj~zlcYmc<=?i+T6S}>uYuSr^&s5qY ztn`vg&ZFId4*lN$QcxVx1hPYZ>D>(vyO60<;xKcFf~N?wZ>Pt7hzc9(E<1F7X{UO<{P4X z_`}3wI)EzFUte>D7H2K*EyjHjQ{R0M!Y;*4U7>14v#xz_&2PO=?|?t;+4d=|E?wr1 z9^nNV!OuZcl^=y-Pv6{({bpF;0?T3fr8~FncHq)G$dNx8=2DCjMO~d-HfJ=iX>RZc zfdVULDuvUj3JLf#HUkt(=M_nO&7nOe^m7z<(u6h4#6cEqiPXO9&Q|@sDvj`nWgjNX zBRwG(Qg1IJEddo{Q;sPux@_^B;>CFn0-)3y(WOa5-e35tEUE?G{9U;8O=A6xPR}8W zB`f2c=+|jRNiv7L24v^204Dw)4$XIusyMycBZBz&Ge5cI{l;d=WCRjO4&Y53mEKe* zitB7OJ-s$ARMk&MFR)UVMEaAuNwDrrLfRc!kVlNixIw=Yy%admE#j zK*KucItAy2M5j(%&@+T6LSuDvDmuY36g`qb*lVOp)6 zaP`9tMmT;l+|JJMSfXgFhmv6eQz?uh%!_5`lXFP>49SmijkZ)U%7LIVXdAfG)?6^@ zF=Z`ybY`$s=NcoC`ZQSbQE==*CPR(y9R&q->&u66#}EM99?tp1=i8T94($0Q7-F5y zYQ?08&GQ__Ye~|>uumcWx2|`ee=s87O{F>pPpENs3+cDnGPwyQMR?Al($Uy|POkrF zkXY~9Nv)hL**E!Q1H)kGJw?Ie2u#T>@>A`5S1ancbN1VuX3g}~vb)W?33xjl{I>&t zykA{|XKQZ=|167i6dEA0caIJvs8bj6+$b>U&47F*JsXfJf|gD`v;Q=oc(gso-)KI2QEd%~0#@^^o4$>1pUv zV0QBHrEFg_baJYGtdzlKPf$xw3%PP7GdQ;bksp!|pR1uKGb(~M0Nw)Y!R^T6rzHWe zp#Gx0%B3o8AmjOlj3)gkS}=FhL@}0DTh;JetEeRdG+ERlf_k1^Jdl zrNohz=-kqCT|jjb91CA=z0a5>*)R0g#(;CS3Azd|3vHyzOKzS+&qL0`R!OZ+r3zgb zJ~I7>eRn z$Y$gVWqb?YVt*RR1Ea2fYM`E1TwTdmM;7Ay%nV-4rv@Z6b9j>Be@2-GrWi@p+y5~z z4oxmElz|_w@Z=6|%croeqskH`&!<~13l1Jm|E^|zvBv7{C82AU6CSQumsb>eyWX(y zR{Ju?<}v+id&slahU?cl8xOtC>w@Q7%l$>klkUrA!H0(E`J*AFfS6S$%T=MjX6YP1 z1Go`pD3&55HHm2sSfUL@PmLjMJFM$=IV<+%U3|sCMn`k=DwnJM+vu0eOw5uzV3O3y zar#*D#1ujrk(G!>1uoeJ;_*VFyE{ip72T(yJ4rt=2i7C@;0VodVFqZ4zUrwW`~fo7JkUfv!dOmChM9U!?73SL)hYrnL8 zZpT!YAB^<0c%P;Px^0HK3#mwy$C^F&AjS(!vr;GaZG^8R!D+*d_DFG~_Lnf9N#ie7 zBL|jnHl0S`BMtWmIr^{NqO*($86|^b#%6FP_tZ`=sgSQt&(xWM5!%`P;J381uE?M7 z{OMp@z(5Y`;vPuQE;~fg@~!w;Y^%0J&;q|GXTgAKDBhzOR}jc5RIgFx8NZ zbOB&(d?oX&CQ|mspGq%^k^TM(*|PZU)BKQW&i6M?=%ZwCF3fz;TqUU9qfst76=cYk&*jnc;3>>Bqf3VO`7`WPxDr+dNB-x231^P8# z6_I*4R(qLm4~Y!+mr!By3otzmN+i(1;2&HtK^#l3&GyCoaQaFeD#$uH~3$*0VhMN zoN-_6Lu@qQLsWK|S@Eb7w1i>DhZ--In|2Msqc#^J+85rmb~m!TXCSW{${>3U3r_=l z;-1k&;zQ6#O5RrJ{VqP_82Fxh%>i6Z1y85lo^9sVZ_^U?Y_aK$@-EJ4lgjB;XQ{Kf z)cy&#;Hb2{FSh#vzN<5VCLP;Ep34ubab%|AkI21Hmm49aI*H-)7I3JU*z2* zxE~)9(7DIapT6kKwd>%RrRO$R?8-4pSP+^|fNP8U*lYvtOPFKnB^0Eka;8DmQb?8w zlnLXZtaRGdA{oEYNfP((M2fkSV(+YvkvAC7--r~D!2cazGqABY`mK7z&jSM{&jI@Q z%m3S3WSoxm96jQ|W^9w--edW#Nv`gBAp*b)L@^AY=*9TJWRbR%#4XXH>5FX(R5`_Y zk2A(0)k5{LB%aAZx{h(j*vYSs%i2rCJUi#{5kU9X>XDCSl9*rGn#VS)sq^a9ohN$I z&IxL75B4JxZv>iwRJz?DP4SI|YURi~jWvWm9_KcPX^(`FSEbx&nUct-uV0gJBGcGX zl50EQ&Y43Z;RSNwMP5dhFT*@`$&~=AU~`-b=pPyd3}-h$=EnF~i6#;m+1Os16&2aV zEUQQLt9jv#Q{x2dG0d64rgiQu;%)edmmLh?w#j#;B`Sl!IZu`Uyp7kkd%VXc$C-8D z14re{gBjoLy*#^KoSb9XhqvDB_O|60IpGQ`RuWX~cFp%;vzI~2zcB18)1~&eF3nRv z03@o+7ZwA&NRv!hq1>qRuH!X!6>}I@bRw;#66x;yTgORWKH10@z#Ww_Ru`?dFu#kG zvl)G1=|2+L?InGz;ww~ofY$v)#NV*KmEEXbPv79dqcQ;K4 zZDLmw7+1>BB&4kNA#l2S_( z9?$7u5n04=|Ji=MhWZauehDe*`U!9o_=RS&}Jj{FvLr!L+sTf zuk6SJ^71WuP-_&sYS(D5yf^JCV*<>JLv*R1B{?#x%q`+LCK{ZA-`ZM8feEFQLtdm_ zYS{AHTYv*PSy(hTZLBYOyx*?RXJ22c-hBWCHJC`3JUF~a@X1UPhht5=P1Bn$-;)+x zrzbYe3f*mU?sqPp*I$znViFcV)CxAOGrtahrH8DaRz*!4hD%yG5*S*1kQX^AcQ--s z;HHl2562X}0=2x>^2^v2mx?AXYT6tL?QN4fckMTX7*5(@kmc?(x-BrXnS#5XyR^|D z>b|(0(fQZ%B80TIWARa5tUnq7|0cWd1BNZSgWT{`43Fml6+OL@z}t zjsk%oz>(w4#s;{~^bx~Za%s#W($yZ;Rh%#oIIWC-boku-$|7^aj@&S_I;;5>Sckau zp>sF}t&P+O*Db-q9MbiT(!@yHAz0A13n?=1rB#>0H6Vyk6-(WJ81XE@+;cRpAAVNz z;V$J^tRH+lk+y$fM+pF(ag|q)LIK0iRvoTPDLsBb_-VrZP5V{3cCUXrQ`0C!cs0u3 z<~l%54lsh~N&G4ncc-^0RKm(&Wf~(jd5tT{7Kj)%ZFtguOh%u!`xOLYv?RJio5p0A z!+UI|2#`?_;hM!`aov7%dLTvl8GPZsHQ?>@rQ=UKi4*(j88|d#KZl@E#*C&|I6Tcz z%hg+HG)J;(>p&Nut|F2rGWX8!lY2N+{NWCJaEDI$LJ=}{GV|jg2popOxLu~<~= z5r{&SP_bbduqtR0Q49d#7lY>gbWe#^765`PCMQtfFV`2f)V$mSnhyuWM% zAK&QM8(BKg{f8*~PeVWl4zn5t5KvVP5D?N|%z%J!KK;Qx{Ro_#tUk{FZzt9`3l@$0 zXp6`{&Wiar`+|=K?04>}m7as6k-Y=m|GfVj43zBf+VjVie*J(!{Tqxm@gFd@dL~8= zbpP4^8^n-_r7_A!61;wJXTSUnBAxsXh~E?9um1dNqR{nV3O9d1Abh}moc=G{3B?~! ze>n9!F-SHzg!)U&1!9GDM8{J-D<0qs-%f%vd>{kT0NA4&Z8 zL|>))1MQFN`~Cet9L#^-`-D)qFL`=EK;B?r|48&W#y?>GO!R++_$N>MPuENm{|@n| zU;hF6SHJ#Aef|?oKj{y&zYwAS>CZozsDGLxrT$_5PhRT(!uZFe{GS*S`9MJb?Lz!* zivFJ%{~Vh66CvUhx8*P9{}VF) o)B5jC=3lL=KPdVCO@cogP+19Zh~NH(^zoYbs0$Rke*5%)08dk|z5oCK literal 0 HcmV?d00001 diff --git a/build.mk b/build.mk new file mode 100644 index 0000000..dc2ae5e --- /dev/null +++ b/build.mk @@ -0,0 +1,5 @@ +SRC += . +SRC += ebtn + +INCLUDE += . +INCLUDE += ebtn diff --git a/code_format.py b/code_format.py new file mode 100644 index 0000000..48a9092 --- /dev/null +++ b/code_format.py @@ -0,0 +1,27 @@ +import os +import subprocess +import re +import sys + +def format_all_file(root): + for root, dirs, files in os.walk(root): + + # root 表示当前正在访问的文件夹路径 + # dirs 表示该文件夹下的子目录名list + # files 表示该文件夹下的文件list + + # 遍历文件 + for f in files: + if f.endswith('.c') or f.endswith('.h'): + full_path = os.path.join(root, f) + #print("root: %s, path: %s" % (root, full_path)) + print(full_path) + + command = 'clang-format -style=file -i %s' % full_path + #print(command) + proc = subprocess.run(command, shell=True, stdout=subprocess.PIPE) + +if __name__ == '__main__': + format_all_file('.') + + #proc = subprocess.run(command, shell=True, stdout=subprocess.PIPE) diff --git a/ebtn/bit_array.h b/ebtn/bit_array.h new file mode 100644 index 0000000..06070d3 --- /dev/null +++ b/ebtn/bit_array.h @@ -0,0 +1,597 @@ +#ifndef _BIT_ARRAY_H_ +#define _BIT_ARRAY_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +// #define BIT_ARRAY_CONFIG_64 + +// here can change to uint64_t, if you system is 64bit. +#ifdef BIT_ARRAY_CONFIG_64 +typedef uint64_t bit_array_t; +#define BIT_ARRAY_BIT(n) (1ULL << (n)) +#else +typedef uint32_t bit_array_t; +#define BIT_ARRAY_BIT(n) (1UL << (n)) +#endif +typedef bit_array_t bit_array_val_t; + +#define BIT_ARRAY_BITS (sizeof(bit_array_val_t) * 8) + +#define BIT_ARRAY_BIT_WORD(bit) ((bit) / BIT_ARRAY_BITS) +#define BIT_ARRAY_BIT_INDEX(bit) ((bit_array_val_t)(bit) & (BIT_ARRAY_BITS - 1U)) + +#define BIT_ARRAY_MASK(bit) BIT_ARRAY_BIT(BIT_ARRAY_BIT_INDEX(bit)) +#define BIT_ARRAY_ELEM(addr, bit) ((addr)[BIT_ARRAY_BIT_WORD(bit)]) + +// word of all 1s +#define BIT_ARRAY_WORD_MAX (~(bit_array_val_t)0) + +#define BIT_ARRAY_SUB_MASK(nbits) ((nbits) ? BIT_ARRAY_WORD_MAX >> (BIT_ARRAY_BITS - (nbits)) : (bit_array_val_t)0) + +// A possibly faster way to combine two words with a mask +// #define bitmask_merge(a,b,abits) ((a & abits) | (b & ~abits)) +#define bitmask_merge(a, b, abits) (b ^ ((a ^ b) & abits)) + +/** + * @brief This macro computes the number of bit array variables necessary to + * represent a bitmap with @a num_bits. + * + * @param num_bits Number of bits. + */ +#define BIT_ARRAY_BITMAP_SIZE(num_bits) (1 + ((num_bits)-1) / BIT_ARRAY_BITS) + +/** + * @brief Define an array of bit array variables. + * + * This macro defines an array of bit array variables containing at least + * @a num_bits bits. + * + * @note + * If used from file scope, the bits of the array are initialized to zero; + * if used from within a function, the bits are left uninitialized. + * + * @cond INTERNAL_HIDDEN + * @note + * This macro should be replicated in the PREDEFINED field of the documentation + * Doxyfile. + * @endcond + * + * @param name Name of array of bit array variables. + * @param num_bits Number of bits needed. + */ +#define BIT_ARRAY_DEFINE(name, num_bits) bit_array_t name[BIT_ARRAY_BITMAP_SIZE(num_bits)] + +#if 1 +// See http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel +static inline bit_array_val_t _windows_popcount(bit_array_val_t w) +{ + w = w - ((w >> 1) & (bit_array_val_t) ~(bit_array_val_t)0 / 3); + w = (w & (bit_array_val_t) ~(bit_array_val_t)0 / 15 * 3) + ((w >> 2) & (bit_array_val_t) ~(bit_array_val_t)0 / 15 * 3); + w = (w + (w >> 4)) & (bit_array_val_t) ~(bit_array_val_t)0 / 255 * 15; + return (bit_array_val_t)(w * ((bit_array_val_t) ~(bit_array_val_t)0 / 255)) >> (sizeof(bit_array_val_t) - 1) * 8; +} + +#define POPCOUNT(x) _windows_popcount(x) +#else +#define POPCOUNT(x) (unsigned)__builtin_popcountll(x) +#endif + +#define bits_in_top_word(nbits) ((nbits) ? BIT_ARRAY_BIT_INDEX((nbits)-1) + 1 : 0) + +static inline void _bit_array_mask_top_word(bit_array_t *target, int num_bits) +{ + // Mask top word + int num_of_words = BIT_ARRAY_BITMAP_SIZE(num_bits); + int bits_active = bits_in_top_word(num_bits); + target[num_of_words - 1] &= BIT_ARRAY_SUB_MASK(bits_active); +} + +/** + * @brief Bit Array test a bit. + * + * This routine tests whether bit number @a bit of @a target is set or not. + * + * @param target Address of bit array variable or array. + * @param bit Bit number (starting from 0). + * + * @return true if the bit was set, false if it wasn't. + */ +static inline int bit_array_get(const bit_array_t *target, int bit) +{ + bit_array_val_t val = BIT_ARRAY_ELEM(target, bit); + + return (1 & (val >> (bit & (BIT_ARRAY_BITS - 1)))) != 0; +} + +/** + * @brief Bit Array clear a bit. + * + * Bit Array clear bit number @a bit of @a target. + * + * @param target Address of bit array variable or array. + * @param bit Bit number (starting from 0). + */ +static inline void bit_array_clear(bit_array_t *target, int bit) +{ + bit_array_val_t mask = BIT_ARRAY_MASK(bit); + + BIT_ARRAY_ELEM(target, bit) &= ~mask; +} + +/** + * @brief Bit Array set a bit. + * + * Bit Array set bit number @a bit of @a target. + * + * @param target Address of bit array variable or array. + * @param bit Bit number (starting from 0). + */ +static inline void bit_array_set(bit_array_t *target, int bit) +{ + bit_array_val_t mask = BIT_ARRAY_MASK(bit); + + BIT_ARRAY_ELEM(target, bit) |= mask; +} + +/** + * @brief Bit Array toggle a bit. + * + * Bit Array toggle bit number @a bit of @a target. + * + * @param target Address of bit array variable or array. + * @param bit Bit number (starting from 0). + */ +static inline void bit_array_toggle(bit_array_t *target, int bit) +{ + bit_array_val_t mask = BIT_ARRAY_MASK(bit); + + BIT_ARRAY_ELEM(target, bit) ^= mask; +} + +/** + * @brief Bit Array set a bit to a given value. + * + * Bit Array set bit number @a bit of @a target to value @a val. + * + * @param target Address of bit array variable or array. + * @param bit Bit number (starting from 0). + * @param val true for 1, false for 0. + */ +static inline void bit_array_assign(bit_array_t *target, int bit, int val) +{ + bit_array_val_t mask = BIT_ARRAY_MASK(bit); + + if (val) + { + BIT_ARRAY_ELEM(target, bit) |= mask; + } + else + { + BIT_ARRAY_ELEM(target, bit) &= ~mask; + } +} + +static inline void bit_array_clear_all(bit_array_t *target, int num_bits) +{ + memset((void *)target, 0, BIT_ARRAY_BITMAP_SIZE(num_bits) * sizeof(bit_array_val_t)); +} + +static inline void bit_array_set_all(bit_array_t *target, int num_bits) +{ + memset((void *)target, 0xff, BIT_ARRAY_BITMAP_SIZE(num_bits) * sizeof(bit_array_val_t)); + _bit_array_mask_top_word(target, num_bits); +} + +static inline void bit_array_toggle_all(bit_array_t *target, int num_bits) +{ + for (int i = 0; i < BIT_ARRAY_BITMAP_SIZE(num_bits); i++) + { + target[i] ^= BIT_ARRAY_WORD_MAX; + } + _bit_array_mask_top_word(target, num_bits); +} + +// +// Strings and printing +// + +// Construct a BIT_ARRAY from a substring with given on and off characters. + +// From string method +static inline void bit_array_from_str(bit_array_t *bitarr, const char *str) +{ + int i, index; + int space = 0; + int len = strlen(str); + + for (i = 0; i < len; i++) + { + index = i - space; + if (strchr("1", str[i]) != NULL) + { + bit_array_set(bitarr, index); + } + else if (strchr("0", str[i]) != NULL) + { + bit_array_clear(bitarr, index); + } + else + { + // error. + space++; + } + } +} + +// Takes a char array to write to. `str` must be bitarr->num_of_bits+1 in length +// Terminates string with '\0' +static inline char *bit_array_to_str(const bit_array_t *bitarr, int num_bits, char *str) +{ + int i; + + for (i = 0; i < num_bits; i++) + { + str[i] = bit_array_get(bitarr, i) ? '1' : '0'; + } + + str[num_bits] = '\0'; + + return str; +} + +// Takes a char array to write to. `str` must be bitarr->num_of_bits+1 in length +// Terminates string with '\0' +static inline char *bit_array_to_str_8(const bit_array_t *bitarr, int num_bits, char *str) +{ + int i; + int space = 0; + + for (i = 0; i < num_bits; i++) + { + str[i + space] = bit_array_get(bitarr, i) ? '1' : '0'; + + if ((i + 1) % 8 == 0) + { + space++; + str[i + space] = ' '; + } + } + + str[num_bits + space] = '\0'; + + return str; +} + +// +// Get and set words (internal use only -- no bounds checking) +// + +static inline bit_array_val_t _bit_array_get_word(const bit_array_t *target, int num_bits, int start) +{ + int word_index = BIT_ARRAY_BIT_WORD(start); + int word_offset = BIT_ARRAY_BIT_INDEX(start); + + bit_array_val_t result = target[word_index] >> word_offset; + + int bits_taken = BIT_ARRAY_BITS - word_offset; + + // word_offset is now the number of bits we need from the next word + // Check the next word has at least some bits + if (word_offset > 0 && start + bits_taken < num_bits) + { + result |= target[word_index + 1] << (BIT_ARRAY_BITS - word_offset); + } + + return result; +} + +// Set 64 bits from a particular start position +// Doesn't extend bit array +static inline void _bit_array_set_word(bit_array_t *target, int num_bits, int start, bit_array_val_t word) +{ + int word_index = BIT_ARRAY_BIT_WORD(start); + int word_offset = BIT_ARRAY_BIT_INDEX(start); + + if (word_offset == 0) + { + target[word_index] = word; + } + else + { + target[word_index] = (word << word_offset) | (target[word_index] & BIT_ARRAY_SUB_MASK(word_offset)); + + if (word_index + 1 < BIT_ARRAY_BITMAP_SIZE(num_bits)) + { + target[word_index + 1] = (word >> (BIT_ARRAY_BITS - word_offset)) | (target[word_index + 1] & (BIT_ARRAY_WORD_MAX << word_offset)); + } + } + + // Mask top word + _bit_array_mask_top_word(target, num_bits); +} + +// +// Fill a region (internal use only) +// + +// FillAction is fill with 0 or 1 or toggle +typedef enum +{ + ZERO_REGION, + FILL_REGION, + SWAP_REGION +} FillAction; + +static inline void _bit_array_set_region(bit_array_t *target, int start, int length, FillAction action) +{ + if (length == 0) + return; + + int first_word = BIT_ARRAY_BIT_WORD(start); + int last_word = BIT_ARRAY_BIT_WORD(start + length - 1); + int foffset = BIT_ARRAY_BIT_INDEX(start); + int loffset = BIT_ARRAY_BIT_INDEX(start + length - 1); + + if (first_word == last_word) + { + bit_array_val_t mask = BIT_ARRAY_SUB_MASK(length) << foffset; + + switch (action) + { + case ZERO_REGION: + target[first_word] &= ~mask; + break; + case FILL_REGION: + target[first_word] |= mask; + break; + case SWAP_REGION: + target[first_word] ^= mask; + break; + } + } + else + { + // Set first word + switch (action) + { + case ZERO_REGION: + target[first_word] &= BIT_ARRAY_SUB_MASK(foffset); + break; + case FILL_REGION: + target[first_word] |= ~BIT_ARRAY_SUB_MASK(foffset); + break; + case SWAP_REGION: + target[first_word] ^= ~BIT_ARRAY_SUB_MASK(foffset); + break; + } + + int i; + + // Set whole words + switch (action) + { + case ZERO_REGION: + for (i = first_word + 1; i < last_word; i++) + target[i] = (bit_array_val_t)0; + break; + case FILL_REGION: + for (i = first_word + 1; i < last_word; i++) + target[i] = BIT_ARRAY_WORD_MAX; + break; + case SWAP_REGION: + for (i = first_word + 1; i < last_word; i++) + target[i] ^= BIT_ARRAY_WORD_MAX; + break; + } + + // Set last word + switch (action) + { + case ZERO_REGION: + target[last_word] &= ~BIT_ARRAY_SUB_MASK(loffset + 1); + break; + case FILL_REGION: + target[last_word] |= BIT_ARRAY_SUB_MASK(loffset + 1); + break; + case SWAP_REGION: + target[last_word] ^= BIT_ARRAY_SUB_MASK(loffset + 1); + break; + } + } +} + +// Get the number of bits set (hamming weight) +static inline int bit_array_num_bits_set(bit_array_t *target, int num_bits) +{ + int i; + + int num_of_bits_set = 0; + + for (i = 0; i < BIT_ARRAY_BITMAP_SIZE(num_bits); i++) + { + if (target[i] > 0) + { + num_of_bits_set += POPCOUNT(target[i]); + } + } + + return num_of_bits_set; +} + +// Get the number of bits not set (1 - hamming weight) +static inline int bit_array_num_bits_cleared(bit_array_t *target, int num_bits) +{ + return num_bits - bit_array_num_bits_set(target, num_bits); +} + +// Copy bits from one array to another +// Note: use MACRO bit_array_copy +// Destination and source can be the same bit_array and +// src/dst regions can overlap +static inline void bit_array_copy(bit_array_t *dst, int dstindx, const bit_array_t *src, int srcindx, int length, int src_num_bits, int dst_num_bits) +{ + // Num of full words to copy + int num_of_full_words = length / BIT_ARRAY_BITS; + int i; + + int bits_in_last_word = bits_in_top_word(length); + + if (dst == src && srcindx > dstindx) + { + // Work left to right + for (i = 0; i < num_of_full_words; i++) + { + bit_array_val_t word = _bit_array_get_word(src, src_num_bits, srcindx + i * BIT_ARRAY_BITS); + _bit_array_set_word(dst, dst_num_bits, dstindx + i * BIT_ARRAY_BITS, word); + } + + if (bits_in_last_word > 0) + { + bit_array_val_t src_word = _bit_array_get_word(src, src_num_bits, srcindx + i * BIT_ARRAY_BITS); + bit_array_val_t dst_word = _bit_array_get_word(dst, dst_num_bits, dstindx + i * BIT_ARRAY_BITS); + + bit_array_val_t mask = BIT_ARRAY_SUB_MASK(bits_in_last_word); + bit_array_val_t word = bitmask_merge(src_word, dst_word, mask); + + _bit_array_set_word(dst, dst_num_bits, dstindx + num_of_full_words * BIT_ARRAY_BITS, word); + } + } + else + { + // Work right to left + for (i = 0; i < num_of_full_words; i++) + { + bit_array_val_t word = _bit_array_get_word(src, src_num_bits, srcindx + length - (i + 1) * BIT_ARRAY_BITS); + _bit_array_set_word(dst, dst_num_bits, dstindx + length - (i + 1) * BIT_ARRAY_BITS, word); + } + + if (bits_in_last_word > 0) + { + bit_array_val_t src_word = _bit_array_get_word(src, src_num_bits, srcindx); + bit_array_val_t dst_word = _bit_array_get_word(dst, dst_num_bits, dstindx); + + bit_array_val_t mask = BIT_ARRAY_SUB_MASK(bits_in_last_word); + bit_array_val_t word = bitmask_merge(src_word, dst_word, mask); + _bit_array_set_word(dst, dst_num_bits, dstindx, word); + } + } + + _bit_array_mask_top_word(dst, dst_num_bits); +} + +// copy all of src to dst. dst is resized to match src. +static inline void bit_array_copy_all(bit_array_t *dst, const bit_array_t *src, int num_bits) +{ + for (int i = 0; i < BIT_ARRAY_BITMAP_SIZE(num_bits); i++) + { + dst[i] = src[i]; + } +} + +// +// Logic operators +// + +// Destination can be the same as one or both of the sources +static inline void bit_array_and(bit_array_t *dest, const bit_array_t *src1, const bit_array_t *src2, int num_bits) +{ + for (int i = 0; i < BIT_ARRAY_BITMAP_SIZE(num_bits); i++) + { + dest[i] = src1[i] & src2[i]; + } +} + +static inline void bit_array_or(bit_array_t *dest, const bit_array_t *src1, const bit_array_t *src2, int num_bits) +{ + for (int i = 0; i < BIT_ARRAY_BITMAP_SIZE(num_bits); i++) + { + dest[i] = src1[i] | src2[i]; + } +} + +static inline void bit_array_xor(bit_array_t *dest, const bit_array_t *src1, const bit_array_t *src2, int num_bits) +{ + for (int i = 0; i < BIT_ARRAY_BITMAP_SIZE(num_bits); i++) + { + dest[i] = src1[i] ^ src2[i]; + } +} + +static inline void bit_array_not(bit_array_t *dest, const bit_array_t *src, int num_bits) +{ + for (int i = 0; i < BIT_ARRAY_BITMAP_SIZE(num_bits); i++) + { + dest[i] = ~src[i]; + } +} + +// +// Shift array left/right. If fill is zero, filled with 0, otherwise 1 +// + +// Shift towards LSB / lower index +static inline void bit_array_shift_right(bit_array_t *target, int num_bits, int shift_dist, int fill) +{ + if (shift_dist >= num_bits) + { + fill ? bit_array_set_all(target, num_bits) : bit_array_clear_all(target, num_bits); + return; + } + else if (shift_dist == 0) + { + return; + } + + FillAction action = fill ? FILL_REGION : ZERO_REGION; + + int cpy_length = num_bits - shift_dist; + bit_array_copy(target, 0, target, shift_dist, cpy_length, num_bits, num_bits); + + _bit_array_set_region(target, cpy_length, shift_dist, action); +} + +// Shift towards MSB / higher index +static inline void bit_array_shift_left(bit_array_t *target, int num_bits, int shift_dist, int fill) +{ + if (shift_dist >= num_bits) + { + fill ? bit_array_set_all(target, num_bits) : bit_array_clear_all(target, num_bits); + return; + } + else if (shift_dist == 0) + { + return; + } + + FillAction action = fill ? FILL_REGION : ZERO_REGION; + + int cpy_length = num_bits - shift_dist; + bit_array_copy(target, shift_dist, target, 0, cpy_length, num_bits, num_bits); + _bit_array_set_region(target, 0, shift_dist, action); +} + +// +// Comparisons +// + +// Compare two bit arrays by value stored, with index 0 being the Least +// Significant Bit (LSB). Arrays must have the same length. +// returns: +// >0 iff bitarr1 > bitarr2 +// 0 iff bitarr1 == bitarr2 +// <0 iff bitarr1 < bitarr2 +static inline int bit_array_cmp(const bit_array_t *bitarr1, const bit_array_t *bitarr2, int num_bits) +{ + return memcmp(bitarr1, bitarr2, BIT_ARRAY_BITMAP_SIZE(num_bits) * sizeof(bit_array_val_t)); +} + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* _BIT_ARRAY_H_ */ diff --git a/ebtn/ebtn.c b/ebtn/ebtn.c new file mode 100644 index 0000000..302d2a0 --- /dev/null +++ b/ebtn/ebtn.c @@ -0,0 +1,550 @@ +#include +#include "ebtn.h" + +#define EBTN_FLAG_ONPRESS_SENT ((uint16_t)0x0001) /*!< Flag indicates that on-press event has been sent */ +#define EBTN_FLAG_IN_PROCESS ((uint16_t)0x0002) /*!< Flag indicates that button in process */ + +/* Default button group instance */ +static ebtn_t ebtn_default; + +/** + * \brief Process the button information and state + * + * \param[in] btn: Button instance to process + * \param[in] old_state: old state + * \param[in] new_state: new state + * \param[in] mstime: Current milliseconds system time + */ +static void prv_process_btn(ebtn_btn_t *btn, uint8_t old_state, uint8_t new_state, ebtn_time_t mstime) +{ + ebtn_t *ebtobj = &ebtn_default; + + /* Check params set or not. */ + if (btn->param == NULL) + { + return; + } + + /* Button state has just changed */ + if (new_state != old_state) + { + btn->time_state_change = mstime; + + if (new_state) + { + btn->flags |= EBTN_FLAG_IN_PROCESS; + } + } + /* Button is still pressed */ + if (new_state) + { + /* + * Handle debounce and send on-press event + * + * This is when we detect valid press + */ + if (!(btn->flags & EBTN_FLAG_ONPRESS_SENT)) + { + /* + * Run if statement when: + * + * - Runtime mode is enabled -> user sets its own config for debounce + * - Config debounce time for press is more than `0` + */ + if (ebtn_timer_sub(mstime, btn->time_state_change) >= btn->param->time_debounce) + { + /* + * Check mutlti click limit reach or not. + */ + if ((btn->click_cnt > 0) && (ebtn_timer_sub(mstime, btn->click_last_time) >= btn->param->time_click_multi_max)) + { + ebtobj->evt_fn(btn, EBTN_EVT_ONCLICK); + btn->click_cnt = 0; + } + + /* Start with new on-press */ + btn->flags |= EBTN_FLAG_ONPRESS_SENT; + ebtobj->evt_fn(btn, EBTN_EVT_ONPRESS); + + /* Set keep alive time */ + btn->keepalive_last_time = mstime; + btn->keepalive_cnt = 0; + + btn->time_change = mstime; /* Button state has now changed */ + } + } + + /* + * Handle keep alive, but only if on-press event has been sent + * + * Keep alive is sent when valid press is being detected + */ + else + { + while ((btn->param->time_keepalive_period > 0) && (ebtn_timer_sub(mstime, btn->keepalive_last_time) >= btn->param->time_keepalive_period)) + { + btn->keepalive_last_time += btn->param->time_keepalive_period; + ++btn->keepalive_cnt; + ebtobj->evt_fn(btn, EBTN_EVT_KEEPALIVE); + } + + // Scene1: multi click end with a long press, need send onclick event. + if ((btn->click_cnt > 0) && (ebtn_timer_sub(mstime, btn->time_change) > btn->param->time_click_pressed_max)) + { + ebtobj->evt_fn(btn, EBTN_EVT_ONCLICK); + + btn->click_cnt = 0; + } + } + } + /* Button is still released */ + else + { + /* + * We only need to react if on-press event has even been started. + * + * Do nothing if that was not the case + */ + if (btn->flags & EBTN_FLAG_ONPRESS_SENT) + { + /* + * Run if statement when: + * + * - Runtime mode is enabled -> user sets its own config for debounce + * - Config debounce time for release is more than `0` + */ + if (ebtn_timer_sub(mstime, btn->time_state_change) >= btn->param->time_debounce_release) + { + /* Handle on-release event */ + btn->flags &= ~EBTN_FLAG_ONPRESS_SENT; + ebtobj->evt_fn(btn, EBTN_EVT_ONRELEASE); + + /* Check time validity for click event */ + if (ebtn_timer_sub(mstime, btn->time_change) >= btn->param->time_click_pressed_min && + ebtn_timer_sub(mstime, btn->time_change) <= btn->param->time_click_pressed_max) + { + ++btn->click_cnt; + + btn->click_last_time = mstime; + } + else + { + // Scene2: If last press was too short, and previous sequence of clicks was + // positive, send event to user. + if ((btn->click_cnt > 0) && (ebtn_timer_sub(mstime, btn->time_change) < btn->param->time_click_pressed_min)) + { + ebtobj->evt_fn(btn, EBTN_EVT_ONCLICK); + } + /* + * There was an on-release event, but timing + * for click event detection is outside allowed window. + * + * Reset clicks counter -> not valid sequence for click event. + */ + btn->click_cnt = 0; + } + + // Scene3: this part will send on-click event immediately after release event, if + // maximum number of consecutive clicks has been reached. + if ((btn->click_cnt > 0) && (btn->click_cnt == btn->param->max_consecutive)) + { + ebtobj->evt_fn(btn, EBTN_EVT_ONCLICK); + btn->click_cnt = 0; + } + + btn->time_change = mstime; /* Button state has now changed */ + } + } + else + { + /* + * Based on te configuration, this part of the code + * will send on-click event after certain timeout. + * + * This feature is useful if users prefers multi-click feature + * that is reported only after last click event happened, + * including number of clicks made by user + */ + if (btn->click_cnt > 0) + { + if (ebtn_timer_sub(mstime, btn->click_last_time) >= btn->param->time_click_multi_max) + { + ebtobj->evt_fn(btn, EBTN_EVT_ONCLICK); + btn->click_cnt = 0; + } + } + else + { + // check button in process + if (btn->flags & EBTN_FLAG_IN_PROCESS) + { + btn->flags &= ~EBTN_FLAG_IN_PROCESS; + } + } + } + } +} + +int ebtn_init(ebtn_btn_t *btns, uint16_t btns_cnt, ebtn_btn_combo_t *btns_combo, uint16_t btns_combo_cnt, ebtn_get_state_fn get_state_fn, ebtn_evt_fn evt_fn) +{ + ebtn_t *ebtobj = &ebtn_default; + + if (evt_fn == NULL || get_state_fn == NULL /* Parameter is a must only in callback-only mode */ + ) + { + return 0; + } + + memset(ebtobj, 0x00, sizeof(*ebtobj)); + ebtobj->btns = btns; + ebtobj->btns_cnt = btns_cnt; + ebtobj->btns_combo = btns_combo; + ebtobj->btns_combo_cnt = btns_combo_cnt; + ebtobj->evt_fn = evt_fn; + ebtobj->get_state_fn = get_state_fn; + + return 1; +} + +/** + * \brief Get all button state with get_state_fn. + * + * \param[out] state_array: store the button state + */ +static void ebtn_get_current_state(bit_array_t *state_array) +{ + ebtn_t *ebtobj = &ebtn_default; + ebtn_btn_dyn_t *target; + int i; + + /* Process all buttons */ + for (i = 0; i < ebtobj->btns_cnt; ++i) + { + /* Get button state */ + uint8_t new_state = ebtobj->get_state_fn(&ebtobj->btns[i]); + // save state + bit_array_assign(state_array, i, new_state); + } + + for (target = ebtobj->btn_dyn_head, i = ebtobj->btns_cnt; target; target = target->next, i++) + { + /* Get button state */ + uint8_t new_state = ebtobj->get_state_fn(&target->btn); + + // save state + bit_array_assign(state_array, i, new_state); + } +} + +/** + * \brief Process the button state + * + * \param[in] btn: Button instance to process + * \param[in] old_state: all button old state + * \param[in] curr_state: all button current state + * \param[in] idx: Button internal key_idx + * \param[in] mstime: Current milliseconds system time + */ +static void ebtn_process_btn(ebtn_btn_t *btn, bit_array_t *old_state, bit_array_t *curr_state, int idx, ebtn_time_t mstime) +{ + prv_process_btn(btn, bit_array_get(old_state, idx), bit_array_get(curr_state, idx), mstime); +} + +/** + * \brief Process the combo-button state + * + * \param[in] btn: Button instance to process + * \param[in] old_state: all button old state + * \param[in] curr_state: all button current state + * \param[in] comb_key: Combo key + * \param[in] mstime: Current milliseconds system time + */ +static void ebtn_process_btn_combo(ebtn_btn_t *btn, bit_array_t *old_state, bit_array_t *curr_state, bit_array_t *comb_key, ebtn_time_t mstime) +{ + BIT_ARRAY_DEFINE(tmp_data, EBTN_MAX_KEYNUM) = {0}; + + if (bit_array_num_bits_set(comb_key, EBTN_MAX_KEYNUM) == 0) + { + return; + } + bit_array_and(tmp_data, curr_state, comb_key, EBTN_MAX_KEYNUM); + uint8_t curr = bit_array_cmp(tmp_data, comb_key, EBTN_MAX_KEYNUM) == 0; + + bit_array_and(tmp_data, old_state, comb_key, EBTN_MAX_KEYNUM); + uint8_t old = bit_array_cmp(tmp_data, comb_key, EBTN_MAX_KEYNUM) == 0; + + prv_process_btn(btn, old, curr, mstime); +} + +void ebtn_process_with_curr_state(bit_array_t *curr_state, ebtn_time_t mstime) +{ + ebtn_t *ebtobj = &ebtn_default; + ebtn_btn_dyn_t *target; + ebtn_btn_combo_dyn_t *target_combo; + int i; + + /* Process all buttons */ + for (i = 0; i < ebtobj->btns_cnt; ++i) + { + ebtn_process_btn(&ebtobj->btns[i], ebtobj->old_state, curr_state, i, mstime); + } + + for (target = ebtobj->btn_dyn_head, i = ebtobj->btns_cnt; target; target = target->next, i++) + { + ebtn_process_btn(&target->btn, ebtobj->old_state, curr_state, i, mstime); + } + + /* Process all comb buttons */ + for (i = 0; i < ebtobj->btns_combo_cnt; ++i) + { + ebtn_process_btn_combo(&ebtobj->btns_combo[i].btn, ebtobj->old_state, curr_state, ebtobj->btns_combo[i].comb_key, mstime); + } + + for (target_combo = ebtobj->btn_combo_dyn_head; target_combo; target_combo = target_combo->next) + { + ebtn_process_btn_combo(&target_combo->btn.btn, ebtobj->old_state, curr_state, target_combo->btn.comb_key, mstime); + } + + bit_array_copy_all(ebtobj->old_state, curr_state, EBTN_MAX_KEYNUM); +} + +void ebtn_process(ebtn_time_t mstime) +{ + BIT_ARRAY_DEFINE(curr_state, EBTN_MAX_KEYNUM) = {0}; + + // Get Current State + ebtn_get_current_state(curr_state); + + ebtn_process_with_curr_state(curr_state, mstime); +} + +int ebtn_get_total_btn_cnt(void) +{ + ebtn_t *ebtobj = &ebtn_default; + int total_cnt = 0; + ebtn_btn_dyn_t *curr = ebtobj->btn_dyn_head; + + total_cnt += ebtobj->btns_cnt; + + while (curr) + { + total_cnt++; + curr = curr->next; + } + return total_cnt; +} + +int ebtn_get_btn_index_by_key_id(uint16_t key_id) +{ + ebtn_t *ebtobj = &ebtn_default; + int i = 0; + ebtn_btn_dyn_t *target; + + for (i = 0; i < ebtobj->btns_cnt; ++i) + { + if (ebtobj->btns[i].key_id == key_id) + { + return i; + } + } + + for (target = ebtobj->btn_dyn_head, i = ebtobj->btns_cnt; target; target = target->next, i++) + { + if (target->btn.key_id == key_id) + { + return i; + } + } + + return -1; +} + +ebtn_btn_t *ebtn_get_btn_by_key_id(uint16_t key_id) +{ + ebtn_t *ebtobj = &ebtn_default; + int i = 0; + ebtn_btn_dyn_t *target; + + for (i = 0; i < ebtobj->btns_cnt; ++i) + { + if (ebtobj->btns[i].key_id == key_id) + { + return &ebtobj->btns[i]; + } + } + + for (target = ebtobj->btn_dyn_head, i = ebtobj->btns_cnt; target; target = target->next, i++) + { + if (target->btn.key_id == key_id) + { + return &target->btn; + } + } + + return NULL; +} + +int ebtn_get_btn_index_by_btn(ebtn_btn_t *btn) +{ + return ebtn_get_btn_index_by_key_id(btn->key_id); +} + +int ebtn_get_btn_index_by_btn_dyn(ebtn_btn_dyn_t *btn) +{ + return ebtn_get_btn_index_by_key_id(btn->btn.key_id); +} + +void ebtn_combo_btn_add_btn_by_idx(ebtn_btn_combo_t *btn, int idx) +{ + bit_array_set(btn->comb_key, idx); +} + +void ebtn_combo_btn_remove_btn_by_idx(ebtn_btn_combo_t *btn, int idx) +{ + bit_array_clear(btn->comb_key, idx); +} + +void ebtn_combo_btn_add_btn(ebtn_btn_combo_t *btn, uint16_t key_id) +{ + int idx = ebtn_get_btn_index_by_key_id(key_id); + if (idx < 0) + { + return; + } + ebtn_combo_btn_add_btn_by_idx(btn, idx); +} + +void ebtn_combo_btn_remove_btn(ebtn_btn_combo_t *btn, uint16_t key_id) +{ + int idx = ebtn_get_btn_index_by_key_id(key_id); + if (idx < 0) + { + return; + } + ebtn_combo_btn_remove_btn_by_idx(btn, idx); +} + +int ebtn_is_btn_active(const ebtn_btn_t *btn) +{ + return btn != NULL && (btn->flags & EBTN_FLAG_ONPRESS_SENT); +} + +int ebtn_is_btn_in_process(const ebtn_btn_t *btn) +{ + return btn != NULL && (btn->flags & EBTN_FLAG_IN_PROCESS); +} + +int ebtn_is_in_process(void) +{ + ebtn_t *ebtobj = &ebtn_default; + ebtn_btn_dyn_t *target; + ebtn_btn_combo_dyn_t *target_combo; + int i; + + /* Process all buttons */ + for (i = 0; i < ebtobj->btns_cnt; ++i) + { + if (ebtn_is_btn_in_process(&ebtobj->btns[i])) + { + return 1; + } + } + + for (target = ebtobj->btn_dyn_head, i = ebtobj->btns_cnt; target; target = target->next, i++) + { + if (ebtn_is_btn_in_process(&target->btn)) + { + return 1; + } + } + + /* Process all comb buttons */ + for (i = 0; i < ebtobj->btns_combo_cnt; ++i) + { + if (ebtn_is_btn_in_process(&ebtobj->btns_combo[i].btn)) + { + return 1; + } + } + + for (target_combo = ebtobj->btn_combo_dyn_head; target_combo; target_combo = target_combo->next) + { + if (ebtn_is_btn_in_process(&target_combo->btn.btn)) + { + return 1; + } + } + + return 0; +} + +int ebtn_register(ebtn_btn_dyn_t *button) +{ + ebtn_t *ebtobj = &ebtn_default; + + ebtn_btn_dyn_t *curr = ebtobj->btn_dyn_head; + ebtn_btn_dyn_t *last = NULL; + + if (!button) + { + return 0; + } + + if (ebtn_get_total_btn_cnt() >= EBTN_MAX_KEYNUM) + { + return 0; /* reach max cnt. */ + } + + if (curr == NULL) + { + ebtobj->btn_dyn_head = button; + return 1; + } + + while (curr) + { + if (curr == button) + { + return 0; /* already exist. */ + } + last = curr; + curr = curr->next; + } + + last->next = button; + + return 1; +} + +int ebtn_combo_register(ebtn_btn_combo_dyn_t *button) +{ + ebtn_t *ebtobj = &ebtn_default; + + ebtn_btn_combo_dyn_t *curr = ebtobj->btn_combo_dyn_head; + ebtn_btn_combo_dyn_t *last = NULL; + + if (!button) + { + return 0; + } + + if (curr == NULL) + { + ebtobj->btn_combo_dyn_head = button; + return 1; + } + + while (curr) + { + if (curr == button) + { + return 0; /* already exist. */ + } + last = curr; + curr = curr->next; + } + + last->next = button; + + return 1; +} diff --git a/ebtn/ebtn.h b/ebtn/ebtn.h new file mode 100644 index 0000000..72f343e --- /dev/null +++ b/ebtn/ebtn.h @@ -0,0 +1,463 @@ +#ifndef _EBTN_H +#define _EBTN_H + +#include +#include + +#include "bit_array.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +// #define EBTN_CONFIG_TIMER_16 + +// here can change to uint16_t, if you want reduce RAM size. +#ifdef EBTN_CONFIG_TIMER_16 +typedef uint16_t ebtn_time_t; +typedef int16_t ebtn_time_sign_t; +#else +typedef uint32_t ebtn_time_t; +typedef int32_t ebtn_time_sign_t; +#endif + +/* Forward declarations */ +struct ebtn_btn; +struct ebtn; + +#define EBTN_MAX_KEYNUM (64) + +/** + * \brief List of button events + * + */ +typedef enum +{ + EBTN_EVT_ONPRESS = 0x00, /*!< On press event - sent when valid press is detected */ + EBTN_EVT_ONRELEASE, /*!< On release event - sent when valid release event is detected (from + active to inactive) */ + EBTN_EVT_ONCLICK, /*!< On Click event - sent when valid sequence of on-press and on-release + events occurs */ + EBTN_EVT_KEEPALIVE, /*!< Keep alive event - sent periodically when button is active */ +} ebtn_evt_t; + +/** + * @brief Returns the difference between two absolute times: time1-time2. + * @param[in] time1: Absolute time expressed in internal time units. + * @param[in] time2: Absolute time expressed in internal time units. + * @return resulting signed relative time expressed in internal time units. + */ +static inline ebtn_time_sign_t ebtn_timer_sub(ebtn_time_t time1, ebtn_time_t time2) +{ + return time1 - time2; +} + +// test time overflow error +// #define ebtn_timer_sub(time1, time2) (time1 - time2) + +/** + * \brief Button event function callback prototype + * \param[in] btn: Button instance from array for which event occured + * \param[in] evt: Event type + */ +typedef void (*ebtn_evt_fn)(struct ebtn_btn *btn, ebtn_evt_t evt); + +/** + * \brief Get button/input state callback function + * + * \param[in] btn: Button instance from array to read state + * \return `1` when button is considered `active`, `0` otherwise + */ +typedef uint8_t (*ebtn_get_state_fn)(struct ebtn_btn *btn); + +/** + * \brief Button Params structure + */ +typedef struct ebtn_btn_param +{ + /** + * \brief Minimum debounce time for press event in units of milliseconds + * + * This is the time when the input shall have stable active level to detect valid *onpress* + * event. + * + * When value is set to `> 0`, input must be in active state for at least + * minimum milliseconds time, before valid *onpress* event is detected. + * + * \note If value is set to `0`, debounce is not used and *press* event will be + * triggered immediately when input states goes to *inactive* state. + * + * To be safe not using this feature, external logic must ensure stable + * transition at input level. + * + */ + uint16_t time_debounce; /*!< Debounce time in milliseconds */ + + /** + * \brief Minimum debounce time for release event in units of milliseconds + * + * This is the time when the input shall have minimum stable released level to detect valid + * *onrelease* event. + * + * This setting can be useful if application wants to protect against + * unwanted glitches on the line when input is considered "active". + * + * When value is set to `> 0`, input must be in inactive low for at least + * minimum milliseconds time, before valid *onrelease* event is detected + * + * \note If value is set to `0`, debounce is not used and *release* event will be + * triggered immediately when input states goes to *inactive* state + * + */ + uint16_t time_debounce_release; /*!< Debounce time in milliseconds for release event */ + + /** + * \brief Minimum active input time for valid click event, in milliseconds + * + * Input shall be in active state (after debounce) at least this amount of time to even consider + * the potential valid click event. Set the value to `0` to disable this feature + * + */ + uint16_t time_click_pressed_min; /*!< Minimum pressed time for valid click event */ + + /** + * \brief Maximum active input time for valid click event, in milliseconds + * + * Input shall be pressed at most this amount of time to still trigger valid click. + * Set to `-1` to allow any time triggering click event. + * + * When input is active for more than the configured time, click even is not detected and is + * ignored. + * + */ + uint16_t time_click_pressed_max; /*!< Maximum pressed time for valid click event*/ + + /** + * \brief Maximum allowed time between last on-release and next valid on-press, + * to still allow multi-click events, in milliseconds + * + * This value is also used as a timeout length to send the *onclick* event to application from + * previously detected valid click events. + * + * If application relies on multi consecutive clicks, this is the max time to allow user + * to trigger potential new click, or structure will get reset (before sent to user if any + * clicks have been detected so far) + * + */ + uint16_t time_click_multi_max; /*!< Maximum time between 2 clicks to be considered consecutive + click */ + + /** + * \brief Keep-alive event period, in milliseconds + * + * When input is active, keep alive events will be sent through this period of time. + * First keep alive will be sent after input being considered + * active. + * + */ + uint16_t time_keepalive_period; /*!< Time in ms for periodic keep alive event */ + + /** + * \brief Maximum number of allowed consecutive click events, + * before structure gets reset to default value. + * + * \note When consecutive value is reached, application will get notification of + * clicks. This can be executed immediately after last click has been detected, or after + * standard timeout (unless next on-press has already been detected, then it is send to + * application just before valid next press event). + * + */ + uint16_t max_consecutive; /*!< Max number of consecutive clicks */ +} ebtn_btn_param_t; + +#define EBTN_PARAMS_INIT(_time_debounce, _time_debounce_release, _time_click_pressed_min, _time_click_pressed_max, _time_click_multi_max, \ + _time_keepalive_period, _max_consecutive) \ + { \ + .time_debounce = _time_debounce, .time_debounce_release = _time_debounce_release, .time_click_pressed_min = _time_click_pressed_min, \ + .time_click_pressed_max = _time_click_pressed_max, .time_click_multi_max = _time_click_multi_max, .time_keepalive_period = _time_keepalive_period, \ + .max_consecutive = _max_consecutive \ + } + +#define EBTN_BUTTON_INIT(_key_id, _param) \ + { \ + .key_id = _key_id, .param = _param, \ + } + +#define EBTN_BUTTON_DYN_INIT(_key_id, _param) \ + { \ + .next = NULL, .btn = EBTN_BUTTON_INIT(_key_id, _param), \ + } + +#define EBTN_BUTTON_COMBO_INIT(_key_id, _param) \ + { \ + .comb_key = {0}, .btn = EBTN_BUTTON_INIT(_key_id, _param), \ + } + +#define EBTN_BUTTON_COMBO_DYN_INIT(_key_id, _param) \ + { \ + .next = NULL, .btn = EBTN_BUTTON_COMBO_INIT(_key_id, _param), \ + } + +#define EBTN_ARRAY_SIZE(_arr) sizeof(_arr) / sizeof((_arr)[0]) + +/** + * \brief Button structure + */ +typedef struct ebtn_btn +{ + uint16_t key_id; /*!< User defined custom argument for callback function purpose */ + uint16_t flags; /*!< Private button flags management */ + ebtn_time_t time_change; /*!< Time in ms when button state got changed last time after valid + debounce */ + ebtn_time_t time_state_change; /*!< Time in ms when button state got changed last time */ + + ebtn_time_t keepalive_last_time; /*!< Time in ms of last send keep alive event */ + ebtn_time_t click_last_time; /*!< Time in ms of last successfully detected (not sent!) click event + */ + + uint16_t keepalive_cnt; /*!< Number of keep alive events sent after successful on-press + detection. Value is reset after on-release */ + uint16_t click_cnt; /*!< Number of consecutive clicks detected, respecting maximum timeout + between clicks */ + + const ebtn_btn_param_t *param; +} ebtn_btn_t; + +/** + * \brief ComboButton structure + */ +typedef struct ebtn_btn_combo +{ + BIT_ARRAY_DEFINE(comb_key, EBTN_MAX_KEYNUM); /*!< select key index - `1` means active, `0` means inactive */ + + ebtn_btn_t btn; +} ebtn_btn_combo_t; + +/** + * \brief Dynamic Button structure + */ +typedef struct ebtn_btn_dyn +{ + struct ebtn_btn_dyn *next; /*!< point to next button */ + + ebtn_btn_t btn; +} ebtn_btn_dyn_t; + +/** + * \brief Dynamic ComboButton structure + */ +typedef struct ebtn_btn_combo_dyn +{ + struct ebtn_btn_combo_dyn *next; /*!< point to next combo-button */ + + ebtn_btn_combo_t btn; +} ebtn_btn_combo_dyn_t; + +/** + * \brief easy_button group structure + */ +typedef struct ebtn +{ + ebtn_btn_t *btns; /*!< Pointer to buttons array */ + uint16_t btns_cnt; /*!< Number of buttons in array */ + ebtn_btn_combo_t *btns_combo; /*!< Pointer to comb-buttons array */ + uint16_t btns_combo_cnt; /*!< Number of comb-buttons in array */ + + ebtn_btn_dyn_t *btn_dyn_head; /*!< Pointer to btn-dynamic list */ + ebtn_btn_combo_dyn_t *btn_combo_dyn_head; /*!< Pointer to btn-combo-dynamic list */ + + ebtn_evt_fn evt_fn; /*!< Pointer to event function */ + ebtn_get_state_fn get_state_fn; /*!< Pointer to get state function */ + + BIT_ARRAY_DEFINE(old_state, EBTN_MAX_KEYNUM); /*!< Old button state - `1` means active, `0` means inactive */ +} ebtn_t; + +/** + * \brief Button processing function, that reads the inputs and makes actions accordingly. + * + * + * \param[in] mstime: Current system time in milliseconds + */ +void ebtn_process(ebtn_time_t mstime); + +/** + * \brief Button processing function, with all button input state. + * + * \param[in] curr_state: Current all button input state + * \param[in] mstime: Current system time in milliseconds + */ +void ebtn_process_with_curr_state(bit_array_t *curr_state, ebtn_time_t mstime); + +/** + * \brief Check if button is active. + * Active is considered when initial debounce period has been a pass. + * This is the period between on-press and on-release events. + * + * \param[in] btn: Button handle to check + * \return `1` if active, `0` otherwise + */ +int ebtn_is_btn_active(const ebtn_btn_t *btn); + +/** + * \brief Check if button is in process. + * Used for low-power processing, indicating that the buttons are temporarily idle, and embedded systems can consider entering deep sleep. + * + * \param[in] btn: Button handle to check + * \return `1` if in process, `0` otherwise + */ +int ebtn_is_btn_in_process(const ebtn_btn_t *btn); + +/** + * \brief Check if some button is in process. + * Used for low-power processing, indicating that the buttons are temporarily idle, and embedded systems can consider entering deep sleep. + * + * \return `1` if in process, `0` otherwise + */ +int ebtn_is_in_process(void); + +/** + * \brief Initialize button manager + * \param[in] btns: Array of buttons to process + * \param[in] btns_cnt: Number of buttons to process + * \param[in] btns_combo: Array of combo-buttons to process + * \param[in] btns_combo_cnt: Number of combo-buttons to process + * \param[in] get_state_fn: Pointer to function providing button state on demand. + * \param[in] evt_fn: Button event function callback + * + * \return `1` on success, `0` otherwise + */ +int ebtn_init(ebtn_btn_t *btns, uint16_t btns_cnt, ebtn_btn_combo_t *btns_combo, uint16_t btns_combo_cnt, ebtn_get_state_fn get_state_fn, ebtn_evt_fn evt_fn); + +/** + * @brief Register a dynamic button + * + * @param button: Dynamic button structure instance + * \return `1` on success, `0` otherwise + */ +int ebtn_register(ebtn_btn_dyn_t *button); + +/** + * \brief Register a dynamic combo-button + * \param[in] button: Dynamic combo-button structure instance + * + * \return `1` on success, `0` otherwise + */ +int ebtn_combo_register(ebtn_btn_combo_dyn_t *button); + +/** + * \brief Get the current total button cnt + * + * \return size of button. + */ +int ebtn_get_total_btn_cnt(void); + +/** + * \brief Get the internal key_idx of the key_id + * \param[in] key_id: key_id + * + * \return '-1' on error, other is key_idx + */ +int ebtn_get_btn_index_by_key_id(uint16_t key_id); + +/** + * \brief Get the internal btn instance of the key_id, here is the button instance, and what is dynamically registered is also to obtain its button + * instance + * + * \param[in] key_id: key_id + * + * \return 'NULL' on error, other is button instance + */ +ebtn_btn_t *ebtn_get_btn_by_key_id(uint16_t key_id); + +/** + * \brief Get the internal key_idx of the button + * \param[in] btn: Button + * + * \return '-1' on error, other is key_idx + */ +int ebtn_get_btn_index_by_btn(ebtn_btn_t *btn); + +/** + * \brief Get the internal key_idx of the dynamic button + * \param[in] btn: Button + * + * \return '-1' on error, other is key_idx + */ +int ebtn_get_btn_index_by_btn_dyn(ebtn_btn_dyn_t *btn); + +/** + * \brief Bind combo-button key with key_idx + * \param[in] btn: Combo Button + * \param[in] idx: key_idx + * + */ +void ebtn_combo_btn_add_btn_by_idx(ebtn_btn_combo_t *btn, int idx); + +/** + * \brief Remove combo-button key with key_idx + * \param[in] btn: Combo Button + * \param[in] idx: key_idx + * + */ +void ebtn_combo_btn_remove_btn_by_idx(ebtn_btn_combo_t *btn, int idx); + +/** + * \brief Bind combo-button key with key_id, make sure key_id(button) is already register. + * \param[in] btn: Combo Button + * \param[in] key_id: key_id + * + */ +void ebtn_combo_btn_add_btn(ebtn_btn_combo_t *btn, uint16_t key_id); + +/** + * \brief Remove combo-button key with key_id, make sure key_id(button) is already + * register. \param[in] btn: Combo Button \param[in] key_id: key_id + * + */ +void ebtn_combo_btn_remove_btn(ebtn_btn_combo_t *btn, uint16_t key_id); + +/** + * \brief Get keep alive period for specific button + * \param[in] btn: Button instance to get keep alive period for + * \return Keep alive period in `ms` + */ +#define ebtn_keepalive_get_period(btn) ((btn)->time_keepalive_period) + +/** + * \brief Get actual number of keep alive counts since the last on-press event. + * It is set to `0` if btn isn't pressed + * \param[in] btn: Button instance to get keep alive period for + * \return Number of keep alive events since on-press event + */ +#define ebtn_keepalive_get_count(btn) ((btn)->keepalive_cnt) + +/** + * \brief Get number of keep alive counts for specific required time in milliseconds. + * It will calculate number of keepalive ticks specific button shall make, + * before requested time is reached. + * + * Result of the function can be used with \ref ebtn_keepalive_get_count which returns + * actual number of keep alive counts since last on-press event of the button. + * + * \note Value is always integer aligned, with granularity of one keepalive time period + * \note Implemented as macro, as it may be optimized by compiler when static keep alive + * is used + * + * \param[in] btn: Button to use for check + * \param[in] ms_time: Time in ms to calculate number of keep alive counts + * \return Number of keep alive counts + */ +#define ebtn_keepalive_get_count_for_time(btn, ms_time) ((ms_time) / ebtn_keepalive_get_period(btn)) + +/** + * \brief Get number of consecutive click events on a button + * \param[in] btn: Button instance to get number of clicks + * \return Number of consecutive clicks on a button + */ +#define ebtn_click_get_count(btn) ((btn)->click_cnt) + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* _EBTN_H */ diff --git a/example_test.c b/example_test.c new file mode 100644 index 0000000..7a391b6 --- /dev/null +++ b/example_test.c @@ -0,0 +1,755 @@ +#include +#include + +#include "windows.h" + +#include "ebtn.h" + +// +// Tests +// +const char *suite_name; +char suite_pass; +int suites_run = 0, suites_failed = 0, suites_empty = 0; +int tests_in_suite = 0, tests_run = 0, tests_failed = 0; + +#define QUOTE(str) #str +#define ASSERT(x) \ + { \ + tests_run++; \ + tests_in_suite++; \ + if (!(x)) \ + { \ + printf("failed assert [%s:%i] %s\n", __FILE__, __LINE__, QUOTE(x)); \ + suite_pass = 0; \ + tests_failed++; \ + } \ + } + +void SUITE_START(const char *name) +{ + suite_pass = 1; + suite_name = name; + suites_run++; + tests_in_suite = 0; +} + +void SUITE_END(void) +{ + printf("Testing %s ", suite_name); + size_t suite_i; + for (suite_i = strlen(suite_name); suite_i < 80 - 8 - 5; suite_i++) + printf("."); + printf("%s\n", suite_pass ? " pass" : " fail"); + if (!suite_pass) + suites_failed++; + if (!tests_in_suite) + suites_empty++; +} + +typedef enum +{ + USER_BUTTON_default = 0, + USER_BUTTON_onrelease_debounce, + USER_BUTTON_keepalive_with_click, + USER_BUTTON_max_click_3, + USER_BUTTON_click_multi_max_0, + USER_BUTTON_keep_alive_0, + USER_BUTTON_MAX, + + USER_BUTTON_COMBO_0 = 0x100, + USER_BUTTON_COMBO_1, + USER_BUTTON_COMBO_2, + USER_BUTTON_COMBO_3, + USER_BUTTON_COMBO_MAX, +} user_button_t; + +/** + * \brief Input state information + */ +typedef struct +{ + uint16_t key_id; /*!< Select Key id */ + uint8_t state; /*!< Input state -> 1 = active, 0 = inactive */ + uint32_t duration; /*!< Time until this state is enabled */ +} btn_test_time_t; + +/** + * \brief Event sequence + */ +typedef struct +{ + ebtn_evt_t evt; /*!< Event type */ + uint8_t keepalive_cnt; /*!< Number of keep alive events while button is active */ + uint8_t conseq_clicks; /*!< Number of consecutive clicks detected */ +} btn_test_evt_t; + +typedef struct +{ + const char *test_name; + uint16_t test_key_id; + int test_sequence_cnt; + btn_test_time_t *test_sequence; /*!< Input state -> 1 = active, 0 = inactive */ + int test_events_cnt; + const btn_test_evt_t *test_events; /*!< Time until this state is enabled */ +} btn_test_arr_t; + +#define TEST_ARRAY_DEFINE(_key_id, _seq, _evt) \ + { \ + .test_key_id = _key_id, .test_name = #_seq, .test_sequence_cnt = EBTN_ARRAY_SIZE(_seq), .test_sequence = _seq, \ + .test_events_cnt = EBTN_ARRAY_SIZE(_evt), .test_events = _evt \ + } + +/* Max number of ms to demonstrate */ +#define MAX_TIME_MS 0x3FFFF + +#define EBTN_PARAM_TIME_DEBOUNCE_PRESS(_param) _param.time_debounce +#define EBTN_PARAM_TIME_DEBOUNCE_RELEASE(_param) _param.time_debounce_release +#define EBTN_PARAM_TIME_CLICK_MIN(_param) _param.time_click_pressed_min +#define EBTN_PARAM_TIME_CLICK_MAX(_param) _param.time_click_pressed_max +#define EBTN_PARAM_TIME_CLICK_MULTI_MAX(_param) _param.time_click_multi_max +#define EBTN_PARAM_TIME_KEEPALIVE_PERIOD(_param) _param.time_keepalive_period +#define EBTN_PARAM_CLICK_MAX_CONSECUTIVE(_param) _param.max_consecutive + +static const ebtn_btn_param_t param_default = EBTN_PARAMS_INIT(20, 0, 20, 300, 200, 500, 10); + +static const ebtn_btn_param_t param_onrelease_debounce = EBTN_PARAMS_INIT(20, 80, 0, 300, 200, 500, 10); + +static const ebtn_btn_param_t param_keepalive_with_click = EBTN_PARAMS_INIT(20, 80, 0, 400, 200, 100, 10); + +static const ebtn_btn_param_t param_max_click_3 = EBTN_PARAMS_INIT(20, 80, 0, 400, 200, 100, 3); + +static const ebtn_btn_param_t param_click_multi_max_0 = EBTN_PARAMS_INIT(20, 80, 0, 400, 0, 100, 3); + +static const ebtn_btn_param_t param_keep_alive_0 = EBTN_PARAMS_INIT(20, 80, 0, 400, 200, 0, 3); + +/* List of used buttons -> test case */ +static ebtn_btn_t btns[] = {EBTN_BUTTON_INIT(USER_BUTTON_default, ¶m_default), + EBTN_BUTTON_INIT(USER_BUTTON_onrelease_debounce, ¶m_onrelease_debounce), + EBTN_BUTTON_INIT(USER_BUTTON_keepalive_with_click, ¶m_keepalive_with_click), + EBTN_BUTTON_INIT(USER_BUTTON_max_click_3, ¶m_max_click_3), + EBTN_BUTTON_INIT(USER_BUTTON_click_multi_max_0, ¶m_click_multi_max_0), + EBTN_BUTTON_INIT(USER_BUTTON_keep_alive_0, ¶m_keep_alive_0)}; + +static volatile uint32_t test_processed_time_current; + +/* Set button state -> used for test purposes */ +#define BTN_STATE_RAW(_key_id_, _state_, _duration_) \ + { \ + .key_id = (_key_id_), .state = (_state_), .duration = (_duration_) \ + } + +#define BTN_STATE(_state_, _duration_) BTN_STATE_RAW(USER_BUTTON_default, _state_, _duration_) + +/* On-Press event */ +#define BTN_EVENT_ONPRESS() \ + { \ + .evt = EBTN_EVT_ONPRESS \ + } +/* On-Release event */ +#define BTN_EVENT_ONRELEASE() \ + { \ + .evt = EBTN_EVT_ONRELEASE \ + } +/* On-Click event */ +#define BTN_EVENT_ONCLICK(_conseq_clicks_) \ + { \ + .evt = EBTN_EVT_ONCLICK, .conseq_clicks = (_conseq_clicks_) \ + } +/* On-Click event */ +#define BTN_EVENT_KEEPALIVE(_keepalive_cnt_) \ + { \ + .evt = EBTN_EVT_KEEPALIVE, .keepalive_cnt = (_keepalive_cnt_) \ + } + +/* + * Simulate click event + */ +static btn_test_time_t test_sequence_single_click[] = { + /* + * Step 1: + * Go to active state and stay there for a period + * of minimum debounce time and minimum + * time input must be active to later detect click event + * + * Step 2: + * Go low and stay inactive until time to report click has elapsed. + * Include debounce timing for release event. + * + * Add +1 to the end, to force click event, + * and not to go to "consecutive clicks" if any further tests are added in this sequence + */ + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_single_click[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONCLICK(1), +}; + +static btn_test_time_t test_sequence_double_click[] = { + /* + * Repeat above steps, this time w/o +1 at the end. + * + * Simulate "2" consecutive clicks and report final "click" event at the end of the + * sequence, with "2" consecutive clicks in the report info + */ + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_double_click[] = { + BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONCLICK(2), +}; + +static btn_test_time_t test_sequence_triple_click[] = { + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_triple_click[] = { + BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONCLICK(3), +}; + +static btn_test_time_t test_sequence_double_click_critical_time[] = { + /* + * Repeat above steps, this time w/o +1 at the end. + * + * Simulate "2" consecutive clicks and report final "click" event at the end of the + * sequence, with "2" consecutive clicks in the report info + */ + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + /* Hold button in release state for time that is max for 2 clicks - time that we will + indicate in the next press state -> this is the frequency between detected events */ + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default) + /* Decrease by active time in next step */ + - (EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default)) - 2), + /* Active time */ + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_double_click_critical_time[] = { + BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONCLICK(2), +}; + +static btn_test_time_t test_sequence_double_click_critical_time_over[] = { + /* + * This test shows how to handle case when 2 clicks are being executed, + * but time between 2 release events is larger than maximum + * allowed time for consecutive clicks. + * + * In this case, 2 onclick events are sent, + * both with consecutive clicks counter set to 1 + */ + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default) - + (EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default))), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_double_click_critical_time_over[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + /* This one is to handle click for first sequence */ + BTN_EVENT_ONCLICK(1), + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + /* This one is to handle click for second sequence */ + BTN_EVENT_ONCLICK(1), +}; + +static btn_test_time_t test_sequence_click_with_keepalive[] = { + /* + * Make a click event, followed by the longer press. + * Simulate "long press" w/ previous click, that has click counter set to 1 + */ + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default)), + BTN_STATE(1, EBTN_PARAM_TIME_KEEPALIVE_PERIOD(param_default)), + BTN_STATE(1, EBTN_PARAM_TIME_KEEPALIVE_PERIOD(param_default)), + BTN_STATE(1, EBTN_PARAM_TIME_KEEPALIVE_PERIOD(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_click_with_keepalive[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONPRESS(), + /* This one is to handle click before long press */ + BTN_EVENT_ONCLICK(1), + BTN_EVENT_KEEPALIVE(1), + BTN_EVENT_KEEPALIVE(2), + BTN_EVENT_KEEPALIVE(3), + BTN_EVENT_ONRELEASE(), +}; + +static btn_test_time_t test_sequence_click_with_short[] = { + /* Make with short press (shorter than minimum required) */ + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default) / 2), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_click_with_short[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), +}; + +static btn_test_time_t test_sequence_click_with_short_with_multi[] = { + /* Make with short press (shorter than minimum required) */ + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default) / 2), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_click_with_short_with_multi[] = { + /* This one is short... */ + BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONCLICK(2), +}; + +static btn_test_time_t test_sequence_multi_click_with_short[] = { + /* Make 2 clicks, and 3rd one with short press (shorter than minimum required) */ + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default) / 2), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default)), +}; + +static const btn_test_evt_t test_events_multi_click_with_short[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + /* This one is short... */ + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONCLICK(2), +}; + +static btn_test_time_t test_sequence_onpress_debounce[] = { + BTN_STATE(0, 0), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) / 2), + BTN_STATE(0, 1), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) / 2), + BTN_STATE(0, 1), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) / 2), + BTN_STATE(0, 1), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default) + 1), +}; + +static const btn_test_evt_t test_events_onpress_debounce[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONCLICK(1), +}; + +// for test overflow, make sure enable macro 'EBTN_CONFIG_TIMER_16' or compile with 'make all +// CFLAGS=-DEBTN_CONFIG_TIMER_16' +static btn_test_time_t test_sequence_time_overflow_onpress_debounce[] = { + BTN_STATE(0, 0x0ffff - (EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) / 2)), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_time_overflow_onpress_debounce[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONCLICK(1), +}; + +static btn_test_time_t test_sequence_time_overflow_onpress[] = { + BTN_STATE(0, 0x0ffff - (EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default) / 2)), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_time_overflow_onpress[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONCLICK(1), +}; + +static btn_test_time_t test_sequence_time_overflow_onrelease_muti[] = { + BTN_STATE(0, 0x0ffff - (EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default) + + EBTN_PARAM_TIME_CLICK_MIN(param_default) / 2)), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_time_overflow_onrelease_muti[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONCLICK(1), +}; + +static btn_test_time_t test_sequence_time_overflow_keepalive[] = { + BTN_STATE(0, 0x0ffff - (EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default) + + EBTN_PARAM_TIME_KEEPALIVE_PERIOD(param_default) / 2)), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_default) + EBTN_PARAM_TIME_CLICK_MIN(param_default)), + BTN_STATE(1, EBTN_PARAM_TIME_KEEPALIVE_PERIOD(param_default)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_default) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_default)), +}; + +static const btn_test_evt_t test_events_time_overflow_keepalive[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_KEEPALIVE(1), + BTN_EVENT_ONRELEASE(), +}; + +/// +/// Test onrelease debounce +/// + +static btn_test_time_t test_sequence_onrelease_debounce[] = { + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_onrelease_debounce) + EBTN_PARAM_TIME_CLICK_MIN(param_onrelease_debounce)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_onrelease_debounce) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_onrelease_debounce)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_onrelease_debounce) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_onrelease_debounce)), +}; + +static const btn_test_evt_t test_events_onrelease_debounce[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONCLICK(1), +}; + +static btn_test_time_t test_sequence_onrelease_debounce_over[] = { + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_onrelease_debounce) + EBTN_PARAM_TIME_CLICK_MIN(param_onrelease_debounce)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_onrelease_debounce)), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_onrelease_debounce) + EBTN_PARAM_TIME_CLICK_MIN(param_onrelease_debounce)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_onrelease_debounce) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_onrelease_debounce)), +}; + +static const btn_test_evt_t test_events_onrelease_debounce_over[] = { + BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONCLICK(2), +}; + +static btn_test_time_t test_sequence_onrelease_debounce_time_overflow[] = { + BTN_STATE(0, 0x0ffff - (EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_onrelease_debounce) + EBTN_PARAM_TIME_CLICK_MIN(param_onrelease_debounce) + + EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_onrelease_debounce) / 2)), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_onrelease_debounce) + EBTN_PARAM_TIME_CLICK_MIN(param_onrelease_debounce)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_onrelease_debounce) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_onrelease_debounce)), +}; + +static const btn_test_evt_t test_events_onrelease_debounce_time_overflow[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONCLICK(1), +}; + +/// +/// Test keepalive with click debounce +/// +static btn_test_time_t test_sequence_keepalive_with_click[] = { + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_keepalive_with_click) + EBTN_PARAM_TIME_CLICK_MIN(param_keepalive_with_click)), + BTN_STATE(1, EBTN_PARAM_TIME_KEEPALIVE_PERIOD(param_keepalive_with_click)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_keepalive_with_click) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_keepalive_with_click)), +}; + +static const btn_test_evt_t test_events_keepalive_with_click[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_KEEPALIVE(1), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONCLICK(1), +}; + +static btn_test_time_t test_sequence_keepalive_with_click_double[] = { + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_keepalive_with_click) + EBTN_PARAM_TIME_CLICK_MIN(param_keepalive_with_click)), + BTN_STATE(1, EBTN_PARAM_TIME_KEEPALIVE_PERIOD(param_keepalive_with_click)), + BTN_STATE(1, EBTN_PARAM_TIME_KEEPALIVE_PERIOD(param_keepalive_with_click)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_keepalive_with_click) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_keepalive_with_click)), +}; + +static const btn_test_evt_t test_events_keepalive_with_click_double[] = { + BTN_EVENT_ONPRESS(), BTN_EVENT_KEEPALIVE(1), BTN_EVENT_KEEPALIVE(2), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONCLICK(1), +}; + +/// +/// Test max multi click with 3 +/// +static btn_test_time_t test_sequence_max_click_3[] = { + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MIN(param_max_click_3)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_max_click_3) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MIN(param_max_click_3)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_max_click_3) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MIN(param_max_click_3)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_max_click_3)), +}; + +static const btn_test_evt_t test_events_max_click_3[] = { + BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONCLICK(3), +}; + +static btn_test_time_t test_sequence_max_click_3_over[] = { + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MIN(param_max_click_3)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_max_click_3) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MIN(param_max_click_3)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_max_click_3) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MIN(param_max_click_3)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_max_click_3) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MIN(param_max_click_3)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_max_click_3) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_max_click_3)), +}; + +static const btn_test_evt_t test_events_max_click_3_over[] = { + BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), BTN_EVENT_ONCLICK(3), BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONCLICK(1), +}; + +/// +/// Test click multi max with 0 +/// +static btn_test_time_t test_sequence_click_multi_max_0[] = { + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_click_multi_max_0) + EBTN_PARAM_TIME_CLICK_MIN(param_click_multi_max_0)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_click_multi_max_0) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_click_multi_max_0) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_click_multi_max_0) + EBTN_PARAM_TIME_CLICK_MIN(param_click_multi_max_0)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_click_multi_max_0) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_click_multi_max_0) / 2), + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_click_multi_max_0) + EBTN_PARAM_TIME_CLICK_MIN(param_click_multi_max_0)), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_click_multi_max_0) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_click_multi_max_0)), +}; + +static const btn_test_evt_t test_events_click_multi_max_0[] = { + BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONCLICK(1), BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONCLICK(1), BTN_EVENT_ONPRESS(), BTN_EVENT_ONRELEASE(), BTN_EVENT_ONCLICK(1), +}; + +/// +/// Test max keepalive with 0 +/// +static btn_test_time_t test_sequence_keep_alive_0[] = { + BTN_STATE(1, EBTN_PARAM_TIME_DEBOUNCE_PRESS(param_keep_alive_0) + EBTN_PARAM_TIME_CLICK_MIN(param_keep_alive_0)), + BTN_STATE(1, EBTN_PARAM_TIME_CLICK_MAX(param_keep_alive_0) / 2), + BTN_STATE(0, EBTN_PARAM_TIME_DEBOUNCE_RELEASE(param_keep_alive_0) + EBTN_PARAM_TIME_CLICK_MULTI_MAX(param_keep_alive_0)), +}; + +static const btn_test_evt_t test_events_keep_alive_0[] = { + BTN_EVENT_ONPRESS(), + BTN_EVENT_ONRELEASE(), + BTN_EVENT_ONCLICK(1), +}; + +static btn_test_arr_t test_list[] = { + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_single_click, test_events_single_click), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_double_click, test_events_double_click), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_triple_click, test_events_triple_click), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_double_click_critical_time, test_events_double_click_critical_time), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_double_click_critical_time_over, test_events_double_click_critical_time_over), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_click_with_keepalive, test_events_click_with_keepalive), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_click_with_short, test_events_click_with_short), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_click_with_short_with_multi, test_events_click_with_short_with_multi), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_multi_click_with_short, test_events_multi_click_with_short), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_onpress_debounce, test_events_onpress_debounce), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_time_overflow_onpress_debounce, test_events_time_overflow_onpress_debounce), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_time_overflow_onpress, test_events_time_overflow_onpress), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_time_overflow_onrelease_muti, test_events_time_overflow_onrelease_muti), + TEST_ARRAY_DEFINE(USER_BUTTON_default, test_sequence_time_overflow_keepalive, test_events_time_overflow_keepalive), + + TEST_ARRAY_DEFINE(USER_BUTTON_onrelease_debounce, test_sequence_onrelease_debounce, test_events_onrelease_debounce), + TEST_ARRAY_DEFINE(USER_BUTTON_onrelease_debounce, test_sequence_onrelease_debounce_over, test_events_onrelease_debounce_over), + TEST_ARRAY_DEFINE(USER_BUTTON_onrelease_debounce, test_sequence_onrelease_debounce_time_overflow, test_events_onrelease_debounce_time_overflow), + + TEST_ARRAY_DEFINE(USER_BUTTON_keepalive_with_click, test_sequence_keepalive_with_click, test_events_keepalive_with_click), + TEST_ARRAY_DEFINE(USER_BUTTON_keepalive_with_click, test_sequence_keepalive_with_click_double, test_events_keepalive_with_click_double), + + TEST_ARRAY_DEFINE(USER_BUTTON_max_click_3, test_sequence_max_click_3, test_events_max_click_3), + TEST_ARRAY_DEFINE(USER_BUTTON_max_click_3, test_sequence_max_click_3_over, test_events_max_click_3_over), + + TEST_ARRAY_DEFINE(USER_BUTTON_click_multi_max_0, test_sequence_click_multi_max_0, test_events_click_multi_max_0), + + TEST_ARRAY_DEFINE(USER_BUTTON_keep_alive_0, test_sequence_keep_alive_0, test_events_keep_alive_0), +}; + +static btn_test_arr_t *select_test_item; + +/* Get button state for given current time */ +static uint8_t prv_get_state_for_time(uint16_t key_id, uint32_t time) +{ + uint8_t state = 0; + uint32_t duration = 0; + + if (select_test_item->test_key_id == key_id) + { + // printf("time: %d, key_id: %d\n", time, key_id); + for (size_t i = 0; i < select_test_item->test_sequence_cnt; ++i) + { + if (select_test_item->test_sequence[i].duration == 0) + { + continue; + } + duration += select_test_item->test_sequence[i].duration + 1; /* Advance time, need add 1 for state switch time. */ + // printf("i: %d, duration: %d\n", i, duration); + if (time <= duration) + { + state = select_test_item->test_sequence[i].state; + // printf("i: %d, time: %d, duration: %d, state: %d\n", i, time, duration, state); + // printf("state: %d\n", state); + break; + } + } + } + + return state; +} + +static uint32_t test_get_state_total_duration(void) +{ + uint32_t duration = 0; + + for (size_t i = 0; i < select_test_item->test_sequence_cnt; ++i) + { + if (select_test_item->test_sequence[i].duration == 0) + { + continue; + } + duration += select_test_item->test_sequence[i].duration + 1; /* Advance time, need add 1 for state switch time. */ + } + + return duration; +} + +/* Get button state */ +static uint8_t prv_btn_get_state(struct ebtn_btn *btn) +{ + uint8_t state = prv_get_state_for_time(btn->key_id, test_processed_time_current); + + (void)btn; + return state; +} + +static uint32_t test_processed_event_time_prev; +static uint32_t test_processed_array_index = 0; +/* Process button event */ +static void prv_btn_event(struct ebtn_btn *btn, ebtn_evt_t evt) +{ + const char *s; + uint32_t color, keepalive_cnt = 0, diff_time; + const btn_test_evt_t *test_evt_data = NULL; + HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + + if (test_processed_array_index >= select_test_item->test_events_cnt) + { + SetConsoleTextAttribute(hConsole, FOREGROUND_RED); + printf("[%7u] ERROR! Array index is out of bounds!\r\n", (unsigned)test_processed_time_current); + SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + } + else + { + test_evt_data = &select_test_item->test_events[test_processed_array_index]; + } + + /* Handle timing */ + diff_time = test_processed_time_current - test_processed_event_time_prev; + test_processed_event_time_prev = test_processed_time_current; + keepalive_cnt = btn->keepalive_cnt; + + /* Event type must match */ + ASSERT((test_evt_data != NULL) && (test_evt_data->evt == evt)); + + /* Get event string */ + if (evt == EBTN_EVT_KEEPALIVE) + { + s = "KEEPALIVE"; + color = FOREGROUND_RED | FOREGROUND_BLUE; + + ASSERT((test_evt_data != NULL) && (test_evt_data->keepalive_cnt == keepalive_cnt)); + } + else if (evt == EBTN_EVT_ONPRESS) + { + s = "ONPRESS"; + color = FOREGROUND_GREEN; + } + else if (evt == EBTN_EVT_ONRELEASE) + { + s = "ONRELEASE"; + color = FOREGROUND_BLUE; + } + else if (evt == EBTN_EVT_ONCLICK) + { + s = "ONCLICK"; + color = FOREGROUND_RED | FOREGROUND_GREEN; + + ASSERT((test_evt_data != NULL) && (test_evt_data->conseq_clicks == btn->click_cnt)); + } + else + { + s = "UNKNOWN"; + color = FOREGROUND_RED; + } + + SetConsoleTextAttribute(hConsole, color); + printf("[%7u][%6u] ID(hex):%4x, evt:%10s, keep-alive cnt: %3u, click cnt: %3u\r\n", (unsigned)test_processed_time_current, (unsigned)diff_time, btn->key_id, + s, (unsigned)keepalive_cnt, (unsigned)btn->click_cnt); + + SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + ++test_processed_array_index; /* Go to next step in next event */ +} + +/** + * \brief Test function + */ +int example_test(void) +{ + printf("Test running\r\n"); + + for (int index = 0; index < EBTN_ARRAY_SIZE(test_list); index++) + { + select_test_item = &test_list[index]; + + SUITE_START(select_test_item->test_name); + + // printf("\n"); + + // init variable + test_processed_event_time_prev = 0; + test_processed_array_index = 0; + + /* Define buttons */ + ebtn_init(btns, EBTN_ARRAY_SIZE(btns), NULL, 0, prv_btn_get_state, prv_btn_event); + + /* Counter simulates ms tick */ + for (uint32_t i = 0; i < MAX_TIME_MS; ++i) + { + test_processed_time_current = i; /* Set current time used in callback */ + ebtn_process(i); /* Now run processing */ + + // printf("time: %d, end: %d, in_process(): %d/%d\n", i, test_processed_array_index >= select_test_item->test_events_cnt + // , ebtn_is_btn_in_process(ebtn_get_btn_by_key_id(select_test_item->test_key_id)), ebtn_is_in_process()); + // check end + if (test_processed_array_index >= select_test_item->test_events_cnt) + { + uint32_t duration = test_get_state_total_duration(); + if (i > duration + 1) + { + ASSERT(!ebtn_is_btn_in_process(ebtn_get_btn_by_key_id(select_test_item->test_key_id))); + ASSERT(!ebtn_is_in_process()); + } + } + } + ASSERT(test_processed_array_index == select_test_item->test_events_cnt); + + // printf("\n"); + + SUITE_END(); + } + return 0; +} diff --git a/example_user.c b/example_user.c new file mode 100644 index 0000000..ab0dd11 --- /dev/null +++ b/example_user.c @@ -0,0 +1,290 @@ +#include "windows.h" +#include +#include + +#include "ebtn.h" + +static LARGE_INTEGER freq, sys_start_time; +static uint32_t get_tick(void); + +typedef enum +{ + USER_BUTTON_0 = 0, + USER_BUTTON_1, + USER_BUTTON_2, + USER_BUTTON_3, + USER_BUTTON_4, + USER_BUTTON_5, + USER_BUTTON_6, + USER_BUTTON_7, + USER_BUTTON_8, + USER_BUTTON_9, + USER_BUTTON_INVALID, + USER_BUTTON_MAX, + + USER_BUTTON_COMBO_0 = 0x100, + USER_BUTTON_COMBO_1, + USER_BUTTON_COMBO_2, + USER_BUTTON_COMBO_3, + USER_BUTTON_COMBO_MAX, +} user_button_t; + +/* User defined settings */ +static const ebtn_btn_param_t defaul_ebtn_param = EBTN_PARAMS_INIT(20, 0, 20, 300, 200, 500, 10); + +static ebtn_btn_t btns[] = { + // For key_idx double test, need full key map size + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + + EBTN_BUTTON_INIT(USER_BUTTON_0, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_1, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_2, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_3, &defaul_ebtn_param), + + // For key_idx double test, need full key map size + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + + EBTN_BUTTON_INIT(USER_BUTTON_4, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_5, &defaul_ebtn_param), + + // For key_idx double test, need full key map size + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), +}; + +static ebtn_btn_dyn_t btns_dyn[] = { + // For key_idx double test, need full key map size + EBTN_BUTTON_DYN_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_DYN_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_DYN_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_DYN_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + + EBTN_BUTTON_DYN_INIT(USER_BUTTON_6, &defaul_ebtn_param), + EBTN_BUTTON_DYN_INIT(USER_BUTTON_7, &defaul_ebtn_param), + + // For key_idx double test, need full key map size + EBTN_BUTTON_DYN_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_DYN_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_DYN_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + EBTN_BUTTON_DYN_INIT(USER_BUTTON_INVALID, &defaul_ebtn_param), + + EBTN_BUTTON_DYN_INIT(USER_BUTTON_8, &defaul_ebtn_param), + EBTN_BUTTON_DYN_INIT(USER_BUTTON_9, &defaul_ebtn_param), +}; + +uint32_t last_time_keys[USER_BUTTON_MAX - USER_BUTTON_0] = {0}; + +static ebtn_btn_combo_t btns_combo[] = { + EBTN_BUTTON_COMBO_INIT(USER_BUTTON_COMBO_0, &defaul_ebtn_param), + EBTN_BUTTON_COMBO_INIT(USER_BUTTON_COMBO_1, &defaul_ebtn_param), +}; + +static ebtn_btn_combo_dyn_t btns_combo_dyn[] = { + EBTN_BUTTON_COMBO_DYN_INIT(USER_BUTTON_COMBO_2, &defaul_ebtn_param), + EBTN_BUTTON_COMBO_DYN_INIT(USER_BUTTON_COMBO_3, &defaul_ebtn_param), +}; + +uint32_t last_time_keys_combo[USER_BUTTON_COMBO_MAX - USER_BUTTON_COMBO_0] = {0}; + +static int windows_get_match_key(uint16_t key_id) +{ + int key = 0; + switch (key_id) + { + case USER_BUTTON_0: + key = '0'; + break; + case USER_BUTTON_1: + key = '1'; + break; + case USER_BUTTON_2: + key = '2'; + break; + case USER_BUTTON_3: + key = '3'; + break; + case USER_BUTTON_4: + key = '4'; + break; + case USER_BUTTON_5: + key = '5'; + break; + case USER_BUTTON_6: + key = '6'; + break; + case USER_BUTTON_7: + key = '7'; + break; + case USER_BUTTON_8: + key = '8'; + break; + case USER_BUTTON_9: + key = '9'; + break; + } + + return key; +} + +/** + * \brief Get input state callback + * \param btn: Button instance + * \return `1` if button active, `0` otherwise + */ +uint8_t prv_btn_get_state(struct ebtn_btn *btn) +{ + /* + * Function will return negative number if button is pressed, + * or zero if button is releases + */ + return GetAsyncKeyState(windows_get_match_key(btn->key_id)) < 0; +} + +/** + * \brief Button event + * + * \param btn: Button instance + * \param evt: Button event + */ +void prv_btn_event(struct ebtn_btn *btn, ebtn_evt_t evt) +{ + const char *s; + uint32_t color, keepalive_cnt = 0; + HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + uint32_t diff_time = 0; + uint32_t *diff_time_ptr = NULL; + if (btn->key_id < USER_BUTTON_MAX) + { + diff_time_ptr = &last_time_keys[btn->key_id - USER_BUTTON_0]; + } + else + { + diff_time_ptr = &last_time_keys[btn->key_id - USER_BUTTON_COMBO_0]; + } + diff_time = get_tick() - *diff_time_ptr; + + /* This is for purpose of test and timing validation */ + if (diff_time > 2000) + { + diff_time = 0; + } + + *diff_time_ptr = get_tick(); /* Set current date as last one */ + + /* Get event string */ + if (evt == EBTN_EVT_KEEPALIVE) + { + s = "KEEPALIVE"; + color = FOREGROUND_RED | FOREGROUND_BLUE; + } + else if (evt == EBTN_EVT_ONPRESS) + { + s = "ONPRESS"; + color = FOREGROUND_GREEN; + } + else if (evt == EBTN_EVT_ONRELEASE) + { + s = "ONRELEASE"; + color = FOREGROUND_BLUE; + } + else if (evt == EBTN_EVT_ONCLICK) + { + s = "ONCLICK"; + color = FOREGROUND_RED | FOREGROUND_GREEN; + } + else + { + s = "UNKNOWN"; + color = FOREGROUND_RED; + } + + SetConsoleTextAttribute(hConsole, color); + printf("[%7u][%6u] ID(hex):%4x, evt: %10s, keep-alive cnt: %3u, click cnt: %3u\r\n", (unsigned)get_tick(), (unsigned)diff_time, btn->key_id, s, + (unsigned)ebtn_keepalive_get_count(btn), (unsigned)ebtn_click_get_count(btn)); + SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); +} + +/** + * \brief Example function + */ +int example_user(void) +{ + uint32_t time_last; + printf("Application running\r\n"); + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&sys_start_time); + + /* Define buttons */ + ebtn_init(btns, EBTN_ARRAY_SIZE(btns), btns_combo, EBTN_ARRAY_SIZE(btns_combo), prv_btn_get_state, prv_btn_event); + + ebtn_combo_btn_add_btn(&btns_combo[0], USER_BUTTON_0); + ebtn_combo_btn_add_btn(&btns_combo[0], USER_BUTTON_1); + + ebtn_combo_btn_add_btn(&btns_combo[1], USER_BUTTON_2); + ebtn_combo_btn_add_btn(&btns_combo[1], USER_BUTTON_3); + + // dynamic register + for (int i = 0; i < (EBTN_ARRAY_SIZE(btns_dyn)); i++) + { + ebtn_register(&btns_dyn[i]); + } + + ebtn_combo_btn_add_btn(&btns_combo_dyn[0].btn, USER_BUTTON_4); + ebtn_combo_btn_add_btn(&btns_combo_dyn[0].btn, USER_BUTTON_5); + + ebtn_combo_btn_add_btn(&btns_combo_dyn[1].btn, USER_BUTTON_6); + ebtn_combo_btn_add_btn(&btns_combo_dyn[1].btn, USER_BUTTON_7); + + for (int i = 0; i < (EBTN_ARRAY_SIZE(btns_combo_dyn)); i++) + { + ebtn_combo_register(&btns_combo_dyn[i]); + } + + while (1) + { + /* Process forever */ + ebtn_process(get_tick()); + + /* Artificial sleep to offload win process */ + Sleep(5); + } + return 0; +} + +/** + * \brief Get current tick in ms from start of program + * \return uint32_t: Tick in ms + */ +static uint32_t get_tick(void) +{ + LONGLONG ret; + LARGE_INTEGER now; + + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&now); + ret = now.QuadPart - sys_start_time.QuadPart; + return (uint32_t)((ret * 1000) / freq.QuadPart); +} diff --git a/main.c b/main.c new file mode 100644 index 0000000..245f29c --- /dev/null +++ b/main.c @@ -0,0 +1,14 @@ +#include +#include +#include "ebtn.h" +#include "windows.h" + +extern int example_test(void); +extern int example_user(void); + +int main(void) +{ + example_test(); + // example_user(); + return 0; +}