Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NFC Lock Chars & Services and WR functionality #801

Open
wants to merge 19 commits into
base: dev
Choose a base branch
from

Conversation

rednblkx
Copy link

@rednblkx rednblkx commented Mar 6, 2024

Hi there!

Love the project and i'm currently using HomeSpan to build this which implements Apple's HomeKey into a ESP32 powered HomeKit lock.

In achieving the above i've had to make some modifications to HomeSpan which include the following:

  • Add the necessary characteristics and services of an NFC Lock
  • Implement callback functionality as the new NFCAccessControlPoint characteristic expects a response to each request that is received for this characteristic
  • Call the user-defined pairCallback fn everytime a controller is added in order to craft the list of "issuers" for the HomeKey

I have also added some delays as a quick fix for a "task watchdog triggered" issue that occurs when using ESP-IDF built from source and not the version builtin with Arduino-ESP32.

Tried setting the EV permission for the NFCAccessControlPoint char in order to set it's value in hoping it would work as a response and the value would get set but it wouldn't send it, if i return true from update() it would send just 204 No Content and if i return false it would send 207 Multi-Status(which we need for WR) but the data would lack the value property and the status would be -70402 instead of 0 as i see that is tied to the return value here

I've no idea if this fits in with the project but thought i should still try to open a PR, i know the current code for the callback is at best a dirty hack but i'm willing to help integrate it or modify the set value functions for characteristics to accommodate write response.

Looking forward to hear your feedback.

@HomeSpan
Copy link
Owner

HomeSpan commented Mar 9, 2024

This looks like a really cool project - thanks for the PR. I've reviewed the changes as well as the info on all the links referencing HomeKey that you included. A few initial thoughts:

  • The changes you are making are only to extend HomeSpan's core functionality (such as additional Characteristics and types of Characteristics), rather than trying to add NFC and HomeKey code directly to HomeSpan. That is 100% the right approach - much appreciated.

  • At present, HomeSpan does not implement the WR permission or the TLV8 format for Characteristics.

    • I did not add TLV8 as a format originally since I had no intention of supporting audio/video, but in HomeSpan 1.9.0 I refactored all the TLV8 code used for pairing so that it can be potentially used for Characteristics as well. My goal was to add Lock Management to implement log files, which require some TLV8 Characteristics. But, somewhat ironically, Apple has recently incorporated automatic log files directly into the Home App for Locks, Garage Doors, etc., without the need to implement any log file Characteristics. Hence, I no longer had a good reason to implement the TLV8 format type. It seems you are parsing TLV8 directly, but I can revisit and add it in as a fully supported mechanism so that you can read and write TLV8 data without needing to parse the base-64 code or the TLV format. Since you are already doing this, I'll consider adding TLV8 as a lower priority.
    • I did not implement the Write Response (WR) permission since none of the HAP Characteristics seem to require it, and I would not have a way of testing my implementation to see if it works as expected. HAP documentation is generally terrible and the description of the write-response protocol is no exception. As usual, the only away figure out what HAP really wants is by trial and error. However, I think I get the gist and it seems that most of your changes are designed to force a multi-response to a write command. I believe there is an easier way to do this (instead of using a callback) that capitalizes on the existing multi-response mechanism used for writes when there is an error. I'll give some more thought and try to incorporate directly.
  • For the pairCallback(), it looks like you need info on every Controller that has access, not just the initial Controller used to pair. Is that correct? If so, I can add a separate callback function used whenever a Controller is added or deleted and pass a read-only pointer (or constant iterator) to the underlying data.

