├── .gitattributes ├── .github └── workflows │ ├── platformio-build.yml │ ├── tools-build-lin.yml │ ├── tools-build-mac.yml │ └── tools-build-win.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEBUGGING.md ├── LICENSE ├── README-MQTT.md ├── README.md ├── bin ├── blank4mb.bin ├── esptool.exe └── flash.bat ├── demo ├── board.jpg ├── mqtt-settings.png └── showcase.gif ├── platformio.ini ├── scripts ├── DBGdeploy.py ├── GENdeploy.py └── OBdeploy.py ├── src ├── PN532.cpp ├── PN532.h ├── Utils.cpp ├── Utils.h ├── beeper.esp ├── config.esp ├── config.h ├── door.esp ├── doorbell.esp ├── helpers.esp ├── led.esp ├── log.esp ├── magicnumbers.h ├── main.cpp ├── mqtt.esp ├── rfid.esp ├── rfid125kHz.cpp ├── rfid125kHz.h ├── todo ├── webh │ ├── esprfid.htm.gz.h │ ├── esprfid.js.gz.h │ ├── glyphicons-halflings-regular.woff.gz.h │ ├── index.html.gz.h │ ├── required.css.gz.h │ └── required.js.gz.h ├── webserver.esp ├── websocket.esp ├── websrc │ ├── 3rdparty │ │ ├── css │ │ │ ├── bootstrap-3.3.7.min.css │ │ │ ├── footable.bootstrap-3.1.6.min.css │ │ │ └── sidebar.css │ │ ├── fonts │ │ │ └── glyphicons-halflings-regular.woff │ │ └── js │ │ │ ├── 01-jquery-1.12.4.min.js │ │ │ ├── 02-bootstrap-3.3.7.min.js │ │ │ └── 03-footable-3.1.6.min.js │ ├── esprfid.htm │ ├── index.html │ └── js │ │ └── esprfid.js ├── wifi.esp └── wsResponses.esp └── tools ├── README.md ├── webfilesbuilder ├── bin.js ├── gulpfile.js ├── package-lock.json └── package.json └── wsemulator ├── package-lock.json ├── package.json └── wserver.js /.gitattributes: -------------------------------------------------------------------------------- 1 | src/websrc/3rdparty/* linguist-vendored 2 | src/webh/* linguist-vendored 3 | 4 | *.esp linguist-language=C++ -------------------------------------------------------------------------------- /.github/workflows/platformio-build.yml: -------------------------------------------------------------------------------- 1 | name: Platformio build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.10 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: "3.10" 16 | - name: Install Platformio 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install platformio 20 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 21 | - name: Run Platformio builds 22 | run: | 23 | platformio run -e generic -e debug 24 | - name: Export bins 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: bins 28 | path: | 29 | bin/generic.bin 30 | bin/forV2Board.bin 31 | bin/debug.bin 32 | -------------------------------------------------------------------------------- /.github/workflows/tools-build-lin.yml: -------------------------------------------------------------------------------- 1 | name: Tools build for Linux 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: '16' 13 | - name: Install pkg 14 | run: npm i -g pkg 15 | - name: Generate webfilesbuilder binaries 16 | run: | 17 | cd tools/webfilesbuilder 18 | npm install 19 | pkg -t node16-linux -C GZip . 20 | - name: Generate wsemulator binaries 21 | run: | 22 | cd tools/wsemulator 23 | npm install 24 | pkg -t node16-linux -C GZip . 25 | 26 | - name: Export Linux binary 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: bins 30 | path: | 31 | tools/webfilesbuilder/webfilesbuilder 32 | tools/wsemulator/wsemulator 33 | -------------------------------------------------------------------------------- /.github/workflows/tools-build-mac.yml: -------------------------------------------------------------------------------- 1 | name: Tools build for Mac 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: '16' 13 | - name: Install pkg 14 | run: npm i -g pkg 15 | - name: Generate webfilesbuilder binaries 16 | run: | 17 | cd tools/webfilesbuilder 18 | npm install 19 | pkg -t node16-mac -C GZip . 20 | - name: Generate wsemulator binaries 21 | run: | 22 | cd tools/wsemulator 23 | npm install 24 | pkg -t node16-mac -C GZip . 25 | 26 | - name: Export Mac binary 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: bins 30 | path: | 31 | tools/webfilesbuilder/webfilesbuilder 32 | tools/wsemulator/wsemulator 33 | -------------------------------------------------------------------------------- /.github/workflows/tools-build-win.yml: -------------------------------------------------------------------------------- 1 | name: Tools build for Windows 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: '16' 13 | - name: Install pkg 14 | run: npm i -g pkg 15 | - name: Generate webfilesbuilder binaries 16 | run: | 17 | cd tools/webfilesbuilder 18 | npm install 19 | pkg -t node16-win -C GZip . 20 | - name: Generate wsemulator binaries 21 | run: | 22 | cd tools/wsemulator 23 | npm install 24 | pkg -t node16-win -C GZip . 25 | 26 | - name: Export Windows binary 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: bins 30 | path: | 31 | tools/webfilesbuilder/webfilesbuilder.exe 32 | tools/wsemulator/wsemulator.exe 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # platformio 5 | 6 | .pio 7 | .pioenvs 8 | .piolibdeps 9 | .clang_complete 10 | .gcc-flags.json 11 | .pio 12 | 13 | # esp-rfid 14 | src/websrc/css/required.css 15 | src/websrc/js/required.js 16 | src/websrc/fonts 17 | src/websrc/gzipped 18 | src/websrc/process 19 | 20 | bin/generic.bin 21 | bin/forV2Board.bin 22 | bin/debug.bin 23 | 24 | # node js 25 | 26 | # Logs 27 | logs 28 | *.log 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # Runtime data 34 | pids 35 | *.pid 36 | *.seed 37 | *.pid.lock 38 | 39 | # Directory for instrumented libs generated by jscoverage/JSCover 40 | lib-cov 41 | 42 | # Coverage directory used by tools like istanbul 43 | coverage 44 | 45 | # nyc test coverage 46 | .nyc_output 47 | 48 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 49 | .grunt 50 | 51 | # Bower dependency directory (https://bower.io/) 52 | bower_components 53 | 54 | # node-waf configuration 55 | .lock-wscript 56 | 57 | # Compiled binary addons (https://nodejs.org/api/addons.html) 58 | build/Release 59 | 60 | # Dependency directories 61 | node_modules/ 62 | jspm_packages/ 63 | 64 | # Typescript v1 declaration files 65 | typings/ 66 | 67 | # Optional npm cache directory 68 | .npm 69 | 70 | # Optional eslint cache 71 | .eslintcache 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # dotenv environment variables file 83 | .env 84 | 85 | # next.js build output 86 | .next 87 | .vscode/.browse.c_cpp.db* 88 | .vscode/c_cpp_properties.json 89 | .vscode/launch.json 90 | .vscode/* 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contributo to esp-rfid 2 | 3 | If you want to contribute, first of all, thank you very much! 4 | 5 | ## If you want to contribute some code 6 | 7 | ### Bug fix 8 | 9 | In case of a bug fix you can open a PR on the `stable` branch. 10 | 11 | 12 | ### New feature 13 | 14 | For new features please open a PR on the `dev` branch which will be merged on `stable` when a new release will be launched. 15 | 16 | Remember to add documentation in the main readme and in the changelog. If MQTT is affected, please also update the README-MQTT.md file. 17 | 18 | When touching the configuration file, you should make sure that the old file will be supported by the new version and that your feature should work also with the old config file format. 19 | 20 | ### Frontend 21 | 22 | You cannot simply edit Web UI files because you will need to convert them to C arrays, which can be done automatically by a gulp script that can be found in tools directory or you can use compiled executables at the same directory as well (for Windows PCs only). 23 | 24 | If you want to edit esp-rfid's Web UI you will need (unless using compiled executables): 25 | * NodeJS 26 | * npm (comes with NodeJS installer) 27 | * Gulp (can be installed with npm) 28 | 29 | Gulp script also minifies HTML and JS files and compresses (gzip) them. 30 | 31 | To minify and compress the frontend, enter the folder ```tools/webfilesbuilder``` and: 32 | * Run ```npm install``` to install dependencies 33 | * Run ```npm start``` to compress the web UI to make it ready for the ESP 34 | 35 | In order to test your changes without flashing the firmware you can launch websocket emulator which is included in tools directory. 36 | * You will need to Node JS for websocket emulator. 37 | * Run ```npm install``` to install dependencies 38 | * Run emulator ```node wserver.js``` 39 | 40 | There are two alternative ways to test the UI 41 | 1. you can launch your browser with CORS disabled: 42 | ```chrome.exe --args --disable-web-security -–allow-file-access-from-files --user-data-dir="C:\Users\USERNAME"``` 43 | and then open the HTML files directly (Get more information [here](https://stackoverflow.com/questions/3102819/disable-same-origin-policy-in-chrome)) 44 | 2. alternatively, you can launch a web server from the ```src/websrc``` folder, for example with Python, like this: 45 | ```python3 -m http.server``` 46 | and then visit ```http://0.0.0.0:8000/``` 47 | 48 | When testing locally, use the password ```neo``` for admin capabilities. 49 | 50 | ## TODO 51 | 52 | Explain more ways to help that are not code-related -------------------------------------------------------------------------------- /DEBUGGING.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | When the ESP crashes and it's connected to the serial port in debug mode, you can get its stacktrace. 4 | 5 | But then you need to decode it to see what's going on. To do that you need to: 6 | 7 | - install https://github.com/janLo/EspArduinoExceptionDecoder/ 8 | 9 | - save the stacktrace in a file, e.g. debug.txt 10 | 11 | - run `python3 ~/.platformio/packages/toolchain-xtensa/decoder.py -e .pio/build/debug/firmware.elf debug.txt -s` 12 | 13 | References: 14 | - https://github.com/esp8266/Arduino/blob/master/doc/faq/a02-my-esp-crashes.rst 15 | - https://arduino-esp8266.readthedocs.io/en/latest/exception_causes.html 16 | - https://arduino-esp8266.readthedocs.io/en/latest/Troubleshooting/stack_dump.html 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 esp-rfid Community 4 | Copyright (c) 2017 Ömer Şiar Baysal 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README-MQTT.md: -------------------------------------------------------------------------------- 1 | # ESP RFID with extended MQTT Functions 2 | 3 | Hardware: 4 | * Any esp-rfid board like esp-rfid-relay-board, marelab ESP-DOOR 5 | 6 | This has been added so far: 7 | * Reading all user data over MQTT 8 | * Sending User data to RFID-DOOR/ESP-RFID over MQTT 9 | * Sending door open command over MQTT 10 | * Sending door status over MQTT as event 11 | * Sending Sync of a RFID-DOOR (IP/Hostname) over MQTT 12 | * Configure Sync interval over ESP-RFID GUI 13 | * Deleting all User of a ESP-RFID device over MQTT 14 | * Sending log event & access data over MQTT 15 | * Demo NODE-RED flow & GUI to centralize managment of ESP-RFID devices & users 16 | 17 | ## Broker settings 18 | You can add all the broker details in the web UI: 19 | 20 | ![MQTT settings](./demo/mqtt-settings.png) <- to update 21 | 22 | ## Using MQTT Topics 23 | For the MQTT communication some additional TOPICs have been added internally. The default Topic is configured in the web UI. If you use more then one device, every device should have the same `TOPIC` name configured. All MQTT communication is done with JSON Payload as MQTT Message. 24 | 25 | This is the used Topic hierarchy: 26 | 27 | ``` 28 | TOPIC---+---/send 29 | | 30 | +---/cmd 31 | ``` 32 | 33 | e.g. if you configure in the web UI `TOPIC` = "/rfid" these topic queues can be used: 34 | * /rfid 35 | * /rfid/send 36 | * /rfid/cmd 37 | 38 | ### Auto-topic 39 | 40 | If auto-topic option is selected, esp-rfid will add the last 6 characters from the device MAC address to the MQTT topic. 41 | 42 | This can be useful to deploy a batch of esp-rfid in one go. By knowing the MAC addresses in advance you can setup them all with a standard configuration and each one will talk on a separate topic. 43 | 44 | ## Commands received by ESP-RFID 45 | The message format is JSON. 46 | 47 | The message has to include the IP of the device together with one of the supported commands. 48 | 49 | The commands should be sent to the `TOPIC/cmd` and the responses will be sent on the topic `TOPIC/send`. 50 | 51 | ### Get all the users 52 | When sending the following command: 53 | ``` 54 | { 55 | "cmd":"getuserlist", 56 | "doorip":"(The ESP-RFID IP address as String)" 57 | } 58 | ``` 59 | 60 | A list of messages like the following will be sent, one for each user: 61 | ``` 62 | { 63 | "command": "userfile", 64 | "uid": "1234", 65 | "user": "User Name", 66 | "acctype": 1, 67 | "acctype2": null, 68 | "acctype3": null, 69 | "acctype4": null, 70 | "validsince": 0, 71 | "validuntil": 1608336000 72 | } 73 | ``` 74 | 75 | ### Open door 76 | Opens the Door/Magnetic Lock. 77 | 78 | Command: 79 | ``` 80 | { 81 | "cmd":"opendoor", 82 | "doorip":"(The ESP-RFID IP of the door to open as String)" 83 | } 84 | ``` 85 | 86 | Response will be a standard [Publish Access](#publish-access) message. 87 | 88 | ### Delete user 89 | Delete a single user by UID. 90 | 91 | Command: 92 | ``` 93 | { 94 | "cmd":"deletuid", 95 | "doorip":"(The ESP-RFID IP address as String)", 96 | "uid":"(The UID of the user to delete as String)" 97 | } 98 | ``` 99 | 100 | Response will be an acknowledgment to let the other party know that the message was processed: 101 | ``` 102 | { 103 | "type":"deletuid", 104 | "ip":"192.168.1.xxx", 105 | "hostname":"your esp-rfid hostname" 106 | } 107 | ``` 108 | 109 | ### Delete all users 110 | Delete all user files. 111 | 112 | Command: 113 | ``` 114 | { 115 | "cmd":"deletusers", 116 | "doorip":"(The ESP-RFID IP address as String)" 117 | } 118 | ``` 119 | 120 | Response will be an acknowledgment to let the other party know that the message was processed: 121 | ``` 122 | { 123 | "type":"deletusers", 124 | "ip":"192.168.1.xxx", 125 | "hostname":"your esp-rfid hostname" 126 | } 127 | ``` 128 | 129 | ### Add user 130 | Adds a user as a file to the device. 131 | 132 | Command: 133 | ``` 134 | { 135 | "cmd":"adduser", 136 | "doorip":"(The ESP-RFID IP address as String)", 137 | "uid": "(The PIN as String)", 138 | "user": "(User Name as String)", 139 | "acctype": "1", 140 | "validsince": "0", 141 | "validuntil": "1608466200" 142 | } 143 | ``` 144 | * _acctype_ 145 | * 0 = Disabled 146 | * 1 = Always 147 | * 99 = Admin 148 | 149 | * _validsince_ 150 | * User valid since date/time as Unix epoch timestamp 151 | * Can send calculations based on now: 152 | * ```validsince: {{ (as_timestamp(now()) + (2*24*3600)) }}``` 153 | 154 | * _validuntil_ 155 | * Expiration date/time as Unix epoch timestamp 156 | * Can send calculations based on now: 157 | * ```validuntil: {{ (as_timestamp(now()) + (2*24*3600)) }}``` 158 | 159 | Response will be an acknowledgment to let the other party know that the message was processed: 160 | ``` 161 | { 162 | "type":"adduser", 163 | "ip":"192.168.1.xxx", 164 | "hostname":"your esp-rfid hostname" 165 | } 166 | ``` 167 | 168 | ### Get configuration 169 | Get the global configuration. 170 | 171 | Command: 172 | ``` 173 | { 174 | "cmd":"getconf", 175 | "doorip":"(The ESP-RFID IP address as String)" 176 | } 177 | ``` 178 | 179 | Response will be an object with a `configfile` key which holds the entire configuration object. The same object that you can download from the "Backup & Restore" section. 180 | ``` 181 | { 182 | "type":"getconf", 183 | "ip":"192.168.1.xxx", 184 | "hostname":"your esp-rfid hostname", 185 | "configfile": { 186 | // the entire configuration object 187 | } 188 | } 189 | ``` 190 | 191 | ### Update configuration 192 | Update the global configuration. You can pass a configuration object, which will be used as the new configuration. Then the system will restart to load the new configuration. 193 | 194 | Command: 195 | ``` 196 | { 197 | "cmd":"updateconf", 198 | "doorip":"(The ESP-RFID IP address as String)", 199 | "configfile": { 200 | // the entire configuration object 201 | } 202 | } 203 | ``` 204 | 205 | Response will be an acknowledgment to let the other party know that the message was processed: 206 | ``` 207 | { 208 | "type":"updateconf", 209 | "ip":"192.168.1.xxx", 210 | "hostname":"your esp-rfid hostname" 211 | } 212 | ``` 213 | 214 | Then the system will automatically restart to use the new configuration. 215 | 216 | ## Messages sent by ESP-RFID 217 | ESP-RFID sends a set of MQTT messages for the most significant actions that it does, plus can be configured to send all the logs over MQTT, instead of keeping them locally. 218 | 219 | All the messages are sent at the topic: `TOPIC/send`. 220 | 221 | For Home Assistant, check the specific topics and messages below. 222 | 223 | ### Base messages 224 | 225 | #### Boot 226 | Once booted and connected to the WiFi, it sends this message. 227 | 228 | ``` 229 | { 230 | "type":"boot", 231 | "time":1605987212, 232 | "uptime":0, 233 | "ip":"192.168.1.xxx", 234 | "hostname":"your esp-rfid hostname" 235 | } 236 | ``` 237 | 238 | #### Heartbeat 239 | Every X seconds ESP-RFID sends a heartbeat over MQTT. The interval can be customised in the web UI, the default is 180 seconds. 240 | 241 | ``` 242 | { 243 | "type":"heartbeat", 244 | "time":1605991375, 245 | "uptime":999, 246 | "ip":"192.168.1.xxx", 247 | "hostname":"your esp-rfid hostname" 248 | } 249 | ``` 250 | 251 | #### Publish Access 252 | When a RFID token is detected a set of messages can be sent, depending on the presence of the token UID in the database. 253 | 254 | If the UID is in the users list, there can be a set of possible "access" configurations. It can be: 255 | 256 | * `Admin` for admin users 257 | * `Always` for access enabled 258 | * `Disabled` for access disabled 259 | * `Expired` for access expired 260 | 261 | ``` 262 | { 263 | "type":"access", 264 | "time":1605991375, 265 | "isKnown":"true", 266 | "access":"the access state", 267 | "username":"username", 268 | "uid":"token UID", 269 | "pincode":"user pincode", 270 | "hostname":"your esp-rfid hostname", 271 | "doorName":"your door name" 272 | } 273 | ``` 274 | If instead the UID is not present in the users list the message will be: 275 | 276 | ``` 277 | { 278 | "type":"access", 279 | "time":1605991375, 280 | "isKnown":"false", 281 | "access":"Denied", 282 | "username":"Unknown", 283 | "uid":"token UID", 284 | "pincode":"user pincode", 285 | "hostname":"your esp-rfid hostname" 286 | } 287 | ``` 288 | 289 | If the tag is unknown the message will be different: 290 | 291 | ``` 292 | { 293 | "type":"WARN", 294 | "src":"rfid", 295 | "desc":"Unknown rfid tag is scanned", 296 | "data":"", 297 | "time":1605991375, 298 | "cmd":"event", 299 | "hostname":"your esp-rfid hostname" 300 | } 301 | ``` 302 | 303 | In case of multiple doors managed by one esp-rfid, you'll get an array for doorname and for access: 304 | 305 | ``` 306 | { 307 | "type":"access", 308 | "time":1605991375, 309 | "isKnown":"true", 310 | "access":["the access state door 1", "access state door 2"], 311 | "username":"username", 312 | "uid":"token UID", 313 | "pincode":"user pincode", 314 | "hostname":"your esp-rfid hostname", 315 | "doorName":["door 1", "door 2"] 316 | } 317 | ``` 318 | 319 | ### Log messages 320 | Besides the above messages, ESP-RFID can send all the logs via MQTT instead of storing those locally. If this is enabled via the web UI, also the following messages are sent. 321 | 322 | #### Door status 323 | If the door sensor is enabled, two messages are sent, one when the door is opened: 324 | 325 | ``` 326 | { 327 | "type":"INFO", 328 | "src":"door", 329 | "desc":"Door Open", 330 | "data":"", 331 | "time":1605991375, 332 | "cmd":"event", 333 | "hostname":"your esp-rfid hostname" 334 | } 335 | ``` 336 | 337 | And one when the door is closed: 338 | 339 | ``` 340 | { 341 | "type":"INFO", 342 | "src":"door", 343 | "desc":"Door Closed", 344 | "data":"", 345 | "time":1605991375, 346 | "cmd":"event", 347 | "hostname":"your esp-rfid hostname" 348 | } 349 | ``` 350 | 351 | #### Doorbell 352 | If the doorbell is enabled, a message is sent when it's ringing: 353 | 354 | ``` 355 | { 356 | "type":"INFO", 357 | "src":"doorbell", 358 | "desc":"Doorbell ringing", 359 | "data":"", 360 | "time":1605991375, 361 | "cmd":"event", 362 | "hostname":"your esp-rfid hostname" 363 | } 364 | ``` 365 | 366 | #### Tamper status 367 | If the door is tampered, or open when it shouldn't be a message is sent: 368 | 369 | ``` 370 | { 371 | "type":"WARN", 372 | "src":"tamper", 373 | "desc":"Door was tampered!", 374 | "data":"", 375 | "time":1605991375, 376 | "cmd":"event", 377 | "hostname":"your esp-rfid hostname" 378 | } 379 | ``` 380 | 381 | And then if the open is not closed before the maximum allowed time: 382 | 383 | ``` 384 | { 385 | "type":"WARN", 386 | "src":"tamper", 387 | "desc":"Door wasn't closed within max open time!", 388 | "data":"", 389 | "time":1605991375, 390 | "cmd":"event", 391 | "hostname":"your esp-rfid hostname" 392 | } 393 | ``` 394 | 395 | #### WiFi status 396 | Some messages around WiFi handling are also sent. 397 | 398 | Enabling/disabling of WiFi: 399 | 400 | ``` 401 | { 402 | "type":"INFO", 403 | "src":"wifi", 404 | "desc":"WiFi is going to be disabled / Enabling WiFi", 405 | "data":"", 406 | "time":1605991375, 407 | "cmd":"event", 408 | "hostname":"your esp-rfid hostname" 409 | } 410 | ``` 411 | 412 | #### MQTT status 413 | MQTT connection or message handling related. 414 | 415 | On connecting to the broker: 416 | ``` 417 | { 418 | "type":"INFO", 419 | "src":"mqtt", 420 | "desc":"Connected to MQTT Server", 421 | "data":"Session Present", 422 | "time":1605991375, 423 | "cmd":"event", 424 | "hostname":"your esp-rfid hostname" 425 | } 426 | ``` 427 | 428 | On disconnecting from the broker: 429 | ``` 430 | { 431 | "type":"WARN", 432 | "src":"mqtt", 433 | "desc":"Disconnected from MQTT server", 434 | "data":"reason of disconnection", 435 | "time":1605991375, 436 | "cmd":"event", 437 | "hostname":"your esp-rfid hostname" 438 | } 439 | ``` 440 | 441 | When the ESP8266 cannot store in memory incoming messages due to low memory a message will be sent back: 442 | ``` 443 | { 444 | "type":"ERRO", 445 | "src":"mqtt", 446 | "desc":"Dropping MQTT message, out of memory", 447 | "data":"", 448 | "time":1605991375, 449 | "cmd":"event", 450 | "hostname":"your esp-rfid hostname" 451 | } 452 | ``` 453 | 454 | #### System messages 455 | When the system configuration is done and the system is up and running 456 | ``` 457 | { 458 | "type":"INFO", 459 | "src":"sys", 460 | "desc":"System setup completed, running", 461 | "data":"", 462 | "time":1605991375, 463 | "cmd":"event", 464 | "hostname":"your esp-rfid hostname" 465 | } 466 | ``` 467 | 468 | Before performing a requested reboot: 469 | ``` 470 | { 471 | "type":"INFO", 472 | "src":"sys", 473 | "desc":"System is going to reboot", 474 | "data":"", 475 | "time":1605991375, 476 | "cmd":"event", 477 | "hostname":"your esp-rfid hostname" 478 | } 479 | ``` 480 | 481 | Before performing a scheduled reboot: 482 | ``` 483 | { 484 | "type":"WARN", 485 | "src":"sys", 486 | "desc":"Auto restarting...", 487 | "data":"", 488 | "time":1605991375, 489 | "cmd":"event", 490 | "hostname":"your esp-rfid hostname" 491 | } 492 | ``` 493 | 494 | On a FS format: 495 | ``` 496 | { 497 | "type":"WARN", 498 | "src":"sys", 499 | "desc":"Filesystem formatted", 500 | "data":"", 501 | "time":1605991375, 502 | "cmd":"event", 503 | "hostname":"your esp-rfid hostname" 504 | } 505 | ``` 506 | 507 | When saving the configuration on SPIFFS: 508 | ``` 509 | { 510 | "type":"INFO", 511 | "src":"sys", 512 | "desc":"Config stored in the SPIFFS", 513 | "data":"xxx bytes", 514 | "time":1605991375, 515 | "cmd":"event", 516 | "hostname":"your esp-rfid hostname" 517 | } 518 | ``` 519 | 520 | After deleting all the event logs: 521 | ``` 522 | { 523 | "type":"WARN", 524 | "src":"sys", 525 | "desc":"Event log cleared!", 526 | "data":"", 527 | "time":1605991375, 528 | "cmd":"event", 529 | "hostname":"your esp-rfid hostname" 530 | } 531 | 532 | After deleting all the access logs: 533 | ``` 534 | { 535 | "type":"WARN", 536 | "src":"sys", 537 | "desc":"Latest Access log cleared!", 538 | "data":"", 539 | "time":1605991375, 540 | "cmd":"event", 541 | "hostname":"your esp-rfid hostname" 542 | } 543 | 544 | When starting the firmware update: 545 | ``` 546 | { 547 | "type":"INFO", 548 | "src":"updt", 549 | "desc":"Firmware update started", 550 | "data":"file name", 551 | "time":1605991375, 552 | "cmd":"event", 553 | "hostname":"your esp-rfid hostname" 554 | } 555 | 556 | If there's no space to upload the firmware: 557 | ``` 558 | { 559 | "type":"ERRO", 560 | "src":"updt", 561 | "desc":"Not enough space to update", 562 | "data":"", 563 | "time":1605991375, 564 | "cmd":"event", 565 | "hostname":"your esp-rfid hostname" 566 | } 567 | 568 | If the firmware update failed because it's not possible to write on flash: 569 | ``` 570 | { 571 | "type":"ERRO", 572 | "src":"updt", 573 | "desc":"Writing to flash is failed", 574 | "data":"file name", 575 | "time":1605991375, 576 | "cmd":"event", 577 | "hostname":"your esp-rfid hostname" 578 | } 579 | 580 | If the firmware update successfully finished: 581 | ``` 582 | { 583 | "type":"INFO", 584 | "src":"updt", 585 | "desc":"Firmware update is finished", 586 | "data":"", 587 | "time":1605991375, 588 | "cmd":"event", 589 | "hostname":"your esp-rfid hostname" 590 | } 591 | 592 | If the firmware update failed: 593 | ``` 594 | { 595 | "type":"ERRO", 596 | "src":"updt", 597 | "desc":"Update is failed", 598 | "data":"", 599 | "time":1605991375, 600 | "cmd":"event", 601 | "hostname":"your esp-rfid hostname" 602 | } 603 | 604 | ### Home Assistant messages 605 | 606 | #### Boot sequence 607 | When esp-rfid finishes the boot sequence and connects to WiFi and MQTT broker sends HA-specific messages to setup: lock, door, doorbell, tag, user, door tamper, avty. 608 | 609 | #### IO messages 610 | During normal usage, esp-rfid sends to Home Assistant messages on the `/io/*` topics for the following: 611 | - door status, open/closed 612 | - door tamper 613 | - doorbell on/off 614 | - lock locked/unlocked 615 | 616 | 617 | #### Publish access 618 | Similarly to what is published for the standard MQTT settings, when Home Assistant is setup, esp-rfid sends a set of messages when a rfid card is swiped. 619 | 620 | Read above for the possible cases, the significant part fo HA is that the topic to which the message is sent is different: `TOPIC/tag`. 621 | 622 | And the message looks like this: 623 | 624 | ``` 625 | { 626 | "uid":"token UID", 627 | "username":"username", 628 | "pincode":"user pincode", 629 | "access":"the access state", 630 | "time":1605991375, 631 | } 632 | ``` 633 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP RFID - Access Control with ESP8266, RC522 PN532 Wiegand RDM6300 2 | 3 | [![Chat at https://gitter.im/esp-rfid/Lobby](https://badges.gitter.im/esp-rfid.svg)](https://gitter.im/esp-rfid/Lobby) [![Backers on Open Collective](https://opencollective.com/esp-rfid/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/esp-rfid/sponsors/badge.svg)](#sponsors) 4 | 5 | Access Control system using a cheap MFRC522, PN532 RFID, RDM6300 readers or Wiegand RFID readers and Espressif's ESP8266 Microcontroller. 6 | 7 | ![Showcase Gif](https://raw.githubusercontent.com/esprfid/esp-rfid/stable/demo/showcase.gif)[![Board](https://raw.githubusercontent.com/esprfid/esp-rfid/stable/demo/board.jpg)](https://www.tindie.com/products/nardev/esp-rfid-relay-blue-board/) 8 | 9 | ## Features 10 | ### For Users 11 | * Minimal effort for setting up your Access Control system, just flash and everything can be configured via Web UI 12 | * Capable of managing up to 1.000 Users (even more is possible) 13 | * Great for Maker Spaces, Labs, Schools, etc 14 | * Cheap to build and easy to maintain 15 | ### For Tinkerers 16 | * Open Source (minimum amount of hardcoded variable, this means more freedom) 17 | * Using WebSocket protocol to exchange data between Hardware and Web Browser 18 | * Data is encoded as JSON object 19 | * Records are Timestamped (Time synced from a NTP Server) 20 | * MQTT enabled 21 | * Bootstrap, jQuery, FooTables for beautiful Web Pages for both Mobile and Desktop Screens 22 | * Thanks to ESPAsyncWebServer Library communication is Asynchronous 23 | ### Official Hardware 24 | * Small size form factor, sometimes it is possible to glue it into existing readers. 25 | * Single power source to power 12V/2A powers ESP12 module, RFID Wiegand Reader and magnetic lock for opening doors. 26 | * Exposed programming pins for ESP8266 27 | * Regarding hardware design, you get multiple possible setup options: 28 | * Forward Bell ringing on reader to MCU or pass it out of board 29 | * Track Door Status 30 | * Control reader’s status LED 31 | * Control reader’s status BUZZER sound * 32 | * Power reader, lock and the board through single 12V, 2A PSU 33 | * Optionally power magnetic lock through external AC/DC PSU 34 | * Possible to use any kind and any type of Wiegand readers 35 | * Enables you to make IOT Access System with very little wiring 36 | * Fits in an universal enclosures with DIN mount 37 | * Open Source Hardware 38 | 39 | Get more information and see accessory options from [Tindie Store](https://www.tindie.com/products/nardev/esp-rfid-relay-blue-board/) 40 | 41 | | What are others saying about esp-rfid? | 42 | | ---- | 43 | | _“Hi, nice project.”_ – [@Rotzbua]() | 44 | | _“Your app works like a charm”_ – [@tueddy ]() | 45 | | _“Just stumbled upon this project while planning to do something similar. Very beautifully done!”_ – [@LifeP]() | 46 | | _“Hello, I've come across your project and first of all… wow - thanks to all contributors for your hard work!”_ – [@byt3w4rri0r]() | 47 | | _“Brilliant work.”_ – [@danbicks]() | 48 | | _“This is an impressive project.”_ – [@appi1]() | 49 | | _“I'd like to thank every single contributor for creating this epic project.”_ – [@TheCellMc]() | 50 | | _“Congratulations for your awesome work! This project is absolutely brilliant.”_ – [@quikote]() | 51 | 52 | ## Getting Started 53 | This project still in its development phase. New features (and also bugs) are introduced often and some functions may become deprecated. Please feel free to comment or give feedback. 54 | 55 | * Get the latest release from [here](https://github.com/esprfid/esp-rfid/releases). 56 | * See [Known Issues](https://github.com/esprfid/esp-rfid#known-issues) before starting right away. 57 | * See [Security](https://github.com/esprfid/esp-rfid#security) for your safety. 58 | * See [ChangeLog](https://github.com/esprfid/esp-rfid/blob/dev/CHANGELOG.md) 59 | 60 | ### What You Will Need 61 | ### Hardware 62 | * [Official ESP-RFID Relay Board](https://www.tindie.com/products/nardev/esp-rfid-relay-blue-board/) 63 | or 64 | * An ESP8266 module or a development board like **WeMos D1 mini** or **NodeMcu 1.0** with at least **32Mbit Flash (equals to 4MBytes)** (ESP32 is not supported for now) 65 | * A MFRC522 RFID PCD Module or PN532 NFC Reader Module or RDM6300 125KHz RFID Module Wiegand based RFID reader 66 | * A Relay Module (or you can build your own circuit) 67 | * n quantity of Mifare Classic 1KB (recommended due to available code base) PICCs (RFID Tags) equivalent to User Number 68 | 69 | ### Software 70 | 71 | #### Using Compiled Binaries 72 | Download compiled binaries from GitHub Releases page 73 | https://github.com/esprfid/esp-rfid/releases 74 | 75 | On Windows you can use **"flash.bat"**, it will ask you which COM port that ESP is connected and then flashes it. You can use any flashing tool and do the flashing manually. The flashing process itself has been described at numerous places on Internet. 76 | 77 | #### Building With PlatformIO 78 | 79 | The build environment is based on [PlatformIO](http://platformio.org). Follow the instructions found here: http://platformio.org/#!/get-started for installing it but skip the ```platform init``` step as this has already been done, modified and it is included in this repository. In summary: 80 | 81 | ``` 82 | sudo pip install -U pip setuptools 83 | sudo pip install -U platformio 84 | git clone https://github.com/esprfid/esp-rfid.git 85 | cd esp-rfid 86 | platformio run 87 | ``` 88 | 89 | When you run ```platformio run``` for the first time, it will download the toolchains and all necessary libraries automatically. 90 | 91 | ##### Useful commands: 92 | 93 | * ```platformio run``` - process/build all targets 94 | * ```platformio run -e generic -t upload``` - process/build and flash just the ESP12e target (the NodeMcu v2) 95 | * ```platformio run -t clean``` - clean project (remove compiled files) 96 | 97 | The resulting (built) image(s) can be found in the directory ```/bin``` created during the build process. 98 | 99 | ##### How to modify the project 100 | 101 | If you want to modify the code, you can read more info in the [CONTRIBUTING](./CONTRIBUTING.md) file. 102 | 103 | 104 | ### Pin Layout 105 | 106 | The following table shows the typical pin layout used for connecting readers hardware to ESP: 107 | 108 | | ESP8266 | NodeMcu/WeMos | Wiegand | PN532 | MFRC522 | RDM6300 | 109 | |--------:|:-------------:|:-------:|:-------------:|:-------:|:-------:| 110 | | GPIO-16 | D0 | | SS (Wemos D1) | SDA/SS | | 111 | | GPIO-15 | D8 | | | SDA/SS | | 112 | | GPIO-13 | D7 | D0 | MOSI | MOSI | | 113 | | GPIO-12 | D6 | D1 | MISO | MISO | | 114 | | GPIO-14 | D5 | | SCK | SCK | | 115 | | GPIO-04 | D2 | | | | TX | 116 | | GPIO-05 | D1 | | SS | | | 117 | 118 | For Wiegand based readers, you can configure D0 and D1 pins via settings page. By default, D0 is GPIO-4 and D1 is GPIO-5 119 | 120 | ### Steps 121 | * First, flash firmware (you can use /bin/flash.bat on Windows) to your ESP either using Arduino IDE or with your favourite flash tool 122 | * (optional) Fire up your serial monitor to get informed 123 | * Search for Wireless Network "esp-rfid-xxxxxx" and connect to it (It should be an open network and does not require password) 124 | * Open your browser and visit either "http://192.168.4.1" or "http://esp-rfid.local" (.local needs Bonjour installed on your computer). 125 | * Log on to ESP, default password is "admin" 126 | * Go to "Settings" page 127 | * Configure your amazing access control device. Push "Scan" button to join your wireless network, configure RFID hardware, Relay Module. 128 | * Save settings, when rebooted your ESP will try to join your wireless network. 129 | * Check your new IP address from serial monitor and connect to your ESP again. (You can also connect to "http://esp-rfid.local") 130 | * Go to "Users" page 131 | * Scan a PICC (RFID Tag) then it should glimpse on your Browser's screen. 132 | * Type "User Name" or "Label" for the PICC you scanned. 133 | * Choose "Allow Access" if you want to 134 | * Click "Add" 135 | * Congratulations, everything went well, if you encounter any issue feel free to ask help on GitHub. 136 | 137 | ### MQTT 138 | You can integrate ESP-RFID with other systems using MQTT. Read the [additional documentation](./README-MQTT.md) for all the details. 139 | 140 | ### Known Issues 141 | * You need to connect your MFRC522 reader to your ESP properly or you will end up with a boot loop 142 | * Please also check [GitHub issues](https://github.com/esprfid/esp-rfid/issues). 143 | 144 | #### Time 145 | We are syncing time from a NTP Server (in Client -aka infrastructure- Mode). This will require ESP to have an Internet connection. Additionally your ESP can also work without Internet connection (Access Point -aka Ad-Hoc- Mode), without giving up functionality. 146 | This will require you to sync time manually. ESP can store and hold time for you approximately 51 days without major issues, device time can drift from actual time depending on usage, temperature, etc. so you have to login to settings page and sync it in a timely fashion. 147 | Timezones are supported with automatic switch to and from daylight saving time. 148 | 149 | ## **Security** 150 | We assume **ESP-RFID** project -as a whole- does not offer strong security. There are PICCs available that their UID (Unique Identification Numbers) can be set manually (Currently esp-rfid relies only UID to identify its users). Also there may be a bug in the code that may result free access to your belongings. And also, like every other network connected device esp-rfid is vulnerable to many attacks including Man-in-the-middle, Brute-force, etc. 151 | 152 | This is a simple, hobby grade project, do not use it where strong security is needed. 153 | 154 | What can be done to increase security? (by you and by us) 155 | 156 | * We are working on more secure ways to Authenticate RFID Tags. 157 | * You can disable wireless network to reduce attack surface. (This can be configured in Web UI Settings page) 158 | * Choose a strong password for the Web UI 159 | 160 | ## Scalability 161 | Since we are limited on both flash and ram size things may get ugly at some point in the future. You can find out some test results below. 162 | 163 | ### Tests 164 | 165 | #### 1) How many RFID Tag can be handled? 166 | Restore some randomly generated user data on File System worth: 167 | 168 | * 1000 separate "userfile" 169 | * random 4 Bytes long UID and 170 | * random User Names and 171 | * 4 bytes random Unix Time Stamp 172 | * each have "access type" 1 byte integer "1" or "0". 173 | 174 | Total 122,880 Bytes 175 | 176 | At least 1000 unique User (RFID Tag) can be handled, the test were performed on WeMos D1 mini. 177 | 178 | #### Additional testing is needed: 179 | 180 | * Logging needs testing. How long should it need to log access? What if a Boss needs whole year log? 181 | * Reliability on Flash (these NOR Flash have limited write cycle on their cells). It depends on manufacturer choice of Flash Chip and usage. 182 | 183 | ## Community 184 | 185 | [![Chat at https://gitter.im/esp-rfid/Lobby](https://badges.gitter.im/esp-rfid.svg)](https://gitter.im/esp-rfid/Lobby) Join community chat on Gitter 186 | 187 | ### Projects that are based on esp-rfid 188 | 189 | * [ESP-IO](https://github.com/Pako2/EventGhostPlugins/tree/master/ESP-IO) Project to manipulate GPIOs with EventGhost 190 | * [ESP-RCM](https://github.com/Pako2/esp-rcm) Room Climate Monitor with ESP8266, HTU21D, Si7021, AM2320 191 | * [ESP-RFID-PY](https://github.com/esprfid/esp-rfid-py) Micro-Python implementation of esp-rfid is also made available by @iBobik 192 | 193 | ### Acknowledgements 194 | 195 | - @rneurink 196 | - @thunderace 197 | - @zeraien 198 | - @nardev 199 | - @romanzava 200 | - @arduino12 201 | - @Pako2 202 | - @marelab 203 | 204 | See [ChangeLog](https://github.com/esprfid/esp-rfid/blob/dev/CHANGELOG.md) 205 | 206 | ## Donations 207 | [![OC](https://opencollective.com/esp-rfid/tiers/esp-rfid-user.svg?avatarHeight=56)](https://opencollective.com/esp-rfid) 208 | 209 | Developing fully open, extensively tested embedded software is hard and time consuming work. Please consider making donations to support developers behind this beautiful software. 210 | 211 | Donations **transparently** processed by **[Open Collective](https://opencollective.com/how-it-works)** and expenses are being made public by OC's open ledger. 212 | 213 | * 2017-10-03 [steinar-t](https://github.com/steinar-t) 214 | * 2017-12-10 [saschaludwig](https://github.com/saschaludwig) 215 | * 2018-10-02 Dennis Parsch 216 | * 2019-01-12 Chris-topher Slater 217 | * 2019-04-23 Klaus Blum 218 | * 2019-04-25 Andre Dieteich 219 | 220 | ## Contributors 221 | 222 | This project exists thanks to all the people who contribute. 223 | 224 | 225 | ## License 226 | The code parts written by ESP-RFID project's authors are licensed under [MIT License](https://github.com/esprfid/esp-rfid/blob/stable/LICENSE), 3rd party libraries that are used by this project are licensed under different license schemes, please check them out as well. 227 | -------------------------------------------------------------------------------- /bin/esptool.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esprfid/esp-rfid/a7b43a7c0e57fcb253237b376fb2169e626cbb78/bin/esptool.exe -------------------------------------------------------------------------------- /bin/flash.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | cls 3 | 4 | echo - [1] Flash Generic Firmware 5 | echo - [2] Flash Firmware for Official Hardware (v2) 6 | echo - [3] Erase the Firmware on ESP8266 by flashing empty file 7 | echo - [4] Flash Generic DEBUG Firmware 8 | 9 | set /p opt=Please choose an option eg. 1: 10 | 11 | 2>NUL CALL :%opt% 12 | IF ERRORLEVEL 1 CALL :DEFAULT_CASE 13 | 14 | :1 15 | set /p com=Enter which COM Port your ESP is connected eg. COM1 COM2 COM7: 16 | esptool.exe -vv -cd nodemcu -cb 921600 -cp %com% -ca 0x00000 -cf generic.bin 17 | GOTO EXIT_CASE 18 | :2 19 | set /p com=Enter which COM Port your ESP is connected eg. COM1 COM2 COM7: 20 | esptool.exe -vv -cd nodemcu -cb 921600 -cp %com% -ca 0x00000 -cf forV2Board.bin 21 | GOTO EXIT_CASE 22 | :3 23 | set /p com=Enter which COM Port your ESP is connected eg. COM1 COM2 COM7: 24 | esptool.exe -vv -cd nodemcu -cb 921600 -cp %com% -ca 0x00000 -cf blank4mb.bin 25 | GOTO EXIT_CASE 26 | :4 27 | set /p com=Enter which COM Port your ESP is connected eg. COM1 COM2 COM7: 28 | esptool.exe -vv -cd nodemcu -cb 921600 -cp %com% -ca 0x00000 -cf debug.bin 29 | GOTO EXIT_CASE 30 | :DEFAULT_CASE 31 | ECHO Unknown option "%opt%" 32 | GOTO END_CASE 33 | :END_CASE 34 | VER > NUL # reset ERRORLEVEL 35 | GOTO :EOF # return from CALL 36 | :EXIT_CASE 37 | exit 38 | 39 | 40 | -------------------------------------------------------------------------------- /demo/board.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esprfid/esp-rfid/a7b43a7c0e57fcb253237b376fb2169e626cbb78/demo/board.jpg -------------------------------------------------------------------------------- /demo/mqtt-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esprfid/esp-rfid/a7b43a7c0e57fcb253237b376fb2169e626cbb78/demo/mqtt-settings.png -------------------------------------------------------------------------------- /demo/showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esprfid/esp-rfid/a7b43a7c0e57fcb253237b376fb2169e626cbb78/demo/showcase.gif -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | default_envs = generic 3 | 4 | [common] 5 | platform = espressif8266@2.3.2 6 | f_cpu = 160000000L 7 | framework = arduino 8 | board = esp12e 9 | build_flags = -Wl,-Teagle.flash.4m2m.ld 10 | upload_speed = 460800 11 | monitor_speed = 115200 12 | lib_deps = 13 | ArduinoJson@6.19.1 14 | ESPAsyncTCP 15 | ESPAsyncUDP 16 | https://github.com/lorol/ESPAsyncWebServer 17 | AsyncMqttClient@0.9.0 18 | https://github.com/miguelbalboa/rfid#ea7ee3f3daafd46d0c5b8438ba41147c384a1f0d 19 | matjack1/Wiegand Protocol Library for Arduino - for esp-rfid@^1.1.1 20 | Time@1.5 21 | Bounce2@2.52 22 | 23 | ; boards which GPIO0 and RESET controlled using two NPN transistors as nodemcu devkit (includes wemos d1 mini) 24 | [env:generic] 25 | board_build.f_cpu = ${common.f_cpu} 26 | platform = ${common.platform} 27 | framework = ${common.framework} 28 | board = ${common.board} 29 | ;upload_resetmethod = nodemcu 30 | lib_deps = ${common.lib_deps} 31 | extra_scripts = scripts/GENdeploy.py 32 | build_flags = ${common.build_flags} 33 | ;https://github.com/platformio/platform-espressif8266/issues/153 34 | upload_speed = ${common.upload_speed} 35 | monitor_speed = ${common.monitor_speed} 36 | board_build.flash_mode = dio 37 | 38 | ; generic firmware for debugging purposes 39 | [env:debug] 40 | board_build.f_cpu = ${common.f_cpu} 41 | platform = ${common.platform} 42 | framework = ${common.framework} 43 | board = ${common.board} 44 | lib_deps = ${common.lib_deps} 45 | build_flags = ${common.build_flags} 46 | -DDEBUG 47 | extra_scripts = scripts/DBGdeploy.py 48 | upload_speed = ${common.upload_speed} 49 | monitor_speed = ${common.monitor_speed} 50 | -------------------------------------------------------------------------------- /scripts/DBGdeploy.py: -------------------------------------------------------------------------------- 1 | Import("env") 2 | import shutil 3 | import os 4 | # 5 | # Dump build environment (for debug) 6 | # print env.Dump() 7 | # 8 | 9 | # 10 | # Upload actions 11 | # 12 | 13 | def after_build(source, target, env): 14 | shutil.copy(firmware_source, 'bin/debug.bin') 15 | 16 | env.AddPostAction("buildprog", after_build) 17 | 18 | firmware_source = os.path.join(env.subst("$BUILD_DIR"), "firmware.bin") 19 | -------------------------------------------------------------------------------- /scripts/GENdeploy.py: -------------------------------------------------------------------------------- 1 | Import("env") 2 | import shutil 3 | import os 4 | # 5 | # Dump build environment (for debug) 6 | # print env.Dump() 7 | # 8 | 9 | # 10 | # Upload actions 11 | # 12 | 13 | def after_build(source, target, env): 14 | shutil.copy(firmware_source, 'bin/generic.bin') 15 | 16 | env.AddPostAction("buildprog", after_build) 17 | 18 | firmware_source = os.path.join(env.subst("$BUILD_DIR"), "firmware.bin") 19 | -------------------------------------------------------------------------------- /scripts/OBdeploy.py: -------------------------------------------------------------------------------- 1 | Import("env") 2 | import shutil 3 | import os 4 | # 5 | # Dump build environment (for debug) 6 | # print env.Dump() 7 | # 8 | 9 | # 10 | # Upload actions 11 | # 12 | 13 | def after_build(source, target, env): 14 | shutil.copy(firmware_source, 'bin/forV2Board.bin') 15 | 16 | env.AddPostAction("buildprog", after_build) 17 | 18 | firmware_source = os.path.join(env.subst("$BUILD_DIR"), "firmware.bin") 19 | -------------------------------------------------------------------------------- /src/PN532.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef ADAFRUIT_PN532_H 3 | #define ADAFRUIT_PN532_H 4 | 5 | #include "Utils.h" 6 | 7 | // ---------------------------------------------------------------------- 8 | 9 | // This parameter may be used to slow down the software SPI bus speed. 10 | // This is required when there is a long cable between the PN532 and the Teensy. 11 | // This delay in microseconds (not milliseconds!) is made between toggeling the CLK line. 12 | // Use an oscilloscope to check the resulting speed! 13 | // A value of 50 microseconds results in a clock signal of 10 kHz 14 | // A value of 0 results in maximum speed (depends on CPU speed). 15 | // This parameter is not used for hardware SPI mode. 16 | #define PN532_SOFT_SPI_DELAY 50 17 | 18 | // The clock (in Hertz) when using Hardware SPI mode 19 | // This parameter is not used for software SPI mode. 20 | #define PN532_HARD_SPI_CLOCK 1000000 21 | 22 | // The maximum time to wait for an answer from the PN532 23 | // Do NOT use infinite timeouts like in Adafruit code! 24 | #define PN532_TIMEOUT 1000 25 | 26 | // The packet buffer is used for sending commands and for receiving responses from the PN532 27 | #define PN532_PACKBUFFSIZE 80 28 | 29 | // ---------------------------------------------------------------------- 30 | 31 | #define PN532_PREAMBLE (0x00) 32 | #define PN532_STARTCODE1 (0x00) 33 | #define PN532_STARTCODE2 (0xFF) 34 | #define PN532_POSTAMBLE (0x00) 35 | 36 | #define PN532_HOSTTOPN532 (0xD4) 37 | #define PN532_PN532TOHOST (0xD5) 38 | 39 | // PN532 Commands 40 | #define PN532_COMMAND_DIAGNOSE (0x00) 41 | #define PN532_COMMAND_GETFIRMWAREVERSION (0x02) 42 | #define PN532_COMMAND_GETGENERALSTATUS (0x04) 43 | #define PN532_COMMAND_READREGISTER (0x06) 44 | #define PN532_COMMAND_WRITEREGISTER (0x08) 45 | #define PN532_COMMAND_READGPIO (0x0C) 46 | #define PN532_COMMAND_WRITEGPIO (0x0E) 47 | #define PN532_COMMAND_SETSERIALBAUDRATE (0x10) 48 | #define PN532_COMMAND_SETPARAMETERS (0x12) 49 | #define PN532_COMMAND_SAMCONFIGURATION (0x14) 50 | #define PN532_COMMAND_POWERDOWN (0x16) 51 | #define PN532_COMMAND_RFCONFIGURATION (0x32) 52 | #define PN532_COMMAND_RFREGULATIONTEST (0x58) 53 | #define PN532_COMMAND_INJUMPFORDEP (0x56) 54 | #define PN532_COMMAND_INJUMPFORPSL (0x46) 55 | #define PN532_COMMAND_INLISTPASSIVETARGET (0x4A) 56 | #define PN532_COMMAND_INATR (0x50) 57 | #define PN532_COMMAND_INPSL (0x4E) 58 | #define PN532_COMMAND_INDATAEXCHANGE (0x40) 59 | #define PN532_COMMAND_INCOMMUNICATETHRU (0x42) 60 | #define PN532_COMMAND_INDESELECT (0x44) 61 | #define PN532_COMMAND_INRELEASE (0x52) 62 | #define PN532_COMMAND_INSELECT (0x54) 63 | #define PN532_COMMAND_INAUTOPOLL (0x60) 64 | #define PN532_COMMAND_TGINITASTARGET (0x8C) 65 | #define PN532_COMMAND_TGSETGENERALBYTES (0x92) 66 | #define PN532_COMMAND_TGGETDATA (0x86) 67 | #define PN532_COMMAND_TGSETDATA (0x8E) 68 | #define PN532_COMMAND_TGSETMETADATA (0x94) 69 | #define PN532_COMMAND_TGGETINITIATORCOMMAND (0x88) 70 | #define PN532_COMMAND_TGRESPONSETOINITIATOR (0x90) 71 | #define PN532_COMMAND_TGGETTARGETSTATUS (0x8A) 72 | 73 | #define PN532_WAKEUP (0x55) 74 | 75 | #define PN532_SPI_STATUSREAD (0x02) 76 | #define PN532_SPI_DATAWRITE (0x01) 77 | #define PN532_SPI_DATAREAD (0x03) 78 | #define PN532_SPI_READY (0x01) 79 | 80 | #define PN532_I2C_ADDRESS (0x48 >> 1) 81 | #define PN532_I2C_READY (0x01) 82 | 83 | #define PN532_GPIO_P30 (0x01) 84 | #define PN532_GPIO_P31 (0x02) 85 | #define PN532_GPIO_P32 (0x04) 86 | #define PN532_GPIO_P33 (0x08) 87 | #define PN532_GPIO_P34 (0x10) 88 | #define PN532_GPIO_P35 (0x20) 89 | #define PN532_GPIO_VALIDATIONBIT (0x80) 90 | 91 | #define CARD_TYPE_106KB_ISO14443A (0x00) // card baudrate 106 kB 92 | #define CARD_TYPE_212KB_FELICA (0x01) // card baudrate 212 kB 93 | #define CARD_TYPE_424KB_FELICA (0x02) // card baudrate 424 kB 94 | #define CARD_TYPE_106KB_ISO14443B (0x03) // card baudrate 106 kB 95 | #define CARD_TYPE_106KB_JEWEL (0x04) // card baudrate 106 kB 96 | 97 | // Prefixes for NDEF Records (to identify record type), not used 98 | #define NDEF_URIPREFIX_NONE (0x00) 99 | #define NDEF_URIPREFIX_HTTP_WWWDOT (0x01) 100 | #define NDEF_URIPREFIX_HTTPS_WWWDOT (0x02) 101 | #define NDEF_URIPREFIX_HTTP (0x03) 102 | #define NDEF_URIPREFIX_HTTPS (0x04) 103 | #define NDEF_URIPREFIX_TEL (0x05) 104 | #define NDEF_URIPREFIX_MAILTO (0x06) 105 | #define NDEF_URIPREFIX_FTP_ANONAT (0x07) 106 | #define NDEF_URIPREFIX_FTP_FTPDOT (0x08) 107 | #define NDEF_URIPREFIX_FTPS (0x09) 108 | #define NDEF_URIPREFIX_SFTP (0x0A) 109 | #define NDEF_URIPREFIX_SMB (0x0B) 110 | #define NDEF_URIPREFIX_NFS (0x0C) 111 | #define NDEF_URIPREFIX_FTP (0x0D) 112 | #define NDEF_URIPREFIX_DAV (0x0E) 113 | #define NDEF_URIPREFIX_NEWS (0x0F) 114 | #define NDEF_URIPREFIX_TELNET (0x10) 115 | #define NDEF_URIPREFIX_IMAP (0x11) 116 | #define NDEF_URIPREFIX_RTSP (0x12) 117 | #define NDEF_URIPREFIX_URN (0x13) 118 | #define NDEF_URIPREFIX_POP (0x14) 119 | #define NDEF_URIPREFIX_SIP (0x15) 120 | #define NDEF_URIPREFIX_SIPS (0x16) 121 | #define NDEF_URIPREFIX_TFTP (0x17) 122 | #define NDEF_URIPREFIX_BTSPP (0x18) 123 | #define NDEF_URIPREFIX_BTL2CAP (0x19) 124 | #define NDEF_URIPREFIX_BTGOEP (0x1A) 125 | #define NDEF_URIPREFIX_TCPOBEX (0x1B) 126 | #define NDEF_URIPREFIX_IRDAOBEX (0x1C) 127 | #define NDEF_URIPREFIX_FILE (0x1D) 128 | #define NDEF_URIPREFIX_URN_EPC_ID (0x1E) 129 | #define NDEF_URIPREFIX_URN_EPC_TAG (0x1F) 130 | #define NDEF_URIPREFIX_URN_EPC_PAT (0x20) 131 | #define NDEF_URIPREFIX_URN_EPC_RAW (0x21) 132 | #define NDEF_URIPREFIX_URN_EPC (0x22) 133 | #define NDEF_URIPREFIX_URN_NFC (0x23) 134 | 135 | enum eCardType 136 | { 137 | CARD_Unknown = 0, // Mifare Classic or other card 138 | CARD_Desfire = 1, // A Desfire card with normal 7 byte UID (bit 0) 139 | CARD_DesRandom = 3, // A Desfire card with 4 byte random UID (bit 0 + 1) 140 | }; 141 | 142 | class PN532 143 | { 144 | public: 145 | PN532(); 146 | 147 | #if USE_SOFTWARE_SPI 148 | void InitSoftwareSPI(byte u8_Clk, byte u8_Miso, byte u8_Mosi, byte u8_Sel, byte u8_Reset); 149 | #endif 150 | #if USE_HARDWARE_SPI 151 | void InitHardwareSPI(byte u8_Sel, byte u8_Reset); 152 | #endif 153 | #if USE_HARDWARE_I2C 154 | void InitI2C(byte u8_Reset); 155 | #endif 156 | 157 | // Generic PN532 functions 158 | void begin(); 159 | void SetDebugLevel(byte level); 160 | bool SamConfig(); 161 | bool GetFirmwareVersion(byte *pIcType, byte *pVersionHi, byte *pVersionLo, byte *pFlags); 162 | bool WriteGPIO(bool P30, bool P31, bool P33, bool P35); 163 | bool SetPassiveActivationRetries(); 164 | bool DeselectCard(); 165 | bool ReleaseCard(); 166 | bool SelectCard(); 167 | 168 | // This function is overridden in Desfire.cpp 169 | virtual bool SwitchOffRfField(); 170 | 171 | // ISO14443A functions 172 | bool ReadPassiveTargetID(byte *uidBuffer, byte *uidLength, eCardType *pe_CardType); 173 | 174 | protected: 175 | // Low Level functions 176 | bool CheckPN532Status(byte u8_Status); 177 | bool SendCommandCheckAck(byte *cmd, byte cmdlen); 178 | byte ReadData(byte *buff, byte len); 179 | bool ReadPacket(byte *buff, byte len); 180 | void WriteCommand(byte *cmd, byte cmdlen); 181 | void SendPacket(byte *buff, byte len); 182 | bool IsReady(); 183 | bool WaitReady(); 184 | bool ReadAck(); 185 | void SpiWrite(byte c); 186 | byte SpiRead(void); 187 | 188 | byte mu8_DebugLevel; // 0, 1, or 2 189 | byte mu8_PacketBuffer[PN532_PACKBUFFSIZE]; 190 | 191 | private: 192 | byte mu8_ClkPin; 193 | byte mu8_MisoPin; 194 | byte mu8_MosiPin; 195 | byte mu8_SselPin; 196 | byte mu8_ResetPin; 197 | }; 198 | 199 | #endif 200 | -------------------------------------------------------------------------------- /src/Utils.cpp: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | 3 | @author Elmü 4 | class Utils: Some small functions. 5 | 6 | **************************************************************************/ 7 | 8 | #include "Utils.h" 9 | 10 | // Utils::Print("Hello World", LF); --> prints "Hello World\r\n" 11 | void Utils::Print(const char *s8_Text, const char *s8_LF) //=NULL 12 | { 13 | SerialClass::Print(s8_Text); 14 | if (s8_LF) 15 | SerialClass::Print(s8_LF); 16 | } 17 | void Utils::PrintDec(int s32_Data, const char *s8_LF) // =NULL 18 | { 19 | char s8_Buf[20]; 20 | sprintf(s8_Buf, "%d", s32_Data); 21 | Print(s8_Buf, s8_LF); 22 | } 23 | void Utils::PrintHex8(byte u8_Data, const char *s8_LF) // =NULL 24 | { 25 | char s8_Buf[20]; 26 | sprintf(s8_Buf, "%02X", u8_Data); 27 | Print(s8_Buf, s8_LF); 28 | } 29 | void Utils::PrintHex16(uint16_t u16_Data, const char *s8_LF) // =NULL 30 | { 31 | char s8_Buf[20]; 32 | sprintf(s8_Buf, "%04X", u16_Data); 33 | Print(s8_Buf, s8_LF); 34 | } 35 | void Utils::PrintHex32(uint32_t u32_Data, const char *s8_LF) // =NULL 36 | { 37 | char s8_Buf[20]; 38 | sprintf(s8_Buf, "%08X", (unsigned int)u32_Data); 39 | Print(s8_Buf, s8_LF); 40 | } 41 | 42 | // Prints a hexadecimal buffer as 2 digit HEX numbers 43 | // At the byte position s32_Brace1 a "<" will be inserted 44 | // At the byte position s32_Brace2 a ">" will be inserted 45 | // Output will look like: "00 00 FF 03 FD E0 00" 46 | // This is used to mark the data bytes in the packet. 47 | // If the parameters s32_Brace1, s32_Brace2 are -1, they do not appear 48 | void Utils::PrintHexBuf(const byte *u8_Data, const uint32_t u32_DataLen, const char *s8_LF, int s32_Brace1, int s32_Brace2) 49 | { 50 | for (uint32_t i = 0; i < u32_DataLen; i++) 51 | { 52 | if ((int)i == s32_Brace1) 53 | Print(" <"); 54 | else if ((int)i == s32_Brace2) 55 | Print("> "); 56 | else if (i > 0) 57 | Print(" "); 58 | 59 | PrintHex8(u8_Data[i]); 60 | } 61 | if (s8_LF) 62 | Print(s8_LF); 63 | } 64 | 65 | // Converts an interval in milliseconds into days, hours, minutes and prints it 66 | void Utils::PrintInterval(uint64_t u64_Time, const char *s8_LF) 67 | { 68 | char Buf[30]; 69 | u64_Time /= 60 * 1000; 70 | int s32_Min = (int)(u64_Time % 60); 71 | u64_Time /= 60; 72 | int s32_Hour = (int)(u64_Time % 24); 73 | u64_Time /= 24; 74 | int s32_Days = (int)u64_Time; 75 | sprintf(Buf, "%d days, %02d:%02d hours", s32_Days, s32_Hour, s32_Min); 76 | Print(Buf, s8_LF); 77 | } 78 | 79 | // We need a special time counter that does not roll over after 49 days (as millis() does) 80 | uint64_t Utils::GetMillis64() 81 | { 82 | static uint32_t u32_High = 0; 83 | static uint32_t u32_Last = 0; 84 | 85 | uint32_t u32_Now = GetMillis(); // starts at zero after CPU reset 86 | 87 | // Check for roll-over 88 | if (u32_Now < u32_Last) 89 | u32_High++; 90 | u32_Last = u32_Now; 91 | 92 | uint64_t u64_Time = u32_High; 93 | u64_Time <<= 32; 94 | u64_Time |= u32_Now; 95 | return u64_Time; 96 | } 97 | 98 | // Multi byte XOR operation In -> Out 99 | // If u8_Out and u8_In are the same buffer use the other function below. 100 | void Utils::XorDataBlock(byte *u8_Out, const byte *u8_In, const byte *u8_Xor, int s32_Length) 101 | { 102 | for (int B = 0; B < s32_Length; B++) 103 | { 104 | u8_Out[B] = u8_In[B] ^ u8_Xor[B]; 105 | } 106 | } 107 | 108 | // Multi byte XOR operation in the same buffer 109 | void Utils::XorDataBlock(byte *u8_Data, const byte *u8_Xor, int s32_Length) 110 | { 111 | for (int B = 0; B < s32_Length; B++) 112 | { 113 | u8_Data[B] ^= u8_Xor[B]; 114 | } 115 | } 116 | 117 | // Rotate a block of 8 byte to the left by one byte. 118 | // ATTENTION: u8_Out and u8_In must not be the same buffer! 119 | void Utils::RotateBlockLeft(byte *u8_Out, const byte *u8_In, int s32_Length) 120 | { 121 | int s32_Last = s32_Length - 1; 122 | memcpy(u8_Out, u8_In + 1, s32_Last); 123 | u8_Out[s32_Last] = u8_In[0]; 124 | } 125 | 126 | // Logical Bit Shift Left. Shift MSB out, and place a 0 at LSB position 127 | void Utils::BitShiftLeft(uint8_t *u8_Data, int s32_Length) 128 | { 129 | for (int n = 0; n < s32_Length - 1; n++) 130 | { 131 | u8_Data[n] = (u8_Data[n] << 1) | (u8_Data[n + 1] >> 7); 132 | } 133 | u8_Data[s32_Length - 1] <<= 1; 134 | } 135 | 136 | // Generate multi byte random 137 | void Utils::GenerateRandom(byte *u8_Random, int s32_Length) 138 | { 139 | uint32_t u32_Now = GetMillis(); 140 | for (int i = 0; i < s32_Length; i++) 141 | { 142 | u8_Random[i] = (byte)u32_Now; 143 | u32_Now *= 127773; 144 | u32_Now += 16807; 145 | } 146 | } 147 | 148 | // ITU-V.41 (ISO 14443A) 149 | // This CRC is used only for legacy authentication. (not implemented anymore) 150 | uint16_t Utils::CalcCrc16(const byte *u8_Data, int s32_Length) 151 | { 152 | uint16_t u16_Crc = 0x6363; 153 | for (int i = 0; i < s32_Length; i++) 154 | { 155 | byte ch = u8_Data[i]; 156 | ch = ch ^ (byte)u16_Crc; 157 | ch = ch ^ (ch << 4); 158 | u16_Crc = (u16_Crc >> 8) ^ ((uint16_t)ch << 8) ^ ((uint16_t)ch << 3) ^ ((uint16_t)ch >> 4); 159 | } 160 | return u16_Crc; 161 | } 162 | 163 | // This CRC is used for ISO and AES authentication. 164 | // The new Desfire EV1 authentication calculates the CRC32 also over the command, but the command is not encrypted later. 165 | // This function allows to include the command into the calculation without the need to add the command to the same buffer that is later encrypted. 166 | uint32_t Utils::CalcCrc32(const byte *u8_Data1, int s32_Length1, // data to process 167 | const byte *u8_Data2, int s32_Length2) // optional additional data to process (these parameters may be omitted) 168 | { 169 | uint32_t u32_Crc = 0xFFFFFFFF; 170 | u32_Crc = CalcCrc32(u8_Data1, s32_Length1, u32_Crc); 171 | u32_Crc = CalcCrc32(u8_Data2, s32_Length2, u32_Crc); 172 | return u32_Crc; 173 | } 174 | 175 | // private 176 | uint32_t Utils::CalcCrc32(const byte *u8_Data, int s32_Length, uint32_t u32_Crc) 177 | { 178 | for (int i = 0; i < s32_Length; i++) 179 | { 180 | u32_Crc ^= u8_Data[i]; 181 | for (int b = 0; b < 8; b++) 182 | { 183 | bool b_Bit = (u32_Crc & 0x01) > 0; 184 | u32_Crc >>= 1; 185 | if (b_Bit) 186 | u32_Crc ^= 0xEDB88320; 187 | } 188 | } 189 | return u32_Crc; 190 | } 191 | 192 | // ----------------------------------------------------------------------------------------------- 193 | // These functions are only required for some boards which do not define stricmp() and strnicmp() 194 | // They work only for english characters, but this is completely sufficient for this project. 195 | // For Teensy they can be replaced with the original functions. 196 | int Utils::stricmp(const char *str1, const char *str2) 197 | { 198 | return strnicmp(str1, str2, 0xFFFFFFFF); 199 | } 200 | int Utils::strnicmp(const char *str1, const char *str2, uint32_t u32_MaxCount) 201 | { 202 | byte c1 = 0; 203 | byte c2 = 0; 204 | for (uint32_t i = 0; i < u32_MaxCount; i++) 205 | { 206 | c1 = str1[i]; 207 | c2 = str2[i]; 208 | if (c1 >= 'a' && c1 <= 'z') 209 | c1 -= 32; // make upper case 210 | if (c2 >= 'a' && c2 <= 'z') 211 | c2 -= 32; 212 | if (c1 != c2 || c1 == 0) 213 | break; 214 | } 215 | if (c1 < c2) 216 | return -1; 217 | if (c1 > c2) 218 | return 1; 219 | return 0; 220 | } 221 | // ----------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/Utils.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef UTILS_H 3 | #define UTILS_H 4 | 5 | // If you compile this project on Visual Studio... 6 | #ifdef _MSC_VER 7 | #include "../DoorOpenerSolution/WinDefines.h" 8 | 9 | // If you use the Arduino Compiler.... 10 | #else 11 | #include 12 | 13 | #define TRUE true 14 | #define FALSE false 15 | #endif 16 | 17 | // ********************************************************************************* 18 | // The following switches define how the Teensy communicates with the PN532 board. 19 | // For the DoorOpener sketch the only valid option is Software SPI. 20 | // For other projects with the PN532 you may change this. 21 | // ATTENTION: Only one of the following defines must be set to true! 22 | // NOTE: In Software SPI mode there is no external libraray required. Only 4 regular digital pins are used. 23 | // If you want to transfer the code to another processor the easiest way will be to use Software SPI mode. 24 | #define USE_SOFTWARE_SPI TRUE // Visual Studio needs this in upper case 25 | #define USE_HARDWARE_SPI FALSE // Visual Studio needs this in upper case 26 | #define USE_HARDWARE_I2C FALSE // Visual Studio needs this in upper case 27 | // ********************************************************************************/ 28 | 29 | #if USE_HARDWARE_SPI 30 | #include // Hardware SPI bus 31 | #elif USE_HARDWARE_I2C 32 | #include // Hardware I2C bus 33 | #elif USE_SOFTWARE_SPI 34 | // no #include required 35 | #else 36 | #error "You must specify the PN532 communication mode." 37 | #endif 38 | 39 | #define LF "\r\n" // LineFeed 40 | 41 | // Teensy definitions for digital pins: 42 | #ifndef INPUT 43 | #define OUTPUT 0x1 44 | #define INPUT 0x0 45 | #define HIGH 0x1 46 | #define LOW 0x0 47 | #endif 48 | 49 | // ------------------------------------------------------------------------------------------------------------------- 50 | 51 | // USB connection to Terminal program (Teraterm) on PC via COM port 52 | // You can leave all functions empty and only redirect Print() to printf(). 53 | class SerialClass 54 | { 55 | public: 56 | // Create a COM connection via USB. 57 | // Teensy ignores the baudrate parameter (only for older Arduino boards) 58 | static inline void Begin(uint32_t u32_Baud) 59 | { 60 | Serial.begin(u32_Baud); 61 | } 62 | // returns how many characters the user has typed in the Terminal program on the PC which have not yet been read with Read() 63 | static inline int Available() 64 | { 65 | return Serial.available(); 66 | } 67 | // Get the next character from the Terminal program on the PC 68 | // returns -1 if no character available 69 | static inline int Read() 70 | { 71 | return Serial.read(); 72 | } 73 | // Print text to the Terminal program on the PC 74 | // On Windows/Linux use printf() here to write debug output an errors to the Console. 75 | static inline void Print(const char *s8_Text) 76 | { 77 | Serial.print(s8_Text); 78 | } 79 | }; 80 | 81 | // ------------------------------------------------------------------------------------------------------------------- 82 | 83 | #if USE_HARDWARE_SPI 84 | // This class implements Hardware SPI (4 wire bus). It is not used for the DoorOpener sketch. 85 | // NOTE: This class is not used when you switched to I2C mode with PN532::InitI2C() or Software SPI mode with PN532::InitSoftwareSPI(). 86 | class SpiClass 87 | { 88 | public: 89 | static inline void Begin(uint32_t u32_Clock) 90 | { 91 | SPI.begin(); 92 | SPI.beginTransaction(SPISettings(u32_Clock, LSBFIRST, SPI_MODE0)); 93 | } 94 | // Write one byte to the MOSI pin and at the same time receive one byte on the MISO pin. 95 | static inline byte Transfer(byte u8_Data) 96 | { 97 | return SPI.transfer(u8_Data); 98 | } 99 | }; 100 | #endif 101 | 102 | // ------------------------------------------------------------------------------------------------------------------- 103 | 104 | #if USE_HARDWARE_I2C 105 | // This class implements Hardware I2C (2 wire bus with pull-up resistors). It is not used for the DoorOpener sketch. 106 | // NOTE: This class is not used when you switched to SPI mode with PN532::InitSoftwareSPI() or PN532::InitHardwareSPI(). 107 | class I2cClass 108 | { 109 | public: 110 | // Initialize the I2C pins 111 | static inline void Begin() 112 | { 113 | Wire.begin(); 114 | } 115 | // --------------------- READ ------------------------- 116 | // Read the requested amount of bytes at once from the I2C bus into an internal buffer. 117 | // ATTENTION: The Arduino library is extremely primitive. A timeout has not been implemented. 118 | // When the CLK line is permanently low this function hangs forever! 119 | static inline byte RequestFrom(byte u8_Address, byte u8_Quantity) 120 | { 121 | return Wire.requestFrom(u8_Address, u8_Quantity); 122 | } 123 | // Read one byte from the buffer that has been read when calling RequestFrom() 124 | static inline int Read() 125 | { 126 | return Wire.read(); 127 | } 128 | // --------------------- WRITE ------------------------- 129 | // Initiates a Send transmission 130 | static inline void BeginTransmission(byte u8_Address) 131 | { 132 | Wire.beginTransmission(u8_Address); 133 | } 134 | // Write one byte to the I2C bus 135 | static inline void Write(byte u8_Data) 136 | { 137 | Wire.write(u8_Data); 138 | } 139 | // Ends a Send transmission 140 | static inline void EndTransmission() 141 | { 142 | Wire.endTransmission(); 143 | } 144 | }; 145 | #endif 146 | 147 | // ------------------------------------------------------------------------------------------------------------------- 148 | 149 | class Utils 150 | { 151 | public: 152 | // returns the current tick counter 153 | // If you compile on Visual Studio see WinDefines.h 154 | static inline uint32_t GetMillis() 155 | { 156 | return millis(); 157 | } 158 | 159 | // If you compile on Visual Studio see WinDefines.h 160 | static inline void DelayMilli(int s32_MilliSeconds) 161 | { 162 | delay(s32_MilliSeconds); 163 | } 164 | 165 | // This function is only required for Software SPI mode. 166 | // If you compile on Visual Studio see WinDefines.h 167 | static inline void DelayMicro(int s32_MicroSeconds) 168 | { 169 | delayMicroseconds(s32_MicroSeconds); 170 | } 171 | 172 | // Defines if a digital processor pin is used as input or output 173 | // u8_Mode = INPUT or OUTPUT 174 | // If you compile on Visual Studio see WinDefines.h 175 | static inline void SetPinMode(byte u8_Pin, byte u8_Mode) 176 | { 177 | pinMode(u8_Pin, u8_Mode); 178 | } 179 | 180 | // Sets a digital processor pin high or low. 181 | // u8_Status = HIGH or LOW 182 | // If you compile on Visual Studio see WinDefines.h 183 | static inline void WritePin(byte u8_Pin, byte u8_Status) 184 | { 185 | digitalWrite(u8_Pin, u8_Status); 186 | } 187 | 188 | // reads the current state of a digital processor pin. 189 | // returns HIGH or LOW 190 | // If you compile on Visual Studio see WinDefines.h 191 | static inline byte ReadPin(byte u8_Pin) 192 | { 193 | return digitalRead(u8_Pin); 194 | } 195 | 196 | static uint64_t GetMillis64(); 197 | static void Print(const char *s8_Text, const char *s8_LF = NULL); 198 | static void PrintDec(int s32_Data, const char *s8_LF = NULL); 199 | static void PrintHex8(byte u8_Data, const char *s8_LF = NULL); 200 | static void PrintHex16(uint16_t u16_Data, const char *s8_LF = NULL); 201 | static void PrintHex32(uint32_t u32_Data, const char *s8_LF = NULL); 202 | static void PrintHexBuf(const byte *u8_Data, const uint32_t u32_DataLen, const char *s8_LF = NULL, int s32_Brace1 = -1, int S32_Brace2 = -1); 203 | static void PrintInterval(uint64_t u64_Time, const char *s8_LF = NULL); 204 | static void GenerateRandom(byte *u8_Random, int s32_Length); 205 | static void RotateBlockLeft(byte *u8_Out, const byte *u8_In, int s32_Length); 206 | static void BitShiftLeft(uint8_t *u8_Data, int s32_Length); 207 | static void XorDataBlock(byte *u8_Out, const byte *u8_In, const byte *u8_Xor, int s32_Length); 208 | static void XorDataBlock(byte *u8_Data, const byte *u8_Xor, int s32_Length); 209 | static uint16_t CalcCrc16(const byte *u8_Data, int s32_Length); 210 | static uint32_t CalcCrc32(const byte *u8_Data1, int s32_Length1, const byte *u8_Data2 = NULL, int s32_Length2 = 0); 211 | static int strnicmp(const char *str1, const char *str2, uint32_t u32_MaxCount); 212 | static int stricmp(const char *str1, const char *str2); 213 | 214 | private: 215 | static uint32_t CalcCrc32(const byte *u8_Data, int s32_Length, uint32_t u32_Crc); 216 | }; 217 | 218 | #endif // UTILS_H 219 | -------------------------------------------------------------------------------- /src/beeper.esp: -------------------------------------------------------------------------------- 1 | unsigned int beeperInterval = 0; 2 | unsigned int beeperOffTime = 0; 3 | 4 | void beeperBeep() 5 | { 6 | if (config.beeperpin != 255) 7 | { 8 | if (currentMillis > beeperOffTime && digitalRead(config.beeperpin) == BEEPERon) 9 | { 10 | digitalWrite(config.beeperpin, BEEPERoff); 11 | #ifdef DEBUG 12 | Serial.println("Beeper OFF"); 13 | #endif 14 | beeperInterval = 0; 15 | } 16 | else if (beeperInterval != 0) 17 | { 18 | int beeperState = digitalRead(config.beeperpin); 19 | if (currentMillis - previousMillis >= beeperInterval) 20 | { 21 | previousMillis = currentMillis; 22 | if (beeperState == BEEPERon) { 23 | beeperState = BEEPERoff; 24 | #ifdef DEBUG 25 | Serial.println("Beeper OFF"); 26 | #endif 27 | } else { 28 | beeperState = BEEPERon; 29 | #ifdef DEBUG 30 | Serial.println("Beeper ON"); 31 | #endif 32 | } 33 | digitalWrite(config.beeperpin, beeperState); 34 | } 35 | } 36 | } 37 | } 38 | 39 | void beeperValidAccess() 40 | { 41 | if (config.beeperpin != 255) 42 | { 43 | beeperOffTime = currentMillis + 2000; 44 | digitalWrite(config.beeperpin, BEEPERon); 45 | #ifdef DEBUG 46 | Serial.println("Beeper ON"); 47 | #endif 48 | } 49 | } 50 | 51 | void beeperAdminAccess() 52 | { 53 | if (config.beeperpin != 255) { 54 | beeperOffTime = currentMillis + 3000; 55 | digitalWrite(config.beeperpin, BEEPERon); 56 | #ifdef DEBUG 57 | Serial.println("Beeper ON"); 58 | #endif 59 | } 60 | } 61 | 62 | void beeperAccessDenied() 63 | { 64 | if (config.beeperpin != 255) 65 | { 66 | beeperOffTime = currentMillis + 1000; 67 | beeperInterval = 200; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/config.esp: -------------------------------------------------------------------------------- 1 | bool ICACHE_FLASH_ATTR loadConfiguration(Config &config) 2 | { 3 | File configFile = SPIFFS.open("/config.json", "r"); 4 | if (!configFile) 5 | { 6 | #ifdef DEBUG 7 | Serial.println(F("[ WARN ] Failed to open config file")); 8 | #endif 9 | return false; 10 | } 11 | size_t size = configFile.size(); 12 | std::unique_ptr buf(new char[size]); 13 | configFile.readBytes(buf.get(), size); 14 | DynamicJsonDocument json(2048); 15 | auto error = deserializeJson(json, buf.get(), size); 16 | if (error) 17 | { 18 | #ifdef DEBUG 19 | Serial.println(F("[ WARN ] Failed to parse config file")); 20 | #endif 21 | return false; 22 | } 23 | #ifdef DEBUG 24 | Serial.println(F("[ INFO ] Config file found")); 25 | #endif 26 | JsonObject network = json["network"]; 27 | JsonObject hardware = json["hardware"]; 28 | JsonObject general = json["general"]; 29 | JsonObject mqtt = json["mqtt"]; 30 | JsonObject ntp = json["ntp"]; 31 | #ifdef DEBUG 32 | Serial.println(F("[ INFO ] Trying to setup RFID Hardware")); 33 | #endif 34 | if (hardware.containsKey("wifipin")) 35 | { 36 | config.wifipin = hardware["wifipin"]; 37 | if (config.wifipin != 255) 38 | { 39 | pinMode(config.wifipin, OUTPUT); 40 | digitalWrite(config.wifipin, LEDoff); 41 | } 42 | } 43 | 44 | if (hardware.containsKey("doorstatpin")) 45 | { 46 | config.doorstatpin = hardware["doorstatpin"]; 47 | if (config.doorstatpin != 255) 48 | { 49 | pinMode(config.doorstatpin, INPUT); 50 | } 51 | } 52 | 53 | if (hardware.containsKey("maxOpenDoorTime")) 54 | { 55 | config.maxOpenDoorTime = hardware["maxOpenDoorTime"]; 56 | } 57 | 58 | if (hardware.containsKey("doorbellpin")) 59 | { 60 | config.doorbellpin = hardware["doorbellpin"]; 61 | if (config.doorbellpin != 255) 62 | { 63 | pinMode(config.doorbellpin, INPUT); 64 | } 65 | } 66 | 67 | if (hardware.containsKey("accessdeniedpin")) 68 | { 69 | config.accessdeniedpin = hardware["accessdeniedpin"]; 70 | if (config.accessdeniedpin != 255) 71 | { 72 | pinMode(config.accessdeniedpin, OUTPUT); 73 | digitalWrite(config.accessdeniedpin, LOW); 74 | } 75 | } 76 | 77 | if (hardware.containsKey("beeperpin")) 78 | { 79 | config.beeperpin = hardware["beeperpin"]; 80 | if (config.beeperpin != 255) 81 | { 82 | pinMode(config.beeperpin, OUTPUT); 83 | digitalWrite(config.beeperpin, BEEPERoff); 84 | } 85 | } 86 | 87 | if (hardware.containsKey("ledwaitingpin")) 88 | { 89 | config.ledwaitingpin = hardware["ledwaitingpin"]; 90 | if (config.ledwaitingpin != 255) 91 | { 92 | pinMode(config.ledwaitingpin, OUTPUT); 93 | digitalWrite(config.ledwaitingpin, LEDoff); 94 | } 95 | } 96 | 97 | if (hardware.containsKey("openlockpin")) 98 | { 99 | config.openlockpin = hardware["openlockpin"]; 100 | if (config.openlockpin != 255) 101 | { 102 | openLockButton = Bounce(); 103 | openLockButton.attach(config.openlockpin, INPUT_PULLUP); 104 | openLockButton.interval(30); 105 | } 106 | } 107 | 108 | if (hardware.containsKey("numrelays")) 109 | { 110 | config.numRelays = hardware["numrelays"]; 111 | } 112 | else 113 | config.numRelays = 1; 114 | 115 | config.readertype = hardware["readertype"]; 116 | int rfidss; 117 | config.pinCodeRequested = false; 118 | config.pinCodeOnly = false; 119 | if (config.readertype == READER_WIEGAND || config.readertype == READER_WIEGAND_RDM6300) 120 | { 121 | int wgd0pin = hardware["wgd0pin"]; 122 | int wgd1pin = hardware["wgd1pin"]; 123 | if (hardware.containsKey("requirepincodeafterrfid")) 124 | { 125 | config.pinCodeRequested = hardware["requirepincodeafterrfid"]; 126 | } 127 | if (hardware.containsKey("allowpincodeonly")) 128 | { 129 | config.pinCodeOnly = hardware["allowpincodeonly"]; 130 | } 131 | if (hardware.containsKey("removeparitybits")) 132 | { 133 | config.removeParityBits = hardware["removeparitybits"]; 134 | } 135 | if (hardware.containsKey("useridstoragemode")) 136 | { 137 | config.wiegandReadHex = hardware["useridstoragemode"] == "hexadecimal"; 138 | } 139 | setupWiegandReader(wgd0pin, wgd1pin, config.removeParityBits); // also some other settings like weather to use keypad or not, LED pin, BUZZER pin, Wiegand 26/34 version 140 | } 141 | else if (config.readertype == READER_MFRC522 || config.readertype == READER_MFRC522_RDM6300) 142 | { 143 | rfidss = 15; 144 | if (hardware.containsKey("sspin")) 145 | { 146 | rfidss = hardware["sspin"]; 147 | } 148 | int rfidgain = hardware["rfidgain"]; 149 | setupMFRC522Reader(rfidss, rfidgain); 150 | } 151 | else if (config.readertype == READER_PN532 || config.readertype == READER_PN532_RDM6300) 152 | { 153 | rfidss = hardware["sspin"]; 154 | setupPN532Reader(rfidss); 155 | } 156 | // RDM6300 can be configured alongside the other readers 157 | if (config.readertype > READER_PN532) 158 | { 159 | int rmd6300TxPin = hardware["rdm6300pin"]; 160 | setupRDM6300Reader(rmd6300TxPin); 161 | } 162 | 163 | if (network.containsKey("fallbackmode")) 164 | { 165 | config.fallbackMode = network["fallbackmode"] == 1; 166 | } 167 | config.autoRestartIntervalSeconds = general["restart"]; 168 | config.wifiTimeout = network["offtime"]; 169 | const char *bssidmac = network["bssid"]; 170 | if (strlen(bssidmac) > 0) 171 | parseBytes(bssidmac, ':', config.bssid, 6, 16); 172 | if (general.containsKey("hostnm")) 173 | { 174 | config.deviceHostname = strdup(general["hostnm"]); 175 | } 176 | if (ntp.containsKey("server")) 177 | { 178 | config.ntpServer = strdup(ntp["server"]); 179 | } 180 | config.ntpInterval = ntp["interval"]; 181 | 182 | // support for old config 183 | if (ntp.containsKey("timezone")) 184 | { 185 | config.tzInfo = (char *) malloc(10 * sizeof(char)); 186 | float tz = ntp["timezone"]; 187 | if(tz > 0) 188 | { 189 | snprintf(config.tzInfo, 10, "UTC+%.2f", tz); 190 | } 191 | else if(tz < 0) 192 | { 193 | snprintf(config.tzInfo, 10, "UTC-%.2f", tz); 194 | } 195 | else 196 | { 197 | snprintf(config.tzInfo, 10, "UTC"); 198 | } 199 | } 200 | if (ntp.containsKey("tzinfo")) 201 | { 202 | config.tzInfo = (char *) malloc(strlen(ntp["tzinfo"]) * sizeof(char)); 203 | config.tzInfo = strdup(ntp["tzinfo"]); 204 | } 205 | configTime(0, 0, config.ntpServer); 206 | // See https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv for Timezone codes for your region 207 | setenv("TZ", config.tzInfo, 1); 208 | 209 | config.activateTime[0] = hardware["rtime"]; 210 | config.lockType[0] = hardware["ltype"]; 211 | config.relayType[0] = hardware["rtype"]; 212 | if (hardware.containsKey("doorname")) 213 | { 214 | config.doorName[0] = strdup(hardware["doorname"]); 215 | } 216 | 217 | config.relayPin[0] = hardware["rpin"]; 218 | pinMode(config.relayPin[0], OUTPUT); 219 | digitalWrite(config.relayPin[0], !config.relayType[0]); 220 | 221 | for (int i = 1; i < config.numRelays; i++) 222 | { 223 | JsonObject relay = hardware["relay" + String((i + 1))]; 224 | config.activateTime[i] = relay["rtime"]; 225 | config.lockType[i] = relay["ltype"]; 226 | config.relayType[i] = relay["rtype"]; 227 | config.relayPin[i] = relay["rpin"]; 228 | if (relay.containsKey("doorname")) 229 | { 230 | config.doorName[i]= strdup(relay["doorname"]); 231 | } 232 | pinMode(config.relayPin[i], OUTPUT); 233 | digitalWrite(config.relayPin[i], !config.relayType[i]); 234 | } 235 | 236 | if (network.containsKey("ssid")) 237 | { 238 | config.ssid = strdup(network["ssid"]); 239 | } 240 | if (network.containsKey("pswd")) 241 | { 242 | config.wifiPassword = strdup(network["pswd"]); 243 | } 244 | config.accessPointMode = network["wmode"] == 1; 245 | if (network.containsKey("apip")) 246 | { 247 | config.wifiApIp = strdup(network["apip"]); 248 | } 249 | if (network.containsKey("apsubnet")) 250 | { 251 | config.wifiApSubnet = strdup(network["apsubnet"]); 252 | } 253 | config.networkHidden = network["hide"] == 1; 254 | if (general.containsKey("pswd")) 255 | { 256 | config.httpPass = strdup(general["pswd"]); 257 | } 258 | config.dhcpEnabled = network["dhcp"] == 1; 259 | if (network.containsKey("ip")) 260 | { 261 | config.ipAddress.fromString(network["ip"].as()); 262 | } 263 | if (network.containsKey("subnet")) 264 | { 265 | config.subnetIp.fromString(network["subnet"].as()); 266 | } 267 | if (network.containsKey("gateway")) 268 | { 269 | config.gatewayIp.fromString(network["gateway"].as()); 270 | } 271 | if (network.containsKey("dns")) 272 | { 273 | config.dnsIp.fromString(network["dns"].as()); 274 | } 275 | 276 | const char *apipch; 277 | if (config.wifiApIp) 278 | { 279 | apipch = config.wifiApIp; 280 | } 281 | else 282 | { 283 | apipch = "192.168.4.1"; 284 | } 285 | const char *apsubnetch; 286 | if (config.wifiApSubnet) 287 | { 288 | apsubnetch = config.wifiApSubnet; 289 | } 290 | else 291 | { 292 | apsubnetch = "255.255.255.0"; 293 | } 294 | config.accessPointIp.fromString(apipch); 295 | config.accessPointSubnetIp.fromString(apsubnetch); 296 | 297 | ws.setAuthentication("admin", config.httpPass); 298 | 299 | for (int d = 0; d < 7; d++) 300 | { 301 | if (general.containsKey("openinghours")) 302 | { 303 | config.openingHours[d] = strdup(general["openinghours"][d].as()); 304 | } 305 | else 306 | { 307 | config.openingHours[d] = strdup("111111111111111111111111"); 308 | } 309 | } 310 | 311 | config.mqttEnabled = mqtt["enabled"] == 1; 312 | 313 | if (config.mqttEnabled) 314 | { 315 | String mhsString = mqtt["host"]; 316 | config.mqttHost = strdup(mhsString.c_str()); 317 | config.mqttPort = mqtt["port"]; 318 | String muserString = mqtt["user"]; 319 | config.mqttUser = strdup(muserString.c_str()); 320 | String mpasString = mqtt["pswd"]; 321 | config.mqttPass = strdup(mpasString.c_str()); 322 | String mqttTopicString = mqtt["topic"]; 323 | config.mqttTopic = strdup(mqttTopicString.c_str()); 324 | if (mqtt.containsKey("autotopic")) 325 | { 326 | config.mqttAutoTopic = mqtt["autotopic"]; 327 | } 328 | if (config.mqttAutoTopic) 329 | { 330 | uint8_t macAddr[6]; 331 | WiFi.softAPmacAddress(macAddr); 332 | char topicSuffix[7]; 333 | sprintf(topicSuffix, "-%02x%02x%02x", macAddr[3], macAddr[4], macAddr[5]); 334 | char *newTopic = (char *)malloc(sizeof(char) * 80); 335 | strcpy(newTopic, config.mqttTopic); 336 | strcat(newTopic, topicSuffix); 337 | config.mqttTopic = newTopic; 338 | } 339 | if (mqtt.containsKey("syncrate")) 340 | { 341 | config.mqttInterval = mqtt["syncrate"]; 342 | } 343 | if (mqtt.containsKey("mqttlog")) 344 | { 345 | config.mqttEvents = mqtt["mqttlog"] == 1; 346 | } 347 | if (mqtt.containsKey("mqttha")) 348 | { 349 | config.mqttHA = mqtt["mqttha"] == 1; 350 | } 351 | } 352 | #ifdef DEBUG 353 | Serial.println(F("[ INFO ] Configuration done.")); 354 | #endif 355 | config.present = true; 356 | return true; 357 | } 358 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | struct Config { 2 | int relayPin[MAX_NUM_RELAYS]; 3 | uint8_t accessdeniedpin = 255; 4 | bool accessPointMode = false; 5 | IPAddress accessPointIp; 6 | IPAddress accessPointSubnetIp; 7 | unsigned long activateTime[MAX_NUM_RELAYS]; 8 | unsigned long autoRestartIntervalSeconds = 0; 9 | unsigned long beeperInterval = 0; 10 | unsigned long beeperOffTime = 0; 11 | uint8_t beeperpin = 255; 12 | byte bssid[6] = {0, 0, 0, 0, 0, 0}; 13 | char *deviceHostname = NULL; 14 | bool dhcpEnabled = true; 15 | IPAddress dnsIp; 16 | uint8_t doorbellpin = 255; 17 | char *doorName[MAX_NUM_RELAYS]; 18 | uint8_t doorstatpin = 255; 19 | bool fallbackMode = false; 20 | IPAddress gatewayIp; 21 | char *httpPass = NULL; 22 | IPAddress ipAddress; 23 | uint8_t ledwaitingpin = 255; 24 | int lockType[MAX_NUM_RELAYS]; 25 | uint8_t maxOpenDoorTime = 0; 26 | bool mqttAutoTopic = false; 27 | bool mqttEnabled = false; 28 | bool mqttEvents = false; // Sends events over MQTT disables SPIFFS file logging 29 | bool mqttHA = false; // Sends events over simple MQTT topics and AutoDiscovery 30 | char *mqttHost = NULL; 31 | unsigned long mqttInterval = 180; // Add to GUI & json config 32 | char *mqttPass = NULL; 33 | int mqttPort; 34 | char *mqttTopic = NULL; 35 | char *mqttUser = NULL; 36 | bool networkHidden = false; 37 | char *ntpServer = NULL; 38 | int ntpInterval = 0; 39 | int numRelays = 1; 40 | char *openingHours[7]; 41 | uint8_t openlockpin = 255; 42 | bool pinCodeRequested = false; 43 | bool pinCodeOnly = false; 44 | bool wiegandReadHex = true; 45 | bool present = false; 46 | int readertype; 47 | int relayType[MAX_NUM_RELAYS]; 48 | bool removeParityBits = true; 49 | IPAddress subnetIp; 50 | const char *ssid; 51 | char *tzInfo = (char *)""; 52 | const char *wifiApIp = NULL; 53 | const char *wifiApSubnet = NULL; 54 | uint8_t wifipin = 255; 55 | const char *wifiPassword = NULL; 56 | unsigned long wifiTimeout = 0; 57 | }; 58 | -------------------------------------------------------------------------------- /src/door.esp: -------------------------------------------------------------------------------- 1 | // door lock 2 | // nardev -> functions in relation to opening doors with card or with door opening button, should be moved here 3 | 4 | // door status 5 | void doorStatus(/* arguments */) 6 | { 7 | if (config.doorstatpin == 255) 8 | { 9 | return; 10 | } 11 | 12 | // if this changes and if mqtt and mqtt logging enabled, push the message, also to a web socket! 13 | if ((digitalRead(config.doorstatpin) == HIGH) && (lastDoorState == 0)) 14 | { 15 | writeEvent("INFO", "door", "Door Closed", ""); 16 | if (lastTamperState == 1) 17 | { 18 | lastTamperState = 0; 19 | mqttPublishIo("tamper", "OFF"); 20 | } 21 | mqttPublishIo("door", "OFF"); 22 | lastDoorState = 1; 23 | } 24 | 25 | if ((digitalRead(config.doorstatpin) == LOW) && (lastDoorState == 1)) 26 | { 27 | writeEvent("INFO", "door", "Door Open", ""); 28 | if (digitalRead(config.relayPin[0]) == !config.relayType[0]) 29 | { 30 | writeEvent("WARN", "tamper", "Door was tampered!", ""); 31 | lastTamperState = 1; 32 | mqttPublishIo("tamper", "ON"); 33 | } 34 | else 35 | { 36 | openDoorMillis = currentMillis; 37 | #ifdef DEBUG 38 | Serial.print("openDoorMillis : "); 39 | Serial.println(openDoorMillis); 40 | #endif 41 | } 42 | mqttPublishIo("door", "ON"); 43 | lastDoorState = 0; 44 | } 45 | if (config.maxOpenDoorTime != 0 && (lastDoorState == 0) && (lastTamperState == 0)) 46 | { 47 | if (currentMillis - openDoorMillis >= config.maxOpenDoorTime*1000) 48 | { 49 | #ifdef DEBUG 50 | Serial.print("currentMillis : "); 51 | Serial.println(currentMillis); 52 | Serial.print("delta millis : "); 53 | Serial.println(currentMillis - openDoorMillis); 54 | #endif 55 | 56 | writeEvent("WARN", "tamper", "Door wasn't closed within max open time!", ""); 57 | lastTamperState = 1; 58 | mqttPublishIo("tamper", "ON"); 59 | } 60 | } 61 | delayMicroseconds(500); 62 | } 63 | -------------------------------------------------------------------------------- /src/doorbell.esp: -------------------------------------------------------------------------------- 1 | // doorbell support 2 | 3 | void doorbellStatus() 4 | { 5 | if (config.doorbellpin == 255) 6 | { 7 | return; 8 | } 9 | // if this changes and if mqtt and mqtt logging enabled, push the message, also to a web socket! 10 | if ((digitalRead(config.doorbellpin) == HIGH) && (lastDoorbellState == 0)) 11 | { 12 | writeEvent("INFO", "doorbell", "Doorbell ringing", ""); 13 | mqttPublishIo("doorbell", "ON"); 14 | lastDoorbellState = 1; 15 | } 16 | 17 | if ((digitalRead(config.doorbellpin) == LOW) && (lastDoorbellState == 1)) 18 | { 19 | mqttPublishIo("doorbell", "OFF"); 20 | lastDoorbellState = 0; 21 | } 22 | delayMicroseconds(500); 23 | } 24 | -------------------------------------------------------------------------------- /src/helpers.esp: -------------------------------------------------------------------------------- 1 | String ICACHE_FLASH_ATTR printIP(IPAddress adress) 2 | { 3 | return (String)adress[0] + "." + (String)adress[1] + "." + (String)adress[2] + "." + (String)adress[3]; 4 | } 5 | 6 | void ICACHE_FLASH_ATTR parseBytes(const char *str, char sep, byte *bytes, int maxBytes, int base) 7 | { 8 | for (int i = 0; i < maxBytes; i++) 9 | { 10 | bytes[i] = strtoul(str, NULL, base); // Convert byte 11 | str = strchr(str, sep); // Find next separator 12 | if (str == NULL || *str == '\0') 13 | { 14 | break; // No more separators, exit 15 | } 16 | str++; // Point to next character after separator 17 | } 18 | } 19 | 20 | void trySyncNTPtime(unsigned long retrySeconds) 21 | { 22 | // If time since last sync is less then retrySeconds then update using last clock 23 | if (millis() - lastNTPSync < retrySeconds * 1000) 24 | { 25 | epoch = lastNTPepoch + (millis() - lastNTPSync) / 1000; 26 | return; 27 | } 28 | 29 | time_t previousEpoch = epoch; 30 | time(&epoch); 31 | lastNTPSync = millis(); 32 | localtime_r(&epoch, &timeinfo); 33 | 34 | if (timeinfo.tm_year <= (2016 - 1900)) 35 | { 36 | #if DEBUG 37 | Serial.println("[ WARN ] NTP sync failed"); 38 | #endif 39 | 40 | epoch = previousEpoch; // fallback to previous value, so that you can use the manually set time 41 | return; 42 | } 43 | 44 | lastNTPepoch = epoch; 45 | } 46 | -------------------------------------------------------------------------------- /src/led.esp: -------------------------------------------------------------------------------- 1 | unsigned long accessdeniedOffTime = 0; 2 | 3 | void ledWaitingOn() 4 | { 5 | if (config.ledwaitingpin != 255) 6 | { 7 | digitalWrite(config.ledwaitingpin, LEDon); 8 | #ifdef DEBUG 9 | Serial.println("LED waiting ON"); 10 | #endif 11 | } 12 | } 13 | 14 | void ledWaitingOff() 15 | { 16 | if (config.ledwaitingpin != 255) 17 | { 18 | digitalWrite(config.ledwaitingpin, HIGH); 19 | #ifdef DEBUG 20 | Serial.println("LED waiting OFF"); 21 | #endif 22 | } 23 | } 24 | 25 | void ledWifiOn() 26 | { 27 | if (config.wifipin != 255) 28 | { 29 | digitalWrite(config.wifipin, LEDon); 30 | #ifdef DEBUG 31 | Serial.println("LED WiFi ON"); 32 | #endif 33 | } 34 | } 35 | 36 | void ledWifiOff() 37 | { 38 | if (config.wifipin != 255) 39 | { 40 | digitalWrite(config.wifipin, LEDoff); 41 | #ifdef DEBUG 42 | Serial.println("LED WiFi OFF"); 43 | #endif 44 | } 45 | } 46 | 47 | // blink when not connected, on when connected 48 | void ledWifiStatus() 49 | { 50 | if (config.wifipin != 255 && !config.accessPointMode) 51 | { 52 | if (!WiFi.isConnected()) 53 | { 54 | if ((currentMillis - wifiPinBlink) > 500) 55 | { 56 | wifiPinBlink = currentMillis; 57 | digitalWrite(config.wifipin, !digitalRead(config.wifipin)); 58 | } 59 | } 60 | else 61 | { 62 | if (!(digitalRead(config.wifipin) == LEDon)) 63 | digitalWrite(config.wifipin, LEDon); 64 | } 65 | } 66 | } 67 | 68 | void ledAccessDeniedOff() 69 | { 70 | if (config.accessdeniedpin != 255 && currentMillis > accessdeniedOffTime && digitalRead(config.accessdeniedpin) == LEDon) 71 | { 72 | digitalWrite(config.accessdeniedpin, LEDoff); 73 | #ifdef DEBUG 74 | Serial.println("LED access denied OFF"); 75 | #endif 76 | } 77 | } 78 | 79 | void ledAccessDeniedOn() 80 | { 81 | if (config.accessdeniedpin != 255) 82 | { 83 | accessdeniedOffTime = currentMillis + 1000; 84 | digitalWrite(config.accessdeniedpin, LEDon); 85 | #ifdef DEBUG 86 | Serial.println("LED access denied ON"); 87 | #endif 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/log.esp: -------------------------------------------------------------------------------- 1 | void extern mqttPublishEvent(JsonDocument *root); 2 | 3 | void ICACHE_FLASH_ATTR writeEvent(String type, String src, String desc, String data) 4 | { 5 | DynamicJsonDocument root(512); 6 | root["type"] = type; 7 | root["src"] = src; 8 | root["desc"] = desc; 9 | root["data"] = data; 10 | root["time"] = epoch; 11 | if (config.mqttEvents && config.mqttEnabled) // log to MQTT 12 | { 13 | root["cmd"] = "event"; 14 | root["hostname"] = config.deviceHostname; 15 | mqttPublishEvent(&root); 16 | } 17 | else // log to file 18 | { 19 | File eventlog = SPIFFS.open("/eventlog.json", "a"); 20 | serializeJson(root, eventlog); 21 | eventlog.print("\n"); 22 | eventlog.close(); 23 | } 24 | #ifdef DEBUG 25 | Serial.println("[ " + type + " ] " + src + " | " + desc + " | " + data); 26 | #endif 27 | } 28 | 29 | void ICACHE_FLASH_ATTR writeLatest(String uid, String username, int acctype, int access = ACCESS_GRANTED) 30 | { 31 | DynamicJsonDocument root(512); 32 | root["uid"] = uid; 33 | root["username"] = username; 34 | root["acctype"] = acctype; 35 | root["access"] = access; 36 | root["timestamp"] = epoch; 37 | File latestlog = SPIFFS.open("/latestlog.json", "a"); 38 | serializeJson(root, latestlog); 39 | latestlog.print("\n"); 40 | latestlog.close(); 41 | } 42 | 43 | size_t lastPos; // position counter for fast seek 44 | #define LOGTYPE_LATESTLOG 0 45 | #define LOGTYPE_EVENTLOG 1 46 | #define ITEMS_PER_PAGE 10 47 | #define FILES_PER_PAGE 10.0 48 | #define MIN_SPIFF_BYTES 4096 49 | 50 | void ICACHE_FLASH_ATTR sendLogFile(int page, String fileName, int logFileType, AsyncWebSocketClient *client) 51 | { 52 | 53 | // if we are reading the first page then we reset 54 | // the position counter 55 | 56 | if (page == 1) 57 | lastPos = 0; 58 | float pages; 59 | DynamicJsonDocument root(2048); 60 | if (logFileType == LOGTYPE_EVENTLOG) 61 | root["command"] = "eventlist"; 62 | if (logFileType == LOGTYPE_LATESTLOG) 63 | root["command"] = "latestlist"; 64 | root["page"] = page; 65 | JsonArray items = root.createNestedArray("list"); 66 | 67 | File logFile; 68 | 69 | if (!SPIFFS.exists(fileName)) 70 | { 71 | logFile = SPIFFS.open(fileName, "w"); 72 | logFile.close(); 73 | } 74 | 75 | logFile = SPIFFS.open(fileName, "r"); 76 | 77 | // move the file pointer to the last known position 78 | 79 | logFile.seek(lastPos); 80 | int numLines = 0; 81 | 82 | // read in 10 lines or until EOF whatever happens first 83 | 84 | while (logFile.available() && (numLines < ITEMS_PER_PAGE)) 85 | { 86 | String item = String(); 87 | item = logFile.readStringUntil('\n'); 88 | items.add(item); 89 | numLines++; 90 | } 91 | 92 | // remember the last position 93 | 94 | lastPos = logFile.position(); 95 | 96 | // calculate the number of remaining pages 97 | 98 | if (logFile.available()) // tell bootstrap footable on the client side that there are more pages to come 99 | { 100 | float bytesPerPageRoughly = (lastPos / page); 101 | float totalPagesRoughly = logFile.size() / bytesPerPageRoughly; 102 | pages = totalPagesRoughly <= page ? page + 1 : totalPagesRoughly; 103 | } 104 | else 105 | pages = page; // this was the last page 106 | 107 | logFile.close(); 108 | root["haspages"] = ceil(pages); 109 | size_t len = measureJson(root); 110 | AsyncWebSocketMessageBuffer *buffer = ws.makeBuffer(len); 111 | if (buffer) 112 | { 113 | serializeJson(root, (char *)buffer->get(), len + 1); 114 | client->text(buffer); 115 | if (logFileType == LOGTYPE_EVENTLOG) 116 | client->text("{\"command\":\"result\",\"resultof\":\"eventlist\",\"result\": true}"); 117 | if (logFileType == LOGTYPE_LATESTLOG) 118 | client->text("{\"command\":\"result\",\"resultof\":\"latestlist\",\"result\": true}"); 119 | } 120 | } 121 | 122 | void ICACHE_FLASH_ATTR logMaintenance(String action, String filename, AsyncWebSocketClient *client) 123 | { 124 | #ifdef DEBUG 125 | Serial.printf("[DEBUG] Log Maintenance Action: %s on %s\n", action.c_str(), filename.c_str()); 126 | #endif 127 | 128 | // delete a file 129 | 130 | if (action == "delete") 131 | { 132 | SPIFFS.remove(filename); 133 | } 134 | 135 | // rollover a file, i.e. rename 136 | 137 | if (action == "rollover") 138 | { 139 | size_t rolloverExtension = 1; 140 | while (SPIFFS.exists(filename + "." + rolloverExtension)) 141 | rolloverExtension++; 142 | SPIFFS.rename(filename, filename + "." + rolloverExtension); 143 | } 144 | 145 | // split a file, i.e. create two new files of roughly the same size 146 | // or at least as big as SPIFFS free space allows 147 | 148 | if (action == "split") 149 | { 150 | size_t rolloverExtension1 = 1; 151 | while (SPIFFS.exists(filename + ".split." + rolloverExtension1)) 152 | rolloverExtension1++; 153 | size_t rolloverExtension2 = rolloverExtension1 + 1; 154 | while (SPIFFS.exists(filename + ".split." + rolloverExtension2)) 155 | rolloverExtension2++; 156 | 157 | File logFile = SPIFFS.open(filename, "r"); 158 | File newFile1 = SPIFFS.open(filename + ".split." + rolloverExtension1, "w"); 159 | File newFile2 = SPIFFS.open(filename + ".split." + rolloverExtension2, "w"); 160 | 161 | FSInfo fs_info; 162 | SPIFFS.info(fs_info); 163 | 164 | size_t truncatePosition = logFile.size() / 2; 165 | logFile.seek(truncatePosition); 166 | logFile.readStringUntil('\n'); 167 | truncatePosition = logFile.position(); 168 | logFile.seek(0); 169 | 170 | // check if we have enough space for the split operation 171 | 172 | if ((fs_info.totalBytes - fs_info.usedBytes) < (logFile.size() + MIN_SPIFF_BYTES)) 173 | { 174 | if (client) 175 | { 176 | client->text("{\"command\":\"result\",\"resultof\":\"logfileMaintenance\",\"result\": false,\"message\":\"Not enough space on SPIFF Filesystem\"}"); 177 | } 178 | } 179 | else 180 | { 181 | 182 | // mind the watchdog timer - this may take a couple of seconds... 183 | 184 | ESP.wdtDisable(); 185 | ESP.wdtEnable(1500); 186 | 187 | // read the first half of the file 188 | 189 | while (logFile.available() && logFile.position() < truncatePosition) 190 | { 191 | String item = String(); 192 | item = logFile.readStringUntil('\n'); 193 | newFile1.println(item); 194 | ESP.wdtFeed(); // tell the watchdog we're still doing stuff 195 | } 196 | 197 | // read the rest 198 | 199 | while (logFile.available()) 200 | { 201 | String item = String(); 202 | item = logFile.readStringUntil('\n'); 203 | newFile2.println(item); 204 | ESP.wdtFeed(); // no reset please ;-) 205 | } 206 | 207 | logFile.close(); 208 | newFile1.close(); 209 | newFile2.close(); 210 | } 211 | } 212 | 213 | ESP.wdtEnable(5000); 214 | if (client) 215 | { 216 | client->text("{\"command\":\"result\",\"resultof\":\"logfileMaintenance\",\"result\": true}"); 217 | } 218 | } 219 | 220 | void ICACHE_FLASH_ATTR sendFileList(int page, AsyncWebSocketClient *client) 221 | { 222 | 223 | DynamicJsonDocument root(512); 224 | root["command"] = "listfiles"; 225 | root["page"] = page; 226 | JsonArray items = root.createNestedArray("list"); 227 | 228 | size_t first = (page - 1) * FILES_PER_PAGE; 229 | size_t last = page * FILES_PER_PAGE; 230 | size_t numFiles = 0; 231 | 232 | Dir dir = SPIFFS.openDir("/"); 233 | while (dir.next()) 234 | { 235 | 236 | // if (dir.isFile()) // isFile is implemented in Arduino Core 2.5.1 - We'll have to wait for the ISR not in IRAM fix 237 | 238 | String thisFileName = dir.fileName(); 239 | if ((thisFileName.indexOf("latestlog") >= 0) || (thisFileName.indexOf("eventlog") >= 0)) // for the time being we just check filenames 240 | { 241 | if (numFiles >= first && numFiles < last) 242 | { 243 | JsonObject item = items.createNestedObject(); 244 | item["filename"] = dir.fileName(); 245 | item["filesize"] = dir.fileSize(); 246 | } // first, last 247 | numFiles++; 248 | } // isFile 249 | } // dir next 250 | 251 | float pages = numFiles / FILES_PER_PAGE; 252 | root["haspages"] = ceil(pages); 253 | 254 | size_t len = measureJson(root); 255 | AsyncWebSocketMessageBuffer *buffer = ws.makeBuffer(len); 256 | if (buffer) 257 | { 258 | serializeJson(root, (char *)buffer->get(), len + 1); 259 | client->text(buffer); 260 | client->text("{\"command\":\"result\",\"resultof\":\"listfiles\",\"result\": true}"); 261 | } 262 | } 263 | 264 | void ICACHE_FLASH_ATTR sendEventLog(int page, String fileName, AsyncWebSocketClient *client) 265 | { 266 | if (fileName.length() == 0) 267 | fileName = "/eventlog.json"; 268 | sendLogFile(page, fileName, LOGTYPE_EVENTLOG, client); 269 | } 270 | 271 | void ICACHE_FLASH_ATTR sendLatestLog(int page, String fileName, AsyncWebSocketClient *client) 272 | { 273 | if (fileName.length() == 0) 274 | fileName = "/latestlog.json"; 275 | sendLogFile(page, fileName, LOGTYPE_LATESTLOG, client); 276 | } 277 | -------------------------------------------------------------------------------- /src/magicnumbers.h: -------------------------------------------------------------------------------- 1 | // reader types 2 | 3 | #define READER_MFRC522 0 4 | #define READER_WIEGAND 1 5 | #define READER_PN532 2 6 | #define READER_RDM6300 3 7 | #define READER_MFRC522_RDM6300 4 8 | #define READER_WIEGAND_RDM6300 5 9 | #define READER_PN532_RDM6300 6 10 | 11 | // timing constants 12 | 13 | #define COOLDOWN_MILIS 2000 // Milliseconds the RFID reader will be blocked between inputs 14 | #define KEYBOARD_TIMEOUT_MILIS 10000 // timeout in milis for keyboard input 15 | 16 | // user related numbers 17 | 18 | #define ACCESS_GRANTED 1 19 | #define ACCESS_ADMIN 99 20 | #define ACCESS_DENIED 0 21 | 22 | // Reader defines 23 | 24 | #define WIEGANDTYPE_KEYPRESS4 4 25 | #define WIEGANDTYPE_KEYPRESS8 8 26 | #define WIEGANDTYPE_PICC24 24 27 | #define WIEGANDTYPE_PICC34 34 28 | 29 | #define RDM6300_BAUDRATE 9600 30 | #define RDM6300_READ_TIMEOUT 20 31 | 32 | // hardware defines 33 | 34 | #define MAX_NUM_RELAYS 4 35 | 36 | #define LOCKTYPE_MOMENTARY 0 37 | #define LOCKTYPE_CONTINUOUS 1 38 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2018 esp-rfid Community 5 | Copyright (c) 2017 Ömer Şiar Baysal 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | #define VERSION "2.0.0" 26 | 27 | #include "Arduino.h" 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include "magicnumbers.h" 41 | #include "config.h" 42 | 43 | Config config; 44 | 45 | #include 46 | #include "PN532.h" 47 | #include 48 | #include "rfid125kHz.h" 49 | #include 50 | 51 | MFRC522 mfrc522 = MFRC522(); 52 | PN532 pn532; 53 | WIEGAND wg; 54 | RFID_Reader RFIDr; 55 | SoftwareSerial *rdm6300SwSerial = NULL; 56 | 57 | // relay specific variables 58 | bool activateRelay[MAX_NUM_RELAYS] = {false, false, false, false}; 59 | bool deactivateRelay[MAX_NUM_RELAYS] = {false, false, false, false}; 60 | 61 | // these are from vendors 62 | #include "webh/glyphicons-halflings-regular.woff.gz.h" 63 | #include "webh/required.css.gz.h" 64 | #include "webh/required.js.gz.h" 65 | 66 | // these are from us which can be updated and changed 67 | #include "webh/esprfid.js.gz.h" 68 | #include "webh/esprfid.htm.gz.h" 69 | #include "webh/index.html.gz.h" 70 | 71 | AsyncMqttClient mqttClient; 72 | Ticker mqttReconnectTimer; 73 | Ticker wifiReconnectTimer; 74 | Ticker wsMessageTicker; 75 | WiFiEventHandler wifiDisconnectHandler, wifiConnectHandler, wifiOnStationModeGotIPHandler; 76 | Bounce openLockButton; 77 | 78 | AsyncWebServer server(80); 79 | AsyncWebSocket ws("/ws"); 80 | 81 | #define LEDoff HIGH 82 | #define LEDon LOW 83 | 84 | #define BEEPERoff HIGH 85 | #define BEEPERon LOW 86 | 87 | // Variables for whole scope 88 | unsigned long cooldown = 0; 89 | unsigned long currentMillis = 0; 90 | unsigned long deltaTime = 0; 91 | bool doEnableWifi = false; 92 | bool formatreq = false; 93 | const char *httpUsername = "admin"; 94 | unsigned long keyTimer = 0; 95 | uint8_t lastDoorbellState = 0; 96 | uint8_t lastDoorState = 0; 97 | uint8_t lastTamperState = 0; 98 | unsigned long nextbeat = 0; 99 | time_t epoch; 100 | time_t lastNTPepoch; 101 | unsigned long lastNTPSync = 0; 102 | unsigned long openDoorMillis = 0; 103 | unsigned long previousLoopMillis = 0; 104 | unsigned long previousMillis = 0; 105 | bool shouldReboot = false; 106 | tm timeinfo; 107 | unsigned long uptimeSeconds = 0; 108 | unsigned long wifiPinBlink = millis(); 109 | unsigned long wiFiUptimeMillis = 0; 110 | 111 | #include "led.esp" 112 | #include "beeper.esp" 113 | #include "log.esp" 114 | #include "mqtt.esp" 115 | #include "helpers.esp" 116 | #include "wsResponses.esp" 117 | #include "rfid.esp" 118 | #include "wifi.esp" 119 | #include "config.esp" 120 | #include "websocket.esp" 121 | #include "webserver.esp" 122 | #include "door.esp" 123 | #include "doorbell.esp" 124 | 125 | void ICACHE_FLASH_ATTR setup() 126 | { 127 | #ifdef DEBUG 128 | Serial.begin(115200); 129 | Serial.println(); 130 | 131 | Serial.print(F("[ INFO ] ESP RFID v")); 132 | Serial.println(VERSION); 133 | 134 | uint32_t realSize = ESP.getFlashChipRealSize(); 135 | uint32_t ideSize = ESP.getFlashChipSize(); 136 | FlashMode_t ideMode = ESP.getFlashChipMode(); 137 | Serial.printf("Flash real id: %08X\n", ESP.getFlashChipId()); 138 | Serial.printf("Flash real size: %u\n\n", realSize); 139 | Serial.printf("Flash ide size: %u\n", ideSize); 140 | Serial.printf("Flash ide speed: %u\n", ESP.getFlashChipSpeed()); 141 | Serial.printf("Flash ide mode: %s\n", (ideMode == FM_QIO ? "QIO" : ideMode == FM_QOUT ? "QOUT" 142 | : ideMode == FM_DIO ? "DIO" 143 | : ideMode == FM_DOUT ? "DOUT" 144 | : "UNKNOWN")); 145 | if (ideSize != realSize) 146 | { 147 | Serial.println("Flash Chip configuration wrong!\n"); 148 | } 149 | else 150 | { 151 | Serial.println("Flash Chip configuration ok.\n"); 152 | } 153 | #endif 154 | 155 | if (!SPIFFS.begin()) 156 | { 157 | if (SPIFFS.format()) 158 | { 159 | writeEvent("WARN", "sys", "Filesystem formatted", ""); 160 | } 161 | else 162 | { 163 | #ifdef DEBUG 164 | Serial.println(F(" failed!")); 165 | Serial.println(F("[ WARN ] Could not format filesystem!")); 166 | #endif 167 | } 168 | } 169 | 170 | bool configured = false; 171 | configured = loadConfiguration(config); 172 | setupMqtt(); 173 | setupWebServer(); 174 | setupWifi(configured); 175 | writeEvent("INFO", "sys", "System setup completed, running", ""); 176 | } 177 | 178 | void ICACHE_RAM_ATTR loop() 179 | { 180 | currentMillis = millis(); 181 | deltaTime = currentMillis - previousLoopMillis; 182 | uptimeSeconds = currentMillis / 1000; 183 | previousLoopMillis = currentMillis; 184 | 185 | trySyncNTPtime(10); 186 | 187 | openLockButton.update(); 188 | if (config.openlockpin != 255 && openLockButton.fell()) 189 | { 190 | writeLatest(" ", "Button", 1); 191 | mqttPublishAccess(epoch, "true", "Always", "Button", " ", " "); 192 | activateRelay[0] = true; 193 | beeperValidAccess(); 194 | // TODO: handle other relays 195 | } 196 | 197 | ledWifiStatus(); 198 | ledAccessDeniedOff(); 199 | beeperBeep(); 200 | doorStatus(); 201 | doorbellStatus(); 202 | 203 | if (currentMillis >= cooldown) 204 | { 205 | rfidLoop(); 206 | } 207 | 208 | for (int currentRelay = 0; currentRelay < config.numRelays; currentRelay++) 209 | { 210 | if (config.lockType[currentRelay] == LOCKTYPE_CONTINUOUS) // Continuous relay mode 211 | { 212 | if (activateRelay[currentRelay]) 213 | { 214 | if (digitalRead(config.relayPin[currentRelay]) == !config.relayType[currentRelay]) // currently OFF, need to switch ON 215 | { 216 | mqttPublishIo("lock" + String(currentRelay), "UNLOCKED"); 217 | #ifdef DEBUG 218 | Serial.print("mili : "); 219 | Serial.println(millis()); 220 | Serial.printf("activating relay %d now\n", currentRelay); 221 | #endif 222 | digitalWrite(config.relayPin[currentRelay], config.relayType[currentRelay]); 223 | } 224 | else // currently ON, need to switch OFF 225 | { 226 | mqttPublishIo("lock" + String(currentRelay), "LOCKED"); 227 | #ifdef DEBUG 228 | Serial.print("mili : "); 229 | Serial.println(millis()); 230 | Serial.printf("deactivating relay %d now\n", currentRelay); 231 | #endif 232 | digitalWrite(config.relayPin[currentRelay], !config.relayType[currentRelay]); 233 | } 234 | activateRelay[currentRelay] = false; 235 | } 236 | } 237 | else if (config.lockType[currentRelay] == LOCKTYPE_MOMENTARY) // Momentary relay mode 238 | { 239 | if (activateRelay[currentRelay]) 240 | { 241 | mqttPublishIo("lock" + String(currentRelay), "UNLOCKED"); 242 | #ifdef DEBUG 243 | Serial.print("mili : "); 244 | Serial.println(millis()); 245 | Serial.printf("activating relay %d now\n", currentRelay); 246 | #endif 247 | digitalWrite(config.relayPin[currentRelay], config.relayType[currentRelay]); 248 | previousMillis = millis(); 249 | activateRelay[currentRelay] = false; 250 | deactivateRelay[currentRelay] = true; 251 | } 252 | else if ((currentMillis - previousMillis >= config.activateTime[currentRelay]) && (deactivateRelay[currentRelay])) 253 | { 254 | mqttPublishIo("lock" + String(currentRelay), "LOCKED"); 255 | #ifdef DEBUG 256 | Serial.println(currentMillis); 257 | Serial.println(previousMillis); 258 | Serial.println(config.activateTime[currentRelay]); 259 | Serial.println(activateRelay[currentRelay]); 260 | Serial.println("deactivate relay after this"); 261 | Serial.print("mili : "); 262 | Serial.println(millis()); 263 | #endif 264 | digitalWrite(config.relayPin[currentRelay], !config.relayType[currentRelay]); 265 | deactivateRelay[currentRelay] = false; 266 | } 267 | } 268 | } 269 | if (formatreq) 270 | { 271 | #ifdef DEBUG 272 | Serial.println(F("[ WARN ] Factory reset initiated...")); 273 | #endif 274 | SPIFFS.end(); 275 | ws.enable(false); 276 | SPIFFS.format(); 277 | ESP.restart(); 278 | } 279 | 280 | if (config.autoRestartIntervalSeconds > 0 && uptimeSeconds > config.autoRestartIntervalSeconds) 281 | { 282 | writeEvent("WARN", "sys", "Auto restarting...", ""); 283 | shouldReboot = true; 284 | } 285 | 286 | if (shouldReboot) 287 | { 288 | writeEvent("INFO", "sys", "System is going to reboot", ""); 289 | SPIFFS.end(); 290 | ESP.restart(); 291 | } 292 | 293 | if (WiFi.isConnected()) 294 | { 295 | wiFiUptimeMillis += deltaTime; 296 | } 297 | 298 | if (config.wifiTimeout > 0 && wiFiUptimeMillis > (config.wifiTimeout * 1000) && WiFi.isConnected()) 299 | { 300 | writeEvent("INFO", "wifi", "WiFi is going to be disabled", ""); 301 | disableWifi(); 302 | } 303 | 304 | // don't try connecting to WiFi when waiting for pincode 305 | if (doEnableWifi == true && keyTimer == 0 && activateRelay[0] == true) 306 | { 307 | if (!WiFi.isConnected()) 308 | { 309 | enableWifi(); 310 | writeEvent("INFO", "wifi", "Enabling WiFi", ""); 311 | doEnableWifi = false; 312 | } 313 | } 314 | 315 | if (config.mqttEnabled && mqttClient.connected()) 316 | { 317 | if ((unsigned)epoch > nextbeat) 318 | { 319 | mqttPublishHeartbeat(epoch, uptimeSeconds); 320 | nextbeat = (unsigned)epoch + config.mqttInterval; 321 | #ifdef DEBUG 322 | Serial.print("[ INFO ] Nextbeat="); 323 | Serial.println(nextbeat); 324 | #endif 325 | } 326 | processMqttQueue(); 327 | } 328 | 329 | processWsQueue(); 330 | 331 | // clean unused websockets 332 | ws.cleanupClients(); 333 | } 334 | -------------------------------------------------------------------------------- /src/rfid.esp: -------------------------------------------------------------------------------- 1 | int accountType; 2 | int accountTypes[MAX_NUM_RELAYS]; 3 | String currentInput = ""; 4 | String pinCode = ""; 5 | String type = ""; 6 | String uid = ""; 7 | String v1uid = ""; 8 | String username = ""; 9 | bool wiegandAvailable = false; 10 | 11 | #define WIEGAND_ENT 0xD 12 | #define WIEGAND_ESC 0x1B 13 | 14 | enum RfidStates 15 | { 16 | waitingRfid, 17 | cardSwiped, 18 | pinCodeEntered 19 | }; 20 | enum RfidProcessingStates 21 | { 22 | waitingProcessing, 23 | notValid, 24 | wrongPincode, 25 | expired, 26 | unknown, 27 | valid, 28 | validAdmin, 29 | cancelled 30 | }; 31 | 32 | RfidStates rfidState = waitingRfid; 33 | RfidProcessingStates processingState = waitingProcessing; 34 | 35 | void loadWiegandData() 36 | { 37 | wiegandAvailable = false; 38 | // wg.available checks if there's new info and populates all the internal data 39 | // so it should be called only once per loop 40 | wiegandAvailable = wg.available(); 41 | } 42 | 43 | void rfidPrepareRead() 44 | { 45 | if (config.readertype == READER_WIEGAND) 46 | { 47 | loadWiegandData(); 48 | } 49 | } 50 | 51 | void wiegandRead() 52 | { 53 | if (wiegandAvailable && rfidState == waitingRfid) 54 | { 55 | // if we get anything between 24 or 34 bit burst then we have a scanned PICC 56 | if (wg.getWiegandType() >= WIEGANDTYPE_PICC24 && wg.getWiegandType() <= WIEGANDTYPE_PICC34) 57 | { 58 | uid = String(wg.getCode(), config.wiegandReadHex ? HEX : DEC); 59 | type = String(wg.getWiegandType(), DEC); 60 | #ifdef DEBUG 61 | Serial.print(F("[ INFO ] PICC's UID: ")); 62 | Serial.println(uid); 63 | #endif 64 | 65 | File f = SPIFFS.open("/P/" + uid, "r"); 66 | // user exists, we should wait for pincode 67 | if (f) 68 | { 69 | size_t size = f.size(); 70 | std::unique_ptr buf(new char[size]); 71 | f.readBytes(buf.get(), size); 72 | f.close(); 73 | DynamicJsonDocument json(512); 74 | auto error = deserializeJson(json, buf.get(), size); 75 | if (error) 76 | { 77 | processingState = notValid; 78 | #ifdef DEBUG 79 | Serial.println(""); 80 | Serial.println(F("[ WARN ] Failed to parse User Data")); 81 | #endif 82 | return; 83 | } 84 | rfidState = cardSwiped; 85 | if (config.pinCodeRequested) 86 | { 87 | if(json["pincode"] == "") 88 | { 89 | rfidState = pinCodeEntered; 90 | } else 91 | { 92 | keyTimer = millis(); 93 | ledWaitingOn(); 94 | } 95 | } 96 | } else 97 | { 98 | cooldown = millis() + COOLDOWN_MILIS; 99 | rfidState = waitingRfid; 100 | processingState = unknown; 101 | } 102 | } 103 | } 104 | } 105 | 106 | void mfrc522Read() 107 | { 108 | if (!mfrc522.PICC_IsNewCardPresent() || !mfrc522.PICC_ReadCardSerial()) 109 | { 110 | return; 111 | } 112 | mfrc522.PICC_HaltA(); 113 | cooldown = millis() + COOLDOWN_MILIS; 114 | 115 | /* 116 | * Convert RC522 UID into string 117 | * esp-rfid v1 had a bug where the UID string may miss some '0's. To 118 | * maintain compatibility, calculate incorrect UID here as well for 119 | * later checking in case old users exist in the config. 120 | */ 121 | for (byte i = 0; i < mfrc522.uid.size; i++) 122 | { 123 | uid+=(String(mfrc522.uid.uidByte[i] < 0x10 ? "0" : "")); 124 | uid+=(String(mfrc522.uid.uidByte[i], HEX)); 125 | v1uid+=(String(mfrc522.uid.uidByte[i], HEX)); 126 | } 127 | rfidState = cardSwiped; 128 | 129 | #ifdef DEBUG 130 | Serial.print(F("[ INFO ] PICC's UID: ")); 131 | Serial.print(uid); 132 | #endif 133 | 134 | MFRC522::PICC_Type piccType = mfrc522.PICC_GetType(mfrc522.uid.sak); 135 | type = mfrc522.PICC_GetTypeName(piccType); 136 | 137 | #ifdef DEBUG 138 | Serial.print(" " + type); 139 | #endif 140 | } 141 | 142 | void pn532Read() 143 | { 144 | bool found = false; 145 | byte pnuid[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 146 | eCardType e_CardType; 147 | byte u8_UidLength = 0x00; // UID = 4 or 7 bytes 148 | found = pn532.ReadPassiveTargetID(pnuid, &u8_UidLength, &e_CardType); 149 | if (found && u8_UidLength >= 4) 150 | { 151 | #ifdef DEBUG 152 | Serial.print(F("[ INFO ] PICC's UID: ")); 153 | #endif 154 | for (uint8_t i = 0; i < u8_UidLength; i++) 155 | { 156 | uid += String(pnuid[i], HEX); 157 | rfidState = cardSwiped; 158 | } 159 | #ifdef DEBUG 160 | Serial.print(uid); 161 | #endif 162 | cooldown = millis() + COOLDOWN_MILIS; 163 | } 164 | } 165 | 166 | 167 | /* 168 | * Try first to read from RDM6300 hardware. If that received 169 | * nothing, check the other configured reader. 170 | */ 171 | void genericRead() 172 | { 173 | while (rdm6300SwSerial->available() > 0) 174 | { 175 | char read = rdm6300SwSerial->read(); 176 | RFIDr.rfidSerial(read); 177 | } 178 | if (RFIDr.Available()) 179 | { 180 | uid = RFIDr.GetHexID(); 181 | type = RFIDr.GetTagType(); 182 | rfidState = cardSwiped; 183 | cooldown = millis() + COOLDOWN_MILIS; 184 | #ifdef DEBUG 185 | Serial.print(F("[ INFO ] PICC's UID: ")); 186 | Serial.print(uid); 187 | #endif 188 | } 189 | 190 | /* 191 | * If nothing read from the RDM6300, check the other hardware 192 | */ 193 | if (uid.length() == 0) { 194 | if (config.readertype == READER_MFRC522_RDM6300) 195 | { 196 | mfrc522Read(); 197 | } 198 | 199 | else if (config.readertype == READER_WIEGAND_RDM6300) 200 | { 201 | wiegandRead(); 202 | } 203 | 204 | else if (config.readertype == READER_PN532_RDM6300) 205 | { 206 | pn532Read(); 207 | } 208 | } 209 | } 210 | 211 | 212 | /* 213 | * Main function to read RFID cards. This function will call the 214 | * correct reader function depending on the configured hardware, 215 | * or otherwise call genericRead to read both RDM6300 and another 216 | * configured reader. 217 | */ 218 | void rfidRead() 219 | { 220 | /* 221 | * Do not try and read if we are already processing a card 222 | */ 223 | if (rfidState == cardSwiped) 224 | { 225 | return; 226 | } 227 | 228 | /* 229 | * Call the appropriate function based on the configured 230 | * hardware 231 | */ 232 | if (config.readertype == READER_MFRC522) 233 | { 234 | mfrc522Read(); 235 | } 236 | 237 | else if (config.readertype == READER_WIEGAND) 238 | { 239 | wiegandRead(); 240 | } 241 | 242 | else if (config.readertype == READER_PN532) 243 | { 244 | pn532Read(); 245 | } 246 | 247 | else if (config.readertype > READER_PN532) 248 | { 249 | // This is a combination of RDM6300 and one of the above 250 | genericRead(); 251 | } 252 | } 253 | 254 | 255 | /* 256 | * Try and read a PIN code from Wiegand hardware 257 | */ 258 | void pinCodeRead() 259 | { 260 | if (config.readertype != READER_WIEGAND || 261 | !wiegandAvailable || 262 | (!config.pinCodeRequested && rfidState == cardSwiped) || 263 | (!config.pinCodeOnly && rfidState == waitingRfid) || 264 | rfidState == pinCodeEntered) 265 | { 266 | return; 267 | } 268 | 269 | // if we get a 4 bit burst then a key has been pressed 270 | // add the key to the current input and reset the Waiting time 271 | // for the next key unless * or # have been pressed 272 | // we do not require * as the first character because some 273 | // readers use this as special admin code and would hence require *#PIN# 274 | if (wg.getWiegandType() == WIEGANDTYPE_KEYPRESS4 || wg.getWiegandType() == WIEGANDTYPE_KEYPRESS8) 275 | { 276 | if (wg.getCode() != WIEGAND_ENT && wg.getCode() != WIEGAND_ESC) // normal key entry, add to currentInput 277 | { 278 | #ifdef DEBUG 279 | Serial.println("Keycode captured. . ."); 280 | #endif 281 | currentInput = currentInput + String(wg.getCode()); 282 | keyTimer = millis(); 283 | ledWaitingOn(); 284 | } else if (keyTimer > 0) // if we are waitingProcessing on input still 285 | { 286 | if(wg.getCode() == WIEGAND_ESC) // esc, abort pincode 287 | { 288 | #ifdef DEBUG 289 | Serial.println("Keycode escape, aborting pincode entry"); 290 | #endif 291 | rfidState = waitingRfid; 292 | processingState = cancelled; 293 | cooldown = millis() + COOLDOWN_MILIS; 294 | } 295 | if(wg.getCode() == WIEGAND_ENT) // enter, process pincode 296 | { 297 | pinCode = currentInput; 298 | if (config.pinCodeOnly && rfidState == waitingRfid) 299 | { 300 | uid = pinCode; 301 | rfidState = cardSwiped; 302 | } else 303 | { 304 | rfidState = pinCodeEntered; 305 | } 306 | #ifdef DEBUG 307 | Serial.println("Stop capture keycode . . ."); 308 | Serial.print(F("[ INFO ] PICC's pin code: ")); 309 | Serial.println(currentInput); 310 | #endif 311 | currentInput = ""; 312 | keyTimer = 0; 313 | cooldown = millis() + COOLDOWN_MILIS; 314 | } 315 | } 316 | } 317 | } 318 | 319 | int weekdayFromMonday(int weekdayFromSunday) { 320 | // we expect weeks starting from Sunday equals to 1 321 | // we return week day starting from Monday equals to 0 322 | return ( weekdayFromSunday + 5 ) % 7; 323 | } 324 | 325 | 326 | /* 327 | * If we have successfully read an RFID card, check if access 328 | * should be granted 329 | */ 330 | void rfidProcess() 331 | { 332 | if (rfidState == waitingRfid || 333 | (config.pinCodeRequested && rfidState == cardSwiped)) 334 | { 335 | return; 336 | } 337 | 338 | /* Each user has a file named after the RFID UID */ 339 | File f = SPIFFS.open("/P/" + uid, "r"); 340 | 341 | /* 342 | * If the file was not found then this is an unknown user, so no more 343 | * processing to be done. However, for backwards compatibility we do a 344 | * secondary check here to see if an old esp-rfid v1 uid exists and if 345 | * so use that. 346 | */ 347 | if (!f) 348 | { 349 | /* Test to see if there was a uid in v1 format */ 350 | f = SPIFFS.open("/P/" + v1uid, "r"); 351 | if (!f) 352 | { 353 | processingState = unknown; 354 | return; 355 | } 356 | uid = v1uid; 357 | #ifdef DEBUG 358 | Serial.print(" (found uid in v1 format: "); 359 | Serial.print(v1uid); 360 | Serial.print(")"); 361 | #endif 362 | } 363 | 364 | /* 365 | * Read the user's settings 366 | */ 367 | size_t size = f.size(); 368 | std::unique_ptr buf(new char[size]); 369 | f.readBytes(buf.get(), size); 370 | f.close(); 371 | DynamicJsonDocument json(512); 372 | auto error = deserializeJson(json, buf.get(), size); 373 | 374 | /* 375 | * Corrupt user data file, so return invalid user 376 | */ 377 | if (error) 378 | { 379 | processingState = notValid; 380 | #ifdef DEBUG 381 | Serial.println(""); 382 | Serial.println(F("[ WARN ] Failed to parse User Data")); 383 | #endif 384 | return; 385 | } 386 | 387 | // if the pin code is wrong we deny access 388 | // pinCode is equal to uid if we allow pin code only access 389 | if(config.pinCodeRequested && pinCode != json["pincode"] && pinCode != uid && json["pincode"] != "") 390 | { 391 | processingState = wrongPincode; 392 | #ifdef DEBUG 393 | Serial.println("Wrong pin code"); 394 | #endif 395 | return; 396 | } 397 | 398 | /* 399 | * Get account type (for FIRST relay only) and username from user's data 400 | */ 401 | accountType = json["acctype"]; 402 | username = json["user"].as(); 403 | 404 | #ifdef DEBUG 405 | Serial.println(" = known PICC"); 406 | Serial.print("[ INFO ] User Name: '"); 407 | if (username == "undefined") 408 | Serial.print(uid); 409 | else 410 | Serial.print(username); 411 | Serial.print("'"); 412 | #endif 413 | 414 | if (accountType == ACCESS_GRANTED) 415 | { 416 | /* 417 | * Normal user - relay but no admin access 418 | */ 419 | unsigned long validSinceL = json["validsince"]; 420 | unsigned long validUntilL = json["validuntil"]; 421 | unsigned long nowL = epoch; 422 | int hourTz = timeinfo.tm_hour; 423 | 424 | if (validUntilL < nowL || validSinceL > nowL) 425 | { 426 | processingState = expired; 427 | } else if (config.openingHours[weekdayFromMonday(weekday(epoch))][hourTz] != '1') 428 | { 429 | processingState = notValid; 430 | } else 431 | { 432 | processingState = valid; 433 | } 434 | } else if (accountType == ACCESS_ADMIN) 435 | { 436 | /* 437 | * Admin user - enable relay (with no time limits) and wifi 438 | */ 439 | #ifdef DEBUG 440 | Serial.println(" has admin access, enable wifi"); 441 | #endif 442 | doEnableWifi = true; 443 | processingState = validAdmin; 444 | } else { 445 | /* 446 | * User exists but does not have access 447 | */ 448 | processingState = notValid; 449 | } 450 | 451 | /* 452 | * If user is valid and opening hour time is allowed, go through each relay 453 | * in turn to see if it needs to be activated 454 | */ 455 | if (processingState == valid || processingState == validAdmin) 456 | { 457 | for (int currentRelay = 0; currentRelay < config.numRelays; currentRelay++) 458 | { 459 | // Get user data JSON access type entry for this relay 460 | if (currentRelay == 0) { 461 | accountTypes[currentRelay] = json["acctype"]; 462 | } else { 463 | accountTypes[currentRelay] = json["acctype" + String(currentRelay + 1)]; 464 | } 465 | 466 | // Enable activation if permissions are correct 467 | activateRelay[currentRelay] = (accountTypes[currentRelay] == ACCESS_GRANTED); 468 | 469 | // ...except Admin, which always activates everything 470 | if (processingState == validAdmin) 471 | { 472 | activateRelay[currentRelay] = true; 473 | } 474 | } 475 | } 476 | } 477 | 478 | void rfidOutsideMessaging() 479 | { 480 | if (processingState == valid) 481 | { 482 | ws.textAll("{\"command\":\"giveAccess\"}"); 483 | #ifdef DEBUG 484 | Serial.println(" has access relay"); 485 | #endif 486 | writeLatest(uid, username, accountType, ACCESS_GRANTED); 487 | if (config.numRelays == 1) { 488 | mqttPublishAccess(epoch, "true", "Always", username, uid, pinCode); 489 | } else { 490 | mqttPublishAccess(epoch, "true", accountTypes, username, uid, pinCode); 491 | } 492 | beeperValidAccess(); 493 | } 494 | if (processingState == validAdmin) 495 | { 496 | ws.textAll("{\"command\":\"giveAccess\"}"); 497 | writeLatest(uid, username, accountType, ACCESS_GRANTED); 498 | if (config.numRelays == 1) { 499 | mqttPublishAccess(epoch, "true", "Admin", username, uid, pinCode); 500 | } else { 501 | mqttPublishAccess(epoch, "true", accountTypes, username, uid, pinCode); 502 | } 503 | beeperAdminAccess(); 504 | } 505 | if (processingState == expired) 506 | { 507 | #ifdef DEBUG 508 | Serial.println(" expired"); 509 | #endif 510 | writeLatest(uid, username, accountType, ACCESS_DENIED); 511 | mqttPublishAccess(epoch, "true", "Expired", username, uid, pinCode); 512 | ledAccessDeniedOn(); 513 | beeperAccessDenied(); 514 | } 515 | if (processingState == wrongPincode) 516 | { 517 | writeLatest(uid, username, accountType, ACCESS_DENIED); 518 | mqttPublishAccess(epoch, "true", "Wrong pin code", username, uid, pinCode); 519 | ledAccessDeniedOn(); 520 | beeperAccessDenied(); 521 | } 522 | if (processingState == notValid) 523 | { 524 | #ifdef DEBUG 525 | Serial.println(" does not have access"); 526 | #endif 527 | writeLatest(uid, username, accountType, ACCESS_DENIED); 528 | mqttPublishAccess(epoch, "true", "Disabled", username, uid, pinCode); 529 | ledAccessDeniedOn(); 530 | beeperAccessDenied(); 531 | } 532 | if (processingState == unknown) 533 | { 534 | String data = String(uid) += " " + String(type); 535 | writeEvent("WARN", "rfid", "Unknown rfid tag is scanned", data); 536 | writeLatest(uid, "Unknown", 98, ACCESS_DENIED); 537 | DynamicJsonDocument root(512); 538 | root["command"] = "piccscan"; 539 | root["uid"] = uid; 540 | root["type"] = type; 541 | root["known"] = 0; 542 | size_t len = measureJson(root); 543 | AsyncWebSocketMessageBuffer *buffer = ws.makeBuffer(len); 544 | if (buffer) 545 | { 546 | serializeJson(root, (char *)buffer->get(), len + 1); 547 | ws.textAll(buffer); 548 | } 549 | mqttPublishAccess(epoch, "false", "Denied", "Unknown", uid, " "); 550 | ledAccessDeniedOn(); 551 | beeperAccessDenied(); 552 | } else if (uid != "" && processingState != waitingProcessing) 553 | { 554 | // message to the web UI to tell who has opened the door 555 | DynamicJsonDocument root(512); 556 | root["command"] = "piccscan"; 557 | root["uid"] = uid; 558 | root["type"] = type; 559 | root["known"] = 1; 560 | root["acctype"] = accountType; 561 | root["user"] = username; 562 | size_t len = measureJson(root); 563 | AsyncWebSocketMessageBuffer *buffer = ws.makeBuffer(len); 564 | if (buffer) 565 | { 566 | serializeJson(root, (char *)buffer->get(), len + 1); 567 | ws.textAll(buffer); 568 | } 569 | } 570 | } 571 | 572 | void cleanRfidLoop() 573 | { 574 | if (rfidState == waitingRfid) 575 | { 576 | delay(50); 577 | } 578 | // Keep an eye on timeout waitingProcessing for keypress 579 | // Clear code and timer when timeout is reached 580 | if ((keyTimer > 0 && millis() - keyTimer >= KEYBOARD_TIMEOUT_MILIS) || processingState != waitingProcessing) 581 | { 582 | #ifdef DEBUG 583 | Serial.println("[ INFO ] Read timeout or clean after read"); 584 | #endif 585 | keyTimer = 0; 586 | currentInput = ""; 587 | type = ""; 588 | uid = ""; 589 | v1uid = ""; 590 | rfidState = waitingRfid; 591 | processingState = waitingProcessing; 592 | ledWaitingOff(); 593 | } 594 | } 595 | 596 | void rfidLoop() 597 | { 598 | rfidPrepareRead(); 599 | rfidRead(); 600 | pinCodeRead(); 601 | rfidProcess(); 602 | rfidOutsideMessaging(); 603 | cleanRfidLoop(); 604 | } 605 | 606 | #ifdef DEBUG 607 | void ICACHE_FLASH_ATTR ShowMFRC522ReaderDetails() 608 | { 609 | // Get the MFRC522 software version 610 | byte v = mfrc522.PCD_ReadRegister(mfrc522.VersionReg); 611 | Serial.print(F("[ INFO ] MFRC522 Version: 0x")); 612 | Serial.print(v, HEX); 613 | if (v == 0x91) 614 | Serial.print(F(" = v1.0")); 615 | else if (v == 0x92) 616 | Serial.print(F(" = v2.0")); 617 | else if (v == 0x88) 618 | Serial.print(F(" = clone")); 619 | else 620 | Serial.print(F(" (unknown)")); 621 | Serial.println(""); 622 | // When 0x00 or 0xFF is returned, communication probably failed 623 | if ((v == 0x00) || (v == 0xFF)) 624 | { 625 | Serial.println(F("[ WARN ] Communication failure, check if MFRC522 properly connected")); 626 | } 627 | } 628 | #endif 629 | 630 | void ICACHE_FLASH_ATTR setupWiegandReader(int d0, int d1, bool removeParityBits) 631 | { 632 | wg.begin(d0, d1, removeParityBits); 633 | rfidState = waitingRfid; 634 | } 635 | 636 | void ICACHE_FLASH_ATTR setupMFRC522Reader(int rfidss, int rfidgain) 637 | { 638 | SPI.begin(); // MFRC522 Hardware uses SPI protocol 639 | mfrc522.PCD_Init(rfidss, UINT8_MAX); // Initialize MFRC522 Hardware 640 | // Set RFID Hardware Antenna Gain 641 | // This may not work with some boards 642 | mfrc522.PCD_SetAntennaGain(rfidgain); 643 | #ifdef DEBUG 644 | Serial.printf("[ INFO ] RFID SS_PIN: %u and Gain Factor: %u", rfidss, rfidgain); 645 | Serial.println(""); 646 | #endif 647 | #ifdef DEBUG 648 | ShowMFRC522ReaderDetails(); // Show details of PCD - MFRC522 Card Reader details 649 | #endif 650 | } 651 | 652 | void ICACHE_FLASH_ATTR setupPN532Reader(int rfidss) 653 | { 654 | // init controller 655 | pn532.InitSoftwareSPI(14, 12, 13, rfidss, 0); 656 | do 657 | { // pseudo loop (just used for aborting with break;) 658 | // Reset the PN532 659 | pn532.begin(); // delay > 400 ms 660 | byte IC, VersionHi, VersionLo, Flags; 661 | if (!pn532.GetFirmwareVersion(&IC, &VersionHi, &VersionLo, &Flags)) 662 | break; 663 | #ifdef DEBUG 664 | char Buf[80]; 665 | sprintf(Buf, "Chip: PN5%02X, Firmware version: %d.%d\r\n", IC, VersionHi, VersionLo); 666 | Utils::Print(Buf); 667 | sprintf(Buf, "Supports ISO 14443A:%s, ISO 14443B:%s, ISO 18092:%s\r\n", (Flags & 1) ? "Yes" : "No", 668 | (Flags & 2) ? "Yes" : "No", 669 | (Flags & 4) ? "Yes" : "No"); 670 | Utils::Print(Buf); 671 | #endif 672 | // Set the max number of retry attempts to read from a card. 673 | // This prevents us from waitingProcessing forever for a card, which is the default behaviour of the PN532. 674 | if (!pn532.SetPassiveActivationRetries()) 675 | { 676 | break; 677 | } 678 | // configure the PN532 to read RFID tags 679 | if (!pn532.SamConfig()) 680 | { 681 | break; 682 | } 683 | } while (false); 684 | } 685 | 686 | void ICACHE_FLASH_ATTR setupRDM6300Reader(int rmd6300TxPin) 687 | { 688 | rdm6300SwSerial = new SoftwareSerial(rmd6300TxPin, -1); 689 | rdm6300SwSerial->begin(RDM6300_BAUDRATE); 690 | rdm6300SwSerial->setTimeout(RDM6300_READ_TIMEOUT); 691 | } 692 | -------------------------------------------------------------------------------- /src/rfid125kHz.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | RFID reader. 3 | Based on the "RFID_Readerer" library (https://github.com/travisfarmer) from Travis Farmer. 4 | 5 | ================================================================== 6 | Copyright (c) 2018 Lubos Ruckl 7 | 8 | Permission is hereby granted, free of charge, to any person 9 | obtaining a copy of this software and associated documentation 10 | files (the "Software"), to deal in the Software without 11 | restriction, including without limitation the rights to use, 12 | copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the 14 | Software is furnished to do so, subject to the following 15 | conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | OTHER DEALINGS IN THE SOFTWARE. 28 | ================================================================== 29 | 30 | Hardware: RDM6300 or RF125-PS or Gwiot 7941E 31 | Uses 125KHz RFID tags. 32 | */ 33 | 34 | #include "Arduino.h" 35 | #include "rfid125kHz.h" 36 | 37 | char *RFID_Reader::ulltostr(unsigned long long value, char *ptr, int base) 38 | { 39 | unsigned long long t = 0, res = 0; 40 | unsigned long long tmp = value; 41 | int count = 0; 42 | if (NULL == ptr) 43 | { 44 | return NULL; 45 | } 46 | if (tmp == 0) 47 | { 48 | count++; 49 | } 50 | while (tmp > 0) 51 | { 52 | tmp = tmp / base; 53 | count++; 54 | } 55 | ptr += count; 56 | *ptr = '\0'; 57 | do 58 | { 59 | res = value - base * (t = value / base); 60 | if (res < 10) 61 | { 62 | *--ptr = '0' + res; 63 | } 64 | else if ((res >= 10) && (res < 16)) 65 | { 66 | *--ptr = 'A' - 10 + res; 67 | } 68 | } while ((value = t) != 0); 69 | return (ptr); 70 | } 71 | 72 | /* 73 | this is to simply return true when there is RFID data available. 74 | it is a function to prevent external manipulation of the boolean variable. 75 | */ 76 | bool RFID_Reader::Available() 77 | { 78 | return (data_available); 79 | } 80 | 81 | /* 82 | Returns the ID hexadecimal representation, and resets the Available flag. 83 | */ 84 | String RFID_Reader::GetHexID() 85 | { 86 | if (data_available) 87 | { 88 | uint8_t b[5]; 89 | memcpy(b, &new_ID, 5); 90 | char buf[11]; 91 | sprintf(buf, "%02x%02x%02x%02x%02x", b[4], b[3], b[2], b[1], b[0]); 92 | lasttagtype = tagtype; 93 | data_available = false; 94 | return String(buf); 95 | } 96 | return "None"; 97 | } 98 | 99 | /* 100 | Returns Tag type. 101 | */ 102 | String RFID_Reader::GetTagType() 103 | { 104 | for (uint8_t x = 0; x < 12; x++) 105 | { 106 | if (typeDict[x].itype == lasttagtype) 107 | return String(typeDict[x].stype); 108 | } 109 | return "Unknown"; 110 | } 111 | 112 | /* 113 | Returns the ID decimal representation, and resets the Available flag. 114 | */ 115 | String RFID_Reader::GetDecID() 116 | { 117 | if (data_available) 118 | { 119 | char ptr[128]; 120 | ulltostr(new_ID, ptr, 10); 121 | lasttagtype = tagtype; 122 | data_available = false; 123 | return String(ptr); 124 | } 125 | return "None"; 126 | } 127 | 128 | void RFID_Reader::rfidSerial(char x) 129 | { 130 | //if (x == StartByte && (ix==0 || ix > 1)) 131 | if (x == StartByte && ix != 1) 132 | { 133 | ix = 0; 134 | } 135 | else if (x == EndByte) 136 | { 137 | msg[ix] = '\0'; 138 | msgLen = ix; 139 | parse(); 140 | ix = 0; 141 | } 142 | else 143 | { 144 | msg[ix] = x; 145 | ix++; 146 | } 147 | } 148 | 149 | uint8_t RFID_Reader::get_checksum(unsigned long long data) 150 | { 151 | uint8_t b[5]; 152 | memcpy(b, &data, 5); 153 | return b[0] ^ b[1] ^ b[2] ^ b[3] ^ b[4]; 154 | } 155 | 156 | uint8_t RFID_Reader::char2int(char c) 157 | { 158 | c -= asciiNum_diff; 159 | if (c > 9) 160 | c -= asciiUpp_diff; 161 | return c; 162 | } 163 | 164 | /* 165 | the module spits out HEX values, we need to convert them to an unsigned long. 166 | */ 167 | void RFID_Reader::parse() 168 | { 169 | uint8_t lshift = 40; 170 | unsigned long long val = 0; 171 | unsigned long long tagIdValue = 0; 172 | uint8_t i; 173 | uint8_t checksum = 0; 174 | uint8_t recChecksum = 0; 175 | if ((msgLen + 2) == msg[0]) //Gwiot 7941E 176 | { 177 | for (i = 0; i < msgLen - 1; i++) 178 | { 179 | val = msg[i]; 180 | checksum = checksum ^ val; 181 | if (i > 1) 182 | { 183 | //lshift = 8 * (msgLen - 2 - i); 184 | lshift = (msgLen - 2 - i) << 3; 185 | tagIdValue |= val << lshift; 186 | } 187 | } 188 | recChecksum = (uint8_t)msg[i]; 189 | tagtype = (uint8_t)msg[1]; 190 | } 191 | else 192 | { 193 | for (i = 0; i < 10; i++) 194 | { 195 | val = char2int(msg[i]); 196 | //lshift = 4 * (9 - i); 197 | lshift -= 4; 198 | tagIdValue |= val << lshift; 199 | } 200 | checksum = get_checksum(tagIdValue); 201 | new_ID = 0ULL; 202 | if (msgLen == 12) 203 | { 204 | recChecksum = char2int(msg[i + 1]) | char2int(msg[i]) << 4; 205 | } //RDM6300 206 | else if (msgLen == 11) 207 | { 208 | recChecksum = (uint8_t)msg[i]; 209 | } //RF125-PS 210 | tagtype = 2; 211 | } 212 | if (checksum != recChecksum) 213 | return; 214 | unsigned long _now = millis(); 215 | if ((_now - LastRFID > 3000) || (tagIdValue != last_ID)) 216 | { 217 | new_ID = tagIdValue; 218 | last_ID = tagIdValue; 219 | data_available = true; 220 | LastRFID = _now; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/rfid125kHz.h: -------------------------------------------------------------------------------- 1 | /* 2 | RFID reader. 3 | Based on the "rfid_reader" library (https://github.com/travisfarmer) from Travis Farmer. 4 | 5 | ================================================================== 6 | Copyright (c) 2018 Lubos Ruckl 7 | 8 | Permission is hereby granted, free of charge, to any person 9 | obtaining a copy of this software and associated documentation 10 | files (the "Software"), to deal in the Software without 11 | restriction, including without limitation the rights to use, 12 | copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the 14 | Software is furnished to do so, subject to the following 15 | conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | OTHER DEALINGS IN THE SOFTWARE. 28 | ================================================================== 29 | 30 | Hardware: RDM6300 or RF125-PS or Gwiot 7941E 31 | Uses 125KHz RFID tags. 32 | */ 33 | 34 | #ifndef rfid125kHz_h 35 | #define rfid125kHz_h 36 | 37 | #include "Arduino.h" 38 | 39 | class RFID_Reader 40 | { 41 | public: 42 | void rfidSerial(char x); 43 | bool Available(); 44 | String GetHexID(); 45 | String GetDecID(); 46 | String GetTagType(); 47 | 48 | private: 49 | char *ulltostr(unsigned long long value, char *ptr, int base); 50 | void parse(); 51 | uint8_t char2int(char c); 52 | uint8_t get_checksum(unsigned long long data); 53 | static const char asciiNum_diff = 48; 54 | static const char asciiUpp_diff = 7; 55 | bool data_available = false; 56 | unsigned long long new_ID = 0ULL; 57 | unsigned long long last_ID = 0ULL; 58 | uint8_t tagtype; 59 | uint8_t lasttagtype; 60 | unsigned long LastRFID = 0UL; 61 | char msg[15]; 62 | uint8_t msgLen; 63 | byte ix = 0; 64 | byte StartByte = 0x02; 65 | byte EndByte = 0x03; 66 | typedef struct 67 | { 68 | uint8_t itype; 69 | char *stype; 70 | } typeDictionary; 71 | 72 | const typeDictionary typeDict[12]{ 73 | {0x01, (char *)"MIFARE 1K"}, 74 | {0x02, (char *)"EM4100"}, 75 | {0x03, (char *)"MIFARE 4K"}, 76 | {0x10, (char *)"HID card"}, 77 | {0x11, (char *)"T5567"}, 78 | {0x20, (char *)"2G certificate"}, 79 | {0x21, (char *)"IS014443B"}, 80 | {0x22, (char *)"FELICA"}, 81 | {0x30, (char *)"15693 tag"}, 82 | {0x50, (char *)"CPU card"}, 83 | {0x51, (char *)"sector information"}, 84 | {0xFF, (char *)"keyboard data"}}; 85 | }; 86 | #endif 87 | -------------------------------------------------------------------------------- /src/todo: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - bug da papà di Giutrec 4 | Oggi ho fatto delle prove con la scheda apriporta di prova 5 | se si setta la lettura del tag in decimale e si flaggano le due opzioni, si verifica un problema serio 6 | a) se si inseriscono i pin apre 7 | b) se si passa il tag e poi il pin: apre 8 | ma se si digita il codice decimale del tag apre senza digitare il pin 9 | 10 | Ho in test il primo degli ultimi codici 11 | 1 problema: il log segnala solo i tag errati, non i pin errati, quindi se qualcuno si mette a giocare con la tastiera non registra nulla. 12 | 2 problema: se si imposta una data di scadenza non permette tutti gli ingressi dalle 00:00 alle 00:59, inoltre se si imposta una data esempio oggi 19/03 già dalle 00:01 del 19 non permette l’accesso. 13 | 14 | “però in esadecimale, lasciando flaggate le due opzioni, ogni tanto fa casino e non legge nulla ed occorre riavviare la scheda e talvolta non serve neanche quello. Bisogna aspettare un po’ e poi riprende a funzionare. Accade specialmente se si inserisce un nuovo tag o un nuovo pin.” 15 | -------------------------------------------------------------------------------- /src/webh/index.html.gz.h: -------------------------------------------------------------------------------- 1 | #define index_html_gz_len 3124 2 | const uint8_t index_html_gz[] PROGMEM = { 3 | 0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0xe5,0x5a,0xc9,0x72,0xe3,0x38,0xb6,0xdd,0xf7,0x57,0x20,0x59,0x11,0xd5,0x99,0xe1,0xa4,0x69,0x4b,0x1e,0xb3,0x24,0x75,0x50,0x83,0x6d,0xd9,0x92,0x07,0x0d,0x96,0x95,0x3b,0x90,0x80,0x48,0x58,0x20,0x40,0x03,0xa0,0x28,0xa9,0xe3,0x45,0xbc,0xdf,0x78,0xbf,0xf7,0xbe,0xa4,0x03,0x20,0xa9,0xc1,0x65,0xa5,0x5c,0x8b,0x5e,0x95,0x17,0x0a,0x12,0xc3,0xbd,0x17,0xe7,0xce,0xa0,0x2b,0x5f,0x9a,0x0f,0x8d,0xc1,0xf8,0xb1,0x05,0x42,0x15,0xd1,0x5a,0x45,0xff,0x02,0x0a,0x59,0x50,0xb5,0x30,0xb3,0x6a,0x95,0x10,0x43,0x54,0xab,0x44,0x58,0x41,0xe0,0x87,0x50,0x48,0xac,0xaa,0x56,0xa2,0x26,0xf6,0x85,0x55,0xab,0x50,0xc2,0xa6,0x20,0x14,0x78,0x52,0xb5,0x10,0x54,0xf0,0x07,0x89,0x60,0x80,0x9d,0x98,0x05,0x7f,0x78,0x50,0xe2,0xb3,0x93,0xef,0xe4,0xb9,0xfe,0xd0,0x4b,0x8f,0xee,0xae,0x03,0xee,0xba,0xae,0x7b,0xdf,0x1f,0x86,0xad,0x61,0xe0,0xba,0x6e,0x43,0xbf,0xba,0x41,0xc3,0x1d,0xbb,0xae,0x5b,0x5f,0x62,0x26,0x8e,0xf4,0x40,0x3d,0xba,0xed,0xf4,0x8e,0x9e,0x5c,0x27,0x6d,0x3a,0xee,0xe3,0x41,0x30,0x83,0x78,0xa0,0xc7,0x1b,0x2f,0xf5,0xf6,0xe8,0xa5,0xeb,0xba,0xae,0xd4,0xef,0x9d,0x56,0xda,0x72,0xa3,0x38,0x1d,0x9b,0x3d,0xe5,0xde,0xed,0x60,0x38,0xbc,0x39,0x09,0x38,0x6a,0xba,0x53,0xfc,0x32,0x5c,0x9e,0x84,0x66,0xe2,0x38,0x7e,0xbe,0x0a,0x8f,0x9e,0x4a,0x97,0xca,0x1b,0x3d,0x27,0xc8,0xcd,0xfe,0x9e,0xca,0xb7,0x74,0xfc,0xd2,0xa3,0x3f,0x1b,0xf5,0x32,0x7c,0xe9,0xf1,0x76,0x0b,0xdd,0x0e,0x9e,0xeb,0xd3,0x4e,0x34,0xbc,0xd1,0xac,0x9a,0x47,0xb4,0xf5,0xf4,0xdc,0x1b,0xa7,0x07,0xe5,0xd1,0x43,0x49,0x0e,0x87,0xbd,0x81,0x7b,0xf1,0x7c,0xb0,0xb8,0xbb,0x7e,0x0d,0xdb,0xf3,0x76,0x93,0x87,0x6e,0x5f,0x0c,0x9e,0x4a,0x0d,0x71,0xae,0x06,0x8d,0xdb,0xd1,0x24,0x68,0xaa,0xf3,0xa7,0x76,0xec,0xdd,0x95,0x2f,0xea,0x77,0x9d,0xd2,0xe8,0x6d,0x5a,0xef,0x8c,0x17,0x8d,0x7e,0xbf,0xfb,0x56,0x7a,0x8c,0xbb,0xe9,0xdd,0xe0,0xe9,0xbe,0x39,0xbe,0x1b,0x87,0x0f,0xf7,0xc9,0x72,0xcc,0xfc,0x0e,0x93,0xe3,0xd2,0xcf,0x52,0x67,0xf8,0xf4,0xf8,0x52,0xf7,0xa2,0xf3,0xb8,0xbc,0x64,0xe5,0x47,0xe7,0xf2,0x62,0x99,0x8a,0x23,0x34,0xb9,0x54,0x32,0xbe,0x9e,0x96,0x47,0xa7,0x0a,0x75,0xdc,0xf9,0xb2,0xf1,0x30,0x5a,0xf2,0x8b,0xfb,0x83,0x59,0xd2,0xbe,0xa4,0xc7,0x37,0x6e,0xb7,0x75,0xfc,0x7a,0x75,0x34,0x27,0xcf,0xe3,0x20,0x16,0x6f,0x75,0x7f,0xac,0xce,0xdd,0xd9,0x71,0x52,0xbe,0x61,0xde,0xeb,0x41,0xf3,0xe1,0xfe,0x79,0xaa,0x50,0xda,0x0e,0xbd,0x72,0xef,0xe8,0xe5,0x4c,0x06,0x6f,0x74,0x9c,0xa6,0x4d,0xdc,0x3e,0xbe,0x9a,0x75,0xd1,0xc3,0xe0,0xba,0x3d,0x64,0x67,0xb7,0x8d,0xf8,0xa9,0xf1,0x70,0x72,0x94,0x4e,0xdf,0xae,0xbb,0xa3,0xa6,0xe3,0xf6,0xda,0x97,0x64,0xec,0x8e,0x8f,0xc3,0xfa,0xf1,0x69,0xd8,0x1e,0x5f,0x8f,0xd3,0xfb,0xd3,0xf1,0x73,0x1a,0x93,0x6b,0x71,0xd9,0x88,0xeb,0x4c,0x92,0x61,0xdf,0xf3,0x07,0xa3,0x9f,0xfd,0x23,0x1f,0xa9,0xc5,0x63,0x78,0xc0,0x3c,0x9f,0x9c,0x33,0x76,0x31,0x6c,0x50,0xf7,0xe5,0x9c,0xf8,0xaf,0xe3,0x32,0x19,0x5d,0xde,0xbb,0x75,0xae,0x58,0x2b,0x08,0xe7,0x52,0x36,0x8e,0x6f,0x5a,0x97,0x77,0x0e,0x1d,0x8e,0xee,0x4f,0x78,0xe3,0xa6,0xe5,0xa2,0xd2,0x43,0x32,0xbd,0x25,0xb1,0xeb,0x1c,0xcf,0xf9,0xd1,0xf1,0xfc,0xb6,0xeb,0xbc,0xb6,0x82,0xf1,0xf9,0xcd,0xc3,0x43,0xb3,0x54,0x3f,0x25,0x9d,0xa8,0x47,0x5e,0xc3,0xa7,0x9e,0xbf,0x14,0xde,0xe9,0xf2,0xf9,0x22,0x1d,0x5f,0x94,0x06,0x97,0xe7,0xb7,0xc4,0x7b,0xea,0xdf,0xfc,0x2c,0x8d,0x3d,0x72,0x05,0x4b,0xa8,0xd5,0x1d,0xab,0xd2,0xed,0xd5,0xd9,0x51,0x00,0x55,0xeb,0x76,0x79,0xfa,0x93,0xcf,0x0f,0x4a,0xa5,0xf6,0xf2,0x80,0x78,0xcb,0xd6,0xb0,0xc3,0xe7,0xa4,0x3e,0x94,0xa7,0x69,0xe3,0xed,0xba,0x75,0x7a,0x75,0xe6,0x0e,0xae,0xdd,0xfb,0x94,0xb5,0x70,0x7c,0x31,0x5e,0xf4,0xaf,0xde,0x0e,0xea,0xe9,0x0b,0x22,0x13,0x7a,0x36,0x1f,0x93,0xab,0x97,0x87,0x39,0xfb,0x49,0x8e,0x47,0x9d,0x7e,0xa3,0xb9,0x2c,0xb5,0xa6,0x9d,0x17,0x15,0xc2,0x5b,0x75,0x90,0x06,0xf8,0x72,0xd8,0x6c,0xcf,0x5a,0x17,0xdd,0xdb,0xa8,0x7d,0x30,0x68,0x9d,0xdf,0x9c,0x4c,0x4e,0xba,0x27,0x8d,0x51,0x72,0x72,0xd3,0x7a,0x9c,0x2d,0xfd,0x6b,0x7e,0x8e,0xdb,0x27,0xce,0xd1,0xa5,0xdb,0x86,0xa4,0x3c,0xc0,0xf7,0xdc,0xb9,0xea,0x9f,0x89,0x71,0xc3,0x5f,0x06,0xe8,0xf2,0x72,0x71,0x15,0x95,0x6f,0xa6,0xb4,0xdc,0x98,0xb7,0xca,0xa5,0x72,0xeb,0xb1,0x7b,0xf2,0x3c,0x78,0x3e,0x55,0x47,0xe7,0xdd,0x93,0x52,0x7f,0x70,0xda,0x55,0x74,0x4a,0xf1,0x15,0xbc,0x9d,0x5c,0x3c,0xd1,0xd1,0x4f,0x32,0xed,0x74,0x7f,0x8e,0x46,0xce,0x10,0xbe,0xc8,0xf3,0x7e,0x3d,0x72,0x4e,0x7b,0x69,0x0f,0x3e,0x3c,0x47,0xcd,0xa3,0x9e,0x48,0xe2,0x52,0xdc,0x8d,0xca,0x3d,0xc5,0x5e,0xde,0x82,0xf8,0x05,0xbd,0x26,0xf8,0xb6,0xdb,0xa9,0x5f,0xd1,0x65,0x33,0x29,0x9d,0xb5,0xee,0xe6,0xa2,0x79,0x84,0x1e,0x97,0xa5,0xba,0x37,0x0b,0x26,0xce,0x1c,0x5d,0xf4,0x66,0xfd,0x91,0x8c,0x6f,0x27,0x65,0x37,0x79,0x31,0xee,0xe1,0xf6,0x87,0xcf,0x0f,0xbd,0xbb,0xd3,0xc6,0xb8,0xdd,0xae,0x5a,0x40,0x60,0x5a,0xb5,0x88,0xcf,0x99,0x05,0xd4,0x22,0xc6,0x55,0x2b,0xf3,0xd9,0xb9,0x6d,0xc6,0x72,0x47,0x0f,0x95,0x8a,0x6d,0xfc,0x96,0x90,0x59,0xd5,0x7a,0xb1,0x87,0xae,0xdd,0xe0,0x51,0x0c,0x15,0xf1,0x28,0xb6,0x80,0xcf,0x99,0xc2,0x4c,0x55,0xad,0x76,0xab,0x8a,0x51,0x80,0x8b,0x4d,0x0c,0x46,0xb8,0x6a,0xcd,0x08,0x4e,0x63,0x2e,0xd4,0xc6,0xba,0x94,0x20,0x15,0x56,0x11,0x9e,0x11,0x1f,0xdb,0xe6,0xe5,0x3b,0x61,0x44,0x11,0x48,0x6d,0xe9,0x43,0x8a,0xab,0xc7,0xdb, 4 | 0x24,0x10,0x96,0xbe,0x20,0xb1,0x22,0x5a,0xc8,0x15,0x95,0xed,0x35,0x30,0x51,0x21,0x17,0xdb,0xd3,0x8a,0x28,0x8a,0x6b,0x58,0xc6,0xb6,0x98,0x10,0x54,0x71,0xb2,0xf7,0xca,0x17,0xdb,0x06,0x75,0xce,0x95,0x54,0x02,0xc6,0xc0,0xe7,0x02,0x83,0x46,0xbf,0x0f,0x6c,0x7b,0x2b,0x7e,0xf9,0x52,0x3a,0x42,0x1f,0x59,0x60,0x74,0xe8,0x4b,0x99,0x23,0x25,0xd5,0x82,0x62,0x19,0x62,0xac,0xac,0x5a,0xc5,0xbc,0xd4,0x7e,0xe3,0x31,0x66,0x84,0x05,0x21,0x4f,0x84,0x04,0x2a,0x04,0xff,0xfe,0x07,0xd8,0xf8,0x53,0x78,0xae,0x6c,0x48,0x49,0xc0,0x7e,0x00,0x1f,0x33,0x85,0xc5,0x1f,0xab,0xf9,0xff,0x59,0x3d,0xbd,0x23,0x82,0xde,0x11,0x89,0x21,0x42,0x84,0x05,0x3f,0x40,0x29,0x9e,0x7f,0x66,0xfb,0x0f,0xa6,0x42,0xdb,0x0f,0x09,0x45,0x5f,0x4b,0xec,0xdb,0xf7,0xf7,0xf3,0xe1,0xf6,0xfc,0x3b,0x66,0x1e,0xf4,0xa7,0x81,0xe0,0x09,0x43,0xb6,0xcf,0x29,0x17,0x3f,0xc0,0x6f,0x9e,0xe7,0x6d,0xb0,0xad,0x38,0xd9,0xc9,0x6b,0x15,0x27,0xcb,0x06,0x1e,0x47,0x8b,0x5a,0x05,0x91,0x19,0xf0,0x29,0x94,0xb2,0x6a,0xa5,0x02,0xc6,0x31,0x16,0x56,0x86,0x76,0x9f,0x20,0xec,0x41,0x01,0x6e,0x38,0x45,0x58,0x18,0xa4,0x19,0x9c,0x01,0x82,0xaa,0x96,0xcc,0xa6,0xac,0x6c,0xb7,0x1e,0x41,0x44,0x46,0x44,0x4a,0xab,0x56,0x21,0x05,0xb5,0x80,0x2e,0xe2,0x50,0xdb,0x23,0x58,0x3d,0xd9,0x50,0x08,0x9e,0xda,0x14,0x4f,0xb4,0x22,0x1c,0x52,0xab,0x38,0x88,0xcc,0xb6,0x64,0xc8,0x49,0xdb,0x5a,0x44,0x23,0x8a,0x27,0x6a,0x95,0xf0,0x78,0x0f,0x5d,0x05,0x03,0x69,0x01,0x28,0x08,0xb4,0x43,0x82,0x10,0x66,0x55,0x4b,0x89,0x04,0x67,0x4c,0xd6,0xb6,0xa4,0xe9,0x14,0x12,0xcb,0x85,0x54,0x38,0xd2,0x76,0x68,0x15,0x94,0x8d,0xd2,0x33,0x6d,0x5b,0x85,0x68,0xd9,0x6f,0x42,0x8b,0x35,0x94,0x48,0x65,0x27,0xcc,0x40,0x89,0x80,0xcf,0xa3,0x98,0x33,0xcc,0x94,0x34,0x79,0xb4,0x58,0x04,0x7d,0x45,0x66,0x9a,0x3b,0xcc,0xed,0xf2,0x37,0x2b,0xe3,0xa9,0xa0,0x4a,0xf6,0x82,0x14,0xf2,0x28,0x97,0xbc,0x6f,0xd6,0x57,0x1c,0x58,0xab,0x38,0x94,0x68,0x0e,0x1b,0x24,0xf5,0xb2,0x7e,0xe2,0x45,0x98,0x25,0x16,0xd0,0x89,0xdb,0x56,0x3c,0x08,0x28,0xae,0x5a,0x3e,0xa7,0x14,0xc6,0x12,0xe7,0x88,0xe0,0x79,0x0c,0x19,0xc2,0xa8,0x6a,0x4d,0x20,0x95,0x78,0x1f,0x7b,0x9f,0x07,0x39,0x77,0xac,0x14,0x61,0x41,0xc6,0x7f,0x8d,0x40,0x41,0x1d,0x6c,0x41,0x91,0x1d,0x70,0x53,0xa6,0x77,0xe2,0x66,0x0b,0x18,0x56,0x29,0x17,0xd3,0x7d,0x32,0x48,0x12,0x30,0x48,0x33,0x31,0x46,0x44,0x60,0x8a,0xa5,0x04,0xf7,0xd9,0xde,0x1d,0x70,0xe4,0x02,0x40,0x81,0x52,0x28,0xf6,0x1e,0x32,0x15,0x98,0xf9,0x61,0xc6,0xe0,0x26,0xdf,0x03,0xb6,0x0e,0xbc,0x8b,0x43,0x80,0x19,0x16,0x46,0xb6,0x5f,0x32,0x30,0xe0,0x40,0x9a,0xdb,0xf9,0x75,0xb6,0xe9,0x73,0x1c,0xa2,0x37,0xa5,0xf6,0x93,0x67,0xd3,0x8c,0x74,0xf7,0x69,0x30,0xf8,0x1c,0x5d,0xa6,0xe2,0xfd,0xa6,0x97,0x88,0x40,0x4f,0x67,0xb4,0xef,0x07,0x8f,0xe0,0xeb,0x80,0x44,0xf8,0xdb,0x07,0x1c,0x9c,0x84,0xee,0xe6,0x95,0x48,0x2c,0xf6,0x1a,0xba,0x5e,0x94,0x31,0x1a,0xea,0xe5,0x3b,0x84,0xa7,0x3c,0x90,0xff,0x0d,0x3b,0x9f,0x98,0xb8,0x66,0xeb,0x18,0x9b,0x09,0xd1,0xe1,0x7f,0xc9,0xd6,0x37,0xe5,0xfa,0x10,0x03,0x0a,0x15,0x96,0x8a,0x1a,0x77,0xfa,0xb5,0xc7,0x41,0x8a,0x19,0x82,0x39,0x16,0xae,0xef,0x6b,0x6b,0xef,0xf0,0xe0,0x97,0xda,0xc4,0x33,0xcc,0x3e,0x43,0x5c,0x09,0xc8,0xe4,0xa4,0x00,0xba,0xa5,0x77,0xed,0xa5,0x4d,0x79,0x10,0x41,0xa2,0x73,0x32,0x64,0xfe,0x5f,0xf2,0xa5,0x0e,0x0f,0x26,0x84,0x62,0xd0,0x5d,0x6f,0xdf,0x65,0x31,0xbb,0x98,0xeb,0x44,0x96,0xec,0xb5,0xd4,0x09,0xe5,0x71,0xbc,0xb0,0x11,0x91,0xb9,0x1f,0xd4,0xcd,0x36,0xf0,0x3b,0xe8,0x61,0xa9,0xb8,0xc0,0xbf,0x3c,0xa0,0xc0,0x12,0xef,0xf5,0x31,0x81,0x63,0x0c,0x73,0x07,0xbe,0x82,0xbe,0xe2,0x62,0xa1,0x89,0x63,0xf5,0x9e,0xf4,0x96,0x51,0x46,0x1c,0x41,0x6a,0x15,0xec,0x92,0x18,0x41,0xb5,0x17,0xc1,0xb5,0x0d,0x0e,0xcd,0xfa,0x77,0x90,0xed,0xca,0x3e,0x8d,0x81,0x2b,0xdf,0x99,0x9e,0xae,0xf9,0xe4,0x0f,0xc7,0x09,0x88,0x0a,0x13,0xef,0xd0,0xe7,0x91,0x83,0x65,0xac,0xb3,0x9f,0x53,0xa4,0xc1,0x55,0xba,0x43,0x3c,0x65,0x94,0x43,0x64,0xd5,0x1e,0x05,0x7f,0xc5,0xbe,0xfa,0xa7,0x04,0xd7,0x44,0xdd,0x24,0x1e,0x78,0x84,0xc1,0x0e,0x00,0x0b,0x06,0x12,0x52,0x75,0xe8,0xf1,0x84,0xa9,0x85,0xe4,0x89,0xf0,0xb1,0xe1,0xe5,0x87,0xd8,0x9f,0xf2,0x44,0x39,0x30,0xd2,0x53,0xff,0x52,0x18,0x46,0xd5,0xdd,0x8c,0x3f,0xc8,0xd8,0xdb,0x60,0xc6,0x3c,0xe6,0x33,0x2c,0x8a,0x51,0x41,0x82,0x00,0x0b,0x9d,0x66,0xd6,0x83,0x31,0x85,0x3e,0x8e,0x4c,0xf5,0xa8,0x78,0x9c,0x0f,0xae,0x0a,0xca,0x7b,0xae,0x42,0xc2,0x02,0x20,0xe1,0x42,0x02,0x0f,0x2b,0x85,0x05,0x50,0x21,0x64,0x53,0xb0,0xe0,0x89,0x79,0x02,0x10,0x20,0xce,0xa0,0xae,0x53,0x0f,0xf7,0xe9,0x09,0x27,0x82,0x67,0x7a,0x6a,0xea,0x2d,0xbb,0x6d,0xac,0xa8,0x03,0x84,0x22,0xbe,0xae,0xb6,0x39,0xf3,0x29,0xf1,0xa7,0xc6,0xb3,0x78,0xa2,0xbe,0x7e,0xb3,0xb4,0xa7,0xf0,0xe4,0x9d,0x29,0x85,0x67,0x59,0x02,0x80,0x84,0x99,0x03,0x7e,0x5c,0x97,0x84,0x67,0x5b,0xfe,0xc4,0xe0,0x2c,0x2b,0xd8,0xb4,0xd2,0x40,0x23,0x3b,0xf9,0x66,0xd5,0x56,0x54,0x3c,0x39,0x28,0xba,0xa6,0x4a,0x94,0xe2,0x2c,0x6f,0x1d,0xb2,0x17,0x6b,0xb3,0xb0,0x6b,0xac,0x02,0x6b,0x2e,0x81,0xa7,0x18,0xf0,0x14,0xb3,0x09,0x9b,0x70,0xc0,0xe0,0x4c,0x57,0x68,0x9e,0x62,0xfb,0xf0,0xd2,0xb1,0xd1,0x0e,0x61,0xe4,0x25,0x22,0x28,0x22,0x10,0xa8,0xc8,0x18,0xb2,0x5a,0x17,0xb3,0xa4,0xe2,0x98,0xc7,0x8a,0x93,0x89,0xb0,0x55,0x07,0xc6,0x09,0xa5,0xb6,0x20,0x41,0xa8,0x32,0xc9,0x52, 5 | 0xa9,0x95,0xca,0xb0,0xaf,0x15,0x65,0xaf,0x0a,0xa9,0x8d,0x1d,0x90,0x62,0xa1,0x80,0xf9,0xb5,0x53,0x28,0x74,0xe1,0x6c,0x01,0xc1,0xb5,0x19,0x99,0xc1,0xbd,0x61,0x2c,0xdb,0x63,0x6a,0x8f,0x2d,0x51,0x1b,0x2b,0xbe,0x20,0x25,0x2a,0x04,0x85,0x3d,0x03,0xe8,0x49,0xcc,0xd4,0x21,0xe8,0xe1,0x42,0x34,0x16,0x1c,0x1e,0x1e,0xae,0x8e,0xb5,0x51,0x40,0x16,0x3a,0x80,0xaf,0x70,0xbe,0xd6,0xc3,0xf6,0x9c,0xc0,0x33,0x9f,0x47,0x11,0x51,0x2b,0xd8,0x4d,0x2c,0x01,0x13,0x88,0x70,0x71,0x12,0x44,0x60,0x16,0xf3,0x37,0x0e,0x6e,0x56,0xd9,0xab,0x19,0x6d,0x0a,0x5d,0xb3,0x31,0x67,0x54,0xd8,0xc0,0xd6,0xf2,0xb5,0x10,0x7f,0x9a,0x5a,0x97,0xde,0x1f,0x99,0x49,0x91,0x19,0x29,0x97,0x85,0xb7,0xe6,0xc5,0x7f,0x11,0xfb,0x6a,0xbf,0x2b,0x12,0x61,0xf9,0xc7,0x5a,0xaf,0xe1,0xc9,0x36,0x07,0xd3,0xd3,0x59,0xb5,0x47,0x8a,0xa1,0xc4,0x40,0x60,0xdd,0x6e,0xea,0x7b,0x29,0x16,0x60,0x59,0x71,0xc2,0x93,0x0f,0xfa,0x82,0x6c,0xa3,0x6e,0x59,0xac,0x5a,0x25,0x16,0xd8,0x20,0xf6,0x2a,0x39,0x0b,0x8d,0xa1,0x6b,0x30,0x63,0x81,0x77,0x6e,0x9c,0x70,0xae,0xf6,0x9e,0xa9,0xb0,0x72,0x84,0x27,0x30,0xa1,0x6a,0xc7,0xe9,0x1a,0x3a,0xa9,0xd1,0xd5,0xe1,0xc0,0xc7,0x24,0x57,0x1e,0x9f,0xa9,0xf4,0xeb,0xb7,0x3f,0x71,0x91,0x89,0x49,0xf4,0x3b,0xb8,0xf4,0xe1,0x0c,0x9b,0x64,0xe6,0x71,0xae,0xd6,0x48,0x6e,0xda,0xd4,0x47,0xf6,0x85,0xb0,0x54,0x82,0x2f,0xfe,0x2e,0x16,0xe4,0x0a,0x6c,0x02,0x39,0xf4,0x24,0xa7,0x89,0xc2,0x74,0x01,0x64,0x22,0xf0,0xbf,0x76,0x18,0xd1,0x47,0x21,0xa2,0x56,0x09,0x4f,0xf5,0xa5,0x80,0xe0,0x2c,0xa8,0x8d,0xb2,0xc1,0x2f,0xba,0x57,0x36,0x03,0x60,0xc8,0xf0,0x3c,0xc6,0xbe,0xc2,0x08,0x78,0x10,0x01,0x93,0x4d,0x24,0x48,0x09,0xa5,0x20,0xd4,0xdd,0x32,0x03,0x64,0x62,0x44,0x40,0x9c,0xfd,0xff,0xff,0xfe,0x9f,0x02,0x02,0x67,0xcb,0xe4,0x97,0x8a,0xa3,0x29,0xff,0xda,0x92,0xc3,0xd3,0xda,0x20,0x24,0x12,0xc0,0x2c,0xbe,0x14,0x72,0xf8,0x90,0x31,0xad,0xf6,0x42,0x0a,0x0f,0x83,0x84,0x21,0xce,0xf0,0x21,0x30,0xcb,0x0d,0xff,0x18,0x8b,0x08,0xea,0xe6,0x93,0x2e,0x00,0xc2,0x14,0x2b,0xbc,0xda,0x0f,0x29,0x05,0x2a,0xc4,0x40,0x57,0xd8,0x06,0xe1,0xef,0x40,0xe6,0x05,0x3c,0x80,0x0c,0x01,0x5d,0xbb,0x1e,0xae,0xc9,0xe7,0x8e,0x18,0xc1,0x29,0x36,0xf8,0x99,0x03,0x45,0x10,0x61,0x00,0x41,0x56,0x8e,0x81,0x09,0x11,0x52,0x1d,0x66,0x47,0xd2,0x87,0x31,0x25,0x88,0x56,0x22,0x86,0xe2,0x87,0xc7,0x55,0xf8,0x47,0xde,0xab,0x67,0xe7,0x0d,0x4f,0x0b,0xef,0xd6,0xba,0x07,0x84,0x19,0x71,0x42,0x2e,0x95,0xee,0xb8,0x01,0x9f,0x98,0xf7,0xec,0x36,0x09,0x28,0xae,0x0d,0x6d,0x42,0x44,0xf4,0x79,0x06,0x71,0xad,0x42,0x58,0x9c,0xa8,0xdc,0xb6,0x74,0x8a,0x5c,0x59,0xd6,0x84,0x8b,0xc8,0x98,0xa7,0xe0,0x14,0x98,0x55,0xb6,0x47,0xb9,0x3f,0xb5,0xf2,0x34,0x18,0xc5,0xba,0x43,0x04,0x9c,0x99,0xb9,0xd5,0x48,0x33,0xf3,0x1d,0x9d,0x9c,0x2b,0x4e,0xfc,0xe9,0x50,0xb2,0xe1,0x76,0x3a,0x25,0xbe,0x33,0x76,0x44,0x24,0xf4,0xa8,0x6e,0x4a,0x36,0x62,0x02,0x5a,0x71,0x7a,0x1f,0x14,0x8c,0x9c,0x59,0x10,0xd2,0xd1,0x50,0x58,0xb5,0xb6,0xd6,0x3c,0x16,0x52,0x69,0xbd,0x69,0xd0,0x7c,0xce,0x24,0x7e,0x4b,0x30,0xf3,0xb1,0xd6,0xe1,0x67,0x23,0x83,0x4e,0x6b,0x84,0xfd,0x5d,0x02,0x43,0x6e,0x7c,0x7d,0x12,0x30,0xd0,0x66,0x3b,0xc2,0x81,0xe0,0x69,0x6e,0x53,0x1b,0x83,0x3e,0xa7,0x76,0x84,0xec,0x0b,0x90,0x3f,0xf0,0xc9,0x44,0x62,0x65,0x97,0xb6,0x8f,0x43,0x79,0x40,0x98,0x1d,0x43,0x86,0x29,0xd8,0xf8,0x5d,0xa5,0x8e,0xed,0x5a,0xc6,0x4c,0xe5,0x1e,0xaf,0x8d,0x33,0x47,0x5c,0x3f,0x6a,0xab,0x90,0x89,0x17,0x11,0xa5,0x0b,0x00,0x95,0x08,0x06,0x0c,0xed,0xaf,0xdf,0xbe,0x7f,0xd1,0x37,0xaa,0x13,0x82,0x29,0x92,0x58,0x6d,0x11,0x34,0x06,0xae,0x2f,0xfb,0x4c,0x9b,0x64,0xdc,0x40,0xab,0x38,0x86,0x52,0xa6,0x5c,0xa0,0x0f,0x1d,0xc1,0x02,0xa6,0x52,0xce,0xd2,0x65,0xd5,0x7a,0x5c,0xad,0xcd,0xae,0x62,0xd7,0x7b,0x33,0x9d,0xac,0xdf,0x67,0x90,0x26,0x58,0x9b,0x6f,0x71,0xb1,0xaa,0x9f,0x0d,0xcc,0x55,0x2b,0x87,0xd9,0x94,0xa6,0x3a,0x68,0x08,0xb0,0xda,0x57,0xe0,0xbd,0xa5,0xe9,0xec,0xa4,0xbb,0x52,0xa1,0x79,0x8e,0x10,0xd8,0xa8,0xfd,0x74,0x99,0x4c,0xd8,0x86,0x95,0xaf,0xf1,0x70,0xf4,0xe9,0x76,0xdb,0xfd,0x6e,0x4f,0xc8,0x7b,0xb2,0xbf,0x89,0x27,0x64,0x1d,0x25,0xb8,0x22,0x22,0xd2,0x97,0x5c,0x7f,0x29,0x33,0xee,0xcc,0x8a,0x9f,0x4d,0x18,0x7b,0xb2,0x5f,0x96,0x2a,0x4e,0x6a,0x1d,0x73,0x53,0x02,0x9e,0xb1,0x90,0x3a,0x0b,0x3e,0x30,0x4a,0x58,0x2e,0x69,0xa1,0x34,0x6e,0xc6,0x56,0xed,0x74,0x78,0x9a,0x97,0xcc,0x46,0x10,0x0d,0xa6,0xe9,0x8c,0x3e,0x97,0x3b,0xb4,0xf2,0x56,0x35,0x64,0x4e,0x22,0x93,0xe8,0x3a,0x4b,0x92,0x20,0xe3,0x03,0x74,0xbb,0x23,0x22,0xd3,0x1b,0x82,0x89,0xe0,0x51,0xde,0x1e,0x9b,0x1a,0x5f,0x57,0x9b,0xe0,0x9d,0xd6,0x37,0x0d,0xb7,0x02,0x77,0xb5,0x32,0x45,0xfb,0xbb,0x3a,0xcc,0x67,0x6a,0xd2,0x58,0x90,0x08,0x8a,0x85,0x55,0x6b,0xe6,0xbb,0x37,0x7c,0x02,0xee,0xb2,0xf6,0x3d,0x38,0x14,0xf0,0x37,0x12,0x21,0x74,0xcf,0x98,0xe3,0xff,0x23,0x43,0x3e,0x87,0x78,0x96,0x0d,0xbe,0x87,0xf8,0xd7,0x91,0xc8,0xcb,0x90,0x33,0x29,0xcf,0x94,0xf5,0xda,0xc2,0x28,0x4f,0x87,0xb1,0x16,0x5d,0xe7,0xbd,0xec,0xb0,0xfa,0xae,0xa8,0x88,0x40,0x85,0x5b,0x42,0xdf,0xc7,0xb1,0xaa,0x5a,0x87,0x1e,0x61,0xd6,0x07,0xa6,0xba,0x05,0x72,0x0e,0xdd,0x2a,0xb3,0x26,0x2b,0x06,0x3b,0xf0,0xcb,0x23,0x80,0xc9,0xd4,0x1b,0xb9,0x79,0x75,0xf1,0xb2,0x3b,0x9d,0x7e,0x0e,0xd0,0x1d,0xb5,0xc2,0xaf,0x23,0x53,0xb6,0x6c,0x8d,0x69,0xbe,0x69,0xdd,0xb8, 6 | 0x6f,0x75,0x85,0x3a,0xaa,0x40,0xc2,0xcc,0x92,0xf0,0x6c,0xeb,0x92,0x20,0x4a,0x14,0x46,0xd6,0xea,0x8b,0x07,0xb0,0xb3,0x72,0x51,0x17,0x98,0xe0,0xaf,0xdc,0x11,0x39,0x1e,0xe5,0x9e,0x23,0x95,0x86,0xc7,0xe9,0xb4,0x1b,0xad,0xfb,0x7e,0xcb,0xaa,0x4d,0x04,0x36,0x57,0x1e,0x40,0xf2,0x89,0xd2,0xc1,0xe4,0x30,0xbf,0x8c,0xc8,0x8e,0x92,0xc9,0xbd,0x85,0x82,0xbe,0xa7,0xa1,0x70,0xb1,0x02,0x20,0xfb,0x24,0x08,0xa4,0xf0,0x75,0xf3,0xb6,0xfe,0x54,0xf7,0x6a,0x2e,0x9c,0xb3,0xd9,0x3f,0xad,0xca,0x85,0xfb,0x70,0x51,0x4d,0x2a,0x28,0xd4,0xd7,0x6f,0xeb,0xf1,0x5c,0x98,0xec,0xe3,0x96,0x63,0xfe,0x1b,0xe2,0x3f,0xaa,0xee,0xaa,0x88,0x1d,0x21,0x00,0x00 7 | }; -------------------------------------------------------------------------------- /src/webserver.esp: -------------------------------------------------------------------------------- 1 | void ICACHE_FLASH_ATTR setupWebServer() { 2 | server.addHandler(&ws); 3 | ws.onEvent(onWsEvent); 4 | server.onNotFound([](AsyncWebServerRequest *request) { 5 | AsyncWebServerResponse *response = request->beginResponse(404, "text/plain", "Not found"); 6 | request->send(response); 7 | }); 8 | server.on("/update", HTTP_POST, [](AsyncWebServerRequest *request) { 9 | AsyncWebServerResponse * response = request->beginResponse(200, "text/plain", shouldReboot ? "OK" : "FAIL"); 10 | response->addHeader("Connection", "close"); 11 | request->send(response); 12 | }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { 13 | if (!request->authenticate(httpUsername, config.httpPass)) { 14 | return; 15 | } 16 | if (!index) { 17 | writeEvent("INFO", "updt", "Firmware update started", filename.c_str()); 18 | Update.runAsync(true); 19 | if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { 20 | writeEvent("ERRO", "updt", "Not enough space to update",""); 21 | #ifdef DEBUG 22 | Update.printError(Serial); 23 | #endif 24 | } 25 | } 26 | if (!Update.hasError()) { 27 | if (Update.write(data, len) != len) { 28 | writeEvent("ERRO", "updt", "Writing to flash is failed", filename.c_str()); 29 | #ifdef DEBUG 30 | Update.printError(Serial); 31 | #endif 32 | } 33 | } 34 | if (final) { 35 | if (Update.end(true)) { 36 | writeEvent("INFO", "updt", "Firmware update is finished", ""); 37 | #ifdef DEBUG 38 | Serial.printf("[ UPDT ] Firmware update finished: %uB\n", index + len); 39 | #endif 40 | shouldReboot = !Update.hasError(); 41 | } else { 42 | writeEvent("ERRO", "updt", "Update is failed", ""); 43 | #ifdef DEBUG 44 | Update.printError(Serial); 45 | #endif 46 | } 47 | } 48 | }); 49 | server.on("/fonts/glyphicons-halflings-regular.woff", HTTP_GET, [](AsyncWebServerRequest *request) { 50 | AsyncWebServerResponse *response = request->beginResponse_P(200, "font/woff", glyphicons_halflings_regular_woff_gz, glyphicons_halflings_regular_woff_gz_len); 51 | response->addHeader("Content-Encoding", "gzip"); 52 | request->send(response); 53 | }); 54 | server.on("/css/required.css", HTTP_GET, [](AsyncWebServerRequest *request) { 55 | AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", required_css_gz, required_css_gz_len); 56 | response->addHeader("Content-Encoding", "gzip"); 57 | request->send(response); 58 | }); 59 | server.on("/js/required.js", HTTP_GET, [](AsyncWebServerRequest *request) { 60 | AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", required_js_gz, required_js_gz_len); 61 | response->addHeader("Content-Encoding", "gzip"); 62 | request->send(response); 63 | }); 64 | server.on("/js/esprfid.js", HTTP_GET, [](AsyncWebServerRequest *request) { 65 | AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", esprfid_js_gz, esprfid_js_gz_len); 66 | response->addHeader("Content-Encoding", "gzip"); 67 | request->send(response); 68 | }); 69 | 70 | server.on("/index.html", HTTP_GET, [](AsyncWebServerRequest *request) { 71 | AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", index_html_gz, index_html_gz_len); 72 | response->addHeader("Content-Encoding", "gzip"); 73 | request->send(response); 74 | }); 75 | 76 | server.on("/esprfid.htm", HTTP_GET, [](AsyncWebServerRequest *request) { 77 | AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", esprfid_htm_gz, esprfid_htm_gz_len); 78 | response->addHeader("Content-Encoding", "gzip"); 79 | request->send(response); 80 | }); 81 | 82 | if (config.httpPass == NULL) { 83 | config.httpPass = strdup("admin"); 84 | } 85 | server.on("/login", HTTP_GET, [](AsyncWebServerRequest *request) { 86 | String remoteIP = printIP(request->client()->remoteIP()); 87 | if (!request->authenticate(httpUsername, config.httpPass)) { 88 | writeEvent("WARN", "websrv", "New login attempt", remoteIP); 89 | return request->requestAuthentication(); 90 | } 91 | request->send(200, "text/plain", "Success"); 92 | writeEvent("INFO", "websrv", "Login success!", remoteIP); 93 | }); 94 | server.rewrite("/", "/index.html"); 95 | server.begin(); 96 | } -------------------------------------------------------------------------------- /src/websocket.esp: -------------------------------------------------------------------------------- 1 | #define MAX_WS_BUFFER 2048 2 | 3 | char wsBuffer[MAX_WS_BUFFER]; 4 | 5 | struct WsMessage 6 | { 7 | char *serializedMessage; 8 | AsyncWebSocketClient *client; 9 | WsMessage *nextMessage = NULL; 10 | }; 11 | 12 | WsMessage *wsMessageQueue = NULL; 13 | 14 | // messageSize needs to be one char bigger than the string to contain the string terminator 15 | void addWsMessageToQueue(AsyncWebSocketClient *client, int messageSize) 16 | { 17 | WsMessage *incomingMessage = new WsMessage; 18 | incomingMessage->serializedMessage = (char *)malloc(messageSize); 19 | strlcpy(incomingMessage->serializedMessage, (const char *)client->_tempObject, messageSize); 20 | free(client->_tempObject); 21 | client->_tempObject = NULL; 22 | 23 | incomingMessage->client = client; 24 | 25 | WsMessage *lastMessage = wsMessageQueue; 26 | // process only one message at the time 27 | if (lastMessage == NULL) 28 | { 29 | wsMessageQueue = incomingMessage; 30 | } 31 | else 32 | { 33 | while (lastMessage->nextMessage != NULL) 34 | { 35 | lastMessage = lastMessage->nextMessage; 36 | } 37 | lastMessage->nextMessage = incomingMessage; 38 | } 39 | } 40 | 41 | void ICACHE_FLASH_ATTR processWsMessage(WsMessage *incomingMessage) 42 | { 43 | // We should always get a JSON object (stringfied) from browser, so parse it 44 | DynamicJsonDocument root(2448); 45 | AsyncWebSocketClient *client = incomingMessage->client; 46 | // cast to const char * to avoid in-place editing of serializedMessage 47 | auto error = deserializeJson(root, (const char *)incomingMessage->serializedMessage); 48 | 49 | if (error) 50 | { 51 | #ifdef DEBUG 52 | Serial.println(F("[ WARN ] Couldn't parse WebSocket message")); 53 | #endif 54 | free(incomingMessage->serializedMessage); 55 | free(incomingMessage); 56 | return; 57 | } 58 | // Web Browser sends some commands, check which command is given 59 | const char *command = root["command"]; 60 | // Check whatever the command is and act accordingly 61 | if (strcmp(command, "remove") == 0) 62 | { 63 | const char *uid = root["uid"]; 64 | String filename = "/P/"; 65 | filename += uid; 66 | SPIFFS.remove(filename); 67 | ws.textAll("{\"command\":\"result\",\"resultof\":\"remove\",\"result\": true}"); 68 | } 69 | else if (strcmp(command, "configfile") == 0) 70 | { 71 | File f = SPIFFS.open("/config.json", "w"); 72 | if (f) 73 | { 74 | size_t len = 0; 75 | while (incomingMessage->serializedMessage[len] != '\0' && len < MAX_WS_BUFFER) 76 | { 77 | len++; 78 | } 79 | f.write(incomingMessage->serializedMessage, len); 80 | f.close(); 81 | shouldReboot = true; 82 | writeEvent("INFO", "sys", "Config stored in the SPIFFS", String(len) + " bytes"); 83 | } 84 | } 85 | else if (strcmp(command, "userlist") == 0) 86 | { 87 | int page = root["page"]; 88 | sendUserList(page, incomingMessage->client); 89 | } 90 | else if (strcmp(command, "status") == 0) 91 | { 92 | sendStatus(client); 93 | } 94 | else if (strcmp(command, "restart") == 0) 95 | { 96 | shouldReboot = true; 97 | } 98 | else if (strcmp(command, "destroy") == 0) 99 | { 100 | formatreq = true; 101 | } 102 | else if (strcmp(command, "geteventlog") == 0) 103 | { 104 | int page = root["page"]; 105 | const char *xfileName = root["filename"]; 106 | sendEventLog(page, xfileName, client); 107 | } 108 | else if (strcmp(command, "getlatestlog") == 0) 109 | { 110 | int page = root["page"]; 111 | const char *xfileName = root["filename"]; 112 | sendLatestLog(page, xfileName, client); 113 | } 114 | else if (strcmp(command, "listfiles") == 0) 115 | { 116 | int page = root["page"]; 117 | sendFileList(page, client); 118 | } 119 | else if (strcmp(command, "logMaintenance") == 0) 120 | { 121 | logMaintenance(root["action"],root["filename"], client); 122 | } 123 | else if (strcmp(command, "clearevent") == 0) 124 | { 125 | SPIFFS.remove("/eventlog.json"); 126 | writeEvent("WARN", "sys", "Event log cleared!", ""); 127 | } 128 | else if (strcmp(command, "clearlatest") == 0) 129 | { 130 | SPIFFS.remove("/latestlog.json"); 131 | writeEvent("WARN", "sys", "Latest Access log cleared!", ""); 132 | } 133 | else if (strcmp(command, "userfile") == 0) 134 | { 135 | #ifdef DEBUG 136 | Serial.println(F("[ DEBUG ] userfile received")); 137 | serializeJson(root, Serial); 138 | #endif 139 | const char *uid = root["uid"]; 140 | String filename = "/P/"; 141 | filename += uid; 142 | File f = SPIFFS.open(filename, "w+"); 143 | // Check if we created the file 144 | if (f) 145 | { 146 | serializeJson(root, f); 147 | #ifdef DEBUG 148 | Serial.println(F("[ DEBUG ] userfile saved")); 149 | #endif 150 | } 151 | f.close(); 152 | client->text("{\"command\":\"result\",\"resultof\":\"userfile\",\"result\": true}"); 153 | } 154 | else if (strcmp(command, "testrelay1") == 0) 155 | { 156 | activateRelay[0] = true; 157 | previousMillis = millis(); 158 | client->text("{\"command\":\"giveAccess\"}"); 159 | } 160 | else if (strcmp(command, "testrelay2") == 0) 161 | { 162 | activateRelay[1] = true; 163 | previousMillis = millis(); 164 | client->text("{\"command\":\"giveAccess\"}"); 165 | } 166 | else if (strcmp(command, "testrelay3") == 0) 167 | { 168 | activateRelay[2] = true; 169 | previousMillis = millis(); 170 | client->text("{\"command\":\"giveAccess\"}"); 171 | } 172 | else if (strcmp(command, "testrelay4") == 0) 173 | { 174 | activateRelay[3] = true; 175 | previousMillis = millis(); 176 | client->text("{\"command\":\"giveAccess\"}"); 177 | } 178 | else if (strcmp(command, "scan") == 0) 179 | { 180 | WiFi.scanNetworksAsync(printScanResult, true); 181 | } 182 | else if (strcmp(command, "gettime") == 0) 183 | { 184 | sendTime(client); 185 | } 186 | else if (strcmp(command, "settime") == 0) 187 | { 188 | time_t t = root["epoch"]; 189 | epoch = t; 190 | lastNTPepoch = t; 191 | sendTime(client); 192 | } 193 | else if (strcmp(command, "getconf") == 0) 194 | { 195 | File configFile = SPIFFS.open("/config.json", "r"); 196 | if (configFile) 197 | { 198 | size_t len = configFile.size(); 199 | AsyncWebSocketMessageBuffer *buffer = ws.makeBuffer(len); // creates a buffer (len + 1) for you. 200 | if (buffer) 201 | { 202 | configFile.readBytes((char *)buffer->get(), len + 1); 203 | client->text(buffer); 204 | } 205 | configFile.close(); 206 | } 207 | } 208 | free(incomingMessage->serializedMessage); 209 | free(incomingMessage); 210 | yield(); 211 | } 212 | 213 | // Handles WebSocket Events 214 | void ICACHE_FLASH_ATTR onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) 215 | { 216 | if (type == WS_EVT_ERROR) 217 | { 218 | #ifdef DEBUG 219 | Serial.printf("[ WARN ] WebSocket[%s][%u] error(%u): %s\r\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data); 220 | #endif 221 | } 222 | else if (type == WS_EVT_DATA) 223 | { 224 | AwsFrameInfo *info = (AwsFrameInfo *)arg; 225 | if (info->final && info->index == 0 && info->len == len) 226 | { 227 | //the whole message is in a single frame and we got all of it's data 228 | client->_tempObject = malloc(info->len); 229 | memcpy((uint8_t *)(client->_tempObject), data, len); 230 | addWsMessageToQueue(client, info->len + 1); 231 | } 232 | else 233 | { 234 | //message is comprised of multiple frames or the frame is split into multiple packets 235 | if (info->index == 0) 236 | { 237 | if (info->num == 0 && client->_tempObject == NULL) 238 | { 239 | client->_tempObject = malloc(info->len); 240 | } 241 | } 242 | if (client->_tempObject != NULL) 243 | { 244 | memcpy((uint8_t *)(client->_tempObject) + info->index, data, len); 245 | } 246 | if ((info->index + len) == info->len) 247 | { 248 | if (info->final) 249 | { 250 | addWsMessageToQueue(client, info->len + 1); 251 | } 252 | } 253 | } 254 | } 255 | } 256 | 257 | void processWsQueue() 258 | { 259 | while (wsMessageQueue != NULL) 260 | { 261 | WsMessage *messageToProcess = wsMessageQueue; 262 | wsMessageQueue = messageToProcess->nextMessage; 263 | processWsMessage(messageToProcess); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css: -------------------------------------------------------------------------------- 1 | table.footable-details,table.footable>thead>tr.footable-filtering>th div.form-group{margin-bottom:0}table.footable,table.footable-details{position:relative;width:100%;border-spacing:0;border-collapse:collapse}table.footable-hide-fouc{display:none}table>tbody>tr>td>span.footable-toggle{margin-right:8px;opacity:.3}table>tbody>tr>td>span.footable-toggle.last-column{margin-left:8px;float:right}table.table-condensed>tbody>tr>td>span.footable-toggle{margin-right:5px}table.footable-details>tbody>tr>th:nth-child(1){min-width:40px;width:120px}table.footable-details>tbody>tr>td:nth-child(2){word-break:break-all}table.footable-details>tbody>tr:first-child>td,table.footable-details>tbody>tr:first-child>th,table.footable-details>tfoot>tr:first-child>td,table.footable-details>tfoot>tr:first-child>th,table.footable-details>thead>tr:first-child>td,table.footable-details>thead>tr:first-child>th{border-top-width:0}table.footable-details.table-bordered>tbody>tr:first-child>td,table.footable-details.table-bordered>tbody>tr:first-child>th,table.footable-details.table-bordered>tfoot>tr:first-child>td,table.footable-details.table-bordered>tfoot>tr:first-child>th,table.footable-details.table-bordered>thead>tr:first-child>td,table.footable-details.table-bordered>thead>tr:first-child>th{border-top-width:1px}div.footable-loader{vertical-align:middle;text-align:center;height:300px;position:relative}div.footable-loader>span.fooicon{display:inline-block;opacity:.3;font-size:30px;line-height:32px;width:32px;height:32px;margin-top:-16px;margin-left:-16px;position:absolute;top:50%;left:50%;-webkit-animation:fooicon-spin-r 2s infinite linear;animation:fooicon-spin-r 2s infinite linear}table.footable>tbody>tr.footable-empty>td{vertical-align:middle;text-align:center;font-size:30px}table.footable>tbody>tr>td,table.footable>tbody>tr>th{display:none}table.footable>tbody>tr.footable-detail-row>td,table.footable>tbody>tr.footable-detail-row>th,table.footable>tbody>tr.footable-empty>td,table.footable>tbody>tr.footable-empty>th{display:table-cell}@-webkit-keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fooicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings'!important;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fooicon:after,.fooicon:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.fooicon-loader:before{content:"\e030"}.fooicon-plus:before{content:"\2b"}.fooicon-minus:before{content:"\2212"}.fooicon-search:before{content:"\e003"}.fooicon-remove:before{content:"\e014"}.fooicon-sort:before{content:"\e150"}.fooicon-sort-asc:before{content:"\e155"}.fooicon-sort-desc:before{content:"\e156"}.fooicon-pencil:before{content:"\270f"}.fooicon-trash:before{content:"\e020"}.fooicon-eye-close:before{content:"\e106"}.fooicon-flash:before{content:"\e162"}.fooicon-cog:before{content:"\e019"}.fooicon-stats:before{content:"\e185"}table.footable>thead>tr.footable-filtering>th{border-bottom-width:1px;font-weight:400}.footable-filtering-external.footable-filtering-right,table.footable.footable-filtering-right>thead>tr.footable-filtering>th,table.footable>thead>tr.footable-filtering>th{text-align:right}.footable-filtering-external.footable-filtering-left,table.footable.footable-filtering-left>thead>tr.footable-filtering>th{text-align:left}.footable-filtering-external.footable-filtering-center,.footable-paging-external.footable-paging-center,table.footable-paging-center>tfoot>tr.footable-paging>td,table.footable.footable-filtering-center>thead>tr.footable-filtering>th,table.footable>tfoot>tr.footable-paging>td{text-align:center}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:5px}table.footable>thead>tr.footable-filtering>th div.input-group{width:100%}.footable-filtering-external ul.dropdown-menu>li>a.checkbox,table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox{margin:0;display:block;position:relative}.footable-filtering-external ul.dropdown-menu>li>a.checkbox>label,table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox>label{display:block;padding-left:20px}.footable-filtering-external ul.dropdown-menu>li>a.checkbox input[type=checkbox],table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox input[type=checkbox]{position:absolute;margin-left:-20px}@media (min-width:768px){table.footable>thead>tr.footable-filtering>th div.input-group{width:auto}table.footable>thead>tr.footable-filtering>th div.form-group{margin-left:2px;margin-right:2px}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:0}}table.footable>tbody>tr>td.footable-sortable,table.footable>tbody>tr>th.footable-sortable,table.footable>tfoot>tr>td.footable-sortable,table.footable>tfoot>tr>th.footable-sortable,table.footable>thead>tr>td.footable-sortable,table.footable>thead>tr>th.footable-sortable{position:relative;padding-right:30px;cursor:pointer}td.footable-sortable>span.fooicon,th.footable-sortable>span.fooicon{position:absolute;right:6px;top:50%;margin-top:-7px;opacity:0;transition:opacity .3s ease-in}td.footable-sortable.footable-asc>span.fooicon,td.footable-sortable.footable-desc>span.fooicon,td.footable-sortable:hover>span.fooicon,th.footable-sortable.footable-asc>span.fooicon,th.footable-sortable.footable-desc>span.fooicon,th.footable-sortable:hover>span.fooicon{opacity:1}table.footable-sorting-disabled td.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled td.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled td.footable-sortable:hover>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled th.footable-sortable:hover>span.fooicon{opacity:0;visibility:hidden}.footable-paging-external ul.pagination,table.footable>tfoot>tr.footable-paging>td>ul.pagination{margin:10px 0 0}.footable-paging-external span.label,table.footable>tfoot>tr.footable-paging>td>span.label{display:inline-block;margin:0 0 10px;padding:4px 10px}.footable-paging-external.footable-paging-left,table.footable-paging-left>tfoot>tr.footable-paging>td{text-align:left}.footable-paging-external.footable-paging-right,table.footable-editing-right td.footable-editing,table.footable-editing-right tr.footable-editing,table.footable-paging-right>tfoot>tr.footable-paging>td{text-align:right}ul.pagination>li.footable-page{display:none}ul.pagination>li.footable-page.visible{display:inline}td.footable-editing{width:90px;max-width:90px}table.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit td.footable-editing,table.footable-editing-no-view td.footable-editing{width:70px;max-width:70px}table.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit.footable-editing-no-view td.footable-editing{width:50px;max-width:50px}table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view th.footable-editing{width:0;max-width:0;display:none!important}table.footable-editing-left td.footable-editing,table.footable-editing-left tr.footable-editing{text-align:left}table.footable-editing button.footable-add,table.footable-editing button.footable-hide,table.footable-editing-show button.footable-show,table.footable-editing.footable-editing-always-show button.footable-hide,table.footable-editing.footable-editing-always-show button.footable-show,table.footable-editing.footable-editing-always-show.footable-editing-no-add tr.footable-editing{display:none}table.footable-editing.footable-editing-always-show button.footable-add,table.footable-editing.footable-editing-show button.footable-add,table.footable-editing.footable-editing-show button.footable-hide{display:inline-block} -------------------------------------------------------------------------------- /src/websrc/3rdparty/css/sidebar.css: -------------------------------------------------------------------------------- 1 | html {position: relative;overflow: scroll;overflow-x: hidden;min-height: 100% }::-webkit-scrollbar {width: 0px;background: transparent;}::-webkit-scrollbar-thumb {background: #e8e8e8;}body {background: #f1f3f6;margin-bottom: 60px }p {font-size: 1.1em;font-weight: 300;line-height: 1.7em;color: #999 }a, a:focus, a:hover {color: inherit;text-decoration: none;transition: all .3s }.navbar {padding: 15px 10px;background: #fff;border: none;border-radius: 0;margin-bottom: 40px;box-shadow: 1px 1px 3px rgba(0, 0, 0, .1) }#dismiss, #sidebar {background: #337ab7 }#content.navbar-btn {box-shadow: none;outline: 0;border: none }.line {width: 100%;height: 1px;border-bottom: 1px dashed #ddd;margin: 40px 0 }#sidebar {width: 250px;position: fixed;top: 0;left: -250px;height: 100vh;z-index: 999;color: #fff;transition: all .3s;overflow-y: auto;box-shadow: 3px 3px 3px rgba(0, 0, 0, .2) }@media screen and (min-width:768px) {#sidebar {left: 0 }.footer {margin-left: 250px }#ajaxcontent {margin-left: 250px }#dismiss, .navbar-btn {display: none }}#sidebar.active {left: 0 }#dismiss {width: 35px;height: 35px;line-height: 35px;text-align: center;position: absolute;top: 10px;right: 10px;cursor: pointer;-webkit-transition: all .3s;-o-transition: all .3s;transition: all .3s }#dismiss:hover {background: #fff;color: #337ab7 }.overlay {position: fixed;width: 100vw;height: 100vh;background: rgba(0, 0, 0, .7);z-index: 998;display: none }#sidebar .sidebar-header {padding: 20px;background: #337ab7 }#sidebar ul.components {padding: 20px 0;border-bottom: 1px solid #47748b }#content, ul.CTAs {padding: 20px }#sidebar ul p {color: #fff;padding: 10px }#sidebar ul li a {padding: 10px;font-size: 1.1em;display: block }#sidebar ul li a:hover {color: #337ab7;background: #fff }#sidebar ul li.active>a, a[aria-expanded=true] {color: #fff;background: #2e6da4 }a[data-toggle=collapse] {position: relative }a[aria-expanded=false]::before, a[aria-expanded=true]::before {content: '\e259';display: block;position: absolute;right: 20px;font-family: 'Glyphicons Halflings';font-size: .6em }#sidebar ul ul a, ul.CTAs a {font-size: .9em }a[aria-expanded=true]::before {content: '\e260' }#sidebar ul ul a {padding-left: 30px;background: #2e6da4 }ul.CTAs a {text-align: center;display: block;border-radius: 5px;margin-bottom: 5px }a.download {background: #fff;color: #337ab7 }#sidebar a.article, a.article:hover {background: #2e6da4;color: #fff }#content {width: 100%;min-height: 100vh;transition: all .3s;position: absolute;top: 0;right: 0 }.footer {position: fixed;bottom: 0;width: 100%;height: 45px;background-color: #f1f3f6 }i {margin-right: 1em } -------------------------------------------------------------------------------- /src/websrc/3rdparty/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esprfid/esp-rfid/a7b43a7c0e57fcb253237b376fb2169e626cbb78/src/websrc/3rdparty/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/websrc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | esp-rfid 12 | 13 | 14 | > 25 | 26 | 27 | 28 |
29 | 30 | 106 | 107 |
108 | 112 |
113 | 117 |
118 |
119 |
120 | 138 | 170 | 198 | 247 |
248 |
249 |
250 |
esp-rfid - This is a free software.
251 |
252 |
253 |
254 | 255 | 256 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /src/wifi.esp: -------------------------------------------------------------------------------- 1 | void setEnableWifi() 2 | { 3 | doEnableWifi = true; 4 | } 5 | 6 | void onWifiConnect(const WiFiEventStationModeConnected &event) 7 | { 8 | #ifdef DEBUG 9 | Serial.println(F("[ INFO ] WiFi STA Connected")); 10 | #endif 11 | mqttReconnectTimer.detach(); 12 | if (!wifiReconnectTimer.active() && !config.fallbackMode) 13 | { 14 | wifiReconnectTimer.once(300, setEnableWifi); 15 | } 16 | ledWifiOff(); 17 | } 18 | 19 | void onWifiDisconnect(const WiFiEventStationModeDisconnected &event) 20 | { 21 | if ( !WiFi.isConnected() ) 22 | { 23 | return; 24 | } 25 | #ifdef DEBUG 26 | Serial.println(F("[ INFO ] WiFi STA Disconnected")); 27 | #endif 28 | mqttReconnectTimer.detach(); 29 | disconnectMqtt(); 30 | if (!wifiReconnectTimer.active() && !config.fallbackMode) 31 | { 32 | wifiReconnectTimer.once(300, setEnableWifi); 33 | } 34 | ledWifiOff(); 35 | } 36 | 37 | void onWifiGotIP(const WiFiEventStationModeGotIP &event) 38 | { 39 | #ifdef DEBUG 40 | Serial.print("[ INFO ] WiFi IP Connected: "); 41 | Serial.println(WiFi.localIP().toString()); 42 | #endif 43 | wifiReconnectTimer.detach(); 44 | ledWifiOn(); 45 | connectToMqtt(); 46 | } 47 | 48 | bool ICACHE_FLASH_ATTR startAP(IPAddress apip, IPAddress apsubnet, bool hidden, const char *ssid, const char *password = NULL) 49 | { 50 | #ifdef DEBUG 51 | Serial.println(F("[ INFO ] ESP-RFID is running in AP Mode ")); 52 | #endif 53 | WiFi.mode(WIFI_AP); 54 | #ifdef DEBUG 55 | Serial.print(F("[ INFO ] Configuring access point... ")); 56 | #endif 57 | 58 | WiFi.softAPConfig(apip, apip, apsubnet); 59 | 60 | bool success; 61 | if (hidden) 62 | { 63 | success = WiFi.softAP(ssid, password, 3, true); 64 | } 65 | else 66 | { 67 | success = WiFi.softAP(ssid, password); 68 | } 69 | #ifdef DEBUG 70 | Serial.println(success ? F("Ready") : F("Failed!")); 71 | #endif 72 | 73 | if (success) 74 | { 75 | ledWifiOn(); 76 | } 77 | 78 | #ifdef DEBUG 79 | IPAddress myIP = WiFi.softAPIP(); 80 | 81 | Serial.print(F("[ INFO ] AP IP address: ")); 82 | Serial.println(myIP); 83 | Serial.printf("[ INFO ] AP SSID: %s\n", ssid); 84 | #endif 85 | return success; 86 | } 87 | 88 | // Fallback to AP Mode, so we can connect to ESP if there is no Internet connection 89 | void ICACHE_FLASH_ATTR fallbacktoAPMode() 90 | { 91 | config.accessPointMode = true; 92 | #ifdef DEBUG 93 | Serial.println(F("[ INFO ] ESP-RFID is running in Fallback AP Mode")); 94 | #endif 95 | WiFi.mode(WIFI_AP); 96 | uint8_t macAddr[6]; 97 | WiFi.softAPmacAddress(macAddr); 98 | char ssid[15]; 99 | sprintf(ssid, "ESP-RFID-%02x%02x%02x", macAddr[3], macAddr[4], macAddr[5]); 100 | if (WiFi.softAP(ssid)) 101 | { 102 | ledWifiOn(); 103 | #ifdef DEBUG 104 | IPAddress myIP = WiFi.softAPIP(); 105 | 106 | Serial.print(F("[ INFO ] AP IP address: ")); 107 | Serial.println(myIP); 108 | Serial.printf("[ INFO ] AP SSID: %s\n", ssid); 109 | #endif 110 | } 111 | } 112 | 113 | // Try to connect Wi-Fi 114 | bool ICACHE_FLASH_ATTR connectSTA(const char *ssid, const char *password, byte bssid[6]) 115 | { 116 | bool useBSSID = false; 117 | WiFi.mode(WIFI_STA); 118 | WiFi.persistent(false); 119 | 120 | if (!config.dhcpEnabled) 121 | { 122 | WiFi.config(config.ipAddress, config.gatewayIp, config.subnetIp, config.dnsIp); 123 | } 124 | #ifdef DEBUG 125 | Serial.print(F("[ INFO ] Trying to connect WiFi: ")); 126 | Serial.println(ssid); 127 | Serial.print(F("[ INFO ] WiFi BSSID: ")); 128 | #endif 129 | for (int i = 0; i < 6; i++) 130 | { 131 | #ifdef DEBUG 132 | Serial.print(bssid[i]); 133 | if (i < 5) 134 | Serial.print(F(":")); 135 | else 136 | Serial.println(); 137 | #endif 138 | if (bssid[i] != 0) 139 | useBSSID = true; 140 | } 141 | if (useBSSID) 142 | { 143 | #ifdef DEBUG 144 | Serial.println(F("[ INFO ] BSSID locked")); 145 | #endif 146 | WiFi.begin(ssid, password, 0, bssid); 147 | } 148 | else 149 | { 150 | #ifdef DEBUG 151 | Serial.println(F("[ INFO ] any BSSID")); 152 | #endif 153 | WiFi.begin(ssid, password); 154 | } 155 | unsigned long now = millis(); 156 | uint8_t timeout = 15; // define when to time out in seconds 157 | do 158 | { 159 | ledWifiStatus(); 160 | delay(500); 161 | #ifdef DEBUG 162 | if (!WiFi.isConnected()) 163 | Serial.print(F(".")); 164 | #endif 165 | if (WiFi.isConnected()) 166 | break; 167 | } while (millis() - now < timeout * 1000); 168 | 169 | // We now out of the while loop, either time is out or we connected. check what happened 170 | if (WiFi.isConnected()) 171 | { 172 | String data = ssid; 173 | data += " " + WiFi.localIP().toString(); 174 | writeEvent("INFO", "wifi", "WiFi is connected", data); 175 | return true; 176 | } 177 | else 178 | { 179 | #ifdef DEBUG 180 | Serial.println(); 181 | Serial.println(F("[ WARN ] Couldn't connect in time")); 182 | #endif 183 | if (!config.fallbackMode) 184 | { 185 | #ifdef DEBUG 186 | Serial.println(); 187 | Serial.println(F("[ INFO ] trying to reconnect to WiFi")); 188 | #endif 189 | wifiReconnectTimer.once(300, setEnableWifi); 190 | } 191 | return false; 192 | } 193 | } 194 | 195 | void ICACHE_FLASH_ATTR disableWifi() 196 | { 197 | wiFiUptimeMillis = 0; 198 | WiFi.disconnect(true); 199 | WiFi.softAPdisconnect(true); 200 | #ifdef DEBUG 201 | Serial.println(F("Turn wifi off.")); 202 | #endif 203 | } 204 | 205 | void ICACHE_FLASH_ATTR enableWifi() 206 | { 207 | wiFiUptimeMillis = 0; 208 | if (config.accessPointMode) 209 | { 210 | startAP(config.accessPointIp, config.accessPointSubnetIp, config.networkHidden, config.ssid, config.wifiPassword); 211 | } 212 | else 213 | { 214 | bool connected = connectSTA(config.ssid, config.wifiPassword, config.bssid); 215 | if (!connected && config.fallbackMode) 216 | { 217 | fallbacktoAPMode(); 218 | } 219 | } 220 | } 221 | 222 | void setupWifi(bool configured) 223 | { 224 | if (!configured) 225 | { 226 | WiFi.hostname("esp-rfid"); 227 | fallbacktoAPMode(); 228 | } else 229 | { 230 | wifiConnectHandler = WiFi.onStationModeConnected(onWifiConnect); 231 | wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWifiDisconnect); 232 | wifiOnStationModeGotIPHandler = WiFi.onStationModeGotIP(onWifiGotIP); 233 | WiFi.hostname(config.deviceHostname); 234 | enableWifi(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/wsResponses.esp: -------------------------------------------------------------------------------- 1 | void ICACHE_FLASH_ATTR sendUserList(int page, AsyncWebSocketClient *client) 2 | { 3 | DynamicJsonDocument root(2048); 4 | root["command"] = "userlist"; 5 | root["page"] = page; 6 | JsonArray users = root.createNestedArray("list"); 7 | Dir dir = SPIFFS.openDir("/P/"); 8 | int first = (page - 1) * 10; 9 | int last = page * 10; 10 | int i = 0; 11 | while (dir.next()) 12 | { 13 | if (i >= first && i < last) 14 | { 15 | JsonObject item = users.createNestedObject(); 16 | String uid = dir.fileName(); 17 | uid.remove(0, 3); 18 | item["uid"] = uid; 19 | File f = SPIFFS.open(dir.fileName(), "r"); 20 | size_t size = f.size(); 21 | std::unique_ptr buf(new char[size]); 22 | f.readBytes(buf.get(), size); 23 | DynamicJsonDocument json(512); 24 | auto error = deserializeJson(json, buf.get(), size); 25 | if (!error) 26 | { 27 | String username = json["user"]; 28 | String pincode = ""; 29 | if(json.containsKey("pincode")) 30 | { 31 | pincode = String((const char *)json["pincode"]); 32 | } 33 | for (int x = 1; x <= MAX_NUM_RELAYS; x++) 34 | { 35 | String theKey = String(); 36 | if (x == 1) 37 | theKey = "acctype"; 38 | else 39 | theKey = "acctype" + String(x); 40 | int AccType = json[theKey]; 41 | item[theKey] = AccType; 42 | } 43 | unsigned long validsince = json.containsKey("validsince") ? json["validsince"] : 0; 44 | unsigned long validuntil = json["validuntil"]; 45 | item["username"] = username; 46 | item["validsince"] = validsince; 47 | item["validuntil"] = validuntil; 48 | item["pincode"] = pincode; 49 | } 50 | } 51 | i++; 52 | yield(); 53 | } 54 | float pages = i / 10.0; 55 | root["haspages"] = ceil(pages); 56 | size_t len = measureJson(root); 57 | AsyncWebSocketMessageBuffer *buffer = ws.makeBuffer(len); 58 | if (buffer) 59 | { 60 | serializeJson(root, (char *)buffer->get(), len + 1); 61 | if (client) 62 | { 63 | wsMessageTicker.once_ms_scheduled(50, [client, buffer]() { 64 | client->text(buffer); 65 | client->text("{\"command\":\"result\",\"resultof\":\"userlist\",\"result\": true}"); 66 | }); 67 | } 68 | else 69 | { 70 | ws.textAll("{\"command\":\"result\",\"resultof\":\"userlist\",\"result\": false}"); 71 | } 72 | } 73 | } 74 | 75 | void ICACHE_FLASH_ATTR sendStatus(AsyncWebSocketClient *client) 76 | { 77 | struct ip_info info; 78 | FSInfo fsinfo; 79 | if (!SPIFFS.info(fsinfo)) 80 | { 81 | #ifdef DEBUG 82 | Serial.print(F("[ WARN ] Error getting info on SPIFFS")); 83 | #endif 84 | } 85 | DynamicJsonDocument root(512); 86 | root["command"] = "status"; 87 | root["heap"] = ESP.getFreeHeap(); 88 | root["chipid"] = String(ESP.getChipId(), HEX); 89 | root["cpu"] = ESP.getCpuFreqMHz(); 90 | root["sketchsize"] = ESP.getSketchSize(); 91 | root["availsize"] = ESP.getFreeSketchSpace(); 92 | root["availspiffs"] = fsinfo.totalBytes - fsinfo.usedBytes; 93 | root["spiffssize"] = fsinfo.totalBytes; 94 | 95 | int h = uptimeSeconds / 3600; 96 | int rem = uptimeSeconds % 3600; 97 | int m = rem / 60; 98 | int s = rem % 60; 99 | char uptimebuffer[9]; 100 | sprintf(uptimebuffer, "%02d:%02d:%02d", h, m, s); 101 | root["uptime"] = uptimebuffer; 102 | 103 | root["version"] = VERSION; 104 | root["hostname"] = WiFi.hostname(); 105 | 106 | if (config.accessPointMode) 107 | { 108 | wifi_get_ip_info(SOFTAP_IF, &info); 109 | struct softap_config conf; 110 | wifi_softap_get_config(&conf); 111 | root["ssid"] = String(reinterpret_cast(conf.ssid)); 112 | root["dns"] = printIP(WiFi.softAPIP()); 113 | root["mac"] = WiFi.softAPmacAddress(); 114 | } 115 | else 116 | { 117 | wifi_get_ip_info(STATION_IF, &info); 118 | struct station_config conf; 119 | wifi_station_get_config(&conf); 120 | root["ssid"] = String(reinterpret_cast(conf.ssid)); 121 | root["dns"] = printIP(WiFi.dnsIP()); 122 | root["mac"] = WiFi.macAddress(); 123 | } 124 | 125 | IPAddress ipaddr = IPAddress(info.ip.addr); 126 | IPAddress gwaddr = IPAddress(info.gw.addr); 127 | IPAddress nmaddr = IPAddress(info.netmask.addr); 128 | root["ip"] = printIP(ipaddr); 129 | root["gateway"] = printIP(gwaddr); 130 | root["netmask"] = printIP(nmaddr); 131 | 132 | size_t len = measureJson(root); 133 | AsyncWebSocketMessageBuffer *buffer = ws.makeBuffer(len); 134 | if (buffer) 135 | { 136 | serializeJson(root, (char *)buffer->get(), len + 1); 137 | if (client) 138 | { 139 | client->text(buffer); 140 | } 141 | else 142 | { 143 | ws.textAll(buffer); 144 | } 145 | } 146 | } 147 | 148 | void ICACHE_FLASH_ATTR printScanResult(int networksFound) 149 | { 150 | // sort by RSSI 151 | int n = networksFound; 152 | int indices[n]; 153 | int skip[n]; 154 | for (int i = 0; i < networksFound; i++) 155 | { 156 | indices[i] = i; 157 | } 158 | for (int i = 0; i < networksFound; i++) 159 | { 160 | for (int j = i + 1; j < networksFound; j++) 161 | { 162 | if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) 163 | { 164 | std::swap(indices[i], indices[j]); 165 | std::swap(skip[i], skip[j]); 166 | } 167 | } 168 | } 169 | DynamicJsonDocument root(512); 170 | root["command"] = "ssidlist"; 171 | JsonArray scan = root.createNestedArray("list"); 172 | for (int i = 0; i < 5 && i < networksFound; ++i) 173 | { 174 | JsonObject item = scan.createNestedObject(); 175 | item["ssid"] = WiFi.SSID(indices[i]); 176 | item["bssid"] = WiFi.BSSIDstr(indices[i]); 177 | item["rssi"] = WiFi.RSSI(indices[i]); 178 | item["channel"] = WiFi.channel(indices[i]); 179 | item["enctype"] = WiFi.encryptionType(indices[i]); 180 | item["hidden"] = WiFi.isHidden(indices[i]) ? true : false; 181 | } 182 | size_t len = measureJson(root); 183 | AsyncWebSocketMessageBuffer *buffer = ws.makeBuffer(len); // creates a buffer (len + 1) for you. 184 | if (buffer) 185 | { 186 | serializeJson(root, (char *)buffer->get(), len + 1); 187 | ws.textAll(buffer); 188 | } 189 | WiFi.scanDelete(); 190 | } 191 | 192 | void ICACHE_FLASH_ATTR sendTime(AsyncWebSocketClient *client) 193 | { 194 | DynamicJsonDocument root(512); 195 | root["command"] = "gettime"; 196 | root["epoch"] = epoch; 197 | size_t len = measureJson(root); 198 | AsyncWebSocketMessageBuffer *buffer = ws.makeBuffer(len); 199 | if (buffer) 200 | { 201 | serializeJson(root, (char *)buffer->get(), len + 1); 202 | if (client) 203 | { 204 | client->text(buffer); 205 | } 206 | else 207 | { 208 | ws.textAll(buffer); 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | Both tools can be found already built in the Github releases, if you don't want to install all the needed dependencies. 4 | 5 | If you want to modify the tools instead you need to install all the Node dependencies and hack your way around. 6 | 7 | On every push the binaries are generated by Github actions and if needed a new release should be published with the binaries. 8 | 9 | ## npm usage 10 | 11 | The web files located in src/websrc/ will be minified, combined (css, js), gzipped, converted to byte arrays, and these are placed in src/webh/ when the builder is run. 12 | To run the builder script with npm do as follows: 13 | 14 | 1. change directory into the webfilesbuilder folder 15 | 2. execute `npm install` in the webfilesbuilder folder to install the npm dependancies (listed in package.json) 16 | 3. execute `npm start` to run the web files builder script (gulpfile.js) 17 | 4. now you can compile the firmware using PlatformIO 18 | 19 | ## webfilesbuilder 20 | 21 | The `webfilesbuilder` executable that you can get from the releases should be copied in the `tools/webfilesbuilder/` folder and run from there as it looks for files in specific folders relative to that location. -------------------------------------------------------------------------------- /tools/webfilesbuilder/bin.js: -------------------------------------------------------------------------------- 1 | const builder = require('./gulpfile.js').default; 2 | 3 | builder(); -------------------------------------------------------------------------------- /tools/webfilesbuilder/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const fs = require('fs-extra'); 3 | const zlib = require('zlib'); 4 | const concat = require('gulp-concat'); 5 | const path = require('path'); 6 | const htmlmin = require('gulp-htmlmin'); 7 | const uglify = require('gulp-uglify'); 8 | const websrc = '../../src/websrc/'; 9 | const webh = `../../src/webh/`; 10 | const tpDir = `${websrc}3rdparty/`; 11 | const prDir = `${websrc}process/`; 12 | const fnDir = `${prDir}final/`; 13 | const gzDir = `${prDir}gzip/`; 14 | const directories = [ 15 | websrc, 16 | webh, 17 | tpDir, 18 | prDir, 19 | fnDir, 20 | gzDir 21 | ]; 22 | const mn = 'required'; 23 | 24 | // Recreate output directories (destructive) 25 | const ensureDirs = (cb) => { 26 | console.log('RECREAT OUTPUT DIRECTORIES (destructive of: webh & process)'); 27 | fs.removeSync(webh); 28 | fs.removeSync(prDir); 29 | directories.forEach(dir => { 30 | fs.ensureDirSync(dir); 31 | }); 32 | cb(); 33 | }; 34 | 35 | // Minify JavaScript files 36 | function minify(srcFolder, destFolder) { 37 | return gulp.src(`${srcFolder}*.js`).pipe(uglify()).pipe(gulp.dest(destFolder)); 38 | } 39 | 40 | // Concat files into a single file. 41 | function merge(srcFolder, fileType, destDir, outputFile) { 42 | return gulp.src(`${srcFolder}*.${fileType}`).pipe(concat({ 43 | path: outputFile, 44 | stat: { 45 | mode: 0o666 46 | } 47 | })).pipe(gulp.dest(destDir)); 48 | } 49 | 50 | // Gzip files 51 | function gzip(srcFile, destFile, cb) { 52 | const input = fs.readFileSync(srcFile); 53 | const output = zlib.gzipSync(input); 54 | fs.writeFileSync(destFile, output); 55 | cb(); 56 | } 57 | 58 | // Create byte arrays of gzipped files 59 | function byteArray(source, destination, name, cb) { 60 | const arrayName = name.replace(/\.|-/g, "_"); 61 | const data = fs.readFileSync(source); 62 | const wstream = fs.createWriteStream(destination); 63 | wstream.write(`#define ${arrayName}_len ${data.length}\n`); 64 | wstream.write(`const uint8_t ${arrayName}[] PROGMEM = {`); 65 | for (let i = 0; i < data.length; i++) { 66 | if (i % 1000 === 0) 67 | wstream.write('\n'); 68 | wstream.write('0x' + data[i].toString(16).padStart(2, '0') + (i < data.length - 1 ? ',' : '')); 69 | } 70 | wstream.write('\n};'); 71 | wstream.end(cb); 72 | } 73 | 74 | // Task: Process assets (scripts, UI scripts, HTML, styles, fonts) 75 | const process = (cb) => { 76 | console.log('PROCESS UI & 3RDPARTY FILES TO FINAL: minify, merge, copy'); 77 | const mergeJS = () => merge(`${tpDir}js/`, 'js', `${fnDir}`, `${mn}.js`); 78 | const mergeCSS = () => merge(`${tpDir}css/`, 'css', `${fnDir}`, `${mn}.css`); 79 | const minifyUIjs = () => minify(`${websrc}js/`, `${fnDir}`); 80 | const minifyUIhtml = () => gulp.src(`${websrc}*.htm*`).pipe(htmlmin({ collapseWhitespace: true, minifyJS: true }).on('error', console.error)).pipe(gulp.dest(`${fnDir}`)); 81 | const copyFonts = (cb) => { fs.copy(`${tpDir}fonts/`, `${fnDir}`, cb); }; 82 | gulp.parallel(mergeJS, minifyUIjs, minifyUIhtml, mergeCSS, copyFonts)(cb); 83 | }; 84 | 85 | // Task: Gzip files 86 | function gzipAll(cb) { 87 | console.log('GZIP FROM FINAL'); 88 | const files = fs.readdirSync(fnDir); 89 | const tasks = files.map((file) => { 90 | const srcFile = path.join(fnDir, file); 91 | const destFile = path.join(gzDir, file + '.gz'); 92 | const taskName = (done) => gzip(srcFile, destFile, done); 93 | Object.defineProperty(taskName, 'name', { value: `${file}.gz` }); 94 | return taskName; 95 | }); 96 | gulp.parallel(...tasks)(cb); 97 | } 98 | 99 | // Task: Create byte arrays from gzipped files 100 | function byteArrayAll(cb) { 101 | console.log('BYTE ARRAY FROM GZIP'); 102 | const files = fs.readdirSync(gzDir); 103 | const tasks = files.map(file => { 104 | const srcFile = path.join(gzDir, file); 105 | const destFile = `${webh}${file}.h`; 106 | const taskName = (done) => byteArray(srcFile, destFile, file, done); 107 | Object.defineProperty(taskName, 'name', { value: `${file}.h` }); 108 | return taskName; 109 | }); 110 | gulp.parallel(...tasks)(cb); 111 | } 112 | 113 | // Main runner function 114 | function runner(cb) { 115 | gulp.series(ensureDirs, process, gzipAll, byteArrayAll)(cb); 116 | } 117 | exports.default = runner; -------------------------------------------------------------------------------- /tools/webfilesbuilder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webfilesbuilder", 3 | "version": "1.0.0", 4 | "description": "Combine all js and css files into one and gzip them for the ESP-RFID project", 5 | "scripts": { 6 | "start": "gulp" 7 | }, 8 | "author": "ESP-RFID Developers", 9 | "license": "UNLICENSED", 10 | "dependencies": { 11 | "fs-extra": "^11.3.0", 12 | "gulp": "^5.0.0", 13 | "gulp-concat": "^2.6.1", 14 | "gulp-flatmap": "^1.0.2", 15 | "gulp-htmlmin": "^5.0.1", 16 | "gulp-uglify": "^3.0.2" 17 | }, 18 | "bin": "bin.js" 19 | } 20 | -------------------------------------------------------------------------------- /tools/wsemulator/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wsemulator", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "wsemulator", 9 | "version": "1.0.0", 10 | "license": "UNLICENSED", 11 | "dependencies": { 12 | "websocket": "^1.0.28", 13 | "ws": "^4.0.0" 14 | }, 15 | "bin": { 16 | "wsemulator": "wserver.js" 17 | } 18 | }, 19 | "node_modules/async-limiter": { 20 | "version": "1.0.0", 21 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 22 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 23 | }, 24 | "node_modules/debug": { 25 | "version": "2.6.9", 26 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 27 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 28 | "dependencies": { 29 | "ms": "2.0.0" 30 | } 31 | }, 32 | "node_modules/is-typedarray": { 33 | "version": "1.0.0", 34 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 35 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 36 | }, 37 | "node_modules/ms": { 38 | "version": "2.0.0", 39 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 40 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 41 | }, 42 | "node_modules/nan": { 43 | "version": "2.12.1", 44 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", 45 | "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==" 46 | }, 47 | "node_modules/safe-buffer": { 48 | "version": "5.1.1", 49 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 50 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 51 | }, 52 | "node_modules/typedarray-to-buffer": { 53 | "version": "3.1.5", 54 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 55 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 56 | "dependencies": { 57 | "is-typedarray": "^1.0.0" 58 | } 59 | }, 60 | "node_modules/websocket": { 61 | "version": "1.0.28", 62 | "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.28.tgz", 63 | "integrity": "sha512-00y/20/80P7H4bCYkzuuvvfDvh+dgtXi5kzDf3UcZwN6boTYaKvsrtZ5lIYm1Gsg48siMErd9M4zjSYfYFHTrA==", 64 | "hasInstallScript": true, 65 | "dependencies": { 66 | "debug": "^2.2.0", 67 | "nan": "^2.11.0", 68 | "typedarray-to-buffer": "^3.1.5", 69 | "yaeti": "^0.0.6" 70 | }, 71 | "engines": { 72 | "node": ">=0.10.0" 73 | } 74 | }, 75 | "node_modules/ws": { 76 | "version": "4.1.0", 77 | "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", 78 | "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", 79 | "dependencies": { 80 | "async-limiter": "~1.0.0", 81 | "safe-buffer": "~5.1.0" 82 | } 83 | }, 84 | "node_modules/yaeti": { 85 | "version": "0.0.6", 86 | "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", 87 | "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=", 88 | "engines": { 89 | "node": ">=0.10.32" 90 | } 91 | } 92 | }, 93 | "dependencies": { 94 | "async-limiter": { 95 | "version": "1.0.0", 96 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 97 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 98 | }, 99 | "debug": { 100 | "version": "2.6.9", 101 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 102 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 103 | "requires": { 104 | "ms": "2.0.0" 105 | } 106 | }, 107 | "is-typedarray": { 108 | "version": "1.0.0", 109 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 110 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 111 | }, 112 | "ms": { 113 | "version": "2.0.0", 114 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 115 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 116 | }, 117 | "nan": { 118 | "version": "2.12.1", 119 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", 120 | "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==" 121 | }, 122 | "safe-buffer": { 123 | "version": "5.1.1", 124 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 125 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 126 | }, 127 | "typedarray-to-buffer": { 128 | "version": "3.1.5", 129 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 130 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 131 | "requires": { 132 | "is-typedarray": "^1.0.0" 133 | } 134 | }, 135 | "websocket": { 136 | "version": "1.0.28", 137 | "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.28.tgz", 138 | "integrity": "sha512-00y/20/80P7H4bCYkzuuvvfDvh+dgtXi5kzDf3UcZwN6boTYaKvsrtZ5lIYm1Gsg48siMErd9M4zjSYfYFHTrA==", 139 | "requires": { 140 | "debug": "^2.2.0", 141 | "nan": "^2.11.0", 142 | "typedarray-to-buffer": "^3.1.5", 143 | "yaeti": "^0.0.6" 144 | } 145 | }, 146 | "ws": { 147 | "version": "4.1.0", 148 | "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", 149 | "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", 150 | "requires": { 151 | "async-limiter": "~1.0.0", 152 | "safe-buffer": "~5.1.0" 153 | } 154 | }, 155 | "yaeti": { 156 | "version": "0.0.6", 157 | "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", 158 | "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tools/wsemulator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wsemulator", 3 | "version": "1.0.0", 4 | "description": "Emulate websocket communication ", 5 | "main": "wserver.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "esp-rfid developers", 10 | "license": "UNLICENSED", 11 | "dependencies": { 12 | "websocket": "^1.0.28", 13 | "ws": "^4.0.0" 14 | }, 15 | "bin": "wserver.js" 16 | } 17 | --------------------------------------------------------------------------------