One limitation to note is that HomeSpan cannot be powered by battery (well, maybe a car battery 😄) since it requires an always-on WiFi connection, which is incredibly power-hungry. I believe all commercial lock mechanisms connect to HomeKit via HAP's (low energy) Bluetooth Protocol, which I've not implemented in HomeSpan (it's completely different than the HAP WiFi protocol). Hopefully this limitation does not put a damper on the project.

@rednblkx
Copy link
Author

Thank you for your response.

Some comments on your feedback:

The changes you are making are only to extend HomeSpan's core functionality (such as additional Characteristics and types of Characteristics), rather than trying to add NFC and HomeKey code directly to HomeSpan. That is 100% the right approach - much appreciated.

I really never intended to integrate the whole thing with HomeSpan since it make no sense given the fact that they serve different purposes and it's also a lot of code to add for no reason just to burden the whole thing. NFC would've been nice i guess but that's only being used by HomeKey and to add it externally of HomeKit again wouldn't make sense.

At present, HomeSpan does not implement the WR permission or the TLV8 format for Characteristics.

It was a bummer at the start but after reading the code and the HAP spec i did notice that no characteristic makes use of the WR perm or TLV which immediately made sens why you would choose not to add it and you will notice that the WR permission is not mentioned in my code as i didn't knew where that could fit and didn't bother figuring it out since the whole implementation for write response is also not very fitting but it works which is what mattered to me at the time and it's the reason why i just chose to pass along the base64 value instead of decoding it and parsing the TLV from inside HomeSpan.

in HomeSpan 1.9.0 I refactored all the TLV8 code used for pairing so that it can be potentially used for Characteristics as well.

Really appreciate the work you did on the new TLV8 code, it makes it easier and safer to work with TLV data, still don't like that i have to define an array with names for each tag though i love the concept.

I think I get the gist and it seems that most of your changes are designed to force a multi-response to a write command. I believe there is an easier way to do this (instead of using a callback) that capitalizes on the existing multi-response mechanism used for writes when there is an error.

We can definitely integrate into setString with a check for WR permission and make use of the "r" field that's parsed here which denotes a response is expected so we know to give the value that was previously set with setString and i think this could just be a boolean on the SpanBuf object that can picked up by printfAttributes

For the pairCallback(), it looks like you need info on every Controller that has access, not just the initial Controller used to pair. Is that correct? If so, I can add a separate callback function used whenever a Controller is added or deleted and pass a read-only pointer (or constant iterator) to the underlying data.

That is right, in order for the whole to "work as expected", it needs to retrieve the public key of all controllers for two reasons:

  1. So it can calculate the Issuer ID which is used to identify the person(Apple ID) so later when a device provisions it's public key for HomeKey we know that "yeah, this person is on the list, add the device key under the issuer"
  2. HomeKey employs multiple authentications flow that are used based on which one failed and the last one ATTESTATION uses the Controller public key to verify if the data was indeed signed by the same device.

An overloaded pairCallback would be great. I understand that changing the signature would introduce a breaking change which i just added to call the pairCallback from addController, i wish could've done something like that for the callback 😅, it's basically drilling through entire functions right now 😂.


Regarding the battery limitation, yeah, i know i've mentioned the battery on my README but reality is that is not going to come too soon for the exact same reason and i also haven't found any HAP library that has BLE implemented except for Apple's HomeKitADK which i don't really wanna use, don't know if it even works with ESP-IDF which is probably why Espressif made their own version.

I'll see to make modifications to the code to incorporate what i've suggested above and perhaps try to integrate the TLV8 format a bit more.

@HomeSpan
Copy link
Owner

I'll add the WR and "r" for the write-response functionality and post an update to the dev branch in the next or so.

@rednblkx rednblkx changed the base branch from master to dev March 11, 2024 13:25
@rednblkx rednblkx changed the title NFC Lock Chars & Services and Callback Implementation NFC Lock Chars & Services and WR functionality Mar 11, 2024
@HomeSpan
Copy link
Owner

Update: I've added native support for the write-response mechanism. Code is available in the dev branch. As you can see from the latest commit, only a handful of lines needed to change (though it required a lot of thought and some surgical precision to keep things as generic as possible).

HomeSpan now looks for an "r":true JSON pair in any Home App update request and if found it creates the proper "write-response"(including in mixed cases where "r":true is only specified for some, but not all Characteristics that are to be updated). HomeSpan already includes support for the WR permission flag, but since the new code is generic enough to handle a write-reponse for any Characteristic, I don't check to see if the WR permission is set. Basically, if HomeSpan receives a write-response request for a Characteristic, it will process it.

However, I found a terrific use for the WR permission. If you add it to any Characteristic, it forces the Home App to always request write-responses for that Characteristic when calling for updates. This even includes standard HAP Characteristics, which made it possible for me to test the code. I created a simple Lightbulb Service with On() and Brightness() Characteristics, and then added WR to the Brightness() permissions. The Home App started to send "r":true every time it tried to change the Brightness(), allowing me to test a lot of edge cases to make sure everything continues to work.

For your NFC Characteristics, it may be that the Home App will send proper "r":true write-requests even without you setting the permissions to include WR, but I recommend you add the WR anyway to be safe. And I think I found another use for it (more on this below).

There is (almost) nothing special you need to do to change the value of Characteristic subject to a write-response request. You will see that Characteristic updated in the update() method of your NFC Service, and, similar to all Characteristics, you can update it with the setVal() method. HOWEVER, setVal() is not normally meant to be used within the update() method for Characteristics that are being updated simultaneously by the Home App since this would cause a conflict - you are supposed to either accept the update and process accordingly, or reject the update by returning false. But doing this for a Characteristic subject to a write-response is perfectly fine, since the Home App is expecting that you are going to change the value it sends to you to something else after you process the update. The only caveat is that if you set the value, with setVal() you need to tell HomeSpan to suppress sending out a notice that the value has been updated to any Controllers that have requested EV notifications. Most likely none will for your application, since the NFC TLV Characteristic probably doesn't support the EV permission, but just in case, when using setVal(), always set the second optional argument to false. This suppresses EV notifications (I added this a few years ago based on other use cases where notification suppression was needed).

In the next update I will add some additional logic that prevents using setVal() to update a Characteristic inside update() if it has indeed been updated by the Home App, UNLESS the permissions include WR. I'll also have it automatically suppress any EV notifications so you won't have to add false as a second argument.

I plan to also add some tweaks to the STRING type to better support TLV Characteristics, since there is a hardcoded 64-character limit on STRINGS (which would include base64 TLV data) that will likely be too limiting.

Give the dev branch a try and let me know how it goes.

@rednblkx
Copy link
Author

Fantastic!

I finally gave it a go and i think it doesn't like me anymore or something 😂.
It keeps crashing on boot on HomeSpan.h#L532 which i don't even see to be related to this update, based on the log it's crashing on characteristic HardwareFinish("AQQAAAAA") for which the value barely has 8 characters.

Did a clean flash, not really sure what is going on, the only change i did was to add the Services and Characteristics included in this PR.

This is the log, it's the same thing over and over

************************************************************
Welcome to HomeSpan!
Apple HomeKit for the Espressif ESP-32 WROOM and Arduino IDE
************************************************************

** Please ensure serial monitor is set to transmit <newlines>

Message Logs:     Level 0
Status LED:       Pin 2  (Auto Off=15 sec)
Device Control:   Pin 26
Sketch Version:   n/a
HomeSpan Version: 1.9.1
Arduino-ESP Ver.: 2.0.14
ESP-IDF Version:  4.4.6
ESP32 Chip:       ESP32-D0WD-V3 Rev 3 dual-core 4MB Flash
ESP32 Board:      esp32
PWM Resources:    16 channels, 8 timers, max 20-bit duty resolution
Sodium Version:   1.0.12-idf  Lib 9.4
MbedTLS Version:  mbed TLS 2.28.4
Sketch Compiled:  Mar 22 2024 00:16:00
Partition:        app0I (2154) system_api: Base MAC address is not set
I (2154) system_api: read default base MAC address from EFUSE

MAC Address:      08:D1:F9:26:25:D8

Device Name:      HK Lock

I (2168) gpio: GPIO[5]| InputEn: 1| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 
I (2172) gpio: GPIO[18]| InputEn: 1| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 
I (2181) gpio: GPIO[19]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 
I (2190) gpio: GPIO[23]| InputEn: 1| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 
I (2207) NFC_SETUP: Found chip PN532
I (2208) NFC_SETUP: Firmware ver. 1.6
I (2220) NFC_SETUP: Waiting for an ISO14443A card
I (2221) LockMechanism: LockMechanism > Configuring LockMechanism
I (2224) NFCAccess: NFCAccess > Configuring NFCAccess

*** HomeSpan Info ***

➤ Accessory:  AID=1
   ➟ Service AccessoryInformation:  IID=1, UUID="3E"
      ⇨ Characteristic Identify(1):  IID=2, UUID="14", Perms=PW
      ⇨ Characteristic Manufacturer("Kodeative"):  IID=3, UUID="20", Perms=PR
      ⇨ Characteristic Model("HomeKey-ESP32"):  IID=4, UUID="21", Perms=PR
      ⇨ Characteristic Name("HK Lock"):  IID=5, UUID="23", Perms=PR
      ⇨ Characteristic SerialNumber("HK-360"):  IID=6, UUID="30", Perms=PR
      ⇨ Characteristic FirmwareRevision("0.1"):  IID=7, UUID="52", Perms=PR+EV, EV=()
      ⇨ Characteristic HardwareFinish("AQQAAAAA"):  IID=8, UUID="26C", Perms=PRGuru Meditation Error: Core  1 panic'ed (LoadProhibited). Exception was unhandled.

Core  1 register dump:
PC      : 0x400014fd  PS      : 0x00060c30  A0      : 0x801bcf38  A1      : 0x3ffc1fe0  
A2      : 0x00000000  A3      : 0xfffffffc  A4      : 0x000000ff  A5      : 0x0000ff00  
A6      : 0x00ff0000  A7      : 0xff000000  A8      : 0x00000000  A9      : 0x3ffc2290  
A10     : 0x00000001  A11     : 0x3ffbde40  A12     : 0x00000000  A13     : 0x00000000  
A14     : 0x00000000  A15     : 0x00000000  SAR     : 0x00000014  EXCCAUSE: 0x0000001c  
EXCVADDR: 0x00000000  LBEG    : 0x400014fd  LEND    : 0x4000150d  LCOUNT  : 0xffffffff  


Backtrace: 0x400014fa:0x3ffc1fe0 0x401bcf35:0x3ffc1ff0 0x401b9bc5:0x3ffc2300 0x4010ce89:0x3ffc23c0 0x4010e61d:0x3ffc2690 0x4010ed5b:0x3ffc2770 0x400ecc6a:0x3ffc2790 0x4012fdc4:0x3ffc27b0

  #0  0x400014fa:0x3ffc1fe0 in ?? ??:0
  #1  0x401bcf35:0x3ffc1ff0 in _svfprintf_r at /builds/idf/crosstool-NG/.build/xtensa-esp32-elf/src/newlib/newlib/libc/stdio/vfprintf.c:1528
  #2  0x401b9bc5:0x3ffc2300 in sprintf at /builds/idf/crosstool-NG/.build/xtensa-esp32-elf/src/newlib/newlib/libc/stdio/sprintf.c:618
  #3  0x4010ce89:0x3ffc23c0 in SpanCharacteristic::uvPrint(SpanCharacteristic::UVal&) at .pio/libdeps/release/HomeSpan/src/HomeSpan.h:533
      (inlined by) Span::processSerialCommand(char const*) at .pio/libdeps/release/HomeSpan/src/HomeSpan.cpp:905
  #4  0x4010e61d:0x3ffc2690 in Span::pollTask() at .pio/libdeps/release/HomeSpan/src/HomeSpan.cpp:197
  #5  0x4010ed5b:0x3ffc2770 in Span::poll() at .pio/libdeps/release/HomeSpan/src/HomeSpan.cpp:183
  #6  0x400ecc6a:0x3ffc2790 in loop() at src/main.cpp:779
  #7  0x4012fdc4:0x3ffc27b0 in loopTask(void*) at /home/red/.platformio/packages/framework-arduinoespressif32/cores/esp32/main.cpp:50

I did also tried to create a new project, ran SimpleLightBulb and then just added the HardwareFinish Characteristic in order to isolate HomeSpan and make sure i did not messed something up but still bootlooped.

Any idea what might be the issue?

@HomeSpan
Copy link
Owner

Sorry, subsequent to my note above I started working on native support for TLV and inadvertently pushed a work-in-progress to GitHub. For testing just the WR stuff, please use commit 41f7550.

I will be working on the rest of the TLV stuff over the next week or so.

HomeSpan and others added 19 commits March 24, 2024 22:03
Though this increases space requirement for TLV8 (just a little), using std::list allows adding elements directly to end, which ensure the order is maintained when packing and unpacking.  Will avoid potential confusion when TLV8 is used for Characteristics.
Also truncated string Characteristics printed via 'i' CLI Command to show only first 32 characters followed by "..." if there are more than 32.
Works, but is memory-inefficient since it decodes entire string before unpacking.  Need to add new functionality to TLV8 so that unpacking can be done in chunks similar to how pack() works.

Also need to create getNewTLV() OR make all get/getNew generic to reduce code size.
This is much more memory efficient.  Instead of decoding entire STRING from base64 to a temporary buffer or potentially very large size and then unpacking into TLV object, we decode a maximum of 48 characters at a time and unpack the resulting 36 (max) bytes until entire STRING is consumed.

getTLV() returns pack_size of final TLV, unless there is a problem with conversion, in which cae TLV is wiped and return value=0
avoids unnecessary duplication of code where the only difference is whether you need the value or the newValue of a Characteristic.
Though this simplifies the code, the code size is still the same - compiler must have already optimized these functions.
Apple "escapes" forward slashes in JSON output, replacing '/' with '\/', which destroys base64 strings.
Allows easy-add of a sub TLV to an existing TLV8
Also fixed bug using increment operator (++) by replacing with std::next()
and a watchdog trigger prevention hack
@rednblkx
Copy link
Author

Hello!

It's me again, sorry for the delay, i see you have created a new branch tlvwork on top of latest dev branch commit so thought i would give that one a go since it includes the tlv format for characteristics.

So far it seems to be working as intended, also checked the ios logs and , had an issue with setString but only because i was mistakenly passing something undefined 😅, thank you very much for your work.

i rebased the branch to the tlvwork with my modification for the homekey characteristics on top, hope that doesn't complicate things.

Now remains to also implement something for the paircallback, tried to do something a couple weeks but it got me into circular dependency hell so i left it for another time, do you have anything in mind for this?

Again, thank you for the work you've done.

@HomeSpan
Copy link
Owner

I still have a little more work to do on the TLV stuff before I merge into the dev branch, including a dedicated page that documents all the TLV functions. I'll add a function to accomplish pairCallback at that time as well.

Once completed you should not have to make any modifications to the library in order to implement a HomeKey. Should be able to everything from within a sketch, including adding new Services and Characteristics using the new CUSTOM_CHAR_TLV() macro.

@rednblkx
Copy link
Author

Ok, great, feel free to close this one, should’ve opened an issue instead but no point to it now.

@HomeSpan
Copy link
Owner

TLV8 work is now complete and documented. See the new TLV8.md page for details. You should now be able to add HomeKey-specific Services and Characteristics by using the CUSTOM_SERV() and CUSTOM_CHAR_TLV8() macros, as well as create TLV8 objects to use with those Characteristics.

I've also exposed the full list of Controller data through the use of iterators. homeSpan.controllerListBegin() and homeSpan.controllerListEnd() return constant iterators pointing to the first element in the Controller List and the end() of the Controller List, respectively. This allows you to easily loop over all Controllers whenever needed. To access the data itself, Controller has three new methods: boolean isAdmin(), const uint8_t *getID() and const uint8_t *getLTPK().

For example, the following function can be added to your sketch to loop over all Controller List data and print out the values:

void list_controllers(){

  Serial.printf("\nControllers\n");

  for(auto it=homeSpan.controllerListBegin(); it!=homeSpan.controllerListEnd(); ++it){
    Serial.printf("Admin=%d  ID=",it->isAdmin());

    for(int i=0;i<36;i++)  // note length of ID is always 36 bytes
      Serial.printf("%02X",it->getID()[i]);

    Serial.printf("  LTPK=");
    for(int i=0;i<32;i++)  // note length of LTPK is always 32 bytes
      Serial.printf("%02X",it->getLTPK()[i]);

    Serial.printf("\n");
  }
}

I also added a homeSpan.setControllerCallback(), which is called whenever the Controller List is updated (through an add, remove, or change). For example, you can have HomeSpan call the above code whenever the Controller List is updated by setting homeSpan.setControllerCallback(list_controllers);

Code is available in the tlvwork branch. Will move to dev branch after I finish documenting the Controller List stuff.

Let me know how it works. If I've understood everything correctly, you should NOT have to make any modifications to the HomeSpan library itself to add HomeKey functionality to your sketch.

If everything works and is reasonably robust, I'll add the HomeKey Services and Characteristics to the library as well so they can be used by everyone.

@rednblkx
Copy link
Author

Hello!

I have now integrated the new APIs and i'll say that is working without issues so far, i've been running it for some days whilst i was on working on same other features and it's great also i see you've merged into dev so i've been using that.

Haven't used the new TLV8 APIs much but i appreciate that i can now set TLV data on a characteristic and let homespan encode it to base64 and everything else.

Don't really have much to say except thank you for the stellar work you've done.

Though i have some feedback for the TLV8 Serializer, i like the structure and i now use it(partially) in my HomeKey library but there is one issue, during the Attestation flow a huge data package of 719 bytes gets received in BER-TLV format, obviously you can't encode that length in a single byte and so three bytes are used for length as indicated by the BER encoding. Now, i know it might not make sense to modify the unpacking logic for this since TLV8 is it's own thing, just thought i might share this.

@HomeKidd
Copy link

@HomeSpan Is there any schedule for the public release of the dev branch? 😄

@HomeSpan
Copy link
Owner

Not yet - there is a memory leak I am trying to resolve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants